skilldrop-cli 0.11.2 → 0.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,16 +3,16 @@
3
3
  [![npm](https://img.shields.io/npm/v/skilldrop-cli)](https://www.npmjs.com/package/skilldrop-cli)
4
4
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
5
 
6
- **[Browse the catalogue →](https://sananthanarayan.github.io/skilldrop/)** — all 56 skills, filterable by pack, tag, and model tier.
6
+ **[Browse the catalogue →](https://sananthanarayan.github.io/skilldrop/)** — all 57 skills, filterable by outcome, pack, tag, and model tier.
7
7
 
8
- **56 portable AI-agent skills for the deliverables you actually ship** — diagrams, design docs, ADRs, PRDs, runbooks, decks, decision logs, threat models, adversarial reviews — installable into **Claude Code**, **Cursor**, **Kiro**, **Codex**, **GitHub Copilot**, and any AI tool that accepts custom instructions, one skill or one role-based pack at a time:
8
+ **57 portable AI-agent skills for the deliverables knowledge workers actually ship** — diagrams, design docs, ADRs, PRDs, runbooks, decks, decision logs, threat models, adversarial reviews — installable into **Claude Code**, **Cursor**, **Kiro**, **Codex**, **GitHub Copilot**, and any AI tool that accepts custom instructions, one skill or one role-based pack at a time:
9
9
 
10
10
  ```bash
11
11
  npx skilldrop-cli install --pack product-manager # or: solution-architect, dev-team,
12
12
  npx skilldrop-cli list # sre-oncall, stakeholder-comms, ai-engineering
13
13
  ```
14
14
 
15
- Originally scoped to solution architects, now broadly useful to PMs, founders, consultants, engineering leaders, exec assistants — anyone who turns ideas into stakeholder-ready artifacts.
15
+ Originally scoped to solution architects, now broadly useful to PMs, founders, consultants, engineering leaders, exec assistants — anyone who turns ideas into stakeholder-ready artifacts.
16
16
 
17
17
  Every skill is a plain `SKILL.md` folder — the [**Agent Skills open standard**](https://agentskills.io) Anthropic ratified — so the same folder runs unchanged in Claude.ai, Claude Code, the Claude Agent SDK, Cursor, Kiro, Codex, Copilot, Antigravity, and anything else that reads the format. Portable by construction: no runtime, no platform, no lock-in.
18
18
 
@@ -77,6 +77,7 @@ flowchart LR
77
77
  |---|---|
78
78
  | [`brief-intake`](skills/brief-intake/SKILL.md) | Upstream collector. Takes raw mess — a Slack thread, meeting transcript, ticket, email chain, paragraph of notes — and emits a structured brief shaped for whichever downstream skill comes next (ADR, design doc, runbook, exec summary, deck, comparison matrix, decision log). Every field is tagged `[explicit] / [implied] / [inferred] / [missing]` with verbatim quotes from the source. |
79
79
  | [`doc-critique`](skills/doc-critique/SKILL.md) | Counterpart reviewer. Takes an existing doc (ADR, design doc, runbook, exec summary, comparison matrix, deck, decision log) and produces a structured critique against the same rubrics the generators enforce — verdict + severity-tagged findings (blocker / major / minor / nit) with quoted evidence and concrete fixes, plus a "what's working" section. |
80
+ | [`output-hygiene`](skills/output-hygiene/SKILL.md) | Pre-ship surface pass. Runs a stdlib-only script over agent-written text to find what the machine put there — invisible Unicode, non-breaking spaces, homoglyphs, harness-added provenance trailers, trailing chat closers — reporting each with its codepoint and line, then sweeps the prose for the tells a script cannot see and quotes every one with a rewrite. Separates what is safe to auto-apply from what needs a decision, and holds the provenance class where an academic, employer, or contribution policy requires disclosure. |
80
81
 
81
82
  ### Planning & delivery
82
83
 
@@ -235,12 +236,12 @@ Categories (above) say what a skill *is*; packs say *who needs it*. [`packs.json
235
236
 
236
237
  | Pack | Skills | For |
237
238
  |---|---|---|
238
- | `solution-architect` | 15 | Design-phase artifacts: diagrams, ADRs, design docs, contracts, schemas, threat models, and the reviews that gate them |
239
- | `product-manager` | 12 | Direction to requirements: PR/FAQs, strategy frameworks, OKRs, PRDs, journey maps, story splitting, success measurement |
240
- | `dev-team` | 11 | Build-and-ship loop: implementation with adversarial review, test plans, triage, migrations, release notes, quality gates |
241
- | `stakeholder-comms` | 8 | Non-technical audiences: audience profiling, deck outlines and real `.pptx` decks, exec summaries, decision logs, guides |
239
+ | `solution-architect` | 17 | Design-phase artifacts: diagrams, ADRs, design docs, contracts, schemas, threat models, and the reviews that gate them |
240
+ | `dev-team` | 14 | Build-and-ship loop: implementation with adversarial review, test plans, triage, migrations, release notes, quality gates, pre-ship hygiene |
241
+ | `product-manager` | 13 | Direction to requirements: PR/FAQs, strategy frameworks, OKRs, PRDs, journey maps, story splitting, success measurement |
242
+ | `ai-engineering` | 12 | Build and run AI systems: agent loop design, subagent orchestration, spend budgets, eval harnesses, usage reporting, data contracts |
243
+ | `stakeholder-comms` | 9 | Non-technical audiences: audience profiling, deck outlines and real `.pptx` decks, exec summaries, decision logs, guides |
242
244
  | `sre-oncall` | 5 | Operate the service: runbooks, observability design, incident comms, postmortems, capacity/cost models |
243
- | `ai-engineering` | 6 | Build and run AI systems: agent loop design, subagent orchestration, spend budgets, eval harnesses, usage reporting, data contracts |
244
245
 
245
246
  ```bash
246
247
  python3 pack.py # list packs
@@ -252,6 +253,26 @@ python3 pack.py product-manager --install --dest .cursor/skills # any dir, for
252
253
 
253
254
  For non-Claude IDEs, `--dest` drops the folders where your tool expects them; the per-IDE wiring steps below still apply. Packs are metadata only — skills stay in flat `skills/<name>` folders, so per-skill `cp -R` installs keep working unchanged ([RFC-0001](docs/rfcs/0001-skill-packs.md) records the design decision).
254
255
 
256
+ ## Outcomes — browse by why you're here
257
+
258
+ Packs answer *who am I*. Outcomes answer *why did I open this*. The seven below are the nine
259
+ categories above, restated as the things people actually arrive wanting to do, and they live in
260
+ the `outcomes` block of [`packs.json`](packs.json) so the [catalogue site](https://sananthanarayan.github.io/skilldrop/)
261
+ can filter on them. Every skill belongs to at least one; `validate.py` enforces it.
262
+
263
+ | Outcome | Skills | What it covers |
264
+ |---|---|---|
265
+ | `design-the-system` | 15 | Decision records, design docs, diagrams, contracts, schemas, migration and threat models, agentic design |
266
+ | `decide-what-to-build` | 11 | Press releases, strategy frameworks, OKRs, business cases, requirements, PRDs, success metrics |
267
+ | `build-and-review-software` | 11 | Supervised implementation, review panels, test plans, triage, accessibility and static-analysis passes |
268
+ | `explain-it-to-decision-makers` | 6 | Audience profiling, deck outlines and real decks, exec summaries, decision logs, guides |
269
+ | `govern-ai-use` | 6 | Readiness, use-case triage, rollout, acceptable-use policy, adoption staging, usage reporting |
270
+ | `run-and-recover-the-service` | 5 | Runbooks, observability design, release notes, incident comms, postmortems |
271
+ | `get-a-draft-ready-to-ship` | 3 | Collect a messy input, critique the argument, strip the machine artifacts |
272
+
273
+ Outcomes are a browse aid, not an install unit — there is no `--outcome` flag. Install by skill
274
+ or by pack.
275
+
255
276
  ## Installing a skill into your IDE
256
277
 
257
278
  ### Quickest: the skilldrop CLI
@@ -527,8 +548,9 @@ Full contributor guide — the three lanes, the PR gates, and the release flow
527
548
  4. If your skill needs scripts, drop them in `scripts/` and reference them with a path relative to the skill folder — **avoid hard-coding `${CLAUDE_SKILL_DIR}` only**; show both paths so non–Claude-Code users aren't stuck.
528
549
  5. Add an `evals/` folder: `evals.json` (at least one realistic prompt with a list of assertions the output must satisfy) and `eval_queries.json` (phrases that should and should **not** trigger the skill). These double as the checklist for the manual test pass and keep the `description` honest about when the skill fires.
529
550
  6. Add an entry to the **Skills in this repo** table above and to the **Installing dependencies** table.
530
- 7. Add the skill to at least one pack in `packs.json`.
531
- 8. Run `python3 validate.py` from the repo root — it checks name consistency, the tier sync with `model-routing.json`, the `related`↔SKILL.md reference sync, description sync, pack membership, and eval file shape.
551
+ 7. Add the skill to at least one pack **and** at least one outcome in `packs.json`.
552
+ 8. If the change is going out in a release, add a bullet to [`CHANGELOG.md`](CHANGELOG.md) under the new version — the site build fails without one.
553
+ 9. Run `python3 validate.py` from the repo root — it checks name consistency, the tier sync with `model-routing.json`, the `related`↔SKILL.md reference sync, description sync, pack and outcome membership, and eval file shape.
532
554
 
533
555
  ## License
534
556
 
package/agents/README.md CHANGED
@@ -24,7 +24,7 @@ npx skilldrop-cli install --agent devils-advocate --ide kiro # .kiro/agents/*.
24
24
  | [`code-quality`](code-quality.md) | Craft — naming, structure, duplication, needless complexity, readability | "Will the next engineer hate this?" |
25
25
  | [`security-reviewer`](security-reviewer.md) | Security — authz, injection, secret exposure, SSRF, unsafe deserialization, weak crypto, risky deps | "What can an attacker do with this?" |
26
26
 
27
- They are deliberately **split, not merged.** A single "review my code" agent dilutes the first three — bug-hunting, craft, and exploitability pull in different directions. Run them as separate passes; together those three are the **code reviewer panel** that [`feature-implement-loop`](../skills/feature-implement-loop/SKILL.md) and [`pre-merge-review`](../skills/pre-merge-review/SKILL.md) drive after each generation round. `devils-advocate` here is the subagent form of the [`devils-advocate` skill](../skills/devils-advocate/SKILL.md); same persona, packaged for a tool's native agent slot instead of on-demand skill invocation.
27
+ They are deliberately **split, not merged.** A single "review my code" agent dilutes all three — bug-hunting, craft, and exploitability pull in different directions. Run them as separate passes; together they are the reviewer panel [`feature-implement-loop`](../skills/feature-implement-loop/SKILL.md) drives after each generation round. `devils-advocate` here is the subagent form of the [`devils-advocate` skill](../skills/devils-advocate/SKILL.md); same persona, packaged for a tool's native agent slot instead of on-demand skill invocation.
28
28
 
29
29
  ## Fire them as a panel
30
30
 
package/bin/skilldrop.js CHANGED
@@ -204,6 +204,16 @@ function skillDir(cat, s) {
204
204
  if (cat.shape === "apm") return cat.skillIndex[s] && cat.skillIndex[s].dir;
205
205
  return path.join(cat.skillsDir, s);
206
206
  }
207
+ // Skill names reach RegExp when unwiring hooks. skillExists() proves a directory exists; it
208
+ // does not prove the name is regex-safe, and a third-party catalog (RFC-0003) may name a
209
+ // skill anything a filesystem accepts. Escape before interpolating.
210
+ // Read a file if it is there. Checking existsSync first then reading is a race: the file can
211
+ // vanish in between and the read throws. Handle the absence, do not predict it.
212
+ function readIfPresent(p, absent = "") {
213
+ try { return fs.readFileSync(p, "utf8"); }
214
+ catch (e) { if (e.code === "ENOENT") return absent; throw e; }
215
+ }
216
+ function reEscape(s) { return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
207
217
  function skillExists(cat, s) { const d = skillDir(cat, s); return !!d && fs.existsSync(d); }
208
218
  function manifestOf(cat, s) {
209
219
  if (cat.shape === "apm") {
@@ -591,7 +601,7 @@ function gitRoot(startDir) {
591
601
  function writeGitPreCommitHook(root, skill, hook) {
592
602
  const p = path.join(root, ".git", "hooks", "pre-commit");
593
603
  const marker = `skilldrop-hook:${skill}:pre-commit-review`;
594
- let body = fs.existsSync(p) ? fs.readFileSync(p, "utf8") : "";
604
+ let body = readIfPresent(p); // read-and-handle, not check-then-read
595
605
  if (!body.startsWith("#!")) body = "#!/bin/sh\n" + body;
596
606
  if (body.includes(marker)) return p; // already wired
597
607
  const line = `echo "skilldrop: run /${hook.action} on your staged changes before committing — ${hook.description}"`;
@@ -606,8 +616,9 @@ function writeGitPreCommitHook(root, skill, hook) {
606
616
  // or null if the file exists but is malformed (we never clobber unparseable JSON).
607
617
  function writeClaudeSessionHook(settingsPath, skill, hook) {
608
618
  let data = {};
609
- if (fs.existsSync(settingsPath)) {
610
- try { data = JSON.parse(fs.readFileSync(settingsPath, "utf8")); }
619
+ const raw = readIfPresent(settingsPath, null);
620
+ if (raw !== null) {
621
+ try { data = JSON.parse(raw); }
611
622
  catch (e) { return null; }
612
623
  }
613
624
  data.hooks = data.hooks || {};
@@ -653,18 +664,20 @@ function removeHooksFor(skill, dest, ide) {
653
664
  const root = gitRoot(process.cwd());
654
665
  if (root) {
655
666
  const p = path.join(root, ".git", "hooks", "pre-commit");
656
- if (fs.existsSync(p)) {
657
- const body = fs.readFileSync(p, "utf8");
658
- const re = new RegExp(`\\n?# >>> skilldrop-hook:${skill}:[^\\n]* >>>[\\s\\S]*?# <<< skilldrop-hook:${skill}:[^\\n]* <<<\\n?`, "g");
667
+ const body = readIfPresent(p, null);
668
+ if (body !== null) {
669
+ const q = reEscape(skill);
670
+ const re = new RegExp(`\\n?# >>> skilldrop-hook:${q}:[^\\n]* >>>[\\s\\S]*?# <<< skilldrop-hook:${q}:[^\\n]* <<<\\n?`, "g");
659
671
  const next = body.replace(re, "\n");
660
672
  if (next !== body) fs.writeFileSync(p, next);
661
673
  }
662
674
  }
663
675
  if (ide === "claude") {
664
676
  const sp = path.join(path.dirname(dest), "settings.json");
665
- if (fs.existsSync(sp)) {
677
+ const raw = readIfPresent(sp, null);
678
+ if (raw !== null) {
666
679
  try {
667
- const data = JSON.parse(fs.readFileSync(sp, "utf8"));
680
+ const data = JSON.parse(raw);
668
681
  const arr = data.hooks && data.hooks.SessionStart;
669
682
  if (Array.isArray(arr)) {
670
683
  const kept = arr.filter((e) => !JSON.stringify(e).includes(`skilldrop-hook:${skill}:`));
@@ -98,229 +98,62 @@
98
98
  }
99
99
  ],
100
100
  "skills": {
101
- "accessibility-audit": {
102
- "tier": "heavy",
103
- "rationale": "Adversarial review against WCAG success criteria — cheaper models fault alt-text presence over quality, under-call severity, miss focus-order/name-role-value issues, and claim AT passes from screenshots. Never downgrade."
104
- },
105
- "adr-generator": {
106
- "tier": "standard",
107
- "rationale": "Structures a decision into MADR/Nygard. Some judgment on title/consequences; not adversarial."
108
- },
109
- "agent-adoption-stage": {
110
- "tier": "standard",
111
- "rationale": "Places a team on a fixed 5-stage ladder from observables and routes one unlock. Bounded diagnosis against a rubric; escalate via ambiguous-input when the observables are contradictory or the distribution is heavily skewed."
112
- },
113
- "agent-budget": {
114
- "tier": "standard",
115
- "rationale": "Quantitative spec synthesis against a fixed tier vocabulary and degradation ladder. Cap-setting judgment is bounded by the 3-5x rule; escalate via ambiguous-input when the workflow itself is undefined."
116
- },
117
- "agent-loop-design": {
118
- "tier": "standard",
119
- "rationale": "Design synthesis against a fixed state-machine pattern and exit-criteria catalog. The gate-placement judgment is bounded by the blast-radius rule; escalate via ambiguous-input when the task itself is contested."
120
- },
121
- "agent-threat-model": {
122
- "tier": "heavy",
123
- "rationale": "Adversarial reasoning over a capability graph — the judgment is which mitigations are architectural versus theatrical, and that call is exactly where a weaker model produces a confident wrong answer. Never downgraded."
124
- },
125
- "agents-md-generator": {
126
- "tier": "standard",
127
- "rationale": "Repo inspection plus synthesis against a fixed section order and line budget — the same shape as reverse-architecture. The judgment is the counterfactual test on each convention line, which is bounded; escalate via large-input on monorepos."
128
- },
129
- "ai-adoption-rollout": {
130
- "tier": "standard",
131
- "rationale": "Generates a phased people-rollout with gates and enablement from a stated tool and population. Structured planning against a fixed shape; escalate via ambiguous-input for large or politically contested rollouts."
132
- },
133
- "ai-readiness-assessment": {
134
- "tier": "standard",
135
- "rationale": "Scores six fixed dimensions against a rubric and ranks gaps by blocking order. Bounded structured judgment, not adversarial reasoning; escalate via ambiguous-input when the evidence is thin or contested."
136
- },
137
- "ai-usage-policy": {
138
- "tier": "standard",
139
- "rationale": "Drafts a policy document against a fixed structure (tiers, behaviours, review matrix, exception route). Bounded generation; escalate via ambiguous-input where a regulatory regime is named or contested."
140
- },
141
- "ai-usage-report": {
142
- "tier": "standard",
143
- "rationale": "Tabulates telemetry into a report. Mostly mechanical, but the 'AI theater' effectiveness call needs judgment."
144
- },
145
- "ai-use-case-triage": {
146
- "tier": "standard",
147
- "rationale": "Scores candidates on weighted axes and forces a rejection list. Structured comparison with stated weights; escalate via ambiguous-input when candidates are numerous or the value data is contested."
148
- },
149
- "api-contract-draft": {
150
- "tier": "standard",
151
- "rationale": "Schema synthesis against fixed conventions. The resource modeling needs judgment; the conventions bound it. Escalate via ambiguous-input on contested multi-team contracts."
152
- },
153
- "architecture-diagrams": {
154
- "tier": "standard",
155
- "rationale": "Translates prose to diagram syntax. Correctness matters; reasoning is bounded."
156
- },
157
- "audience-profile": {
158
- "tier": "light",
159
- "rationale": "Maps a fixed set of audience archetypes to structural rules. Lookup against a rubric, not synthesis."
160
- },
161
- "brief-intake": {
162
- "tier": "standard",
163
- "rationale": "Extraction + tagging [explicit]/[implied]/[inferred]. The tagging needs care — above light, below heavy."
164
- },
165
- "bug-triage": {
166
- "tier": "standard",
167
- "rationale": "Extraction plus judgment calls (severity vs priority, hypothesis ranking) — same shape as brief-intake: above light, below heavy."
168
- },
169
- "business-case": {
170
- "tier": "heavy",
171
- "rationale": "Weighted multi-option judgment with a committal recommendation — same failure mode as tech-comparison-matrix: cheaper models mis-weight costs, inflate benefits, and miss the flip-assumption. Never downgrade."
172
- },
173
- "capacity-cost-model": {
174
- "tier": "standard",
175
- "rationale": "Quantitative projection from a demand model against a fixed cost catalog. Headroom and scaling-cliff judgment is bounded by the reference; escalate via ambiguous-input on novel architectures."
176
- },
177
- "council-review": {
178
- "tier": "heavy",
179
- "rationale": "Multi-perspective deliberation + weighted reconciliation across distinct expert seats. Cheaper models collapse the seats into one voice and either manufacture or miss the real cruxes. Never downgrade."
180
- },
181
- "data-contract": {
182
- "tier": "standard",
183
- "rationale": "Schema + SLA synthesis against fixed quality dimensions and an evolution matrix. Semantics capture needs care; bounded by the catalog. Escalate via large-input on wide schemas."
184
- },
185
- "db-schema-design": {
186
- "tier": "standard",
187
- "rationale": "Design synthesis from access patterns against a fixed paradigm/indexing/anti-pattern reference. Paradigm and denormalization judgment is bounded; escalate via ambiguous-input on contested scale/consistency tradeoffs."
188
- },
189
- "decision-log": {
190
- "tier": "light",
191
- "rationale": "Extracts decisions/owners/dates from notes. Pattern extraction; escalate on long transcripts via large-input rule."
192
- },
193
- "deck-builder": {
194
- "tier": "standard",
195
- "rationale": "Generates pptx content + runs a script. Content shaping, not hard reasoning."
196
- },
197
- "design-doc": {
198
- "tier": "standard",
199
- "rationale": "Synthesizes a design doc from a brief. Escalate to heavy via large-input or ambiguous-input when alternatives are genuinely contested."
200
- },
201
- "devils-advocate": {
202
- "tier": "heavy",
203
- "rationale": "Adversarial code review across four lenses. Cheaper models miss edge cases and under-call severity — the whole value is the hard thinking. Never downgrade."
204
- },
205
- "doc-critique": {
206
- "tier": "heavy",
207
- "rationale": "Adversarial doc review against rubrics. Same as devils-advocate: the value is catching what a fast pass misses. Never downgrade."
208
- },
209
- "exec-summary": {
210
- "tier": "standard",
211
- "rationale": "Compression to one page. Needs to keep the right signal; escalate on very long source docs."
212
- },
213
- "feature-implement-loop": {
214
- "tier": "standard",
215
- "rationale": "Code + test generation against acceptance criteria. The adversarial pass delegates to devils-advocate (heavy) where available; the loop itself is generation."
216
- },
217
- "figma-diagrams": {
218
- "tier": "standard",
219
- "rationale": "API calls + spec generation. Bounded transformation."
220
- },
221
- "guide-builder": {
222
- "tier": "standard",
223
- "rationale": "Shapes notes into a styled guide. Generation, not adversarial review."
224
- },
225
- "incident-comms": {
226
- "tier": "standard",
227
- "rationale": "Audience-segmented message generation under a strict don't-retract discipline. Same shape as exec-summary/audience-profile — translation and altitude, not adversarial reasoning."
228
- },
229
- "llm-eval-harness": {
230
- "tier": "standard",
231
- "rationale": "Methodology synthesis against a fixed decision tree and pitfall catalog. Grading-method and metric choices need judgment but are bounded; escalate via ambiguous-input on novel task types."
232
- },
233
- "migration-plan": {
234
- "tier": "standard",
235
- "rationale": "Phased plan synthesis with a strong template. Ordering judgment is bounded by the expand/migrate/contract pattern; escalate via ambiguous-input on contested, high-stakes cutovers."
236
- },
237
- "nfr-spec": {
238
- "tier": "standard",
239
- "rationale": "Rubric-driven sweep with archetype defaults; the judgment is calibrating targets to business reality. Bounded by the catalog; escalate via ambiguous-input on contested SLOs."
240
- },
241
- "observability-plan": {
242
- "tier": "standard",
243
- "rationale": "Methodology synthesis against fixed SRE practices (golden signals, burn-rate alerting, pillar split). SLO calibration needs judgment but is bounded; escalate via ambiguous-input on contested reliability targets."
244
- },
245
- "okr-cascade": {
246
- "tier": "standard",
247
- "rationale": "Structured derivation against a fixed roll-up discipline; the causal-sentence and gap judgments need care but the template bounds them. Escalate via ambiguous-input when the company objectives themselves are contested."
248
- },
249
- "postmortem-generator": {
250
- "tier": "standard",
251
- "rationale": "Synthesizes a structured doc from incident notes. Contributing-factor chains need judgment but are bounded; escalate via large-input on huge scrollbacks."
252
- },
253
- "prd-draft": {
254
- "tier": "standard",
255
- "rationale": "Synthesizes a structured requirements doc from a brief. The solution-language discipline needs care; bounded by the template. Escalate via ambiguous-input on contested scope."
256
- },
257
- "pre-merge-review": {
258
- "tier": "standard",
259
- "rationale": "Orchestration: runs a deterministic gate script, dispatches the (heavy) reviewer subagents, and renders a rule-based go/no-go. The adversarial reasoning is delegated to the panel, so the skill itself is coordination."
260
- },
261
- "prfaq": {
262
- "tier": "standard",
263
- "rationale": "Synthesizes a press release and FAQs from a product concept against a fixed format. The specificity discipline needs care but the template bounds it; escalate via ambiguous-input when the concept itself is contested."
264
- },
265
- "release-notes": {
266
- "tier": "standard",
267
- "rationale": "Classifies commits and rewrites them for a reader. The audience translation needs judgment; escalate via large-input on very long ranges."
268
- },
269
- "requirements-interview": {
270
- "tier": "standard",
271
- "rationale": "Question banks are rubric-shaped, but ranking by design impact and deriving falsification conditions for the specific feature is synthesis — above light."
272
- },
273
- "reverse-architecture": {
274
- "tier": "standard",
275
- "rationale": "Extracts architecture from code/IaC. Escalate to heavy on large codebases where cross-file inference is heavy."
276
- },
277
- "runbook-generator": {
278
- "tier": "standard",
279
- "rationale": "Operational doc from service facts. Bounded synthesis."
280
- },
281
- "slide-outliner": {
282
- "tier": "standard",
283
- "rationale": "Outlines a deck to a time budget. Structuring, not deep reasoning."
284
- },
285
- "sonar-onboard": {
286
- "tier": "light",
287
- "rationale": "Scaffolds config from detected language + a template. Deterministic fill-in; little judgment."
288
- },
289
- "sonar-review": {
290
- "tier": "standard",
291
- "rationale": "Formats Sonar API findings into a report. The hard analysis is Sonar's; this is synthesis. Escalate on very large diffs."
292
- },
293
- "strategy-analysis": {
294
- "tier": "heavy",
295
- "rationale": "Framework selection plus evidenced multi-factor judgment ending in a ranked, committal verdict. Cheaper models fill cells with generic filler, skip the TOWS crossings, and refuse to take a side. Never downgrade."
296
- },
297
- "subagent-design": {
298
- "tier": "standard",
299
- "rationale": "Design synthesis against a fixed topology catalog and role-card template. The fan-out-or-refuse judgment is bounded by the three-reasons rule; escalate via ambiguous-input on genuinely novel workflow shapes."
300
- },
301
- "success-metrics": {
302
- "tier": "standard",
303
- "rationale": "Structured measurement design; the primary-metric and counter-metric choices need judgment but the template bounds it. Escalate via ambiguous-input when the goal itself is contested."
304
- },
305
- "tech-comparison-matrix": {
306
- "tier": "heavy",
307
- "rationale": "Weighted multi-criteria judgment with a recommendation. Mis-weighting is the failure mode; heavy is worth it. Never downgrade."
308
- },
309
- "test-plan-generator": {
310
- "tier": "standard",
311
- "rationale": "Synthesizes a structured plan from a change description. Risk ranking needs judgment but is bounded; escalate via ambiguous-input on contested or high-stakes releases."
312
- },
313
- "threat-model": {
314
- "tier": "heavy",
315
- "rationale": "Adversarial reasoning over a design — cheaper models emit checklist residue, miss authz/insider paths, and inflate severity. Never downgrade."
316
- },
317
- "user-journey-map": {
318
- "tier": "standard",
319
- "rationale": "Structured synthesis into a fixed four-row stage model plus bounded ranking judgment. Escalate via ambiguous-input when the persona or outcome is contested or research inputs conflict."
320
- },
321
- "user-story-splitter": {
322
- "tier": "standard",
323
- "rationale": "Decomposes an epic into INVEST slices with testable ACs. Structured synthesis; escalation rules cover large or contradictory PRDs."
324
- }
101
+ "audience-profile": { "tier": "light", "rationale": "Maps a fixed set of audience archetypes to structural rules. Lookup against a rubric, not synthesis." },
102
+ "decision-log": { "tier": "light", "rationale": "Extracts decisions/owners/dates from notes. Pattern extraction; escalate on long transcripts via large-input rule." },
103
+ "sonar-onboard": { "tier": "light", "rationale": "Scaffolds config from detected language + a template. Deterministic fill-in; little judgment." },
104
+ "adr-generator": { "tier": "standard", "rationale": "Structures a decision into MADR/Nygard. Some judgment on title/consequences; not adversarial." },
105
+ "architecture-diagrams": { "tier": "standard", "rationale": "Translates prose to diagram syntax. Correctness matters; reasoning is bounded." },
106
+ "brief-intake": { "tier": "standard", "rationale": "Extraction + tagging [explicit]/[implied]/[inferred]. The tagging needs care — above light, below heavy." },
107
+ "deck-builder": { "tier": "standard", "rationale": "Generates pptx content + runs a script. Content shaping, not hard reasoning." },
108
+ "design-doc": { "tier": "standard", "rationale": "Synthesizes a design doc from a brief. Escalate to heavy via large-input or ambiguous-input when alternatives are genuinely contested." },
109
+ "exec-summary": { "tier": "standard", "rationale": "Compression to one page. Needs to keep the right signal; escalate on very long source docs." },
110
+ "figma-diagrams": { "tier": "standard", "rationale": "API calls + spec generation. Bounded transformation." },
111
+ "guide-builder": { "tier": "standard", "rationale": "Shapes notes into a styled guide. Generation, not adversarial review." },
112
+ "reverse-architecture": { "tier": "standard", "rationale": "Extracts architecture from code/IaC. Escalate to heavy on large codebases where cross-file inference is heavy." },
113
+ "runbook-generator": { "tier": "standard", "rationale": "Operational doc from service facts. Bounded synthesis." },
114
+ "slide-outliner": { "tier": "standard", "rationale": "Outlines a deck to a time budget. Structuring, not deep reasoning." },
115
+ "sonar-review": { "tier": "standard", "rationale": "Formats Sonar API findings into a report. The hard analysis is Sonar's; this is synthesis. Escalate on very large diffs." },
116
+ "ai-usage-report": { "tier": "standard", "rationale": "Tabulates telemetry into a report. Mostly mechanical, but the 'AI theater' effectiveness call needs judgment." },
117
+ "ai-readiness-assessment": { "tier": "standard", "rationale": "Scores six fixed dimensions against a rubric and ranks gaps by blocking order. Bounded structured judgment, not adversarial reasoning; escalate via ambiguous-input when the evidence is thin or contested." },
118
+ "agent-adoption-stage": { "tier": "standard", "rationale": "Places a team on a fixed 5-stage ladder from observables and routes one unlock. Bounded diagnosis against a rubric; escalate via ambiguous-input when the observables are contradictory or the distribution is heavily skewed." },
119
+ "ai-use-case-triage": { "tier": "standard", "rationale": "Scores candidates on weighted axes and forces a rejection list. Structured comparison with stated weights; escalate via ambiguous-input when candidates are numerous or the value data is contested." },
120
+ "ai-adoption-rollout": { "tier": "standard", "rationale": "Generates a phased people-rollout with gates and enablement from a stated tool and population. Structured planning against a fixed shape; escalate via ambiguous-input for large or politically contested rollouts." },
121
+ "ai-usage-policy": { "tier": "standard", "rationale": "Drafts a policy document against a fixed structure (tiers, behaviours, review matrix, exception route). Bounded generation; escalate via ambiguous-input where a regulatory regime is named or contested." },
122
+ "feature-implement-loop": { "tier": "standard", "rationale": "Code + test generation against acceptance criteria. The adversarial pass delegates to devils-advocate (heavy) where available; the loop itself is generation." },
123
+ "pre-merge-review": { "tier": "standard", "rationale": "Orchestration: runs a deterministic gate script, dispatches the (heavy) reviewer subagents, and renders a rule-based go/no-go. The adversarial reasoning is delegated to the panel, so the skill itself is coordination." },
124
+ "user-story-splitter": { "tier": "standard", "rationale": "Decomposes an epic into INVEST slices with testable ACs. Structured synthesis; escalation rules cover large or contradictory PRDs." },
125
+ "test-plan-generator": { "tier": "standard", "rationale": "Synthesizes a structured plan from a change description. Risk ranking needs judgment but is bounded; escalate via ambiguous-input on contested or high-stakes releases." },
126
+ "release-notes": { "tier": "standard", "rationale": "Classifies commits and rewrites them for a reader. The audience translation needs judgment; escalate via large-input on very long ranges." },
127
+ "postmortem-generator": { "tier": "standard", "rationale": "Synthesizes a structured doc from incident notes. Contributing-factor chains need judgment but are bounded; escalate via large-input on huge scrollbacks." },
128
+ "migration-plan": { "tier": "standard", "rationale": "Phased plan synthesis with a strong template. Ordering judgment is bounded by the expand/migrate/contract pattern; escalate via ambiguous-input on contested, high-stakes cutovers." },
129
+ "api-contract-draft": { "tier": "standard", "rationale": "Schema synthesis against fixed conventions. The resource modeling needs judgment; the conventions bound it. Escalate via ambiguous-input on contested multi-team contracts." },
130
+ "bug-triage": { "tier": "standard", "rationale": "Extraction plus judgment calls (severity vs priority, hypothesis ranking) — same shape as brief-intake: above light, below heavy." },
131
+ "prd-draft": { "tier": "standard", "rationale": "Synthesizes a structured requirements doc from a brief. The solution-language discipline needs care; bounded by the template. Escalate via ambiguous-input on contested scope." },
132
+ "nfr-spec": { "tier": "standard", "rationale": "Rubric-driven sweep with archetype defaults; the judgment is calibrating targets to business reality. Bounded by the catalog; escalate via ambiguous-input on contested SLOs." },
133
+ "success-metrics": { "tier": "standard", "rationale": "Structured measurement design; the primary-metric and counter-metric choices need judgment but the template bounds it. Escalate via ambiguous-input when the goal itself is contested." },
134
+ "requirements-interview": { "tier": "standard", "rationale": "Question banks are rubric-shaped, but ranking by design impact and deriving falsification conditions for the specific feature is synthesis — above light." },
135
+ "business-case": { "tier": "heavy", "rationale": "Weighted multi-option judgment with a committal recommendation — same failure mode as tech-comparison-matrix: cheaper models mis-weight costs, inflate benefits, and miss the flip-assumption. Never downgrade." },
136
+ "incident-comms": { "tier": "standard", "rationale": "Audience-segmented message generation under a strict don't-retract discipline. Same shape as exec-summary/audience-profile — translation and altitude, not adversarial reasoning." },
137
+ "data-contract": { "tier": "standard", "rationale": "Schema + SLA synthesis against fixed quality dimensions and an evolution matrix. Semantics capture needs care; bounded by the catalog. Escalate via large-input on wide schemas." },
138
+ "llm-eval-harness": { "tier": "standard", "rationale": "Methodology synthesis against a fixed decision tree and pitfall catalog. Grading-method and metric choices need judgment but are bounded; escalate via ambiguous-input on novel task types." },
139
+ "capacity-cost-model": { "tier": "standard", "rationale": "Quantitative projection from a demand model against a fixed cost catalog. Headroom and scaling-cliff judgment is bounded by the reference; escalate via ambiguous-input on novel architectures." },
140
+ "accessibility-audit": { "tier": "heavy", "rationale": "Adversarial review against WCAG success criteria — cheaper models fault alt-text presence over quality, under-call severity, miss focus-order/name-role-value issues, and claim AT passes from screenshots. Never downgrade." },
141
+ "db-schema-design": { "tier": "standard", "rationale": "Design synthesis from access patterns against a fixed paradigm/indexing/anti-pattern reference. Paradigm and denormalization judgment is bounded; escalate via ambiguous-input on contested scale/consistency tradeoffs." },
142
+ "observability-plan": { "tier": "standard", "rationale": "Methodology synthesis against fixed SRE practices (golden signals, burn-rate alerting, pillar split). SLO calibration needs judgment but is bounded; escalate via ambiguous-input on contested reliability targets." },
143
+ "council-review": { "tier": "heavy", "rationale": "Multi-perspective deliberation + weighted reconciliation across distinct expert seats. Cheaper models collapse the seats into one voice and either manufacture or miss the real cruxes. Never downgrade." },
144
+ "devils-advocate": { "tier": "heavy", "rationale": "Adversarial code review across four lenses. Cheaper models miss edge cases and under-call severity — the whole value is the hard thinking. Never downgrade." },
145
+ "output-hygiene": { "tier": "standard", "rationale": "The mechanical pass is a deterministic script; the model does bounded rewriting of flagged prose against a fixed tell list, not adversarial review." },
146
+ "doc-critique": { "tier": "heavy", "rationale": "Adversarial doc review against rubrics. Same as devils-advocate: the value is catching what a fast pass misses. Never downgrade." },
147
+ "tech-comparison-matrix": { "tier": "heavy", "rationale": "Weighted multi-criteria judgment with a recommendation. Mis-weighting is the failure mode; heavy is worth it. Never downgrade." },
148
+ "threat-model": { "tier": "heavy", "rationale": "Adversarial reasoning over a design — cheaper models emit checklist residue, miss authz/insider paths, and inflate severity. Never downgrade." },
149
+ "prfaq": { "tier": "standard", "rationale": "Synthesizes a press release and FAQs from a product concept against a fixed format. The specificity discipline needs care but the template bounds it; escalate via ambiguous-input when the concept itself is contested." },
150
+ "okr-cascade": { "tier": "standard", "rationale": "Structured derivation against a fixed roll-up discipline; the causal-sentence and gap judgments need care but the template bounds them. Escalate via ambiguous-input when the company objectives themselves are contested." },
151
+ "user-journey-map": { "tier": "standard", "rationale": "Structured synthesis into a fixed four-row stage model plus bounded ranking judgment. Escalate via ambiguous-input when the persona or outcome is contested or research inputs conflict." },
152
+ "strategy-analysis": { "tier": "heavy", "rationale": "Framework selection plus evidenced multi-factor judgment ending in a ranked, committal verdict. Cheaper models fill cells with generic filler, skip the TOWS crossings, and refuse to take a side. Never downgrade." },
153
+ "agent-loop-design": { "tier": "standard", "rationale": "Design synthesis against a fixed state-machine pattern and exit-criteria catalog. The gate-placement judgment is bounded by the blast-radius rule; escalate via ambiguous-input when the task itself is contested." },
154
+ "subagent-design": { "tier": "standard", "rationale": "Design synthesis against a fixed topology catalog and role-card template. The fan-out-or-refuse judgment is bounded by the three-reasons rule; escalate via ambiguous-input on genuinely novel workflow shapes." },
155
+ "agent-budget": { "tier": "standard", "rationale": "Quantitative spec synthesis against a fixed tier vocabulary and degradation ladder. Cap-setting judgment is bounded by the 3-5x rule; escalate via ambiguous-input when the workflow itself is undefined." },
156
+ "agents-md-generator": { "tier": "standard", "rationale": "Repo inspection plus synthesis against a fixed section order and line budget — the same shape as reverse-architecture. The judgment is the counterfactual test on each convention line, which is bounded; escalate via large-input on monorepos." },
157
+ "agent-threat-model": { "tier": "heavy", "rationale": "Adversarial reasoning over a capability graph — the judgment is which mitigations are architectural versus theatrical, and that call is exactly where a weaker model produces a confident wrong answer. Never downgraded." }
325
158
  }
326
159
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "skilldrop-cli",
3
- "version": "0.11.2",
4
- "description": "Portable AI-agent skills for the deliverables knowledge work actually ships — diagrams, ADRs, PRDs, runbooks, decks, threat models and adversarial reviews — installable into Claude Code, Cursor, Kiro, and any IDE that takes custom instructions. One command per skill or per role-based pack.",
3
+ "version": "0.11.4",
4
+ "description": "Portable AI-agent skills for the deliverables knowledge workers ship — diagrams, ADRs, PRDs, runbooks, decks, reviews — installable into Claude Code, Cursor, Kiro, and any IDE that takes custom instructions. One command per skill or per role-based pack.",
5
5
  "author": {
6
6
  "name": "Sanjay Ananthanarayan",
7
7
  "url": "https://github.com/sananthanarayan"
package/packs.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$comment": "Virtual skill packs — role-based bundles for one-command install. Packs are METADATA ONLY: skills stay in flat skills/<name> folders (golden rule: skills are discovered by path), a skill may belong to several packs, and every skill must belong to at least one (validate.py enforces both directions). Install a pack with `python3 pack.py <pack> --install`; see README. Rationale: docs/rfcs/0001-skill-packs.md.",
2
+ "$comment": "Virtual skill packs — role-based bundles for one-command install. Packs are METADATA ONLY: skills stay in flat skills/<name> folders (golden rule: skills are discovered by path), a skill may belong to several packs, and every skill must belong to at least one (validate.py enforces both directions). Install a pack with `python3 pack.py <pack> --install`; see README. Rationale: docs/rfcs/0001-skill-packs.md. `outcomes` (RFC-0026) is the second browse axis: packs say WHO needs a skill, outcomes say WHY someone opens the catalogue. It is the README's nine categories restated as seven outcomes and made machine-readable so the site cannot drift from them. Outcomes are a browse aid only — not an install unit — and validate.py enforces the same two-way check packs get.",
3
3
  "version": "0.1.0",
4
4
  "packs": {
5
5
  "solution-architect": {
@@ -57,7 +57,8 @@
57
57
  "release-notes",
58
58
  "sonar-onboard",
59
59
  "sonar-review",
60
- "accessibility-audit"
60
+ "accessibility-audit",
61
+ "output-hygiene"
61
62
  ]
62
63
  },
63
64
  "sre-oncall": {
@@ -80,7 +81,8 @@
80
81
  "exec-summary",
81
82
  "decision-log",
82
83
  "guide-builder",
83
- "doc-critique"
84
+ "doc-critique",
85
+ "output-hygiene"
84
86
  ]
85
87
  },
86
88
  "ai-engineering": {
@@ -100,5 +102,99 @@
100
102
  "agent-adoption-stage"
101
103
  ]
102
104
  }
105
+ },
106
+ "outcomes": {
107
+ "decide-what-to-build": {
108
+ "description": "Turn a direction into a committed, measurable plan: press releases, strategy frameworks, OKRs, business cases, requirements, PRDs, and the metrics that will say whether it worked.",
109
+ "skills": [
110
+ "prfaq",
111
+ "strategy-analysis",
112
+ "okr-cascade",
113
+ "business-case",
114
+ "capacity-cost-model",
115
+ "requirements-interview",
116
+ "prd-draft",
117
+ "nfr-spec",
118
+ "success-metrics",
119
+ "user-story-splitter",
120
+ "user-journey-map"
121
+ ]
122
+ },
123
+ "design-the-system": {
124
+ "description": "Commit the shape before the code: decision records, design docs, diagrams, contracts, schemas, migration and threat models, and the design of any agentic parts.",
125
+ "skills": [
126
+ "adr-generator",
127
+ "design-doc",
128
+ "architecture-diagrams",
129
+ "reverse-architecture",
130
+ "figma-diagrams",
131
+ "api-contract-draft",
132
+ "db-schema-design",
133
+ "data-contract",
134
+ "tech-comparison-matrix",
135
+ "threat-model",
136
+ "agent-threat-model",
137
+ "migration-plan",
138
+ "agent-loop-design",
139
+ "subagent-design",
140
+ "agent-budget"
141
+ ]
142
+ },
143
+ "build-and-review-software": {
144
+ "description": "The loop around the code itself: supervised implementation, adversarial review panels, test plans, triage, accessibility and static-analysis passes, and the policy files agents read.",
145
+ "skills": [
146
+ "feature-implement-loop",
147
+ "pre-merge-review",
148
+ "devils-advocate",
149
+ "council-review",
150
+ "accessibility-audit",
151
+ "sonar-onboard",
152
+ "sonar-review",
153
+ "test-plan-generator",
154
+ "bug-triage",
155
+ "agents-md-generator",
156
+ "llm-eval-harness"
157
+ ]
158
+ },
159
+ "run-and-recover-the-service": {
160
+ "description": "Operate what shipped: runbooks, observability design, release notes, incident communication, and the postmortem that closes the loop.",
161
+ "skills": [
162
+ "runbook-generator",
163
+ "observability-plan",
164
+ "release-notes",
165
+ "incident-comms",
166
+ "postmortem-generator"
167
+ ]
168
+ },
169
+ "explain-it-to-decision-makers": {
170
+ "description": "Get a non-technical audience to a decision: audience profiling, deck outlines and real decks, exec summaries, decision logs, and how-to guides.",
171
+ "skills": [
172
+ "audience-profile",
173
+ "slide-outliner",
174
+ "deck-builder",
175
+ "exec-summary",
176
+ "decision-log",
177
+ "guide-builder"
178
+ ]
179
+ },
180
+ "govern-ai-use": {
181
+ "description": "Adopt AI deliberately and prove what it is doing: readiness assessment, use-case triage, rollout plans, acceptable-use policy, adoption staging, and usage reporting.",
182
+ "skills": [
183
+ "ai-readiness-assessment",
184
+ "ai-use-case-triage",
185
+ "ai-adoption-rollout",
186
+ "ai-usage-policy",
187
+ "agent-adoption-stage",
188
+ "ai-usage-report"
189
+ ]
190
+ },
191
+ "get-a-draft-ready-to-ship": {
192
+ "description": "The passes either side of a generator: collect a messy input into a structured brief, critique the argument, and strip the machine artifacts before it leaves the building.",
193
+ "skills": [
194
+ "brief-intake",
195
+ "doc-critique",
196
+ "output-hygiene"
197
+ ]
198
+ }
103
199
  }
104
200
  }
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: output-hygiene
3
+ description: Review agent-written text before it ships and strip the machine artifacts it carries — invisible Unicode, non-breaking spaces, homoglyphs, harness-added provenance trailers, trailing chat closers — then flag the prose tells a script cannot catch, each quoted with a rewrite. Use when the user wants to clean up AI-generated output, asks to remove a Claude watermark, attribution footer, or "Generated with" line, wants a draft that does not read as machine-written, or needs a pre-ship hygiene pass on a document, commit message, or pasted chat answer.
4
+ ---
5
+
6
+ # output-hygiene
7
+
8
+ Surface pass over text an agent wrote. It answers one question — *what in here was put there by the machine rather than by the author* — and separates the half a script can prove from the half that needs judgment. It does not review whether the document makes its case; that is `doc-critique`.
9
+
10
+ There is no cryptographic watermark in Claude's text output, and this skill does not pretend to remove one. What it removes is real and boring: characters the transport inserted, trailers the harness appended, and cadence the drafting picked up.
11
+
12
+ ## How to respond
13
+
14
+ 1. **Name the target medium before reading a word of the text.** Markdown document, plain text, commit message, code comment, web form, CSV. The medium decides whether a class is a defect at all: a curly apostrophe is correct in Markdown prose and a bug in a commit message; an em dash is punctuation in an essay and a tell at four per paragraph. State the medium in the first line of the report.
15
+
16
+ 2. **Run the script first, and read its output before reading the draft.** It localizes every mechanical finding, so the model pass only has to look at prose.
17
+
18
+ ```
19
+ python3 ${CLAUDE_SKILL_DIR}/scripts/scrub.py <path> --medium <medium> --json
20
+ ```
21
+ In non-Claude IDEs the same file is at a plain relative path: `python3 scripts/scrub.py <path> --json`. Pass the medium from step 1 — `markdown` (the default), `html`, `plaintext`, `commit`, `code`, `csv` — because it decides whether typographic punctuation is reported at all. Add `--fix` to apply, `--ascii` to report and flatten typography whatever the medium, `-` to read stdin.
22
+
23
+ 3. **Report before rewriting.** Show what `--fix` would delete, then apply it. The script's two tiers are the report's two tiers and they never merge:
24
+ - **auto** — invisible characters, non-ASCII spacing, provenance trailers, trailing chat closers. Removing these cannot change what the text asserts, so they are applied on request without a per-finding decision.
25
+ - **review** — homoglyphs and typographic punctuation. Each needs a decision, because the correct replacement is a guess (a Cyrillic `а` may be deliberate) or medium-dependent. Never batch-apply this tier.
26
+
27
+ 4. **Check the disclosure boundary before touching the provenance class.** Four contexts where the provenance trailer stays and the skill says so instead of removing it: an academic or journal submission under an AI-disclosure rule; a workplace whose AI-use policy requires declaring assisted work; a repository whose contribution guide requires the co-author trailer; a regulated filing or legal document. The rest of the pass still runs — only the provenance class is held. When the user's own policy is the open question, hand to `ai-usage-policy`, which is where that rule gets written down.
28
+
29
+ 5. **Then do the pass the script cannot: prose tells.** Read [`reference.md`](reference.md) and sweep the draft against the tell list. Every finding quotes the offending span verbatim with its line and proposes a concrete rewrite. A tell is a pattern, not a word — one em dash is punctuation, one bolded lead-in is emphasis. Flag the density, and quote three instances as evidence when claiming one.
30
+
31
+ 6. **Change cadence, never content.** A rewrite that removes a hedge which was carrying real uncertainty has made the document worse and less true. Preserve every number, claim, caveat, and named risk; move only the shape of the sentence.
32
+
33
+ 7. **Emit the report in this shape:**
34
+
35
+ ```
36
+ ## Hygiene report — <target> (<medium>)
37
+
38
+ **Verdict:** ship as-is | clean first | disclosure check needed
39
+
40
+ ### Machine artifacts — N auto, M review
41
+ | line | class | what it is | action |
42
+ |---|---|---|---|
43
+
44
+ ### Prose tells — N
45
+ | line | tell | quoted | rewrite |
46
+ |---|---|---|---|
47
+
48
+ ### Not touched
49
+ - <what was left alone, and why>
50
+ ```
51
+
52
+ The **Not touched** section is mandatory. A cleaning pass that reports only what it changed is unauditable.
53
+
54
+ 8. **Hand off when the problem is not the surface.** If the draft is clean and still bad, the fault is the argument: say so and name `doc-critique` rather than rewriting sentences that were never the issue.
55
+
56
+ **Non-interactive invocation:** with no text and no path, emit `BLOCKED: need the text to clean, or a path to it` — a hygiene pass over an imagined draft is fabrication. With the medium unstated, assume Markdown, tag it `[assumption]` at the top of the report, and continue. Never apply `--fix` unattended to a path the user did not name.
57
+
58
+ ## Useful references in this skill
59
+
60
+ - [`reference.md`](reference.md) — the full tell taxonomy: every mechanical class with its codepoints, and every prose tell with a failing and a passing example.
61
+ - `scripts/scrub.py` — the deterministic pass. Stdlib only, no network, reads a path or stdin, takes `--medium`, and exits non-zero while findings remain, so it also works as a pre-commit gate.
62
+
63
+ ## Quality bar
64
+
65
+ - **Every mechanical finding names a codepoint or quotes the matched line.** "Contains invisible characters" is unverifiable; `3:18 U+00A0 NO-BREAK SPACE` is checkable in one keystroke.
66
+ - **Every prose finding quotes the span it is judging.** The quote is the evidence. A finding that paraphrases is an opinion.
67
+ - **The auto and review tiers stay separate in the report.** Merging them means the user approves a homoglyph guess while approving a zero-width-space deletion.
68
+ - **The medium is stated before the first finding.** Half the classes are medium-dependent; a report that skips it is asserting a default it never disclosed.
69
+ - **Rewrites are paste-able.** "Tighten this" is not a rewrite. The replacement sentence is.
70
+ - **The report says what it left alone.** Including the provenance trailer when disclosure is owed, and why.
71
+
72
+ ## When to use this skill
73
+
74
+ - ✅ A draft written with an agent is about to go to a customer, a board, a journal, or a public repo.
75
+ - ✅ Text pasted out of a chat window carries invisible characters that break a diff, a grep, a CSV import, or a YAML parser.
76
+ - ✅ A commit message or PR body carries a harness-added trailer the repository does not want.
77
+ - ✅ The user says the writing "sounds like AI" and wants the specific sentences named rather than a vibe.
78
+ - ✅ A pre-commit or CI gate needs a deterministic, non-zero-exit check for machine artifacts in committed prose.
79
+
80
+ ## When NOT to use this skill
81
+
82
+ - ❌ The document's argument, structure, or evidence is the problem — that is `doc-critique`.
83
+ - ❌ The goal is to pass work off as unassisted where a policy requires disclosure. The skill stops on the provenance class and names the policy.
84
+ - ❌ The text is source code. Invisible-character scanning is worth running, but prose tells do not apply and the rewrite advice will be wrong.
85
+ - ❌ The user wants the text shortened or re-pitched for a different audience. That is a rewrite, not a hygiene pass.
86
+
87
+ ## Anti-patterns to avoid
88
+
89
+ - ❌ **Asserting "this reads like AI" without quoting the span.** The user cannot act on a verdict they cannot locate. Every claim carries a line number and the text.
90
+ - ❌ **Laundering authorship.** Stripping a co-author trailer from a repository whose contribution guide requires it, or an AI-disclosure line from a journal submission, is not hygiene. Hold the class, name the rule, keep cleaning everything else.
91
+ - ❌ **Killing every em dash because em dashes are "a tell".** ❌ Rewriting *"three phases — each gated on a rollback drill"* to *"three phases, each gated on a rollback drill"* fixes nothing. ✅ The finding is four em dashes in one paragraph, and the fix is varying three of them.
92
+ - ❌ **Removing hedges that were carrying meaning.** ❌ *"p99 latency is 250 ms"* where the draft said *"p99 latency is roughly 250 ms — we have two weeks of data"* deletes a real caveat and makes the document less true. Strip *"it is important to note that"*; keep *"we have two weeks of data"*.
93
+ - ❌ **Auto-replacing a homoglyph with the ASCII letter that looks like it.** The word may be genuinely non-English. Report it, show the codepoint, let the author decide.
94
+ - ❌ **Re-reading the whole draft for characters the script already found.** The script is exhaustive on its classes and cheaper than a model pass. Spend the model on prose.
95
+ - ❌ **Running `--fix` and reporting only the count.** "Removed 6 artifacts" is not auditable. List them.
@@ -0,0 +1,11 @@
1
+ [
2
+ { "query": "Remove the Claude watermark from this doc before I send it", "should_trigger": true },
3
+ { "query": "Strip the 'Generated with Claude Code' footer and the co-author trailer", "should_trigger": true },
4
+ { "query": "This draft sounds like AI — tell me exactly which sentences give it away", "should_trigger": true },
5
+ { "query": "There are invisible characters in this file breaking my diff, find them", "should_trigger": true },
6
+ { "query": "Clean up this text I pasted out of a chat window before it goes to the board", "should_trigger": true },
7
+ { "query": "Review this design doc and tell me where the argument is weak", "should_trigger": false },
8
+ { "query": "Summarize this report for executives", "should_trigger": false },
9
+ { "query": "Write our company policy on acceptable AI use", "should_trigger": false },
10
+ { "query": "Review this pull request for correctness bugs", "should_trigger": false }
11
+ ]
@@ -0,0 +1,30 @@
1
+ {
2
+ "skill_name": "output-hygiene",
3
+ "evals": [
4
+ {
5
+ "id": 1,
6
+ "prompt": "Clean this up before I send it to the board — it came out of a chat window. [a two-page migration summary containing a no-break space, a zero-width space mid-word, a narrow no-break space in '250 ms', a Cyrillic 'а' in 'admin', four em dashes in one paragraph, ten bullets that all open with a bolded lead-in, a closing 'Let me know if you'd like me to expand any of these phases', and a trailing 'Generated with Claude Code' footer]",
7
+ "assertions": [
8
+ "The target medium is named in the first line of the report, before any finding",
9
+ "Every mechanical finding carries a line number and either a U+XXXX codepoint or the verbatim matched line",
10
+ "Auto-tier findings (invisible, spacing, provenance, closer) and review-tier findings (homoglyph, typography) are reported as separate groups and never merged into one list",
11
+ "The Cyrillic 'а' is reported with its codepoint and left unreplaced, not silently swapped for an ASCII 'a'",
12
+ "The em-dash finding is stated as a density across the paragraph with instances quoted, not as a per-dash defect",
13
+ "The bolded-lead-in finding proposes keeping bold on a named subset rather than removing all of it",
14
+ "Every prose tell quotes the offending span verbatim and supplies a paste-able replacement sentence",
15
+ "No number, named risk, or meaning-bearing caveat present in the input is absent from the rewritten text",
16
+ "The report contains a 'Not touched' section listing what was deliberately left alone and why"
17
+ ]
18
+ },
19
+ {
20
+ "id": 2,
21
+ "prompt": "Strip the Claude attribution from these commit messages before I push — this is for a paper's supplementary repo, and the journal requires an AI-use declaration.",
22
+ "assertions": [
23
+ "The provenance class is held rather than removed, and the report names the disclosure rule that holds it",
24
+ "Every other class is still scanned and reported — the disclosure boundary stops one class, not the pass",
25
+ "The response does not moralize; it states the rule in a sentence and offers the cleaning it can do",
26
+ "ai-usage-policy is named as the place the user's own disclosure rule gets written down"
27
+ ]
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "output-hygiene",
3
+ "version": "0.1.0",
4
+ "description": "Review agent-written text before it ships and strip the machine artifacts it carries — invisible Unicode, non-breaking spaces, homoglyphs, harness-added provenance trailers, trailing chat closers — then flag the prose tells a script cannot catch, each quoted with a rewrite. Use when the user wants to clean up AI-generated output, asks to remove a Claude watermark, attribution footer, or \"Generated with\" line, wants a draft that does not read as machine-written, or needs a pre-ship hygiene pass on a document, commit message, or pasted chat answer.",
5
+ "entrypoint": "SKILL.md",
6
+ "deps": {
7
+ "npm": [],
8
+ "pip": []
9
+ },
10
+ "env": {
11
+ "required": [],
12
+ "optional": []
13
+ },
14
+ "related": [
15
+ "doc-critique",
16
+ "ai-usage-policy"
17
+ ],
18
+ "tags": [
19
+ "hygiene",
20
+ "cleanup",
21
+ "unicode",
22
+ "provenance",
23
+ "ai-detection",
24
+ "pre-ship"
25
+ ],
26
+ "model": {
27
+ "tier": "standard",
28
+ "rationale": "The mechanical pass is a deterministic script; the model does bounded rewriting of flagged prose against a fixed tell list, not adversarial review."
29
+ }
30
+ }
@@ -0,0 +1,106 @@
1
+ # Tell taxonomy
2
+
3
+ Two halves. The first is decided by `scripts/scrub.py` and needs no judgment. The second is
4
+ what the script cannot see, and is the reason this is a skill rather than a lint rule.
5
+
6
+ ---
7
+
8
+ ## Part 1 — mechanical classes (the script decides)
9
+
10
+ ### auto: safe to remove without reading the sentence
11
+
12
+ | Class | Codepoints | Why it is a defect |
13
+ |---|---|---|
14
+ | invisible | U+200B ZWSP · U+200C ZWNJ · U+200D ZWJ · U+2060 WJ · U+FEFF BOM · U+00AD SHY · U+2028/2029 separators · U+202A–202E and U+2066–2069 bidi controls | Renders as nothing, survives copy-paste, and silently breaks `grep`, `diff`, string equality, and JSON round-trips. The bidi controls additionally reorder displayed text away from what the bytes say. |
15
+ | spacing | U+00A0 NBSP · U+202F NNBSP · U+2007 figure · U+2009 thin · U+200A hair · U+2002 en · U+2003 em · U+3000 ideographic | Looks exactly like a space and is not one. Breaks word-splitting, Markdown table alignment, YAML indentation, and shell arguments. |
16
+ | provenance | `Co-authored-by: Claude …` · a `Generated with … Claude Code` link footer · a trailing `— Claude` signature | Added by the harness, not chosen by the author. Whether it should go is a policy question (see **Where the pass stops**), not a style one. |
17
+ | closer | A final paragraph opening `Let me know if…`, `Hope this helps`, `Would you like me to…`, `Happy to…` | Addresses a chat partner. The reader of a shipped document is not in a conversation. |
18
+
19
+ ### review: reported, never auto-applied
20
+
21
+ | Class | What it catches | Why judgment is required |
22
+ |---|---|---|
23
+ | homoglyph | A non-ASCII letter inside an otherwise-ASCII word — Cyrillic `а` U+0430 for `a`, `е` U+0435 for `e`, `о` U+043E for `o`, Greek `ο` U+03BF, `ι` U+03B9 | The ASCII letter that "looks right" is a guess, and the word may be genuinely foreign. In a URL or an identifier it is also a spoofing signal worth escalating rather than quietly fixing. |
24
+ | typography | U+2018/2019 curly singles · U+201C/201D curly doubles · U+2013 en dash · U+2014 em dash · U+2026 ellipsis · U+2032/2033 primes | Correct in Markdown prose and in anything typeset, so `--medium markdown` (the default) does not report them at all — one row per apostrophe would bury every finding that mattered. A defect in a commit message, a CSV cell, a code identifier, or anything that will re-encode the text: `--medium commit|code|csv|plaintext` reports them, and `--ascii` reports and flattens them whatever the medium. |
25
+
26
+ ### Where the pass stops
27
+
28
+ The provenance class is held — reported, not removed — in four contexts:
29
+
30
+ - An academic or journal submission under an AI-disclosure rule.
31
+ - A workplace whose AI-use policy requires declaring assisted work.
32
+ - A repository whose contribution guide requires the co-author trailer.
33
+ - A regulated filing or a legal document.
34
+
35
+ In all four the report names the rule and cleans every other class. For a repository that does
36
+ *not* want the trailer, the durable fix is prospective and lives in the harness, not in a
37
+ cleaning pass: Claude Code writes it per its own settings, so turning it off there stops it at
38
+ the source and leaves history alone.
39
+
40
+ ---
41
+
42
+ ## Part 2 — prose tells (the model decides)
43
+
44
+ A tell is a **density**, not a word. One instance is a style choice; five in three paragraphs is
45
+ a fingerprint. Never flag an instance — flag the pattern, and quote three instances as evidence.
46
+
47
+ ### The symmetric triple
48
+
49
+ ❌ *"The migration is faster, cheaper, and safer."*
50
+ ✅ *"The migration halves cutover time. It also costs less, though the saving is small enough that it should not drive the decision."*
51
+
52
+ Why: three balanced adjectives is a rhythm, not an argument. The second version ranks them and admits one is weak.
53
+
54
+ ### The not-just-X-but-Y frame
55
+
56
+ ❌ *"This is not just a schema change, it's a change in how the team reasons about ownership."*
57
+ ✅ *"The schema change forces every service to declare an owner, which is the part that will cause arguments."*
58
+
59
+ Why: the frame promises a reveal and delivers a restatement. Say the second clause and drop the first.
60
+
61
+ ### Bolded lead-ins on every bullet
62
+
63
+ ❌ Ten bullets, each opening with two bold words and a colon.
64
+ ✅ Bold the three bullets a skimmer must not miss; leave the other seven plain.
65
+
66
+ Why: when everything is emphasized, nothing is. Uniform bolding is a template, and it reads as one.
67
+
68
+ ### The hedge stack
69
+
70
+ ❌ *"It is important to note that this approach may potentially introduce some additional complexity."*
71
+ ✅ *"This adds a second write path. That is the cost."*
72
+
73
+ Why: four hedges in one sentence assert nothing. Strip hedges that qualify the hedge — keep the
74
+ one that carries a real, named uncertainty (*"we have two weeks of data"* stays).
75
+
76
+ ### Section symmetry
77
+
78
+ ❌ Six sections, each 3–4 sentences, each with a heading of the same grammatical shape.
79
+ ✅ The section that matters runs nine sentences; two others run one line each.
80
+
81
+ Why: real documents are lumpy because real subjects are. Perfect evenness is generated shape.
82
+
83
+ ### The restating conclusion
84
+
85
+ ❌ A closing paragraph that summarizes the four sections above and adds nothing.
86
+ ✅ End on the decision, the owner, and the date — or end where the argument ends.
87
+
88
+ Why: the summary-of-the-summary is filler the reader has already read.
89
+
90
+ ### Vocabulary flags
91
+
92
+ `delve` · `leverage` (as a verb) · `robust` · `seamless` · `underscores the importance of` ·
93
+ `in today's fast-paced` · `it is worth noting that` · `navigate the complexities of` ·
94
+ `a testament to` · `tapestry`.
95
+
96
+ These are flags, not bans. One `robust` in a document about error handling is fine. Three
97
+ different words from this list in one paragraph is the finding.
98
+
99
+ ### What is not a tell
100
+
101
+ - **Em dashes.** Punctuation. Only a finding at four-plus per paragraph.
102
+ - **Bullet lists.** Structure. Humans write them.
103
+ - **Correct grammar.** Consistent spelling is not evidence of anything.
104
+ - **Headings.** A document with headings is a document with headings.
105
+
106
+ Flagging any of these is how a hygiene pass loses the author's trust for the findings that were real.
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env python3
2
+ """Deterministic surface-hygiene pass over agent-written text. Stdlib only, no network.
3
+
4
+ python3 scrub.py DRAFT.md # report findings, exit 1 if any
5
+ python3 scrub.py DRAFT.md --fix # rewrite the file, report what changed
6
+ python3 scrub.py - < draft.md # read stdin, report
7
+ python3 scrub.py - --fix < draft.md # read stdin, write cleaned text to stdout
8
+ python3 scrub.py DRAFT.md --json # machine-readable findings (for a hook)
9
+ python3 scrub.py MSG.txt --medium commit # a medium where curly quotes ARE a defect
10
+ python3 scrub.py DRAFT.md --ascii --fix # flatten curly quotes and dashes regardless
11
+
12
+ Two tiers, and the split is the point:
13
+
14
+ AUTO invisible characters, non-ASCII spacing, provenance trailers, chat closers.
15
+ Removing these cannot change what the text asserts, so --fix applies them.
16
+ REVIEW homoglyphs and typographic punctuation. A blind rewrite here corrupts meaning
17
+ (a Cyrillic 'а' may be intentional; an em dash in Markdown prose is correct),
18
+ so these are reported and left alone unless the caller opts in with --ascii.
19
+
20
+ Typography is medium-dependent and the default medium is markdown, where curly quotes and
21
+ em dashes are correct typesetting rather than defects — reporting them there would bury the
22
+ findings that matter under one row per apostrophe. Pass --medium for a target that will
23
+ re-encode the text (commit, code, csv, plaintext) and they become review findings.
24
+
25
+ What this script cannot do is the other half of the job: prose tells — the cadence, the
26
+ bolded lead-in on every bullet, the hedge vocabulary — need a model pass. See SKILL.md.
27
+
28
+ Exit codes: 0 nothing left to fix · 1 findings remain · 2 usage or I/O error.
29
+ """
30
+ import argparse
31
+ import json
32
+ import re
33
+ import sys
34
+ import unicodedata
35
+
36
+ # --- AUTO tier ------------------------------------------------------------------------
37
+
38
+ # Zero-width, directional, and formatting characters. They survive copy-paste, render as
39
+ # nothing, and break grep, diff, and anything that compares strings.
40
+ INVISIBLE = {
41
+ "​": "ZERO WIDTH SPACE",
42
+ "‌": "ZERO WIDTH NON-JOINER",
43
+ "‍": "ZERO WIDTH JOINER",
44
+ "⁠": "WORD JOINER",
45
+ "": "ZERO WIDTH NO-BREAK SPACE (BOM)",
46
+ "­": "SOFT HYPHEN",
47
+ "
": "LINE SEPARATOR",
48
+ "
": "PARAGRAPH SEPARATOR",
49
+ "‪": "LEFT-TO-RIGHT EMBEDDING",
50
+ "‫": "RIGHT-TO-LEFT EMBEDDING",
51
+ "‬": "POP DIRECTIONAL FORMATTING",
52
+ "‭": "LEFT-TO-RIGHT OVERRIDE",
53
+ "‮": "RIGHT-TO-LEFT OVERRIDE",
54
+ "⁦": "LEFT-TO-RIGHT ISOLATE",
55
+ "⁧": "RIGHT-TO-LEFT ISOLATE",
56
+ "⁨": "FIRST STRONG ISOLATE",
57
+ "⁩": "POP DIRECTIONAL ISOLATE",
58
+ }
59
+
60
+ # Spaces that are not U+0020. They look identical and silently break word-splitting,
61
+ # Markdown table alignment, and YAML.
62
+ SPACING = {
63
+ " ": "NO-BREAK SPACE",
64
+ " ": "NARROW NO-BREAK SPACE",
65
+ " ": "FIGURE SPACE",
66
+ " ": "THIN SPACE",
67
+ " ": "HAIR SPACE",
68
+ " ": "EN SPACE",
69
+ " ": "EM SPACE",
70
+ " ": "IDEOGRAPHIC SPACE",
71
+ }
72
+
73
+ # Whole-line provenance trailers added by an agent harness, not by the author.
74
+ PROVENANCE = [
75
+ ("co-authored-by trailer", re.compile(r"^\s*co-authored-by:\s*claude\b.*$", re.I)),
76
+ ("generated-with footer", re.compile(r"^\s*(?:\S\s*)?generated with \[?claude code\]?.*$", re.I)),
77
+ ("generated-with footer", re.compile(r"^\s*(?:\S\s*)?generated (?:with|by) \[[^\]]+\]\(https://claude\.com[^)]*\).*$", re.I)),
78
+ # A signature is a dash, a name, and nothing else. A plain "-" opener is excluded and the
79
+ # tail is capped: "- Claude Code project settings: …" is a bullet, not a sign-off, and a
80
+ # cleaner that eats real list items loses the author's trust for the findings that were real.
81
+ ("assistant signature", re.compile(r"^\s*(?:—|–|--)\s*claude\b[\w.()\s-]{0,24}$", re.I)),
82
+ ]
83
+
84
+ # A trailing paragraph that addresses the reader as a chat partner. Matched only as the
85
+ # final block of the text, and only when it opens with one of these — a conservative list,
86
+ # because a false positive here deletes real content.
87
+ CLOSER_OPENERS = re.compile(
88
+ r"^(?:let me know (?:if|whether)|(?:i )?hope (?:this|that) helps|"
89
+ r"would you like me to|want me to|happy to (?:help|expand|adjust)|"
90
+ r"feel free to (?:ask|reach out|let me know)|"
91
+ r"if you(?:'| a)?d like(?:,| )?\s*i can)\b",
92
+ re.I,
93
+ )
94
+
95
+ # --- REVIEW tier ----------------------------------------------------------------------
96
+
97
+ # Non-ASCII letters sitting inside an otherwise-ASCII word: the classic paste artifact
98
+ # (and, occasionally, a deliberate spoof). Never auto-fixed — the correct ASCII letter is
99
+ # a guess, and the word may be genuinely foreign.
100
+ WORD = re.compile(r"[^\W\d_]+", re.UNICODE)
101
+
102
+ TYPOGRAPHY = {
103
+ "‘": ("'", "LEFT SINGLE QUOTATION MARK"),
104
+ "’": ("'", "RIGHT SINGLE QUOTATION MARK"),
105
+ "“": ('"', "LEFT DOUBLE QUOTATION MARK"),
106
+ "”": ('"', "RIGHT DOUBLE QUOTATION MARK"),
107
+ "–": ("-", "EN DASH"),
108
+ "—": ("--", "EM DASH"),
109
+ "…": ("...", "HORIZONTAL ELLIPSIS"),
110
+ "′": ("'", "PRIME"),
111
+ "″": ('"', "DOUBLE PRIME"),
112
+ }
113
+
114
+
115
+ def _positions(text):
116
+ """Offset -> (line, col), both 1-based. Computed once; the scan is O(n)."""
117
+ line, col, out = 1, 1, []
118
+ for ch in text:
119
+ out.append((line, col))
120
+ if ch == "\n":
121
+ line, col = line + 1, 1
122
+ else:
123
+ col += 1
124
+ return out
125
+
126
+
127
+ def _finding(cls, tier, line, col, detail, excerpt):
128
+ return {"class": cls, "tier": tier, "line": line, "col": col,
129
+ "detail": detail, "excerpt": excerpt}
130
+
131
+
132
+ def _excerpt(text, offset, width=36):
133
+ lo = text.rfind("\n", 0, offset) + 1
134
+ hi = text.find("\n", offset)
135
+ hi = len(text) if hi == -1 else hi
136
+ frag = text[lo:hi]
137
+ rel = offset - lo
138
+ start = max(0, rel - width // 2)
139
+ snippet = frag[start:start + width].replace("\t", " ")
140
+ return ("…" if start > 0 else "") + snippet.strip() + ("…" if start + width < len(frag) else "")
141
+
142
+
143
+ # Media where typographic punctuation survives and is correct. Everywhere else it is a
144
+ # defect waiting for a re-encode.
145
+ TYPOGRAPHY_OK = {"markdown", "html"}
146
+
147
+
148
+ def scan(text, ascii_mode=False, medium="markdown"):
149
+ """Return (findings, cleaned_text). cleaned_text applies the AUTO tier, plus the
150
+ REVIEW typography tier when ascii_mode is on. Findings describe the ORIGINAL text."""
151
+ findings = []
152
+ report_typography = ascii_mode or medium not in TYPOGRAPHY_OK
153
+ pos = _positions(text)
154
+
155
+ # --- character-level classes
156
+ for i, ch in enumerate(text):
157
+ line, col = pos[i]
158
+ if ch in INVISIBLE:
159
+ findings.append(_finding("invisible", "auto", line, col,
160
+ f"U+{ord(ch):04X} {INVISIBLE[ch]} — renders as nothing",
161
+ _excerpt(text, i)))
162
+ elif ch in SPACING:
163
+ findings.append(_finding("spacing", "auto", line, col,
164
+ f"U+{ord(ch):04X} {SPACING[ch]} — not a plain space",
165
+ _excerpt(text, i)))
166
+ elif ch in TYPOGRAPHY and report_typography:
167
+ repl, name = TYPOGRAPHY[ch]
168
+ findings.append(_finding("typography", "auto" if ascii_mode else "review", line, col,
169
+ f"U+{ord(ch):04X} {name} — flatten to {repl!r} for plain-text targets",
170
+ _excerpt(text, i)))
171
+
172
+ # --- homoglyphs: a non-ASCII letter inside a word that is otherwise ASCII
173
+ for m in WORD.finditer(text):
174
+ word = m.group(0)
175
+ # A homoglyph is a LETTER that impersonates another letter. Superscripts and symbols
176
+ # inside a word — the ² in "O(n²)" — are typography, and flagging them is the noise
177
+ # that teaches an author to ignore the findings that were real.
178
+ odd = [(k, c) for k, c in enumerate(word)
179
+ if ord(c) > 127 and unicodedata.category(c).startswith("L")]
180
+ if not odd or len(odd) == len(word):
181
+ continue
182
+ for k, c in odd:
183
+ off = m.start() + k
184
+ line, col = pos[off]
185
+ try:
186
+ name = unicodedata.name(c)
187
+ except ValueError:
188
+ name = "UNNAMED"
189
+ findings.append(_finding("homoglyph", "review", line, col,
190
+ f"U+{ord(c):04X} {name} inside ASCII word {word!r} — verify before replacing",
191
+ _excerpt(text, off)))
192
+
193
+ # --- line-level provenance
194
+ drop_lines = set()
195
+ for n, raw in enumerate(text.split("\n"), start=1):
196
+ for label, pattern in PROVENANCE:
197
+ if pattern.match(raw):
198
+ findings.append(_finding("provenance", "auto", n, 1,
199
+ f"{label} — harness-added, not authored", raw.strip()))
200
+ drop_lines.add(n)
201
+ break
202
+
203
+ cleaned = "\n".join(l for n, l in enumerate(text.split("\n"), start=1) if n not in drop_lines)
204
+ if drop_lines:
205
+ cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
206
+ cleaned = re.sub(r"\n+\Z", "\n" if text.endswith("\n") else "", cleaned)
207
+
208
+ # --- trailing chat closer, measured against the text as it stands after line drops
209
+ trimmed = cleaned.rstrip()
210
+ blocks = re.split(r"\n\s*\n", trimmed)
211
+ if len(blocks) > 1 and CLOSER_OPENERS.match(blocks[-1].strip()):
212
+ tail = blocks[-1]
213
+ idx = trimmed.rfind(tail)
214
+ findings.append(_finding("closer", "auto", trimmed.count("\n", 0, max(idx, 0)) + 1, 1,
215
+ "trailing chat closer — addresses a chat partner, not a reader",
216
+ " ".join(tail.split())[:80]))
217
+ cleaned = "\n\n".join(blocks[:-1]).rstrip() + ("\n" if text.endswith("\n") else "")
218
+
219
+ # --- character substitutions
220
+ for ch in INVISIBLE:
221
+ cleaned = cleaned.replace(ch, "\n" if ch in ("
", "
") else "")
222
+ for ch in SPACING:
223
+ cleaned = cleaned.replace(ch, " ")
224
+ if ascii_mode:
225
+ for ch, (repl, _) in TYPOGRAPHY.items():
226
+ cleaned = cleaned.replace(ch, repl)
227
+
228
+ findings.sort(key=lambda f: (f["line"], f["col"]))
229
+ return findings, cleaned
230
+
231
+
232
+ TIER_ORDER = {"auto": 0, "review": 1}
233
+
234
+
235
+ def report(path, findings, fixed, ascii_mode, medium):
236
+ auto = [f for f in findings if f["tier"] == "auto"]
237
+ review = [f for f in findings if f["tier"] == "review"]
238
+ head = (f"{path} [{medium}]: {len(findings)} finding(s) — "
239
+ f"{len(auto)} auto, {len(review)} review")
240
+ lines = [head, "=" * len(head)]
241
+ for tier, group, note in (
242
+ ("auto", auto, "applied" if fixed else "safe to apply with --fix"),
243
+ ("review", review, "left alone — judgment required" + ("" if ascii_mode else "; --ascii flattens typography")),
244
+ ):
245
+ if not group:
246
+ continue
247
+ lines.append(f"\n[{tier}] {len(group)} — {note}")
248
+ for f in group:
249
+ lines.append(f" {str(f['line']) + ':' + str(f['col']):<9} {f['class']:<11} {f['detail']}")
250
+ lines.append(f" {'':<9} {'':<11} {f['excerpt']}")
251
+ if not findings:
252
+ lines = [f"{path} [{medium}]: clean"]
253
+ return "\n".join(lines)
254
+
255
+
256
+ def main():
257
+ ap = argparse.ArgumentParser(
258
+ description="Report and optionally remove machine artifacts from agent-written text.")
259
+ ap.add_argument("paths", nargs="+", help="files to scan, or - for stdin")
260
+ ap.add_argument("--fix", action="store_true",
261
+ help="apply the auto tier (rewrites files in place; stdin goes to stdout)")
262
+ ap.add_argument("--medium", default="markdown",
263
+ choices=["markdown", "html", "plaintext", "commit", "code", "csv"],
264
+ help="where the text is going; decides whether typographic punctuation "
265
+ "is a defect (default: markdown, where it is not)")
266
+ ap.add_argument("--ascii", action="store_true",
267
+ help="report AND flatten curly quotes, dashes, and ellipses whatever the medium")
268
+ ap.add_argument("--json", action="store_true", help="emit findings as JSON")
269
+ ap.add_argument("--quiet", action="store_true", help="exit code only, no report")
270
+ args = ap.parse_args()
271
+
272
+ if args.json and args.quiet:
273
+ ap.error("--json and --quiet are mutually exclusive")
274
+
275
+ results, remaining = [], 0
276
+ for path in args.paths:
277
+ try:
278
+ text = sys.stdin.read() if path == "-" else open(path, encoding="utf-8").read()
279
+ except OSError as e:
280
+ print(f"scrub.py: cannot read {path}: {e}", file=sys.stderr)
281
+ return 2
282
+ except UnicodeDecodeError:
283
+ print(f"scrub.py: {path} is not UTF-8 text", file=sys.stderr)
284
+ return 2
285
+
286
+ findings, cleaned = scan(text, ascii_mode=args.ascii, medium=args.medium)
287
+ if args.fix and cleaned != text:
288
+ if path == "-":
289
+ sys.stdout.write(cleaned)
290
+ else:
291
+ try:
292
+ with open(path, "w", encoding="utf-8") as f:
293
+ f.write(cleaned)
294
+ except OSError as e:
295
+ print(f"scrub.py: cannot write {path}: {e}", file=sys.stderr)
296
+ return 2
297
+ elif args.fix and path == "-":
298
+ sys.stdout.write(cleaned)
299
+
300
+ left = [f for f in findings if f["tier"] == "review" or not args.fix]
301
+ remaining += len(left)
302
+ results.append({"path": path, "findings": findings,
303
+ "fixed": args.fix and cleaned != text})
304
+
305
+ if args.json:
306
+ print(json.dumps({"results": results, "remaining": remaining}, indent=2))
307
+ elif not args.quiet:
308
+ out = sys.stderr if (args.fix and "-" in args.paths) else sys.stdout
309
+ for r in results:
310
+ print(report(r["path"], r["findings"], r["fixed"], args.ascii, args.medium), file=out)
311
+ return 1 if remaining else 0
312
+
313
+
314
+ if __name__ == "__main__":
315
+ sys.exit(main())