Commit graph

4 commits

Author SHA1 Message Date
Claude
c4f1d33987
fix(linkedin): judge every occurrence on its own merits, band a flat centre honestly
Three findings from the fourth review round on this PR.

1. The policy gate de-duplicated matches by snippet TEXT before deciding whether
   an exemption applied, so the second use of a generic word inherited the first
   one's fate. "automate posting via linkedin's native scheduler, and also
   automate direct messaging to my whole network" excused the first "automate"
   (native scheduling is endorsed), then skipped the second as a duplicate - so
   the mass-DM half was never evaluated and the request returned ALLOW. Each
   occurrence is now judged at its own position; de-duplication is for the
   display lists only. Verified: this case and two variants move ALLOW -> REFUSE
   on P1-AUTOMATION with the same word in both `matched` and `exempted`, which is
   the fix itself firing rather than another rule covering for it.

2. The analyzer treated iqr == 0 as "nothing is an outlier" and labelled every
   post TYPICAL. A flat middle 50% is a realistic export shape - many similar
   posts plus a couple of viral ones - so that hid genuine breakouts, trading one
   honesty problem for another. A degenerate centre now ranks against the median:
   equal is TYPICAL, above is BREAKOUT, below is WEAK. All-identical input still
   returns 12 TYPICAL; flat centre plus two viral posts returns 10 TYPICAL and
   2 BREAKOUT.

3. P5's group pattern listed "communities" twice; the second alternative is now
   "forums?", which the rule could not previously match.

Verified: 18-case adversarial battery (12 refuse, 6 allow) all correct, with
refusals attributed to the specific rule rather than assumed; pre-fix control
confirms 3 of the 4 automation cases returned ALLOW before this change. Blocking
gates green - compileall, check_paths, check_frontmatter, check_dual_publish,
check_model_freshness, check_skill_names, check_plugin_json --all,
derive_counters --check, smoke_scripts 696/696. Pinned sample outputs unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BswsZp5zrJWFAGU6KWNA1s
2026-08-26 14:52:39 +00:00
Claude
81c2c81296
fix(linkedin): scope exemption signals to their sentence, close the spam hole they opened
The third review on PR #997 found that the exemption mechanism I added reopened a
refusal this gate previously made. Reproduced against origin/dev:

  "I want to auto-post daily to 50 LinkedIn groups without permission, promoting my
   course. LinkedIn's native scheduler is neat, right?"

     pre-PR:  exit 4 (REFUSE)     with my exemption:  exit 0 (ALLOW)

_exemption_for searched `signal` across the entire input, so an endorsement in one
sentence retroactively excused an unrelated match in another. Turning a refusal into
an allow is the one direction this gate must never move by accident, and my change
did it.

`signal` is now matched only within the sentence containing the match. The trade is
stated in the docstring: a legitimate two-sentence phrasing ("I use LinkedIn's
native scheduler. I auto-post weekly.") now refuses and the user can rephrase, while
the alternative cost was a false ALLOW the user cannot detect at all.

An adversarial battery then found a hole the review's example did not cover: with
the endorsement in the SAME sentence, "auto-post daily to 50 groups without
permission using LinkedIn's native scheduler" was still allowed. The exemption is
right — native scheduling is the supported path P7 names — so the real gap was that
no rule covered unsolicited bulk POSTING. Pre-PR the gate refused that text only
incidentally, by refusing every automation word. P5 is broadened from messaging to
messaging-or-posting, with patterns for posting to double-digit groups, spamming
groups or feeds, and doing either without permission, so it now refuses for the
actual reason.

That rule also closes a gap the gate always had: "schedule my posts to 40
communities without permission" contains no automation word and was ALLOWED pre-PR
(exit 0). It now refuses. Small-scale group posting ("I post to 3 groups I actually
belong to") still allows, so the rule did not become a blunt instrument.

Also from the review: _read_input caught OSError but UnicodeDecodeError is a
ValueError subclass, so an existing file that is not valid UTF-8 still escaped as a
traceback — the same failure mode the helper exists to prevent. Fixed in all 11
scripts; a non-UTF-8 file now exits 2 with a message.

Twelve-case adversarial battery, all correct: both spam variants refuse, all three
aside-based bypasses refuse, all three legitimate exemption uses allow, small-scale
group posting allows, and both controls are unchanged. Other tools unaffected:
headline 93/SHIP, profile 37/WEAK, analyzer ANALYSED 6/3/3, cadence
NO_POSTS_AFFORDABLE/3. Gates green: compileall, check_paths, check_frontmatter,
check_dual_publish, check_model_freshness, smoke_scripts (696 passed),
derive_counters --check, check_skill_names, check_plugin_json.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BswsZp5zrJWFAGU6KWNA1s
2026-08-26 14:41:32 +00:00
Claude
4e771557a5
fix(linkedin): repair the policy gate's self-contradictions and six related defects
Addresses the marketing/linkedin findings raised during PR #994's review, which were
deliberately left out of that PR because the plugin was already-merged content riding
along in the diff. Each was reproduced before being fixed, and each fix verified in
both directions.

Two rules refused the exact action their own substitute recommends:

  export my connections list as a csv          was REFUSE -> now ALLOW
  LinkedIn's native scheduler to auto-post     was REFUSE -> now ALLOW

P2-SCRAPING told the user to run LinkedIn's own data export while refusing the
phrase for it; P1-AUTOMATION refused native scheduling that P7's substitute names
as the supported path. Rather than loosening the patterns, _scan now supports
per-rule exemptions that drop a single matched snippet and never the whole rule, so
a sentence mixing an endorsed action with a prohibited one still refuses. Verified:
"native scheduler to auto-post AND auto-connect with 500 recruiters" still exits 4,
as does "export my connections list AND scrape their emails", and a leads database
is still not the self-export. The exemption applied is reported in the output rather
than silently swallowed.

P3's pod lookahead could only match noun-before-verb ("pods to join"), so the common
"join a pod" was structurally unmatchable. Added the verb-first form; both phrasings
now refuse.

outreach_message_builder.py only checked the --ask field for a premature ask, so the
same ask moved into --reason or --specific-line passed clean, making the documented
"refuses an ask in a first-touch note" guarantee bypassable. The note body is now
scanned with a meeting-request pattern rather than the loose ASK_RE, which would
have flagged "your post on on-call rotations" — verified that innocent phrasing
still passes while both hiding places now block.

post_performance_analyzer.py labelled every post BREAKOUT on zero-dispersion data:
with identical rates the IQR is 0, every fence collapses onto the median, and the
>= hi_fence test fires for all of them. Twelve identical posts went from 12 BREAKOUT
to 12 TYPICAL; varied data is unchanged.

Two profile tools credited prose as signal. The auditor counted bare "to"/"from"/"x"
substrings as outcomes, so "Reported to the VP of Engineering" scored as
outcome-carrying; multipliers and from/to deltas are now shapes requiring a number.
headline_scorer counted bare "for "/"to "/"from "/"into ", so "excited to share my
thoughts from the conference" scored both audience and outcome. Removing them alone
cost a real signal — the sample's "Head of Data for Series A/B SaaS" names an
audience through the construction — so the directed-at construction is restored as a
shape that must land on an actual audience noun. Sample score is unchanged at 93 and
the weak sample at 19, while the prose false positives are gone.

All eleven scripts crashed with a FileNotFoundError traceback and exit 1 on a
mistyped --input; each now exits 2 with a message. The two analytics scripts died
with AttributeError on a bare list of scalars; both now exit 4 naming the problem.
Note that catching json.JSONDecodeError does not catch ValueError - the subclass
relation runs the other way - so the analyzer's except clause was widened rather
than left to trade one traceback for another.

cadence_planner reported FITS with exit 0 while handing back a week containing zero
posts; that now returns NO_POSTS_AFFORDABLE with exit 3 and a blocking finding, with
the docstring's exit table updated. Normal budgets are unaffected, the below-floor
path still exits 2, and --sample still exits 3 for its own pre-existing reason.

pattern_miner's multiple-comparisons note now states plainly that it is an accounting
of expected false positives and not an applied Bonferroni or BH correction, so the
number cannot be read as a stronger guarantee than it is.

Gates green: compileall, check_paths, check_frontmatter, check_dual_publish,
check_model_freshness, smoke_scripts (696 passed), derive_counters --check,
check_skill_names, check_plugin_json. The advisory JSON-output gate still reports its
8 pre-existing agent-launcher failures; none is in this plugin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BswsZp5zrJWFAGU6KWNA1s
2026-08-26 14:08:56 +00:00
Claude
645c523be1
feat(marketing): add linkedin plugin — organic presence with platform rules in code
Answers discussion #934, which asked for a strategic assistant for growing a
LinkedIn presence organically rather than a post generator.

Six skills under marketing/linkedin/: an orchestrator (context: fork) plus
profile, strategy, content, engagement, and analytics lanes. 17 stdlib-only
tools, 15 references, 2 agents, 8 /cs:* commands.

The design constraint is the differentiator: no LinkedIn credentials, no API
calls, no scraping, nothing auto-sent. Automated posting, connecting, and
commenting are prohibited by LinkedIn's User Agreement 8.2, and a restricted
account ends a compounding asset. linkedin_policy_gate.py runs before any
drafting and refuses seven request classes — automation, scraping, engagement
pods, bulk messaging, fake identity, fabricated proof, named third-party
automation platforms — each carrying the policy anchor and a compliant
substitute, so the gate never just says no.

Refusals are real rather than advisory. A cadence under 90 minutes a week
returns a comment-only plan instead of a schedule that dies in week five. A
newsletter whose six-month cost exceeds the budget is refused before the promise
is made. An experiment needing more posts than a quarter allows is reported
infeasible rather than quietly re-sized. The pattern miner refuses to test
anything below 10 posts and reports NOTHING_SURVIVED as a finding.

Evidence discipline: two widely repeated claims are corrected rather than
propagated. The "personalised note triples acceptance" claim is not supported by
the largest samples (acceptance is near-identical either way, ~26.4%); what a
note moves is the post-accept reply rate (~5.4% to ~9.4%), which is why the
message builder refuses an ask in a first-touch note. The ~19% in-body link
reach reduction has never been confirmed by LinkedIn as a penalty and has a
plausible dwell-time explanation, so it is a warning rather than a block. Every
reference carries per-claim confidence levels.

Accessibility is a blocking lint finding: Unicode pseudo-bold is announced by
screen readers as mathematical symbols and is not indexed by search.

All six SKILL.md files are 6/6 PASS on the write-a-skill checklist. Every tool
supports --help, --sample, and --output json with typed exit codes.

Counters: skills 380 -> 386; plugins 96 -> 97; tools 706 -> 723; refs 823 -> 838;
agents 114 -> 116; commands 138 -> 146 (derive_counters.py --check).

Also syncs three previously-merged skills (agent-memory, hivemind, skill-doctor)
into the .hermes/ and .vibe/ mirror trees, which had drifted behind .codex/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JSPxUHU6utqme7qC6EwHEh
2026-08-25 07:32:30 +00:00