From 489ff4eac09cb7a9269c598eff3deba2f0fa1ecf Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 08:10:32 -0400 Subject: [PATCH 01/22] Add v0.3 design: portable skill across Claude Code, Codex, Cursor, Hermes, Claude Desktop Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- .../design/2026-09-14-humanize-v0.3-design.md | 280 ++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 docs/design/2026-09-14-humanize-v0.3-design.md diff --git a/docs/design/2026-09-14-humanize-v0.3-design.md b/docs/design/2026-09-14-humanize-v0.3-design.md new file mode 100644 index 0000000..9c9f92f --- /dev/null +++ b/docs/design/2026-09-14-humanize-v0.3-design.md @@ -0,0 +1,280 @@ +# humanize v0.3 — portable skill (design) + +Date: 2026-09-14 +Status: draft for review (revision 1). +Builds on: `2026-09-14-humanize-v0.2-design.md` (v0.2, shipped as 0.2.0 at main 00a3d74). +Evidence: verified harness research run 2026-09-14 (Codex CLI 0.154.0 empirically; Cursor from +primary docs; Hermes Agent v0.11.0 installed + `origin/main` source), the superpowers 6.3.0 and +agentcairn packaging layouts, and local inspection of Claude Desktop 1.52386.6 (Cowork). + +## 0. Goal and non-goals + +**Goal.** The same `humanize` skill installs and runs on Claude Code, Codex CLI, Cursor, Hermes +Agent, and the Claude Desktop app, using each harness's own installer. No behaviour of the +scanner or the skill's method changes; only packaging, paths, and one portable wording pass. + +**Decision (user, 2026-09-14): native installers + compliance tests.** No custom installer CLI, +no setup meta-skill. The harnesses have converged on the Agent Skills folder contract +(`SKILL.md` + `scripts/` + `references/`) and on `~/.agents/skills/` as a shared root, so the +work is to *be* a compliant skill and to prove it. + +**Non-goals (v0.3):** Gemini CLI, OpenCode, Kimi, Antigravity, Devin (the layout admits them; +no untested rows); an installer CLI; a `humanize-setup` skill; any scanner change; website-copy +extraction (v0.4, separate design). + +## 1. Repository layout + +The plugin moves from `plugins/humanize/` to the repository root — the layout Codex, Cursor, +Hermes taps, and the skills.sh CLI discover without flags, and the one superpowers ships. + +``` +plugin.json Agent Plugins 1.0 manifest (Codex, Cursor, Hermes main) +.claude-plugin/plugin.json Claude Code (also accepted by Codex) +.claude-plugin/marketplace.json Claude Code marketplace; source "./" +.codex-plugin/plugin.json Codex canonical manifest + marketplace `interface` block +skills/humanize/SKILL.md the skill (unchanged location relative to `skills/`) +skills/humanize/references/ principles, tells, fingerprints, SOURCES +skills/humanize/scripts/surface_scan.py +tests/ tools/ data/ docs/ unchanged +``` + +`git mv plugins/humanize/skills skills`; `git mv plugins/humanize/.claude-plugin/plugin.json +.claude-plugin/plugin.json`; delete `plugins/`. Every path that named `plugins/humanize/…` +changes: `CLAUDE.md` commands, README tree, `tests/test_manifests.py`, `tests/test_fixtures.py`, +`tests/test_surface_scan.py` (script import path), `tools/gen_tell_scaffold.py` (output dir), +`.cursor/BUGBOT.md`, `.pre-commit-config.yaml` (if it filters by path), and the smoke command +(`--plugin-dir .`). + +### 1.1 Manifests (verbatim; `` is the existing plugin description string) + +`plugin.json` — Agent Plugins 1.0. Cursor requires the `$schema` identifier; Codex 0.154.0 reads +this "portable root format" and discovers `skills/` automatically; Hermes `origin/main` installs +it with `hermes plugins install`. Fields are the subset shared with the Claude manifest. + +```json +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "humanize", + "version": "0.3.0", + "description": "", + "author": { "name": "ccf" }, + "license": "MIT", + "homepage": "https://github.com/ccf/humanize", + "repository": "https://github.com/ccf/humanize", + "keywords": ["writing", "prose", "editing", "ai-detection", "storyscope", "humanize"] +} +``` + +`.claude-plugin/plugin.json` — unchanged fields, `version` 0.3.0. + +`.claude-plugin/marketplace.json` — unchanged except `"source": "./"`, both `version` fields +0.3.0, `strict: true` kept. (superpowers' root `.claude-plugin/` holds both files the same way.) + +`.codex-plugin/plugin.json` — Codex's bundled `validate_plugin.py` accepts only this layout, and +the `interface` block is what its marketplace UI renders: + +```json +{ + "name": "humanize", + "version": "0.3.0", + "description": "", + "author": { "name": "ccf" }, + "homepage": "https://github.com/ccf/humanize", + "repository": "https://github.com/ccf/humanize", + "license": "MIT", + "keywords": ["writing", "prose", "editing", "ai-detection", "storyscope", "humanize"], + "skills": "./skills/", + "interface": { + "displayName": "humanize", + "shortDescription": "Audit prose for AI tells and rewrite it to read as human writing.", + "longDescription": "Scans text for the surface, style, and grammar features that separate AI from human prose (StoryScope and register studies), quotes the evidence, and rewrites without converging on a new default voice.", + "developerName": "ccf", + "category": "Developer Tools", + "capabilities": ["Read", "Write"], + "defaultPrompt": ["Humanize this draft.", "Audit this text for AI tells, don't rewrite it."], + "websiteURL": "https://github.com/ccf/humanize" + } +} +``` + +No `.cursor-plugin/` (Cursor accepts Agent Plugins; the Cursor-only manifest adds nothing we +ship) and no `.hermes-plugin/` (not a Hermes convention; Hermes reads `plugin.json` on main and +plain skill folders on every version). No `hooks`, `commands`, `agents`, or MCP anywhere. + +## 2. SKILL.md — portable wording, 150 lines exactly + +Three edits, line-neutral in total. Frontmatter keeps `argument-hint` (Claude Code renders it; +Codex and Cursor ignore it at runtime; Codex's linter warns — documented, accepted). No other +frontmatter key is added. + +**A. Invocation (lines 25–28).** The heading stays. Replace the two-line paragraph with: + +``` +Applies only when the user typed `/humanize …` (Codex: `$humanize …`). On +auto-invoke there are no arguments: skip this section. Options below also +work as plain words in a request ("audit only", "this is fiction"). +``` +(+1 line.) The `$ARGUMENTS` line and its literal-text fallback stay: Codex, Cursor, and Hermes +substitute nothing, so the fallback is what makes the section safe there. + +**B. Scan command (lines 69–75).** The fenced command becomes +`python3 scripts/surface_scan.py --text `, and the paragraph under it gains one sentence: +"Run it from this skill's folder — the directory holding this SKILL.md (`${HERMES_SKILL_DIR}` on +Hermes)." (+1 line.) `${CLAUDE_PLUGIN_ROOT}` disappears from the skill. + +**C. Non-text sources (lines 77–84, 8 lines → 6).** Replace with: + +``` +**Non-text sources** (`.docx`, `.pdf`, `.pptx`, `.odt`, `.rtf`): the scanner reads +plain text only. Extract first with the harness's document skills (Anthropic's +`docx`/`pdf` skills ship with Claude Code's `document-skills` plugin and with +Claude Desktop) to write a temp `.md`, then scan and audit that. Without them, +read the file yourself if you can and write the text to a temp file; otherwise +ask for a text export. Deliver Markdown; hand a Word file back via `docx`. +``` +(−2 lines.) The `/plugin …` commands leave the skill; they live in the README. + +Everything else in SKILL.md is untouched. `wc -l` = 150 (guarded by `test_skill_line_budget`). + +## 3. Compliance tests — `tests/test_portability.py` + +Pure-pytest, no network, run in CI with everything else. Constants come from the verified +harness limits (Codex `quick_validate.py`, Cursor docs, agentskills.io spec). + +1. **Frontmatter.** Keys ⊆ `{name, description, license, allowed-tools, metadata, argument-hint}`; + `name` matches `^[a-z0-9]+(-[a-z0-9]+)*$`, ≤ 64 chars, and equals the skill's folder name + (Cursor hard-requires this); `description` ≤ 1024 chars, contains neither `<` nor `>`, does + not start with `[TODO:`. +2. **No harness-specific paths.** `CLAUDE_PLUGIN_ROOT` appears nowhere under `skills/`; + SKILL.md contains the literal-`$ARGUMENTS` fallback sentence. +3. **Support files are referenced.** Every `skills/humanize/references/*.md` except `SOURCES.md` + and `scripts/surface_scan.py` are named by relative path in SKILL.md — Hermes URL installs + fetch only files the SKILL.md names. +4. **Guard-clean scripts.** No `subprocess`, `os.environ`, `os.system`, `eval(`, `exec(`, + `socket`, `urllib`, `requests` in `skills/*/scripts/*.py` (Hermes's `skills_guard.py` + patterns; also our own no-network invariant). +5. **Manifests agree.** `name` is `humanize` in all four manifests; `version` is identical in + `plugin.json`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, and both + `marketplace.json` version fields, and equals the newest `## [x.y.z]` heading in + `CHANGELOG.md`; `plugin.json.$schema` is exactly the 1.0.0 URL; `.codex-plugin` `skills` + resolves to a directory containing `humanize/SKILL.md`; marketplace `source` resolves to a + directory containing `.claude-plugin/plugin.json`. + +`tests/test_manifests.py` keeps its current checks at the new paths. + +## 4. Documentation + +**README — "Install" replaces the current single-harness instructions.** One table, then a +paragraph of notes per harness. Commands are the verified ones: + +| Harness | Install | Verify | Invoke | +|---|---|---|---| +| Claude Code | `/plugin marketplace add ccf/humanize` · `/plugin install humanize@humanize` | `claude plugin list` | `/humanize` or auto | +| Codex CLI | `codex plugin marketplace add ccf/humanize` · `codex plugin add humanize@humanize` — or `cp -R skills/humanize ~/.agents/skills/` | `codex debug prompt-input "hi"` lists it | `$humanize …` or auto | +| Cursor | `cp -R skills/humanize ~/.cursor/skills/` (user; syncs to Cloud Agents) or `.cursor/skills/` (project) — or `agent plugin marketplace add https://github.com/ccf/humanize` | skill appears in `/` menu | `/humanize` or auto | +| Hermes Agent | `hermes skills install ccf/humanize/humanize --category writing`, then `/reload-skills` in a running session | `hermes skills list` | `/humanize …` or auto | +| Any Agent-Skills harness | `npx skills add ccf/humanize` (→ `~/.agents/skills/`) or `cp -R skills/humanize ~/.agents/skills/` | harness-specific | harness-specific | +| Claude Desktop | see §6 | — | describe the task or say "humanize this" | + +Notes carry the gotchas the research surfaced: Codex reads `/.agents/skills/` but not +`.claude/skills/`; Codex caps the injected skills catalog at 2% of context; Cursor only syncs +`~/.cursor/skills/` to Cloud Agents; Hermes scans installed scripts (ours is stdlib and +guard-clean) and `${HERMES_SKILL_DIR}` is a host path — a remote terminal backend (docker, +modal, ssh) will not see the script, so Hermes users on remote backends get the audit without +the scan table; Codex's linter warns about `argument-hint`, which is Claude-Code-only and inert +elsewhere. + +**CLAUDE.md** — Commands section paths; new invariants: "the skill is harness-agnostic: no +`${CLAUDE_PLUGIN_ROOT}` or other harness variables, no harness-specific install commands inside +`skills/`; `argument-hint` is the only non-spec frontmatter key"; "version lives in four +manifests plus CHANGELOG — `tests/test_portability.py` fails on drift"; layout note. Workflow +section adds the smoke matrix as the pre-tag acceptance step. **BUGBOT.md** mirrors the two +invariants. + +## 5. Smoke matrix — `tools/smoke_harnesses.sh` (local, not CI) + +For each harness whose CLI is on `PATH`, install the working-tree skill in an isolated way, run +the audit-only fixture headlessly, and assert the four expected rows (`trailing participial +clause`, `verbatim repetition`, `container-noun phrase`, `safety disclaimer opener`) appear and +no nominalization row does. + +- Claude Code: `claude plugin disable humanize@humanize; claude -p "/humanize + tests/fixtures/ai_report.txt --audit-only" --plugin-dir . --output-format text + --allowedTools "Bash,Read,Glob,Grep"; claude plugin enable humanize@humanize` (existing). +- Codex: copy `skills/humanize` into a temp project's `.agents/skills/`, `codex exec` the same + request from that directory (project scope; nothing written under `$HOME`). +- Cursor: same temp project, `.cursor/skills/`, `agent -p …` (the CLI must be current; the + script prints its version). +- Hermes: install into `~/.hermes/skills/writing/humanize` via `cp -R`, run `hermes chat` in + non-interactive mode with the request, then remove the directory. (No project scope in + v0.11.0.) + +The script prints a PASS/FAIL line per harness and writes each audit table to +`docs/acceptance/v0.3/.md` — the transcripts a release needs, small enough to commit. +Exact non-interactive flags for `codex exec`, `agent -p`, and `hermes chat` are pinned in the +plan from `--help` output. + +## 6. Claude Desktop + +Local evidence: Claude Desktop 1.52386.6 runs Cowork with a bundled Claude Code 2.1.x and a +local VM, installs skills as plugins in the Claude Code plugin format (`.claude-plugin/plugin.json` ++ `skills/`), and ships Anthropic's `docx`/`pdf`/`pptx`/`xlsx` skills — the ones §2C names. + +Two delivery paths, both shipped: + +1. **Cowork plugin (preferred).** The v0.3 repo is already a valid Cowork plugin. The smoke + matrix's last step is manual: add `ccf/humanize` as a marketplace in Cowork (Customize → + Plugins), install, and run the fixture audit in a Cowork session. If Cowork accepts custom + marketplaces, README §4 gains the Cowork row with those clicks; if it only lists Anthropic's + directory, the row says so and points to path 2. +2. **claude.ai skill upload (always available).** `tools/package_skill_zip.py` (stdlib) writes + `dist/humanize-skill-.zip` containing `humanize/SKILL.md`, `humanize/references/`, + and `humanize/scripts/surface_scan.py` — the folder-rooted layout Anthropic's skill examples + use — with the version read from `plugin.json`. README documents Settings → Capabilities → + Skills → upload, and that in claude.ai the skill is triggered by its description (no slash + command) and the scanner runs in Claude's code-execution sandbox (stdlib only, which we are). + The zip is attached to the GitHub release. + +Both paths use the same SKILL.md; nothing Desktop-specific enters the skill. + +## 7. Versioning and release + +- Version **0.3.0** in the four manifests; `CHANGELOG.md` `[0.3.0]` with Added (Agent Plugins + manifest, Codex manifest, portability tests, smoke matrix, skill-zip packager, install table), + Changed (repo layout; SKILL.md portable wording; document-skills instructions moved to README), + and a **Migration** note: existing Claude Code installs run + `claude plugin marketplace update humanize && claude plugin update humanize@humanize`; the + marketplace `source` moved from `./plugins/humanize` to `./`. +- Release checklist (CLAUDE.md Workflow): CI green → smoke matrix PASS on every locally available + harness, transcripts committed → PR rebase-merged → tag `v0.3.0` → `gh release create v0.3.0 + dist/humanize-skill-0.3.0.zip` → plugin update on this machine. + +## 8. Acceptance + +1. `uv run pytest -q -W error` passes (existing 100 + portability tests) on 3.9 and 3.13; + `claude plugin validate --strict .` passes at the new layout; `wc -l skills/humanize/SKILL.md` + = 150. +2. Smoke matrix PASS on Claude Code, Codex, Cursor, and Hermes on this machine, with transcripts + under `docs/acceptance/v0.3/`. +3. `codex plugin marketplace add ccf/humanize && codex plugin add humanize@humanize` installs + from GitHub and the skill surfaces as `humanize` (or `humanize:humanize`) in `codex debug + prompt-input "hi"`. +4. `hermes skills install ccf/humanize/humanize` pulls SKILL.md, all five referenced references, + and the script; `hermes skills list` shows it with a clean guard verdict. +5. Cowork: one recorded attempt, outcome documented in the README either way; skill zip uploads + and triggers on a pasted paragraph in claude.ai. +6. Existing Claude Code install upgrades 0.2.0 → 0.3.0 via the migration command with one skill + registered. + +## 9. Risks and open points (settled by the smoke matrix, not by more research) + +- Manifest precedence on Codex when `.codex-plugin/`, `.claude-plugin/`, and root `plugin.json` + coexist — expected: `.codex-plugin` wins; verified by acceptance 3. +- Agent Plugins 1.0 schema strictness on unknown fields — the manifest uses only fields shared + with the Claude/Codex manifests; if Cursor rejects any, drop it to `{$schema, name, version, + description}`. +- The `agent` CLI on this machine is 2026.01.23; the smoke matrix updates it first. +- skills.sh CLI (`npx skills add`) discovery of `skills/` at repo root — verified once in the + smoke matrix; the row is removed if it fails. +- Claude Code marketplace `source: "./"` — validated by `claude plugin validate --strict .` and + by acceptance 6. From 811e428bd9c932fb4eef642e1c640407a62811cb Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 08:13:17 -0400 Subject: [PATCH 02/22] v0.3 design: fold in verified Claude Desktop findings Drop argument-hint (hard error outside Claude Code); Cowork custom marketplaces verified; six-field frontmatter allow-list in the portability test. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- .../design/2026-09-14-humanize-v0.3-design.md | 84 ++++++++++++------- 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/docs/design/2026-09-14-humanize-v0.3-design.md b/docs/design/2026-09-14-humanize-v0.3-design.md index 9c9f92f..49d1e18 100644 --- a/docs/design/2026-09-14-humanize-v0.3-design.md +++ b/docs/design/2026-09-14-humanize-v0.3-design.md @@ -103,9 +103,12 @@ plain skill folders on every version). No `hooks`, `commands`, `agents`, or MCP ## 2. SKILL.md — portable wording, 150 lines exactly -Three edits, line-neutral in total. Frontmatter keeps `argument-hint` (Claude Code renders it; -Codex and Cursor ignore it at runtime; Codex's linter warns — documented, accepted). No other -frontmatter key is added. +Four edits, line-neutral in total. **Frontmatter drops `argument-hint`** (−1 line): outside Claude +Code it is a hard error — claude.ai uploads, the Skills API, and `package_skill.py` reject any +key beyond the six Agent Skills fields (`Unexpected key(s) in SKILL.md frontmatter: argument-hint. +Allowed properties are: allowed-tools, compatibility, description, license, metadata, name`), and +Codex's validator rejects it too. Claude Code loses only the placeholder text in its `/` menu; +`/humanize ` still works. Frontmatter is then exactly `name` and `description`. **A. Invocation (lines 25–28).** The heading stays. Replace the two-line paragraph with: @@ -122,29 +125,33 @@ substitute nothing, so the fallback is what makes the section safe there. "Run it from this skill's folder — the directory holding this SKILL.md (`${HERMES_SKILL_DIR}` on Hermes)." (+1 line.) `${CLAUDE_PLUGIN_ROOT}` disappears from the skill. -**C. Non-text sources (lines 77–84, 8 lines → 6).** Replace with: +**C. Non-text sources (lines 77–84, 8 lines → 7).** Replace with: ``` **Non-text sources** (`.docx`, `.pdf`, `.pptx`, `.odt`, `.rtf`): the scanner reads plain text only. Extract first with the harness's document skills (Anthropic's `docx`/`pdf` skills ship with Claude Code's `document-skills` plugin and with -Claude Desktop) to write a temp `.md`, then scan and audit that. Without them, -read the file yourself if you can and write the text to a temp file; otherwise -ask for a text export. Deliver Markdown; hand a Word file back via `docx`. +Claude Desktop) to write a temp `.md`, then scan and audit that. In claude.ai the +file is an attachment: convert it in the sandbox (pandoc, pdfplumber). Without +document skills, read the file yourself if you can and write the text to a temp +file; otherwise ask for a text export. Deliver Markdown; a Word file via `docx`. ``` -(−2 lines.) The `/plugin …` commands leave the skill; they live in the README. +(−1 line.) The `/plugin …` commands leave the skill; they live in the README. -Everything else in SKILL.md is untouched. `wc -l` = 150 (guarded by `test_skill_line_budget`). +Net: −1 (frontmatter) +1 (A) +1 (B) −1 (C) = 0. Everything else in SKILL.md is untouched. +`wc -l` = 150 (guarded by `test_skill_line_budget`). No `!` command injection or other Claude +Code-only body features are used (none exist today; the portability test guards the paths). ## 3. Compliance tests — `tests/test_portability.py` Pure-pytest, no network, run in CI with everything else. Constants come from the verified harness limits (Codex `quick_validate.py`, Cursor docs, agentskills.io spec). -1. **Frontmatter.** Keys ⊆ `{name, description, license, allowed-tools, metadata, argument-hint}`; - `name` matches `^[a-z0-9]+(-[a-z0-9]+)*$`, ≤ 64 chars, and equals the skill's folder name - (Cursor hard-requires this); `description` ≤ 1024 chars, contains neither `<` nor `>`, does - not start with `[TODO:`. +1. **Frontmatter.** Keys ⊆ `{name, description, license, compatibility, metadata, allowed-tools}` + — the Agent Skills spec set and nothing else (so `argument-hint` can never return); `name` + matches `^[a-z0-9]+(-[a-z0-9]+)*$`, ≤ 64 chars, contains neither `claude` nor `anthropic`, + and equals the skill's folder name (Cursor hard-requires this); `description` ≤ 1024 chars, + contains neither `<` nor `>`, does not start with `[TODO:`. 2. **No harness-specific paths.** `CLAUDE_PLUGIN_ROOT` appears nowhere under `skills/`; SKILL.md contains the literal-`$ARGUMENTS` fallback sentence. 3. **Support files are referenced.** Every `skills/humanize/references/*.md` except `SOURCES.md` @@ -174,15 +181,16 @@ paragraph of notes per harness. Commands are the verified ones: | Cursor | `cp -R skills/humanize ~/.cursor/skills/` (user; syncs to Cloud Agents) or `.cursor/skills/` (project) — or `agent plugin marketplace add https://github.com/ccf/humanize` | skill appears in `/` menu | `/humanize` or auto | | Hermes Agent | `hermes skills install ccf/humanize/humanize --category writing`, then `/reload-skills` in a running session | `hermes skills list` | `/humanize …` or auto | | Any Agent-Skills harness | `npx skills add ccf/humanize` (→ `~/.agents/skills/`) or `cp -R skills/humanize ~/.agents/skills/` | harness-specific | harness-specific | -| Claude Desktop | see §6 | — | describe the task or say "humanize this" | +| Claude Desktop — Cowork | Customize → Plugins → Add from repository `ccf/humanize` → install `humanize` | plugin listed under Customize → Plugins | `/` or `+` picker, or auto | +| Claude Desktop — chat (claude.ai) | Customize → Skills → Upload `humanize-skill-.zip` (from the GitHub release); "Code execution and file creation" must be on | skill listed under Customize → Skills | auto (description match); no slash command | Notes carry the gotchas the research surfaced: Codex reads `/.agents/skills/` but not `.claude/skills/`; Codex caps the injected skills catalog at 2% of context; Cursor only syncs `~/.cursor/skills/` to Cloud Agents; Hermes scans installed scripts (ours is stdlib and guard-clean) and `${HERMES_SKILL_DIR}` is a host path — a remote terminal backend (docker, modal, ssh) will not see the script, so Hermes users on remote backends get the audit without -the scan table; Codex's linter warns about `argument-hint`, which is Claude-Code-only and inert -elsewhere. +the scan table; in claude.ai chat the skill is description-triggered and the scanner runs in +Claude's sandbox; Cowork plugin updates require a `version` bump to be picked up. **CLAUDE.md** — Commands section paths; new invariants: "the skill is harness-agnostic: no `${CLAUDE_PLUGIN_ROOT}` or other harness variables, no harness-specific install commands inside @@ -220,22 +228,28 @@ Local evidence: Claude Desktop 1.52386.6 runs Cowork with a bundled Claude Code local VM, installs skills as plugins in the Claude Code plugin format (`.claude-plugin/plugin.json` + `skills/`), and ships Anthropic's `docx`/`pdf`/`pptx`/`xlsx` skills — the ones §2C names. -Two delivery paths, both shipped: - -1. **Cowork plugin (preferred).** The v0.3 repo is already a valid Cowork plugin. The smoke - matrix's last step is manual: add `ccf/humanize` as a marketplace in Cowork (Customize → - Plugins), install, and run the fixture audit in a Cowork session. If Cowork accepts custom - marketplaces, README §4 gains the Cowork row with those clicks; if it only lists Anthropic's - directory, the row says so and points to path 2. -2. **claude.ai skill upload (always available).** `tools/package_skill_zip.py` (stdlib) writes +Verified (Anthropic docs, 2026-09-14): Cowork installs plugins from a GitHub repository carrying +`.claude-plugin/marketplace.json` (Customize → Plugins → Add from repository) or from an uploaded +plugin file; skills are invoked from the `/` or `+` picker or by description; the VM runs bash +and Python; an update is picked up only when `version` changes. claude.ai chat accepts a custom +skill as a ZIP whose root is the skill folder named exactly `name`, honours only the six spec +frontmatter fields, triggers by description (no slash commands), and runs scripts in the +code-execution sandbox when "Code execution and file creation" is on. Both are paid-plan +features; Enterprise admins may block skills containing `*.py` as high risk. The Claude Code tab +in Desktop is Claude Code and needs nothing. + +Two delivery paths, both shipped from the same SKILL.md: + +1. **Cowork plugin (preferred).** The v0.3 repo is already a valid Cowork plugin; the README row + in §4 is the install. Acceptance runs the fixture audit in a Cowork session. +2. **claude.ai skill upload.** `tools/package_skill_zip.py` (stdlib) writes `dist/humanize-skill-.zip` containing `humanize/SKILL.md`, `humanize/references/`, - and `humanize/scripts/surface_scan.py` — the folder-rooted layout Anthropic's skill examples - use — with the version read from `plugin.json`. README documents Settings → Capabilities → - Skills → upload, and that in claude.ai the skill is triggered by its description (no slash - command) and the scanner runs in Claude's code-execution sandbox (stdlib only, which we are). - The zip is attached to the GitHub release. + and `humanize/scripts/surface_scan.py`, version read from `plugin.json`, and refuses to build + if the frontmatter has any key outside the six. The zip is attached to the GitHub release. -Both paths use the same SKILL.md; nothing Desktop-specific enters the skill. +Nothing Desktop-specific enters the skill: the relative `scripts/` path (§2B) is what both the +Cowork VM and the claude.ai sandbox resolve, and `${CLAUDE_PLUGIN_ROOT}` — whose substitution in +Cowork is unverified — is gone. ## 7. Versioning and release @@ -261,8 +275,11 @@ Both paths use the same SKILL.md; nothing Desktop-specific enters the skill. prompt-input "hi"`. 4. `hermes skills install ccf/humanize/humanize` pulls SKILL.md, all five referenced references, and the script; `hermes skills list` shows it with a clean guard verdict. -5. Cowork: one recorded attempt, outcome documented in the README either way; skill zip uploads - and triggers on a pasted paragraph in claude.ai. +5. Cowork: `ccf/humanize` added as a marketplace, `humanize` installed, fixture audit produces + the four rows in a Cowork session. claude.ai: the zip uploads with the current 308-character + description (the spec limit is 1024; Anthropic's best-practice note says ~200 — if the + uploader rejects it, shorten the description in the same release and record the limit in + CLAUDE.md), and the skill triggers on a pasted paragraph. 6. Existing Claude Code install upgrades 0.2.0 → 0.3.0 via the migration command with one skill registered. @@ -278,3 +295,6 @@ Both paths use the same SKILL.md; nothing Desktop-specific enters the skill. smoke matrix; the row is removed if it fails. - Claude Code marketplace `source: "./"` — validated by `claude plugin validate --strict .` and by acceptance 6. +- Whether Cowork validates a plugin-bundled SKILL.md against the six-field set is unverified; + the spec removes the only non-spec key, so the answer no longer matters. +- Description length in the claude.ai uploader (acceptance 5). From 94c53d8b7636c65dbc1834a9b5c673e2455d66f4 Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 08:33:12 -0400 Subject: [PATCH 03/22] v0.3 design: add positioning copy section (research-grounded, every layer) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- .../design/2026-09-14-humanize-v0.3-design.md | 72 ++++++++++++++++++- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/docs/design/2026-09-14-humanize-v0.3-design.md b/docs/design/2026-09-14-humanize-v0.3-design.md index 49d1e18..4e87971 100644 --- a/docs/design/2026-09-14-humanize-v0.3-design.md +++ b/docs/design/2026-09-14-humanize-v0.3-design.md @@ -255,8 +255,9 @@ Cowork is unverified — is gone. - Version **0.3.0** in the four manifests; `CHANGELOG.md` `[0.3.0]` with Added (Agent Plugins manifest, Codex manifest, portability tests, smoke matrix, skill-zip packager, install table), - Changed (repo layout; SKILL.md portable wording; document-skills instructions moved to README), - and a **Migration** note: existing Claude Code installs run + Changed (repo layout; SKILL.md portable wording; document-skills instructions moved to README; + positioning copy per §9 — humanize is described as grounded in the SOURCES.md registry, not in + StoryScope alone; `argument-hint` removed), and a **Migration** note: existing Claude Code installs run `claude plugin marketplace update humanize && claude plugin update humanize@humanize`; the marketplace `source` moved from `./plugins/humanize` to `./`. - Release checklist (CLAUDE.md Workflow): CI green → smoke matrix PASS on every locally available @@ -282,8 +283,73 @@ Cowork is unverified — is gone. CLAUDE.md), and the skill triggers on a pasted paragraph. 6. Existing Claude Code install upgrades 0.2.0 → 0.3.0 via the migration command with one skill registered. +7. Every §9 string is present verbatim on its surface; `grep -ri storyscope` over the manifests, + `pyproject.toml`, SKILL.md, and README lines 1–20 returns nothing; `test_study_count_matches_sources` + passes; the README grounding paragraph scanned with `surface_scan.py` shows no wordlist hits. + +## 9. Positioning copy — research-grounded, every layer, not StoryScope-centred + +Decision (user, 2026-09-14): public copy stops presenting humanize as "grounded in StoryScope" +and presents it as grounded in the body of studies in `references/SOURCES.md` (thirteen today) +and as covering every layer of a text. StoryScope keeps its attribution where it is the source +(base rates, `style-tells.md`/`narrative-tells.md` file descriptions, the license paragraph, +principle 1) — accuracy, not prominence. Copy below was drafted under limits, checked against +each source's `May support:` line, and passed through `surface_scan.py` (no wordlist hits, no +not-but, one tricolon). Strings are verbatim; the plan applies them as-is. + +| Surface | Value | +|---|---| +| `.claude-plugin/marketplace.json` `metadata.description` (≤160) | Measures prose against 13 studies of how AI and human writing differ, from word choice to sentence rhythm to story shape, then rewrites the tells out. | +| `description` in `plugin.json`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, marketplace `plugins[0]` (≤200) | Audits a draft layer by layer (vocabulary, punctuation, sentence shape, openers, closers, grammar, narrative, per-model habits) against 13 studies, then rewrites what it flags in the author's voice. | +| `.codex-plugin` `interface.shortDescription` (≤80) | Audit prose for AI tells at every layer, then rewrite it in the author's voice. | +| `.codex-plugin` `interface.longDescription` (≤320) | Reads a draft against what 13 studies measured about AI writing: word choice, punctuation, sentence shape, openers, closers, grammar, register, fiction habits, per-model quirks. A dependency-free scanner supplies the counts a model cannot eyeball, and each flag cites the sentence that triggered it. | +| `keywords` (all manifests) | `["humanize", "writing", "prose", "editing", "style", "ai-writing", "rewriting", "linguistics"]` — drops `storyscope` and the detector-adjacent `ai-detection` | +| `pyproject.toml` `description` | Agent skill that audits prose for AI tells and rewrites it in the author's voice, on evidence from 13 studies. | +| `CLAUDE.md` lines 3–4 | `Agent skill that audits prose for AI tells and rewrites it. Evidence base: 13 studies` / `` in `references/SOURCES.md`. Spec and plan: `docs/design/`. Changelog: `CHANGELOG.md`. `` | + +These two supersede the §1.1 `` placeholder and the §1.1 `interface` strings. + +**SKILL.md lines 10–11** (line 9 unchanged; two lines, 71 and 70 chars; the §2 budget holds): -## 9. Risks and open points (settled by the smoke matrix, not by more research) +``` +mark it as AI-generated. Grounded in thirteen studies of measured style +differences: AI converges on shared defaults; human writing disperses. +``` + +**README.** The H1 stays `# humanize`. The paragraph under it becomes: + +> An agent skill that audits prose for the tells of AI writing and rewrites them out, without +> flattening the author's voice. Runs in Claude Code, Codex, Cursor, Hermes Agent, Claude +> Desktop. + +The StoryScope paragraph (current lines 7–17) becomes: + +> The reference entries rest on thirteen studies. StoryScope (2026) measured 304 narrative and +> stylistic features over 61,575 stories, and its widest human-vs-AI gaps supply the base rates. +> Reinhart et al. (2025) put participial modifiers at 5.3 times the human rate and nominalization +> at 2.1 times. Jakesch et al. (2023) found repeated phrasing the strongest true signal of a +> text's source. Herbold et al. (2023) recorded a lexical-diversity reversal between model +> generations, a reminder that findings expire. humanize turns that work into an audit checklist +> plus a dependency-free scanner for the counts a model cannot eyeball; every flag quotes the line +> it came from. One pattern organizes the whole checklist: AI converges on shared defaults while +> human writing disperses. Every cited number resolves in references/SOURCES.md. + +README tree lines for `style-tells.md` / `narrative-tells.md` read "20 style features with +StoryScope base rates" / "57 narrative features (fiction only), StoryScope base rates". The +license/credit paragraph is unchanged (attribution is required there). `.cursor/BUGBOT.md` line +3 mirrors the CLAUDE.md sentence. `principles.md` is unchanged: principle 1 attributes the +convergence result to StoryScope, which is where it comes from. + +**Guard.** `tests/test_portability.py` gains `test_study_count_matches_sources`: the number of +`` ## `key` `` entries in `SOURCES.md` equals the count written in every surface above +(digits in the manifests, `pyproject.toml`, `CLAUDE.md`, README; the word in SKILL.md and the +README grounding paragraph, via a small number-word map). Adding a source then fails CI until +the copy is updated — a release-level change anyway, since it bumps the manifests. + +Wording rule (added to CLAUDE.md invariants): public copy names StoryScope only where it is the +specific source of a number or a file; the tool is described as grounded in the registry. + +## 10. Risks and open points (settled by the smoke matrix, not by more research) - Manifest precedence on Codex when `.codex-plugin/`, `.claude-plugin/`, and root `plugin.json` coexist — expected: `.codex-plugin` wins; verified by acceptance 3. From fcb3a06c69298100d4a79db4fd133acf66f1f505 Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 08:47:11 -0400 Subject: [PATCH 04/22] v0.3 design revision 2: adjudicate the three-lens adversarial review 21 findings folded in: conftest path, Cursor CLI reality, scanner evidence in the smoke matrix, no harness variables in the skill, phased acceptance, zip packager test, guard-pattern list, Hermes flags, claude.ai slash menu, study-count guard. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- .../design/2026-09-14-humanize-v0.3-design.md | 412 ++++++++++-------- 1 file changed, 239 insertions(+), 173 deletions(-) diff --git a/docs/design/2026-09-14-humanize-v0.3-design.md b/docs/design/2026-09-14-humanize-v0.3-design.md index 4e87971..02be476 100644 --- a/docs/design/2026-09-14-humanize-v0.3-design.md +++ b/docs/design/2026-09-14-humanize-v0.3-design.md @@ -1,17 +1,22 @@ # humanize v0.3 — portable skill (design) Date: 2026-09-14 -Status: draft for review (revision 1). +Status: draft for review (revision 2). Revision 1 had a three-lens adversarial review (harness +facts, repo mechanics, tests/acceptance): 3 blockers, 16 important, 2 minor, all adjudicated +below; a `claude plugin validate` dry run and a full pytest run on a rearranged scratch copy +confirmed the layout. Builds on: `2026-09-14-humanize-v0.2-design.md` (v0.2, shipped as 0.2.0 at main 00a3d74). Evidence: verified harness research run 2026-09-14 (Codex CLI 0.154.0 empirically; Cursor from -primary docs; Hermes Agent v0.11.0 installed + `origin/main` source), the superpowers 6.3.0 and -agentcairn packaging layouts, and local inspection of Claude Desktop 1.52386.6 (Cowork). +primary docs and the installed `agent` 2026.01.23; Hermes Agent v0.11.0 installed + `origin/main` +source), Anthropic documentation for claude.ai skills and Cowork plugins, the superpowers 6.3.0 +and agentcairn packaging layouts, and local inspection of Claude Desktop 1.52386.6. ## 0. Goal and non-goals **Goal.** The same `humanize` skill installs and runs on Claude Code, Codex CLI, Cursor, Hermes -Agent, and the Claude Desktop app, using each harness's own installer. No behaviour of the -scanner or the skill's method changes; only packaging, paths, and one portable wording pass. +Agent, and the Claude Desktop app, using each harness's own installer, and is described +everywhere as a research-grounded tool that covers every layer of a text (§9). No behaviour of +the scanner or the skill's method changes; only packaging, paths, wording, and tests. **Decision (user, 2026-09-14): native installers + compliance tests.** No custom installer CLI, no setup meta-skill. The harnesses have converged on the Agent Skills folder contract @@ -39,17 +44,28 @@ tests/ tools/ data/ docs/ unchanged ``` `git mv plugins/humanize/skills skills`; `git mv plugins/humanize/.claude-plugin/plugin.json -.claude-plugin/plugin.json`; delete `plugins/`. Every path that named `plugins/humanize/…` -changes: `CLAUDE.md` commands, README tree, `tests/test_manifests.py`, `tests/test_fixtures.py`, -`tests/test_surface_scan.py` (script import path), `tools/gen_tell_scaffold.py` (output dir), -`.cursor/BUGBOT.md`, `.pre-commit-config.yaml` (if it filters by path), and the smoke command -(`--plugin-dir .`). - -### 1.1 Manifests (verbatim; `` is the existing plugin description string) +.claude-plugin/plugin.json`; delete `plugins/`. + +**Files that name the old path** (verified by grep on a rearranged scratch copy — with these +patched, all 100 existing tests pass at the new layout): +- `tests/conftest.py:4` — adds the script directory to `sys.path`; this is the line whose omission + makes collection fail with `ModuleNotFoundError: surface_scan`. `tests/test_surface_scan.py` and + `tests/test_fixtures.py` import through it and need no edit. +- `tests/test_manifests.py` — the three path constants (lines 38/43/53) and, in the same commit, + the `argument-hint` assertion at line 23 (removed; see §2). +- `.claude-plugin/marketplace.json:16` (`source`), `CLAUDE.md:13` (scan command), `README.md:103` + and `:128` (tree and command), `.cursor/BUGBOT.md:9` and `:14`, the headless smoke command + (`--plugin-dir .`). +- Unchanged: `tools/gen_tell_scaffold.py`, `.pre-commit-config.yaml`, `.github/workflows/ci.yml` + (its `claude plugin validate --strict .` already runs at the repo root). + +### 1.1 Manifests (verbatim; `` and `keywords` are the §9 strings) `plugin.json` — Agent Plugins 1.0. Cursor requires the `$schema` identifier; Codex 0.154.0 reads this "portable root format" and discovers `skills/` automatically; Hermes `origin/main` installs -it with `hermes plugins install`. Fields are the subset shared with the Claude manifest. +it with `hermes plugins install` — installed disabled, read-only, namespaced +`agent-plugin--`, which is why §4 recommends `hermes skills install` instead. Fields +are the subset shared with the Claude manifest. ```json { @@ -61,17 +77,24 @@ it with `hermes plugins install`. Fields are the subset shared with the Claude m "license": "MIT", "homepage": "https://github.com/ccf/humanize", "repository": "https://github.com/ccf/humanize", - "keywords": ["writing", "prose", "editing", "ai-detection", "storyscope", "humanize"] + "keywords": } ``` -`.claude-plugin/plugin.json` — unchanged fields, `version` 0.3.0. +`.claude-plugin/plugin.json` — `version` 0.3.0, `description` and `keywords` from §9, other +fields unchanged. -`.claude-plugin/marketplace.json` — unchanged except `"source": "./"`, both `version` fields -0.3.0, `strict: true` kept. (superpowers' root `.claude-plugin/` holds both files the same way.) +`.claude-plugin/marketplace.json` — `"source": "./"`, both `version` fields 0.3.0, +`metadata.description` and `plugins[0].description`/`keywords` from §9, `strict: true` kept. +(superpowers' root `.claude-plugin/` holds both files the same way; Codex 0.154.0 reads this +file with a plain-string `source`.) `claude plugin validate --strict .` validates the +**marketplace JSON only** — it exits 0 with a nonexistent `source` and with broken SKILL.md +frontmatter — so the `source` is proven by §3 test 5 and acceptance 6, not by the validator. It +warns about a `CLAUDE.md` at the plugin root; the warning is accepted and noted in CLAUDE.md. `.codex-plugin/plugin.json` — Codex's bundled `validate_plugin.py` accepts only this layout, and -the `interface` block is what its marketplace UI renders: +the `interface` block is what its marketplace UI renders. `shortDescription`/`longDescription` +are the §9 strings. ```json { @@ -82,12 +105,12 @@ the `interface` block is what its marketplace UI renders: "homepage": "https://github.com/ccf/humanize", "repository": "https://github.com/ccf/humanize", "license": "MIT", - "keywords": ["writing", "prose", "editing", "ai-detection", "storyscope", "humanize"], + "keywords": , "skills": "./skills/", "interface": { "displayName": "humanize", - "shortDescription": "Audit prose for AI tells and rewrite it to read as human writing.", - "longDescription": "Scans text for the surface, style, and grammar features that separate AI from human prose (StoryScope and register studies), quotes the evidence, and rewrites without converging on a new default voice.", + "shortDescription": "", + "longDescription": "", "developerName": "ccf", "category": "Developer Tools", "capabilities": ["Read", "Write"], @@ -97,18 +120,21 @@ the `interface` block is what its marketplace UI renders: } ``` -No `.cursor-plugin/` (Cursor accepts Agent Plugins; the Cursor-only manifest adds nothing we -ship) and no `.hermes-plugin/` (not a Hermes convention; Hermes reads `plugin.json` on main and -plain skill folders on every version). No `hooks`, `commands`, `agents`, or MCP anywhere. +No `.cursor-plugin/` (Cursor accepts Agent Plugins; its own manifest adds nothing we ship) and +no `.hermes-plugin/` (not a Hermes convention). No `hooks`, `commands`, `agents`, or MCP. + +## 2. SKILL.md — portable wording, at most 150 lines -## 2. SKILL.md — portable wording, 150 lines exactly +**Budget.** The invariant is `wc -l ≤ 150`, guarded by `test_skill_line_budget`; the plan gives +exact replacement text and the result is 149 or 150 depending on one wrap. Nothing else in the +file changes. -Four edits, line-neutral in total. **Frontmatter drops `argument-hint`** (−1 line): outside Claude -Code it is a hard error — claude.ai uploads, the Skills API, and `package_skill.py` reject any -key beyond the six Agent Skills fields (`Unexpected key(s) in SKILL.md frontmatter: argument-hint. -Allowed properties are: allowed-tools, compatibility, description, license, metadata, name`), and -Codex's validator rejects it too. Claude Code loses only the placeholder text in its `/` menu; -`/humanize ` still works. Frontmatter is then exactly `name` and `description`. +**Frontmatter drops `argument-hint`** (−1 line). Outside Claude Code it is a hard error — +claude.ai uploads, the Skills API, and `package_skill.py` reject any key beyond the six Agent +Skills fields (`Unexpected key(s) in SKILL.md frontmatter: argument-hint. Allowed properties +are: allowed-tools, compatibility, description, license, metadata, name`), and Codex's validator +rejects it too. Claude Code loses only the placeholder text in its `/` menu; `/humanize ` +still works. Frontmatter is then exactly `name` and `description`. **A. Invocation (lines 25–28).** The heading stays. Replace the two-line paragraph with: @@ -117,13 +143,14 @@ Applies only when the user typed `/humanize …` (Codex: `$humanize …`). On auto-invoke there are no arguments: skip this section. Options below also work as plain words in a request ("audit only", "this is fiction"). ``` -(+1 line.) The `$ARGUMENTS` line and its literal-text fallback stay: Codex, Cursor, and Hermes -substitute nothing, so the fallback is what makes the section safe there. +(+1 line.) The `$ARGUMENTS` line and its literal-text fallback stay: Codex, Cursor, Hermes, and +the Desktop pickers substitute nothing, so the fallback is what makes the section safe there. **B. Scan command (lines 69–75).** The fenced command becomes -`python3 scripts/surface_scan.py --text `, and the paragraph under it gains one sentence: -"Run it from this skill's folder — the directory holding this SKILL.md (`${HERMES_SKILL_DIR}` on -Hermes)." (+1 line.) `${CLAUDE_PLUGIN_ROOT}` disappears from the skill. +`python3 scripts/surface_scan.py --text `, and the sentence "Run it from this skill's +folder (the directory holding this SKILL.md)." is added to the paragraph below it (+0 or +1 +line). No harness variable appears: `${CLAUDE_PLUGIN_ROOT}` leaves the skill and +`${HERMES_SKILL_DIR}` never enters it — the Hermes path note lives in the README (§4). **C. Non-text sources (lines 77–84, 8 lines → 7).** Replace with: @@ -138,154 +165,197 @@ file; otherwise ask for a text export. Deliver Markdown; a Word file via `docx`. ``` (−1 line.) The `/plugin …` commands leave the skill; they live in the README. -Net: −1 (frontmatter) +1 (A) +1 (B) −1 (C) = 0. Everything else in SKILL.md is untouched. -`wc -l` = 150 (guarded by `test_skill_line_budget`). No `!` command injection or other Claude -Code-only body features are used (none exist today; the portability test guards the paths). +**D. Grounding (lines 10–11)** — the two §9 lines. + +Net: −1 (frontmatter) +1 (A) +0/+1 (B) −1 (C) +0 (D) = 149 or 150. No `!` command injection or +other Claude Code-only body features are used (none exist today; test 2 guards the paths). ## 3. Compliance tests — `tests/test_portability.py` -Pure-pytest, no network, run in CI with everything else. Constants come from the verified -harness limits (Codex `quick_validate.py`, Cursor docs, agentskills.io spec). +Pure pytest, no network, in CI. Constants come from the verified harness limits (Codex +`quick_validate.py`, agentskills.io, Cursor docs, Hermes `tools/skills_guard.py`). 1. **Frontmatter.** Keys ⊆ `{name, description, license, compatibility, metadata, allowed-tools}` - — the Agent Skills spec set and nothing else (so `argument-hint` can never return); `name` - matches `^[a-z0-9]+(-[a-z0-9]+)*$`, ≤ 64 chars, contains neither `claude` nor `anthropic`, - and equals the skill's folder name (Cursor hard-requires this); `description` ≤ 1024 chars, - contains neither `<` nor `>`, does not start with `[TODO:`. -2. **No harness-specific paths.** `CLAUDE_PLUGIN_ROOT` appears nowhere under `skills/`; - SKILL.md contains the literal-`$ARGUMENTS` fallback sentence. -3. **Support files are referenced.** Every `skills/humanize/references/*.md` except `SOURCES.md` - and `scripts/surface_scan.py` are named by relative path in SKILL.md — Hermes URL installs - fetch only files the SKILL.md names. -4. **Guard-clean scripts.** No `subprocess`, `os.environ`, `os.system`, `eval(`, `exec(`, - `socket`, `urllib`, `requests` in `skills/*/scripts/*.py` (Hermes's `skills_guard.py` - patterns; also our own no-network invariant). + and nothing else (so `argument-hint` can never return); `name` matches + `^[a-z0-9]+(-[a-z0-9]+)*$`, ≤ 64 chars, contains neither `claude` nor `anthropic`, and equals + the skill's folder name (Cursor hard-requires this); `description` ≤ 1024 chars, contains + neither `<` nor `>`, does not start with `[TODO:`. +2. **No harness-specific content under `skills/`.** None of these substrings appears in any + file under `skills/`: `${CLAUDE_`, `CLAUDE_PLUGIN_ROOT`, `HERMES_SKILL_DIR`, `CURSOR_`, + `CODEX_`, `/plugin `, `npx skills`. SKILL.md, with whitespace collapsed, contains the + substring ``reads literally as `$ARGUMENTS` `` (the fallback sentence wraps across two lines). +3. **Runtime references are named.** Each of the five references SKILL.md loads at runtime + (`principles`, `surface-tells`, `style-tells`, `narrative-tells`, `model-fingerprints`) and + `scripts/surface_scan.py` are named by relative path in SKILL.md. `SOURCES.md` is exempt: it + is a maintainer registry never loaded at runtime. (Hermes GitHub-source installs copy the whole + skill directory; bare-URL installs are single-file and unsupported — the README says so.) +4. **Guard-clean scripts.** No bare token from this list in `skills/**/scripts/*.py`: + `subprocess`, `os.system`, `os.popen`, `os.environ`, `getenv`, `shutil.rmtree`, `eval(`, + `exec(`, `compile(`, `getattr(`, `codecs`, `socket`, `urllib`, `requests`, `# /// script`. + Docstring: deliberately stricter than Hermes's line-by-line regexes; also our own no-network + invariant. (The current script is clean against every pattern.) 5. **Manifests agree.** `name` is `humanize` in all four manifests; `version` is identical in `plugin.json`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, and both `marketplace.json` version fields, and equals the newest `## [x.y.z]` heading in - `CHANGELOG.md`; `plugin.json.$schema` is exactly the 1.0.0 URL; `.codex-plugin` `skills` - resolves to a directory containing `humanize/SKILL.md`; marketplace `source` resolves to a - directory containing `.claude-plugin/plugin.json`. - -`tests/test_manifests.py` keeps its current checks at the new paths. + `CHANGELOG.md` (the `[Unreleased]` heading is skipped); `plugin.json.$schema` is exactly the + 1.0.0 URL; `.codex-plugin` `skills` resolves to a directory containing `humanize/SKILL.md`; + marketplace `source` resolves to a directory containing `.claude-plugin/plugin.json`. +6. **Zip packager.** `tools/package_skill_zip.py` (§6) is importable; its builder, run into a + temp dir, produces a zip whose member set equals exactly `humanize/SKILL.md`, + `humanize/references/`, `humanize/scripts/surface_scan.py`; the filename version + equals `plugin.json`'s; the builder raises on a SKILL.md with a seventh frontmatter key. +7. **Study count.** The number of `` ## `key` `` entries in `SOURCES.md` equals the count written + on every §9 surface (digits in the manifests, `pyproject.toml`, `CLAUDE.md`, README; the word + in SKILL.md and the README grounding paragraph, via a number-word map). + +`tests/test_manifests.py` keeps its current checks at the new paths, minus the `argument-hint` +assertion. `test_skill_line_budget` stays `≤ 150`. ## 4. Documentation -**README — "Install" replaces the current single-harness instructions.** One table, then a -paragraph of notes per harness. Commands are the verified ones: +**README — "Install"** replaces the current single-harness instructions: one table, then notes. +Every command in the table is run against the CLI's `--help` (or the GUI path against the docs) +before the row ships — acceptance 7. ⚠ marks a cell whose command is verified only by that +acceptance step. | Harness | Install | Verify | Invoke | |---|---|---|---| | Claude Code | `/plugin marketplace add ccf/humanize` · `/plugin install humanize@humanize` | `claude plugin list` | `/humanize` or auto | -| Codex CLI | `codex plugin marketplace add ccf/humanize` · `codex plugin add humanize@humanize` — or `cp -R skills/humanize ~/.agents/skills/` | `codex debug prompt-input "hi"` lists it | `$humanize …` or auto | -| Cursor | `cp -R skills/humanize ~/.cursor/skills/` (user; syncs to Cloud Agents) or `.cursor/skills/` (project) — or `agent plugin marketplace add https://github.com/ccf/humanize` | skill appears in `/` menu | `/humanize` or auto | +| Codex CLI | ⚠ `codex plugin marketplace add ccf/humanize` · `codex plugin add humanize@humanize` — or `cp -R skills/humanize ~/.agents/skills/` | `codex debug prompt-input "hi"` lists it | `$humanize …` or auto | +| Cursor | `cp -R skills/humanize ~/.cursor/skills/` (user) or `.cursor/skills/` (project); the Cursor marketplace is GUI-only and human-reviewed, not a CLI path | skill appears in the `/` menu | `/humanize` or auto | | Hermes Agent | `hermes skills install ccf/humanize/humanize --category writing`, then `/reload-skills` in a running session | `hermes skills list` | `/humanize …` or auto | -| Any Agent-Skills harness | `npx skills add ccf/humanize` (→ `~/.agents/skills/`) or `cp -R skills/humanize ~/.agents/skills/` | harness-specific | harness-specific | -| Claude Desktop — Cowork | Customize → Plugins → Add from repository `ccf/humanize` → install `humanize` | plugin listed under Customize → Plugins | `/` or `+` picker, or auto | -| Claude Desktop — chat (claude.ai) | Customize → Skills → Upload `humanize-skill-.zip` (from the GitHub release); "Code execution and file creation" must be on | skill listed under Customize → Skills | auto (description match); no slash command | - -Notes carry the gotchas the research surfaced: Codex reads `/.agents/skills/` but not -`.claude/skills/`; Codex caps the injected skills catalog at 2% of context; Cursor only syncs -`~/.cursor/skills/` to Cloud Agents; Hermes scans installed scripts (ours is stdlib and -guard-clean) and `${HERMES_SKILL_DIR}` is a host path — a remote terminal backend (docker, -modal, ssh) will not see the script, so Hermes users on remote backends get the audit without -the scan table; in claude.ai chat the skill is description-triggered and the scanner runs in -Claude's sandbox; Cowork plugin updates require a `version` bump to be picked up. - -**CLAUDE.md** — Commands section paths; new invariants: "the skill is harness-agnostic: no -`${CLAUDE_PLUGIN_ROOT}` or other harness variables, no harness-specific install commands inside -`skills/`; `argument-hint` is the only non-spec frontmatter key"; "version lives in four -manifests plus CHANGELOG — `tests/test_portability.py` fails on drift"; layout note. Workflow -section adds the smoke matrix as the pre-tag acceptance step. **BUGBOT.md** mirrors the two -invariants. +| Any Agent-Skills harness | ⚠ `npx skills add ccf/humanize` (→ `~/.agents/skills/`) or `cp -R skills/humanize ~/.agents/skills/` | harness-specific | harness-specific | +| Claude Desktop — Cowork | Customize → Plugins → Add from repository `ccf/humanize` → install `humanize` (route confirmed in acceptance 5; zip fallback below) | plugin listed under Customize → Plugins | `/` or `+` picker, or auto | +| Claude Desktop — chat (claude.ai) | Customize → Skills → Upload `humanize-skill-.zip` (from the GitHub release); "Code execution and file creation" on | skill listed under Customize → Skills | auto (description match) or the sidebar `/` menu; picking a skill passes no arguments | + +Notes carry the verified gotchas: Codex reads `/.agents/skills/` but not `.claude/skills/`; +Codex caps the injected skills catalog at 2% of context; Cursor user-level skills reach Cloud +Agents only with **Sync Skills for Cloud Agents** on (Settings → Agents) and only from +`~/.cursor/skills/`; Hermes scans installed scripts (ours is stdlib and guard-clean), resolves +`${HERMES_SKILL_DIR}` to a host path — so on a remote terminal backend (docker, modal, ssh) the +scan step is skipped and the audit proceeds from reading alone — and its `hermes plugins install` +route installs portable packages disabled and read-only, so `hermes skills install` is the +recommended path; bare-URL skill installs (a raw `SKILL.md` link) fetch one file and are not +supported; in claude.ai the scanner runs in Claude's sandbox; Cowork plugin updates require a +`version` bump. + +**CLAUDE.md** — lines 3–4 from §9; Commands: scan path `skills/humanize/scripts/surface_scan.py`, +`uv run pytest -q` comment loses its test count ("must be warning-free"), `claude plugin validate +--strict .` annotated "validates marketplace JSON only; warns about the root CLAUDE.md +(accepted)"; invariants: "`skills/` is harness-agnostic: no harness variables (`${CLAUDE_…}`, +`HERMES_SKILL_DIR`, `CURSOR_…`, `CODEX_…`) and no install commands inside it; frontmatter is +exactly the Agent Skills fields — `tests/test_portability.py` enforces both"; "version lives in +four manifests plus the CHANGELOG heading — the same test fails on drift" (replaces "bump +`version` in both"); the network rule's glob becomes `skills/**/scripts/`; the §9 wording rule. +Workflow: the smoke matrix and the phased acceptance (§7) become the release steps. +**BUGBOT.md** mirrors: line 3 sentence, the two invariants, the glob at lines 9/14. ## 5. Smoke matrix — `tools/smoke_harnesses.sh` (local, not CI) For each harness whose CLI is on `PATH`, install the working-tree skill in an isolated way, run -the audit-only fixture headlessly, and assert the four expected rows (`trailing participial -clause`, `verbatim repetition`, `container-noun phrase`, `safety disclaimer opener`) appear and -no nominalization row does. - -- Claude Code: `claude plugin disable humanize@humanize; claude -p "/humanize - tests/fixtures/ai_report.txt --audit-only" --plugin-dir . --output-format text - --allowedTools "Bash,Read,Glob,Grep"; claude plugin enable humanize@humanize` (existing). -- Codex: copy `skills/humanize` into a temp project's `.agents/skills/`, `codex exec` the same - request from that directory (project scope; nothing written under `$HOME`). -- Cursor: same temp project, `.cursor/skills/`, `agent -p …` (the CLI must be current; the - script prints its version). -- Hermes: install into `~/.hermes/skills/writing/humanize` via `cp -R`, run `hermes chat` in - non-interactive mode with the request, then remove the directory. (No project scope in - v0.11.0.) - -The script prints a PASS/FAIL line per harness and writes each audit table to -`docs/acceptance/v0.3/.md` — the transcripts a release needs, small enough to commit. -Exact non-interactive flags for `codex exec`, `agent -p`, and `hermes chat` are pinned in the -plan from `--help` output. +the audit-only fixture headlessly, and judge PASS by evidence that the *skill and the scanner* +ran — not by row names alone (three of the four are guessable from the fixture text). + +**Request wording.** No harness but Claude Code substitutes `$ARGUMENTS`, so runs are phrased in +words: "Use the humanize skill on tests/fixtures/ai_report.txt. Audit only — do not rewrite." + +**PASS requires all of:** (a) the four rows — `trailing participial clause`, `verbatim +repetition`, `container-noun phrase`, `safety disclaimer opener` — matched case-insensitively by +regex against the audit table; (b) no `nominalization` row (negative scoped to table rows, not +prose); (c) at least two numbers from a pre-run `python3 skills/humanize/scripts/surface_scan.py +--text tests/fixtures/ai_report.txt` appear verbatim (e.g. the participial count and the +repetition rate), and where the harness exposes tool events, a tool call naming +`surface_scan.py` (Claude: `--output-format stream-json --verbose`; Codex: `--json`); (d) no +rewrite and no "Choices you may want to reverse" section. Two attempts per harness; transcripts +saved to `docs/acceptance/v0.3/.md` (the audit table plus the tool event), the +committed evidence a release needs. + +- **Claude Code:** `claude plugin disable humanize@humanize; claude -p "" --plugin-dir . + --output-format stream-json --verbose --allowedTools "Bash,Read,Glob,Grep"; claude plugin + enable humanize@humanize`. +- **Codex:** copy `skills/humanize` into a temp project's `.agents/skills/`; pre-check with + `codex debug prompt-input "hi"` that the skill is listed; run `codex exec -C + --skip-git-repo-check --ephemeral -s read-only --json -o out.md "" "` from that + directory, flags pinned from `agent --help` (the installed CLI is 2026.01.23; the script prints + the version and updates first). +- **Hermes:** abort if `~/.hermes/skills/writing/humanize` already exists; `cp -R` the skill + there; run non-interactively (`hermes -z ""` or `hermes chat -q "" -Q`, + pinned from `hermes --help` on v0.11.0); remove only the directory the script created. ## 6. Claude Desktop -Local evidence: Claude Desktop 1.52386.6 runs Cowork with a bundled Claude Code 2.1.x and a -local VM, installs skills as plugins in the Claude Code plugin format (`.claude-plugin/plugin.json` -+ `skills/`), and ships Anthropic's `docx`/`pdf`/`pptx`/`xlsx` skills — the ones §2C names. - -Verified (Anthropic docs, 2026-09-14): Cowork installs plugins from a GitHub repository carrying -`.claude-plugin/marketplace.json` (Customize → Plugins → Add from repository) or from an uploaded -plugin file; skills are invoked from the `/` or `+` picker or by description; the VM runs bash -and Python; an update is picked up only when `version` changes. claude.ai chat accepts a custom -skill as a ZIP whose root is the skill folder named exactly `name`, honours only the six spec -frontmatter fields, triggers by description (no slash commands), and runs scripts in the -code-execution sandbox when "Code execution and file creation" is on. Both are paid-plan +Verified (Anthropic docs, 2026-09-14): Cowork installs plugins in the Claude Code plugin format +(`.claude-plugin/plugin.json` + `skills/`), invokes skills from the `/` or `+` picker or by +description, runs them in a Linux VM with bash and Python, and picks up an update only when +`version` changes. claude.ai chat accepts a custom skill as a ZIP whose root is the skill folder +named exactly `name`, honours only the six spec frontmatter fields, triggers by description or +from the sidebar `/` menu (picking passes no arguments, so §2A self-skips), and runs scripts in +the code-execution sandbox when "Code execution and file creation" is on. Both are paid-plan features; Enterprise admins may block skills containing `*.py` as high risk. The Claude Code tab in Desktop is Claude Code and needs nothing. -Two delivery paths, both shipped from the same SKILL.md: +Two delivery paths from the same SKILL.md: -1. **Cowork plugin (preferred).** The v0.3 repo is already a valid Cowork plugin; the README row - in §4 is the install. Acceptance runs the fixture audit in a Cowork session. -2. **claude.ai skill upload.** `tools/package_skill_zip.py` (stdlib) writes +1. **Cowork plugin (preferred, route to confirm).** The repo has the plugin format Cowork loads. + Whether a user can add `ccf/humanize` as a marketplace (Customize → Plugins → Add from + repository) is the one point the two verification runs disagreed on; acceptance 5 settles it + in a Cowork session. If it works, the §4 row stands; if not, the row points to path 2. +2. **claude.ai skill upload (always available).** `tools/package_skill_zip.py` (stdlib) writes `dist/humanize-skill-.zip` containing `humanize/SKILL.md`, `humanize/references/`, - and `humanize/scripts/surface_scan.py`, version read from `plugin.json`, and refuses to build - if the frontmatter has any key outside the six. The zip is attached to the GitHub release. + and `humanize/scripts/surface_scan.py`, version read from `plugin.json`, refusing to build if + the frontmatter has any key outside the six. The zip is attached to the GitHub release. Nothing Desktop-specific enters the skill: the relative `scripts/` path (§2B) is what both the Cowork VM and the claude.ai sandbox resolve, and `${CLAUDE_PLUGIN_ROOT}` — whose substitution in Cowork is unverified — is gone. -## 7. Versioning and release +## 7. Versioning and phased release -- Version **0.3.0** in the four manifests; `CHANGELOG.md` `[0.3.0]` with Added (Agent Plugins - manifest, Codex manifest, portability tests, smoke matrix, skill-zip packager, install table), - Changed (repo layout; SKILL.md portable wording; document-skills instructions moved to README; - positioning copy per §9 — humanize is described as grounded in the SOURCES.md registry, not in - StoryScope alone; `argument-hint` removed), and a **Migration** note: existing Claude Code installs run - `claude plugin marketplace update humanize && claude plugin update humanize@humanize`; the - marketplace `source` moved from `./plugins/humanize` to `./`. -- Release checklist (CLAUDE.md Workflow): CI green → smoke matrix PASS on every locally available - harness, transcripts committed → PR rebase-merged → tag `v0.3.0` → `gh release create v0.3.0 - dist/humanize-skill-0.3.0.zip` → plugin update on this machine. +Version **0.3.0** in the four manifests; `CHANGELOG.md` `[0.3.0]` with Added (Agent Plugins +manifest, Codex manifest, portability tests, smoke matrix, skill-zip packager, install table), +Changed (repo layout; SKILL.md portable wording; document-skills instructions moved to README; +positioning copy per §9; `argument-hint` removed), and a **Migration** note: existing Claude +Code installs run `claude plugin marketplace update humanize && claude plugin update +humanize@humanize`; the marketplace `source` moved from `./plugins/humanize` to `./`. + +`main` is protected with linear history, so anything that installs from GitHub cannot run before +the merge. The release is phased: + +1. **Pre-merge (on the PR):** acceptance 1, 2, 7; acceptance 3 against the branch + (`codex plugin marketplace add ccf/humanize@feat/v0.3`). +2. **Merge** (rebase) → **post-merge, pre-tag:** acceptance 4 and 6 from `main`; the Cowork + marketplace attempt (acceptance 5a). +3. **Tag `v0.3.0`**, `gh release create v0.3.0 dist/humanize-skill-0.3.0.zip`, plugin update on + this machine → **post-release:** acceptance 5b (zip upload). +4. Anything found in phases 2–3 is fixed forward as 0.3.1 through the normal PR path. ## 8. Acceptance -1. `uv run pytest -q -W error` passes (existing 100 + portability tests) on 3.9 and 3.13; - `claude plugin validate --strict .` passes at the new layout; `wc -l skills/humanize/SKILL.md` - = 150. -2. Smoke matrix PASS on Claude Code, Codex, Cursor, and Hermes on this machine, with transcripts - under `docs/acceptance/v0.3/`. -3. `codex plugin marketplace add ccf/humanize && codex plugin add humanize@humanize` installs - from GitHub and the skill surfaces as `humanize` (or `humanize:humanize`) in `codex debug - prompt-input "hi"`. -4. `hermes skills install ccf/humanize/humanize` pulls SKILL.md, all five referenced references, - and the script; `hermes skills list` shows it with a clean guard verdict. -5. Cowork: `ccf/humanize` added as a marketplace, `humanize` installed, fixture audit produces - the four rows in a Cowork session. claude.ai: the zip uploads with the current 308-character - description (the spec limit is 1024; Anthropic's best-practice note says ~200 — if the - uploader rejects it, shorten the description in the same release and record the limit in - CLAUDE.md), and the skill triggers on a pasted paragraph. +1. `uv run pytest -q -W error` passes (existing 100 + §3 tests) on 3.9 and 3.13; + `claude plugin validate --strict .` passes at the new layout (marketplace JSON); + `wc -l skills/humanize/SKILL.md` ≤ 150. +2. Smoke matrix PASS on Claude Code, Codex, Cursor, and Hermes on this machine under the §5 + evidence rule, transcripts under `docs/acceptance/v0.3/`. +3. `codex plugin marketplace add ccf/humanize@feat/v0.3 && codex plugin add humanize@humanize` + installs and the skill surfaces in `codex debug prompt-input "hi"` (pre-merge); the same + without `@ref` after the merge. +4. `hermes skills install ccf/humanize/humanize --category writing` copies the whole skill + directory (six references, the script); `hermes skills list` shows it with a clean guard + verdict. +5. (a) Cowork: `ccf/humanize` added as a marketplace, `humanize` installed, fixture audit + produces the four rows in a Cowork session — or the route is documented as unavailable. + (b) claude.ai: the zip uploads with the current 308-character description (spec limit 1024; + Anthropic's best-practice note says ~200 — if the uploader rejects it, shorten the description + in 0.3.1 and record the limit in CLAUDE.md), and the skill triggers on a pasted paragraph. 6. Existing Claude Code install upgrades 0.2.0 → 0.3.0 via the migration command with one skill - registered. + registered — this is what proves `source: "./"`. 7. Every §9 string is present verbatim on its surface; `grep -ri storyscope` over the manifests, - `pyproject.toml`, SKILL.md, and README lines 1–20 returns nothing; `test_study_count_matches_sources` - passes; the README grounding paragraph scanned with `surface_scan.py` shows no wordlist hits. + `pyproject.toml`, SKILL.md, and README lines 1–20 returns nothing; every README command was run + against its CLI's `--help` (GUI routes against the docs) and the ⚠ cells resolved or removed; + the README grounding paragraph scanned with `surface_scan.py` shows no wordlist hits. ## 9. Positioning copy — research-grounded, every layer, not StoryScope-centred @@ -300,16 +370,14 @@ not-but, one tricolon). Strings are verbatim; the plan applies them as-is. | Surface | Value | |---|---| | `.claude-plugin/marketplace.json` `metadata.description` (≤160) | Measures prose against 13 studies of how AI and human writing differ, from word choice to sentence rhythm to story shape, then rewrites the tells out. | -| `description` in `plugin.json`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, marketplace `plugins[0]` (≤200) | Audits a draft layer by layer (vocabulary, punctuation, sentence shape, openers, closers, grammar, narrative, per-model habits) against 13 studies, then rewrites what it flags in the author's voice. | -| `.codex-plugin` `interface.shortDescription` (≤80) | Audit prose for AI tells at every layer, then rewrite it in the author's voice. | -| `.codex-plugin` `interface.longDescription` (≤320) | Reads a draft against what 13 studies measured about AI writing: word choice, punctuation, sentence shape, openers, closers, grammar, register, fiction habits, per-model quirks. A dependency-free scanner supplies the counts a model cannot eyeball, and each flag cites the sentence that triggered it. | -| `keywords` (all manifests) | `["humanize", "writing", "prose", "editing", "style", "ai-writing", "rewriting", "linguistics"]` — drops `storyscope` and the detector-adjacent `ai-detection` | +| ``: `description` in `plugin.json`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, marketplace `plugins[0]` (≤200) | Audits a draft layer by layer (vocabulary, punctuation, sentence shape, openers, closers, grammar, narrative, per-model habits) against 13 studies, then rewrites what it flags in the author's voice. | +| `` (≤80) | Audit prose for AI tells at every layer, then rewrite it in the author's voice. | +| `` (≤320) | Reads a draft against what 13 studies measured about AI writing: word choice, punctuation, sentence shape, openers, closers, grammar, register, fiction habits, per-model quirks. A dependency-free scanner supplies the counts a model cannot eyeball, and each flag cites the sentence that triggered it. | +| `` (all manifests) | `["humanize", "writing", "prose", "editing", "style", "ai-writing", "rewriting", "linguistics"]` — drops `storyscope` and the detector-adjacent `ai-detection` | | `pyproject.toml` `description` | Agent skill that audits prose for AI tells and rewrites it in the author's voice, on evidence from 13 studies. | | `CLAUDE.md` lines 3–4 | `Agent skill that audits prose for AI tells and rewrites it. Evidence base: 13 studies` / `` in `references/SOURCES.md`. Spec and plan: `docs/design/`. Changelog: `CHANGELOG.md`. `` | -These two supersede the §1.1 `` placeholder and the §1.1 `interface` strings. - -**SKILL.md lines 10–11** (line 9 unchanged; two lines, 71 and 70 chars; the §2 budget holds): +**SKILL.md lines 10–11** (line 9 unchanged; two lines, 71 and 70 chars): ``` mark it as AI-generated. Grounded in thirteen studies of measured style @@ -340,27 +408,25 @@ license/credit paragraph is unchanged (attribution is required there). `.cursor/ 3 mirrors the CLAUDE.md sentence. `principles.md` is unchanged: principle 1 attributes the convergence result to StoryScope, which is where it comes from. -**Guard.** `tests/test_portability.py` gains `test_study_count_matches_sources`: the number of -`` ## `key` `` entries in `SOURCES.md` equals the count written in every surface above -(digits in the manifests, `pyproject.toml`, `CLAUDE.md`, README; the word in SKILL.md and the -README grounding paragraph, via a small number-word map). Adding a source then fails CI until -the copy is updated — a release-level change anyway, since it bumps the manifests. - -Wording rule (added to CLAUDE.md invariants): public copy names StoryScope only where it is the -specific source of a number or a file; the tool is described as grounded in the registry. +Guard: §3 test 7. Wording rule (CLAUDE.md invariant): public copy names StoryScope only where it +is the specific source of a number or a file; the tool is described as grounded in the registry. -## 10. Risks and open points (settled by the smoke matrix, not by more research) +## 10. Risks and fallbacks -- Manifest precedence on Codex when `.codex-plugin/`, `.claude-plugin/`, and root `plugin.json` - coexist — expected: `.codex-plugin` wins; verified by acceptance 3. -- Agent Plugins 1.0 schema strictness on unknown fields — the manifest uses only fields shared - with the Claude/Codex manifests; if Cursor rejects any, drop it to `{$schema, name, version, +- **Self-referential marketplace `source: "./"`** — read by Codex 0.154.0 (verified) and used by + superpowers for Claude Code, but proven for our install only by acceptance 6. Fallback if Claude + Code or Codex rejects it: move the plugin into `plugin/` (agentcairn's layout) with `source: + "./plugin"`, keeping root `plugin.json` and `skills/` as symlinks or a documented copy. +- **Manifest precedence on Codex** when `.codex-plugin/`, `.claude-plugin/`, and root `plugin.json` + coexist — expected `.codex-plugin` wins; acceptance 3. +- **Agent Plugins 1.0 schema strictness** on the extra fields — the manifest uses only fields + shared with the Claude/Codex manifests; if Cursor rejects any, drop to `{$schema, name, version, description}`. -- The `agent` CLI on this machine is 2026.01.23; the smoke matrix updates it first. -- skills.sh CLI (`npx skills add`) discovery of `skills/` at repo root — verified once in the - smoke matrix; the row is removed if it fails. -- Claude Code marketplace `source: "./"` — validated by `claude plugin validate --strict .` and - by acceptance 6. -- Whether Cowork validates a plugin-bundled SKILL.md against the six-field set is unverified; - the spec removes the only non-spec key, so the answer no longer matters. -- Description length in the claude.ai uploader (acceptance 5). +- **skills.sh CLI** (`npx skills add`) discovery of root `skills/` — acceptance 7; the row goes if + it fails. +- **Cursor `agent` CLI** here is 2026.01.23 and has no `plugin` subcommand; the smoke script + updates it first and pins flags from `--help`. +- **Cowork custom-marketplace route** — acceptance 5a; zip fallback regardless. +- **claude.ai description length** — acceptance 5b. +- **Cowork validation of plugin-bundled frontmatter** against the six fields is unverified; the + spec removes the only non-spec key, so it no longer matters. From fb823c0a6f16bbaa56790bc33bf356632cb96762 Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 09:55:28 -0400 Subject: [PATCH 05/22] Add v0.3 implementation plan (portable skill) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- docs/design/2026-09-14-humanize-v0.3-plan.md | 994 +++++++++++++++++++ 1 file changed, 994 insertions(+) create mode 100644 docs/design/2026-09-14-humanize-v0.3-plan.md diff --git a/docs/design/2026-09-14-humanize-v0.3-plan.md b/docs/design/2026-09-14-humanize-v0.3-plan.md new file mode 100644 index 0000000..9d622f6 --- /dev/null +++ b/docs/design/2026-09-14-humanize-v0.3-plan.md @@ -0,0 +1,994 @@ +# humanize v0.3 — portable skill: implementation plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship the same `humanize` skill to Claude Code, Codex CLI, Cursor, Hermes Agent, and Claude Desktop through each harness's own installer, with compliance tests, a smoke matrix, and public copy that presents humanize as research-grounded across every text layer. + +**Architecture:** Flatten the plugin to the repository root (`skills/humanize/`, root `plugin.json`, `.claude-plugin/`, `.codex-plugin/`), make SKILL.md harness-agnostic (six-field frontmatter, relative script path, plain-words options), add `tests/test_portability.py` as the mechanical guard, a stdlib zip packager for claude.ai, and a local smoke script whose PASS rule needs evidence the scanner ran. No scanner or method change. + +**Tech Stack:** Python 3.9+ stdlib (tests, packager), bash (smoke script), pytest, ruff, uv, `claude`/`codex`/`agent`/`hermes` CLIs for acceptance only. + +**Spec:** `docs/design/2026-09-14-humanize-v0.3-design.md` (revision 2; binding). Section numbers below refer to it. + +## Global Constraints + +- `skills/humanize/scripts/surface_scan.py` is untouched by this plan. Any new Python (tests, `tools/package_skill_zip.py`) is standard-library only and Python 3.9-compatible: `from __future__ import annotations`; no `match`; no runtime `X | Y`; no `removeprefix`; no f-string reusing its own quote type inside `{}`. +- No network or LLM calls in `tests/` or `skills/**/scripts/` or `tools/package_skill_zip.py`. `tools/smoke_harnesses.sh` is the only thing that calls a harness, and it is never run by CI or pytest. +- ruff line length 100: no source line, comment, or string literal in Python may exceed it. +- SKILL.md: `wc -l` ≤ 150 after Task 2 (expected exactly 150). Frontmatter keys after Task 2 are exactly `name` and `description`. No `${CLAUDE_`, `CLAUDE_PLUGIN_ROOT`, `HERMES_SKILL_DIR`, `CURSOR_`, `CODEX_`, `/plugin `, or `npx skills` anywhere under `skills/`. +- Never write that output is "undetectable", passes a detector, or is "certified human" — anywhere, including README and manifests. +- Curly characters in existing literals (’ “ ” — –) must survive; verify SKILL.md and README edits with `grep -c` before/after where a block contains them. Use heredocs for Python probes. +- Version `0.3.0` appears in exactly these places and nowhere else new: `plugin.json`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, `.claude-plugin/marketplace.json` (both fields), `pyproject.toml`, `CHANGELOG.md` heading. (Ruling: `pyproject.toml` joins the sync set — it is 0.1.0 today, which is drift.) +- Public copy strings in Tasks 3 and 5 are the spec §9 strings verbatim — copy, never retype. +- Tooling: `uv run pytest -q -W error`; `uv run --python 3.9 pytest -q -W error` once per task; before committing `uv run ruff format && uv run ruff check ` on the Python files you touched (never bare `.`); hooks run on commit; never `--no-verify`. Branch `feat/v0.3`, repo `/Users/ccf/git/humanize`. +- Every commit message ends with: + ``` + Co-Authored-By: Claude Fable 5.1 + Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh + ``` + +## File structure + +Create: +- `plugin.json` — Agent Plugins 1.0 manifest (Task 3) +- `.codex-plugin/plugin.json` — Codex manifest with `interface` (Task 3) +- `tests/test_portability.py` — the seven compliance tests (Tasks 2, 3, 4, 5) +- `tools/package_skill_zip.py` — claude.ai skill zip builder (Task 4) +- `tools/smoke_harnesses.sh` — local acceptance matrix (Task 7) +- `docs/acceptance/v0.3/.md` — smoke transcripts (Task 7) + +Move: `plugins/humanize/skills/` → `skills/`; `plugins/humanize/.claude-plugin/plugin.json` → `.claude-plugin/plugin.json` (Task 1). + +Modify: `tests/conftest.py`, `tests/test_manifests.py`, `.claude-plugin/marketplace.json`, `CLAUDE.md`, `README.md`, `.cursor/BUGBOT.md`, `pyproject.toml`, `CHANGELOG.md`, `.gitignore`, `skills/humanize/SKILL.md`, the spec's status line. + +--- + +### Task 1: Move the plugin to the repository root + +**Files:** +- Move: `plugins/humanize/skills/` → `skills/`; `plugins/humanize/.claude-plugin/plugin.json` → `.claude-plugin/plugin.json` +- Modify: `tests/conftest.py:4`, `tests/test_manifests.py:38,43,53`, `.claude-plugin/marketplace.json:16`, `CLAUDE.md:13`, `README.md:103,128`, `.cursor/BUGBOT.md:9,14` + +**Interfaces:** +- Produces: the layout every later task assumes — `skills/humanize/{SKILL.md,references/,scripts/surface_scan.py}` at the repo root; `ROOT / ".claude-plugin/plugin.json"`. + +- [ ] **Step 1: Move the files** + +```bash +cd /Users/ccf/git/humanize +git mv plugins/humanize/skills skills +git mv plugins/humanize/.claude-plugin/plugin.json .claude-plugin/plugin.json +rmdir plugins/humanize/.claude-plugin plugins/humanize plugins +test -f skills/humanize/SKILL.md && test -f .claude-plugin/plugin.json && echo moved +``` + +- [ ] **Step 2: Run the suite to see the expected failure** + +Run: `uv run pytest -q -W error 2>&1 | tail -3` +Expected: collection error `ModuleNotFoundError: No module named 'surface_scan'` (from `tests/conftest.py`). + +- [ ] **Step 3: Repoint every path** + +`tests/conftest.py` line 4: +```python +SCRIPTS = Path(__file__).resolve().parents[1] / "skills/humanize/scripts" +``` + +`tests/test_manifests.py`: line 38 `ROOT / "plugins/humanize/.claude-plugin/plugin.json"` → `ROOT / ".claude-plugin/plugin.json"`; line 43 `SKILL_MD = ROOT / "plugins/humanize/skills/humanize/SKILL.md"` → `ROOT / "skills/humanize/SKILL.md"`; line 53 `REFS = ROOT / "plugins/humanize/skills/humanize/references"` → `ROOT / "skills/humanize/references"`. + +`.claude-plugin/marketplace.json` line 16: `"source": "./plugins/humanize",` → `"source": "./",` + +`CLAUDE.md` line 13: `uv run python plugins/humanize/skills/humanize/scripts/surface_scan.py --text ` → `uv run python skills/humanize/scripts/surface_scan.py --text ` + +`README.md` line 103 (the tree's first two lines `plugins/humanize/` / ` skills/humanize/`) → one line `skills/humanize/`, and dedent the tree's children by two spaces so the columns stay aligned; line 128: `uv run python plugins/humanize/skills/humanize/scripts/surface_scan.py --text some.txt` → `uv run python skills/humanize/scripts/surface_scan.py --text some.txt`. + +`.cursor/BUGBOT.md` line 9: `` `plugins/humanize/skills/humanize/scripts/surface_scan.py` `` → `` `skills/humanize/scripts/surface_scan.py` ``; line 13 `plugins/**/scripts/` → `skills/**/scripts/`; line 14 `` `plugins/humanize/skills/humanize/references/*.md` `` → `` `skills/humanize/references/*.md` ``. + +Then: `grep -rn 'plugins/humanize' --include='*.md' --include='*.py' --include='*.json' --include='*.toml' --include='*.yml' --include='*.yaml' . | grep -v -E '^\./(docs/design|CHANGELOG\.md|\.superpowers)'` must print nothing. + +- [ ] **Step 4: Verify** + +Run: `uv run pytest -q -W error 2>&1 | tail -1` → `100 passed`. +Run: `uv run --python 3.9 pytest -q -W error 2>&1 | tail -1` → `100 passed`. +Run: `claude plugin validate --strict . 2>&1 | tail -3` → passes (a warning about the root `CLAUDE.md` is accepted; record it in the report). +Run: `python3 skills/humanize/scripts/surface_scan.py --text tests/fixtures/ai_report.txt | head -1` → the words/sentences line. + +- [ ] **Step 5: Commit** + +```bash +git add -A +git commit -m "Move the plugin to the repository root + +Co-Authored-By: Claude Fable 5.1 +Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh" +``` + +--- + +### Task 2: SKILL.md portable wording and the first three portability tests + +**Files:** +- Create: `tests/test_portability.py` +- Modify: `skills/humanize/SKILL.md` (lines 4, 10–11, 27–28, 70, 73–75, 77–84), `tests/test_manifests.py:23` + +**Interfaces:** +- Produces: `tests/test_portability.py` with module constants `ROOT`, `SKILL_DIR = ROOT / "skills/humanize"`, `SKILL_MD`, helper `frontmatter(text) -> dict[str, str]`; later tasks append tests to this file. + +- [ ] **Step 1: Write the failing tests** + +Create `tests/test_portability.py`: + +```python +"""Portability guards: the skill must be a plain Agent Skill every harness can load.""" + +from __future__ import annotations + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SKILL_DIR = ROOT / "skills/humanize" +SKILL_MD = SKILL_DIR / "SKILL.md" + +# The Agent Skills specification fields (agentskills.io; Codex quick_validate.py; +# claude.ai upload validator). Anything else is a hard error outside Claude Code. +SPEC_FIELDS = {"name", "description", "license", "compatibility", "metadata", "allowed-tools"} +NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") + +# Harness-specific tokens that must never appear under skills/ (spec §3 test 2). +HARNESS_TOKENS = ( + "${CLAUDE_", + "CLAUDE_PLUGIN_ROOT", + "HERMES_SKILL_DIR", + "CURSOR_", + "CODEX_", + "/plugin ", + "npx skills", +) + +RUNTIME_REFERENCES = ( + "references/principles.md", + "references/surface-tells.md", + "references/style-tells.md", + "references/narrative-tells.md", + "references/model-fingerprints.md", +) + + +def frontmatter(text: str) -> dict[str, str]: + """Return the YAML frontmatter as a flat key -> raw-value map (one line per key).""" + assert text.startswith("---\n"), "SKILL.md must start with frontmatter" + block = text.split("\n---\n", 1)[0][4:] + out: dict[str, str] = {} + for line in block.splitlines(): + if not line.strip() or line.startswith(" "): + continue + key, _, value = line.partition(":") + out[key.strip()] = value.strip() + return out + + +def test_frontmatter_is_spec_only(): + fm = frontmatter(SKILL_MD.read_text(encoding="utf-8")) + extra = set(fm) - SPEC_FIELDS + assert not extra, f"non-spec frontmatter keys: {sorted(extra)}" + name = fm["name"] + assert NAME_RE.match(name) and len(name) <= 64, name + assert "claude" not in name and "anthropic" not in name + assert name == SKILL_DIR.name, "Cursor requires the folder name to equal `name`" + desc = fm["description"] + assert len(desc) <= 1024, len(desc) + assert "<" not in desc and ">" not in desc + assert not desc.startswith("[TODO:") + + +def test_no_harness_specific_content_under_skills(): + for path in SKILL_DIR.rglob("*"): + if not path.is_file(): + continue + text = path.read_text(encoding="utf-8") + for token in HARNESS_TOKENS: + assert token not in text, f"{path.relative_to(ROOT)} contains {token!r}" + collapsed = " ".join(SKILL_MD.read_text(encoding="utf-8").split()) + assert "reads literally as `$ARGUMENTS`" in collapsed + + +def test_runtime_references_and_script_are_named_in_skill(): + text = SKILL_MD.read_text(encoding="utf-8") + for ref in RUNTIME_REFERENCES: + assert ref in text, ref + assert (SKILL_DIR / ref).is_file(), ref + assert "scripts/surface_scan.py" in text + # SOURCES.md is a maintainer registry, never loaded at runtime, so it is not named. +``` + +- [ ] **Step 2: Run them to verify they fail** + +Run: `uv run pytest -q tests/test_portability.py 2>&1 | tail -4` +Expected: 2 failed (`argument-hint` is a non-spec key; `CLAUDE_PLUGIN_ROOT` and `/plugin ` present in SKILL.md), 1 passed. + +- [ ] **Step 3: Edit SKILL.md — five exact replacements** + +(a) Delete line 4 entirely: `argument-hint: "[path | text] [--audit-only] [--fiction | --prose]"`. Frontmatter is then `name:` and `description:` only. + +(b) Lines 10–11 (now 9–10 after (a)). Replace +``` +mark it as AI-generated. Grounded in StoryScope (Russell et al., 2026) and +register studies: AI converges on shared defaults; human writing disperses. +``` +with +``` +mark it as AI-generated. Grounded in thirteen studies of measured style +differences: AI converges on shared defaults; human writing disperses. +``` + +(c) Invocation paragraph. Replace +``` +Applies only when the user typed `/humanize …`. On auto-invoke (drafting +mode or a natural-language request) there are no arguments: skip this section. +``` +with +``` +Applies only when the user typed `/humanize …` (Codex: `$humanize …`). On +auto-invoke there are no arguments: skip this section. Options below also +work as plain words in a request ("audit only", "this is fiction"). +``` + +(d) Scan block. Replace the fenced command line +``` +python3 "${CLAUDE_PLUGIN_ROOT}/skills/humanize/scripts/surface_scan.py" --text +``` +with +``` +python3 scripts/surface_scan.py --text +``` +and replace the three-line paragraph under the fence +``` +Write pasted text to a temp file first. For the full JSON, drop `--text`. Under +80 words, skip this step; the statistics are noise. The scanner strips code, +links, URLs, and heading markers from Markdown before measuring. +``` +with +``` +Run it from this skill's folder (the directory holding this SKILL.md). Write +pasted text to a temp file first. For the full JSON, drop `--text`. Under 80 +words, skip this step; the statistics are noise. The scanner strips code, links, +URLs, and heading markers from Markdown before measuring. +``` + +(e) Non-text sources. Replace the eight lines beginning `**Non-text sources**` and ending `as Markdown; if the user wants a Word file back, hand off to the `docx` skill.` with +``` +**Non-text sources** (`.docx`, `.pdf`, `.pptx`, `.odt`, `.rtf`): the scanner reads +plain text only. Extract first with the harness's document skills (Anthropic's +`docx`/`pdf` skills ship with Claude Code's `document-skills` plugin and with +Claude Desktop) to write a temp `.md`, then scan and audit that. In claude.ai the +file is an attachment: convert it in the sandbox (pandoc, pdfplumber). Without +document skills, read the file yourself if you can and write the text to a temp +file; otherwise ask for a text export. Deliver Markdown; a Word file via `docx`. +``` + +Then `wc -l skills/humanize/SKILL.md` → 150 (−1 +0 +1 +1 −1). If it prints 151, one of (c)/(d) wrapped differently — re-check the exact text above; never trim another rule. + +(f) `tests/test_manifests.py`: delete line 23, `assert "argument-hint:" in (src / "skills/humanize/SKILL.md").read_text()`. + +- [ ] **Step 4: Run the tests to verify they pass** + +Run: `uv run pytest -q -W error 2>&1 | tail -1` → `103 passed`. +Run: `grep -c '[’“”—–]' skills/humanize/SKILL.md` before and after Step 3 — the count must not decrease by more than the two em dashes removed in (d)'s old command? No: (d) removed none; the count must be unchanged or higher. +Run: `claude plugin validate --strict . 2>&1 | tail -1` → passes. +Run: `sed -n 1,5p skills/humanize/SKILL.md` → `---`, `name: humanize`, `description: …`, `---`, blank. + +- [ ] **Step 5: Commit** + +```bash +uv run ruff format tests/test_portability.py && uv run ruff check tests/test_portability.py +git add skills/humanize/SKILL.md tests/test_portability.py tests/test_manifests.py +git commit -m "Make SKILL.md harness-agnostic; add frontmatter, path, and reference guards + +Co-Authored-By: Claude Fable 5.1 +Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh" +``` + +--- + +### Task 3: Manifests, version 0.3.0, CHANGELOG entry, and the manifest-agreement test + +**Files:** +- Create: `plugin.json`, `.codex-plugin/plugin.json` +- Modify: `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, `pyproject.toml:3-4`, `CHANGELOG.md`, `tests/test_portability.py` (append) + +**Interfaces:** +- Consumes: `frontmatter`, `ROOT` from Task 2. +- Produces: four manifests + `pyproject.toml` + CHANGELOG at 0.3.0; `MANIFEST_VERSION_PATHS` constant in the test module used by Task 4's packager test. + +- [ ] **Step 1: Write the failing test** (append to `tests/test_portability.py`) + +```python +import json # noqa: E402 (keep imports at the top of the file in the real edit) + +AGENT_PLUGINS_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json" +CHANGELOG_HEADING_RE = re.compile(r"^## \[(\d+\.\d+\.\d+)\]", re.M) + + +def _json(rel: str) -> dict: + return json.loads((ROOT / rel).read_text(encoding="utf-8")) + + +def _pyproject_version() -> str: + text = (ROOT / "pyproject.toml").read_text(encoding="utf-8") + return re.search(r'^version = "([^"]+)"', text, re.M).group(1) + + +def test_manifests_agree(): + root = _json("plugin.json") + claude = _json(".claude-plugin/plugin.json") + codex = _json(".codex-plugin/plugin.json") + market = _json(".claude-plugin/marketplace.json") + entry = market["plugins"][0] + for m in (root, claude, codex, entry): + assert m["name"] == "humanize" + versions = { + "plugin.json": root["version"], + ".claude-plugin/plugin.json": claude["version"], + ".codex-plugin/plugin.json": codex["version"], + "marketplace.metadata": market["metadata"]["version"], + "marketplace.plugins[0]": entry["version"], + "pyproject.toml": _pyproject_version(), + } + assert len(set(versions.values())) == 1, versions + changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") + newest = CHANGELOG_HEADING_RE.search(changelog).group(1) # [Unreleased] has no digits + assert newest == root["version"], (newest, root["version"]) + assert root["$schema"] == AGENT_PLUGINS_SCHEMA + assert (ROOT / codex["skills"] / "humanize/SKILL.md").is_file() + assert (ROOT / entry["source"] / ".claude-plugin/plugin.json").is_file() + assert entry["strict"] is True + for m in (root, claude, codex, entry): + assert "storyscope" not in json.dumps(m).lower() +``` + +Move `import json` to the top of the module with the other imports. + +- [ ] **Step 2: Run it to verify it fails** + +Run: `uv run pytest -q tests/test_portability.py::test_manifests_agree 2>&1 | tail -2` +Expected: FAIL — `FileNotFoundError` for `plugin.json`. + +- [ ] **Step 3: Write the manifests and bump the version** + +`plugin.json` (new, repo root): +```json +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "humanize", + "version": "0.3.0", + "description": "Audits a draft layer by layer (vocabulary, punctuation, sentence shape, openers, closers, grammar, narrative, per-model habits) against 13 studies, then rewrites what it flags in the author's voice.", + "author": { "name": "ccf" }, + "license": "MIT", + "homepage": "https://github.com/ccf/humanize", + "repository": "https://github.com/ccf/humanize", + "keywords": ["humanize", "writing", "prose", "editing", "style", "ai-writing", "rewriting", "linguistics"] +} +``` + +`.codex-plugin/plugin.json` (new): +```json +{ + "name": "humanize", + "version": "0.3.0", + "description": "Audits a draft layer by layer (vocabulary, punctuation, sentence shape, openers, closers, grammar, narrative, per-model habits) against 13 studies, then rewrites what it flags in the author's voice.", + "author": { "name": "ccf" }, + "homepage": "https://github.com/ccf/humanize", + "repository": "https://github.com/ccf/humanize", + "license": "MIT", + "keywords": ["humanize", "writing", "prose", "editing", "style", "ai-writing", "rewriting", "linguistics"], + "skills": "./skills/", + "interface": { + "displayName": "humanize", + "shortDescription": "Audit prose for AI tells at every layer, then rewrite it in the author's voice.", + "longDescription": "Reads a draft against what 13 studies measured about AI writing: word choice, punctuation, sentence shape, openers, closers, grammar, register, fiction habits, per-model quirks. A dependency-free scanner supplies the counts a model cannot eyeball, and each flag cites the sentence that triggered it.", + "developerName": "ccf", + "category": "Developer Tools", + "capabilities": ["Read", "Write"], + "defaultPrompt": ["Humanize this draft.", "Audit this text for AI tells, don't rewrite it."], + "websiteURL": "https://github.com/ccf/humanize" + } +} +``` + +`.claude-plugin/plugin.json`: `"version": "0.3.0"`; `"description"` → the same `` string as above; `"keywords"` → the same eight. + +`.claude-plugin/marketplace.json`: `metadata.description` → `Measures prose against 13 studies of how AI and human writing differ, from word choice to sentence rhythm to story shape, then rewrites the tells out.`; `metadata.version` and `plugins[0].version` → `0.3.0`; `plugins[0].description` → ``; `plugins[0].keywords` → the eight. `source` stays `"./"`, `strict` stays `true`. + +`pyproject.toml` lines 3–4: +```toml +version = "0.3.0" +description = "Agent skill that audits prose for AI tells and rewrites it in the author's voice, on evidence from 13 studies." +``` + +`CHANGELOG.md`: under `## [Unreleased]` insert +```markdown +## [0.3.0] - 2026-09-14 + +### Added +- Agent Plugins 1.0 manifest (`plugin.json`) and Codex manifest (`.codex-plugin/plugin.json`), + so the plugin installs into Codex, Cursor, and Hermes with their own tooling. +- `tests/test_portability.py`: spec-only frontmatter, no harness-specific content under + `skills/`, runtime references named, guard-clean scripts, manifests and CHANGELOG in + version lockstep, zip packager contract, study count matches `SOURCES.md`. +- `tools/package_skill_zip.py`: builds the claude.ai skill upload from the plugin. +- `tools/smoke_harnesses.sh`: local acceptance matrix with scanner-evidence PASS rule; + transcripts under `docs/acceptance/v0.3/`. +- README install table for Claude Code, Codex CLI, Cursor, Hermes Agent, Claude Desktop. + +### Changed +- Repository layout: the plugin now lives at the root (`skills/humanize/`); the marketplace + `source` is `./`. +- `SKILL.md` is harness-agnostic: relative `scripts/surface_scan.py` path, plain-words options, + document extraction described generically; `argument-hint` removed (a hard error outside + Claude Code). The `/plugin` install lines moved to the README. +- Public copy describes humanize as grounded in the thirteen studies in + `references/SOURCES.md` and covering every layer of a text; StoryScope is named where it is + the source of a number. +- `pyproject.toml` version now tracks the plugin version. + +### Migration +- Existing Claude Code installs: `claude plugin marketplace update humanize && claude plugin + update humanize@humanize`. +``` +and add `[0.3.0]: https://github.com/ccf/humanize/compare/v0.2.0...v0.3.0` above the `[0.2.0]:` link; edit the `[Unreleased]:` link to `…/compare/v0.3.0...HEAD`. + +- [ ] **Step 4: Run the tests to verify they pass** + +Run: `uv run pytest -q -W error 2>&1 | tail -1` → `104 passed`. +Run: `claude plugin validate --strict . 2>&1 | tail -1` → passes. +Run: `python3 -c "import json;[json.load(open(p)) for p in ('plugin.json','.codex-plugin/plugin.json','.claude-plugin/plugin.json','.claude-plugin/marketplace.json')];print('json ok')"`. +Run: `grep -rn '0\.2\.0' plugin.json .claude-plugin .codex-plugin pyproject.toml` → nothing. + +- [ ] **Step 5: Commit** + +```bash +uv run ruff format tests/test_portability.py && uv run ruff check tests/test_portability.py +git add plugin.json .codex-plugin/plugin.json .claude-plugin pyproject.toml CHANGELOG.md tests/test_portability.py +git commit -m "Add Agent Plugins and Codex manifests; version 0.3.0 in lockstep + +Co-Authored-By: Claude Fable 5.1 +Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh" +``` + +--- + +### Task 4: Guard-pattern test and the claude.ai zip packager + +**Files:** +- Create: `tools/package_skill_zip.py` +- Modify: `tests/test_portability.py` (append), `.gitignore` (add `dist/`) + +**Interfaces:** +- Produces: `package_skill_zip.build_zip(repo_root: Path, out_dir: Path) -> Path`; `package_skill_zip.SPEC_FIELDS`; CLI `python3 tools/package_skill_zip.py [--out dist]`. + +- [ ] **Step 1: Write the failing tests** (append) + +```python +import importlib.util # top of module +import sys +import zipfile + +GUARD_TOKENS = ( + "subprocess", + "os.system", + "os.popen", + "os.environ", + "getenv", + "shutil.rmtree", + "eval(", + "exec(", + "compile(", + "getattr(", + "codecs", + "socket", + "urllib", + "requests", + "# /// script", +) + + +def _guard_hits(text: str) -> list[str]: + return [t for t in GUARD_TOKENS if t in text] + + +def test_scripts_are_guard_clean(): + """Stricter than Hermes's skills_guard.py line regexes; also our no-network rule.""" + scripts = sorted((ROOT / "skills").rglob("scripts/*.py")) + assert scripts, "no scripts found" + for path in scripts: + hits = _guard_hits(path.read_text(encoding="utf-8")) + assert not hits, f"{path.relative_to(ROOT)}: {hits}" + assert _guard_hits("import subprocess\nos.environ['X']") == ["subprocess", "os.environ"] + + +def _load_packager(): + spec = importlib.util.spec_from_file_location("pkg", ROOT / "tools/package_skill_zip.py") + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +def test_zip_packager_contract(tmp_path): + pkg = _load_packager() + out = pkg.build_zip(ROOT, tmp_path) + version = _json("plugin.json")["version"] + assert out.name == f"humanize-skill-{version}.zip" + with zipfile.ZipFile(out) as zf: + members = set(zf.namelist()) + expected = {"humanize/SKILL.md", "humanize/scripts/surface_scan.py"} + expected |= {f"humanize/references/{p.name}" for p in (SKILL_DIR / "references").iterdir()} + assert members == expected, members ^ expected + + +def test_zip_packager_rejects_non_spec_frontmatter(tmp_path): + pkg = _load_packager() + fake = tmp_path / "repo" + (fake / "skills/humanize/references").mkdir(parents=True) + (fake / "skills/humanize/scripts").mkdir() + (fake / "plugin.json").write_text('{"name": "humanize", "version": "9.9.9"}') + (fake / "skills/humanize/scripts/surface_scan.py").write_text("print(1)\n") + (fake / "skills/humanize/SKILL.md").write_text( + "---\nname: humanize\ndescription: x\nargument-hint: y\n---\nbody\n" + ) + try: + pkg.build_zip(fake, tmp_path / "out") + except pkg.PackagingError as e: + assert "argument-hint" in str(e) + else: + raise AssertionError("expected PackagingError") +``` + +- [ ] **Step 2: Run to verify** — `uv run pytest -q tests/test_portability.py -k "guard or zip"` → guard test passes (the script is clean; the negative assertion exercises the checker), the two zip tests fail with `FileNotFoundError` for `tools/package_skill_zip.py`. + +- [ ] **Step 3: Write the packager** + +`tools/package_skill_zip.py`: + +```python +#!/usr/bin/env python3 +"""Build the claude.ai / Claude Desktop skill upload for humanize. + +Produces dist/humanize-skill-.zip whose root is the skill folder (named exactly +`name`), the layout Anthropic's uploader and package_skill.py expect. Standard library only. +""" + +from __future__ import annotations + +import argparse +import json +import sys +import zipfile +from pathlib import Path + +SKILL = "humanize" +SPEC_FIELDS = {"name", "description", "license", "compatibility", "metadata", "allowed-tools"} + + +class PackagingError(Exception): + pass + + +def _frontmatter_keys(skill_md: Path) -> set[str]: + text = skill_md.read_text(encoding="utf-8") + if not text.startswith("---\n"): + raise PackagingError("SKILL.md has no frontmatter") + block = text.split("\n---\n", 1)[0][4:] + keys = set() + for line in block.splitlines(): + if line.strip() and not line.startswith(" "): + keys.add(line.partition(":")[0].strip()) + return keys + + +def build_zip(repo_root: Path, out_dir: Path) -> Path: + skill_dir = repo_root / "skills" / SKILL + skill_md = skill_dir / "SKILL.md" + extra = _frontmatter_keys(skill_md) - SPEC_FIELDS + if extra: + raise PackagingError( + "non-spec frontmatter keys would fail the upload: " + ", ".join(sorted(extra)) + ) + version = json.loads((repo_root / "plugin.json").read_text(encoding="utf-8"))["version"] + members = [skill_md, skill_dir / "scripts" / "surface_scan.py"] + members += sorted(p for p in (skill_dir / "references").iterdir() if p.is_file()) + out_dir.mkdir(parents=True, exist_ok=True) + out = out_dir / f"{SKILL}-skill-{version}.zip" + with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zf: + for path in members: + zf.write(path, f"{SKILL}/{path.relative_to(skill_dir).as_posix()}") + return out + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--out", default="dist", help="output directory (default: dist)") + args = parser.parse_args(argv) + root = Path(__file__).resolve().parents[1] + try: + out = build_zip(root, root / args.out) + except PackagingError as e: + print(f"error: {e}", file=sys.stderr) + return 1 + print(out) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) +``` + +Add `dist/` to `.gitignore`. + +- [ ] **Step 4: Verify** + +Run: `uv run pytest -q -W error 2>&1 | tail -1` → `107 passed`. +Run: `uv run --python 3.9 pytest -q -W error 2>&1 | tail -1` → `107 passed` (the packager's `list[str] | None` annotation is fine under `from __future__ import annotations`). +Run: `python3 tools/package_skill_zip.py && unzip -l dist/humanize-skill-0.3.0.zip | tail -12` → 8 members, all under `humanize/`. + +- [ ] **Step 5: Commit** + +```bash +uv run ruff format tools/package_skill_zip.py tests/test_portability.py && uv run ruff check tools/package_skill_zip.py tests/test_portability.py +git add tools/package_skill_zip.py tests/test_portability.py .gitignore +git commit -m "Add guard-clean script test and the claude.ai skill zip packager + +Co-Authored-By: Claude Fable 5.1 +Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh" +``` + +--- + +### Task 5: Positioning copy in README, CLAUDE.md, BUGBOT.md, and the study-count test + +**Files:** +- Modify: `README.md` (lines 3–17, tree lines for style/narrative tells), `CLAUDE.md:3-4`, `.cursor/BUGBOT.md:3-5`, `tests/test_portability.py` (append) + +- [ ] **Step 1: Write the failing test** (append) + +```python +NUMBER_WORDS = { + "eleven": 11, + "twelve": 12, + "thirteen": 13, + "fourteen": 14, + "fifteen": 15, + "sixteen": 16, + "seventeen": 17, + "eighteen": 18, + "nineteen": 19, + "twenty": 20, +} +STUDY_COUNT_SURFACES = { + "plugin.json": r"against (\d+) studies", + ".claude-plugin/plugin.json": r"against (\d+) studies", + ".codex-plugin/plugin.json": r"(\d+) studies", + ".claude-plugin/marketplace.json": r"(\d+) studies", + "pyproject.toml": r"from (\d+) studies", + "CLAUDE.md": r"Evidence base: (\d+) studies", + "README.md": r"rest on (\w+) studies", + "skills/humanize/SKILL.md": r"Grounded in (\w+) studies", +} + + +def test_study_count_matches_sources(): + sources = (SKILL_DIR / "references/SOURCES.md").read_text(encoding="utf-8") + n = len(re.findall(r"^## `[a-z0-9-]+`", sources, re.M)) + assert n >= 10 + for rel, pattern in STUDY_COUNT_SURFACES.items(): + text = (ROOT / rel).read_text(encoding="utf-8") + found = re.findall(pattern, text) + assert found, f"{rel}: no study count matching {pattern!r}" + for token in found: + value = int(token) if token.isdigit() else NUMBER_WORDS[token.lower()] + assert value == n, f"{rel} says {token}, SOURCES.md has {n}" +``` + +- [ ] **Step 2: Run to verify it fails** — `uv run pytest -q tests/test_portability.py::test_study_count_matches_sources 2>&1 | tail -2` → FAIL on `CLAUDE.md` (no "Evidence base: 13 studies" yet) or `README.md`. + +- [ ] **Step 3: Apply the copy** + +`README.md` lines 3–5 (the paragraph under `# humanize`): +``` +An agent skill that audits prose for the tells of AI writing and rewrites them +out, without flattening the author's voice. Runs in Claude Code, Codex, Cursor, +Hermes Agent, Claude Desktop. +``` + +`README.md` lines 7–17 (the paragraph beginning `It is grounded in [StoryScope]` and ending `every cited number resolves in `references/SOURCES.md`.`): +``` +The reference entries rest on thirteen studies. StoryScope (2026) measured 304 +narrative and stylistic features over 61,575 stories, and its widest human-vs-AI +gaps supply the base rates. Reinhart et al. (2025) put participial modifiers at +5.3 times the human rate and nominalization at 2.1 times. Jakesch et al. (2023) +found repeated phrasing the strongest true signal of a text's source. Herbold et +al. (2023) recorded a lexical-diversity reversal between model generations, a +reminder that findings expire. humanize turns that work into an audit checklist +plus a dependency-free scanner for the counts a model cannot eyeball; every flag +quotes the line it came from. One pattern organizes the whole checklist: AI +converges on shared defaults while human writing disperses. Every cited number +resolves in `references/SOURCES.md`. +``` + +`README.md` tree: `20 StoryScope style features with base rates` → `20 style features with StoryScope base rates`; `57 StoryScope narrative features (fiction only)` → `57 narrative features (fiction only), StoryScope base rates`. + +`CLAUDE.md` lines 3–4: +``` +Agent skill that audits prose for AI tells and rewrites it. Evidence base: 13 studies +in `references/SOURCES.md`. Spec and plan: `docs/design/`. Changelog: `CHANGELOG.md`. +``` + +`.cursor/BUGBOT.md` lines 3–5: +``` +This repo is an agent skill that audits prose for AI tells and rewrites it, on +evidence from the studies in `skills/humanize/references/SOURCES.md`. Design specs: +`docs/design/2026-09-1{3,4}-humanize-*-design.md`. +``` + +- [ ] **Step 4: Verify** + +Run: `uv run pytest -q -W error 2>&1 | tail -1` → `108 passed`. +Run: `grep -in storyscope plugin.json .codex-plugin/plugin.json .claude-plugin/*.json pyproject.toml skills/humanize/SKILL.md; sed -n 1,20p README.md | grep -ic storyscope` → nothing, then `1` (the single attributed mention in the grounding paragraph is expected; the tagline has none). +Run the copy through our own scanner: +```bash +sed -n 3,17p README.md > /tmp/humanize-readme-copy.md +python3 skills/humanize/scripts/surface_scan.py --text /tmp/humanize-readme-copy.md | grep -E 'wordlist|structures' +``` +→ `wordlist: 0.0/1k — none` and `not-but 0`. + +- [ ] **Step 5: Commit** + +```bash +uv run ruff format tests/test_portability.py && uv run ruff check tests/test_portability.py +git add README.md CLAUDE.md .cursor/BUGBOT.md tests/test_portability.py +git commit -m "Reposition humanize as research-grounded across every text layer + +Co-Authored-By: Claude Fable 5.1 +Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh" +``` + +--- + +### Task 6: README install table and the CLAUDE.md / BUGBOT.md invariants and workflow + +**Files:** +- Modify: `README.md` (Install section, lines 20–36), `CLAUDE.md` (Commands, Invariants, Workflow), `.cursor/BUGBOT.md` (Invariants) + +- [ ] **Step 1: README Install section** + +Replace everything from `## Install` through the closing fence before `## Use` with: + +````markdown +## Install + +Requires Python 3.9+ on `PATH` for the scanner. No other dependencies. Every +command below was checked against its CLI's `--help` for this release. + +| Harness | Install | Verify | Invoke | +|---|---|---|---| +| Claude Code | `/plugin marketplace add ccf/humanize` then `/plugin install humanize@humanize` | `claude plugin list` | `/humanize …` or automatically | +| Codex CLI | `codex plugin marketplace add ccf/humanize` then `codex plugin add humanize@humanize` — or `cp -R skills/humanize ~/.agents/skills/` | `codex debug prompt-input "hi"` lists the skill | `$humanize …` or automatically | +| Cursor | `cp -R skills/humanize ~/.cursor/skills/` (every project) or `.cursor/skills/` (this project) | the skill appears in the `/` menu | `/humanize …` or automatically | +| Hermes Agent | `hermes skills install ccf/humanize/humanize --category writing`; in a running session, `/reload-skills` | `hermes skills list` | `/humanize …` or automatically | +| Any Agent-Skills harness | `npx skills add ccf/humanize` or `cp -R skills/humanize ~/.agents/skills/` | harness-specific | harness-specific | +| Claude Desktop — Cowork | Customize → Plugins → Add from repository `ccf/humanize` → install `humanize` | listed under Customize → Plugins | `/` or `+` picker, or automatically | +| Claude Desktop — chat (claude.ai) | Customize → Skills → upload `humanize-skill-.zip` from the [latest release](https://github.com/ccf/humanize/releases); "Code execution and file creation" must be on | listed under Customize → Skills | automatically, or the sidebar `/` menu | + +Notes: + +- **Codex** reads `/.agents/skills/` and `~/.agents/skills/` but not `.claude/skills/`, and + caps the injected skills catalog at 2% of the context window; a long catalog drops skills. +- **Cursor** syncs only `~/.cursor/skills/` to Cloud Agents, and only with **Sync Skills for Cloud + Agents** on (Settings → Agents). The Cursor marketplace is reviewed by hand and has no CLI. +- **Hermes** scans installed scripts (`surface_scan.py` is standard-library and passes) and runs + the scanner on the host, so on a remote terminal backend (docker, modal, ssh) the scan step is + skipped and the audit proceeds from reading alone. `hermes plugins install` also works on + recent builds but installs the package disabled and read-only; prefer `hermes skills install`. + Installing from a raw `SKILL.md` URL fetches one file and is not supported. +- **Claude Desktop** picks up a Cowork plugin update only when `version` changes. In claude.ai + chat the skill triggers by description; picking it from the `/` menu passes no arguments. +- **Word, PDF, PowerPoint inputs.** humanize delegates extraction to the harness's document + skills. In Claude Code that is Anthropic's `document-skills` plugin (`/plugin marketplace add + anthropics/skills`, `/plugin install document-skills@anthropic-agent-skills`; its `docx` read + path uses `pandoc` — `brew install pandoc` on macOS); Claude Desktop ships the same skills. +```` + +- [ ] **Step 2: CLAUDE.md** + +Commands block: `uv run pytest -q # must be warning-free` (drop the count); `claude plugin validate --strict . # marketplace JSON only; warns about this file (accepted)`. + +Invariants: replace `- Nothing under `tests/` or `plugins/**/scripts/` touches the network.` with `- Nothing under `tests/`, `skills/**/scripts/`, or `tools/package_skill_zip.py` touches the network.`; replace the marketplace bullet's `Bump `version` in both when shipping.` with `Version lives in `plugin.json`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, both `marketplace.json` fields, `pyproject.toml`, and the newest CHANGELOG heading — `tests/test_portability.py` fails on drift.`; add: +``` +- `skills/` is harness-agnostic: no harness variables (`${CLAUDE_…}`, `HERMES_SKILL_DIR`, + `CURSOR_…`, `CODEX_…`) and no install commands inside it; frontmatter is exactly the Agent + Skills fields (`argument-hint` is a hard error outside Claude Code). `tests/test_portability.py` + enforces both. +- Public copy names StoryScope only where it is the specific source of a number or a file; the + tool is described as grounded in the `SOURCES.md` registry (the study count is tested). +``` +Workflow: after the merge sentence add: `Release: `tools/smoke_harnesses.sh` must PASS on every harness installed here (transcripts under `docs/acceptance/`), then merge, then post-merge acceptance from `main` (Hermes install, Claude plugin upgrade, Cowork), then tag and `gh release create vX.Y.Z dist/humanize-skill-X.Y.Z.zip` (built by `python3 tools/package_skill_zip.py`). Anything found after the merge is fixed forward as a patch release.` + +- [ ] **Step 3: BUGBOT.md** — mirror the harness-agnostic invariant and the version-lockstep invariant as two bullets under "Invariants to enforce"; the network bullet's glob is already `skills/**/scripts/` from Task 1. + +- [ ] **Step 4: Verify** — `uv run pytest -q -W error | tail -1` → `108 passed`; `grep -n 'plugins/' README.md CLAUDE.md .cursor/BUGBOT.md` → only the `Customize → Plugins` GUI text; `grep -c 'undetectable\|certified human' README.md` → the two existing principle-line mentions only (they say we never claim it). + +- [ ] **Step 5: Commit** + +```bash +git add README.md CLAUDE.md .cursor/BUGBOT.md +git commit -m "Document installation per harness; portability and version invariants + +Co-Authored-By: Claude Fable 5.1 +Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh" +``` + +--- + +### Task 7: Smoke matrix script and pre-merge acceptance runs + +**Files:** +- Create: `tools/smoke_harnesses.sh`, `docs/acceptance/v0.3/README.md`, `docs/acceptance/v0.3/.md` per run + +**Side effects (read before running):** Codex runs use a temp project with `--ephemeral` (nothing under `$HOME`). Cursor runs use a temp project; the script never updates the CLI — if `agent --help` lacks `-p`/`--print`, it prints SKIP with the installed version. Hermes has no project scope on v0.11.0: the script copies into `~/.hermes/skills/writing/humanize`, aborts if that directory already exists, and removes only what it created. Acceptance 3 (`codex plugin marketplace add ccf/humanize@feat/v0.3`) adds a marketplace to the user's Codex config; remove it afterwards with `codex plugin remove humanize@humanize` and `codex plugin marketplace remove humanize` (check `codex plugin marketplace --help` for the exact verb). + +- [ ] **Step 1: Write the script** + +`tools/smoke_harnesses.sh`: + +```bash +#!/usr/bin/env bash +# Local acceptance matrix for the humanize skill. Not run by CI or pytest. +# PASS needs evidence the skill AND the scanner ran (spec §5), not row names alone. +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +FIXTURE="tests/fixtures/ai_report.txt" +OUT="$ROOT/docs/acceptance/v0.3" +REQUEST="Use the humanize skill on $FIXTURE. Audit only — do not rewrite." +mkdir -p "$OUT" + +# Expected rows (case-insensitive regex) and two scanner numbers that must appear verbatim. +ROWS=('trailing participial clause' 'verbatim repetition' 'container-noun phrase' 'safety disclaimer opener') +SCAN="$(python3 "$ROOT/skills/humanize/scripts/surface_scan.py" --text "$ROOT/$FIXTURE")" +NUM_TAILS="$(printf '%s\n' "$SCAN" | sed -n 's/^grammar: participial tails \([0-9]*\).*/\1/p')" +NUM_REP="$(printf '%s\n' "$SCAN" | sed -n 's/^repetition: \([0-9.]*\)\/1k.*/\1/p')" + +judge() { # judge + local h="$1" f="$2" tool_re="$3" ok=1 + for r in "${ROWS[@]}"; do grep -qiE "$r" "$f" || { echo " missing row: $r"; ok=0; }; done + grep -iE '^\|' "$f" | grep -qi 'nominalization' && { echo " nominalization row present"; ok=0; } + grep -q "participial tails $NUM_TAILS" "$f" || grep -q "$NUM_TAILS" "$f" || { echo " scanner count $NUM_TAILS absent"; ok=0; } + grep -q "$NUM_REP" "$f" || { echo " scanner rate $NUM_REP absent"; ok=0; } + [ -n "$tool_re" ] && { grep -qE "$tool_re" "$f" || { echo " no tool event naming surface_scan.py"; ok=0; }; } + grep -qiE 'Choices you may want to reverse|^## Rewrite' "$f" && { echo " rewrite section present"; ok=0; } + [ "$ok" = 1 ] && echo "PASS $h" || echo "FAIL $h" +} + +run_claude() { + command -v claude >/dev/null || { echo "SKIP claude (not installed)"; return; } + local f="$OUT/claude-code.md" + claude plugin disable humanize@humanize >/dev/null 2>&1 || true + (cd "$ROOT" && claude -p "$REQUEST" --plugin-dir . --output-format stream-json --verbose \ + --allowedTools "Bash,Read,Glob,Grep" > "$f.jsonl" 2>&1) + claude plugin enable humanize@humanize >/dev/null 2>&1 || true + python3 - "$f.jsonl" "$f" <<'EOF' +import json, sys +src, dst = sys.argv[1], sys.argv[2] +texts, tools = [], [] +for line in open(src, encoding="utf-8"): + try: ev = json.loads(line) + except ValueError: continue + for blk in (ev.get("message") or {}).get("content", []) if isinstance(ev, dict) else []: + if blk.get("type") == "text": texts.append(blk["text"]) + if blk.get("type") == "tool_use": tools.append(json.dumps(blk.get("input"))[:300]) + if ev.get("type") == "result" and ev.get("result"): texts.append(ev["result"]) +open(dst, "w", encoding="utf-8").write("# Claude Code\n\n## Tool calls\n" + "\n".join(tools) + "\n\n## Output\n" + "\n".join(texts)) +EOF + judge "claude-code" "$f" 'surface_scan\.py' +} + +run_codex() { + command -v codex >/dev/null || { echo "SKIP codex (not installed)"; return; } + local tmp f="$OUT/codex.md"; tmp="$(mktemp -d)" + mkdir -p "$tmp/.agents/skills" && cp -R "$ROOT/skills/humanize" "$tmp/.agents/skills/" && cp "$ROOT/$FIXTURE" "$tmp/" + (cd "$tmp" && codex debug prompt-input "hi" 2>/dev/null | grep -qi humanize) || echo " warning: skill not listed by codex debug prompt-input" + (cd "$tmp" && codex exec -C "$tmp" --skip-git-repo-check --ephemeral -s read-only --json \ + -o "$tmp/out.md" "Use the humanize skill on ai_report.txt. Audit only — do not rewrite." "$tmp/events.jsonl" 2>&1) + { echo "# Codex"; echo; echo "## Tool events"; grep -i 'surface_scan' "$tmp/events.jsonl" | head -5; echo; echo "## Output"; cat "$tmp/out.md" 2>/dev/null; } > "$f" + judge "codex" "$f" 'surface_scan\.py' + rm -rf "$tmp" +} + +run_cursor() { + command -v agent >/dev/null || { echo "SKIP cursor (agent CLI not installed)"; return; } + agent --help 2>/dev/null | grep -qE -- '(^| )-p[ ,]|--print' || { echo "SKIP cursor (agent $(agent --version 2>/dev/null | head -1) has no headless flag)"; return; } + local tmp f="$OUT/cursor.md"; tmp="$(mktemp -d)" + mkdir -p "$tmp/.cursor/skills" && cp -R "$ROOT/skills/humanize" "$tmp/.cursor/skills/" && cp "$ROOT/$FIXTURE" "$tmp/" + (cd "$tmp" && agent -p "Use the humanize skill on ai_report.txt. Audit only — do not rewrite." > "$f" 2>&1) + judge "cursor" "$f" '' + rm -rf "$tmp" +} + +run_hermes() { + command -v hermes >/dev/null || { echo "SKIP hermes (not installed)"; return; } + local dest="$HOME/.hermes/skills/writing/humanize" f="$OUT/hermes.md" + [ -e "$dest" ] && { echo "SKIP hermes ($dest already exists; not touching it)"; return; } + mkdir -p "$(dirname "$dest")" && cp -R "$ROOT/skills/humanize" "$dest" + if hermes --help 2>/dev/null | grep -q -- ' -z'; then + hermes -z "$REQUEST" > "$f" 2>&1 + else + hermes chat -q "$REQUEST" -Q > "$f" 2>&1 + fi + rm -rf "$dest" + judge "hermes" "$f" '' +} + +for h in "${@:-claude codex cursor hermes}"; do "run_$h"; done +``` + +`chmod +x tools/smoke_harnesses.sh`. Write `docs/acceptance/v0.3/README.md`: what the transcripts are, the PASS rule, the date, and the CLI versions (`claude --version`, `codex --version`, `agent --version`, `hermes --version`). + +- [ ] **Step 2: Pin the flags before running** + +Run `codex exec --help`, `agent --help`, `hermes --help`, `hermes chat --help` and confirm every flag the script uses exists on the installed versions; adjust the script where a flag differs and note it in the report. The Hermes non-interactive flag is the one most likely to differ. + +- [ ] **Step 3: Run** + +`tools/smoke_harnesses.sh claude codex` first (no side effects beyond a temp dir), then `tools/smoke_harnesses.sh cursor hermes`. Paste each PASS/FAIL line into the report with the reason lines. Two attempts per harness; a FAIL after two attempts is reported, not fixed here. + +- [ ] **Step 4: Acceptance 3 (pre-merge, Codex from GitHub)** + +```bash +git push origin feat/v0.3 +codex plugin marketplace add ccf/humanize@feat/v0.3 +codex plugin add humanize@humanize +codex debug prompt-input "hi" | grep -i humanize +codex plugin remove humanize@humanize +``` +Record the exact output lines. If `add` reports a manifest problem, that is a finding for the controller, not something to patch silently. + +- [ ] **Step 5: Commit** + +```bash +git add tools/smoke_harnesses.sh docs/acceptance/v0.3 +git commit -m "Add the harness smoke matrix and v0.3 acceptance transcripts + +Co-Authored-By: Claude Fable 5.1 +Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh" +``` + +--- + +### Task 8: Spec status and final gate + +**Files:** +- Modify: `docs/design/2026-09-14-humanize-v0.3-design.md:4` (status line) + +- [ ] **Step 1:** Change `Status: draft for review (revision 2).` to `Status: implemented on feat/v0.3 as 0.3.0 (revision 2); phased acceptance per §7 — pre-merge items recorded in docs/acceptance/v0.3/.` + +- [ ] **Step 2: Gate** + +`uv run pytest -q -W error` → `108 passed`; `uv run --python 3.9 pytest -q -W error` → `108 passed`; `uv run ruff check . && uv run ruff format --check .`; `claude plugin validate --strict .`; `wc -l skills/humanize/SKILL.md` ≤ 150; `python3 tools/package_skill_zip.py`; `grep -rn 'plugins/humanize' . --include='*.md' --include='*.py' --include='*.json' --include='*.toml' | grep -v -E 'docs/design|CHANGELOG|\.superpowers'` → nothing. + +- [ ] **Step 3: Commit** + +```bash +git add docs/design/2026-09-14-humanize-v0.3-design.md +git commit -m "Mark the v0.3 design implemented + +Co-Authored-By: Claude Fable 5.1 +Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh" +``` + +--- + +## Self-review notes + +- Spec coverage: §1 → T1, T3; §2 → T2; §3 tests 1–3 → T2, 5 → T3, 4 and 6 → T4, 7 → T5; §4 → T5 (copy), T6 (install/invariants); §5 → T7; §6 → T4 (packager) + T7/§7 phases (Cowork and zip upload are post-merge/post-release, controller-run); §7 → T3 (CHANGELOG) + T6 (workflow) + controller release steps; §8 acceptance 1, 2, 3, 7 → T7/T8, 4–6 → post-merge; §9 → T3, T5; §10 → T7 records outcomes. +- Test ladder: 100 → T2 103 → T3 104 → T4 107 → T5 108. +- Type consistency: `frontmatter()`, `SKILL_DIR`, `_json()` defined in T2/T3 and used in T4/T5 in the same module; `PackagingError` and `build_zip(repo_root, out_dir)` match between T4's test and implementation. +- Rulings recorded here for the ledger: `pyproject.toml` joins the version-lockstep set; `.cursor/BUGBOT.md` line 3 sentence is the plan's own wording (spec said "mirrors"). From 5a67cc7b779f671325166f2d68115e770f3ba016 Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 10:23:56 -0400 Subject: [PATCH 06/22] v0.3 plan revision 2: apply the dry-run findings Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- docs/design/2026-09-14-humanize-v0.3-plan.md | 49 ++++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/docs/design/2026-09-14-humanize-v0.3-plan.md b/docs/design/2026-09-14-humanize-v0.3-plan.md index 9d622f6..57cc3df 100644 --- a/docs/design/2026-09-14-humanize-v0.3-plan.md +++ b/docs/design/2026-09-14-humanize-v0.3-plan.md @@ -18,7 +18,9 @@ - SKILL.md: `wc -l` ≤ 150 after Task 2 (expected exactly 150). Frontmatter keys after Task 2 are exactly `name` and `description`. No `${CLAUDE_`, `CLAUDE_PLUGIN_ROOT`, `HERMES_SKILL_DIR`, `CURSOR_`, `CODEX_`, `/plugin `, or `npx skills` anywhere under `skills/`. - Never write that output is "undetectable", passes a detector, or is "certified human" — anywhere, including README and manifests. - Curly characters in existing literals (’ “ ” — –) must survive; verify SKILL.md and README edits with `grep -c` before/after where a block contains them. Use heredocs for Python probes. -- Version `0.3.0` appears in exactly these places and nowhere else new: `plugin.json`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, `.claude-plugin/marketplace.json` (both fields), `pyproject.toml`, `CHANGELOG.md` heading. (Ruling: `pyproject.toml` joins the sync set — it is 0.1.0 today, which is drift.) +- Version `0.3.0` appears in exactly these places and nowhere else new: `plugin.json`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, `.claude-plugin/marketplace.json` (both fields), `pyproject.toml`, `CHANGELOG.md` heading. (Ruling: `pyproject.toml` joins the sync set — it is 0.1.0 today, which is drift.) Bumping `pyproject.toml` makes the pre-commit `pytest` hook (`uv run`) rewrite `uv.lock`'s embedded project version; stage `uv.lock` in the same commit or the hook rolls the commit back. +- The dry run confirmed `claude plugin validate --strict .` (CLI 2.1.270) passes cleanly at the new layout with no warning; do not expect or record one. +- `grep -r` here is `ugrep`, which prints paths without a `./` prefix; exclusion patterns use `^(\./)?(…)`. - Public copy strings in Tasks 3 and 5 are the spec §9 strings verbatim — copy, never retype. - Tooling: `uv run pytest -q -W error`; `uv run --python 3.9 pytest -q -W error` once per task; before committing `uv run ruff format && uv run ruff check ` on the Python files you touched (never bare `.`); hooks run on commit; never `--no-verify`. Branch `feat/v0.3`, repo `/Users/ccf/git/humanize`. - Every commit message ends with: @@ -84,13 +86,13 @@ SCRIPTS = Path(__file__).resolve().parents[1] / "skills/humanize/scripts" `.cursor/BUGBOT.md` line 9: `` `plugins/humanize/skills/humanize/scripts/surface_scan.py` `` → `` `skills/humanize/scripts/surface_scan.py` ``; line 13 `plugins/**/scripts/` → `skills/**/scripts/`; line 14 `` `plugins/humanize/skills/humanize/references/*.md` `` → `` `skills/humanize/references/*.md` ``. -Then: `grep -rn 'plugins/humanize' --include='*.md' --include='*.py' --include='*.json' --include='*.toml' --include='*.yml' --include='*.yaml' . | grep -v -E '^\./(docs/design|CHANGELOG\.md|\.superpowers)'` must print nothing. +Then: `grep -rn 'plugins/humanize' --include='*.md' --include='*.py' --include='*.json' --include='*.toml' --include='*.yml' --include='*.yaml' . | grep -v -E '^(\./)?(docs/design|CHANGELOG\.md|\.superpowers)'` must print nothing. - [ ] **Step 4: Verify** Run: `uv run pytest -q -W error 2>&1 | tail -1` → `100 passed`. Run: `uv run --python 3.9 pytest -q -W error 2>&1 | tail -1` → `100 passed`. -Run: `claude plugin validate --strict . 2>&1 | tail -3` → passes (a warning about the root `CLAUDE.md` is accepted; record it in the report). +Run: `claude plugin validate --strict . 2>&1 | tail -3` → `Validation passed`, no warnings. Run: `python3 skills/humanize/scripts/surface_scan.py --text tests/fixtures/ai_report.txt | head -1` → the words/sentences line. - [ ] **Step 5: Commit** @@ -183,8 +185,9 @@ def test_frontmatter_is_spec_only(): def test_no_harness_specific_content_under_skills(): + # conftest.py's sys.path insert makes pytest write scripts/__pycache__/*.pyc; skip binaries. for path in SKILL_DIR.rglob("*"): - if not path.is_file(): + if not path.is_file() or "__pycache__" in path.parts: continue text = path.read_text(encoding="utf-8") for token in HARNESS_TOKENS: @@ -274,7 +277,7 @@ Then `wc -l skills/humanize/SKILL.md` → 150 (−1 +0 +1 +1 −1). If it prints - [ ] **Step 4: Run the tests to verify they pass** Run: `uv run pytest -q -W error 2>&1 | tail -1` → `103 passed`. -Run: `grep -c '[’“”—–]' skills/humanize/SKILL.md` before and after Step 3 — the count must not decrease by more than the two em dashes removed in (d)'s old command? No: (d) removed none; the count must be unchanged or higher. +Run: `grep -c '[’“”—–]' skills/humanize/SKILL.md` before and after Step 3 — the count drops by exactly one (11 → 10): edit (e)'s old text carried one em dash and its replacement carries none. Any other change means a curly literal was lost. Run: `claude plugin validate --strict . 2>&1 | tail -1` → passes. Run: `sed -n 1,5p skills/humanize/SKILL.md` → `---`, `name: humanize`, `description: …`, `---`, blank. @@ -449,7 +452,7 @@ Run: `grep -rn '0\.2\.0' plugin.json .claude-plugin .codex-plugin pyproject.toml ```bash uv run ruff format tests/test_portability.py && uv run ruff check tests/test_portability.py -git add plugin.json .codex-plugin/plugin.json .claude-plugin pyproject.toml CHANGELOG.md tests/test_portability.py +git add plugin.json .codex-plugin/plugin.json .claude-plugin pyproject.toml uv.lock CHANGELOG.md tests/test_portability.py git commit -m "Add Agent Plugins and Codex manifests; version 0.3.0 in lockstep Co-Authored-By: Claude Fable 5.1 @@ -470,8 +473,7 @@ Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh" - [ ] **Step 1: Write the failing tests** (append) ```python -import importlib.util # top of module -import sys +import importlib.util # top of module (no `import sys` — ruff F401) import zipfile GUARD_TOKENS = ( @@ -494,7 +496,11 @@ GUARD_TOKENS = ( def _guard_hits(text: str) -> list[str]: - return [t for t in GUARD_TOKENS if t in text] + hits = [t for t in GUARD_TOKENS if t != "compile(" and t in text] + # Bare compile() is a guard pattern; the scanner's 22 re.compile() calls are not. + if re.search(r"(? local h="$1" f="$2" tool_re="$3" ok=1 for r in "${ROWS[@]}"; do grep -qiE "$r" "$f" || { echo " missing row: $r"; ok=0; }; done grep -iE '^\|' "$f" | grep -qi 'nominalization' && { echo " nominalization row present"; ok=0; } - grep -q "participial tails $NUM_TAILS" "$f" || grep -q "$NUM_TAILS" "$f" || { echo " scanner count $NUM_TAILS absent"; ok=0; } - grep -q "$NUM_REP" "$f" || { echo " scanner rate $NUM_REP absent"; ok=0; } + grep -qE "participial tails[^0-9]{0,20}${NUM_TAILS}([^0-9]|$)" "$f" || { echo " scanner count $NUM_TAILS absent"; ok=0; } + grep -qF "${NUM_REP}/1k" "$f" || { echo " scanner rate ${NUM_REP}/1k absent"; ok=0; } [ -n "$tool_re" ] && { grep -qE "$tool_re" "$f" || { echo " no tool event naming surface_scan.py"; ok=0; }; } grep -qiE 'Choices you may want to reverse|^## Rewrite' "$f" && { echo " rewrite section present"; ok=0; } [ "$ok" = 1 ] && echo "PASS $h" || echo "FAIL $h" @@ -927,7 +933,9 @@ run_hermes() { judge "hermes" "$f" '' } -for h in "${@:-claude codex cursor hermes}"; do "run_$h"; done +HARNESSES=("$@") +[ "${#HARNESSES[@]}" -eq 0 ] && HARNESSES=(claude codex cursor hermes) +for h in "${HARNESSES[@]}"; do "run_$h"; done ``` `chmod +x tools/smoke_harnesses.sh`. Write `docs/acceptance/v0.3/README.md`: what the transcripts are, the PASS rule, the date, and the CLI versions (`claude --version`, `codex --version`, `agent --version`, `hermes --version`). @@ -948,8 +956,9 @@ codex plugin marketplace add ccf/humanize@feat/v0.3 codex plugin add humanize@humanize codex debug prompt-input "hi" | grep -i humanize codex plugin remove humanize@humanize +codex plugin marketplace remove humanize # verb per `codex plugin marketplace --help` ``` -Record the exact output lines. If `add` reports a manifest problem, that is a finding for the controller, not something to patch silently. +Record the exact output lines; `codex plugin marketplace list` afterwards must not show `humanize`. If `add` reports a manifest problem, that is a finding for the controller, not something to patch silently. - [ ] **Step 5: Commit** @@ -972,7 +981,7 @@ Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh" - [ ] **Step 2: Gate** -`uv run pytest -q -W error` → `108 passed`; `uv run --python 3.9 pytest -q -W error` → `108 passed`; `uv run ruff check . && uv run ruff format --check .`; `claude plugin validate --strict .`; `wc -l skills/humanize/SKILL.md` ≤ 150; `python3 tools/package_skill_zip.py`; `grep -rn 'plugins/humanize' . --include='*.md' --include='*.py' --include='*.json' --include='*.toml' | grep -v -E 'docs/design|CHANGELOG|\.superpowers'` → nothing. +`uv run pytest -q -W error` → `108 passed`; `uv run --python 3.9 pytest -q -W error` → `108 passed`; `uv run ruff check tests tools skills && uv run ruff format --check tests tools skills`; `claude plugin validate --strict .` → `Validation passed`; `wc -l skills/humanize/SKILL.md` ≤ 150; `python3 tools/package_skill_zip.py`; `grep -rn 'plugins/humanize' . --include='*.md' --include='*.py' --include='*.json' --include='*.toml' | grep -v -E '^(\./)?(docs/design|CHANGELOG\.md|\.superpowers)'` → nothing. - [ ] **Step 3: Commit** @@ -989,6 +998,6 @@ Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh" ## Self-review notes - Spec coverage: §1 → T1, T3; §2 → T2; §3 tests 1–3 → T2, 5 → T3, 4 and 6 → T4, 7 → T5; §4 → T5 (copy), T6 (install/invariants); §5 → T7; §6 → T4 (packager) + T7/§7 phases (Cowork and zip upload are post-merge/post-release, controller-run); §7 → T3 (CHANGELOG) + T6 (workflow) + controller release steps; §8 acceptance 1, 2, 3, 7 → T7/T8, 4–6 → post-merge; §9 → T3, T5; §10 → T7 records outcomes. -- Test ladder: 100 → T2 103 → T3 104 → T4 107 → T5 108. +- Test ladder: 100 → T2 103 → T3 104 → T4 107 → T5 108 — confirmed by a dry run of Tasks 1–5 in an isolated worktree (3.11 and 3.9), which also found and fixed in this revision: the `__pycache__` read in test 2, the `uv.lock` staging in Task 3, the `compile(` false positive in test 4, an unused `import sys`, the missing marketplace-remove in Task 7, the broken default-argument loop in the smoke script, the non-existent validate warning, and the `ugrep` path-prefix difference. - Type consistency: `frontmatter()`, `SKILL_DIR`, `_json()` defined in T2/T3 and used in T4/T5 in the same module; `PackagingError` and `build_zip(repo_root, out_dir)` match between T4's test and implementation. - Rulings recorded here for the ledger: `pyproject.toml` joins the version-lockstep set; `.cursor/BUGBOT.md` line 3 sentence is the plan's own wording (spec said "mirrors"). From c8b92b6a9e93e69235ae9c8009946b7a83a3997c Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 10:26:27 -0400 Subject: [PATCH 07/22] Move the plugin to the repository root Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- .claude-plugin/marketplace.json | 2 +- .../plugin.json | 0 .cursor/BUGBOT.md | 6 ++-- CLAUDE.md | 2 +- README.md | 35 +++++++++---------- .../skills => skills}/humanize/SKILL.md | 0 .../humanize/references/SOURCES.md | 0 .../humanize/references/model-fingerprints.md | 0 .../humanize/references/narrative-tells.md | 0 .../humanize/references/principles.md | 0 .../humanize/references/style-tells.md | 0 .../humanize/references/surface-tells.md | 0 .../humanize/scripts/surface_scan.py | 0 tests/conftest.py | 2 +- tests/test_manifests.py | 6 ++-- 15 files changed, 26 insertions(+), 27 deletions(-) rename {plugins/humanize/.claude-plugin => .claude-plugin}/plugin.json (100%) rename {plugins/humanize/skills => skills}/humanize/SKILL.md (100%) rename {plugins/humanize/skills => skills}/humanize/references/SOURCES.md (100%) rename {plugins/humanize/skills => skills}/humanize/references/model-fingerprints.md (100%) rename {plugins/humanize/skills => skills}/humanize/references/narrative-tells.md (100%) rename {plugins/humanize/skills => skills}/humanize/references/principles.md (100%) rename {plugins/humanize/skills => skills}/humanize/references/style-tells.md (100%) rename {plugins/humanize/skills => skills}/humanize/references/surface-tells.md (100%) rename {plugins/humanize/skills => skills}/humanize/scripts/surface_scan.py (100%) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 532c81d..ea8f8c3 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -13,7 +13,7 @@ { "name": "humanize", "description": "Audit prose for AI tells and rewrite it to read as natural human writing, grounded in StoryScope's measured human-vs-AI feature gaps", - "source": "./plugins/humanize", + "source": "./", "version": "0.2.0", "author": { "name": "ccf" }, "license": "MIT", diff --git a/plugins/humanize/.claude-plugin/plugin.json b/.claude-plugin/plugin.json similarity index 100% rename from plugins/humanize/.claude-plugin/plugin.json rename to .claude-plugin/plugin.json diff --git a/.cursor/BUGBOT.md b/.cursor/BUGBOT.md index 4a28ce5..b15572b 100644 --- a/.cursor/BUGBOT.md +++ b/.cursor/BUGBOT.md @@ -6,12 +6,12 @@ it. Design spec: `docs/design/2026-09-13-humanize-plugin-design.md` and ## Invariants to enforce -- `plugins/humanize/skills/humanize/scripts/surface_scan.py` imports only the +- `skills/humanize/scripts/surface_scan.py` imports only the Python standard library and runs on Python 3.9+. Flag any third-party import or 3.10+ syntax (match statements, `X | Y` in runtime positions, PEP 604 in non-annotation code). -- Nothing under `tests/` or `plugins/**/scripts/` makes network or LLM calls. -- A `Base rate:` line in `plugins/humanize/skills/humanize/references/*.md` +- Nothing under `tests/` or `skills/**/scripts/` makes network or LLM calls. +- A `Base rate:` line in `skills/humanize/references/*.md` must trace to a row in `data/storyscope_feature_gaps.csv` (or a future CSV documented in `data/README.md`; none added in v0.2). If a PR changes a number, check the CSV row. Any other number must sit on a `Scan:`, `Rule diff --git a/CLAUDE.md b/CLAUDE.md index 97e00fb..05968bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ uv sync # first time uv run pytest -q # 100 tests, must be warning-free uv run ruff format && uv run ruff check --fix claude plugin validate --strict . -uv run python plugins/humanize/skills/humanize/scripts/surface_scan.py --text +uv run python skills/humanize/scripts/surface_scan.py --text python3 tools/gen_tell_scaffold.py style|narrative # regenerate reference scaffolds ``` diff --git a/README.md b/README.md index 12c816f..0e2ce8e 100644 --- a/README.md +++ b/README.md @@ -100,23 +100,22 @@ Rewrite (excerpt): ## What's inside ``` -plugins/humanize/ - skills/humanize/ - SKILL.md the procedure - references/ - principles.md - surface-tells.md vocabulary, punctuation, shape, discourse moves - style-tells.md 20 StoryScope style features with base rates - narrative-tells.md 57 StoryScope narrative features (fiction only) - model-fingerprints.md Claude / GPT / Gemini / DeepSeek / Kimi tendencies - SOURCES.md citation registry (not loaded at runtime) - scripts/surface_scan.py stdlib-only metrics: burstiness and sentence tails, punctuation, - tricolons, not-but, wordlists, closers, repeated phrases, - participial tails, container nouns, nominalization hits, - disclaimer opener -data/ StoryScope taxonomy + computed feature gaps -tools/gen_tell_scaffold.py regenerate reference scaffolds from the data -tests/ pytest; no network, no LLM calls +skills/humanize/ + SKILL.md the procedure + references/ + principles.md + surface-tells.md vocabulary, punctuation, shape, discourse moves + style-tells.md 20 StoryScope style features with base rates + narrative-tells.md 57 StoryScope narrative features (fiction only) + model-fingerprints.md Claude / GPT / Gemini / DeepSeek / Kimi tendencies + SOURCES.md citation registry (not loaded at runtime) + scripts/surface_scan.py stdlib-only metrics: burstiness and sentence tails, punctuation, + tricolons, not-but, wordlists, closers, repeated phrases, + participial tails, container nouns, nominalization hits, + disclaimer opener +data/ StoryScope taxonomy + computed feature gaps +tools/gen_tell_scaffold.py regenerate reference scaffolds from the data +tests/ pytest; no network, no LLM calls ``` ## Development @@ -125,7 +124,7 @@ tests/ pytest; no network, no LLM calls uv sync uv run pytest -q claude plugin validate . -uv run python plugins/humanize/skills/humanize/scripts/surface_scan.py --text some.txt +uv run python skills/humanize/scripts/surface_scan.py --text some.txt ``` CI (`.github/workflows/ci.yml`) runs the same pre-commit hooks, pytest on diff --git a/plugins/humanize/skills/humanize/SKILL.md b/skills/humanize/SKILL.md similarity index 100% rename from plugins/humanize/skills/humanize/SKILL.md rename to skills/humanize/SKILL.md diff --git a/plugins/humanize/skills/humanize/references/SOURCES.md b/skills/humanize/references/SOURCES.md similarity index 100% rename from plugins/humanize/skills/humanize/references/SOURCES.md rename to skills/humanize/references/SOURCES.md diff --git a/plugins/humanize/skills/humanize/references/model-fingerprints.md b/skills/humanize/references/model-fingerprints.md similarity index 100% rename from plugins/humanize/skills/humanize/references/model-fingerprints.md rename to skills/humanize/references/model-fingerprints.md diff --git a/plugins/humanize/skills/humanize/references/narrative-tells.md b/skills/humanize/references/narrative-tells.md similarity index 100% rename from plugins/humanize/skills/humanize/references/narrative-tells.md rename to skills/humanize/references/narrative-tells.md diff --git a/plugins/humanize/skills/humanize/references/principles.md b/skills/humanize/references/principles.md similarity index 100% rename from plugins/humanize/skills/humanize/references/principles.md rename to skills/humanize/references/principles.md diff --git a/plugins/humanize/skills/humanize/references/style-tells.md b/skills/humanize/references/style-tells.md similarity index 100% rename from plugins/humanize/skills/humanize/references/style-tells.md rename to skills/humanize/references/style-tells.md diff --git a/plugins/humanize/skills/humanize/references/surface-tells.md b/skills/humanize/references/surface-tells.md similarity index 100% rename from plugins/humanize/skills/humanize/references/surface-tells.md rename to skills/humanize/references/surface-tells.md diff --git a/plugins/humanize/skills/humanize/scripts/surface_scan.py b/skills/humanize/scripts/surface_scan.py similarity index 100% rename from plugins/humanize/skills/humanize/scripts/surface_scan.py rename to skills/humanize/scripts/surface_scan.py diff --git a/tests/conftest.py b/tests/conftest.py index 9a1ed1f..f0ffa15 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,5 @@ import sys from pathlib import Path -SCRIPTS = Path(__file__).resolve().parents[1] / "plugins/humanize/skills/humanize/scripts" +SCRIPTS = Path(__file__).resolve().parents[1] / "skills/humanize/scripts" sys.path.insert(0, str(SCRIPTS)) diff --git a/tests/test_manifests.py b/tests/test_manifests.py index 590c4c3..9385f27 100644 --- a/tests/test_manifests.py +++ b/tests/test_manifests.py @@ -35,12 +35,12 @@ def test_marketplace_points_at_existing_plugin_components(): def test_plugin_manifest_matches_marketplace_entry(): m = json.loads((ROOT / ".claude-plugin/marketplace.json").read_text())["plugins"][0] - p = json.loads((ROOT / "plugins/humanize/.claude-plugin/plugin.json").read_text()) + p = json.loads((ROOT / ".claude-plugin/plugin.json").read_text()) assert p["name"] == m["name"] == "humanize" assert p["version"] == m["version"] -SKILL_MD = ROOT / "plugins/humanize/skills/humanize/SKILL.md" +SKILL_MD = ROOT / "skills/humanize/SKILL.md" def test_skill_line_budget(): @@ -50,7 +50,7 @@ def test_skill_line_budget(): assert len(lines) <= 150, len(lines) -REFS = ROOT / "plugins/humanize/skills/humanize/references" +REFS = ROOT / "skills/humanize/references" KEY_RE = re.compile(r"\[([a-z-]+-\d{4})\]") EXPECTED_KEYS = { "storyscope-2026", From ef10823b0ade2a33b2048e1a9a562f0d5414cd21 Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 10:30:46 -0400 Subject: [PATCH 08/22] Make SKILL.md harness-agnostic; add frontmatter, path, and reference guards Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- skills/humanize/SKILL.md | 32 +++++++-------- tests/test_manifests.py | 1 - tests/test_portability.py | 82 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 17 deletions(-) create mode 100644 tests/test_portability.py diff --git a/skills/humanize/SKILL.md b/skills/humanize/SKILL.md index 5c44682..c956c70 100644 --- a/skills/humanize/SKILL.md +++ b/skills/humanize/SKILL.md @@ -1,14 +1,13 @@ --- name: humanize description: Use when drafting or editing any prose — email, essay, documentation, blog post, story, chat reply — or when asked to "humanize" text, make it "sound less like AI", "more natural", "less robotic", or remove AI tells. Also use when reviewing prose someone else wrote. Not for code, config, or commit messages. -argument-hint: "[path | text] [--audit-only] [--fiction | --prose]" --- # Humanize Make prose read as natural human writing by finding and removing the tells that -mark it as AI-generated. Grounded in StoryScope (Russell et al., 2026) and -register studies: AI converges on shared defaults; human writing disperses. +mark it as AI-generated. Grounded in thirteen studies of measured style +differences: AI converges on shared defaults; human writing disperses. Read `references/principles.md` first, every time. @@ -24,8 +23,9 @@ was invoked. Follow all six steps below. ## Invocation (`/humanize` only) -Applies only when the user typed `/humanize …`. On auto-invoke (drafting -mode or a natural-language request) there are no arguments: skip this section. +Applies only when the user typed `/humanize …` (Codex: `$humanize …`). On +auto-invoke there are no arguments: skip this section. Options below also +work as plain words in a request ("audit only", "this is fiction"). Arguments: $ARGUMENTS @@ -67,21 +67,21 @@ the length. If the text is 80 words or longer, run the scanner and keep the output: ```bash -python3 "${CLAUDE_PLUGIN_ROOT}/skills/humanize/scripts/surface_scan.py" --text +python3 scripts/surface_scan.py --text ``` -Write pasted text to a temp file first. For the full JSON, drop `--text`. Under -80 words, skip this step; the statistics are noise. The scanner strips code, -links, URLs, and heading markers from Markdown before measuring. +Run it from this skill's folder (the directory holding this SKILL.md). Write +pasted text to a temp file first. For the full JSON, drop `--text`. Under 80 +words, skip this step; the statistics are noise. The scanner strips code, links, +URLs, and heading markers from Markdown before measuring. **Non-text sources** (`.docx`, `.pdf`, `.pptx`, `.odt`, `.rtf`): the scanner reads -plain text only. Extract first — use the `docx` or `pdf` skill if installed -(Anthropic's `document-skills` plugin) to write a temp `.md`, then scan and -audit that. If the skill is not installed, say so and give the two commands: -`/plugin marketplace add anthropics/skills` and -`/plugin install document-skills@anthropic-agent-skills`; for a PDF you can -still Read it yourself and write the text to a temp file. Deliver the rewrite -as Markdown; if the user wants a Word file back, hand off to the `docx` skill. +plain text only. Extract first with the harness's document skills (Anthropic's +`docx`/`pdf` skills ship with Claude Code's `document-skills` plugin and with +Claude Desktop) to write a temp `.md`, then scan and audit that. In claude.ai the +file is an attachment: convert it in the sandbox (pandoc, pdfplumber). Without +document skills, read the file yourself if you can and write the text to a temp +file; otherwise ask for a text export. Deliver Markdown; a Word file via `docx`. ### 3. Audit diff --git a/tests/test_manifests.py b/tests/test_manifests.py index 9385f27..2c33c93 100644 --- a/tests/test_manifests.py +++ b/tests/test_manifests.py @@ -20,7 +20,6 @@ def test_marketplace_points_at_existing_plugin_components(): # /humanize is the skill itself; a separate command file would register a duplicate name. assert not (src / "commands").exists() assert (src / "skills/humanize/SKILL.md").is_file() - assert "argument-hint:" in (src / "skills/humanize/SKILL.md").read_text() assert (src / "skills/humanize/scripts/surface_scan.py").is_file() for doc in ( "principles", diff --git a/tests/test_portability.py b/tests/test_portability.py new file mode 100644 index 0000000..057b986 --- /dev/null +++ b/tests/test_portability.py @@ -0,0 +1,82 @@ +"""Portability guards: the skill must be a plain Agent Skill every harness can load.""" + +from __future__ import annotations + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SKILL_DIR = ROOT / "skills/humanize" +SKILL_MD = SKILL_DIR / "SKILL.md" + +# The Agent Skills specification fields (agentskills.io; Codex quick_validate.py; +# claude.ai upload validator). Anything else is a hard error outside Claude Code. +SPEC_FIELDS = {"name", "description", "license", "compatibility", "metadata", "allowed-tools"} +NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") + +# Harness-specific tokens that must never appear under skills/ (spec §3 test 2). +HARNESS_TOKENS = ( + "${CLAUDE_", + "CLAUDE_PLUGIN_ROOT", + "HERMES_SKILL_DIR", + "CURSOR_", + "CODEX_", + "/plugin ", + "npx skills", +) + +RUNTIME_REFERENCES = ( + "references/principles.md", + "references/surface-tells.md", + "references/style-tells.md", + "references/narrative-tells.md", + "references/model-fingerprints.md", +) + + +def frontmatter(text: str) -> dict[str, str]: + """Return the YAML frontmatter as a flat key -> raw-value map (one line per key).""" + assert text.startswith("---\n"), "SKILL.md must start with frontmatter" + block = text.split("\n---\n", 1)[0][4:] + out: dict[str, str] = {} + for line in block.splitlines(): + if not line.strip() or line.startswith(" "): + continue + key, _, value = line.partition(":") + out[key.strip()] = value.strip() + return out + + +def test_frontmatter_is_spec_only(): + fm = frontmatter(SKILL_MD.read_text(encoding="utf-8")) + extra = set(fm) - SPEC_FIELDS + assert not extra, f"non-spec frontmatter keys: {sorted(extra)}" + name = fm["name"] + assert NAME_RE.match(name) and len(name) <= 64, name + assert "claude" not in name and "anthropic" not in name + assert name == SKILL_DIR.name, "Cursor requires the folder name to equal `name`" + desc = fm["description"] + assert len(desc) <= 1024, len(desc) + assert "<" not in desc and ">" not in desc + assert not desc.startswith("[TODO:") + + +def test_no_harness_specific_content_under_skills(): + # conftest.py's sys.path insert makes pytest write scripts/__pycache__/*.pyc; skip binaries. + for path in SKILL_DIR.rglob("*"): + if not path.is_file() or "__pycache__" in path.parts: + continue + text = path.read_text(encoding="utf-8") + for token in HARNESS_TOKENS: + assert token not in text, f"{path.relative_to(ROOT)} contains {token!r}" + collapsed = " ".join(SKILL_MD.read_text(encoding="utf-8").split()) + assert "reads literally as `$ARGUMENTS`" in collapsed + + +def test_runtime_references_and_script_are_named_in_skill(): + text = SKILL_MD.read_text(encoding="utf-8") + for ref in RUNTIME_REFERENCES: + assert ref in text, ref + assert (SKILL_DIR / ref).is_file(), ref + assert "scripts/surface_scan.py" in text + # SOURCES.md is a maintainer registry, never loaded at runtime, so it is not named. From 170b68017ec16422cb2cdbfccae7fa9f3805ffea Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 10:36:19 -0400 Subject: [PATCH 09/22] Add Agent Plugins and Codex manifests; version 0.3.0 in lockstep Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- .claude-plugin/marketplace.json | 10 ++++---- .claude-plugin/plugin.json | 6 ++--- .codex-plugin/plugin.json | 21 +++++++++++++++++ CHANGELOG.md | 31 +++++++++++++++++++++++- plugin.json | 11 +++++++++ pyproject.toml | 4 ++-- tests/test_portability.py | 42 +++++++++++++++++++++++++++++++++ uv.lock | 2 +- 8 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 .codex-plugin/plugin.json create mode 100644 plugin.json diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index ea8f8c3..edc9637 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,18 +6,18 @@ "url": "https://github.com/ccf/humanize" }, "metadata": { - "description": "Remove AI tells from prose. Skill + /humanize command grounded in StoryScope (arXiv 2604.03136).", - "version": "0.2.0" + "description": "Measures prose against 13 studies of how AI and human writing differ, from word choice to sentence rhythm to story shape, then rewrites the tells out.", + "version": "0.3.0" }, "plugins": [ { "name": "humanize", - "description": "Audit prose for AI tells and rewrite it to read as natural human writing, grounded in StoryScope's measured human-vs-AI feature gaps", + "description": "Audits a draft layer by layer (vocabulary, punctuation, sentence shape, openers, closers, grammar, narrative, per-model habits) against 13 studies, then rewrites what it flags in the author's voice.", "source": "./", - "version": "0.2.0", + "version": "0.3.0", "author": { "name": "ccf" }, "license": "MIT", - "keywords": ["writing", "prose", "editing", "ai-detection", "storyscope", "humanize"], + "keywords": ["humanize", "writing", "prose", "editing", "style", "ai-writing", "rewriting", "linguistics"], "category": "writing", "strict": true } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index d3383ea..8670ef9 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,8 +1,8 @@ { "name": "humanize", - "version": "0.2.0", - "description": "Audit prose for AI tells and rewrite it to read as natural human writing, grounded in StoryScope's measured human-vs-AI feature gaps", + "version": "0.3.0", + "description": "Audits a draft layer by layer (vocabulary, punctuation, sentence shape, openers, closers, grammar, narrative, per-model habits) against 13 studies, then rewrites what it flags in the author's voice.", "author": { "name": "ccf" }, "license": "MIT", - "keywords": ["writing", "prose", "editing", "ai-detection", "storyscope", "humanize"] + "keywords": ["humanize", "writing", "prose", "editing", "style", "ai-writing", "rewriting", "linguistics"] } diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json new file mode 100644 index 0000000..3607618 --- /dev/null +++ b/.codex-plugin/plugin.json @@ -0,0 +1,21 @@ +{ + "name": "humanize", + "version": "0.3.0", + "description": "Audits a draft layer by layer (vocabulary, punctuation, sentence shape, openers, closers, grammar, narrative, per-model habits) against 13 studies, then rewrites what it flags in the author's voice.", + "author": { "name": "ccf" }, + "homepage": "https://github.com/ccf/humanize", + "repository": "https://github.com/ccf/humanize", + "license": "MIT", + "keywords": ["humanize", "writing", "prose", "editing", "style", "ai-writing", "rewriting", "linguistics"], + "skills": "./skills/", + "interface": { + "displayName": "humanize", + "shortDescription": "Audit prose for AI tells at every layer, then rewrite it in the author's voice.", + "longDescription": "Reads a draft against what 13 studies measured about AI writing: word choice, punctuation, sentence shape, openers, closers, grammar, register, fiction habits, per-model quirks. A dependency-free scanner supplies the counts a model cannot eyeball, and each flag cites the sentence that triggered it.", + "developerName": "ccf", + "category": "Developer Tools", + "capabilities": ["Read", "Write"], + "defaultPrompt": ["Humanize this draft.", "Audit this text for AI tells, don't rewrite it."], + "websiteURL": "https://github.com/ccf/humanize" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 2295bd0..38203d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,34 @@ All notable changes to this project are documented here. The format follows ## [Unreleased] +## [0.3.0] - 2026-09-14 + +### Added +- Agent Plugins 1.0 manifest (`plugin.json`) and Codex manifest (`.codex-plugin/plugin.json`), + so the plugin installs into Codex, Cursor, and Hermes with their own tooling. +- `tests/test_portability.py`: spec-only frontmatter, no harness-specific content under + `skills/`, runtime references named, guard-clean scripts, manifests and CHANGELOG in + version lockstep, zip packager contract, study count matches `SOURCES.md`. +- `tools/package_skill_zip.py`: builds the claude.ai skill upload from the plugin. +- `tools/smoke_harnesses.sh`: local acceptance matrix with scanner-evidence PASS rule; + transcripts under `docs/acceptance/v0.3/`. +- README install table for Claude Code, Codex CLI, Cursor, Hermes Agent, Claude Desktop. + +### Changed +- Repository layout: the plugin now lives at the root (`skills/humanize/`); the marketplace + `source` is `./`. +- `SKILL.md` is harness-agnostic: relative `scripts/surface_scan.py` path, plain-words options, + document extraction described generically; `argument-hint` removed (a hard error outside + Claude Code). The `/plugin` install lines moved to the README. +- Public copy describes humanize as grounded in the thirteen studies in + `references/SOURCES.md` and covering every layer of a text; StoryScope is named where it is + the source of a number. +- `pyproject.toml` version now tracks the plugin version. + +### Migration +- Existing Claude Code installs: `claude plugin marketplace update humanize && claude plugin + update humanize@humanize`. + ## [0.2.0] - 2026-09-14 ### Added @@ -87,7 +115,8 @@ Initial release (#1). CI (pre-commit, pytest on Python 3.9 and 3.13, `claude plugin validate --strict`), and a Bugbot review guide. -[Unreleased]: https://github.com/ccf/humanize/compare/v0.2.0...HEAD +[Unreleased]: https://github.com/ccf/humanize/compare/v0.3.0...HEAD +[0.3.0]: https://github.com/ccf/humanize/compare/v0.2.0...v0.3.0 [0.2.0]: https://github.com/ccf/humanize/compare/v0.1.2...v0.2.0 [0.1.2]: https://github.com/ccf/humanize/compare/v0.1.1...v0.1.2 [0.1.1]: https://github.com/ccf/humanize/compare/v0.1.0...v0.1.1 diff --git a/plugin.json b/plugin.json new file mode 100644 index 0000000..b04fef9 --- /dev/null +++ b/plugin.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "humanize", + "version": "0.3.0", + "description": "Audits a draft layer by layer (vocabulary, punctuation, sentence shape, openers, closers, grammar, narrative, per-model habits) against 13 studies, then rewrites what it flags in the author's voice.", + "author": { "name": "ccf" }, + "license": "MIT", + "homepage": "https://github.com/ccf/humanize", + "repository": "https://github.com/ccf/humanize", + "keywords": ["humanize", "writing", "prose", "editing", "style", "ai-writing", "rewriting", "linguistics"] +} diff --git a/pyproject.toml b/pyproject.toml index 7bcaaf6..33962b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "humanize-plugin" -version = "0.1.0" -description = "Claude Code plugin that removes AI tells from prose" +version = "0.3.0" +description = "Agent skill that audits prose for AI tells and rewrites it in the author's voice, on evidence from 13 studies." requires-python = ">=3.9" license = "MIT" dependencies = [] diff --git a/tests/test_portability.py b/tests/test_portability.py index 057b986..f9a5948 100644 --- a/tests/test_portability.py +++ b/tests/test_portability.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json import re from pathlib import Path @@ -80,3 +81,44 @@ def test_runtime_references_and_script_are_named_in_skill(): assert (SKILL_DIR / ref).is_file(), ref assert "scripts/surface_scan.py" in text # SOURCES.md is a maintainer registry, never loaded at runtime, so it is not named. + + +AGENT_PLUGINS_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json" +CHANGELOG_HEADING_RE = re.compile(r"^## \[(\d+\.\d+\.\d+)\]", re.M) + + +def _json(rel: str) -> dict: + return json.loads((ROOT / rel).read_text(encoding="utf-8")) + + +def _pyproject_version() -> str: + text = (ROOT / "pyproject.toml").read_text(encoding="utf-8") + return re.search(r'^version = "([^"]+)"', text, re.M).group(1) + + +def test_manifests_agree(): + root = _json("plugin.json") + claude = _json(".claude-plugin/plugin.json") + codex = _json(".codex-plugin/plugin.json") + market = _json(".claude-plugin/marketplace.json") + entry = market["plugins"][0] + for m in (root, claude, codex, entry): + assert m["name"] == "humanize" + versions = { + "plugin.json": root["version"], + ".claude-plugin/plugin.json": claude["version"], + ".codex-plugin/plugin.json": codex["version"], + "marketplace.metadata": market["metadata"]["version"], + "marketplace.plugins[0]": entry["version"], + "pyproject.toml": _pyproject_version(), + } + assert len(set(versions.values())) == 1, versions + changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") + newest = CHANGELOG_HEADING_RE.search(changelog).group(1) # [Unreleased] has no digits + assert newest == root["version"], (newest, root["version"]) + assert root["$schema"] == AGENT_PLUGINS_SCHEMA + assert (ROOT / codex["skills"] / "humanize/SKILL.md").is_file() + assert (ROOT / entry["source"] / ".claude-plugin/plugin.json").is_file() + assert entry["strict"] is True + for m in (root, claude, codex, entry): + assert "storyscope" not in json.dumps(m).lower() diff --git a/uv.lock b/uv.lock index aaf90b7..42e39f8 100644 --- a/uv.lock +++ b/uv.lock @@ -86,7 +86,7 @@ wheels = [ [[package]] name = "humanize-plugin" -version = "0.1.0" +version = "0.3.0" source = { virtual = "." } [package.dev-dependencies] From daa2cf13fb32a550dd236d4b5afc6ec4696b762b Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 10:41:39 -0400 Subject: [PATCH 10/22] Add guard-clean script test and the claude.ai skill zip packager Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- .gitignore | 1 + tests/test_portability.py | 76 ++++++++++++++++++++++++++++++++++++++ tools/package_skill_zip.py | 70 +++++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 tools/package_skill_zip.py diff --git a/.gitignore b/.gitignore index 54336b0..0707e34 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __pycache__/ .pytest_cache/ .venv/ .superpowers/ +dist/ diff --git a/tests/test_portability.py b/tests/test_portability.py index f9a5948..e45814b 100644 --- a/tests/test_portability.py +++ b/tests/test_portability.py @@ -2,8 +2,10 @@ from __future__ import annotations +import importlib.util import json import re +import zipfile from pathlib import Path ROOT = Path(__file__).resolve().parents[1] @@ -122,3 +124,77 @@ def test_manifests_agree(): assert entry["strict"] is True for m in (root, claude, codex, entry): assert "storyscope" not in json.dumps(m).lower() + + +GUARD_TOKENS = ( + "subprocess", + "os.system", + "os.popen", + "os.environ", + "getenv", + "shutil.rmtree", + "eval(", + "exec(", + "compile(", + "getattr(", + "codecs", + "socket", + "urllib", + "requests", + "# /// script", +) + + +def _guard_hits(text: str) -> list[str]: + hits = [t for t in GUARD_TOKENS if t != "compile(" and t in text] + # Bare compile() is a guard pattern; the scanner's 22 re.compile() calls are not. + if re.search(r"(?.zip whose root is the skill folder (named exactly +`name`), the layout Anthropic's uploader and package_skill.py expect. Standard library only. +""" + +from __future__ import annotations + +import argparse +import json +import sys +import zipfile +from pathlib import Path + +SKILL = "humanize" +SPEC_FIELDS = {"name", "description", "license", "compatibility", "metadata", "allowed-tools"} + + +class PackagingError(Exception): + pass + + +def _frontmatter_keys(skill_md: Path) -> set[str]: + text = skill_md.read_text(encoding="utf-8") + if not text.startswith("---\n"): + raise PackagingError("SKILL.md has no frontmatter") + block = text.split("\n---\n", 1)[0][4:] + keys = set() + for line in block.splitlines(): + if line.strip() and not line.startswith(" "): + keys.add(line.partition(":")[0].strip()) + return keys + + +def build_zip(repo_root: Path, out_dir: Path) -> Path: + skill_dir = repo_root / "skills" / SKILL + skill_md = skill_dir / "SKILL.md" + extra = _frontmatter_keys(skill_md) - SPEC_FIELDS + if extra: + raise PackagingError( + "non-spec frontmatter keys would fail the upload: " + ", ".join(sorted(extra)) + ) + version = json.loads((repo_root / "plugin.json").read_text(encoding="utf-8"))["version"] + members = [skill_md, skill_dir / "scripts" / "surface_scan.py"] + members += sorted(p for p in (skill_dir / "references").iterdir() if p.is_file()) + out_dir.mkdir(parents=True, exist_ok=True) + out = out_dir / f"{SKILL}-skill-{version}.zip" + with zipfile.ZipFile(out, "w", compression=zipfile.ZIP_DEFLATED) as zf: + for path in members: + zf.write(path, f"{SKILL}/{path.relative_to(skill_dir).as_posix()}") + return out + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--out", default="dist", help="output directory (default: dist)") + args = parser.parse_args(argv) + root = Path(__file__).resolve().parents[1] + try: + out = build_zip(root, root / args.out) + except PackagingError as e: + print(f"error: {e}", file=sys.stderr) + return 1 + print(out) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From ad4ac3906d3c318d318dc0252a057e468a3ab2f8 Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 10:47:28 -0400 Subject: [PATCH 11/22] Reposition humanize as research-grounded across every text layer Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- .cursor/BUGBOT.md | 6 +++--- CLAUDE.md | 4 ++-- README.md | 35 ++++++++++++++++----------------- tests/test_portability.py | 41 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 62 insertions(+), 24 deletions(-) diff --git a/.cursor/BUGBOT.md b/.cursor/BUGBOT.md index b15572b..8247844 100644 --- a/.cursor/BUGBOT.md +++ b/.cursor/BUGBOT.md @@ -1,8 +1,8 @@ # Bugbot review guide — humanize -This repo is a Claude Code plugin that audits prose for AI tells and rewrites -it. Design spec: `docs/design/2026-09-13-humanize-plugin-design.md` and -`docs/design/2026-09-14-humanize-v0.2-design.md`. +This repo is an agent skill that audits prose for AI tells and rewrites it, on +evidence from the studies in `skills/humanize/references/SOURCES.md`. Design specs: +`docs/design/2026-09-1{3,4}-humanize-*-design.md`. ## Invariants to enforce diff --git a/CLAUDE.md b/CLAUDE.md index 05968bc..d264407 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # humanize — working notes for Claude -Claude Code plugin that audits prose for AI tells and rewrites it. Evidence base: -StoryScope (arXiv 2604.03136). Spec and plan: `docs/design/`. Changelog: `CHANGELOG.md`. +Agent skill that audits prose for AI tells and rewrites it. Evidence base: 13 studies +in `references/SOURCES.md`. Spec and plan: `docs/design/`. Changelog: `CHANGELOG.md`. ## Commands diff --git a/README.md b/README.md index 0e2ce8e..856fc71 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,20 @@ # humanize -A Claude Code plugin that finds and removes the tells that mark prose as -AI-generated, and rewrites it to read as natural human writing — without -flattening the author's voice. - -It is grounded in [StoryScope](https://github.com/jenna-russell/storyscope) -(Russell, Rajendhran, Pham, Iyyer, Wieting, *StoryScope: Investigating -idiosyncrasies in AI fiction*, -[arXiv:2604.03136](https://arxiv.org/abs/2604.03136)), which measured 304 -narrative and stylistic features on 61,575 stories and found that AI writing -converges on shared defaults while human writing disperses. This plugin turns -the 77 features with the largest human-vs-AI gaps into an audit checklist, -adds the surface-level tells StoryScope deliberately excluded, and pairs both -with a dependency-free scanner for the numbers a model can't eyeball. v0.2 -adds a grammar and repetition layer from register and reader-perception -studies (Reinhart et al. 2025; Jakesch et al. 2023; Herbold et al. 2023 and -others); every cited number resolves in `references/SOURCES.md`. +An agent skill that audits prose for the tells of AI writing and rewrites them +out, without flattening the author's voice. Runs in Claude Code, Codex, Cursor, +Hermes Agent, Claude Desktop. + +The reference entries rest on thirteen studies. StoryScope (2026) measured 304 +narrative and stylistic features over 61,575 stories, and its widest human-vs-AI +gaps supply the base rates. Reinhart et al. (2025) put participial modifiers at +5.3 times the human rate and nominalization at 2.1 times. Jakesch et al. (2023) +found repeated phrasing the strongest true signal of a text's source. Herbold et +al. (2023) recorded a lexical-diversity reversal between model generations, a +reminder that findings expire. humanize turns that work into an audit checklist +plus a dependency-free scanner for the counts a model cannot eyeball; every flag +quotes the line it came from. One pattern organizes the whole checklist: AI +converges on shared defaults while human writing disperses. Every cited number +resolves in `references/SOURCES.md`. ## Install @@ -105,8 +104,8 @@ skills/humanize/ references/ principles.md surface-tells.md vocabulary, punctuation, shape, discourse moves - style-tells.md 20 StoryScope style features with base rates - narrative-tells.md 57 StoryScope narrative features (fiction only) + style-tells.md 20 style features with StoryScope base rates + narrative-tells.md 57 narrative features (fiction only), StoryScope base rates model-fingerprints.md Claude / GPT / Gemini / DeepSeek / Kimi tendencies SOURCES.md citation registry (not loaded at runtime) scripts/surface_scan.py stdlib-only metrics: burstiness and sentence tails, punctuation, diff --git a/tests/test_portability.py b/tests/test_portability.py index e45814b..4621389 100644 --- a/tests/test_portability.py +++ b/tests/test_portability.py @@ -178,7 +178,9 @@ def test_zip_packager_contract(tmp_path): with zipfile.ZipFile(out) as zf: members = set(zf.namelist()) expected = {"humanize/SKILL.md", "humanize/scripts/surface_scan.py"} - expected |= {f"humanize/references/{p.name}" for p in (SKILL_DIR / "references").iterdir()} + expected |= { + f"humanize/references/{p.name}" for p in (SKILL_DIR / "references").iterdir() if p.is_file() + } assert members == expected, members ^ expected @@ -198,3 +200,40 @@ def test_zip_packager_rejects_non_spec_frontmatter(tmp_path): assert "argument-hint" in str(e) else: raise AssertionError("expected PackagingError") + + +NUMBER_WORDS = { + "eleven": 11, + "twelve": 12, + "thirteen": 13, + "fourteen": 14, + "fifteen": 15, + "sixteen": 16, + "seventeen": 17, + "eighteen": 18, + "nineteen": 19, + "twenty": 20, +} +STUDY_COUNT_SURFACES = { + "plugin.json": r"against (\d+) studies", + ".claude-plugin/plugin.json": r"against (\d+) studies", + ".codex-plugin/plugin.json": r"(\d+) studies", + ".claude-plugin/marketplace.json": r"(\d+) studies", + "pyproject.toml": r"from (\d+) studies", + "CLAUDE.md": r"Evidence base: (\d+) studies", + "README.md": r"rest on (\w+) studies", + "skills/humanize/SKILL.md": r"Grounded in (\w+) studies", +} + + +def test_study_count_matches_sources(): + sources = (SKILL_DIR / "references/SOURCES.md").read_text(encoding="utf-8") + n = len(re.findall(r"^## `[a-z0-9-]+`", sources, re.M)) + assert n >= 10 + for rel, pattern in STUDY_COUNT_SURFACES.items(): + text = (ROOT / rel).read_text(encoding="utf-8") + found = re.findall(pattern, text) + assert found, f"{rel}: no study count matching {pattern!r}" + for token in found: + value = int(token) if token.isdigit() else NUMBER_WORDS[token.lower()] + assert value == n, f"{rel} says {token}, SOURCES.md has {n}" From 3060a9f1948abd4c39e21413d66726896553647d Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 10:54:02 -0400 Subject: [PATCH 12/22] Document installation per harness; portability and version invariants Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- .cursor/BUGBOT.md | 9 ++++++++- CLAUDE.md | 24 +++++++++++++++++++----- README.md | 45 ++++++++++++++++++++++++++++++--------------- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/.cursor/BUGBOT.md b/.cursor/BUGBOT.md index 8247844..4403e85 100644 --- a/.cursor/BUGBOT.md +++ b/.cursor/BUGBOT.md @@ -10,7 +10,14 @@ evidence from the studies in `skills/humanize/references/SOURCES.md`. Design spe Python standard library and runs on Python 3.9+. Flag any third-party import or 3.10+ syntax (match statements, `X | Y` in runtime positions, PEP 604 in non-annotation code). -- Nothing under `tests/` or `skills/**/scripts/` makes network or LLM calls. +- Nothing under `tests/`, `skills/**/scripts/`, or `tools/package_skill_zip.py` makes network or LLM calls. +- `skills/` is harness-agnostic: no harness variables (`${CLAUDE_…}`, `HERMES_SKILL_DIR`, + `CURSOR_…`, `CODEX_…`) and no install commands inside it; frontmatter is exactly the Agent + Skills fields (`argument-hint` is a hard error outside Claude Code). `tests/test_portability.py` + enforces both. +- Version lives in `plugin.json`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, both + `marketplace.json` fields, `pyproject.toml`, and the newest CHANGELOG heading — + `tests/test_portability.py` fails on drift. - A `Base rate:` line in `skills/humanize/references/*.md` must trace to a row in `data/storyscope_feature_gaps.csv` (or a future CSV documented in `data/README.md`; none added in v0.2). If a PR changes a diff --git a/CLAUDE.md b/CLAUDE.md index d264407..af32ea1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,9 +7,9 @@ in `references/SOURCES.md`. Spec and plan: `docs/design/`. Changelog: `CHANGELOG ``` uv sync # first time -uv run pytest -q # 100 tests, must be warning-free +uv run pytest -q # must be warning-free uv run ruff format && uv run ruff check --fix -claude plugin validate --strict . +claude plugin validate --strict . # marketplace JSON only; does not inspect plugin or skill contents uv run python skills/humanize/scripts/surface_scan.py --text python3 tools/gen_tell_scaffold.py style|narrative # regenerate reference scaffolds ``` @@ -47,8 +47,16 @@ Never run bare `ruff format .` — ruff 0.16 formats Python fences inside - `SKILL.md` stays under ~150 lines. `/humanize` is the skill itself — do not add a `commands/` directory (it registers a duplicate skill named `humanize`). - Marketplace entry stays `strict: true` with no component arrays; `plugin.json` - is authoritative. Bump `version` in both when shipping. -- Nothing under `tests/` or `plugins/**/scripts/` touches the network. + is authoritative. Version lives in `plugin.json`, `.claude-plugin/plugin.json`, + `.codex-plugin/plugin.json`, both `marketplace.json` fields, `pyproject.toml`, + and the newest CHANGELOG heading — `tests/test_portability.py` fails on drift. +- Nothing under `tests/`, `skills/**/scripts/`, or `tools/package_skill_zip.py` touches the network. +- `skills/` is harness-agnostic: no harness variables (`${CLAUDE_…}`, `HERMES_SKILL_DIR`, + `CURSOR_…`, `CODEX_…`) and no install commands inside it; frontmatter is exactly the Agent + Skills fields (`argument-hint` is a hard error outside Claude Code). `tests/test_portability.py` + enforces both. +- Public copy names StoryScope only where it is the specific source of a number or a file; the + tool is described as grounded in the `SOURCES.md` registry (the study count is tested). ## Workflow @@ -57,7 +65,13 @@ linear history required, the four CI jobs must pass on an up-to-date branch, and every review thread must be resolved — so after fixing a Bugbot finding, resolve its thread (GraphQL `resolveReviewThread`) before merging. Merge with `gh pr merge --rebase` (rebase is the only enabled method; merged branches are -deleted automatically). Tag releases on `main` after the merge (`vX.Y.Z`). CI runs pre-commit, pytest (3.9 and +deleted automatically). Release: `tools/smoke_harnesses.sh` must PASS on every harness installed +here (transcripts under `docs/acceptance/`), then merge, then post-merge acceptance from `main` +(Codex `plugin marketplace add ccf/humanize` without a ref, Hermes `skills install`, Claude plugin +upgrade, Cowork marketplace add), then tag and `gh release create vX.Y.Z +dist/humanize-skill-X.Y.Z.zip` (built by `python3 tools/package_skill_zip.py`), then verify the zip +uploads and triggers in claude.ai chat. Anything found after the merge is fixed forward as a patch +release. Tag releases on `main` after the merge (`vX.Y.Z`). CI runs pre-commit, pytest (3.9 and 3.13), and plugin validation; Cursor Bugbot reviews every PR and re-reviews on push. Pre-commit hooks run on every commit; never `--no-verify`. After merging a plugin change: `claude plugin marketplace update humanize && claude plugin update humanize@humanize`. diff --git a/README.md b/README.md index 856fc71..9bbf570 100644 --- a/README.md +++ b/README.md @@ -18,21 +18,36 @@ resolves in `references/SOURCES.md`. ## Install -``` -/plugin marketplace add ccf/humanize -/plugin install humanize@humanize -``` - -Requires Python 3.9+ on `PATH` for the scanner. No other dependencies. - -Optional, for Word/PDF/PowerPoint inputs: Anthropic's `document-skills` -plugin, which humanize delegates extraction to (its `docx` read path uses -`pandoc`; `brew install pandoc` on macOS). - -``` -/plugin marketplace add anthropics/skills -/plugin install document-skills@anthropic-agent-skills -``` +Requires Python 3.9+ on `PATH` for the scanner. No other dependencies. Every +command below was checked against its CLI's `--help` for this release. + +| Harness | Install | Verify | Invoke | +|---|---|---|---| +| Claude Code | `/plugin marketplace add ccf/humanize` then `/plugin install humanize@humanize` | `claude plugin list` | `/humanize …` or automatically | +| Codex CLI | `codex plugin marketplace add ccf/humanize` then `codex plugin add humanize@humanize` — or `cp -R skills/humanize ~/.agents/skills/` | `codex debug prompt-input "hi"` lists the skill | `$humanize …` or automatically | +| Cursor | `cp -R skills/humanize ~/.cursor/skills/` (every project) or `.cursor/skills/` (this project) | the skill appears in the `/` menu | `/humanize …` or automatically | +| Hermes Agent | `hermes skills install ccf/humanize/humanize --category writing`; in a running session, `/reload-skills` | `hermes skills list` | `/humanize …` or automatically | +| Any Agent-Skills harness | `npx skills add ccf/humanize` or `cp -R skills/humanize ~/.agents/skills/` | harness-specific | harness-specific | +| Claude Desktop — Cowork | Customize → Plugins → Add from repository `ccf/humanize` → install `humanize` | listed under Customize → Plugins | `/` or `+` picker, or automatically | +| Claude Desktop — chat (claude.ai) | Customize → Skills → upload `humanize-skill-.zip` from the [latest release](https://github.com/ccf/humanize/releases); "Code execution and file creation" must be on | listed under Customize → Skills | automatically, or the sidebar `/` menu | + +Notes: + +- **Codex** reads `/.agents/skills/` and `~/.agents/skills/` but not `.claude/skills/`, and + caps the injected skills catalog at 2% of the context window; a long catalog drops skills. +- **Cursor** syncs only `~/.cursor/skills/` to Cloud Agents, and only with **Sync Skills for Cloud + Agents** on (Settings → Agents). The Cursor marketplace is reviewed by hand and has no CLI. +- **Hermes** scans installed scripts (`surface_scan.py` is standard-library and passes) and runs + the scanner on the host, so on a remote terminal backend (docker, modal, ssh) the scan step is + skipped and the audit proceeds from reading alone. `hermes plugins install` also works on + recent builds but installs the package disabled and read-only; prefer `hermes skills install`. + Installing from a raw `SKILL.md` URL fetches one file and is not supported. +- **Claude Desktop** picks up a Cowork plugin update only when `version` changes. In claude.ai + chat the skill triggers by description; picking it from the `/` menu passes no arguments. +- **Word, PDF, PowerPoint inputs.** humanize delegates extraction to the harness's document + skills. In Claude Code that is Anthropic's `document-skills` plugin (`/plugin marketplace add + anthropics/skills`, `/plugin install document-skills@anthropic-agent-skills`; its `docx` read + path uses `pandoc` — `brew install pandoc` on macOS); Claude Desktop ships the same skills. ## Use From ef94c931dd133b38855356a6956226ff252f53b3 Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 10:55:27 -0400 Subject: [PATCH 13/22] Drop the superseded tag sentence from the Workflow notes Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index af32ea1..f247fe0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,7 +71,7 @@ here (transcripts under `docs/acceptance/`), then merge, then post-merge accepta upgrade, Cowork marketplace add), then tag and `gh release create vX.Y.Z dist/humanize-skill-X.Y.Z.zip` (built by `python3 tools/package_skill_zip.py`), then verify the zip uploads and triggers in claude.ai chat. Anything found after the merge is fixed forward as a patch -release. Tag releases on `main` after the merge (`vX.Y.Z`). CI runs pre-commit, pytest (3.9 and +release. CI runs pre-commit, pytest (3.9 and 3.13), and plugin validation; Cursor Bugbot reviews every PR and re-reviews on push. Pre-commit hooks run on every commit; never `--no-verify`. After merging a plugin change: `claude plugin marketplace update humanize && claude plugin update humanize@humanize`. From 2e6c86f9388380c036976008957c4191bec70a04 Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 11:09:58 -0400 Subject: [PATCH 14/22] Add the harness smoke matrix and v0.3 acceptance transcripts Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- docs/acceptance/v0.3/README.md | 69 ++++++++++ docs/acceptance/v0.3/claude-code.md | 201 ++++++++++++++++++++++++++++ docs/acceptance/v0.3/codex.md | 19 +++ docs/acceptance/v0.3/cursor.md | 1 + docs/acceptance/v0.3/hermes.md | 25 ++++ tools/smoke_harnesses.sh | 91 +++++++++++++ 6 files changed, 406 insertions(+) create mode 100644 docs/acceptance/v0.3/README.md create mode 100644 docs/acceptance/v0.3/claude-code.md create mode 100644 docs/acceptance/v0.3/codex.md create mode 100644 docs/acceptance/v0.3/cursor.md create mode 100644 docs/acceptance/v0.3/hermes.md create mode 100755 tools/smoke_harnesses.sh diff --git a/docs/acceptance/v0.3/README.md b/docs/acceptance/v0.3/README.md new file mode 100644 index 0000000..a235d56 --- /dev/null +++ b/docs/acceptance/v0.3/README.md @@ -0,0 +1,69 @@ +# v0.3 pre-merge acceptance + +Local acceptance matrix for the portable `humanize` skill (v0.3), run before merging +`feat/v0.3`. Not run by CI or pytest; run manually with `tools/smoke_harnesses.sh`. + +## Date + +2026-09-14 + +## What the transcripts are + +Each `.md` file in this directory is the captured transcript of one headless +run of a harness (Claude Code, Codex, Cursor, or Hermes) asked to run the humanize +skill, audit-only, against `tests/fixtures/ai_report.txt`. The script installs the +working-tree copy of `skills/humanize` into each harness the harness's own way (a +temp project for Codex and Cursor, `--plugin-dir .` for Claude Code, a copy into +`~/.hermes/skills/writing/humanize` for Hermes), sends one non-interactive request, +and captures the raw output. + +## PASS rule + +A harness is judged PASS only with evidence that both the skill and the scanner ran: + +- All four expected audit rows are present (case-insensitive): trailing participial + clause, verbatim repetition, container-noun phrase, safety disclaimer opener. +- No nominalization row (nominalization is reported-only per spec, never a row). +- The scanner's exact participial-tails count and repetition rate (computed by the + script from a live `surface_scan.py` run against the same fixture) appear verbatim + in the transcript. +- Where the harness exposes tool-call events (Claude Code, Codex), a tool event names + `surface_scan.py`. +- No rewrite section (`## Rewrite` or "Choices you may want to reverse") — the request + was audit-only. + +`tools/smoke_harnesses.sh` prints one `PASS `, `FAIL `, or +`SKIP (reason)` line per harness, with reason lines above a FAIL. + +## CLI versions (pinned before running, 2026-09-14) + +| CLI | Version | +| --- | --- | +| `claude` | 2.1.270 (Claude Code) | +| `codex` | codex-cli 0.154.0 | +| `agent` (Cursor) | 2026.01.23-916f423 | +| `hermes` | Hermes Agent v0.11.0 (2026.4.23) | + +## Deviation from the brief + +The brief's `run_cursor` used `agent -p "$REQUEST"`. The installed `agent` CLI's +config (`~/.cursor/cli-config.json`) has `approvalMode: allowlist` with only +`Shell(ls)` allowed, so a headless run could not execute the scanner. The script +instead runs `agent -p --force --output-format text ""` — `--force` and +`--output-format` are both flags on the installed CLI (confirmed via `agent --help`). +This is an authorized deviation from the controller; it does not change what the CLI +is configured to do outside of this one invocation, and the script never edits Cursor +config. + +## Side effects + +- Codex and Cursor runs use temp projects; Codex additionally runs with `--ephemeral` + (nothing written under `$HOME`). +- Hermes has no per-project skill scope on v0.11.0: the script copies + `skills/humanize` into `~/.hermes/skills/writing/humanize` for the run and removes + only that directory afterward. If that path already exists, the run is SKIPped and + nothing is touched. +- Acceptance step 4 (`codex plugin marketplace add ccf/humanize@feat/v0.3`) adds a + marketplace to the user's Codex config; the same run removes the plugin and the + marketplace afterward and verifies with `codex plugin marketplace list`. +- The script never updates or reconfigures any harness CLI. diff --git a/docs/acceptance/v0.3/claude-code.md b/docs/acceptance/v0.3/claude-code.md new file mode 100644 index 0000000..0dfee37 --- /dev/null +++ b/docs/acceptance/v0.3/claude-code.md @@ -0,0 +1,201 @@ +# Claude Code + +## Tool calls +{"skill": "humanize:humanize", "args": "tests/fixtures/ai_report.txt --audit-only"} +{"command": "cd /Users/ccf/git/humanize/skills/humanize && cat references/principles.md && echo ===== && cat /Users/ccf/git/humanize/tests/fixtures/ai_report.txt && echo ===== && wc -w /Users/ccf/git/humanize/tests/fixtures/ai_report.txt", "description": "Read principles reference and the report fix +{"command": "cd /Users/ccf/git/humanize/skills/humanize && cat /Users/ccf/git/humanize/tests/fixtures/ai_report.txt && echo '-----WC-----' && wc -w /Users/ccf/git/humanize/tests/fixtures/ai_report.txt && echo '-----SCAN-----' && python3 scripts/surface_scan.py --text /Users/ccf/git/humanize/tests/fi +{"command": "cat references/surface-tells.md && echo '-----STYLE-----' && cat references/style-tells.md", "description": "Read the surface and style tell references"} +{"command": "python3 scripts/surface_scan.py /Users/ccf/git/humanize/tests/fixtures/ai_report.txt | python3 -c '\nimport json,sys\nd=json.load(sys.stdin)\nfor k in (\"discourse\",\"grammar\",\"repetition\",\"sentence_len\",\"paragraph_len\",\"structures\"):\n print(k, json.dumps(d.get(k), indent= + +## Output +I'll invoke the humanize skill in audit-only mode on the report fixture. +Base directory for this skill: /Users/ccf/git/humanize/skills/humanize + +# Humanize + +Make prose read as natural human writing by finding and removing the tells that +mark it as AI-generated. Grounded in thirteen studies of measured style +differences: AI converges on shared defaults; human writing disperses. + +Read `references/principles.md` first, every time. + +## Two modes + +**Drafting mode** — you are writing the prose yourself. Before emitting, check +it against `references/surface-tells.md` and `references/style-tells.md` (add +`references/narrative-tells.md` for fiction). Fix what you find. Return only +the text. No audit table, no commentary about tells. + +**Audit mode** — the user asks you to humanize existing text, or `/humanize` +was invoked. Follow all six steps below. + +## Invocation (`/humanize` only) + +Applies only when the user typed `/humanize …` (Codex: `$humanize …`). On +auto-invoke there are no arguments: skip this section. Options below also +work as plain words in a request ("audit only", "this is fiction"). + +Arguments: tests/fixtures/ai_report.txt --audit-only + +Treat an empty line, or one that still reads literally as `tests/fixtures/ai_report.txt --audit-only`, as no +arguments. Then resolve the target, in order: +1. Strip any flags (`--audit-only`, `--fiction`, `--prose`) from the arguments. +2. If what remains is a path to an existing file, read that file. +3. Else if anything remains, treat it as the text itself. +4. Else (no arguments, or flags only): use the most recent prose you produced + in this conversation, or the text the user most recently shared. If there is + none, ask what to humanize. + +Flags: +- `--audit-only` — stop after step 3 (the audit table). Do not rewrite. +- `--fiction` / `--prose` — override step 1's classification (`--prose` means + `expository` or `conversational`; pick whichever fits). + +## Audit mode + +### 1. Classify + +Decide the class from the text itself: +- `fiction` — narrative with characters and events +- `expository` — essay, article, documentation, report, post +- `conversational` — email, message, chat reply, note + +Load `references/principles.md`, `references/surface-tells.md`, and +`references/style-tells.md`. Load `references/narrative-tells.md` only for +`fiction`. Load `references/model-fingerprints.md` only if the user names the +generating model or asks which model wrote it. Honor a `--fiction` / `--prose` +override if given. + +In `expository` prose, nominalizations, container nouns, and participial +tails are native register — prompts to look, not tells, unless extreme for +the length. + +### 2. Scan + +If the text is 80 words or longer, run the scanner and keep the output: + +```bash +python3 scripts/surface_scan.py --text +``` + +Run it from this skill's folder (the directory holding this SKILL.md). Write +pasted text to a temp file first. For the full JSON, drop `--text`. Under 80 +words, skip this step; the statistics are noise. The scanner strips code, links, +URLs, and heading markers from Markdown before measuring. + +**Non-text sources** (`.docx`, `.pdf`, `.pptx`, `.odt`, `.rtf`): the scanner reads +plain text only. Extract first with the harness's document skills (Anthropic's +`docx`/`pdf` skills ship with Claude Code's `document-skills` plugin and with +Claude Desktop) to write a temp `.md`, then scan and audit that. In claude.ai the +file is an attachment: convert it in the sandbox (pandoc, pdfplumber). Without +document skills, read the file yourself if you can and write the text to a temp +file; otherwise ask for a text export. Deliver Markdown; a Word file via `docx`. + +### 3. Audit + +Walk every loaded tell list. For each tell you judge present, record: + +- the tell name +- one quoted example from the text (add `(×N)` if it recurs) +- the base rate line from the reference, or the scan number + +Rank by strength of evidence. Report **at most ten**. For texts under ~300 +words, quote raw counts from `punct.counts`, not per-1k rates. Quote +`repetition.phrases` and `grammar.*.hits` verbatim. `nominalization.hits` +never become a row. Other studies' ratios never go in the base-rate column. +Format: + +``` +| # | Tell | Evidence | Base rate / metric | +|---|------|----------|--------------------| +| 1 | Emotion via embodied sensation | "her chest tightened" (×6) | AI 81% / human 39% | +| 2 | Em-dash density | 14.1 per 1k words | rule of thumb: human 0–4 | +``` + +If `--audit-only`, stop here. + +### 4. Infer voice + +State in one line the register, audience, and intent you will preserve, e.g. +"Direct, peer-to-peer, mildly informal; telling a manager the date will slip." +Ask the user one question only if a rewrite decision hinges on something you +cannot infer (whether the piece is meant to be funny; whether a real name may +be used). Otherwise do not ask. + +### 5. Rewrite + +In priority order: + +1. Preserve every fact, claim, name, number, and the author's position. +2. Fix only the tells that fired. Leave everything else as written. +3. Introduce variance, not a new default: vary sentence and paragraph length; + let a plain sentence stay plain; name an emotion once instead of embodying + it again; stop at the climax; allow a specific real-world reference where + the author plausibly would. +4. Do not strip passives by reflex: GPT-4o (2024-era) used the agentless + passive at about half the human rate (Reinhart et al. 2025). Recast one + only when the inferred voice or a fired tell calls for it. +5. Make `addition`-tagged fixes only when the inferred voice would plausibly do + that, and list them under "Choices you may want to reverse". +6. Match the inferred voice. Terse stays terse. + +### 6. Verify + +Re-run the scanner on the rewrite. Show a before/after line for each metric +that changed materially. Verify by the scan and quoted spans, not by whether +it reads human to you. If the rewrite removed every long sentence +(`sentence_len.max` fell hard) or flattened the burstiness (`cv` fell), say so +and reread: converging is a failure even as tell counts fall. Confirm no fact +was dropped by re-reading both. Never describe the result as undetectable, as +passing a detector, or as certified human. It is better writing; say that. + +## Output shape (audit mode) + +1. Audit table (≤10 rows) +2. One-line voice statement +3. The rewrite +4. Before/after metrics (only those that changed) +5. "Choices you may want to reverse" (only if any `addition` fixes were made) + +**Class:** expository (a quarterly status report, 620 words, 34 sentences, 12 paragraphs). + +| # | Tell | Evidence | Base rate / metric | +|---|------|----------|--------------------| +| 1 | Trailing participial clause | ", ensuring alignment across teams before any record reaches" (×14, plus ", reflecting the late discovery of schema drift in the", ", creating a sense of momentum that was missing last quarter", ", allowing designers to retire eleven legacy patterns") | 22.6/1k; ratio 5.3×, d = 1.38, 2024-era models [reinhart-2025] | +| 2 | Safety disclaimer opener | "It's important to approach this update carefully" and "I'm not able to share vendor pricing here, but" (sentences 1 and 2) | `disclaimer_opener` fired; 46% vs 0.2% of responses per family [rudnicka-2026] | +| 3 | Verbatim repetition | "across all workstreams and teams" (×3, sentences 6, 18, 33) | repeated phrases OR 1.47, the strongest predictor readers miss [jakesch-2023] | +| 4 | Uniform sentence length | every sentence 14–27 words; longest flat run 8 | cv 0.19; 0.0% over 30 words (humans 31.2% vs 17.5–21.0% [munoz-ortiz-2024]); rule of thumb human 0.5–0.9, AI often below 0.4 | +| 5 | Uniform paragraph length | 12 paragraphs, every one 2–3 sentences, each opening on a topic and closing on a status line | paragraph cv 0.137; rule of thumb below 0.3 across five or more paragraphs is uniform | +| 6 | Summary closer | "In summary, delivery across all workstreams and teams stayed on plan despite the schema issue" | `summary_closer` yes | +| 7 | Abstract container-noun phrase | "creating a sense of momentum", "The weight of the decision to pause backfills fell mostly on" (×2) | `container_of` 2; heads attested in LAMP Table 8, rare in human seed paragraphs [chakrabarty-2025] | +| 8 | Voice markedness | a company voice with no person in it: "the overall picture is encouraging and the risks are manageable", "Please raise any concerns before Friday so they can be folded into the planning session" | moderately marked voice: AI 98% / human 83% | +| 9 | Use of irony and humor | nothing in 620 words is meant other than literally, including the backfill pause and the two-week slip | any discernible humor or irony: AI 62% / human 88% | + +Notes outside the table, per the reference rules: + +- **Nominalizations are a prompt, not a row** in expository text. The scanner counted 24 hits with "the X of" frames stacked through nearly every paragraph: "the implementation of", "the migration of", "the optimization of", "the remediation of", "the transformation of", "the escalation of", "the integration of". That is heavy even for report register and would be worth unburying in a rewrite. +- **Clean on the rest.** No wordlist hits, no hedges or intensifiers, zero em-dashes, semicolons, or colons, one tricolon, no rhetorical questions, no parallel-opener runs. The lone not-but match is the disclaimer in sentence 2, already covered by row 2. +- **Rows 8 and 9 are weakest.** Both are fiction-corpus rates, and a status report plausibly has no humor in it. Those fixes are additions and would only be made if the author's voice would do that. + +Stopped after the audit table as requested. No rewrite was made. +**Class:** expository (a quarterly status report, 620 words, 34 sentences, 12 paragraphs). + +| # | Tell | Evidence | Base rate / metric | +|---|------|----------|--------------------| +| 1 | Trailing participial clause | ", ensuring alignment across teams before any record reaches" (×14, plus ", reflecting the late discovery of schema drift in the", ", creating a sense of momentum that was missing last quarter", ", allowing designers to retire eleven legacy patterns") | 22.6/1k; ratio 5.3×, d = 1.38, 2024-era models [reinhart-2025] | +| 2 | Safety disclaimer opener | "It's important to approach this update carefully" and "I'm not able to share vendor pricing here, but" (sentences 1 and 2) | `disclaimer_opener` fired; 46% vs 0.2% of responses per family [rudnicka-2026] | +| 3 | Verbatim repetition | "across all workstreams and teams" (×3, sentences 6, 18, 33) | repeated phrases OR 1.47, the strongest predictor readers miss [jakesch-2023] | +| 4 | Uniform sentence length | every sentence 14–27 words; longest flat run 8 | cv 0.19; 0.0% over 30 words (humans 31.2% vs 17.5–21.0% [munoz-ortiz-2024]); rule of thumb human 0.5–0.9, AI often below 0.4 | +| 5 | Uniform paragraph length | 12 paragraphs, every one 2–3 sentences, each opening on a topic and closing on a status line | paragraph cv 0.137; rule of thumb below 0.3 across five or more paragraphs is uniform | +| 6 | Summary closer | "In summary, delivery across all workstreams and teams stayed on plan despite the schema issue" | `summary_closer` yes | +| 7 | Abstract container-noun phrase | "creating a sense of momentum", "The weight of the decision to pause backfills fell mostly on" (×2) | `container_of` 2; heads attested in LAMP Table 8, rare in human seed paragraphs [chakrabarty-2025] | +| 8 | Voice markedness | a company voice with no person in it: "the overall picture is encouraging and the risks are manageable", "Please raise any concerns before Friday so they can be folded into the planning session" | moderately marked voice: AI 98% / human 83% | +| 9 | Use of irony and humor | nothing in 620 words is meant other than literally, including the backfill pause and the two-week slip | any discernible humor or irony: AI 62% / human 88% | + +Notes outside the table, per the reference rules: + +- **Nominalizations are a prompt, not a row** in expository text. The scanner counted 24 hits with "the X of" frames stacked through nearly every paragraph: "the implementation of", "the migration of", "the optimization of", "the remediation of", "the transformation of", "the escalation of", "the integration of". That is heavy even for report register and would be worth unburying in a rewrite. +- **Clean on the rest.** No wordlist hits, no hedges or intensifiers, zero em-dashes, semicolons, or colons, one tricolon, no rhetorical questions, no parallel-opener runs. The lone not-but match is the disclaimer in sentence 2, already covered by row 2. +- **Rows 8 and 9 are weakest.** Both are fiction-corpus rates, and a status report plausibly has no humor in it. Those fixes are additions and would only be made if the author's voice would do that. + +Stopped after the audit table as requested. No rewrite was made. diff --git a/docs/acceptance/v0.3/codex.md b/docs/acceptance/v0.3/codex.md new file mode 100644 index 0000000..ff3f91c --- /dev/null +++ b/docs/acceptance/v0.3/codex.md @@ -0,0 +1,19 @@ +# Codex + +## Tool events +{"type":"item.completed","item":{"id":"item_2","type":"command_execution","command":"/bin/zsh -lc 'cat .agents/skills/humanize/SKILL.md'","aggregated_output":"---\nname: humanize\ndescription: Use when drafting or editing any prose — email, essay, documentation, blog post, story, chat reply — or when asked to \"humanize\" text, make it \"sound less like AI\", \"more natural\", \"less robotic\", or remove AI tells. Also use when reviewing prose someone else wrote. Not for code, config, or commit messages.\n---\n\n# Humanize\n\nMake prose read as natural human writing by finding and removing the tells that\nmark it as AI-generated. Grounded in thirteen studies of measured style\ndifferences: AI converges on shared defaults; human writing disperses.\n\nRead `references/principles.md` first, every time.\n\n## Two modes\n\n**Drafting mode** — you are writing the prose yourself. Before emitting, check\nit against `references/surface-tells.md` and `references/style-tells.md` (add\n`references/narrative-tells.md` for fiction). Fix what you find. Return only\nthe text. No audit table, no commentary about tells.\n\n**Audit mode** — the user asks you to humanize existing text, or `/humanize`\nwas invoked. Follow all six steps below.\n\n## Invocation (`/humanize` only)\n\nApplies only when the user typed `/humanize …` (Codex: `$humanize …`). On\nauto-invoke there are no arguments: skip this section. Options below also\nwork as plain words in a request (\"audit only\", \"this is fiction\").\n\nArguments: $ARGUMENTS\n\nTreat an empty line, or one that still reads literally as `$ARGUMENTS`, as no\narguments. Then resolve the target, in order:\n1. Strip any flags (`--audit-only`, `--fiction`, `--prose`) from the arguments.\n2. If what remains is a path to an existing file, read that file.\n3. Else if anything remains, treat it as the text itself.\n4. Else (no arguments, or flags only): use the most recent prose you produced\n in this conversation, or the text the user most recently shared. If there is\n none, ask what to humanize.\n\nFlags:\n- `--audit-only` — stop after step 3 (the audit table). Do not rewrite.\n- `--fiction` / `--prose` — override step 1's classification (`--prose` means\n `expository` or `conversational`; pick whichever fits).\n\n## Audit mode\n\n### 1. Classify\n\nDecide the class from the text itself:\n- `fiction` — narrative with characters and events\n- `expository` — essay, article, documentation, report, post\n- `conversational` — email, message, chat reply, note\n\nLoad `references/principles.md`, `references/surface-tells.md`, and\n`references/style-tells.md`. Load `references/narrative-tells.md` only for\n`fiction`. Load `references/model-fingerprints.md` only if the user names the\ngenerating model or asks which model wrote it. Honor a `--fiction` / `--prose`\noverride if given.\n\nIn `expository` prose, nominalizations, container nouns, and participial\ntails are native register — prompts to look, not tells, unless extreme for\nthe length.\n\n### 2. Scan\n\nIf the text is 80 words or longer, run the scanner and keep the output:\n\n```bash\npython3 scripts/surface_scan.py --text \n```\n\nRun it from this skill's folder (the directory holding this SKILL.md). Write\npasted text to a temp file first. For the full JSON, drop `--text`. Under 80\nwords, skip this step; the statistics are noise. The scanner strips code, links,\nURLs, and heading markers from Markdown before measuring.\n\n**Non-text sources** (`.docx`, `.pdf`, `.pptx`, `.odt`, `.rtf`): the scanner reads\nplain text only. Extract first with the harness's document skills (Anthropic's\n`docx`/`pdf` skills ship with Claude Code's `document-skills` plugin and with\nClaude Desktop) to write a temp `.md`, then scan and audit that. In claude.ai the\nfile is an attachment: convert it in the sandbox (pandoc, pdfplumber). Without\ndocument skills, read the file yourself if you can and write the text to a temp\nfile; otherwise ask for a text export. Deliver Markdown; a Word file via `docx`.\n\n### 3. Audit\n\nWalk every loaded tell list. For each tell you judge present, record:\n\n- the tell name\n- one quoted example from the text (add `(×N)` if it recurs)\n- the base rate line from the reference, or the scan number\n\nRank by strength of evidence. Report **at most ten**. For texts under ~300\nwords, quote raw counts from `punct.counts`, not per-1k rates. Quote\n`repetition.phrases` and `grammar.*.hits` verbatim. `nominalization.hits`\nnever become a row. Other studies' ratios never go in the base-rate column.\nFormat:\n\n```\n| # | Tell | Evidence | Base rate / metric |\n|---|------|----------|--------------------|\n| 1 | Emotion via embodied sensation | \"her chest tightened\" (×6) | AI 81% / human 39% |\n| 2 | Em-dash density | 14.1 per 1k words | rule of thumb: human 0–4 |\n```\n\nIf `--audit-only`, stop here.\n\n### 4. Infer voice\n\nState in one line the register, audience, and intent you will preserve, e.g.\n\"Direct, peer-to-peer, mildly informal; telling a manager the date will slip.\"\nAsk the user one question only if a rewrite decision hinges on something you\ncannot infer (whether the piece is meant to be funny; whether a real name may\nbe used). Otherwise do not ask.\n\n### 5. Rewrite\n\nIn priority order:\n\n1. Preserve every fact, claim, name, number, and the author's position.\n2. Fix only the tells that fired. Leave everything else as written.\n3. Introduce variance, not a new default: vary sentence and paragraph length;\n let a plain sentence stay plain; name an emotion once instead of embodying\n it again; stop at the climax; allow a specific real-world reference where\n the author plausibly would.\n4. Do not strip passives by reflex: GPT-4o (2024-era) used the agentless\n passive at about half the human rate (Reinhart et al. 2025). Recast one\n only when the inferred voice or a fired tell calls for it.\n5. Make `addition`-tagged fixes only when the inferred voice would plausibly do\n that, and list them under \"Choices you may want to reverse\".\n6. Match the inferred voice. Terse stays terse.\n\n### 6. Verify\n\nRe-run the scanner on the rewrite. Show a before/after line for each metric\nthat changed materially. Verify by the scan and quoted spans, not by whether\nit reads human to you. If the rewrite removed every long sentence\n(`sentence_len.max` fell hard) or flattened the burstiness (`cv` fell), say so\nand reread: converging is a failure even as tell counts fall. Confirm no fact\nwas dropped by re-reading both. Never describe the result as undetectable, as\npassing a detector, or as certified human. It is better writing; say that.\n\n## Output shape (audit mode)\n\n1. Audit table (≤10 rows)\n2. One-line voice statement\n3. The rewrite\n4. Before/after metrics (only those that changed)\n5. \"Choices you may want to reverse\" (only if any `addition` fixes were made)\n","exit_code":0,"status":"completed"}} +{"type":"item.completed","item":{"id":"item_6","type":"command_execution","command":"/bin/zsh -lc 'cat .agents/skills/humanize/references/surface-tells.md .agents/skills/humanize/references/style-tells.md'","aggregated_output":"# Surface tells\n\nThe layer StoryScope does not measure: vocabulary, grammar, punctuation,\nsentence and paragraph shape, and discourse moves. Applies to every text\nclass. Each entry names the `surface_scan.py` metric that measures it where\none exists. Ranges marked *rule of thumb* are working heuristics from\npractice, not measured in the StoryScope corpus. Numbers that are not\nStoryScope base rates sit on `Scan:` or `Rule of thumb:` lines with a `[key]`\nthat resolves in `SOURCES.md`.\n\n## Vocabulary\n\n### AI-associated wordlist\nLooks like: \"delve\", \"tapestry\", \"a testament to\", \"navigate the complexities\",\n\"it's worth noting\", \"leverage\", \"robust\", \"seamless\", \"crucial\", \"pivotal\",\n\"foster\", \"underscore\", \"multifaceted\", \"landscape\", \"vibrant\", \"nuanced\",\n\"meticulous\", \"harness\", \"synergy\", \"holistic\", \"streamline\", \"elevate\",\n\"empower\", \"unlock\", \"resonate\", \"realm\", \"beacon\", \"unwavering\".\nScan: `wordlist.rate` per 1k words and `wordlist.hits` with sentence positions.\nRule of thumb: human drafts usually < 3/1k; AI drafts commonly 10–30/1k in\nlong-form prose and can exceed 60/1k in short business emails, where\nboilerplate dominates.\nVintage: calibrated on 2023–2024 model output. A wordlist decays — Kobak et\nal. 2025 [kobak-2025] show marker words rising and falling with model\ngenerations (share of biomedical abstracts containing the word, not a per-1k\nrate; not comparable to the rule of thumb above). Re-check against current\nmodels before firing hard.\nWhy it reads as AI: these words are over-represented in RLHF-era model output\nand under-represented in ordinary human prose of the same register; readers\nhave learned the list.\nFix: removal — replace with the plain word the author would use (\"use\" for\n\"leverage\", \"strong\" for \"robust\", \"important\" for \"crucial\") or cut the word\nentirely; most are decorative.\n\n### Latinate lean\nLooks like: \"utilize\", \"facilitate\", \"demonstrate\", \"commence\", \"implement\",\n\"ascertain\" where \"use\", \"help\", \"show\", \"start\", \"do\", \"find out\" would do.\nBase rate: AI 2.83 / human 2.51 on a 1–5 Anglo-Saxon→Latinate scale\n(StoryScope STY_ALL_016).\nWhy it reads as AI: models default to the formal register of their training\nmass; humans pick the short word unless the register demands otherwise.\nFix: rebalance — swap to the short Germanic word where the voice is not formal.\n\n### Nominalized verbs\nLooks like: \"the implementation of the policy led to an improvement in\nretention\" where \"implementing the policy improved retention\" would do; \"the\nX of\" frames stacked through a paragraph.\nScan: `nominalization.hits` and `nominalization.of_frames` — hits only, no\nrate, no threshold; singular and plural forms are separate hits (Herbold et\nal. 2023 [herbold-2023]; Reinhart et al. 2025 [reinhart-2025]).\nWhy it reads as AI: buried verbs rise monotonically across model generations,\nbut formal, legal, academic, and second-language prose nominalize legitimately\n— in `expository` text these hits are a prompt to look, never a table row.\nFix: rebalance — unbury the verb where the register does not earn the noun.\n\n### Abstract container-noun phrase\nLooks like: \"a sense of unease\", \"a mix of pride and fear\", \"the weight of the\ndecision\" — an abstract container standing in for the concrete thing.\nScan: `grammar.container_of` count and hits; the 13 heads are those attested\nin LAMP Table 8 (Chakrabarty et al. 2025 [chakrabarty-2025]), rare in the\nhuman seed paragraphs.\nWhy it reads as AI: a reflex reach for an abstraction where a human names the\nobject or the feeling; fiction uses these legitimately, so judge density.\nFix: removal — name the concrete thing, or cut the frame and keep the noun.\n\n### Hedge stacks\nLooks like: \"It could perhaps be argued that this might, to some extent,\ngenerally be the case.\"\nScan: `hedges.rate` per 1k. Rule of thumb: > 10/1k in expository prose is\na stack.\nWhy it reads as AI: models hedge to avoid being wrong; a person with a view\nstates it and hedges once, if at all.\nFix: removal — keep at most one hedge per claim; delete the rest.\n\n### Intensifier stacks\nLooks like: \"truly remarkable\", \"deeply meaningful\", \"incredibly important\",\n\"genuinely transformative\", several per paragraph.\nScan: `intensifiers.rate` per 1k. Rule of thumb: > 8/1k reads as padding.\nWhy it reads as AI: intensifiers substitute for specifics; humans intensify\nrarely and usually for effect.\nFix: removal — delete the intensifier or replace the phrase with a concrete\ndetail that earns the emphasis.\n\n## Punctuation\n\n### Em-dash density\nLooks like: \"The plan—while ambitious—was sound—and it worked.\"\nScan: `punct.em_dash` per 1k. Rule of thumb: human nonfiction 0–4/1k; AI\ndrafts often 8–20/1k. Per-1k rates need roughly 300+ words to mean anything;\nbelow that, cite the raw count (`punct.counts.em_dash`) and treat one or two\nmarks as noise. CLI flags like `--audit-only` are not counted; only `word--word`\nor spaced ` -- ` forms count as an em-dash.\nWhy it reads as AI: models use the em-dash as a universal joiner where a human\nwould use a comma, a period, or parentheses, and they use it in every paragraph.\nFix: rebalance — keep one em-dash where it does real work; convert the rest to\nperiods (usually) or commas.\n\n### Semicolon and colon habits\nLooks like: semicolons joining independent clauses in casual prose; colons\nintroducing a clause that restates the previous one.\nScan: `punct.semicolon`, `punct.colon` per 1k.\nWhy it reads as AI: semicolons in a text message or casual email are rare for\nhumans; colon-led restatement is a summarizing tic.\nFix: rebalance — in casual registers, split into two sentences.\n\n## Structures\n\n### Tricolon habit (rule of three)\nLooks like: \"fast, reliable, and secure\"; \"we build, we ship, we learn\"; every\nlist has exactly three items.\nScan: `structures.tricolon` count. Rule of thumb: more than one per 150 words\nis a habit, not a choice. Counts any `A, B, and C` sequence, including clause\njoins; treat the number as a prompt to look, not a verdict.\nWhy it reads as AI: the three-item list is rhythmically satisfying and the model\nreaches for it reflexively; humans produce two- and four-item lists as often.\nFix: rebalance — cut one item, add a fourth, or make one item a sentence of its\nown. Keep a tricolon only where the rhythm is the point.\n\n### Not-X-but-Y framing\nLooks like: \"It's not about the code, it's about the culture.\" \"This isn't a\nsetback—it's an opportunity.\" \"Not only did we ship, but we learned.\"\nScan: `structures.not_but` count.\nWhy it reads as AI: a contrast frame that manufactures insight by negating a\nstrawman; models use it to sound reflective.\nFix: removal — state Y directly. Delete the negated X unless someone actually\nclaimed it.\n\n### Rhetorical question then answer\nLooks like: \"So what does this mean for teams? It means…\" \"Why does this matter?\nBecause…\"\nScan: `structures.rhetorical_q` count (outside dialogue).\nWhy it reads as AI: a transition device that simulates dialogue with the reader;\nhumans use it sparingly and usually with an edge.\nFix: removal — delete the question; keep the answer as a statement.\n\n### Parallel sentence openers\nLooks like: three or more consecutive sentences beginning with the same word\n(\"We… We… We…\", \"It… It… It…\").\nScan: `structures.parallel_openers` (runs of ≥3) and `openers.distinct_ratio`.\nRule of thumb: distinct-opener ratio below 0.6 in prose longer than 15 sentences\nis monotonous.\nWhy it reads as AI: anaphora is a deliberate rhetorical figure; unintentional\nanaphora is a generation artifact.\nFix: rebalance — vary the openers; combine two of the sentences.\n\n### Verbatim repetition\nLooks like: a phrase of four or more words reappearing intact across the piece\n— \"across all workstreams and teams\" three times in a status report — or a\nstring lifted from the prompt or title.\nScan: `repetition.phrases` (silent under 150 words); repeated phrases are the\nstrongest true-source predictor readers miss, OR 1.47 (Jakesch et al. 2023\n[jakesch-2023]); `repetition.repeated_phrase_rate` is reported-only.\nWhy it reads as AI: recurrence with no rhetorical intent; terminology, names,\nand identifiers must repeat — exempt technical and legal prose — and a refrain\nin fiction is deliberate.\nFix: removal — keep one instance and vary or cut the rest.\n\n### Trailing participial clause\nLooks like: a finished sentence that keeps going after a comma with an -ing\nverb: \", ensuring seamless integration\", \", allowing teams to move faster\",\n\", highlighting the importance of\".\nScan: `grammar.participial_tail` count, rate, and hits; ratio 5.3×, d = 1.38,\n2024-era models, news and academic registers (Reinhart et al. 2025\n[reinhart-2025]).\nWhy it reads as AI: the tack-on lets a sentence add a consequence without a\nnew subject, and models reach for it several times a paragraph.\nFix: removal — split into a sentence with its own subject, or drop the clause.\n\n### Uniform sentence length\nLooks like: every sentence 14–20 words; no fragments; no 40-word sentence.\nScan: `sentence_len.cv` (stdev/mean); `sentence_len.pct_over_30` (humans\n31.2% vs 17.5–21.0%, 2023 news corpus, direction not magnitude; Muñoz-Ortiz et\nal. 2024 [munoz-ortiz-2024]); `sentence_len.longest_flat_run` (reported-only).\nA flat profile is also the native shape of plain-language and technical prose\n— `tests/fixtures/human_plain.txt` sits in AI territory on every sentence\nmetric — and is not authorship evidence.\nRule of thumb: published human prose commonly 0.5–0.9; AI drafts often below\n0.4.\nWhy it reads as AI: models regress to the mean sentence; humans write in bursts.\nFix: rebalance — split one long sentence into a short one and a fragment; merge\ntwo mid-length sentences into a long one. Aim for range, not a target.\n\n### Uniform paragraph length\nLooks like: every paragraph three to four sentences; every paragraph opens with\na topic sentence and closes with a mini-conclusion.\nScan: `paragraph_len.cv`. Rule of thumb: below 0.3 across five or more\nparagraphs is uniform.\nWhy it reads as AI: the five-paragraph-essay template applied to everything.\nFix: rebalance — allow a one-sentence paragraph; let one paragraph run long.\n\n## Discourse moves\n\n### Validating opener\nLooks like: \"Great question!\" \"I'd be happy to help.\" \"Absolutely!\" \"That's a\nreally insightful point.\"\nScan: `wordlist.hits` includes \"great question\", \"i hope this helps\".\nWhy it reads as AI: assistant-style acknowledgement before content; humans\nanswer.\nFix: removal — start with the content.\n\n### Restating the prompt\nLooks like: the first paragraph paraphrases the question or task before\naddressing it.\nScan: none; judge by reading.\nWhy it reads as AI: models anchor by echoing input; humans assume the reader\nremembers what they asked.\nFix: removal — delete the paraphrase.\n\n### Summary closer\nLooks like: a final paragraph opening \"In conclusion\", \"Ultimately\", \"Overall\",\n\"In short\" that restates the opening.\nScan: `discourse.summary_closer` (boolean).\nWhy it reads as AI: essay-template closure on texts that do not need it; humans\nend when they are done.\nFix: removal — cut the paragraph, or end on the last concrete point.\n\n### Sign-off advice and offers\nLooks like: \"Remember to…\", \"Feel free to reach out\", \"Don't hesitate to…\",\n\"I hope this helps!\"\nScan: `wordlist.hits` includes \"reach out\", \"don't hesitate\", \"i hope this helps\".\nWhy it reads as AI: assistant boilerplate.\nFix: removal — end with the actual last thing you have to say, or a plain sign-off.\n\n### Safety disclaimer opener and AI self-reference\nLooks like: a first paragraph that qualifies before it answers — \"It's\nimportant to approach this carefully\", \"I'm not able to give specific advice,\nbut\", \"consult a professional\" — or any \"As an AI\" self-reference.\nScan: `discourse.disclaimer_opener.fired` and `.hits`; per-family range 46%\nvs 0.2% of responses (Rudnicka & Juzek 2026 [rudnicka-2026]).\nWhy it reads as AI: assistant safety framing on a text that asked for none;\nthe same phrases mid-document are an ordinary discourse observation.\nFix: removal — start with the answer.\n\n### Headings and bullets in short pieces\nLooks like: a 200-word email with three bold headers and two bulleted lists.\nScan: none; judge by reading.\nWhy it reads as AI: structure imposed regardless of length or medium.\nFix: removal — prose for anything under ~300 words unless the medium expects\nlists.\n# Style tells\n\nStoryScope's Style dimension (figurative language, sound, syntax, register,\ntone, allusion), filtered to the 20 features with a human-vs-AI gap of at least\n15 points (categorical) or 0.30 (1–5 scale). Base rates are measured on 61,575\nstories; see `data/README.md`. These apply to every text class; each entry says\nhow it shows up outside fiction.\n\nEntries are ordered by the size of the human-vs-AI gap, largest first.\n\n### Presence of extended conceit\nLooks like: a metaphor that is introduced and then developed across several\nsentences or the whole piece — the company as a ship, grief as a house with\nrooms, the codebase as a garden — with each paragraph extending it.\nBase rate: AI 83% / human 40% (StoryScope STY_FIG_004)\nWhy it reads as AI: models sustain a governing metaphor because it is a\ncoherence strategy; most human writers drop a figure after one use.\nFix: removal — keep the first instance if it earns its place; cut every later\ncallback to the conceit and say the literal thing instead.\nOutside fiction: the \"journey\" or \"building blocks\" frame that runs through an\nentire blog post or team update.\n\n### Lexical register and consistency\nLooks like: a piece that stays in one register throughout — uniformly elevated,\nor uniformly neutral-standard — with no slang, no shift to plain talk, no\nsudden formal aside.\nBase rate: mixed register with code-switching — AI 19% / human 56% (StoryScope STY_ALL_015)\nWhy it reads as AI: humans slip between registers as mood and audience shift\nmid-text; models hold a single register as a consistency default.\nFix: addition — only when in character: let one sentence go colloquial, or let\na plain paragraph be interrupted by a precise technical term. Do not sprinkle\nslang mechanically.\nOutside fiction: an email that never once says \"yeah\", \"honestly\", or \"ugh\"\nfrom a writer who would.\n\n### Sound Patterning Prominence\nLooks like: alliteration and assonance you notice while reading — \"the slow\nslide of silt\", \"a hollow, swallowing hush\" — recurring through the piece rather\nthan once at a deliberate moment.\nBase rate: noticeable sound patterning — AI 91% / human 55% (StoryScope STY_TON_006)\nWhy it reads as AI: models reach for euphony by default, so the ear-pleasing\nversion of each phrase is the one that survives, while most human prose is\nsonically flat.\nFix: removal — keep the one sound effect that is placed for a reason and de-tune\nthe rest by swapping in the word you would have chosen without the ear in mind.\nOutside fiction: a product page where every feature name alliterates, or a\nheadline picked for sound over accuracy.\n\n### Conventional vs fresh figurative language\nLooks like: every image is a new one — \"the grief sat in her like a stone\nswallowed sideways\" — and no stock phrase appears anywhere in the piece.\nBase rate: predominantly fresh and inventive images — AI 65% / human 30% (StoryScope STY_FIG_003)\nWhy it reads as AI: the direction is counter-intuitive — models are more\ninventive than humans, not more clichéd, and the tell is relentless invention\nwith no ordinary sentence between the images.\nFix: removal — cut most of the fresh images and let plain statement carry the\npassage; a stock phrase in the right place is a human signal, not a failure.\nOutside fiction: an essay where every paragraph opens with a new and unusual\ncomparison instead of stating the point.\n\n### Predominant tonal quality\nLooks like: earnestness throughout — nothing is undercut, no line is wry, and\nthe piece means everything it says at face value.\nBase rate: earnest or lyrical tone — AI 71% / human 40% (StoryScope STY_TON_021)\nWhy it reads as AI: sincerity is the safe default for a model that cannot\npredict whether a joke will land.\nFix: removal — take the earnestness off at least one high point: undercut a\ngrand line, let a character be unimpressed, or state the thing flatly.\nOutside fiction: a launch post that is visibly moved by its own product, with no\ndry aside anywhere in it.\n\n### Sentence-structure repertoire\nLooks like: balanced series as the default sentence shape — \"It changed how we\nbuild, how we ship, how we think\" — paragraph after paragraph.\nBase rate: frequent parallel or list-like structures — AI 99% / human 70% (StoryScope STY_CPX_012)\nWhy it reads as AI: this is near-universal in AI text, because parallelism is\nthe cheapest way to sound composed and models apply it by reflex.\nFix: removal — break the series: cut one limb, make one limb a sentence of its\nown, or replace the list with a single specific. See the tricolon habit in\n`surface-tells.md` for the surface-level check.\nOutside fiction: bullet-shaped sentences in an email — \"faster, cheaper, and\neasier to maintain\" — where one accurate clause would do.\n\n### Recurrent metaphorical motif\nLooks like: one image family threaded through the whole piece — weight, tide,\nmachinery — so that unrelated paragraphs reach for the same register of\ncomparison.\nBase rate: motif present — AI 96% / human 69% (StoryScope STY_FIG_005)\nWhy it reads as AI: a recurring motif is a coherence device the model applies\nacross the whole text, while human writers rarely track their images that far.\nFix: removal — keep the motif's single strongest appearance and rewrite the\nothers from whatever the local scene or paragraph actually offers.\nOutside fiction: a strategy memo where everything is weather, momentum, or\nplumbing from the first line to the last.\n\n### Allusion domain diversity\nLooks like: references that stay in the safe canon — myth, history, literature —\nand never a named band, film, product, or team.\nBase rate: pop-culture or brand-name allusions — AI 13% / human 40% (StoryScope STY_ALL_018)\nWhy it reads as AI: models avoid naming real commercial and cultural artifacts,\nso their references land in a timeless, unplaceable nowhere.\nFix: addition — only when in character: name the actual thing the writer would\nname, the specific show or product, instead of \"a popular streaming series\".\nOutside fiction: a post about consumer software that never names a competitor.\n\n### Use of irony and humor\nLooks like: nothing in the piece is meant other than literally — no joke, no\nsarcasm, no dry undercut anywhere in it.\nBase rate: any discernible humor or irony (inverting the straight-faced value) —\nAI 62% / human 88% (StoryScope STY_TON_023)\nWhy it reads as AI: humor is the highest-risk register for a model, so playing\neverything straight is the safe move.\nFix: addition — only when in character: one dry line where the writer would\nactually be dry. Do not insert jokes into prose whose author has none.\nOutside fiction: an incident report or team update with not one wry remark, from\nsomeone whose speech is full of them.\n\n### Dominant Tonal Register\nLooks like: a narrative voice that lingers, reflects, and finds significance,\nrather than one that is reportorial, hardboiled, or analytic.\nBase rate: lyrical or meditative register — AI 77% / human 52% (StoryScope STY_TON_001)\nWhy it reads as AI: the reflective register is the model's comfortable default\nand gets applied whatever the material is.\nFix: removal — cut the reflective sentences and let the reported facts stand,\nthen pick the register the material actually demands and hold it.\nOutside fiction: a postmortem that keeps pausing to consider what the outage\nmeant for the team.\n\n### Dominant Figurative Device Type\nLooks like: comparisons asserted rather than proposed — \"the deadline was a\nwall\" rather than \"the deadline felt like a wall\" — with metaphor crowding out\nsimile and plain description.\nBase rate: metaphor-dominant — AI 73% / human 50% (StoryScope STY_FIG_002)\nWhy it reads as AI: metaphor is the more literary of the two moves, and models\ntake the more literary move.\nFix: removal — convert some metaphors back to similes or to literal statement;\nthe mix, not the metaphor itself, is what reads human.\nOutside fiction: business writing where processes are engines, funnels, and\nflywheels instead of being described.\n\n### Parataxis vs Hypotaxis Preference\nLooks like: syntax that never commits — some coordination, some subordination,\nevenly mixed, with no run of blunt short clauses and no long embedded sentence.\nBase rate: balanced parataxis and hypotaxis — AI 85% / human 64% (StoryScope STY_CPX_004)\nWhy it reads as AI: the balanced middle is the average of all training prose,\nand averaging is what the model does.\nFix: removal — take out the balance: write one passage as a chain of short\ncoordinated clauses and another as a single long subordinated sentence.\nOutside fiction: documentation where every sentence has the same clause count\nregardless of how simple or complicated the step is.\n\n### Primary Function of Allusion\nLooks like: a reference used to point at the moral — \"like Icarus, we flew too\nclose\" — rather than to color a setting or characterize whoever made it.\nBase rate: allusion as theme-signposting or moral commentary — AI 46% / human 26% (StoryScope STY_ALL_004)\nWhy it reads as AI: models use references to make the point legible, where human\nwriters more often use them for texture, humor, or offhand analogy.\nFix: removal — delete the signposting reference, or repurpose it so it says\nsomething about the person making it rather than about the theme.\nOutside fiction: an essay that closes on a myth or a quotation restating the\nargument.\n\n### Sound patterning devices\nLooks like: alliteration specifically, as the salient device whenever the prose\npushes — \"a steady, stubborn strain\", \"policy, practice, and posture\".\nBase rate: alliteration prominent — AI 98% / human 79% (StoryScope STY_TON_025)\nWhy it reads as AI: alliteration is the sound effect producible by word choice\nalone, so it is the one models overuse.\nFix: removal — replace the alliterating word with the accurate one; if the\nphrase survives only because it sounds good, cut the phrase.\nOutside fiction: alliterative section headings and slogans in a deck or a\nREADME.\n\n### Use of Sentence Fragments\nLooks like: fragments for emphasis, several to a page. Short. Punchy. Like this.\nBase rate: fragments present and stylistically significant — AI 85% / human 67% (StoryScope STY_CPX_003)\nWhy it reads as AI: the direction is counter-intuitive — AI fragments more than\nhumans do, because the one-beat emphatic fragment is a learned dramatic device,\nnot a sign of a loose human hand.\nFix: removal — rejoin most fragments into the sentences they broke off from, and\nkeep at most one where the break carries real emphasis.\nOutside fiction: a social post where every third line is a one-word paragraph.\n\n### Voice Markedness\nLooks like: a voice with flavor but no risk — a little rhythm, a few distinctive\nturns, no dialect, no verbal tic, nothing a reader could imitate.\nBase rate: moderately marked voice, distinct but not extreme — AI 98% / human 83% (StoryScope STY_TON_005)\nWhy it reads as AI: almost every AI passage lands in this middle band, while\nhuman writing spreads out into plain report and into genuine idiosyncrasy.\nFix: removal — take the voice off the middle: either strip it back to plain\nreport, or commit to one real mannerism and carry it consistently.\nOutside fiction: internal writing that sounds like a company rather than like a\nperson.\n\n### Figurative Device Density\nLooks like: a metaphor or simile in most paragraphs, including in sentences that\nhad a job to do and did not need one.\nBase rate: AI 3.66 / human 3.00 on a 1–5 scale (StoryScope STY_FIG_001)\nWhy it reads as AI: models decorate by default, and figurative density is the\nmost visible form that decoration takes.\nFix: rebalance — cut roughly a third of the figures, starting with those that\nexplain something already clear, and keep the ones doing work no literal\nsentence could do.\nOutside fiction: an explainer where every concept arrives with an analogy\nattached to it.\n\n### Rhythmic markedness of prose\nLooks like: cadence you can hear — balanced clauses, repeated openings,\nsentences resolving on a stressed beat — sustained across the whole piece.\nBase rate: AI 3.61 / human 3.17 on a 1–5 scale (StoryScope STY_TON_024)\nWhy it reads as AI: the model optimizes each sentence for fluency, and sustained\nfluency turns into a metronome.\nFix: rebalance — break the meter: let one sentence end awkwardly, run another\npast its natural stop, or drop a clause that exists only for balance.\nOutside fiction: a speech-shaped post that reads as though written to be read\naloud when nobody will read it aloud.\n\n### Allusion density\nLooks like: prose that refers to almost nothing outside itself — no cited text,\nno dated event, no artifact a reader would recognize.\nBase rate: AI 2.26 / human 2.59 on a 1–5 scale (StoryScope STY_ALL_017)\nWhy it reads as AI: models avoid specific external references because those are\nthe most checkable thing in a text, while humans reach for them constantly.\nFix: rebalance — add the reference the writer would actually have made, and\nconfirm it is real before keeping it.\nOutside fiction: a memo that cites no paper, no prior project, and no names.\n\n### Latinate vs Anglo-Saxon lexical flavor\nLooks like: \"utilize\", \"facilitate\", \"commence\", \"demonstrate\" where \"use\",\n\"help\", \"start\", and \"show\" would carry the same sense.\nBase rate: AI 2.83 / human 2.51 on a 1–5 scale (StoryScope STY_ALL_016)\nWhy it reads as AI: the formal register dominates the training mass, so the\nlearned word wins unless the context pushes hard the other way.\nFix: rebalance — swap in the short Germanic word wherever the register does not\nrequire the long one; `surface-tells.md` carries the paired surface check.\nOutside fiction: a chat message written in the register of a policy document.\n","exit_code":0,"status":"completed"}} +{"type":"item.started","item":{"id":"item_7","type":"command_execution","command":"/bin/zsh -lc 'python3 scripts/surface_scan.py /var/folders/fh/cczgqw9n4r97y43bbgz9py5w0000gn/T/tmp.yMTB1qMFiJ/ai_report.txt'","aggregated_output":"","exit_code":null,"status":"in_progress"}} +{"type":"item.completed","item":{"id":"item_7","type":"command_execution","command":"/bin/zsh -lc 'python3 scripts/surface_scan.py /var/folders/fh/cczgqw9n4r97y43bbgz9py5w0000gn/T/tmp.yMTB1qMFiJ/ai_report.txt'","aggregated_output":"{\n \"discourse\": {\n \"summary_closer\": true,\n \"disclaimer_opener\": {\n \"fired\": true,\n \"hits\": [\n {\n \"text\": \"it's important to approach\",\n \"sentence\": 0\n },\n {\n \"text\": \"i'm not able to\",\n \"sentence\": 1\n }\n ]\n }\n },\n \"dialogue\": {\n \"ratio\": 0.0\n },\n \"words\": 620,\n \"sentences\": 34,\n \"paragraphs\": 12,\n \"sentence_len\": {\n \"mean\": 18.24,\n \"stdev\": 3.46,\n \"cv\": 0.19,\n \"min\": 14,\n \"max\": 27,\n \"pct_over_30\": 0.0,\n \"p90\": 25,\n \"longest_flat_run\": 8\n },\n \"paragraph_len\": {\n \"mean\": 2.83,\n \"stdev\": 0.39,\n \"cv\": 0.137,\n \"min\": 2,\n \"max\": 3\n },\n \"punct\": {\n \"em_dash\": 0.0,\n \"en_dash\": 0.0,\n \"semicolon\": 0.0,\n \"colon\": 0.0,\n \"ellipsis\": 0.0,\n \"exclamation\": 0.0,\n \"counts\": {\n \"em_dash\": 0,\n \"en_dash\": 0,\n \"semicolon\": 0,\n \"colon\": 0,\n \"ellipsis\": 0,\n \"exclamation\": 0\n }\n },\n \"structures\": {\n \"tricolon\": 1,\n \"not_but\": 1,\n \"rhetorical_q\": 0,\n \"parallel_openers\": 0\n },\n \"openers\": {\n \"distinct_ratio\": 0.735\n },\n \"wordlist\": {\n \"hits\": [],\n \"rate\": 0.0\n },\n \"hedges\": {\n \"rate\": 0.0\n },\n \"intensifiers\": {\n \"rate\": 0.0\n },\n \"repetition\": {\n \"too_short\": false,\n \"repeated_phrase_rate\": 3.2,\n \"longest_repeat\": 5,\n \"phrases\": [\n {\n \"text\": \"across all workstreams and teams\",\n \"count\": 3,\n \"sentences\": [\n 5,\n 17,\n 32\n ]\n }\n ]\n },\n \"grammar\": {\n \"participial_tail\": {\n \"count\": 14,\n \"rate\": 22.6,\n \"hits\": [\n {\n \"text\": \", reflecting the late discovery of schema drift in the\",\n \"sentence\": 2\n },\n {\n \"text\": \", ensuring alignment across teams before any record reaches\",\n \"sentence\": 3\n },\n {\n \"text\": \", exceeding the target we set in March\",\n \"sentence\": 4\n },\n {\n \"text\": \", creating a sense of momentum that was missing last quarter\",\n \"sentence\": 5\n },\n {\n \"text\": \", allowing designers to retire eleven legacy patterns\",\n \"sentence\": 6\n },\n {\n \"text\": \", giving on-call staff a clearer signal during incidents\",\n \"sentence\": 9\n },\n {\n \"text\": \", matching the level the platform group had proposed\",\n \"sentence\": 10\n },\n {\n \"text\": \", resolving most of them within the first business day\",\n \"sentence\": 11\n },\n {\n \"text\": \", leaving two senior positions unfilled going into the next\",\n \"sentence\": 14\n },\n {\n \"text\": \", assuming the compensation adjustments are approved\",\n \"sentence\": 16\n }\n ]\n },\n \"container_of\": {\n \"count\": 2,\n \"hits\": [\n {\n \"text\": \"a sense of\",\n \"sentence\": 5\n },\n {\n \"text\": \"The weight of\",\n \"sentence\": 15\n }\n ]\n }\n },\n \"nominalization\": {\n \"count\": 24,\n \"hits\": [\n {\n \"text\": \"validation\",\n \"count\": 2\n },\n {\n \"text\": \"migration\",\n \"count\": 2\n },\n {\n \"text\": \"implementation\",\n \"count\": 1\n },\n {\n \"text\": \"ingestion\",\n \"count\": 1\n },\n {\n \"text\": \"alignment\",\n \"count\": 1\n },\n {\n \"text\": \"adoption\",\n \"count\": 1\n },\n {\n \"text\": \"completion\",\n \"count\": 1\n },\n {\n \"text\": \"optimization\",\n \"count\": 1\n },\n {\n \"text\": \"deflection\",\n \"count\": 1\n },\n {\n \"text\": \"decision\",\n \"count\": 1\n },\n {\n \"text\": \"compensation\",\n \"count\": 1\n },\n {\n \"text\": \"adjustments\",\n \"count\": 1\n },\n {\n \"text\": \"coordination\",\n \"count\": 1\n },\n {\n \"text\": \"escalation\",\n \"count\": 1\n },\n {\n \"text\": \"transformation\",\n \"count\": 1\n }\n ],\n \"of_frames\": [\n {\n \"text\": \"the implementation of\",\n \"sentence\": 2\n },\n {\n \"text\": \"the migration of\",\n \"sentence\": 7\n },\n {\n \"text\": \"the completion of\",\n \"sentence\": 7\n },\n {\n \"text\": \"the optimization of\",\n \"sentence\": 8\n },\n {\n \"text\": \"the escalation of\",\n \"sentence\": 18\n },\n {\n \"text\": \"the transformation of\",\n \"sentence\": 19\n },\n {\n \"text\": \"the remediation of\",\n \"sentence\": 20\n },\n {\n \"text\": \"the integration of\",\n \"sentence\": 26\n }\n ]\n }\n}\n","exit_code":0,"status":"completed"}} + +## Output +Audited `ai_report.txt` as **expository prose**: 620 words, 34 sentences. File unchanged. These are stylistic patterns, not proof of AI authorship. + +| # | Tell | Evidence | Base rate / metric | +|---|------|----------|--------------------| +| 1 | Verbatim repetition | “across all workstreams and teams” (×3); repeated general wording without a clear rhetorical purpose. | Scanner: 3 occurrences of the same five-word phrase. | +| 2 | Uniform sentence length | “Four product surfaces migrated to the new components, allowing designers to retire eleven legacy patterns.” exemplifies the steady sentence rhythm. | Length CV: **0.19**; range: **14–27 words**. Skill heuristic flags below 0.4; technical prose can also fit this pattern. | +| 3 | Uniform paragraph length | The paragraph beginning “On the reliability side” follows the recurring three-sentence update format. | All 12 paragraphs contain **2–3 sentences**; CV **0.137**, below the skill’s 0.3 uniformity threshold. | +| 4 | Frequent trailing participial clauses | “, ensuring alignment across teams before any record reaches”; “, allowing designers to retire eleven legacy patterns” | Scanner: **14 candidates**, **22.6/1,000 words**. Normal in reports, but frequent enough here to make the syntax repetitive. | +| 5 | Disclaimer opener | “It's important to approach this update carefully”; “I'm not able to share vendor pricing here” | `discourse.disclaimer_opener.fired: true`; **2 matches**. The caution is generic; the pricing restriction may serve a real purpose. | +| 6 | Summary closer | “In summary, delivery across all workstreams and teams stayed on plan despite the schema issue” | `discourse.summary_closer: true`. Repeats the opening’s favorable assessment; the subsequent Friday deadline adds useful information. | diff --git a/docs/acceptance/v0.3/cursor.md b/docs/acceptance/v0.3/cursor.md new file mode 100644 index 0000000..bcdf086 --- /dev/null +++ b/docs/acceptance/v0.3/cursor.md @@ -0,0 +1 @@ +Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable. diff --git a/docs/acceptance/v0.3/hermes.md b/docs/acceptance/v0.3/hermes.md new file mode 100644 index 0000000..9dbd7fa --- /dev/null +++ b/docs/acceptance/v0.3/hermes.md @@ -0,0 +1,25 @@ +Traceback (most recent call last): + File "/Users/ccf/.local/bin/hermes", line 10, in + sys.exit(main()) + ^^^^^^ + File "/Users/ccf/.hermes/hermes-agent/hermes_cli/main.py", line 10068, in main + sys.exit(run_oneshot( + ^^^^^^^^^^^^ + File "/Users/ccf/.hermes/hermes-agent/hermes_cli/oneshot.py", line 80, in run_oneshot + response = _run_agent(prompt, model=model, provider=provider) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/ccf/.hermes/hermes-agent/hermes_cli/oneshot.py", line 149, in _run_agent + runtime = resolve_runtime_provider( + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/ccf/.hermes/hermes-agent/hermes_cli/runtime_provider.py", line 1008, in resolve_runtime_provider + creds = resolve_codex_runtime_credentials() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/ccf/.hermes/hermes-agent/hermes_cli/auth.py", line 2376, in resolve_codex_runtime_credentials + tokens = _refresh_codex_auth_tokens(tokens, refresh_timeout_seconds) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/ccf/.hermes/hermes-agent/hermes_cli/auth.py", line 2302, in _refresh_codex_auth_tokens + refreshed = refresh_codex_oauth_pure( + ^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/ccf/.hermes/hermes-agent/hermes_cli/auth.py", line 2257, in refresh_codex_oauth_pure + raise AuthError( +hermes_cli.auth.AuthError: Codex token refresh failed: Could not validate your refresh token. Please try signing in again. diff --git a/tools/smoke_harnesses.sh b/tools/smoke_harnesses.sh new file mode 100755 index 0000000..d7d17e2 --- /dev/null +++ b/tools/smoke_harnesses.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Local acceptance matrix for the humanize skill. Not run by CI or pytest. +# PASS needs evidence the skill AND the scanner ran (spec §5), not row names alone. +set -u +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +FIXTURE="tests/fixtures/ai_report.txt" +OUT="$ROOT/docs/acceptance/v0.3" +REQUEST="Use the humanize skill on $FIXTURE. Audit only — do not rewrite." +mkdir -p "$OUT" + +# Expected rows (case-insensitive regex) and two scanner numbers that must appear verbatim. +ROWS=('trailing participial clause' 'verbatim repetition' 'container-noun phrase' 'safety disclaimer opener') +SCAN="$(python3 "$ROOT/skills/humanize/scripts/surface_scan.py" --text "$ROOT/$FIXTURE")" +NUM_TAILS="$(printf '%s\n' "$SCAN" | sed -n 's/^grammar: participial tails \([0-9]*\).*/\1/p')" +NUM_REP="$(printf '%s\n' "$SCAN" | sed -n 's/^repetition: \([0-9.]*\)\/1k.*/\1/p')" + +judge() { # judge + local h="$1" f="$2" tool_re="$3" ok=1 + for r in "${ROWS[@]}"; do grep -qiE "$r" "$f" || { echo " missing row: $r"; ok=0; }; done + grep -iE '^\|' "$f" | grep -qi 'nominalization' && { echo " nominalization row present"; ok=0; } + grep -qE "participial tails[^0-9]{0,20}${NUM_TAILS}([^0-9]|$)" "$f" || { echo " scanner count $NUM_TAILS absent"; ok=0; } + grep -qF "${NUM_REP}/1k" "$f" || { echo " scanner rate ${NUM_REP}/1k absent"; ok=0; } + [ -n "$tool_re" ] && { grep -qE "$tool_re" "$f" || { echo " no tool event naming surface_scan.py"; ok=0; }; } + grep -qiE 'Choices you may want to reverse|^## Rewrite' "$f" && { echo " rewrite section present"; ok=0; } + [ "$ok" = 1 ] && echo "PASS $h" || echo "FAIL $h" +} + +run_claude() { + command -v claude >/dev/null || { echo "SKIP claude (not installed)"; return; } + local f="$OUT/claude-code.md" + claude plugin disable humanize@humanize >/dev/null 2>&1 || true + (cd "$ROOT" && claude -p "$REQUEST" --plugin-dir . --output-format stream-json --verbose \ + --allowedTools "Bash,Read,Glob,Grep" > "$f.jsonl" 2>&1) + claude plugin enable humanize@humanize >/dev/null 2>&1 || true + python3 - "$f.jsonl" "$f" <<'EOF' +import json, sys +src, dst = sys.argv[1], sys.argv[2] +texts, tools = [], [] +for line in open(src, encoding="utf-8"): + try: ev = json.loads(line) + except ValueError: continue + for blk in (ev.get("message") or {}).get("content", []) if isinstance(ev, dict) else []: + if blk.get("type") == "text": texts.append(blk["text"]) + if blk.get("type") == "tool_use": tools.append(json.dumps(blk.get("input"))[:300]) + if ev.get("type") == "result" and ev.get("result"): texts.append(ev["result"]) +open(dst, "w", encoding="utf-8").write("# Claude Code\n\n## Tool calls\n" + "\n".join(tools) + "\n\n## Output\n" + "\n".join(texts)) +EOF + judge "claude-code" "$f" 'surface_scan\.py' +} + +run_codex() { + command -v codex >/dev/null || { echo "SKIP codex (not installed)"; return; } + local tmp f="$OUT/codex.md"; tmp="$(mktemp -d)" + mkdir -p "$tmp/.agents/skills" && cp -R "$ROOT/skills/humanize" "$tmp/.agents/skills/" && cp "$ROOT/$FIXTURE" "$tmp/" + (cd "$tmp" && codex debug prompt-input "hi" 2>/dev/null | grep -qi humanize) || echo " warning: skill not listed by codex debug prompt-input" + (cd "$tmp" && codex exec -C "$tmp" --skip-git-repo-check --ephemeral -s read-only --json \ + -o "$tmp/out.md" "Use the humanize skill on ai_report.txt. Audit only — do not rewrite." "$tmp/events.jsonl" 2>&1) + { echo "# Codex"; echo; echo "## Tool events"; grep -i 'surface_scan' "$tmp/events.jsonl" | head -5; echo; echo "## Output"; cat "$tmp/out.md" 2>/dev/null; } > "$f" + judge "codex" "$f" 'surface_scan\.py' + rm -rf "$tmp" +} + +run_cursor() { + command -v agent >/dev/null || { echo "SKIP cursor (agent CLI not installed)"; return; } + agent --help 2>/dev/null | grep -qE -- '(^| )-p[ ,]|--print' || { echo "SKIP cursor (agent $(agent --version 2>/dev/null | head -1) has no headless flag)"; return; } + local tmp f="$OUT/cursor.md"; tmp="$(mktemp -d)" + mkdir -p "$tmp/.cursor/skills" && cp -R "$ROOT/skills/humanize" "$tmp/.cursor/skills/" && cp "$ROOT/$FIXTURE" "$tmp/" + # Deviation from brief: the installed `agent` CLI's approvalMode is `allowlist` with + # only Shell(ls) allowed, so a headless run cannot execute the scanner without --force. + (cd "$tmp" && agent -p --force --output-format text "Use the humanize skill on ai_report.txt. Audit only — do not rewrite." > "$f" 2>&1) + judge "cursor" "$f" '' + rm -rf "$tmp" +} + +run_hermes() { + command -v hermes >/dev/null || { echo "SKIP hermes (not installed)"; return; } + local dest="$HOME/.hermes/skills/writing/humanize" f="$OUT/hermes.md" + [ -e "$dest" ] && { echo "SKIP hermes ($dest already exists; not touching it)"; return; } + mkdir -p "$(dirname "$dest")" && cp -R "$ROOT/skills/humanize" "$dest" + if hermes --help 2>/dev/null | grep -q -- ' -z'; then + hermes -z "$REQUEST" > "$f" 2>&1 + else + hermes chat -q "$REQUEST" -Q > "$f" 2>&1 + fi + rm -rf "$dest" + judge "hermes" "$f" '' +} + +HARNESSES=("$@") +[ "${#HARNESSES[@]}" -eq 0 ] && HARNESSES=(claude codex cursor hermes) +for h in "${HARNESSES[@]}"; do "run_$h"; done From b2ff84fe3cc3e963dd79a3ecf1b54f34d1aa4790 Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 11:22:17 -0400 Subject: [PATCH 15/22] Smoke matrix: section-aware judge, Codex event parser, SKIP on auth Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- docs/acceptance/v0.3/README.md | 45 +- docs/acceptance/v0.3/claude-code.md | 919 ++++++++++++++++++++++------ docs/acceptance/v0.3/codex.md | 301 ++++++++- tools/smoke_harnesses.sh | 194 +++++- 4 files changed, 1230 insertions(+), 229 deletions(-) diff --git a/docs/acceptance/v0.3/README.md b/docs/acceptance/v0.3/README.md index a235d56..d1c04ae 100644 --- a/docs/acceptance/v0.3/README.md +++ b/docs/acceptance/v0.3/README.md @@ -15,26 +15,51 @@ skill, audit-only, against `tests/fixtures/ai_report.txt`. The script installs t working-tree copy of `skills/humanize` into each harness the harness's own way (a temp project for Codex and Cursor, `--plugin-dir .` for Claude Code, a copy into `~/.hermes/skills/writing/humanize` for Hermes), sends one non-interactive request, -and captures the raw output. +and captures the output. + +Every transcript has three H2 sections, in this fixed order: + +- `## Tool calls` — the commands or tool inputs the agent issued (for Claude Code and + Codex, only the actual scanner invocation; reads of `SKILL.md` never appear here). +- `## Scanner output` — the raw output of any scanner invocation. +- `## Output` — the agent's own final text only. For Claude Code this excludes the + Skill launch's injected `SKILL.md` dump (it lands in neither section). Where a + harness's headless mode doesn't expose tool-call/tool-result data at all (Cursor's + `--output-format text`, Hermes's oneshot/quiet mode), the first two sections say so + and the full raw response goes under `## Output`. ## PASS rule -A harness is judged PASS only with evidence that both the skill and the scanner ran: +A harness is judged PASS only with evidence that both the skill and the scanner ran. +The judge is section-aware: each check below runs against the section named, not the +whole transcript. -- All four expected audit rows are present (case-insensitive): trailing participial - clause, verbatim repetition, container-noun phrase, safety disclaimer opener. -- No nominalization row (nominalization is reported-only per spec, never a row). +- All four expected audit rows are present in `## Output` (case-insensitive): trailing + participial clause, verbatim repetition, container-noun phrase, safety disclaimer + opener. +- No nominalization row in `## Output` (nominalization is reported-only per spec, + never a row). - The scanner's exact participial-tails count and repetition rate (computed by the script from a live `surface_scan.py` run against the same fixture) appear verbatim - in the transcript. -- Where the harness exposes tool-call events (Claude Code, Codex), a tool event names - `surface_scan.py`. -- No rewrite section (`## Rewrite` or "Choices you may want to reverse") — the request - was audit-only. + in `## Scanner output` or `## Output`. +- Where the harness exposes tool-call events (Claude Code, Codex), `## Tool calls` + has a tool event naming `surface_scan.py`. +- No rewrite section (`## Rewrite` or "Choices you may want to reverse") anywhere in + `## Output` — the request was audit-only. `tools/smoke_harnesses.sh` prints one `PASS `, `FAIL `, or `SKIP (reason)` line per harness, with reason lines above a FAIL. +## SKIP on authentication failure + +If a harness's raw output contains a recognized authentication-failure signature +(`Authentication required`, `AuthError`, `token refresh failed`, or +`Please run 'agent login'`), the script prints +`SKIP (not authenticated: )` instead of running the +judge, and still saves the raw output as that harness's transcript. The script never +runs a login command itself — an auth failure means the user needs to sign that CLI +in before the next run. + ## CLI versions (pinned before running, 2026-09-14) | CLI | Version | diff --git a/docs/acceptance/v0.3/claude-code.md b/docs/acceptance/v0.3/claude-code.md index 0dfee37..a66ec4e 100644 --- a/docs/acceptance/v0.3/claude-code.md +++ b/docs/acceptance/v0.3/claude-code.md @@ -1,201 +1,752 @@ # Claude Code ## Tool calls -{"skill": "humanize:humanize", "args": "tests/fixtures/ai_report.txt --audit-only"} -{"command": "cd /Users/ccf/git/humanize/skills/humanize && cat references/principles.md && echo ===== && cat /Users/ccf/git/humanize/tests/fixtures/ai_report.txt && echo ===== && wc -w /Users/ccf/git/humanize/tests/fixtures/ai_report.txt", "description": "Read principles reference and the report fix -{"command": "cd /Users/ccf/git/humanize/skills/humanize && cat /Users/ccf/git/humanize/tests/fixtures/ai_report.txt && echo '-----WC-----' && wc -w /Users/ccf/git/humanize/tests/fixtures/ai_report.txt && echo '-----SCAN-----' && python3 scripts/surface_scan.py --text /Users/ccf/git/humanize/tests/fi -{"command": "cat references/surface-tells.md && echo '-----STYLE-----' && cat references/style-tells.md", "description": "Read the surface and style tell references"} -{"command": "python3 scripts/surface_scan.py /Users/ccf/git/humanize/tests/fixtures/ai_report.txt | python3 -c '\nimport json,sys\nd=json.load(sys.stdin)\nfor k in (\"discourse\",\"grammar\",\"repetition\",\"sentence_len\",\"paragraph_len\",\"structures\"):\n print(k, json.dumps(d.get(k), indent= +{ + "skill": "humanize:humanize", + "args": "tests/fixtures/ai_report.txt --audit-only" +} +{ + "command": "cd /Users/ccf/git/humanize/skills/humanize && cat references/principles.md && echo ===== && cat /Users/ccf/git/humanize/tests/fixtures/ai_report.txt && echo ===== && wc -w /Users/ccf/git/humanize/tests/fixtures/ai_report.txt", + "description": "Read principles reference and the fixture text" +} +{ + "command": "cd /Users/ccf/git/humanize/skills/humanize && cat /Users/ccf/git/humanize/tests/fixtures/ai_report.txt && printf '\\n----- words: ' && wc -w < /Users/ccf/git/humanize/tests/fixtures/ai_report.txt", + "description": "Read the fixture text and count words" +} +{ + "command": "cat references/surface-tells.md", + "description": "Read surface-tells reference" +} +{ + "command": "cat references/style-tells.md", + "description": "Read style-tells reference" +} +{ + "command": "python3 scripts/surface_scan.py --text /Users/ccf/git/humanize/tests/fixtures/ai_report.txt", + "description": "Run surface scanner on the fixture" +} +{ + "command": "python3 scripts/surface_scan.py /Users/ccf/git/humanize/tests/fixtures/ai_report.txt | python3 -c \"\nimport json,sys; d=json.load(sys.stdin)\nprint(json.dumps({k:d[k] for k in ('discourse','grammar','repetition','structures')}, indent=1)[:4000])\"", + "description": "Pull discourse, grammar, repetition and structure hits from full scan JSON" +} + +## Scanner output +Launching skill: humanize:humanize +Exit code 1 +# Principles + +Read this before every audit or rewrite. + +1. **Disperse, don't converge.** StoryScope's central result: AI writing clusters in + a shared region of stylistic and narrative space; human writing spreads out. + A tell is a *default* the author didn't choose. The fix is a choice, not a + different default. If every rewrite you produce would look alike, you have + built a new cluster. + +2. **Preserve voice and meaning.** Every fact, name, number, claim, and position + survives. Register survives: a terse engineer stays terse, a warm note stays + warm. You are removing tells, not imposing taste. + +3. **Fix only what fired.** The audit names specific tells with quoted evidence. + Rewrite those. Leave everything else exactly as it was, including things you + would have written differently. + +4. **Removals are safer than additions.** Cutting an unearned epilogue, a stacked + metaphor, or a summary paragraph rarely misrepresents the author. Adding a + joke, a brand name, or a flashback can. Reference entries tag each fix as + `removal`, `addition`, or `rebalance`; make additions only when the inferred + voice would plausibly do that, and say so in the report. + +5. **Numbers are evidence, not verdicts.** Base rates come from a fiction corpus. + Scan metrics are counts. A text can be entirely human and still show three + tells; a text can show none and be generated. Report what fired and why a + reader would notice. Never state or imply that the result is undetectable, + passes a detector, or is "certified human." + Check the direction before you flag it. Findings expire — lexical diversity + reversed between GPT-3.5 and GPT-4 (Herbold et al. 2023 [herbold-2023]). + Some never held — GPT-4o used agentless passives at about half the human + rate (Reinhart et al. 2025 [reinhart-2025], 2024-era models), and 29 of 32 + model settings moved away from the dimension that carries passives (Milička + et al. 2025 [milicka-2025]: a factor loading, not a passive count). Reader + heuristics point backwards (Jakesch et al. 2023 [jakesch-2023], GPT-3-era + self-presentation bios): contractions read as human but lean AI; grammar + errors and long or rare words read as AI but lean human. Prefer recency for + capability-dependent features, replication for stable ones — and never + optimize for what a reader guesses is human. + +6. **Ask rarely.** Infer register, audience, and intent from the text and the + conversation. Ask one question only when a rewrite decision genuinely hinges + on it and you cannot tell from context. + +7. **Variance is the tool.** Vary sentence length. Let a plain sentence stay + plain. Name an emotion once instead of embodying it again. Stop at the climax. + Let one paragraph be a single line. Let a reference be specific. Each of these + is a departure from the AI default; none is a new rule. + +8. **Register and proficiency are not tells.** The measured AI profile — formal, + impersonal, nominalized, flat sentence lengths, narrow lexis, few + contractions — also describes competent second-language English, translated + text, legal, technical, academic, and plain-language prose. That overlap is + this repo's inference from the corpora below, not a finding any of them + tests. Measure the profile; never infer authorship or proficiency from it, + and never rewrite a text into looking less like one of those populations. + Each human baseline here comes from one narrow population — StoryScope: + amateur fiction; Muñoz-Ortiz: NYT lead paragraphs; Herbold: non-native + student essays; Jakesch: short bios — whose own limitations decline to + generalize. All of it is English; quote no number on translated or + non-English text. +(eval):1: ==== not found +It's important to approach this update carefully, since several workstreams changed scope during the quarter. I'm not able to share vendor pricing here, but the overall picture is encouraging and the risks are manageable. + +The implementation of the new ingestion pipeline finished two weeks behind the original estimate, reflecting the late discovery of schema drift in the partner feed. The team resolved the drift by introducing a validation layer, ensuring alignment across teams before any record reaches the warehouse. Throughput now sits at roughly nine thousand events per minute, exceeding the target we set in March. + +Adoption of the shared design system continued across all workstreams and teams, creating a sense of momentum that was missing last quarter. Four product surfaces migrated to the new components, allowing designers to retire eleven legacy patterns. The migration of the billing screens remains in progress, pending the completion of an accessibility review. + +On the reliability side, the optimization of our alerting rules reduced paging volume by about a third. Engineers consolidated forty-two overlapping alerts into nine, giving on-call staff a clearer signal during incidents. Mean time to acknowledge fell from eleven minutes to four, matching the level the platform group had proposed. + +Customer support handled a spike in tickets after the pricing change, resolving most of them within the first business day. The knowledge base articles were rewritten in plainer language, and the deflection rate climbed from thirty to forty-one percent. A small group of enterprise accounts asked for a dedicated onboarding call, and account managers have scheduled those for the second week of the month. + +Hiring closed on three of the five open roles, leaving two senior positions unfilled going into the next cycle. The weight of the decision to pause backfills fell mostly on the data platform group. We expect the remaining offers to close by mid-month, assuming the compensation adjustments are approved. + +Coordination across all workstreams and teams improved once the weekly sync moved to a written format. Fewer meetings meant more focused work, and the written record made the escalation of blockers faster to trace. Product managers reported that the transformation of the roadmap into quarterly themes made prioritization discussions shorter. + +Security completed the remediation of the findings from the spring audit, closing every high-severity item ahead of schedule. Two medium items remain open, awaiting a library upgrade that the vendor has scheduled for next month. The compliance team confirmed that the evidence collection process now runs automatically each week. + +Documentation for the public API moved to the new site, giving external developers a searchable reference for the first time. Traffic to the reference pages doubled in the first month, and the volume of questions in the developer forum dropped noticeably. The technical writers also produced a migration guide for teams still on the deprecated endpoints. + +Looking ahead, the integration of the analytics events into the new pipeline is the main dependency for the reporting launch. The team plans to finish the mapping by the end of the month, leaving three weeks for validation with finance. Budget remains within the approved envelope, and no additional headcount is requested at this time. + +Finance reviewed the quarterly forecast with each product area and found no material variance against the plan. The revised allocation model landed in the reporting tool last week, and cost centre owners can now see their own numbers without filing a ticket. Two departments asked for a shorter reporting cycle, and that request is under review. + +In summary, delivery across all workstreams and teams stayed on plan despite the schema issue, and the quarter closes with fewer open risks than it opened with. Please raise any concerns before Friday so they can be folded into the planning session. + +----- words: 620 +# Surface tells + +The layer StoryScope does not measure: vocabulary, grammar, punctuation, +sentence and paragraph shape, and discourse moves. Applies to every text +class. Each entry names the `surface_scan.py` metric that measures it where +one exists. Ranges marked *rule of thumb* are working heuristics from +practice, not measured in the StoryScope corpus. Numbers that are not +StoryScope base rates sit on `Scan:` or `Rule of thumb:` lines with a `[key]` +that resolves in `SOURCES.md`. + +## Vocabulary + +### AI-associated wordlist +Looks like: "delve", "tapestry", "a testament to", "navigate the complexities", +"it's worth noting", "leverage", "robust", "seamless", "crucial", "pivotal", +"foster", "underscore", "multifaceted", "landscape", "vibrant", "nuanced", +"meticulous", "harness", "synergy", "holistic", "streamline", "elevate", +"empower", "unlock", "resonate", "realm", "beacon", "unwavering". +Scan: `wordlist.rate` per 1k words and `wordlist.hits` with sentence positions. +Rule of thumb: human drafts usually < 3/1k; AI drafts commonly 10–30/1k in +long-form prose and can exceed 60/1k in short business emails, where +boilerplate dominates. +Vintage: calibrated on 2023–2024 model output. A wordlist decays — Kobak et +al. 2025 [kobak-2025] show marker words rising and falling with model +generations (share of biomedical abstracts containing the word, not a per-1k +rate; not comparable to the rule of thumb above). Re-check against current +models before firing hard. +Why it reads as AI: these words are over-represented in RLHF-era model output +and under-represented in ordinary human prose of the same register; readers +have learned the list. +Fix: removal — replace with the plain word the author would use ("use" for +"leverage", "strong" for "robust", "important" for "crucial") or cut the word +entirely; most are decorative. + +### Latinate lean +Looks like: "utilize", "facilitate", "demonstrate", "commence", "implement", +"ascertain" where "use", "help", "show", "start", "do", "find out" would do. +Base rate: AI 2.83 / human 2.51 on a 1–5 Anglo-Saxon→Latinate scale +(StoryScope STY_ALL_016). +Why it reads as AI: models default to the formal register of their training +mass; humans pick the short word unless the register demands otherwise. +Fix: rebalance — swap to the short Germanic word where the voice is not formal. + +### Nominalized verbs +Looks like: "the implementation of the policy led to an improvement in +retention" where "implementing the policy improved retention" would do; "the +X of" frames stacked through a paragraph. +Scan: `nominalization.hits` and `nominalization.of_frames` — hits only, no +rate, no threshold; singular and plural forms are separate hits (Herbold et +al. 2023 [herbold-2023]; Reinhart et al. 2025 [reinhart-2025]). +Why it reads as AI: buried verbs rise monotonically across model generations, +but formal, legal, academic, and second-language prose nominalize legitimately +— in `expository` text these hits are a prompt to look, never a table row. +Fix: rebalance — unbury the verb where the register does not earn the noun. + +### Abstract container-noun phrase +Looks like: "a sense of unease", "a mix of pride and fear", "the weight of the +decision" — an abstract container standing in for the concrete thing. +Scan: `grammar.container_of` count and hits; the 13 heads are those attested +in LAMP Table 8 (Chakrabarty et al. 2025 [chakrabarty-2025]), rare in the +human seed paragraphs. +Why it reads as AI: a reflex reach for an abstraction where a human names the +object or the feeling; fiction uses these legitimately, so judge density. +Fix: removal — name the concrete thing, or cut the frame and keep the noun. + +### Hedge stacks +Looks like: "It could perhaps be argued that this might, to some extent, +generally be the case." +Scan: `hedges.rate` per 1k. Rule of thumb: > 10/1k in expository prose is +a stack. +Why it reads as AI: models hedge to avoid being wrong; a person with a view +states it and hedges once, if at all. +Fix: removal — keep at most one hedge per claim; delete the rest. + +### Intensifier stacks +Looks like: "truly remarkable", "deeply meaningful", "incredibly important", +"genuinely transformative", several per paragraph. +Scan: `intensifiers.rate` per 1k. Rule of thumb: > 8/1k reads as padding. +Why it reads as AI: intensifiers substitute for specifics; humans intensify +rarely and usually for effect. +Fix: removal — delete the intensifier or replace the phrase with a concrete +detail that earns the emphasis. + +## Punctuation + +### Em-dash density +Looks like: "The plan—while ambitious—was sound—and it worked." +Scan: `punct.em_dash` per 1k. Rule of thumb: human nonfiction 0–4/1k; AI +drafts often 8–20/1k. Per-1k rates need roughly 300+ words to mean anything; +below that, cite the raw count (`punct.counts.em_dash`) and treat one or two +marks as noise. CLI flags like `--audit-only` are not counted; only `word--word` +or spaced ` -- ` forms count as an em-dash. +Why it reads as AI: models use the em-dash as a universal joiner where a human +would use a comma, a period, or parentheses, and they use it in every paragraph. +Fix: rebalance — keep one em-dash where it does real work; convert the rest to +periods (usually) or commas. + +### Semicolon and colon habits +Looks like: semicolons joining independent clauses in casual prose; colons +introducing a clause that restates the previous one. +Scan: `punct.semicolon`, `punct.colon` per 1k. +Why it reads as AI: semicolons in a text message or casual email are rare for +humans; colon-led restatement is a summarizing tic. +Fix: rebalance — in casual registers, split into two sentences. + +## Structures + +### Tricolon habit (rule of three) +Looks like: "fast, reliable, and secure"; "we build, we ship, we learn"; every +list has exactly three items. +Scan: `structures.tricolon` count. Rule of thumb: more than one per 150 words +is a habit, not a choice. Counts any `A, B, and C` sequence, including clause +joins; treat the number as a prompt to look, not a verdict. +Why it reads as AI: the three-item list is rhythmically satisfying and the model +reaches for it reflexively; humans produce two- and four-item lists as often. +Fix: rebalance — cut one item, add a fourth, or make one item a sentence of its +own. Keep a tricolon only where the rhythm is the point. + +### Not-X-but-Y framing +Looks like: "It's not about the code, it's about the culture." "This isn't a +setback—it's an opportunity." "Not only did we ship, but we learned." +Scan: `structures.not_but` count. +Why it reads as AI: a contrast frame that manufactures insight by negating a +strawman; models use it to sound reflective. +Fix: removal — state Y directly. Delete the negated X unless someone actually +claimed it. + +### Rhetorical question then answer +Looks like: "So what does this mean for teams? It means…" "Why does this matter? +Because…" +Scan: `structures.rhetorical_q` count (outside dialogue). +Why it reads as AI: a transition device that simulates dialogue with the reader; +humans use it sparingly and usually with an edge. +Fix: removal — delete the question; keep the answer as a statement. + +### Parallel sentence openers +Looks like: three or more consecutive sentences beginning with the same word +("We… We… We…", "It… It… It…"). +Scan: `structures.parallel_openers` (runs of ≥3) and `openers.distinct_ratio`. +Rule of thumb: distinct-opener ratio below 0.6 in prose longer than 15 sentences +is monotonous. +Why it reads as AI: anaphora is a deliberate rhetorical figure; unintentional +anaphora is a generation artifact. +Fix: rebalance — vary the openers; combine two of the sentences. + +### Verbatim repetition +Looks like: a phrase of four or more words reappearing intact across the piece +— "across all workstreams and teams" three times in a status report — or a +string lifted from the prompt or title. +Scan: `repetition.phrases` (silent under 150 words); repeated phrases are the +strongest true-source predictor readers miss, OR 1.47 (Jakesch et al. 2023 +[jakesch-2023]); `repetition.repeated_phrase_rate` is reported-only. +Why it reads as AI: recurrence with no rhetorical intent; terminology, names, +and identifiers must repeat — exempt technical and legal prose — and a refrain +in fiction is deliberate. +Fix: removal — keep one instance and vary or cut the rest. + +### Trailing participial clause +Looks like: a finished sentence that keeps going after a comma with an -ing +verb: ", ensuring seamless integration", ", allowing teams to move faster", +", highlighting the importance of". +Scan: `grammar.participial_tail` count, rate, and hits; ratio 5.3×, d = 1.38, +2024-era models, news and academic registers (Reinhart et al. 2025 +[reinhart-2025]). +Why it reads as AI: the tack-on lets a sentence add a consequence without a +new subject, and models reach for it several times a paragraph. +Fix: removal — split into a sentence with its own subject, or drop the clause. + +### Uniform sentence length +Looks like: every sentence 14–20 words; no fragments; no 40-word sentence. +Scan: `sentence_len.cv` (stdev/mean); `sentence_len.pct_over_30` (humans +31.2% vs 17.5–21.0%, 2023 news corpus, direction not magnitude; Muñoz-Ortiz et +al. 2024 [munoz-ortiz-2024]); `sentence_len.longest_flat_run` (reported-only). +A flat profile is also the native shape of plain-language and technical prose +— `tests/fixtures/human_plain.txt` sits in AI territory on every sentence +metric — and is not authorship evidence. +Rule of thumb: published human prose commonly 0.5–0.9; AI drafts often below +0.4. +Why it reads as AI: models regress to the mean sentence; humans write in bursts. +Fix: rebalance — split one long sentence into a short one and a fragment; merge +two mid-length sentences into a long one. Aim for range, not a target. + +### Uniform paragraph length +Looks like: every paragraph three to four sentences; every paragraph opens with +a topic sentence and closes with a mini-conclusion. +Scan: `paragraph_len.cv`. Rule of thumb: below 0.3 across five or more +paragraphs is uniform. +Why it reads as AI: the five-paragraph-essay template applied to everything. +Fix: rebalance — allow a one-sentence paragraph; let one paragraph run long. + +## Discourse moves + +### Validating opener +Looks like: "Great question!" "I'd be happy to help." "Absolutely!" "That's a +really insightful point." +Scan: `wordlist.hits` includes "great question", "i hope this helps". +Why it reads as AI: assistant-style acknowledgement before content; humans +answer. +Fix: removal — start with the content. + +### Restating the prompt +Looks like: the first paragraph paraphrases the question or task before +addressing it. +Scan: none; judge by reading. +Why it reads as AI: models anchor by echoing input; humans assume the reader +remembers what they asked. +Fix: removal — delete the paraphrase. + +### Summary closer +Looks like: a final paragraph opening "In conclusion", "Ultimately", "Overall", +"In short" that restates the opening. +Scan: `discourse.summary_closer` (boolean). +Why it reads as AI: essay-template closure on texts that do not need it; humans +end when they are done. +Fix: removal — cut the paragraph, or end on the last concrete point. + +### Sign-off advice and offers +Looks like: "Remember to…", "Feel free to reach out", "Don't hesitate to…", +"I hope this helps!" +Scan: `wordlist.hits` includes "reach out", "don't hesitate", "i hope this helps". +Why it reads as AI: assistant boilerplate. +Fix: removal — end with the actual last thing you have to say, or a plain sign-off. + +### Safety disclaimer opener and AI self-reference +Looks like: a first paragraph that qualifies before it answers — "It's +important to approach this carefully", "I'm not able to give specific advice, +but", "consult a professional" — or any "As an AI" self-reference. +Scan: `discourse.disclaimer_opener.fired` and `.hits`; per-family range 46% +vs 0.2% of responses (Rudnicka & Juzek 2026 [rudnicka-2026]). +Why it reads as AI: assistant safety framing on a text that asked for none; +the same phrases mid-document are an ordinary discourse observation. +Fix: removal — start with the answer. + +### Headings and bullets in short pieces +Looks like: a 200-word email with three bold headers and two bulleted lists. +Scan: none; judge by reading. +Why it reads as AI: structure imposed regardless of length or medium. +Fix: removal — prose for anything under ~300 words unless the medium expects +lists. +# Style tells + +StoryScope's Style dimension (figurative language, sound, syntax, register, +tone, allusion), filtered to the 20 features with a human-vs-AI gap of at least +15 points (categorical) or 0.30 (1–5 scale). Base rates are measured on 61,575 +stories; see `data/README.md`. These apply to every text class; each entry says +how it shows up outside fiction. + +Entries are ordered by the size of the human-vs-AI gap, largest first. + +### Presence of extended conceit +Looks like: a metaphor that is introduced and then developed across several +sentences or the whole piece — the company as a ship, grief as a house with +rooms, the codebase as a garden — with each paragraph extending it. +Base rate: AI 83% / human 40% (StoryScope STY_FIG_004) +Why it reads as AI: models sustain a governing metaphor because it is a +coherence strategy; most human writers drop a figure after one use. +Fix: removal — keep the first instance if it earns its place; cut every later +callback to the conceit and say the literal thing instead. +Outside fiction: the "journey" or "building blocks" frame that runs through an +entire blog post or team update. + +### Lexical register and consistency +Looks like: a piece that stays in one register throughout — uniformly elevated, +or uniformly neutral-standard — with no slang, no shift to plain talk, no +sudden formal aside. +Base rate: mixed register with code-switching — AI 19% / human 56% (StoryScope STY_ALL_015) +Why it reads as AI: humans slip between registers as mood and audience shift +mid-text; models hold a single register as a consistency default. +Fix: addition — only when in character: let one sentence go colloquial, or let +a plain paragraph be interrupted by a precise technical term. Do not sprinkle +slang mechanically. +Outside fiction: an email that never once says "yeah", "honestly", or "ugh" +from a writer who would. + +### Sound Patterning Prominence +Looks like: alliteration and assonance you notice while reading — "the slow +slide of silt", "a hollow, swallowing hush" — recurring through the piece rather +than once at a deliberate moment. +Base rate: noticeable sound patterning — AI 91% / human 55% (StoryScope STY_TON_006) +Why it reads as AI: models reach for euphony by default, so the ear-pleasing +version of each phrase is the one that survives, while most human prose is +sonically flat. +Fix: removal — keep the one sound effect that is placed for a reason and de-tune +the rest by swapping in the word you would have chosen without the ear in mind. +Outside fiction: a product page where every feature name alliterates, or a +headline picked for sound over accuracy. + +### Conventional vs fresh figurative language +Looks like: every image is a new one — "the grief sat in her like a stone +swallowed sideways" — and no stock phrase appears anywhere in the piece. +Base rate: predominantly fresh and inventive images — AI 65% / human 30% (StoryScope STY_FIG_003) +Why it reads as AI: the direction is counter-intuitive — models are more +inventive than humans, not more clichéd, and the tell is relentless invention +with no ordinary sentence between the images. +Fix: removal — cut most of the fresh images and let plain statement carry the +passage; a stock phrase in the right place is a human signal, not a failure. +Outside fiction: an essay where every paragraph opens with a new and unusual +comparison instead of stating the point. + +### Predominant tonal quality +Looks like: earnestness throughout — nothing is undercut, no line is wry, and +the piece means everything it says at face value. +Base rate: earnest or lyrical tone — AI 71% / human 40% (StoryScope STY_TON_021) +Why it reads as AI: sincerity is the safe default for a model that cannot +predict whether a joke will land. +Fix: removal — take the earnestness off at least one high point: undercut a +grand line, let a character be unimpressed, or state the thing flatly. +Outside fiction: a launch post that is visibly moved by its own product, with no +dry aside anywhere in it. + +### Sentence-structure repertoire +Looks like: balanced series as the default sentence shape — "It changed how we +build, how we ship, how we think" — paragraph after paragraph. +Base rate: frequent parallel or list-like structures — AI 99% / human 70% (StoryScope STY_CPX_012) +Why it reads as AI: this is near-universal in AI text, because parallelism is +the cheapest way to sound composed and models apply it by reflex. +Fix: removal — break the series: cut one limb, make one limb a sentence of its +own, or replace the list with a single specific. See the tricolon habit in +`surface-tells.md` for the surface-level check. +Outside fiction: bullet-shaped sentences in an email — "faster, cheaper, and +easier to maintain" — where one accurate clause would do. + +### Recurrent metaphorical motif +Looks like: one image family threaded through the whole piece — weight, tide, +machinery — so that unrelated paragraphs reach for the same register of +comparison. +Base rate: motif present — AI 96% / human 69% (StoryScope STY_FIG_005) +Why it reads as AI: a recurring motif is a coherence device the model applies +across the whole text, while human writers rarely track their images that far. +Fix: removal — keep the motif's single strongest appearance and rewrite the +others from whatever the local scene or paragraph actually offers. +Outside fiction: a strategy memo where everything is weather, momentum, or +plumbing from the first line to the last. + +### Allusion domain diversity +Looks like: references that stay in the safe canon — myth, history, literature — +and never a named band, film, product, or team. +Base rate: pop-culture or brand-name allusions — AI 13% / human 40% (StoryScope STY_ALL_018) +Why it reads as AI: models avoid naming real commercial and cultural artifacts, +so their references land in a timeless, unplaceable nowhere. +Fix: addition — only when in character: name the actual thing the writer would +name, the specific show or product, instead of "a popular streaming series". +Outside fiction: a post about consumer software that never names a competitor. + +### Use of irony and humor +Looks like: nothing in the piece is meant other than literally — no joke, no +sarcasm, no dry undercut anywhere in it. +Base rate: any discernible humor or irony (inverting the straight-faced value) — +AI 62% / human 88% (StoryScope STY_TON_023) +Why it reads as AI: humor is the highest-risk register for a model, so playing +everything straight is the safe move. +Fix: addition — only when in character: one dry line where the writer would +actually be dry. Do not insert jokes into prose whose author has none. +Outside fiction: an incident report or team update with not one wry remark, from +someone whose speech is full of them. + +### Dominant Tonal Register +Looks like: a narrative voice that lingers, reflects, and finds significance, +rather than one that is reportorial, hardboiled, or analytic. +Base rate: lyrical or meditative register — AI 77% / human 52% (StoryScope STY_TON_001) +Why it reads as AI: the reflective register is the model's comfortable default +and gets applied whatever the material is. +Fix: removal — cut the reflective sentences and let the reported facts stand, +then pick the register the material actually demands and hold it. +Outside fiction: a postmortem that keeps pausing to consider what the outage +meant for the team. + +### Dominant Figurative Device Type +Looks like: comparisons asserted rather than proposed — "the deadline was a +wall" rather than "the deadline felt like a wall" — with metaphor crowding out +simile and plain description. +Base rate: metaphor-dominant — AI 73% / human 50% (StoryScope STY_FIG_002) +Why it reads as AI: metaphor is the more literary of the two moves, and models +take the more literary move. +Fix: removal — convert some metaphors back to similes or to literal statement; +the mix, not the metaphor itself, is what reads human. +Outside fiction: business writing where processes are engines, funnels, and +flywheels instead of being described. + +### Parataxis vs Hypotaxis Preference +Looks like: syntax that never commits — some coordination, some subordination, +evenly mixed, with no run of blunt short clauses and no long embedded sentence. +Base rate: balanced parataxis and hypotaxis — AI 85% / human 64% (StoryScope STY_CPX_004) +Why it reads as AI: the balanced middle is the average of all training prose, +and averaging is what the model does. +Fix: removal — take out the balance: write one passage as a chain of short +coordinated clauses and another as a single long subordinated sentence. +Outside fiction: documentation where every sentence has the same clause count +regardless of how simple or complicated the step is. + +### Primary Function of Allusion +Looks like: a reference used to point at the moral — "like Icarus, we flew too +close" — rather than to color a setting or characterize whoever made it. +Base rate: allusion as theme-signposting or moral commentary — AI 46% / human 26% (StoryScope STY_ALL_004) +Why it reads as AI: models use references to make the point legible, where human +writers more often use them for texture, humor, or offhand analogy. +Fix: removal — delete the signposting reference, or repurpose it so it says +something about the person making it rather than about the theme. +Outside fiction: an essay that closes on a myth or a quotation restating the +argument. + +### Sound patterning devices +Looks like: alliteration specifically, as the salient device whenever the prose +pushes — "a steady, stubborn strain", "policy, practice, and posture". +Base rate: alliteration prominent — AI 98% / human 79% (StoryScope STY_TON_025) +Why it reads as AI: alliteration is the sound effect producible by word choice +alone, so it is the one models overuse. +Fix: removal — replace the alliterating word with the accurate one; if the +phrase survives only because it sounds good, cut the phrase. +Outside fiction: alliterative section headings and slogans in a deck or a +README. + +### Use of Sentence Fragments +Looks like: fragments for emphasis, several to a page. Short. Punchy. Like this. +Base rate: fragments present and stylistically significant — AI 85% / human 67% (StoryScope STY_CPX_003) +Why it reads as AI: the direction is counter-intuitive — AI fragments more than +humans do, because the one-beat emphatic fragment is a learned dramatic device, +not a sign of a loose human hand. +Fix: removal — rejoin most fragments into the sentences they broke off from, and +keep at most one where the break carries real emphasis. +Outside fiction: a social post where every third line is a one-word paragraph. + +### Voice Markedness +Looks like: a voice with flavor but no risk — a little rhythm, a few distinctive +turns, no dialect, no verbal tic, nothing a reader could imitate. +Base rate: moderately marked voice, distinct but not extreme — AI 98% / human 83% (StoryScope STY_TON_005) +Why it reads as AI: almost every AI passage lands in this middle band, while +human writing spreads out into plain report and into genuine idiosyncrasy. +Fix: removal — take the voice off the middle: either strip it back to plain +report, or commit to one real mannerism and carry it consistently. +Outside fiction: internal writing that sounds like a company rather than like a +person. + +### Figurative Device Density +Looks like: a metaphor or simile in most paragraphs, including in sentences that +had a job to do and did not need one. +Base rate: AI 3.66 / human 3.00 on a 1–5 scale (StoryScope STY_FIG_001) +Why it reads as AI: models decorate by default, and figurative density is the +most visible form that decoration takes. +Fix: rebalance — cut roughly a third of the figures, starting with those that +explain something already clear, and keep the ones doing work no literal +sentence could do. +Outside fiction: an explainer where every concept arrives with an analogy +attached to it. + +### Rhythmic markedness of prose +Looks like: cadence you can hear — balanced clauses, repeated openings, +sentences resolving on a stressed beat — sustained across the whole piece. +Base rate: AI 3.61 / human 3.17 on a 1–5 scale (StoryScope STY_TON_024) +Why it reads as AI: the model optimizes each sentence for fluency, and sustained +fluency turns into a metronome. +Fix: rebalance — break the meter: let one sentence end awkwardly, run another +past its natural stop, or drop a clause that exists only for balance. +Outside fiction: a speech-shaped post that reads as though written to be read +aloud when nobody will read it aloud. + +### Allusion density +Looks like: prose that refers to almost nothing outside itself — no cited text, +no dated event, no artifact a reader would recognize. +Base rate: AI 2.26 / human 2.59 on a 1–5 scale (StoryScope STY_ALL_017) +Why it reads as AI: models avoid specific external references because those are +the most checkable thing in a text, while humans reach for them constantly. +Fix: rebalance — add the reference the writer would actually have made, and +confirm it is real before keeping it. +Outside fiction: a memo that cites no paper, no prior project, and no names. + +### Latinate vs Anglo-Saxon lexical flavor +Looks like: "utilize", "facilitate", "commence", "demonstrate" where "use", +"help", "start", and "show" would carry the same sense. +Base rate: AI 2.83 / human 2.51 on a 1–5 scale (StoryScope STY_ALL_016) +Why it reads as AI: the formal register dominates the training mass, so the +learned word wins unless the context pushes hard the other way. +Fix: rebalance — swap in the short Germanic word wherever the register does not +require the long one; `surface-tells.md` carries the paired surface check. +Outside fiction: a chat message written in the register of a policy document. +words 620 · sentences 34 · paragraphs 12 +sentence length: mean 18.24, stdev 3.46, cv 0.19 (min 14, max 27) +paragraph length: mean 2.83 sentences, stdev 0.39, cv 0.137 +per 1k words: em-dash 0.0 (0) · en-dash 0.0 (0) · semicolon 0.0 (0) · colon 0.0 (0) · ellipsis 0.0 (0) · exclamation 0.0 (0) +structures: tricolon 1 · not-but 1 · rhetorical-q 0 · parallel-opener runs 0 · distinct openers 0.735 +wordlist: 0.0/1k — none +hedges 0.0/1k · intensifiers 0.0/1k +summary closer: yes · dialogue paragraphs: 0% +repetition: 3.2/1k · longest repeat 5 · "across all workstreams and teams"×3 +grammar: participial tails 14 (22.6/1k) ", reflecting the late discovery of schema drift in the" · container-of 2 "a sense of" +sentence tail: over-30 0.0% · p90 25 · longest flat run 8 +nominalization hits: 24 (validation×2, migration×2, implementation×1) · frames: "the implementation of", "the migration of" +{ + "discourse": { + "summary_closer": true, + "disclaimer_opener": { + "fired": true, + "hits": [ + { + "text": "it's important to approach", + "sentence": 0 + }, + { + "text": "i'm not able to", + "sentence": 1 + } + ] + } + }, + "grammar": { + "participial_tail": { + "count": 14, + "rate": 22.6, + "hits": [ + { + "text": ", reflecting the late discovery of schema drift in the", + "sentence": 2 + }, + { + "text": ", ensuring alignment across teams before any record reaches", + "sentence": 3 + }, + { + "text": ", exceeding the target we set in March", + "sentence": 4 + }, + { + "text": ", creating a sense of momentum that was missing last quarter", + "sentence": 5 + }, + { + "text": ", allowing designers to retire eleven legacy patterns", + "sentence": 6 + }, + { + "text": ", giving on-call staff a clearer signal during incidents", + "sentence": 9 + }, + { + "text": ", matching the level the platform group had proposed", + "sentence": 10 + }, + { + "text": ", resolving most of them within the first business day", + "sentence": 11 + }, + { + "text": ", leaving two senior positions unfilled going into the next", + "sentence": 14 + }, + { + "text": ", assuming the compensation adjustments are approved", + "sentence": 16 + } + ] + }, + "container_of": { + "count": 2, + "hits": [ + { + "text": "a sense of", + "sentence": 5 + }, + { + "text": "The weight of", + "sentence": 15 + } + ] + } + }, + "repetition": { + "too_short": false, + "repeated_phrase_rate": 3.2, + "longest_repeat": 5, + "phrases": [ + { + "text": "across all workstreams and teams", + "count": 3, + "sentences": [ + 5, + 17, + 32 + ] + } + ] + }, + "structures": { + "tricolon": 1, + "not_but": 1, + "rhetorical_q": 0, + "parallel_openers": 0 + } +} ## Output -I'll invoke the humanize skill in audit-only mode on the report fixture. -Base directory for this skill: /Users/ccf/git/humanize/skills/humanize +Using the humanize skill to audit the fixture without rewriting. +Audit only, so I stop after the table. The text is 620 words, classified `expository` (a quarterly status report). -# Humanize - -Make prose read as natural human writing by finding and removing the tells that -mark it as AI-generated. Grounded in thirteen studies of measured style -differences: AI converges on shared defaults; human writing disperses. - -Read `references/principles.md` first, every time. - -## Two modes - -**Drafting mode** — you are writing the prose yourself. Before emitting, check -it against `references/surface-tells.md` and `references/style-tells.md` (add -`references/narrative-tells.md` for fiction). Fix what you find. Return only -the text. No audit table, no commentary about tells. - -**Audit mode** — the user asks you to humanize existing text, or `/humanize` -was invoked. Follow all six steps below. - -## Invocation (`/humanize` only) - -Applies only when the user typed `/humanize …` (Codex: `$humanize …`). On -auto-invoke there are no arguments: skip this section. Options below also -work as plain words in a request ("audit only", "this is fiction"). - -Arguments: tests/fixtures/ai_report.txt --audit-only - -Treat an empty line, or one that still reads literally as `tests/fixtures/ai_report.txt --audit-only`, as no -arguments. Then resolve the target, in order: -1. Strip any flags (`--audit-only`, `--fiction`, `--prose`) from the arguments. -2. If what remains is a path to an existing file, read that file. -3. Else if anything remains, treat it as the text itself. -4. Else (no arguments, or flags only): use the most recent prose you produced - in this conversation, or the text the user most recently shared. If there is - none, ask what to humanize. - -Flags: -- `--audit-only` — stop after step 3 (the audit table). Do not rewrite. -- `--fiction` / `--prose` — override step 1's classification (`--prose` means - `expository` or `conversational`; pick whichever fits). - -## Audit mode - -### 1. Classify - -Decide the class from the text itself: -- `fiction` — narrative with characters and events -- `expository` — essay, article, documentation, report, post -- `conversational` — email, message, chat reply, note - -Load `references/principles.md`, `references/surface-tells.md`, and -`references/style-tells.md`. Load `references/narrative-tells.md` only for -`fiction`. Load `references/model-fingerprints.md` only if the user names the -generating model or asks which model wrote it. Honor a `--fiction` / `--prose` -override if given. - -In `expository` prose, nominalizations, container nouns, and participial -tails are native register — prompts to look, not tells, unless extreme for -the length. - -### 2. Scan - -If the text is 80 words or longer, run the scanner and keep the output: - -```bash -python3 scripts/surface_scan.py --text -``` - -Run it from this skill's folder (the directory holding this SKILL.md). Write -pasted text to a temp file first. For the full JSON, drop `--text`. Under 80 -words, skip this step; the statistics are noise. The scanner strips code, links, -URLs, and heading markers from Markdown before measuring. - -**Non-text sources** (`.docx`, `.pdf`, `.pptx`, `.odt`, `.rtf`): the scanner reads -plain text only. Extract first with the harness's document skills (Anthropic's -`docx`/`pdf` skills ship with Claude Code's `document-skills` plugin and with -Claude Desktop) to write a temp `.md`, then scan and audit that. In claude.ai the -file is an attachment: convert it in the sandbox (pandoc, pdfplumber). Without -document skills, read the file yourself if you can and write the text to a temp -file; otherwise ask for a text export. Deliver Markdown; a Word file via `docx`. - -### 3. Audit - -Walk every loaded tell list. For each tell you judge present, record: - -- the tell name -- one quoted example from the text (add `(×N)` if it recurs) -- the base rate line from the reference, or the scan number - -Rank by strength of evidence. Report **at most ten**. For texts under ~300 -words, quote raw counts from `punct.counts`, not per-1k rates. Quote -`repetition.phrases` and `grammar.*.hits` verbatim. `nominalization.hits` -never become a row. Other studies' ratios never go in the base-rate column. -Format: - -``` | # | Tell | Evidence | Base rate / metric | |---|------|----------|--------------------| -| 1 | Emotion via embodied sensation | "her chest tightened" (×6) | AI 81% / human 39% | -| 2 | Em-dash density | 14.1 per 1k words | rule of thumb: human 0–4 | -``` - -If `--audit-only`, stop here. - -### 4. Infer voice +| 1 | Safety disclaimer opener | "It's important to approach this update carefully", "I'm not able to share vendor pricing here, but" (×2, sentences 1–2) | 46% vs 0.2% of responses, per-family range [rudnicka-2026] | +| 2 | Trailing participial clause | ", ensuring alignment across teams before any record reaches" (×14 in 34 sentences) | scan: 22.6/1k; ratio 5.3×, d = 1.38 [reinhart-2025] | +| 3 | Verbatim repetition | "across all workstreams and teams" (×3, sentences 6, 18, 33) | scan: longest repeat 5 words; OR 1.47 [jakesch-2023] | +| 4 | Uniform sentence length | every sentence 14–27 words; longest flat run 8; no sentence over 30 | scan: cv 0.19, over-30 0.0%; rule of thumb human 0.5–0.9; human over-30 31.2% vs AI 17.5–21.0% [munoz-ortiz-2024] | +| 5 | Uniform paragraph length | 12 paragraphs, all 2–3 sentences | scan: paragraph cv 0.137; rule of thumb below 0.3 is uniform | +| 6 | Summary closer | "In summary, delivery across all workstreams and teams stayed on plan despite the schema issue" | scan: summary_closer = yes | +| 7 | Abstract container-noun phrase | "creating a sense of momentum", "The weight of the decision to pause backfills" (×2) | scan: container_of 2; heads from LAMP Table 8 [chakrabarty-2025] | -State in one line the register, audience, and intent you will preserve, e.g. -"Direct, peer-to-peer, mildly informal; telling a manager the date will slip." -Ask the user one question only if a rewrite decision hinges on something you -cannot infer (whether the piece is meant to be funny; whether a real name may -be used). Otherwise do not ask. +Notes on what did not become a row: -### 5. Rewrite +- **Nominalizations** fired 24 hits with stacked frames such as "the implementation of", "the migration of", "the optimization of", "the remediation of", "the transformation of". In expository prose these are a prompt to look, not a row. Here they are dense enough that a rewrite would unbury several verbs. +- **Wordlist, hedges, intensifiers, em-dashes, semicolons** all scanned at zero. The one tricolon and the one not-but structure are within normal range for this length. +- **Style tells** mostly did not fire in a way a status report would not earn. The uniformly earnest, neutral register is native to the genre, so I did not count it as evidence. -In priority order: - -1. Preserve every fact, claim, name, number, and the author's position. -2. Fix only the tells that fired. Leave everything else as written. -3. Introduce variance, not a new default: vary sentence and paragraph length; - let a plain sentence stay plain; name an emotion once instead of embodying - it again; stop at the climax; allow a specific real-world reference where - the author plausibly would. -4. Do not strip passives by reflex: GPT-4o (2024-era) used the agentless - passive at about half the human rate (Reinhart et al. 2025). Recast one - only when the inferred voice or a fired tell calls for it. -5. Make `addition`-tagged fixes only when the inferred voice would plausibly do - that, and list them under "Choices you may want to reverse". -6. Match the inferred voice. Terse stays terse. - -### 6. Verify - -Re-run the scanner on the rewrite. Show a before/after line for each metric -that changed materially. Verify by the scan and quoted spans, not by whether -it reads human to you. If the rewrite removed every long sentence -(`sentence_len.max` fell hard) or flattened the burstiness (`cv` fell), say so -and reread: converging is a failure even as tell counts fall. Confirm no fact -was dropped by re-reading both. Never describe the result as undetectable, as -passing a detector, or as certified human. It is better writing; say that. - -## Output shape (audit mode) - -1. Audit table (≤10 rows) -2. One-line voice statement -3. The rewrite -4. Before/after metrics (only those that changed) -5. "Choices you may want to reverse" (only if any `addition` fixes were made) - -**Class:** expository (a quarterly status report, 620 words, 34 sentences, 12 paragraphs). +Strongest signals are the assistant-style disclaimer opening, the participial tails in nearly half the sentences, and the flat sentence and paragraph rhythm. No rewrite was made. +Audit only, so I stop after the table. The text is 620 words, classified `expository` (a quarterly status report). | # | Tell | Evidence | Base rate / metric | |---|------|----------|--------------------| -| 1 | Trailing participial clause | ", ensuring alignment across teams before any record reaches" (×14, plus ", reflecting the late discovery of schema drift in the", ", creating a sense of momentum that was missing last quarter", ", allowing designers to retire eleven legacy patterns") | 22.6/1k; ratio 5.3×, d = 1.38, 2024-era models [reinhart-2025] | -| 2 | Safety disclaimer opener | "It's important to approach this update carefully" and "I'm not able to share vendor pricing here, but" (sentences 1 and 2) | `disclaimer_opener` fired; 46% vs 0.2% of responses per family [rudnicka-2026] | -| 3 | Verbatim repetition | "across all workstreams and teams" (×3, sentences 6, 18, 33) | repeated phrases OR 1.47, the strongest predictor readers miss [jakesch-2023] | -| 4 | Uniform sentence length | every sentence 14–27 words; longest flat run 8 | cv 0.19; 0.0% over 30 words (humans 31.2% vs 17.5–21.0% [munoz-ortiz-2024]); rule of thumb human 0.5–0.9, AI often below 0.4 | -| 5 | Uniform paragraph length | 12 paragraphs, every one 2–3 sentences, each opening on a topic and closing on a status line | paragraph cv 0.137; rule of thumb below 0.3 across five or more paragraphs is uniform | -| 6 | Summary closer | "In summary, delivery across all workstreams and teams stayed on plan despite the schema issue" | `summary_closer` yes | -| 7 | Abstract container-noun phrase | "creating a sense of momentum", "The weight of the decision to pause backfills fell mostly on" (×2) | `container_of` 2; heads attested in LAMP Table 8, rare in human seed paragraphs [chakrabarty-2025] | -| 8 | Voice markedness | a company voice with no person in it: "the overall picture is encouraging and the risks are manageable", "Please raise any concerns before Friday so they can be folded into the planning session" | moderately marked voice: AI 98% / human 83% | -| 9 | Use of irony and humor | nothing in 620 words is meant other than literally, including the backfill pause and the two-week slip | any discernible humor or irony: AI 62% / human 88% | - -Notes outside the table, per the reference rules: +| 1 | Safety disclaimer opener | "It's important to approach this update carefully", "I'm not able to share vendor pricing here, but" (×2, sentences 1–2) | 46% vs 0.2% of responses, per-family range [rudnicka-2026] | +| 2 | Trailing participial clause | ", ensuring alignment across teams before any record reaches" (×14 in 34 sentences) | scan: 22.6/1k; ratio 5.3×, d = 1.38 [reinhart-2025] | +| 3 | Verbatim repetition | "across all workstreams and teams" (×3, sentences 6, 18, 33) | scan: longest repeat 5 words; OR 1.47 [jakesch-2023] | +| 4 | Uniform sentence length | every sentence 14–27 words; longest flat run 8; no sentence over 30 | scan: cv 0.19, over-30 0.0%; rule of thumb human 0.5–0.9; human over-30 31.2% vs AI 17.5–21.0% [munoz-ortiz-2024] | +| 5 | Uniform paragraph length | 12 paragraphs, all 2–3 sentences | scan: paragraph cv 0.137; rule of thumb below 0.3 is uniform | +| 6 | Summary closer | "In summary, delivery across all workstreams and teams stayed on plan despite the schema issue" | scan: summary_closer = yes | +| 7 | Abstract container-noun phrase | "creating a sense of momentum", "The weight of the decision to pause backfills" (×2) | scan: container_of 2; heads from LAMP Table 8 [chakrabarty-2025] | -- **Nominalizations are a prompt, not a row** in expository text. The scanner counted 24 hits with "the X of" frames stacked through nearly every paragraph: "the implementation of", "the migration of", "the optimization of", "the remediation of", "the transformation of", "the escalation of", "the integration of". That is heavy even for report register and would be worth unburying in a rewrite. -- **Clean on the rest.** No wordlist hits, no hedges or intensifiers, zero em-dashes, semicolons, or colons, one tricolon, no rhetorical questions, no parallel-opener runs. The lone not-but match is the disclaimer in sentence 2, already covered by row 2. -- **Rows 8 and 9 are weakest.** Both are fiction-corpus rates, and a status report plausibly has no humor in it. Those fixes are additions and would only be made if the author's voice would do that. +Notes on what did not become a row: -Stopped after the audit table as requested. No rewrite was made. -**Class:** expository (a quarterly status report, 620 words, 34 sentences, 12 paragraphs). +- **Nominalizations** fired 24 hits with stacked frames such as "the implementation of", "the migration of", "the optimization of", "the remediation of", "the transformation of". In expository prose these are a prompt to look, not a row. Here they are dense enough that a rewrite would unbury several verbs. +- **Wordlist, hedges, intensifiers, em-dashes, semicolons** all scanned at zero. The one tricolon and the one not-but structure are within normal range for this length. +- **Style tells** mostly did not fire in a way a status report would not earn. The uniformly earnest, neutral register is native to the genre, so I did not count it as evidence. -| # | Tell | Evidence | Base rate / metric | -|---|------|----------|--------------------| -| 1 | Trailing participial clause | ", ensuring alignment across teams before any record reaches" (×14, plus ", reflecting the late discovery of schema drift in the", ", creating a sense of momentum that was missing last quarter", ", allowing designers to retire eleven legacy patterns") | 22.6/1k; ratio 5.3×, d = 1.38, 2024-era models [reinhart-2025] | -| 2 | Safety disclaimer opener | "It's important to approach this update carefully" and "I'm not able to share vendor pricing here, but" (sentences 1 and 2) | `disclaimer_opener` fired; 46% vs 0.2% of responses per family [rudnicka-2026] | -| 3 | Verbatim repetition | "across all workstreams and teams" (×3, sentences 6, 18, 33) | repeated phrases OR 1.47, the strongest predictor readers miss [jakesch-2023] | -| 4 | Uniform sentence length | every sentence 14–27 words; longest flat run 8 | cv 0.19; 0.0% over 30 words (humans 31.2% vs 17.5–21.0% [munoz-ortiz-2024]); rule of thumb human 0.5–0.9, AI often below 0.4 | -| 5 | Uniform paragraph length | 12 paragraphs, every one 2–3 sentences, each opening on a topic and closing on a status line | paragraph cv 0.137; rule of thumb below 0.3 across five or more paragraphs is uniform | -| 6 | Summary closer | "In summary, delivery across all workstreams and teams stayed on plan despite the schema issue" | `summary_closer` yes | -| 7 | Abstract container-noun phrase | "creating a sense of momentum", "The weight of the decision to pause backfills fell mostly on" (×2) | `container_of` 2; heads attested in LAMP Table 8, rare in human seed paragraphs [chakrabarty-2025] | -| 8 | Voice markedness | a company voice with no person in it: "the overall picture is encouraging and the risks are manageable", "Please raise any concerns before Friday so they can be folded into the planning session" | moderately marked voice: AI 98% / human 83% | -| 9 | Use of irony and humor | nothing in 620 words is meant other than literally, including the backfill pause and the two-week slip | any discernible humor or irony: AI 62% / human 88% | - -Notes outside the table, per the reference rules: - -- **Nominalizations are a prompt, not a row** in expository text. The scanner counted 24 hits with "the X of" frames stacked through nearly every paragraph: "the implementation of", "the migration of", "the optimization of", "the remediation of", "the transformation of", "the escalation of", "the integration of". That is heavy even for report register and would be worth unburying in a rewrite. -- **Clean on the rest.** No wordlist hits, no hedges or intensifiers, zero em-dashes, semicolons, or colons, one tricolon, no rhetorical questions, no parallel-opener runs. The lone not-but match is the disclaimer in sentence 2, already covered by row 2. -- **Rows 8 and 9 are weakest.** Both are fiction-corpus rates, and a status report plausibly has no humor in it. Those fixes are additions and would only be made if the author's voice would do that. - -Stopped after the audit table as requested. No rewrite was made. +Strongest signals are the assistant-style disclaimer opening, the participial tails in nearly half the sentences, and the flat sentence and paragraph rhythm. No rewrite was made. diff --git a/docs/acceptance/v0.3/codex.md b/docs/acceptance/v0.3/codex.md index ff3f91c..abb34a6 100644 --- a/docs/acceptance/v0.3/codex.md +++ b/docs/acceptance/v0.3/codex.md @@ -1,19 +1,296 @@ # Codex -## Tool events -{"type":"item.completed","item":{"id":"item_2","type":"command_execution","command":"/bin/zsh -lc 'cat .agents/skills/humanize/SKILL.md'","aggregated_output":"---\nname: humanize\ndescription: Use when drafting or editing any prose — email, essay, documentation, blog post, story, chat reply — or when asked to \"humanize\" text, make it \"sound less like AI\", \"more natural\", \"less robotic\", or remove AI tells. Also use when reviewing prose someone else wrote. Not for code, config, or commit messages.\n---\n\n# Humanize\n\nMake prose read as natural human writing by finding and removing the tells that\nmark it as AI-generated. Grounded in thirteen studies of measured style\ndifferences: AI converges on shared defaults; human writing disperses.\n\nRead `references/principles.md` first, every time.\n\n## Two modes\n\n**Drafting mode** — you are writing the prose yourself. Before emitting, check\nit against `references/surface-tells.md` and `references/style-tells.md` (add\n`references/narrative-tells.md` for fiction). Fix what you find. Return only\nthe text. No audit table, no commentary about tells.\n\n**Audit mode** — the user asks you to humanize existing text, or `/humanize`\nwas invoked. Follow all six steps below.\n\n## Invocation (`/humanize` only)\n\nApplies only when the user typed `/humanize …` (Codex: `$humanize …`). On\nauto-invoke there are no arguments: skip this section. Options below also\nwork as plain words in a request (\"audit only\", \"this is fiction\").\n\nArguments: $ARGUMENTS\n\nTreat an empty line, or one that still reads literally as `$ARGUMENTS`, as no\narguments. Then resolve the target, in order:\n1. Strip any flags (`--audit-only`, `--fiction`, `--prose`) from the arguments.\n2. If what remains is a path to an existing file, read that file.\n3. Else if anything remains, treat it as the text itself.\n4. Else (no arguments, or flags only): use the most recent prose you produced\n in this conversation, or the text the user most recently shared. If there is\n none, ask what to humanize.\n\nFlags:\n- `--audit-only` — stop after step 3 (the audit table). Do not rewrite.\n- `--fiction` / `--prose` — override step 1's classification (`--prose` means\n `expository` or `conversational`; pick whichever fits).\n\n## Audit mode\n\n### 1. Classify\n\nDecide the class from the text itself:\n- `fiction` — narrative with characters and events\n- `expository` — essay, article, documentation, report, post\n- `conversational` — email, message, chat reply, note\n\nLoad `references/principles.md`, `references/surface-tells.md`, and\n`references/style-tells.md`. Load `references/narrative-tells.md` only for\n`fiction`. Load `references/model-fingerprints.md` only if the user names the\ngenerating model or asks which model wrote it. Honor a `--fiction` / `--prose`\noverride if given.\n\nIn `expository` prose, nominalizations, container nouns, and participial\ntails are native register — prompts to look, not tells, unless extreme for\nthe length.\n\n### 2. Scan\n\nIf the text is 80 words or longer, run the scanner and keep the output:\n\n```bash\npython3 scripts/surface_scan.py --text \n```\n\nRun it from this skill's folder (the directory holding this SKILL.md). Write\npasted text to a temp file first. For the full JSON, drop `--text`. Under 80\nwords, skip this step; the statistics are noise. The scanner strips code, links,\nURLs, and heading markers from Markdown before measuring.\n\n**Non-text sources** (`.docx`, `.pdf`, `.pptx`, `.odt`, `.rtf`): the scanner reads\nplain text only. Extract first with the harness's document skills (Anthropic's\n`docx`/`pdf` skills ship with Claude Code's `document-skills` plugin and with\nClaude Desktop) to write a temp `.md`, then scan and audit that. In claude.ai the\nfile is an attachment: convert it in the sandbox (pandoc, pdfplumber). Without\ndocument skills, read the file yourself if you can and write the text to a temp\nfile; otherwise ask for a text export. Deliver Markdown; a Word file via `docx`.\n\n### 3. Audit\n\nWalk every loaded tell list. For each tell you judge present, record:\n\n- the tell name\n- one quoted example from the text (add `(×N)` if it recurs)\n- the base rate line from the reference, or the scan number\n\nRank by strength of evidence. Report **at most ten**. For texts under ~300\nwords, quote raw counts from `punct.counts`, not per-1k rates. Quote\n`repetition.phrases` and `grammar.*.hits` verbatim. `nominalization.hits`\nnever become a row. Other studies' ratios never go in the base-rate column.\nFormat:\n\n```\n| # | Tell | Evidence | Base rate / metric |\n|---|------|----------|--------------------|\n| 1 | Emotion via embodied sensation | \"her chest tightened\" (×6) | AI 81% / human 39% |\n| 2 | Em-dash density | 14.1 per 1k words | rule of thumb: human 0–4 |\n```\n\nIf `--audit-only`, stop here.\n\n### 4. Infer voice\n\nState in one line the register, audience, and intent you will preserve, e.g.\n\"Direct, peer-to-peer, mildly informal; telling a manager the date will slip.\"\nAsk the user one question only if a rewrite decision hinges on something you\ncannot infer (whether the piece is meant to be funny; whether a real name may\nbe used). Otherwise do not ask.\n\n### 5. Rewrite\n\nIn priority order:\n\n1. Preserve every fact, claim, name, number, and the author's position.\n2. Fix only the tells that fired. Leave everything else as written.\n3. Introduce variance, not a new default: vary sentence and paragraph length;\n let a plain sentence stay plain; name an emotion once instead of embodying\n it again; stop at the climax; allow a specific real-world reference where\n the author plausibly would.\n4. Do not strip passives by reflex: GPT-4o (2024-era) used the agentless\n passive at about half the human rate (Reinhart et al. 2025). Recast one\n only when the inferred voice or a fired tell calls for it.\n5. Make `addition`-tagged fixes only when the inferred voice would plausibly do\n that, and list them under \"Choices you may want to reverse\".\n6. Match the inferred voice. Terse stays terse.\n\n### 6. Verify\n\nRe-run the scanner on the rewrite. Show a before/after line for each metric\nthat changed materially. Verify by the scan and quoted spans, not by whether\nit reads human to you. If the rewrite removed every long sentence\n(`sentence_len.max` fell hard) or flattened the burstiness (`cv` fell), say so\nand reread: converging is a failure even as tell counts fall. Confirm no fact\nwas dropped by re-reading both. Never describe the result as undetectable, as\npassing a detector, or as certified human. It is better writing; say that.\n\n## Output shape (audit mode)\n\n1. Audit table (≤10 rows)\n2. One-line voice statement\n3. The rewrite\n4. Before/after metrics (only those that changed)\n5. \"Choices you may want to reverse\" (only if any `addition` fixes were made)\n","exit_code":0,"status":"completed"}} -{"type":"item.completed","item":{"id":"item_6","type":"command_execution","command":"/bin/zsh -lc 'cat .agents/skills/humanize/references/surface-tells.md .agents/skills/humanize/references/style-tells.md'","aggregated_output":"# Surface tells\n\nThe layer StoryScope does not measure: vocabulary, grammar, punctuation,\nsentence and paragraph shape, and discourse moves. Applies to every text\nclass. Each entry names the `surface_scan.py` metric that measures it where\none exists. Ranges marked *rule of thumb* are working heuristics from\npractice, not measured in the StoryScope corpus. Numbers that are not\nStoryScope base rates sit on `Scan:` or `Rule of thumb:` lines with a `[key]`\nthat resolves in `SOURCES.md`.\n\n## Vocabulary\n\n### AI-associated wordlist\nLooks like: \"delve\", \"tapestry\", \"a testament to\", \"navigate the complexities\",\n\"it's worth noting\", \"leverage\", \"robust\", \"seamless\", \"crucial\", \"pivotal\",\n\"foster\", \"underscore\", \"multifaceted\", \"landscape\", \"vibrant\", \"nuanced\",\n\"meticulous\", \"harness\", \"synergy\", \"holistic\", \"streamline\", \"elevate\",\n\"empower\", \"unlock\", \"resonate\", \"realm\", \"beacon\", \"unwavering\".\nScan: `wordlist.rate` per 1k words and `wordlist.hits` with sentence positions.\nRule of thumb: human drafts usually < 3/1k; AI drafts commonly 10–30/1k in\nlong-form prose and can exceed 60/1k in short business emails, where\nboilerplate dominates.\nVintage: calibrated on 2023–2024 model output. A wordlist decays — Kobak et\nal. 2025 [kobak-2025] show marker words rising and falling with model\ngenerations (share of biomedical abstracts containing the word, not a per-1k\nrate; not comparable to the rule of thumb above). Re-check against current\nmodels before firing hard.\nWhy it reads as AI: these words are over-represented in RLHF-era model output\nand under-represented in ordinary human prose of the same register; readers\nhave learned the list.\nFix: removal — replace with the plain word the author would use (\"use\" for\n\"leverage\", \"strong\" for \"robust\", \"important\" for \"crucial\") or cut the word\nentirely; most are decorative.\n\n### Latinate lean\nLooks like: \"utilize\", \"facilitate\", \"demonstrate\", \"commence\", \"implement\",\n\"ascertain\" where \"use\", \"help\", \"show\", \"start\", \"do\", \"find out\" would do.\nBase rate: AI 2.83 / human 2.51 on a 1–5 Anglo-Saxon→Latinate scale\n(StoryScope STY_ALL_016).\nWhy it reads as AI: models default to the formal register of their training\nmass; humans pick the short word unless the register demands otherwise.\nFix: rebalance — swap to the short Germanic word where the voice is not formal.\n\n### Nominalized verbs\nLooks like: \"the implementation of the policy led to an improvement in\nretention\" where \"implementing the policy improved retention\" would do; \"the\nX of\" frames stacked through a paragraph.\nScan: `nominalization.hits` and `nominalization.of_frames` — hits only, no\nrate, no threshold; singular and plural forms are separate hits (Herbold et\nal. 2023 [herbold-2023]; Reinhart et al. 2025 [reinhart-2025]).\nWhy it reads as AI: buried verbs rise monotonically across model generations,\nbut formal, legal, academic, and second-language prose nominalize legitimately\n— in `expository` text these hits are a prompt to look, never a table row.\nFix: rebalance — unbury the verb where the register does not earn the noun.\n\n### Abstract container-noun phrase\nLooks like: \"a sense of unease\", \"a mix of pride and fear\", \"the weight of the\ndecision\" — an abstract container standing in for the concrete thing.\nScan: `grammar.container_of` count and hits; the 13 heads are those attested\nin LAMP Table 8 (Chakrabarty et al. 2025 [chakrabarty-2025]), rare in the\nhuman seed paragraphs.\nWhy it reads as AI: a reflex reach for an abstraction where a human names the\nobject or the feeling; fiction uses these legitimately, so judge density.\nFix: removal — name the concrete thing, or cut the frame and keep the noun.\n\n### Hedge stacks\nLooks like: \"It could perhaps be argued that this might, to some extent,\ngenerally be the case.\"\nScan: `hedges.rate` per 1k. Rule of thumb: > 10/1k in expository prose is\na stack.\nWhy it reads as AI: models hedge to avoid being wrong; a person with a view\nstates it and hedges once, if at all.\nFix: removal — keep at most one hedge per claim; delete the rest.\n\n### Intensifier stacks\nLooks like: \"truly remarkable\", \"deeply meaningful\", \"incredibly important\",\n\"genuinely transformative\", several per paragraph.\nScan: `intensifiers.rate` per 1k. Rule of thumb: > 8/1k reads as padding.\nWhy it reads as AI: intensifiers substitute for specifics; humans intensify\nrarely and usually for effect.\nFix: removal — delete the intensifier or replace the phrase with a concrete\ndetail that earns the emphasis.\n\n## Punctuation\n\n### Em-dash density\nLooks like: \"The plan—while ambitious—was sound—and it worked.\"\nScan: `punct.em_dash` per 1k. Rule of thumb: human nonfiction 0–4/1k; AI\ndrafts often 8–20/1k. Per-1k rates need roughly 300+ words to mean anything;\nbelow that, cite the raw count (`punct.counts.em_dash`) and treat one or two\nmarks as noise. CLI flags like `--audit-only` are not counted; only `word--word`\nor spaced ` -- ` forms count as an em-dash.\nWhy it reads as AI: models use the em-dash as a universal joiner where a human\nwould use a comma, a period, or parentheses, and they use it in every paragraph.\nFix: rebalance — keep one em-dash where it does real work; convert the rest to\nperiods (usually) or commas.\n\n### Semicolon and colon habits\nLooks like: semicolons joining independent clauses in casual prose; colons\nintroducing a clause that restates the previous one.\nScan: `punct.semicolon`, `punct.colon` per 1k.\nWhy it reads as AI: semicolons in a text message or casual email are rare for\nhumans; colon-led restatement is a summarizing tic.\nFix: rebalance — in casual registers, split into two sentences.\n\n## Structures\n\n### Tricolon habit (rule of three)\nLooks like: \"fast, reliable, and secure\"; \"we build, we ship, we learn\"; every\nlist has exactly three items.\nScan: `structures.tricolon` count. Rule of thumb: more than one per 150 words\nis a habit, not a choice. Counts any `A, B, and C` sequence, including clause\njoins; treat the number as a prompt to look, not a verdict.\nWhy it reads as AI: the three-item list is rhythmically satisfying and the model\nreaches for it reflexively; humans produce two- and four-item lists as often.\nFix: rebalance — cut one item, add a fourth, or make one item a sentence of its\nown. Keep a tricolon only where the rhythm is the point.\n\n### Not-X-but-Y framing\nLooks like: \"It's not about the code, it's about the culture.\" \"This isn't a\nsetback—it's an opportunity.\" \"Not only did we ship, but we learned.\"\nScan: `structures.not_but` count.\nWhy it reads as AI: a contrast frame that manufactures insight by negating a\nstrawman; models use it to sound reflective.\nFix: removal — state Y directly. Delete the negated X unless someone actually\nclaimed it.\n\n### Rhetorical question then answer\nLooks like: \"So what does this mean for teams? It means…\" \"Why does this matter?\nBecause…\"\nScan: `structures.rhetorical_q` count (outside dialogue).\nWhy it reads as AI: a transition device that simulates dialogue with the reader;\nhumans use it sparingly and usually with an edge.\nFix: removal — delete the question; keep the answer as a statement.\n\n### Parallel sentence openers\nLooks like: three or more consecutive sentences beginning with the same word\n(\"We… We… We…\", \"It… It… It…\").\nScan: `structures.parallel_openers` (runs of ≥3) and `openers.distinct_ratio`.\nRule of thumb: distinct-opener ratio below 0.6 in prose longer than 15 sentences\nis monotonous.\nWhy it reads as AI: anaphora is a deliberate rhetorical figure; unintentional\nanaphora is a generation artifact.\nFix: rebalance — vary the openers; combine two of the sentences.\n\n### Verbatim repetition\nLooks like: a phrase of four or more words reappearing intact across the piece\n— \"across all workstreams and teams\" three times in a status report — or a\nstring lifted from the prompt or title.\nScan: `repetition.phrases` (silent under 150 words); repeated phrases are the\nstrongest true-source predictor readers miss, OR 1.47 (Jakesch et al. 2023\n[jakesch-2023]); `repetition.repeated_phrase_rate` is reported-only.\nWhy it reads as AI: recurrence with no rhetorical intent; terminology, names,\nand identifiers must repeat — exempt technical and legal prose — and a refrain\nin fiction is deliberate.\nFix: removal — keep one instance and vary or cut the rest.\n\n### Trailing participial clause\nLooks like: a finished sentence that keeps going after a comma with an -ing\nverb: \", ensuring seamless integration\", \", allowing teams to move faster\",\n\", highlighting the importance of\".\nScan: `grammar.participial_tail` count, rate, and hits; ratio 5.3×, d = 1.38,\n2024-era models, news and academic registers (Reinhart et al. 2025\n[reinhart-2025]).\nWhy it reads as AI: the tack-on lets a sentence add a consequence without a\nnew subject, and models reach for it several times a paragraph.\nFix: removal — split into a sentence with its own subject, or drop the clause.\n\n### Uniform sentence length\nLooks like: every sentence 14–20 words; no fragments; no 40-word sentence.\nScan: `sentence_len.cv` (stdev/mean); `sentence_len.pct_over_30` (humans\n31.2% vs 17.5–21.0%, 2023 news corpus, direction not magnitude; Muñoz-Ortiz et\nal. 2024 [munoz-ortiz-2024]); `sentence_len.longest_flat_run` (reported-only).\nA flat profile is also the native shape of plain-language and technical prose\n— `tests/fixtures/human_plain.txt` sits in AI territory on every sentence\nmetric — and is not authorship evidence.\nRule of thumb: published human prose commonly 0.5–0.9; AI drafts often below\n0.4.\nWhy it reads as AI: models regress to the mean sentence; humans write in bursts.\nFix: rebalance — split one long sentence into a short one and a fragment; merge\ntwo mid-length sentences into a long one. Aim for range, not a target.\n\n### Uniform paragraph length\nLooks like: every paragraph three to four sentences; every paragraph opens with\na topic sentence and closes with a mini-conclusion.\nScan: `paragraph_len.cv`. Rule of thumb: below 0.3 across five or more\nparagraphs is uniform.\nWhy it reads as AI: the five-paragraph-essay template applied to everything.\nFix: rebalance — allow a one-sentence paragraph; let one paragraph run long.\n\n## Discourse moves\n\n### Validating opener\nLooks like: \"Great question!\" \"I'd be happy to help.\" \"Absolutely!\" \"That's a\nreally insightful point.\"\nScan: `wordlist.hits` includes \"great question\", \"i hope this helps\".\nWhy it reads as AI: assistant-style acknowledgement before content; humans\nanswer.\nFix: removal — start with the content.\n\n### Restating the prompt\nLooks like: the first paragraph paraphrases the question or task before\naddressing it.\nScan: none; judge by reading.\nWhy it reads as AI: models anchor by echoing input; humans assume the reader\nremembers what they asked.\nFix: removal — delete the paraphrase.\n\n### Summary closer\nLooks like: a final paragraph opening \"In conclusion\", \"Ultimately\", \"Overall\",\n\"In short\" that restates the opening.\nScan: `discourse.summary_closer` (boolean).\nWhy it reads as AI: essay-template closure on texts that do not need it; humans\nend when they are done.\nFix: removal — cut the paragraph, or end on the last concrete point.\n\n### Sign-off advice and offers\nLooks like: \"Remember to…\", \"Feel free to reach out\", \"Don't hesitate to…\",\n\"I hope this helps!\"\nScan: `wordlist.hits` includes \"reach out\", \"don't hesitate\", \"i hope this helps\".\nWhy it reads as AI: assistant boilerplate.\nFix: removal — end with the actual last thing you have to say, or a plain sign-off.\n\n### Safety disclaimer opener and AI self-reference\nLooks like: a first paragraph that qualifies before it answers — \"It's\nimportant to approach this carefully\", \"I'm not able to give specific advice,\nbut\", \"consult a professional\" — or any \"As an AI\" self-reference.\nScan: `discourse.disclaimer_opener.fired` and `.hits`; per-family range 46%\nvs 0.2% of responses (Rudnicka & Juzek 2026 [rudnicka-2026]).\nWhy it reads as AI: assistant safety framing on a text that asked for none;\nthe same phrases mid-document are an ordinary discourse observation.\nFix: removal — start with the answer.\n\n### Headings and bullets in short pieces\nLooks like: a 200-word email with three bold headers and two bulleted lists.\nScan: none; judge by reading.\nWhy it reads as AI: structure imposed regardless of length or medium.\nFix: removal — prose for anything under ~300 words unless the medium expects\nlists.\n# Style tells\n\nStoryScope's Style dimension (figurative language, sound, syntax, register,\ntone, allusion), filtered to the 20 features with a human-vs-AI gap of at least\n15 points (categorical) or 0.30 (1–5 scale). Base rates are measured on 61,575\nstories; see `data/README.md`. These apply to every text class; each entry says\nhow it shows up outside fiction.\n\nEntries are ordered by the size of the human-vs-AI gap, largest first.\n\n### Presence of extended conceit\nLooks like: a metaphor that is introduced and then developed across several\nsentences or the whole piece — the company as a ship, grief as a house with\nrooms, the codebase as a garden — with each paragraph extending it.\nBase rate: AI 83% / human 40% (StoryScope STY_FIG_004)\nWhy it reads as AI: models sustain a governing metaphor because it is a\ncoherence strategy; most human writers drop a figure after one use.\nFix: removal — keep the first instance if it earns its place; cut every later\ncallback to the conceit and say the literal thing instead.\nOutside fiction: the \"journey\" or \"building blocks\" frame that runs through an\nentire blog post or team update.\n\n### Lexical register and consistency\nLooks like: a piece that stays in one register throughout — uniformly elevated,\nor uniformly neutral-standard — with no slang, no shift to plain talk, no\nsudden formal aside.\nBase rate: mixed register with code-switching — AI 19% / human 56% (StoryScope STY_ALL_015)\nWhy it reads as AI: humans slip between registers as mood and audience shift\nmid-text; models hold a single register as a consistency default.\nFix: addition — only when in character: let one sentence go colloquial, or let\na plain paragraph be interrupted by a precise technical term. Do not sprinkle\nslang mechanically.\nOutside fiction: an email that never once says \"yeah\", \"honestly\", or \"ugh\"\nfrom a writer who would.\n\n### Sound Patterning Prominence\nLooks like: alliteration and assonance you notice while reading — \"the slow\nslide of silt\", \"a hollow, swallowing hush\" — recurring through the piece rather\nthan once at a deliberate moment.\nBase rate: noticeable sound patterning — AI 91% / human 55% (StoryScope STY_TON_006)\nWhy it reads as AI: models reach for euphony by default, so the ear-pleasing\nversion of each phrase is the one that survives, while most human prose is\nsonically flat.\nFix: removal — keep the one sound effect that is placed for a reason and de-tune\nthe rest by swapping in the word you would have chosen without the ear in mind.\nOutside fiction: a product page where every feature name alliterates, or a\nheadline picked for sound over accuracy.\n\n### Conventional vs fresh figurative language\nLooks like: every image is a new one — \"the grief sat in her like a stone\nswallowed sideways\" — and no stock phrase appears anywhere in the piece.\nBase rate: predominantly fresh and inventive images — AI 65% / human 30% (StoryScope STY_FIG_003)\nWhy it reads as AI: the direction is counter-intuitive — models are more\ninventive than humans, not more clichéd, and the tell is relentless invention\nwith no ordinary sentence between the images.\nFix: removal — cut most of the fresh images and let plain statement carry the\npassage; a stock phrase in the right place is a human signal, not a failure.\nOutside fiction: an essay where every paragraph opens with a new and unusual\ncomparison instead of stating the point.\n\n### Predominant tonal quality\nLooks like: earnestness throughout — nothing is undercut, no line is wry, and\nthe piece means everything it says at face value.\nBase rate: earnest or lyrical tone — AI 71% / human 40% (StoryScope STY_TON_021)\nWhy it reads as AI: sincerity is the safe default for a model that cannot\npredict whether a joke will land.\nFix: removal — take the earnestness off at least one high point: undercut a\ngrand line, let a character be unimpressed, or state the thing flatly.\nOutside fiction: a launch post that is visibly moved by its own product, with no\ndry aside anywhere in it.\n\n### Sentence-structure repertoire\nLooks like: balanced series as the default sentence shape — \"It changed how we\nbuild, how we ship, how we think\" — paragraph after paragraph.\nBase rate: frequent parallel or list-like structures — AI 99% / human 70% (StoryScope STY_CPX_012)\nWhy it reads as AI: this is near-universal in AI text, because parallelism is\nthe cheapest way to sound composed and models apply it by reflex.\nFix: removal — break the series: cut one limb, make one limb a sentence of its\nown, or replace the list with a single specific. See the tricolon habit in\n`surface-tells.md` for the surface-level check.\nOutside fiction: bullet-shaped sentences in an email — \"faster, cheaper, and\neasier to maintain\" — where one accurate clause would do.\n\n### Recurrent metaphorical motif\nLooks like: one image family threaded through the whole piece — weight, tide,\nmachinery — so that unrelated paragraphs reach for the same register of\ncomparison.\nBase rate: motif present — AI 96% / human 69% (StoryScope STY_FIG_005)\nWhy it reads as AI: a recurring motif is a coherence device the model applies\nacross the whole text, while human writers rarely track their images that far.\nFix: removal — keep the motif's single strongest appearance and rewrite the\nothers from whatever the local scene or paragraph actually offers.\nOutside fiction: a strategy memo where everything is weather, momentum, or\nplumbing from the first line to the last.\n\n### Allusion domain diversity\nLooks like: references that stay in the safe canon — myth, history, literature —\nand never a named band, film, product, or team.\nBase rate: pop-culture or brand-name allusions — AI 13% / human 40% (StoryScope STY_ALL_018)\nWhy it reads as AI: models avoid naming real commercial and cultural artifacts,\nso their references land in a timeless, unplaceable nowhere.\nFix: addition — only when in character: name the actual thing the writer would\nname, the specific show or product, instead of \"a popular streaming series\".\nOutside fiction: a post about consumer software that never names a competitor.\n\n### Use of irony and humor\nLooks like: nothing in the piece is meant other than literally — no joke, no\nsarcasm, no dry undercut anywhere in it.\nBase rate: any discernible humor or irony (inverting the straight-faced value) —\nAI 62% / human 88% (StoryScope STY_TON_023)\nWhy it reads as AI: humor is the highest-risk register for a model, so playing\neverything straight is the safe move.\nFix: addition — only when in character: one dry line where the writer would\nactually be dry. Do not insert jokes into prose whose author has none.\nOutside fiction: an incident report or team update with not one wry remark, from\nsomeone whose speech is full of them.\n\n### Dominant Tonal Register\nLooks like: a narrative voice that lingers, reflects, and finds significance,\nrather than one that is reportorial, hardboiled, or analytic.\nBase rate: lyrical or meditative register — AI 77% / human 52% (StoryScope STY_TON_001)\nWhy it reads as AI: the reflective register is the model's comfortable default\nand gets applied whatever the material is.\nFix: removal — cut the reflective sentences and let the reported facts stand,\nthen pick the register the material actually demands and hold it.\nOutside fiction: a postmortem that keeps pausing to consider what the outage\nmeant for the team.\n\n### Dominant Figurative Device Type\nLooks like: comparisons asserted rather than proposed — \"the deadline was a\nwall\" rather than \"the deadline felt like a wall\" — with metaphor crowding out\nsimile and plain description.\nBase rate: metaphor-dominant — AI 73% / human 50% (StoryScope STY_FIG_002)\nWhy it reads as AI: metaphor is the more literary of the two moves, and models\ntake the more literary move.\nFix: removal — convert some metaphors back to similes or to literal statement;\nthe mix, not the metaphor itself, is what reads human.\nOutside fiction: business writing where processes are engines, funnels, and\nflywheels instead of being described.\n\n### Parataxis vs Hypotaxis Preference\nLooks like: syntax that never commits — some coordination, some subordination,\nevenly mixed, with no run of blunt short clauses and no long embedded sentence.\nBase rate: balanced parataxis and hypotaxis — AI 85% / human 64% (StoryScope STY_CPX_004)\nWhy it reads as AI: the balanced middle is the average of all training prose,\nand averaging is what the model does.\nFix: removal — take out the balance: write one passage as a chain of short\ncoordinated clauses and another as a single long subordinated sentence.\nOutside fiction: documentation where every sentence has the same clause count\nregardless of how simple or complicated the step is.\n\n### Primary Function of Allusion\nLooks like: a reference used to point at the moral — \"like Icarus, we flew too\nclose\" — rather than to color a setting or characterize whoever made it.\nBase rate: allusion as theme-signposting or moral commentary — AI 46% / human 26% (StoryScope STY_ALL_004)\nWhy it reads as AI: models use references to make the point legible, where human\nwriters more often use them for texture, humor, or offhand analogy.\nFix: removal — delete the signposting reference, or repurpose it so it says\nsomething about the person making it rather than about the theme.\nOutside fiction: an essay that closes on a myth or a quotation restating the\nargument.\n\n### Sound patterning devices\nLooks like: alliteration specifically, as the salient device whenever the prose\npushes — \"a steady, stubborn strain\", \"policy, practice, and posture\".\nBase rate: alliteration prominent — AI 98% / human 79% (StoryScope STY_TON_025)\nWhy it reads as AI: alliteration is the sound effect producible by word choice\nalone, so it is the one models overuse.\nFix: removal — replace the alliterating word with the accurate one; if the\nphrase survives only because it sounds good, cut the phrase.\nOutside fiction: alliterative section headings and slogans in a deck or a\nREADME.\n\n### Use of Sentence Fragments\nLooks like: fragments for emphasis, several to a page. Short. Punchy. Like this.\nBase rate: fragments present and stylistically significant — AI 85% / human 67% (StoryScope STY_CPX_003)\nWhy it reads as AI: the direction is counter-intuitive — AI fragments more than\nhumans do, because the one-beat emphatic fragment is a learned dramatic device,\nnot a sign of a loose human hand.\nFix: removal — rejoin most fragments into the sentences they broke off from, and\nkeep at most one where the break carries real emphasis.\nOutside fiction: a social post where every third line is a one-word paragraph.\n\n### Voice Markedness\nLooks like: a voice with flavor but no risk — a little rhythm, a few distinctive\nturns, no dialect, no verbal tic, nothing a reader could imitate.\nBase rate: moderately marked voice, distinct but not extreme — AI 98% / human 83% (StoryScope STY_TON_005)\nWhy it reads as AI: almost every AI passage lands in this middle band, while\nhuman writing spreads out into plain report and into genuine idiosyncrasy.\nFix: removal — take the voice off the middle: either strip it back to plain\nreport, or commit to one real mannerism and carry it consistently.\nOutside fiction: internal writing that sounds like a company rather than like a\nperson.\n\n### Figurative Device Density\nLooks like: a metaphor or simile in most paragraphs, including in sentences that\nhad a job to do and did not need one.\nBase rate: AI 3.66 / human 3.00 on a 1–5 scale (StoryScope STY_FIG_001)\nWhy it reads as AI: models decorate by default, and figurative density is the\nmost visible form that decoration takes.\nFix: rebalance — cut roughly a third of the figures, starting with those that\nexplain something already clear, and keep the ones doing work no literal\nsentence could do.\nOutside fiction: an explainer where every concept arrives with an analogy\nattached to it.\n\n### Rhythmic markedness of prose\nLooks like: cadence you can hear — balanced clauses, repeated openings,\nsentences resolving on a stressed beat — sustained across the whole piece.\nBase rate: AI 3.61 / human 3.17 on a 1–5 scale (StoryScope STY_TON_024)\nWhy it reads as AI: the model optimizes each sentence for fluency, and sustained\nfluency turns into a metronome.\nFix: rebalance — break the meter: let one sentence end awkwardly, run another\npast its natural stop, or drop a clause that exists only for balance.\nOutside fiction: a speech-shaped post that reads as though written to be read\naloud when nobody will read it aloud.\n\n### Allusion density\nLooks like: prose that refers to almost nothing outside itself — no cited text,\nno dated event, no artifact a reader would recognize.\nBase rate: AI 2.26 / human 2.59 on a 1–5 scale (StoryScope STY_ALL_017)\nWhy it reads as AI: models avoid specific external references because those are\nthe most checkable thing in a text, while humans reach for them constantly.\nFix: rebalance — add the reference the writer would actually have made, and\nconfirm it is real before keeping it.\nOutside fiction: a memo that cites no paper, no prior project, and no names.\n\n### Latinate vs Anglo-Saxon lexical flavor\nLooks like: \"utilize\", \"facilitate\", \"commence\", \"demonstrate\" where \"use\",\n\"help\", \"start\", and \"show\" would carry the same sense.\nBase rate: AI 2.83 / human 2.51 on a 1–5 scale (StoryScope STY_ALL_016)\nWhy it reads as AI: the formal register dominates the training mass, so the\nlearned word wins unless the context pushes hard the other way.\nFix: rebalance — swap in the short Germanic word wherever the register does not\nrequire the long one; `surface-tells.md` carries the paired surface check.\nOutside fiction: a chat message written in the register of a policy document.\n","exit_code":0,"status":"completed"}} -{"type":"item.started","item":{"id":"item_7","type":"command_execution","command":"/bin/zsh -lc 'python3 scripts/surface_scan.py /var/folders/fh/cczgqw9n4r97y43bbgz9py5w0000gn/T/tmp.yMTB1qMFiJ/ai_report.txt'","aggregated_output":"","exit_code":null,"status":"in_progress"}} -{"type":"item.completed","item":{"id":"item_7","type":"command_execution","command":"/bin/zsh -lc 'python3 scripts/surface_scan.py /var/folders/fh/cczgqw9n4r97y43bbgz9py5w0000gn/T/tmp.yMTB1qMFiJ/ai_report.txt'","aggregated_output":"{\n \"discourse\": {\n \"summary_closer\": true,\n \"disclaimer_opener\": {\n \"fired\": true,\n \"hits\": [\n {\n \"text\": \"it's important to approach\",\n \"sentence\": 0\n },\n {\n \"text\": \"i'm not able to\",\n \"sentence\": 1\n }\n ]\n }\n },\n \"dialogue\": {\n \"ratio\": 0.0\n },\n \"words\": 620,\n \"sentences\": 34,\n \"paragraphs\": 12,\n \"sentence_len\": {\n \"mean\": 18.24,\n \"stdev\": 3.46,\n \"cv\": 0.19,\n \"min\": 14,\n \"max\": 27,\n \"pct_over_30\": 0.0,\n \"p90\": 25,\n \"longest_flat_run\": 8\n },\n \"paragraph_len\": {\n \"mean\": 2.83,\n \"stdev\": 0.39,\n \"cv\": 0.137,\n \"min\": 2,\n \"max\": 3\n },\n \"punct\": {\n \"em_dash\": 0.0,\n \"en_dash\": 0.0,\n \"semicolon\": 0.0,\n \"colon\": 0.0,\n \"ellipsis\": 0.0,\n \"exclamation\": 0.0,\n \"counts\": {\n \"em_dash\": 0,\n \"en_dash\": 0,\n \"semicolon\": 0,\n \"colon\": 0,\n \"ellipsis\": 0,\n \"exclamation\": 0\n }\n },\n \"structures\": {\n \"tricolon\": 1,\n \"not_but\": 1,\n \"rhetorical_q\": 0,\n \"parallel_openers\": 0\n },\n \"openers\": {\n \"distinct_ratio\": 0.735\n },\n \"wordlist\": {\n \"hits\": [],\n \"rate\": 0.0\n },\n \"hedges\": {\n \"rate\": 0.0\n },\n \"intensifiers\": {\n \"rate\": 0.0\n },\n \"repetition\": {\n \"too_short\": false,\n \"repeated_phrase_rate\": 3.2,\n \"longest_repeat\": 5,\n \"phrases\": [\n {\n \"text\": \"across all workstreams and teams\",\n \"count\": 3,\n \"sentences\": [\n 5,\n 17,\n 32\n ]\n }\n ]\n },\n \"grammar\": {\n \"participial_tail\": {\n \"count\": 14,\n \"rate\": 22.6,\n \"hits\": [\n {\n \"text\": \", reflecting the late discovery of schema drift in the\",\n \"sentence\": 2\n },\n {\n \"text\": \", ensuring alignment across teams before any record reaches\",\n \"sentence\": 3\n },\n {\n \"text\": \", exceeding the target we set in March\",\n \"sentence\": 4\n },\n {\n \"text\": \", creating a sense of momentum that was missing last quarter\",\n \"sentence\": 5\n },\n {\n \"text\": \", allowing designers to retire eleven legacy patterns\",\n \"sentence\": 6\n },\n {\n \"text\": \", giving on-call staff a clearer signal during incidents\",\n \"sentence\": 9\n },\n {\n \"text\": \", matching the level the platform group had proposed\",\n \"sentence\": 10\n },\n {\n \"text\": \", resolving most of them within the first business day\",\n \"sentence\": 11\n },\n {\n \"text\": \", leaving two senior positions unfilled going into the next\",\n \"sentence\": 14\n },\n {\n \"text\": \", assuming the compensation adjustments are approved\",\n \"sentence\": 16\n }\n ]\n },\n \"container_of\": {\n \"count\": 2,\n \"hits\": [\n {\n \"text\": \"a sense of\",\n \"sentence\": 5\n },\n {\n \"text\": \"The weight of\",\n \"sentence\": 15\n }\n ]\n }\n },\n \"nominalization\": {\n \"count\": 24,\n \"hits\": [\n {\n \"text\": \"validation\",\n \"count\": 2\n },\n {\n \"text\": \"migration\",\n \"count\": 2\n },\n {\n \"text\": \"implementation\",\n \"count\": 1\n },\n {\n \"text\": \"ingestion\",\n \"count\": 1\n },\n {\n \"text\": \"alignment\",\n \"count\": 1\n },\n {\n \"text\": \"adoption\",\n \"count\": 1\n },\n {\n \"text\": \"completion\",\n \"count\": 1\n },\n {\n \"text\": \"optimization\",\n \"count\": 1\n },\n {\n \"text\": \"deflection\",\n \"count\": 1\n },\n {\n \"text\": \"decision\",\n \"count\": 1\n },\n {\n \"text\": \"compensation\",\n \"count\": 1\n },\n {\n \"text\": \"adjustments\",\n \"count\": 1\n },\n {\n \"text\": \"coordination\",\n \"count\": 1\n },\n {\n \"text\": \"escalation\",\n \"count\": 1\n },\n {\n \"text\": \"transformation\",\n \"count\": 1\n }\n ],\n \"of_frames\": [\n {\n \"text\": \"the implementation of\",\n \"sentence\": 2\n },\n {\n \"text\": \"the migration of\",\n \"sentence\": 7\n },\n {\n \"text\": \"the completion of\",\n \"sentence\": 7\n },\n {\n \"text\": \"the optimization of\",\n \"sentence\": 8\n },\n {\n \"text\": \"the escalation of\",\n \"sentence\": 18\n },\n {\n \"text\": \"the transformation of\",\n \"sentence\": 19\n },\n {\n \"text\": \"the remediation of\",\n \"sentence\": 20\n },\n {\n \"text\": \"the integration of\",\n \"sentence\": 26\n }\n ]\n }\n}\n","exit_code":0,"status":"completed"}} +## Tool calls +/bin/zsh -lc 'python3 scripts/surface_scan.py --text /var/folders/fh/cczgqw9n4r97y43bbgz9py5w0000gn/T/tmp.ODqhB1vxyV/ai_report.txt' +/bin/zsh -lc 'python3 scripts/surface_scan.py /var/folders/fh/cczgqw9n4r97y43bbgz9py5w0000gn/T/tmp.ODqhB1vxyV/ai_report.txt' + +## Scanner output +words 620 · sentences 34 · paragraphs 12 +sentence length: mean 18.24, stdev 3.46, cv 0.19 (min 14, max 27) +paragraph length: mean 2.83 sentences, stdev 0.39, cv 0.137 +per 1k words: em-dash 0.0 (0) · en-dash 0.0 (0) · semicolon 0.0 (0) · colon 0.0 (0) · ellipsis 0.0 (0) · exclamation 0.0 (0) +structures: tricolon 1 · not-but 1 · rhetorical-q 0 · parallel-opener runs 0 · distinct openers 0.735 +wordlist: 0.0/1k — none +hedges 0.0/1k · intensifiers 0.0/1k +summary closer: yes · dialogue paragraphs: 0% +repetition: 3.2/1k · longest repeat 5 · "across all workstreams and teams"×3 +grammar: participial tails 14 (22.6/1k) ", reflecting the late discovery of schema drift in the" · container-of 2 "a sense of" +sentence tail: over-30 0.0% · p90 25 · longest flat run 8 +nominalization hits: 24 (validation×2, migration×2, implementation×1) · frames: "the implementation of", "the migration of" + +{ + "discourse": { + "summary_closer": true, + "disclaimer_opener": { + "fired": true, + "hits": [ + { + "text": "it's important to approach", + "sentence": 0 + }, + { + "text": "i'm not able to", + "sentence": 1 + } + ] + } + }, + "dialogue": { + "ratio": 0.0 + }, + "words": 620, + "sentences": 34, + "paragraphs": 12, + "sentence_len": { + "mean": 18.24, + "stdev": 3.46, + "cv": 0.19, + "min": 14, + "max": 27, + "pct_over_30": 0.0, + "p90": 25, + "longest_flat_run": 8 + }, + "paragraph_len": { + "mean": 2.83, + "stdev": 0.39, + "cv": 0.137, + "min": 2, + "max": 3 + }, + "punct": { + "em_dash": 0.0, + "en_dash": 0.0, + "semicolon": 0.0, + "colon": 0.0, + "ellipsis": 0.0, + "exclamation": 0.0, + "counts": { + "em_dash": 0, + "en_dash": 0, + "semicolon": 0, + "colon": 0, + "ellipsis": 0, + "exclamation": 0 + } + }, + "structures": { + "tricolon": 1, + "not_but": 1, + "rhetorical_q": 0, + "parallel_openers": 0 + }, + "openers": { + "distinct_ratio": 0.735 + }, + "wordlist": { + "hits": [], + "rate": 0.0 + }, + "hedges": { + "rate": 0.0 + }, + "intensifiers": { + "rate": 0.0 + }, + "repetition": { + "too_short": false, + "repeated_phrase_rate": 3.2, + "longest_repeat": 5, + "phrases": [ + { + "text": "across all workstreams and teams", + "count": 3, + "sentences": [ + 5, + 17, + 32 + ] + } + ] + }, + "grammar": { + "participial_tail": { + "count": 14, + "rate": 22.6, + "hits": [ + { + "text": ", reflecting the late discovery of schema drift in the", + "sentence": 2 + }, + { + "text": ", ensuring alignment across teams before any record reaches", + "sentence": 3 + }, + { + "text": ", exceeding the target we set in March", + "sentence": 4 + }, + { + "text": ", creating a sense of momentum that was missing last quarter", + "sentence": 5 + }, + { + "text": ", allowing designers to retire eleven legacy patterns", + "sentence": 6 + }, + { + "text": ", giving on-call staff a clearer signal during incidents", + "sentence": 9 + }, + { + "text": ", matching the level the platform group had proposed", + "sentence": 10 + }, + { + "text": ", resolving most of them within the first business day", + "sentence": 11 + }, + { + "text": ", leaving two senior positions unfilled going into the next", + "sentence": 14 + }, + { + "text": ", assuming the compensation adjustments are approved", + "sentence": 16 + } + ] + }, + "container_of": { + "count": 2, + "hits": [ + { + "text": "a sense of", + "sentence": 5 + }, + { + "text": "The weight of", + "sentence": 15 + } + ] + } + }, + "nominalization": { + "count": 24, + "hits": [ + { + "text": "validation", + "count": 2 + }, + { + "text": "migration", + "count": 2 + }, + { + "text": "implementation", + "count": 1 + }, + { + "text": "ingestion", + "count": 1 + }, + { + "text": "alignment", + "count": 1 + }, + { + "text": "adoption", + "count": 1 + }, + { + "text": "completion", + "count": 1 + }, + { + "text": "optimization", + "count": 1 + }, + { + "text": "deflection", + "count": 1 + }, + { + "text": "decision", + "count": 1 + }, + { + "text": "compensation", + "count": 1 + }, + { + "text": "adjustments", + "count": 1 + }, + { + "text": "coordination", + "count": 1 + }, + { + "text": "escalation", + "count": 1 + }, + { + "text": "transformation", + "count": 1 + } + ], + "of_frames": [ + { + "text": "the implementation of", + "sentence": 2 + }, + { + "text": "the migration of", + "sentence": 7 + }, + { + "text": "the completion of", + "sentence": 7 + }, + { + "text": "the optimization of", + "sentence": 8 + }, + { + "text": "the escalation of", + "sentence": 18 + }, + { + "text": "the transformation of", + "sentence": 19 + }, + { + "text": "the remediation of", + "sentence": 20 + }, + { + "text": "the integration of", + "sentence": 26 + } + ] + } +} + ## Output -Audited `ai_report.txt` as **expository prose**: 620 words, 34 sentences. File unchanged. These are stylistic patterns, not proof of AI authorship. +I’ll use the humanize skill to audit `ai_report.txt` and leave the file unchanged. +Audited `ai_report.txt` as **expository prose** (620 words). File unchanged. These are style observations, not proof of AI authorship. + +| # | Tell | Evidence | Base rate / metric | +|---|------|----------|--------------------| +| 1 | Repeated participial endings | “, exceeding the target we set in March”; “, allowing designers to retire eleven legacy patterns” | Scanner flags 14 tails, or 22.6 per 1,000 words. The ingestion paragraph uses this ending in all three sentences—noticeable even for a report. | +| 2 | Verbatim repetition | “across all workstreams and teams” (×3) | Same five-word phrase repeated in three separate paragraphs; generic wording rather than necessary technical terminology. | +| 3 | Uniform paragraph length | Paragraphs beginning “Adoption of the shared design system” and “On the reliability side” both follow the same three-sentence format. | All 12 paragraphs contain 2–3 sentences; paragraph-length CV 0.137. Skill heuristic: below 0.3 across ≥5 paragraphs is uniform. | +| 4 | Uniform sentence length | “The compliance team confirmed that the evidence collection process now runs automatically each week.” | All 34 sentences fall between 14 and 27 words; sentence-length CV 0.19. Skill heuristic flags below 0.4, though report writing can legitimately be this steady. | +| 5 | Disclaimer opener | “It's important to approach this update carefully” | Scanner flags two disclaimer phrases in the opening. The general caution delays the update; the vendor-pricing restriction may have a legitimate purpose. | +| 6 | Summary closer | “In summary, delivery across all workstreams and teams stayed on plan” | `summary_closer: true`. Repeats the opening’s reassuring assessment; the following Friday deadline adds useful information. | +Audited `ai_report.txt` as **expository prose** (620 words). File unchanged. These are style observations, not proof of AI authorship. | # | Tell | Evidence | Base rate / metric | |---|------|----------|--------------------| -| 1 | Verbatim repetition | “across all workstreams and teams” (×3); repeated general wording without a clear rhetorical purpose. | Scanner: 3 occurrences of the same five-word phrase. | -| 2 | Uniform sentence length | “Four product surfaces migrated to the new components, allowing designers to retire eleven legacy patterns.” exemplifies the steady sentence rhythm. | Length CV: **0.19**; range: **14–27 words**. Skill heuristic flags below 0.4; technical prose can also fit this pattern. | -| 3 | Uniform paragraph length | The paragraph beginning “On the reliability side” follows the recurring three-sentence update format. | All 12 paragraphs contain **2–3 sentences**; CV **0.137**, below the skill’s 0.3 uniformity threshold. | -| 4 | Frequent trailing participial clauses | “, ensuring alignment across teams before any record reaches”; “, allowing designers to retire eleven legacy patterns” | Scanner: **14 candidates**, **22.6/1,000 words**. Normal in reports, but frequent enough here to make the syntax repetitive. | -| 5 | Disclaimer opener | “It's important to approach this update carefully”; “I'm not able to share vendor pricing here” | `discourse.disclaimer_opener.fired: true`; **2 matches**. The caution is generic; the pricing restriction may serve a real purpose. | -| 6 | Summary closer | “In summary, delivery across all workstreams and teams stayed on plan despite the schema issue” | `discourse.summary_closer: true`. Repeats the opening’s favorable assessment; the subsequent Friday deadline adds useful information. | +| 1 | Repeated participial endings | “, exceeding the target we set in March”; “, allowing designers to retire eleven legacy patterns” | Scanner flags 14 tails, or 22.6 per 1,000 words. The ingestion paragraph uses this ending in all three sentences—noticeable even for a report. | +| 2 | Verbatim repetition | “across all workstreams and teams” (×3) | Same five-word phrase repeated in three separate paragraphs; generic wording rather than necessary technical terminology. | +| 3 | Uniform paragraph length | Paragraphs beginning “Adoption of the shared design system” and “On the reliability side” both follow the same three-sentence format. | All 12 paragraphs contain 2–3 sentences; paragraph-length CV 0.137. Skill heuristic: below 0.3 across ≥5 paragraphs is uniform. | +| 4 | Uniform sentence length | “The compliance team confirmed that the evidence collection process now runs automatically each week.” | All 34 sentences fall between 14 and 27 words; sentence-length CV 0.19. Skill heuristic flags below 0.4, though report writing can legitimately be this steady. | +| 5 | Disclaimer opener | “It's important to approach this update carefully” | Scanner flags two disclaimer phrases in the opening. The general caution delays the update; the vendor-pricing restriction may have a legitimate purpose. | +| 6 | Summary closer | “In summary, delivery across all workstreams and teams stayed on plan” | `summary_closer: true`. Repeats the opening’s reassuring assessment; the following Friday deadline adds useful information. | diff --git a/tools/smoke_harnesses.sh b/tools/smoke_harnesses.sh index d7d17e2..9f5f8d6 100755 --- a/tools/smoke_harnesses.sh +++ b/tools/smoke_harnesses.sh @@ -1,6 +1,14 @@ #!/usr/bin/env bash # Local acceptance matrix for the humanize skill. Not run by CI or pytest. # PASS needs evidence the skill AND the scanner ran (spec §5), not row names alone. +# +# Every transcript has three H2 sections, in this order: "## Tool calls" (the +# commands/tool inputs the agent issued), "## Scanner output" (raw output of any +# scanner invocation), "## Output" (the agent's own final text only). The judge +# below is section-aware: rows, the nominalization negative, and the rewrite +# check run against ## Output only; the two scanner numbers may match in either +# ## Scanner output or ## Output; the tool-event regex runs against ## Tool +# calls only. set -u ROOT="$(cd "$(dirname "$0")/.." && pwd)" FIXTURE="tests/fixtures/ai_report.txt" @@ -14,14 +22,32 @@ SCAN="$(python3 "$ROOT/skills/humanize/scripts/surface_scan.py" --text "$ROOT/$F NUM_TAILS="$(printf '%s\n' "$SCAN" | sed -n 's/^grammar: participial tails \([0-9]*\).*/\1/p')" NUM_REP="$(printf '%s\n' "$SCAN" | sed -n 's/^repetition: \([0-9.]*\)\/1k.*/\1/p')" +# check_auth reads a transcript (or raw CLI output) on stdin and prints the first +# line matching a known authentication-failure signature, or nothing. +check_auth() { + grep -m1 -E "Authentication required|AuthError|token refresh failed|Please run 'agent login'" +} + +# section_* extract one H2 section's body from a transcript file, bounded by the +# next known heading (or EOF for the last section) so embedded "## " lines inside +# an agent's own output (e.g. a real "## Rewrite" heading) never get treated as a +# stray section boundary. +section_tool_calls() { awk '/^## Tool calls/{f=1;next} /^## Scanner output/{exit} f' "$1"; } +section_scanner_output() { awk '/^## Scanner output/{f=1;next} /^## Output/{exit} f' "$1"; } +section_output() { awk '/^## Output/{f=1;next} f' "$1"; } + judge() { # judge local h="$1" f="$2" tool_re="$3" ok=1 - for r in "${ROWS[@]}"; do grep -qiE "$r" "$f" || { echo " missing row: $r"; ok=0; }; done - grep -iE '^\|' "$f" | grep -qi 'nominalization' && { echo " nominalization row present"; ok=0; } - grep -qE "participial tails[^0-9]{0,20}${NUM_TAILS}([^0-9]|$)" "$f" || { echo " scanner count $NUM_TAILS absent"; ok=0; } - grep -qF "${NUM_REP}/1k" "$f" || { echo " scanner rate ${NUM_REP}/1k absent"; ok=0; } - [ -n "$tool_re" ] && { grep -qE "$tool_re" "$f" || { echo " no tool event naming surface_scan.py"; ok=0; }; } - grep -qiE 'Choices you may want to reverse|^## Rewrite' "$f" && { echo " rewrite section present"; ok=0; } + local out scan calls + out="$(section_output "$f")" + scan="$(section_scanner_output "$f")" + calls="$(section_tool_calls "$f")" + for r in "${ROWS[@]}"; do printf '%s\n' "$out" | grep -qiE "$r" || { echo " missing row: $r"; ok=0; }; done + printf '%s\n' "$out" | grep -iE '^\|' | grep -qi 'nominalization' && { echo " nominalization row present"; ok=0; } + printf '%s\n%s\n' "$scan" "$out" | grep -qE "participial tails[^0-9]{0,20}${NUM_TAILS}([^0-9]|$)" || { echo " scanner count $NUM_TAILS absent"; ok=0; } + printf '%s\n%s\n' "$scan" "$out" | grep -qF "${NUM_REP}/1k" || { echo " scanner rate ${NUM_REP}/1k absent"; ok=0; } + [ -n "$tool_re" ] && { printf '%s\n' "$calls" | grep -qE "$tool_re" || { echo " no tool event naming surface_scan.py"; ok=0; }; } + printf '%s\n' "$out" | grep -qiE 'Choices you may want to reverse|^## Rewrite' && { echo " rewrite section present"; ok=0; } [ "$ok" = 1 ] && echo "PASS $h" || echo "FAIL $h" } @@ -32,18 +58,57 @@ run_claude() { (cd "$ROOT" && claude -p "$REQUEST" --plugin-dir . --output-format stream-json --verbose \ --allowedTools "Bash,Read,Glob,Grep" > "$f.jsonl" 2>&1) claude plugin enable humanize@humanize >/dev/null 2>&1 || true + local auth_line + auth_line="$(check_auth < "$f.jsonl")" + if [ -n "$auth_line" ]; then + cp "$f.jsonl" "$f" + echo "SKIP claude-code (not authenticated: $auth_line)" + return + fi python3 - "$f.jsonl" "$f" <<'EOF' import json, sys src, dst = sys.argv[1], sys.argv[2] -texts, tools = [], [] +calls, scanner, output = [], [], [] for line in open(src, encoding="utf-8"): - try: ev = json.loads(line) - except ValueError: continue - for blk in (ev.get("message") or {}).get("content", []) if isinstance(ev, dict) else []: - if blk.get("type") == "text": texts.append(blk["text"]) - if blk.get("type") == "tool_use": tools.append(json.dumps(blk.get("input"))[:300]) - if ev.get("type") == "result" and ev.get("result"): texts.append(ev["result"]) -open(dst, "w", encoding="utf-8").write("# Claude Code\n\n## Tool calls\n" + "\n".join(tools) + "\n\n## Output\n" + "\n".join(texts)) + line = line.strip() + if not line: + continue + try: + ev = json.loads(line) + except ValueError: + continue + if not isinstance(ev, dict): + continue + msg = ev.get("message") or {} + role = msg.get("role") + content = msg.get("content") + if isinstance(content, list): + for blk in content: + if not isinstance(blk, dict): + continue + btype = blk.get("type") + if btype == "tool_use": + calls.append(json.dumps(blk.get("input"), indent=2)) + elif btype == "tool_result": + c = blk.get("content") + if isinstance(c, str): + scanner.append(c) + elif isinstance(c, list): + for sub in c: + if isinstance(sub, dict) and sub.get("type") == "text": + scanner.append(sub.get("text", "")) + elif btype == "text" and role == "assistant": + # Only the model's own text becomes ## Output. The Skill launch's + # injected SKILL.md dump arrives as role "user", type "text" and + # is deliberately dropped here — it must land nowhere. + output.append(blk.get("text", "")) + if ev.get("type") == "result" and ev.get("result"): + output.append(ev["result"]) +open(dst, "w", encoding="utf-8").write( + "# Claude Code\n\n## Tool calls\n" + "\n".join(calls) + + "\n\n## Scanner output\n" + "\n".join(scanner) + + "\n\n## Output\n" + "\n".join(output) +) EOF judge "claude-code" "$f" 'surface_scan\.py' } @@ -55,7 +120,55 @@ run_codex() { (cd "$tmp" && codex debug prompt-input "hi" 2>/dev/null | grep -qi humanize) || echo " warning: skill not listed by codex debug prompt-input" (cd "$tmp" && codex exec -C "$tmp" --skip-git-repo-check --ephemeral -s read-only --json \ -o "$tmp/out.md" "Use the humanize skill on ai_report.txt. Audit only — do not rewrite." "$tmp/events.jsonl" 2>&1) - { echo "# Codex"; echo; echo "## Tool events"; grep -i 'surface_scan' "$tmp/events.jsonl" | head -5; echo; echo "## Output"; cat "$tmp/out.md" 2>/dev/null; } > "$f" + local auth_line + auth_line="$(check_auth < "$tmp/events.jsonl")" + if [ -n "$auth_line" ]; then + cp "$tmp/events.jsonl" "$f" + echo "SKIP codex (not authenticated: $auth_line)" + rm -rf "$tmp" + return + fi + python3 - "$tmp/events.jsonl" "$tmp/out.md" "$f" <<'EOF' +import json, sys +events_path, out_md_path, dst = sys.argv[1], sys.argv[2], sys.argv[3] +calls, scanner, output = [], [], [] +for line in open(events_path, encoding="utf-8"): + line = line.strip() + if not line: + continue + try: + ev = json.loads(line) + except ValueError: + continue + if not isinstance(ev, dict) or ev.get("type") != "item.completed": + continue + item = ev.get("item") + if not isinstance(item, dict): + continue + itype = item.get("type") + if itype == "command_execution": + cmd = item.get("command") or "" + # Only the scanner invocation itself becomes tool-call/scanner-output + # evidence. Reads of SKILL.md (cat/sed/head) must not appear here, even + # though SKILL.md's own text happens to mention surface_scan.py. + if "python" in cmd and "surface_scan.py" in cmd: + calls.append(cmd) + scanner.append(item.get("aggregated_output") or "") + elif itype == "agent_message": + text = item.get("text") or "" + if text: + output.append(text) +try: + with open(out_md_path, encoding="utf-8") as fh: + output.append(fh.read()) +except OSError: + pass +open(dst, "w", encoding="utf-8").write( + "# Codex\n\n## Tool calls\n" + "\n".join(calls) + + "\n\n## Scanner output\n" + "\n".join(scanner) + + "\n\n## Output\n" + "\n".join(output) +) +EOF judge "codex" "$f" 'surface_scan\.py' rm -rf "$tmp" } @@ -63,26 +176,61 @@ run_codex() { run_cursor() { command -v agent >/dev/null || { echo "SKIP cursor (agent CLI not installed)"; return; } agent --help 2>/dev/null | grep -qE -- '(^| )-p[ ,]|--print' || { echo "SKIP cursor (agent $(agent --version 2>/dev/null | head -1) has no headless flag)"; return; } - local tmp f="$OUT/cursor.md"; tmp="$(mktemp -d)" + local tmp f="$OUT/cursor.md" raw; tmp="$(mktemp -d)" mkdir -p "$tmp/.cursor/skills" && cp -R "$ROOT/skills/humanize" "$tmp/.cursor/skills/" && cp "$ROOT/$FIXTURE" "$tmp/" - # Deviation from brief: the installed `agent` CLI's approvalMode is `allowlist` with - # only Shell(ls) allowed, so a headless run cannot execute the scanner without --force. - (cd "$tmp" && agent -p --force --output-format text "Use the humanize skill on ai_report.txt. Audit only — do not rewrite." > "$f" 2>&1) - judge "cursor" "$f" '' + # Deviation from the brief: the installed `agent` CLI's approvalMode is `allowlist` + # with only Shell(ls) allowed, so a headless run cannot execute the scanner + # without --force. + raw="$(cd "$tmp" && agent -p --force --output-format text "Use the humanize skill on ai_report.txt. Audit only — do not rewrite." 2>&1)" rm -rf "$tmp" + local auth_line + auth_line="$(printf '%s\n' "$raw" | check_auth)" + if [ -n "$auth_line" ]; then + printf '%s\n' "$raw" > "$f" + echo "SKIP cursor (not authenticated: $auth_line)" + return + fi + { + echo "## Tool calls" + echo "(not separately observable — agent --output-format text does not expose tool-call events)" + echo + echo "## Scanner output" + echo "(not separately observable — see ## Output)" + echo + echo "## Output" + printf '%s\n' "$raw" + } > "$f" + judge "cursor" "$f" '' } run_hermes() { command -v hermes >/dev/null || { echo "SKIP hermes (not installed)"; return; } - local dest="$HOME/.hermes/skills/writing/humanize" f="$OUT/hermes.md" + local dest="$HOME/.hermes/skills/writing/humanize" f="$OUT/hermes.md" raw [ -e "$dest" ] && { echo "SKIP hermes ($dest already exists; not touching it)"; return; } mkdir -p "$(dirname "$dest")" && cp -R "$ROOT/skills/humanize" "$dest" if hermes --help 2>/dev/null | grep -q -- ' -z'; then - hermes -z "$REQUEST" > "$f" 2>&1 + raw="$(hermes -z "$REQUEST" 2>&1)" else - hermes chat -q "$REQUEST" -Q > "$f" 2>&1 + raw="$(hermes chat -q "$REQUEST" -Q 2>&1)" fi rm -rf "$dest" + local auth_line + auth_line="$(printf '%s\n' "$raw" | check_auth)" + if [ -n "$auth_line" ]; then + printf '%s\n' "$raw" > "$f" + echo "SKIP hermes (not authenticated: $auth_line)" + return + fi + { + echo "## Tool calls" + echo "(not separately observable — hermes oneshot/quiet mode does not expose tool-call events)" + echo + echo "## Scanner output" + echo "(not separately observable — see ## Output)" + echo + echo "## Output" + printf '%s\n' "$raw" + } > "$f" judge "hermes" "$f" '' } From 1a8fd759aa3b8f35c4dd1d91ee74d14cd6ee6018 Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 11:37:51 -0400 Subject: [PATCH 16/22] Fix the scan-path wording, tighten step 3, concept-match smoke rows (Bugbot PR #8) Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- docs/acceptance/v0.3/README.md | 25 +- docs/acceptance/v0.3/claude-code.md | 365 +++++++++++------- docs/acceptance/v0.3/codex.md | 36 +- .../design/2026-09-14-humanize-v0.3-design.md | 16 + skills/humanize/SKILL.md | 16 +- tools/smoke_harnesses.sh | 17 +- 6 files changed, 303 insertions(+), 172 deletions(-) diff --git a/docs/acceptance/v0.3/README.md b/docs/acceptance/v0.3/README.md index d1c04ae..714e801 100644 --- a/docs/acceptance/v0.3/README.md +++ b/docs/acceptance/v0.3/README.md @@ -17,7 +17,9 @@ temp project for Codex and Cursor, `--plugin-dir .` for Claude Code, a copy into `~/.hermes/skills/writing/humanize` for Hermes), sends one non-interactive request, and captures the output. -Every transcript has three H2 sections, in this fixed order: +A PASS or FAIL transcript has three H2 sections, in this fixed order; a SKIP +transcript (authentication failure) is the harness's raw CLI output with no section +headings at all: - `## Tool calls` — the commands or tool inputs the agent issued (for Claude Code and Codex, only the actual scanner invocation; reads of `SKILL.md` never appear here). @@ -28,15 +30,20 @@ Every transcript has three H2 sections, in this fixed order: `--output-format text`, Hermes's oneshot/quiet mode), the first two sections say so and the full raw response goes under `## Output`. +The only CLI state the script touches across a run is a transient disable/enable of +the installed `humanize@humanize` plugin around the Claude Code run (so `--plugin-dir +.` is exercised instead of the installed copy); it is re-enabled whether or not the +run succeeds, and nothing else on the machine is reconfigured. + ## PASS rule A harness is judged PASS only with evidence that both the skill and the scanner ran. The judge is section-aware: each check below runs against the section named, not the whole transcript. -- All four expected audit rows are present in `## Output` (case-insensitive): trailing - participial clause, verbatim repetition, container-noun phrase, safety disclaimer - opener. +- All four expected audit rows are present in `## Output` (case-insensitive), matched + by concept rather than by our own reference titles: a participial-tail row, a + repetition row, a container-noun row, and a disclaimer-opener row. - No nominalization row in `## Output` (nominalization is reported-only per spec, never a row). - The scanner's exact participial-tails count and repetition rate (computed by the @@ -50,6 +57,16 @@ whole transcript. `tools/smoke_harnesses.sh` prints one `PASS `, `FAIL `, or `SKIP (reason)` line per harness, with reason lines above a FAIL. +The container-noun row is the concept the Codex smoke run previously dropped while +paraphrasing our reference titles, so its regex now also accepts the scanner's own +`container_of` hit text on the fixture (`a sense of`, `The weight of`), computed fresh +from a live scan each run rather than hardcoded, in addition to the word "container" +itself — a model quoting either satisfies the row. On the 2026-09-14 re-run under this +change, Codex named the row explicitly ("Abstract container phrases — weak signal"), +quoting both hits verbatim, and judged PASS alongside Claude Code; a Codex run that +names only that row incompletely, or omits it, would still FAIL and is a recorded +outcome, not grounds to loosen the judge further. + ## SKIP on authentication failure If a harness's raw output contains a recognized authentication-failure signature diff --git a/docs/acceptance/v0.3/claude-code.md b/docs/acceptance/v0.3/claude-code.md index a66ec4e..8ffcd08 100644 --- a/docs/acceptance/v0.3/claude-code.md +++ b/docs/acceptance/v0.3/claude-code.md @@ -6,28 +6,28 @@ "args": "tests/fixtures/ai_report.txt --audit-only" } { - "command": "cd /Users/ccf/git/humanize/skills/humanize && cat references/principles.md && echo ===== && cat /Users/ccf/git/humanize/tests/fixtures/ai_report.txt && echo ===== && wc -w /Users/ccf/git/humanize/tests/fixtures/ai_report.txt", + "command": "cat skills/humanize/references/principles.md && echo ===== && cat tests/fixtures/ai_report.txt && echo ===== && wc -w tests/fixtures/ai_report.txt", "description": "Read principles reference and the fixture text" } { - "command": "cd /Users/ccf/git/humanize/skills/humanize && cat /Users/ccf/git/humanize/tests/fixtures/ai_report.txt && printf '\\n----- words: ' && wc -w < /Users/ccf/git/humanize/tests/fixtures/ai_report.txt", + "command": "cat tests/fixtures/ai_report.txt; printf '\\n-----\\n'; wc -w tests/fixtures/ai_report.txt", "description": "Read the fixture text and count words" } { - "command": "cat references/surface-tells.md", - "description": "Read surface-tells reference" + "command": "cat skills/humanize/references/surface-tells.md", + "description": "Read surface tells reference" } { - "command": "cat references/style-tells.md", - "description": "Read style-tells reference" + "command": "cat skills/humanize/references/style-tells.md", + "description": "Read style tells reference" } { - "command": "python3 scripts/surface_scan.py --text /Users/ccf/git/humanize/tests/fixtures/ai_report.txt", - "description": "Run surface scanner on the fixture" + "command": "python3 skills/humanize/scripts/surface_scan.py --text tests/fixtures/ai_report.txt", + "description": "Run the surface scanner on the fixture" } { - "command": "python3 scripts/surface_scan.py /Users/ccf/git/humanize/tests/fixtures/ai_report.txt | python3 -c \"\nimport json,sys; d=json.load(sys.stdin)\nprint(json.dumps({k:d[k] for k in ('discourse','grammar','repetition','structures')}, indent=1)[:4000])\"", - "description": "Pull discourse, grammar, repetition and structure hits from full scan JSON" + "command": "python3 skills/humanize/scripts/surface_scan.py tests/fixtures/ai_report.txt | python3 -c \"\nimport json,sys; d=json.load(sys.stdin)\nfor k in ('discourse','grammar','repetition','structures','nominalization'):\n print(k, json.dumps(d.get(k), indent=1)[:2500]); print()\n\"", + "description": "Dump the fired scanner blocks with verbatim hits" } ## Scanner output @@ -120,7 +120,8 @@ Finance reviewed the quarterly forecast with each product area and found no mate In summary, delivery across all workstreams and teams stayed on plan despite the schema issue, and the quarter closes with fewer open risks than it opened with. Please raise any concerns before Friday so they can be folded into the planning session. ------ words: 620 +----- + 620 tests/fixtures/ai_report.txt # Surface tells The layer StoryScope does not measure: vocabulary, grammar, punctuation, @@ -608,145 +609,229 @@ repetition: 3.2/1k · longest repeat 5 · "across all workstreams and teams"×3 grammar: participial tails 14 (22.6/1k) ", reflecting the late discovery of schema drift in the" · container-of 2 "a sense of" sentence tail: over-30 0.0% · p90 25 · longest flat run 8 nominalization hits: 24 (validation×2, migration×2, implementation×1) · frames: "the implementation of", "the migration of" -{ - "discourse": { - "summary_closer": true, - "disclaimer_opener": { - "fired": true, - "hits": [ - { - "text": "it's important to approach", - "sentence": 0 - }, - { - "text": "i'm not able to", - "sentence": 1 - } - ] - } - }, - "grammar": { - "participial_tail": { - "count": 14, - "rate": 22.6, - "hits": [ - { - "text": ", reflecting the late discovery of schema drift in the", - "sentence": 2 - }, - { - "text": ", ensuring alignment across teams before any record reaches", - "sentence": 3 - }, - { - "text": ", exceeding the target we set in March", - "sentence": 4 - }, - { - "text": ", creating a sense of momentum that was missing last quarter", - "sentence": 5 - }, - { - "text": ", allowing designers to retire eleven legacy patterns", - "sentence": 6 - }, - { - "text": ", giving on-call staff a clearer signal during incidents", - "sentence": 9 - }, - { - "text": ", matching the level the platform group had proposed", - "sentence": 10 - }, - { - "text": ", resolving most of them within the first business day", - "sentence": 11 - }, - { - "text": ", leaving two senior positions unfilled going into the next", - "sentence": 14 - }, - { - "text": ", assuming the compensation adjustments are approved", - "sentence": 16 - } - ] - }, - "container_of": { - "count": 2, - "hits": [ - { - "text": "a sense of", - "sentence": 5 - }, - { - "text": "The weight of", - "sentence": 15 - } - ] - } - }, - "repetition": { - "too_short": false, - "repeated_phrase_rate": 3.2, - "longest_repeat": 5, - "phrases": [ +discourse { + "summary_closer": true, + "disclaimer_opener": { + "fired": true, + "hits": [ { - "text": "across all workstreams and teams", - "count": 3, - "sentences": [ - 5, - 17, - 32 - ] + "text": "it's important to approach", + "sentence": 0 + }, + { + "text": "i'm not able to", + "sentence": 1 } ] - }, - "structures": { - "tricolon": 1, - "not_but": 1, - "rhetorical_q": 0, - "parallel_openers": 0 } } -## Output -Using the humanize skill to audit the fixture without rewriting. -Audit only, so I stop after the table. The text is 620 words, classified `expository` (a quarterly status report). +grammar { + "participial_tail": { + "count": 14, + "rate": 22.6, + "hits": [ + { + "text": ", reflecting the late discovery of schema drift in the", + "sentence": 2 + }, + { + "text": ", ensuring alignment across teams before any record reaches", + "sentence": 3 + }, + { + "text": ", exceeding the target we set in March", + "sentence": 4 + }, + { + "text": ", creating a sense of momentum that was missing last quarter", + "sentence": 5 + }, + { + "text": ", allowing designers to retire eleven legacy patterns", + "sentence": 6 + }, + { + "text": ", giving on-call staff a clearer signal during incidents", + "sentence": 9 + }, + { + "text": ", matching the level the platform group had proposed", + "sentence": 10 + }, + { + "text": ", resolving most of them within the first business day", + "sentence": 11 + }, + { + "text": ", leaving two senior positions unfilled going into the next", + "sentence": 14 + }, + { + "text": ", assuming the compensation adjustments are approved", + "sentence": 16 + } + ] + }, + "container_of": { + "count": 2, + "hits": [ + { + "text": "a sense of", + "sentence": 5 + }, + { + "text": "The weight of", + "sentence": 15 + } + ] + } +} -| # | Tell | Evidence | Base rate / metric | -|---|------|----------|--------------------| -| 1 | Safety disclaimer opener | "It's important to approach this update carefully", "I'm not able to share vendor pricing here, but" (×2, sentences 1–2) | 46% vs 0.2% of responses, per-family range [rudnicka-2026] | -| 2 | Trailing participial clause | ", ensuring alignment across teams before any record reaches" (×14 in 34 sentences) | scan: 22.6/1k; ratio 5.3×, d = 1.38 [reinhart-2025] | -| 3 | Verbatim repetition | "across all workstreams and teams" (×3, sentences 6, 18, 33) | scan: longest repeat 5 words; OR 1.47 [jakesch-2023] | -| 4 | Uniform sentence length | every sentence 14–27 words; longest flat run 8; no sentence over 30 | scan: cv 0.19, over-30 0.0%; rule of thumb human 0.5–0.9; human over-30 31.2% vs AI 17.5–21.0% [munoz-ortiz-2024] | -| 5 | Uniform paragraph length | 12 paragraphs, all 2–3 sentences | scan: paragraph cv 0.137; rule of thumb below 0.3 is uniform | -| 6 | Summary closer | "In summary, delivery across all workstreams and teams stayed on plan despite the schema issue" | scan: summary_closer = yes | -| 7 | Abstract container-noun phrase | "creating a sense of momentum", "The weight of the decision to pause backfills" (×2) | scan: container_of 2; heads from LAMP Table 8 [chakrabarty-2025] | +repetition { + "too_short": false, + "repeated_phrase_rate": 3.2, + "longest_repeat": 5, + "phrases": [ + { + "text": "across all workstreams and teams", + "count": 3, + "sentences": [ + 5, + 17, + 32 + ] + } + ] +} -Notes on what did not become a row: +structures { + "tricolon": 1, + "not_but": 1, + "rhetorical_q": 0, + "parallel_openers": 0 +} -- **Nominalizations** fired 24 hits with stacked frames such as "the implementation of", "the migration of", "the optimization of", "the remediation of", "the transformation of". In expository prose these are a prompt to look, not a row. Here they are dense enough that a rewrite would unbury several verbs. -- **Wordlist, hedges, intensifiers, em-dashes, semicolons** all scanned at zero. The one tricolon and the one not-but structure are within normal range for this length. -- **Style tells** mostly did not fire in a way a status report would not earn. The uniformly earnest, neutral register is native to the genre, so I did not count it as evidence. +nominalization { + "count": 24, + "hits": [ + { + "text": "validation", + "count": 2 + }, + { + "text": "migration", + "count": 2 + }, + { + "text": "implementation", + "count": 1 + }, + { + "text": "ingestion", + "count": 1 + }, + { + "text": "alignment", + "count": 1 + }, + { + "text": "adoption", + "count": 1 + }, + { + "text": "completion", + "count": 1 + }, + { + "text": "optimization", + "count": 1 + }, + { + "text": "deflection", + "count": 1 + }, + { + "text": "decision", + "count": 1 + }, + { + "text": "compensation", + "count": 1 + }, + { + "text": "adjustments", + "count": 1 + }, + { + "text": "coordination", + "count": 1 + }, + { + "text": "escalation", + "count": 1 + }, + { + "text": "transformation", + "count": 1 + } + ], + "of_frames": [ + { + "text": "the implementation of", + "sentence": 2 + }, + { + "text": "the migration of", + "sentence": 7 + }, + { + "text": "the completion of", + "sentence": 7 + }, + { + "text": "the optimization of", + "sentence": 8 + }, + { + "text": "the escalation of", + "sentence": 18 + }, + { + "text": "the transformation of", + "sentence": 19 + }, + { + "text": "the remediation of", + "sentence": 20 + }, + { + "text": "the integration of", + "sentence": 26 + } + ] +} -Strongest signals are the assistant-style disclaimer opening, the participial tails in nearly half the sentences, and the flat sentence and paragraph rhythm. No rewrite was made. -Audit only, so I stop after the table. The text is 620 words, classified `expository` (a quarterly status report). +## Output +I'll invoke the humanize skill and run an audit-only pass on the fixture. +Audit complete. Classified as `expository` (quarterly status report, 620 words, 34 sentences, 12 paragraphs). Scanner ran; the table below covers every block that fired. | # | Tell | Evidence | Base rate / metric | |---|------|----------|--------------------| -| 1 | Safety disclaimer opener | "It's important to approach this update carefully", "I'm not able to share vendor pricing here, but" (×2, sentences 1–2) | 46% vs 0.2% of responses, per-family range [rudnicka-2026] | -| 2 | Trailing participial clause | ", ensuring alignment across teams before any record reaches" (×14 in 34 sentences) | scan: 22.6/1k; ratio 5.3×, d = 1.38 [reinhart-2025] | -| 3 | Verbatim repetition | "across all workstreams and teams" (×3, sentences 6, 18, 33) | scan: longest repeat 5 words; OR 1.47 [jakesch-2023] | -| 4 | Uniform sentence length | every sentence 14–27 words; longest flat run 8; no sentence over 30 | scan: cv 0.19, over-30 0.0%; rule of thumb human 0.5–0.9; human over-30 31.2% vs AI 17.5–21.0% [munoz-ortiz-2024] | -| 5 | Uniform paragraph length | 12 paragraphs, all 2–3 sentences | scan: paragraph cv 0.137; rule of thumb below 0.3 is uniform | -| 6 | Summary closer | "In summary, delivery across all workstreams and teams stayed on plan despite the schema issue" | scan: summary_closer = yes | -| 7 | Abstract container-noun phrase | "creating a sense of momentum", "The weight of the decision to pause backfills" (×2) | scan: container_of 2; heads from LAMP Table 8 [chakrabarty-2025] | - -Notes on what did not become a row: - -- **Nominalizations** fired 24 hits with stacked frames such as "the implementation of", "the migration of", "the optimization of", "the remediation of", "the transformation of". In expository prose these are a prompt to look, not a row. Here they are dense enough that a rewrite would unbury several verbs. -- **Wordlist, hedges, intensifiers, em-dashes, semicolons** all scanned at zero. The one tricolon and the one not-but structure are within normal range for this length. -- **Style tells** mostly did not fire in a way a status report would not earn. The uniformly earnest, neutral register is native to the genre, so I did not count it as evidence. - -Strongest signals are the assistant-style disclaimer opening, the participial tails in nearly half the sentences, and the flat sentence and paragraph rhythm. No rewrite was made. +| 1 | Trailing participial clause | ", ensuring alignment across teams before any record reaches" (×14) | scan: 14 tails, 22.6/1k; AI 5.3× human, d = 1.38 [reinhart-2025] | +| 2 | Safety disclaimer opener | "It's important to approach this update carefully" / "I'm not able to share vendor pricing here, but" | scan: fired, 2 hits; 46% vs 0.2% of responses [rudnicka-2026] | +| 3 | Verbatim repetition | "across all workstreams and teams" (×3, sentences 5, 17, 32) | scan: 3.2/1k, longest repeat 5; OR 1.47 [jakesch-2023] | +| 4 | Uniform sentence length | every sentence 14–27 words; none over 30 | scan: cv 0.19, over-30 0.0%, flat run 8; rule of thumb: human 0.5–0.9, AI often < 0.4 | +| 5 | Uniform paragraph length | 12 paragraphs of 2–3 sentences each | scan: paragraph cv 0.137; rule of thumb: < 0.3 across 5+ paragraphs is uniform | +| 6 | Summary closer | "In summary, delivery across all workstreams and teams stayed on plan…" | scan: summary_closer true | +| 7 | Abstract container-noun phrase | "a sense of momentum", "The weight of the decision" (×2) | scan: container-of 2; LAMP Table 8 heads, rare in human seed paragraphs [chakrabarty-2025] | + +Notes on what did not make the table: + +- **Nominalization** is a prompt to look, not a row, in expository prose. The scanner logged 24 hits and eight "the X of" frames ("the implementation of", "the optimization of", "the remediation of"). For a status report this is native register, though eight frames in 620 words is on the heavy side. +- **Not-X-but-Y** counted 1, but the match is the "I'm not able to… but" opener already covered in row 2. +- **Tricolon** counted 1, well under the one-per-150-words rule of thumb. +- Wordlist, hedges, intensifiers, and every punctuation metric came back at zero. Opener diversity is 0.735, above the monotony threshold. +- No style-layer tell fired with enough evidence to rank. The register is plain report throughout, which a quarterly update earns, so tone and voice were not scored against it. + +Stopped after the audit as requested. No rewrite produced. diff --git a/docs/acceptance/v0.3/codex.md b/docs/acceptance/v0.3/codex.md index abb34a6..feb778e 100644 --- a/docs/acceptance/v0.3/codex.md +++ b/docs/acceptance/v0.3/codex.md @@ -1,8 +1,8 @@ # Codex ## Tool calls -/bin/zsh -lc 'python3 scripts/surface_scan.py --text /var/folders/fh/cczgqw9n4r97y43bbgz9py5w0000gn/T/tmp.ODqhB1vxyV/ai_report.txt' -/bin/zsh -lc 'python3 scripts/surface_scan.py /var/folders/fh/cczgqw9n4r97y43bbgz9py5w0000gn/T/tmp.ODqhB1vxyV/ai_report.txt' +/bin/zsh -lc 'python3 .agents/skills/humanize/scripts/surface_scan.py --text ai_report.txt' +/bin/zsh -lc 'python3 .agents/skills/humanize/scripts/surface_scan.py ai_report.txt' ## Scanner output words 620 · sentences 34 · paragraphs 12 @@ -273,24 +273,26 @@ nominalization hits: 24 (validation×2, migration×2, implementation×1) · fram ## Output -I’ll use the humanize skill to audit `ai_report.txt` and leave the file unchanged. -Audited `ai_report.txt` as **expository prose** (620 words). File unchanged. These are style observations, not proof of AI authorship. +I’ll read the humanize skill and audit `ai_report.txt` without changing it. +Audited `ai_report.txt` as **expository prose**. No changes made. These are stylistic findings, not proof of AI authorship. | # | Tell | Evidence | Base rate / metric | |---|------|----------|--------------------| -| 1 | Repeated participial endings | “, exceeding the target we set in March”; “, allowing designers to retire eleven legacy patterns” | Scanner flags 14 tails, or 22.6 per 1,000 words. The ingestion paragraph uses this ending in all three sentences—noticeable even for a report. | -| 2 | Verbatim repetition | “across all workstreams and teams” (×3) | Same five-word phrase repeated in three separate paragraphs; generic wording rather than necessary technical terminology. | -| 3 | Uniform paragraph length | Paragraphs beginning “Adoption of the shared design system” and “On the reliability side” both follow the same three-sentence format. | All 12 paragraphs contain 2–3 sentences; paragraph-length CV 0.137. Skill heuristic: below 0.3 across ≥5 paragraphs is uniform. | -| 4 | Uniform sentence length | “The compliance team confirmed that the evidence collection process now runs automatically each week.” | All 34 sentences fall between 14 and 27 words; sentence-length CV 0.19. Skill heuristic flags below 0.4, though report writing can legitimately be this steady. | -| 5 | Disclaimer opener | “It's important to approach this update carefully” | Scanner flags two disclaimer phrases in the opening. The general caution delays the update; the vendor-pricing restriction may have a legitimate purpose. | -| 6 | Summary closer | “In summary, delivery across all workstreams and teams stayed on plan” | `summary_closer: true`. Repeats the opening’s reassuring assessment; the following Friday deadline adds useful information. | -Audited `ai_report.txt` as **expository prose** (620 words). File unchanged. These are style observations, not proof of AI authorship. +| 1 | Verbatim repetition | “across all workstreams and teams” (×3), without an apparent rhetorical purpose. | Same five-word phrase repeated three times. | +| 2 | Repeated participial tails | “, ensuring alignment across teams before any record reaches”; “, exceeding the target we set in March”; “, allowing designers to retire eleven legacy patterns”. The repeated construction makes results sound mechanically appended. | Scanner: 14 hits; 22.6 per 1,000 words. Legitimate in reports, but conspicuous at this density. | +| 3 | Uniform sentence length | “Two departments asked for a shorter reporting cycle, and that request is under review.” Nearly every sentence has a similar medium-length cadence. | 34 sentences; range 14–27 words; coefficient of variation (CV) 0.19. Skill heuristic flags CV below 0.4 as unusually flat. | +| 4 | Uniform paragraph length | The paragraph beginning “On the reliability side” follows the same three-sentence shape as nearly every substantive update. | All 12 paragraphs contain 2–3 sentences; paragraph CV 0.137, below the skill’s 0.3 uniformity heuristic. | +| 5 | Disclaimer opener | “It's important to approach” and “I'm not able to” delay the substantive update. The vendor-pricing restriction may have a real purpose; the cautionary lead-in is generic. | Scanner: disclaimer opener fired, with two matching phrases. | +| 6 | Summary closer | “In summary, delivery across all workstreams and teams stayed on plan” repeats the broad reassurance. “Stayed on plan” also needs reconciling with the reported two-week delay. | Scanner: summary closer detected. | +| 7 | Abstract container phrases — weak signal | “a sense of” momentum; “The weight of” the decision. These make the account less concrete, but are ordinary report language at this frequency. | Scanner: 2 container-noun hits; insufficient density to treat as a strong tell. | +Audited `ai_report.txt` as **expository prose**. No changes made. These are stylistic findings, not proof of AI authorship. | # | Tell | Evidence | Base rate / metric | |---|------|----------|--------------------| -| 1 | Repeated participial endings | “, exceeding the target we set in March”; “, allowing designers to retire eleven legacy patterns” | Scanner flags 14 tails, or 22.6 per 1,000 words. The ingestion paragraph uses this ending in all three sentences—noticeable even for a report. | -| 2 | Verbatim repetition | “across all workstreams and teams” (×3) | Same five-word phrase repeated in three separate paragraphs; generic wording rather than necessary technical terminology. | -| 3 | Uniform paragraph length | Paragraphs beginning “Adoption of the shared design system” and “On the reliability side” both follow the same three-sentence format. | All 12 paragraphs contain 2–3 sentences; paragraph-length CV 0.137. Skill heuristic: below 0.3 across ≥5 paragraphs is uniform. | -| 4 | Uniform sentence length | “The compliance team confirmed that the evidence collection process now runs automatically each week.” | All 34 sentences fall between 14 and 27 words; sentence-length CV 0.19. Skill heuristic flags below 0.4, though report writing can legitimately be this steady. | -| 5 | Disclaimer opener | “It's important to approach this update carefully” | Scanner flags two disclaimer phrases in the opening. The general caution delays the update; the vendor-pricing restriction may have a legitimate purpose. | -| 6 | Summary closer | “In summary, delivery across all workstreams and teams stayed on plan” | `summary_closer: true`. Repeats the opening’s reassuring assessment; the following Friday deadline adds useful information. | +| 1 | Verbatim repetition | “across all workstreams and teams” (×3), without an apparent rhetorical purpose. | Same five-word phrase repeated three times. | +| 2 | Repeated participial tails | “, ensuring alignment across teams before any record reaches”; “, exceeding the target we set in March”; “, allowing designers to retire eleven legacy patterns”. The repeated construction makes results sound mechanically appended. | Scanner: 14 hits; 22.6 per 1,000 words. Legitimate in reports, but conspicuous at this density. | +| 3 | Uniform sentence length | “Two departments asked for a shorter reporting cycle, and that request is under review.” Nearly every sentence has a similar medium-length cadence. | 34 sentences; range 14–27 words; coefficient of variation (CV) 0.19. Skill heuristic flags CV below 0.4 as unusually flat. | +| 4 | Uniform paragraph length | The paragraph beginning “On the reliability side” follows the same three-sentence shape as nearly every substantive update. | All 12 paragraphs contain 2–3 sentences; paragraph CV 0.137, below the skill’s 0.3 uniformity heuristic. | +| 5 | Disclaimer opener | “It's important to approach” and “I'm not able to” delay the substantive update. The vendor-pricing restriction may have a real purpose; the cautionary lead-in is generic. | Scanner: disclaimer opener fired, with two matching phrases. | +| 6 | Summary closer | “In summary, delivery across all workstreams and teams stayed on plan” repeats the broad reassurance. “Stayed on plan” also needs reconciling with the reported two-week delay. | Scanner: summary closer detected. | +| 7 | Abstract container phrases — weak signal | “a sense of” momentum; “The weight of” the decision. These make the account less concrete, but are ordinary report language at this frequency. | Scanner: 2 container-noun hits; insufficient density to treat as a strong tell. | diff --git a/docs/design/2026-09-14-humanize-v0.3-design.md b/docs/design/2026-09-14-humanize-v0.3-design.md index 02be476..f127f78 100644 --- a/docs/design/2026-09-14-humanize-v0.3-design.md +++ b/docs/design/2026-09-14-humanize-v0.3-design.md @@ -430,3 +430,19 @@ is the specific source of a number or a file; the tool is described as grounded - **claude.ai description length** — acceptance 5b. - **Cowork validation of plugin-bundled frontmatter** against the six fields is unverified; the spec removes the only non-spec key, so it no longer matters. + +## Post-review amendments (2026-09-14) + +- §2B — Bugbot (PR #8): "run it from this skill's folder" implied a `cd` that broke project-relative + `` arguments. The command is now `python3 /scripts/surface_scan.py --text + `, with the working directory kept on the user's project; `` is the + directory holding SKILL.md, which every harness shows the model when the skill loads. +- §2 / step 3 — the Codex smoke run (gpt-6-astra) omitted a scanner-reported grammar block while + paraphrasing tell names. Step 3 now says every fired scanner block gets its own row. Budget + unchanged (150). +- §5 — transcripts have three sections (`## Tool calls`, `## Scanner output`, `## Output`) and the + judge checks each rule against the right one; rows match by concept (participial, repetition, + container or a scanner-quoted container hit, disclaimer), not by reference title; an + unauthenticated harness prints `SKIP` with the reason rather than `FAIL`. +- §8 acceptance 2 — Cursor and Hermes were SKIP (not authenticated on this machine) at the + pre-merge run; they are re-run post-login and recorded in `docs/acceptance/v0.3/`. diff --git a/skills/humanize/SKILL.md b/skills/humanize/SKILL.md index c956c70..b0f6136 100644 --- a/skills/humanize/SKILL.md +++ b/skills/humanize/SKILL.md @@ -67,13 +67,12 @@ the length. If the text is 80 words or longer, run the scanner and keep the output: ```bash -python3 scripts/surface_scan.py --text +python3 /scripts/surface_scan.py --text ``` -Run it from this skill's folder (the directory holding this SKILL.md). Write -pasted text to a temp file first. For the full JSON, drop `--text`. Under 80 -words, skip this step; the statistics are noise. The scanner strips code, links, -URLs, and heading markers from Markdown before measuring. +`` is the directory holding this SKILL.md; keep the working +directory on the user's project so `` resolves. Pasted text goes to a +temp file first; drop `--text` for full JSON. Under 80 words, skip this step. **Non-text sources** (`.docx`, `.pdf`, `.pptx`, `.odt`, `.rtf`): the scanner reads plain text only. Extract first with the harness's document skills (Anthropic's @@ -92,9 +91,10 @@ Walk every loaded tell list. For each tell you judge present, record: - the base rate line from the reference, or the scan number Rank by strength of evidence. Report **at most ten**. For texts under ~300 -words, quote raw counts from `punct.counts`, not per-1k rates. Quote -`repetition.phrases` and `grammar.*.hits` verbatim. `nominalization.hits` -never become a row. Other studies' ratios never go in the base-rate column. +words, quote raw counts from `punct.counts`, not per-1k rates. Every scanner +block that fired (`repetition.phrases`, each `grammar.*` with hits, the +disclaimer opener) gets its own row quoting the hits verbatim. `nominalization` +hits never become a row; other studies' ratios never go in the base-rate column. Format: ``` diff --git a/tools/smoke_harnesses.sh b/tools/smoke_harnesses.sh index 9f5f8d6..e036afe 100755 --- a/tools/smoke_harnesses.sh +++ b/tools/smoke_harnesses.sh @@ -16,8 +16,13 @@ OUT="$ROOT/docs/acceptance/v0.3" REQUEST="Use the humanize skill on $FIXTURE. Audit only — do not rewrite." mkdir -p "$OUT" -# Expected rows (case-insensitive regex) and two scanner numbers that must appear verbatim. -ROWS=('trailing participial clause' 'verbatim repetition' 'container-noun phrase' 'safety disclaimer opener') +# Expected rows (case-insensitive regex, matched by concept rather than by our own +# reference titles) and two scanner numbers that must appear verbatim. CONTAINER_ALT is +# derived from the scanner's own container_of hits on the fixture so a model that quotes +# the hit text verbatim instead of the word "container" still matches. +CONTAINER_ALT="$(python3 "$ROOT/skills/humanize/scripts/surface_scan.py" "$ROOT/$FIXTURE" \ + | python3 -c 'import json,sys; h=json.load(sys.stdin)["grammar"]["container_of"]["hits"]; print("|".join(x["text"].lower() for x in h))')" +ROWS=('participial' 'repetit|repeated' "container|${CONTAINER_ALT:-container}" 'disclaimer') SCAN="$(python3 "$ROOT/skills/humanize/scripts/surface_scan.py" --text "$ROOT/$FIXTURE")" NUM_TAILS="$(printf '%s\n' "$SCAN" | sed -n 's/^grammar: participial tails \([0-9]*\).*/\1/p')" NUM_REP="$(printf '%s\n' "$SCAN" | sed -n 's/^repetition: \([0-9.]*\)\/1k.*/\1/p')" @@ -103,7 +108,11 @@ for line in open(src, encoding="utf-8"): # is deliberately dropped here — it must land nowhere. output.append(blk.get("text", "")) if ev.get("type") == "result" and ev.get("result"): - output.append(ev["result"]) + # stream-json's final top-level "result" event often repeats the last assistant + # text block already collected above; only append it when it actually differs, + # or the final table gets duplicated in ## Output. + if not output or output[-1] != ev["result"]: + output.append(ev["result"]) open(dst, "w", encoding="utf-8").write( "# Claude Code\n\n## Tool calls\n" + "\n".join(calls) + "\n\n## Scanner output\n" + "\n".join(scanner) + @@ -116,6 +125,7 @@ EOF run_codex() { command -v codex >/dev/null || { echo "SKIP codex (not installed)"; return; } local tmp f="$OUT/codex.md"; tmp="$(mktemp -d)" + [ -n "$tmp" ] && [ -d "$tmp" ] || { echo "FAIL codex (mktemp failed)"; return; } mkdir -p "$tmp/.agents/skills" && cp -R "$ROOT/skills/humanize" "$tmp/.agents/skills/" && cp "$ROOT/$FIXTURE" "$tmp/" (cd "$tmp" && codex debug prompt-input "hi" 2>/dev/null | grep -qi humanize) || echo " warning: skill not listed by codex debug prompt-input" (cd "$tmp" && codex exec -C "$tmp" --skip-git-repo-check --ephemeral -s read-only --json \ @@ -177,6 +187,7 @@ run_cursor() { command -v agent >/dev/null || { echo "SKIP cursor (agent CLI not installed)"; return; } agent --help 2>/dev/null | grep -qE -- '(^| )-p[ ,]|--print' || { echo "SKIP cursor (agent $(agent --version 2>/dev/null | head -1) has no headless flag)"; return; } local tmp f="$OUT/cursor.md" raw; tmp="$(mktemp -d)" + [ -n "$tmp" ] && [ -d "$tmp" ] || { echo "FAIL cursor (mktemp failed)"; return; } mkdir -p "$tmp/.cursor/skills" && cp -R "$ROOT/skills/humanize" "$tmp/.cursor/skills/" && cp "$ROOT/$FIXTURE" "$tmp/" # Deviation from the brief: the installed `agent` CLI's approvalMode is `allowlist` # with only Shell(ls) allowed, so a headless run cannot execute the scanner From 3c30d2ee43a34ee1a7a7aff3e4449149527361df Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 11:42:45 -0400 Subject: [PATCH 17/22] Smoke matrix: re-enable trap; Hermes runs from the repo root Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- tools/smoke_harnesses.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tools/smoke_harnesses.sh b/tools/smoke_harnesses.sh index e036afe..4e7817a 100755 --- a/tools/smoke_harnesses.sh +++ b/tools/smoke_harnesses.sh @@ -60,9 +60,12 @@ run_claude() { command -v claude >/dev/null || { echo "SKIP claude (not installed)"; return; } local f="$OUT/claude-code.md" claude plugin disable humanize@humanize >/dev/null 2>&1 || true + # A ^C or kill mid-run must not leave the user's installed plugin disabled. + trap 'claude plugin enable humanize@humanize >/dev/null 2>&1 || true' EXIT INT TERM (cd "$ROOT" && claude -p "$REQUEST" --plugin-dir . --output-format stream-json --verbose \ --allowedTools "Bash,Read,Glob,Grep" > "$f.jsonl" 2>&1) claude plugin enable humanize@humanize >/dev/null 2>&1 || true + trap - EXIT INT TERM local auth_line auth_line="$(check_auth < "$f.jsonl")" if [ -n "$auth_line" ]; then @@ -219,10 +222,12 @@ run_hermes() { local dest="$HOME/.hermes/skills/writing/humanize" f="$OUT/hermes.md" raw [ -e "$dest" ] && { echo "SKIP hermes ($dest already exists; not touching it)"; return; } mkdir -p "$(dirname "$dest")" && cp -R "$ROOT/skills/humanize" "$dest" + # $REQUEST names the fixture by a project-relative path; run from $ROOT so it resolves + # no matter which directory this script itself is invoked from. if hermes --help 2>/dev/null | grep -q -- ' -z'; then - raw="$(hermes -z "$REQUEST" 2>&1)" + raw="$(cd "$ROOT" && hermes -z "$REQUEST" 2>&1)" else - raw="$(hermes chat -q "$REQUEST" -Q 2>&1)" + raw="$(cd "$ROOT" && hermes chat -q "$REQUEST" -Q 2>&1)" fi rm -rf "$dest" local auth_line From e5768820c5a17ad9f673a373312ba895bc3a2e2e Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 11:49:53 -0400 Subject: [PATCH 18/22] Mark the v0.3 design implemented Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- docs/design/2026-09-14-humanize-v0.3-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/2026-09-14-humanize-v0.3-design.md b/docs/design/2026-09-14-humanize-v0.3-design.md index f127f78..85b524f 100644 --- a/docs/design/2026-09-14-humanize-v0.3-design.md +++ b/docs/design/2026-09-14-humanize-v0.3-design.md @@ -1,7 +1,7 @@ # humanize v0.3 — portable skill (design) Date: 2026-09-14 -Status: draft for review (revision 2). Revision 1 had a three-lens adversarial review (harness +Status: implemented on feat/v0.3 as 0.3.0 (revision 2 + post-review amendments); phased acceptance per §7 — pre-merge items recorded in docs/acceptance/v0.3/. Revision 1 had a three-lens adversarial review (harness facts, repo mechanics, tests/acceptance): 3 blockers, 16 important, 2 minor, all adjudicated below; a `claude plugin validate` dry run and a full pytest run on a rearranged scratch copy confirmed the layout. From b69a1697ad2c36cb899a8c9dcf35ece5e1a70651 Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 12:11:49 -0400 Subject: [PATCH 19/22] Smoke matrix: distinct-row concept matching, traps, scanner-only Claude evidence - judge() now matches the four row concepts against the third |-separated field (the Tell cell) of distinct table rows under ## Output, and requires four distinct matching rows; drops the exploitable whole-row grep and the CONTAINER_ALT pipeline (I1). - Guard against a vacuous PASS if the scanner's summary wording ever drifts and NUM_TAILS/NUM_REP come back empty (I3). - Every harness's temp install/stream directory is now protected by an EXIT/INT/TERM trap pair, so a ^C or kill mid-run can't leave the Claude plugin disabled, a Hermes copy stuck in $HOME, or a Codex/Cursor temp project behind (I2, I4). - The Claude Code raw stream now writes to a mktemp -d directory instead of docs/acceptance/, and is removed after parsing (I5). - The Claude parser now mirrors the Codex one: a tool_use only becomes evidence when its serialized input names surface_scan.py, and a tool_result only counts when it answers such a call by tool_use_id. A read of a reference file no longer satisfies the tool-evidence rule or leaks into ## Scanner output (C2). - Minor cleanup: dedupe Codex's out.md text against the last agent_message (mirroring Claude's existing dedupe), anchor check_auth's alternatives, strip __pycache__ from each harness's copied skill, and guard against an unknown harness argument. Re-ran `tools/smoke_harnesses.sh claude codex`: PASS claude-code, PASS codex on the first attempt; transcripts regenerated. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- docs/acceptance/v0.3/claude-code.md | 783 ++-------------------------- docs/acceptance/v0.3/codex.md | 29 +- tools/smoke_harnesses.sh | 96 +++- 3 files changed, 139 insertions(+), 769 deletions(-) diff --git a/docs/acceptance/v0.3/claude-code.md b/docs/acceptance/v0.3/claude-code.md index 8ffcd08..1c65550 100644 --- a/docs/acceptance/v0.3/claude-code.md +++ b/docs/acceptance/v0.3/claude-code.md @@ -1,602 +1,16 @@ # Claude Code ## Tool calls -{ - "skill": "humanize:humanize", - "args": "tests/fixtures/ai_report.txt --audit-only" -} -{ - "command": "cat skills/humanize/references/principles.md && echo ===== && cat tests/fixtures/ai_report.txt && echo ===== && wc -w tests/fixtures/ai_report.txt", - "description": "Read principles reference and the fixture text" -} -{ - "command": "cat tests/fixtures/ai_report.txt; printf '\\n-----\\n'; wc -w tests/fixtures/ai_report.txt", - "description": "Read the fixture text and count words" -} -{ - "command": "cat skills/humanize/references/surface-tells.md", - "description": "Read surface tells reference" -} -{ - "command": "cat skills/humanize/references/style-tells.md", - "description": "Read style tells reference" -} { "command": "python3 skills/humanize/scripts/surface_scan.py --text tests/fixtures/ai_report.txt", "description": "Run the surface scanner on the fixture" } { - "command": "python3 skills/humanize/scripts/surface_scan.py tests/fixtures/ai_report.txt | python3 -c \"\nimport json,sys; d=json.load(sys.stdin)\nfor k in ('discourse','grammar','repetition','structures','nominalization'):\n print(k, json.dumps(d.get(k), indent=1)[:2500]); print()\n\"", - "description": "Dump the fired scanner blocks with verbatim hits" + "command": "python3 skills/humanize/scripts/surface_scan.py tests/fixtures/ai_report.txt | python3 -c '\nimport json,sys\nd=json.load(sys.stdin)\nfor k in (\"grammar\",\"discourse\",\"structures\",\"repetition\",\"sentence_len\",\"paragraph_len\"):\n print(k, json.dumps(d.get(k), indent=1)[:3000])\n'", + "description": "Print scanner JSON blocks with verbatim hits" } ## Scanner output -Launching skill: humanize:humanize -Exit code 1 -# Principles - -Read this before every audit or rewrite. - -1. **Disperse, don't converge.** StoryScope's central result: AI writing clusters in - a shared region of stylistic and narrative space; human writing spreads out. - A tell is a *default* the author didn't choose. The fix is a choice, not a - different default. If every rewrite you produce would look alike, you have - built a new cluster. - -2. **Preserve voice and meaning.** Every fact, name, number, claim, and position - survives. Register survives: a terse engineer stays terse, a warm note stays - warm. You are removing tells, not imposing taste. - -3. **Fix only what fired.** The audit names specific tells with quoted evidence. - Rewrite those. Leave everything else exactly as it was, including things you - would have written differently. - -4. **Removals are safer than additions.** Cutting an unearned epilogue, a stacked - metaphor, or a summary paragraph rarely misrepresents the author. Adding a - joke, a brand name, or a flashback can. Reference entries tag each fix as - `removal`, `addition`, or `rebalance`; make additions only when the inferred - voice would plausibly do that, and say so in the report. - -5. **Numbers are evidence, not verdicts.** Base rates come from a fiction corpus. - Scan metrics are counts. A text can be entirely human and still show three - tells; a text can show none and be generated. Report what fired and why a - reader would notice. Never state or imply that the result is undetectable, - passes a detector, or is "certified human." - Check the direction before you flag it. Findings expire — lexical diversity - reversed between GPT-3.5 and GPT-4 (Herbold et al. 2023 [herbold-2023]). - Some never held — GPT-4o used agentless passives at about half the human - rate (Reinhart et al. 2025 [reinhart-2025], 2024-era models), and 29 of 32 - model settings moved away from the dimension that carries passives (Milička - et al. 2025 [milicka-2025]: a factor loading, not a passive count). Reader - heuristics point backwards (Jakesch et al. 2023 [jakesch-2023], GPT-3-era - self-presentation bios): contractions read as human but lean AI; grammar - errors and long or rare words read as AI but lean human. Prefer recency for - capability-dependent features, replication for stable ones — and never - optimize for what a reader guesses is human. - -6. **Ask rarely.** Infer register, audience, and intent from the text and the - conversation. Ask one question only when a rewrite decision genuinely hinges - on it and you cannot tell from context. - -7. **Variance is the tool.** Vary sentence length. Let a plain sentence stay - plain. Name an emotion once instead of embodying it again. Stop at the climax. - Let one paragraph be a single line. Let a reference be specific. Each of these - is a departure from the AI default; none is a new rule. - -8. **Register and proficiency are not tells.** The measured AI profile — formal, - impersonal, nominalized, flat sentence lengths, narrow lexis, few - contractions — also describes competent second-language English, translated - text, legal, technical, academic, and plain-language prose. That overlap is - this repo's inference from the corpora below, not a finding any of them - tests. Measure the profile; never infer authorship or proficiency from it, - and never rewrite a text into looking less like one of those populations. - Each human baseline here comes from one narrow population — StoryScope: - amateur fiction; Muñoz-Ortiz: NYT lead paragraphs; Herbold: non-native - student essays; Jakesch: short bios — whose own limitations decline to - generalize. All of it is English; quote no number on translated or - non-English text. -(eval):1: ==== not found -It's important to approach this update carefully, since several workstreams changed scope during the quarter. I'm not able to share vendor pricing here, but the overall picture is encouraging and the risks are manageable. - -The implementation of the new ingestion pipeline finished two weeks behind the original estimate, reflecting the late discovery of schema drift in the partner feed. The team resolved the drift by introducing a validation layer, ensuring alignment across teams before any record reaches the warehouse. Throughput now sits at roughly nine thousand events per minute, exceeding the target we set in March. - -Adoption of the shared design system continued across all workstreams and teams, creating a sense of momentum that was missing last quarter. Four product surfaces migrated to the new components, allowing designers to retire eleven legacy patterns. The migration of the billing screens remains in progress, pending the completion of an accessibility review. - -On the reliability side, the optimization of our alerting rules reduced paging volume by about a third. Engineers consolidated forty-two overlapping alerts into nine, giving on-call staff a clearer signal during incidents. Mean time to acknowledge fell from eleven minutes to four, matching the level the platform group had proposed. - -Customer support handled a spike in tickets after the pricing change, resolving most of them within the first business day. The knowledge base articles were rewritten in plainer language, and the deflection rate climbed from thirty to forty-one percent. A small group of enterprise accounts asked for a dedicated onboarding call, and account managers have scheduled those for the second week of the month. - -Hiring closed on three of the five open roles, leaving two senior positions unfilled going into the next cycle. The weight of the decision to pause backfills fell mostly on the data platform group. We expect the remaining offers to close by mid-month, assuming the compensation adjustments are approved. - -Coordination across all workstreams and teams improved once the weekly sync moved to a written format. Fewer meetings meant more focused work, and the written record made the escalation of blockers faster to trace. Product managers reported that the transformation of the roadmap into quarterly themes made prioritization discussions shorter. - -Security completed the remediation of the findings from the spring audit, closing every high-severity item ahead of schedule. Two medium items remain open, awaiting a library upgrade that the vendor has scheduled for next month. The compliance team confirmed that the evidence collection process now runs automatically each week. - -Documentation for the public API moved to the new site, giving external developers a searchable reference for the first time. Traffic to the reference pages doubled in the first month, and the volume of questions in the developer forum dropped noticeably. The technical writers also produced a migration guide for teams still on the deprecated endpoints. - -Looking ahead, the integration of the analytics events into the new pipeline is the main dependency for the reporting launch. The team plans to finish the mapping by the end of the month, leaving three weeks for validation with finance. Budget remains within the approved envelope, and no additional headcount is requested at this time. - -Finance reviewed the quarterly forecast with each product area and found no material variance against the plan. The revised allocation model landed in the reporting tool last week, and cost centre owners can now see their own numbers without filing a ticket. Two departments asked for a shorter reporting cycle, and that request is under review. - -In summary, delivery across all workstreams and teams stayed on plan despite the schema issue, and the quarter closes with fewer open risks than it opened with. Please raise any concerns before Friday so they can be folded into the planning session. - ------ - 620 tests/fixtures/ai_report.txt -# Surface tells - -The layer StoryScope does not measure: vocabulary, grammar, punctuation, -sentence and paragraph shape, and discourse moves. Applies to every text -class. Each entry names the `surface_scan.py` metric that measures it where -one exists. Ranges marked *rule of thumb* are working heuristics from -practice, not measured in the StoryScope corpus. Numbers that are not -StoryScope base rates sit on `Scan:` or `Rule of thumb:` lines with a `[key]` -that resolves in `SOURCES.md`. - -## Vocabulary - -### AI-associated wordlist -Looks like: "delve", "tapestry", "a testament to", "navigate the complexities", -"it's worth noting", "leverage", "robust", "seamless", "crucial", "pivotal", -"foster", "underscore", "multifaceted", "landscape", "vibrant", "nuanced", -"meticulous", "harness", "synergy", "holistic", "streamline", "elevate", -"empower", "unlock", "resonate", "realm", "beacon", "unwavering". -Scan: `wordlist.rate` per 1k words and `wordlist.hits` with sentence positions. -Rule of thumb: human drafts usually < 3/1k; AI drafts commonly 10–30/1k in -long-form prose and can exceed 60/1k in short business emails, where -boilerplate dominates. -Vintage: calibrated on 2023–2024 model output. A wordlist decays — Kobak et -al. 2025 [kobak-2025] show marker words rising and falling with model -generations (share of biomedical abstracts containing the word, not a per-1k -rate; not comparable to the rule of thumb above). Re-check against current -models before firing hard. -Why it reads as AI: these words are over-represented in RLHF-era model output -and under-represented in ordinary human prose of the same register; readers -have learned the list. -Fix: removal — replace with the plain word the author would use ("use" for -"leverage", "strong" for "robust", "important" for "crucial") or cut the word -entirely; most are decorative. - -### Latinate lean -Looks like: "utilize", "facilitate", "demonstrate", "commence", "implement", -"ascertain" where "use", "help", "show", "start", "do", "find out" would do. -Base rate: AI 2.83 / human 2.51 on a 1–5 Anglo-Saxon→Latinate scale -(StoryScope STY_ALL_016). -Why it reads as AI: models default to the formal register of their training -mass; humans pick the short word unless the register demands otherwise. -Fix: rebalance — swap to the short Germanic word where the voice is not formal. - -### Nominalized verbs -Looks like: "the implementation of the policy led to an improvement in -retention" where "implementing the policy improved retention" would do; "the -X of" frames stacked through a paragraph. -Scan: `nominalization.hits` and `nominalization.of_frames` — hits only, no -rate, no threshold; singular and plural forms are separate hits (Herbold et -al. 2023 [herbold-2023]; Reinhart et al. 2025 [reinhart-2025]). -Why it reads as AI: buried verbs rise monotonically across model generations, -but formal, legal, academic, and second-language prose nominalize legitimately -— in `expository` text these hits are a prompt to look, never a table row. -Fix: rebalance — unbury the verb where the register does not earn the noun. - -### Abstract container-noun phrase -Looks like: "a sense of unease", "a mix of pride and fear", "the weight of the -decision" — an abstract container standing in for the concrete thing. -Scan: `grammar.container_of` count and hits; the 13 heads are those attested -in LAMP Table 8 (Chakrabarty et al. 2025 [chakrabarty-2025]), rare in the -human seed paragraphs. -Why it reads as AI: a reflex reach for an abstraction where a human names the -object or the feeling; fiction uses these legitimately, so judge density. -Fix: removal — name the concrete thing, or cut the frame and keep the noun. - -### Hedge stacks -Looks like: "It could perhaps be argued that this might, to some extent, -generally be the case." -Scan: `hedges.rate` per 1k. Rule of thumb: > 10/1k in expository prose is -a stack. -Why it reads as AI: models hedge to avoid being wrong; a person with a view -states it and hedges once, if at all. -Fix: removal — keep at most one hedge per claim; delete the rest. - -### Intensifier stacks -Looks like: "truly remarkable", "deeply meaningful", "incredibly important", -"genuinely transformative", several per paragraph. -Scan: `intensifiers.rate` per 1k. Rule of thumb: > 8/1k reads as padding. -Why it reads as AI: intensifiers substitute for specifics; humans intensify -rarely and usually for effect. -Fix: removal — delete the intensifier or replace the phrase with a concrete -detail that earns the emphasis. - -## Punctuation - -### Em-dash density -Looks like: "The plan—while ambitious—was sound—and it worked." -Scan: `punct.em_dash` per 1k. Rule of thumb: human nonfiction 0–4/1k; AI -drafts often 8–20/1k. Per-1k rates need roughly 300+ words to mean anything; -below that, cite the raw count (`punct.counts.em_dash`) and treat one or two -marks as noise. CLI flags like `--audit-only` are not counted; only `word--word` -or spaced ` -- ` forms count as an em-dash. -Why it reads as AI: models use the em-dash as a universal joiner where a human -would use a comma, a period, or parentheses, and they use it in every paragraph. -Fix: rebalance — keep one em-dash where it does real work; convert the rest to -periods (usually) or commas. - -### Semicolon and colon habits -Looks like: semicolons joining independent clauses in casual prose; colons -introducing a clause that restates the previous one. -Scan: `punct.semicolon`, `punct.colon` per 1k. -Why it reads as AI: semicolons in a text message or casual email are rare for -humans; colon-led restatement is a summarizing tic. -Fix: rebalance — in casual registers, split into two sentences. - -## Structures - -### Tricolon habit (rule of three) -Looks like: "fast, reliable, and secure"; "we build, we ship, we learn"; every -list has exactly three items. -Scan: `structures.tricolon` count. Rule of thumb: more than one per 150 words -is a habit, not a choice. Counts any `A, B, and C` sequence, including clause -joins; treat the number as a prompt to look, not a verdict. -Why it reads as AI: the three-item list is rhythmically satisfying and the model -reaches for it reflexively; humans produce two- and four-item lists as often. -Fix: rebalance — cut one item, add a fourth, or make one item a sentence of its -own. Keep a tricolon only where the rhythm is the point. - -### Not-X-but-Y framing -Looks like: "It's not about the code, it's about the culture." "This isn't a -setback—it's an opportunity." "Not only did we ship, but we learned." -Scan: `structures.not_but` count. -Why it reads as AI: a contrast frame that manufactures insight by negating a -strawman; models use it to sound reflective. -Fix: removal — state Y directly. Delete the negated X unless someone actually -claimed it. - -### Rhetorical question then answer -Looks like: "So what does this mean for teams? It means…" "Why does this matter? -Because…" -Scan: `structures.rhetorical_q` count (outside dialogue). -Why it reads as AI: a transition device that simulates dialogue with the reader; -humans use it sparingly and usually with an edge. -Fix: removal — delete the question; keep the answer as a statement. - -### Parallel sentence openers -Looks like: three or more consecutive sentences beginning with the same word -("We… We… We…", "It… It… It…"). -Scan: `structures.parallel_openers` (runs of ≥3) and `openers.distinct_ratio`. -Rule of thumb: distinct-opener ratio below 0.6 in prose longer than 15 sentences -is monotonous. -Why it reads as AI: anaphora is a deliberate rhetorical figure; unintentional -anaphora is a generation artifact. -Fix: rebalance — vary the openers; combine two of the sentences. - -### Verbatim repetition -Looks like: a phrase of four or more words reappearing intact across the piece -— "across all workstreams and teams" three times in a status report — or a -string lifted from the prompt or title. -Scan: `repetition.phrases` (silent under 150 words); repeated phrases are the -strongest true-source predictor readers miss, OR 1.47 (Jakesch et al. 2023 -[jakesch-2023]); `repetition.repeated_phrase_rate` is reported-only. -Why it reads as AI: recurrence with no rhetorical intent; terminology, names, -and identifiers must repeat — exempt technical and legal prose — and a refrain -in fiction is deliberate. -Fix: removal — keep one instance and vary or cut the rest. - -### Trailing participial clause -Looks like: a finished sentence that keeps going after a comma with an -ing -verb: ", ensuring seamless integration", ", allowing teams to move faster", -", highlighting the importance of". -Scan: `grammar.participial_tail` count, rate, and hits; ratio 5.3×, d = 1.38, -2024-era models, news and academic registers (Reinhart et al. 2025 -[reinhart-2025]). -Why it reads as AI: the tack-on lets a sentence add a consequence without a -new subject, and models reach for it several times a paragraph. -Fix: removal — split into a sentence with its own subject, or drop the clause. - -### Uniform sentence length -Looks like: every sentence 14–20 words; no fragments; no 40-word sentence. -Scan: `sentence_len.cv` (stdev/mean); `sentence_len.pct_over_30` (humans -31.2% vs 17.5–21.0%, 2023 news corpus, direction not magnitude; Muñoz-Ortiz et -al. 2024 [munoz-ortiz-2024]); `sentence_len.longest_flat_run` (reported-only). -A flat profile is also the native shape of plain-language and technical prose -— `tests/fixtures/human_plain.txt` sits in AI territory on every sentence -metric — and is not authorship evidence. -Rule of thumb: published human prose commonly 0.5–0.9; AI drafts often below -0.4. -Why it reads as AI: models regress to the mean sentence; humans write in bursts. -Fix: rebalance — split one long sentence into a short one and a fragment; merge -two mid-length sentences into a long one. Aim for range, not a target. - -### Uniform paragraph length -Looks like: every paragraph three to four sentences; every paragraph opens with -a topic sentence and closes with a mini-conclusion. -Scan: `paragraph_len.cv`. Rule of thumb: below 0.3 across five or more -paragraphs is uniform. -Why it reads as AI: the five-paragraph-essay template applied to everything. -Fix: rebalance — allow a one-sentence paragraph; let one paragraph run long. - -## Discourse moves - -### Validating opener -Looks like: "Great question!" "I'd be happy to help." "Absolutely!" "That's a -really insightful point." -Scan: `wordlist.hits` includes "great question", "i hope this helps". -Why it reads as AI: assistant-style acknowledgement before content; humans -answer. -Fix: removal — start with the content. - -### Restating the prompt -Looks like: the first paragraph paraphrases the question or task before -addressing it. -Scan: none; judge by reading. -Why it reads as AI: models anchor by echoing input; humans assume the reader -remembers what they asked. -Fix: removal — delete the paraphrase. - -### Summary closer -Looks like: a final paragraph opening "In conclusion", "Ultimately", "Overall", -"In short" that restates the opening. -Scan: `discourse.summary_closer` (boolean). -Why it reads as AI: essay-template closure on texts that do not need it; humans -end when they are done. -Fix: removal — cut the paragraph, or end on the last concrete point. - -### Sign-off advice and offers -Looks like: "Remember to…", "Feel free to reach out", "Don't hesitate to…", -"I hope this helps!" -Scan: `wordlist.hits` includes "reach out", "don't hesitate", "i hope this helps". -Why it reads as AI: assistant boilerplate. -Fix: removal — end with the actual last thing you have to say, or a plain sign-off. - -### Safety disclaimer opener and AI self-reference -Looks like: a first paragraph that qualifies before it answers — "It's -important to approach this carefully", "I'm not able to give specific advice, -but", "consult a professional" — or any "As an AI" self-reference. -Scan: `discourse.disclaimer_opener.fired` and `.hits`; per-family range 46% -vs 0.2% of responses (Rudnicka & Juzek 2026 [rudnicka-2026]). -Why it reads as AI: assistant safety framing on a text that asked for none; -the same phrases mid-document are an ordinary discourse observation. -Fix: removal — start with the answer. - -### Headings and bullets in short pieces -Looks like: a 200-word email with three bold headers and two bulleted lists. -Scan: none; judge by reading. -Why it reads as AI: structure imposed regardless of length or medium. -Fix: removal — prose for anything under ~300 words unless the medium expects -lists. -# Style tells - -StoryScope's Style dimension (figurative language, sound, syntax, register, -tone, allusion), filtered to the 20 features with a human-vs-AI gap of at least -15 points (categorical) or 0.30 (1–5 scale). Base rates are measured on 61,575 -stories; see `data/README.md`. These apply to every text class; each entry says -how it shows up outside fiction. - -Entries are ordered by the size of the human-vs-AI gap, largest first. - -### Presence of extended conceit -Looks like: a metaphor that is introduced and then developed across several -sentences or the whole piece — the company as a ship, grief as a house with -rooms, the codebase as a garden — with each paragraph extending it. -Base rate: AI 83% / human 40% (StoryScope STY_FIG_004) -Why it reads as AI: models sustain a governing metaphor because it is a -coherence strategy; most human writers drop a figure after one use. -Fix: removal — keep the first instance if it earns its place; cut every later -callback to the conceit and say the literal thing instead. -Outside fiction: the "journey" or "building blocks" frame that runs through an -entire blog post or team update. - -### Lexical register and consistency -Looks like: a piece that stays in one register throughout — uniformly elevated, -or uniformly neutral-standard — with no slang, no shift to plain talk, no -sudden formal aside. -Base rate: mixed register with code-switching — AI 19% / human 56% (StoryScope STY_ALL_015) -Why it reads as AI: humans slip between registers as mood and audience shift -mid-text; models hold a single register as a consistency default. -Fix: addition — only when in character: let one sentence go colloquial, or let -a plain paragraph be interrupted by a precise technical term. Do not sprinkle -slang mechanically. -Outside fiction: an email that never once says "yeah", "honestly", or "ugh" -from a writer who would. - -### Sound Patterning Prominence -Looks like: alliteration and assonance you notice while reading — "the slow -slide of silt", "a hollow, swallowing hush" — recurring through the piece rather -than once at a deliberate moment. -Base rate: noticeable sound patterning — AI 91% / human 55% (StoryScope STY_TON_006) -Why it reads as AI: models reach for euphony by default, so the ear-pleasing -version of each phrase is the one that survives, while most human prose is -sonically flat. -Fix: removal — keep the one sound effect that is placed for a reason and de-tune -the rest by swapping in the word you would have chosen without the ear in mind. -Outside fiction: a product page where every feature name alliterates, or a -headline picked for sound over accuracy. - -### Conventional vs fresh figurative language -Looks like: every image is a new one — "the grief sat in her like a stone -swallowed sideways" — and no stock phrase appears anywhere in the piece. -Base rate: predominantly fresh and inventive images — AI 65% / human 30% (StoryScope STY_FIG_003) -Why it reads as AI: the direction is counter-intuitive — models are more -inventive than humans, not more clichéd, and the tell is relentless invention -with no ordinary sentence between the images. -Fix: removal — cut most of the fresh images and let plain statement carry the -passage; a stock phrase in the right place is a human signal, not a failure. -Outside fiction: an essay where every paragraph opens with a new and unusual -comparison instead of stating the point. - -### Predominant tonal quality -Looks like: earnestness throughout — nothing is undercut, no line is wry, and -the piece means everything it says at face value. -Base rate: earnest or lyrical tone — AI 71% / human 40% (StoryScope STY_TON_021) -Why it reads as AI: sincerity is the safe default for a model that cannot -predict whether a joke will land. -Fix: removal — take the earnestness off at least one high point: undercut a -grand line, let a character be unimpressed, or state the thing flatly. -Outside fiction: a launch post that is visibly moved by its own product, with no -dry aside anywhere in it. - -### Sentence-structure repertoire -Looks like: balanced series as the default sentence shape — "It changed how we -build, how we ship, how we think" — paragraph after paragraph. -Base rate: frequent parallel or list-like structures — AI 99% / human 70% (StoryScope STY_CPX_012) -Why it reads as AI: this is near-universal in AI text, because parallelism is -the cheapest way to sound composed and models apply it by reflex. -Fix: removal — break the series: cut one limb, make one limb a sentence of its -own, or replace the list with a single specific. See the tricolon habit in -`surface-tells.md` for the surface-level check. -Outside fiction: bullet-shaped sentences in an email — "faster, cheaper, and -easier to maintain" — where one accurate clause would do. - -### Recurrent metaphorical motif -Looks like: one image family threaded through the whole piece — weight, tide, -machinery — so that unrelated paragraphs reach for the same register of -comparison. -Base rate: motif present — AI 96% / human 69% (StoryScope STY_FIG_005) -Why it reads as AI: a recurring motif is a coherence device the model applies -across the whole text, while human writers rarely track their images that far. -Fix: removal — keep the motif's single strongest appearance and rewrite the -others from whatever the local scene or paragraph actually offers. -Outside fiction: a strategy memo where everything is weather, momentum, or -plumbing from the first line to the last. - -### Allusion domain diversity -Looks like: references that stay in the safe canon — myth, history, literature — -and never a named band, film, product, or team. -Base rate: pop-culture or brand-name allusions — AI 13% / human 40% (StoryScope STY_ALL_018) -Why it reads as AI: models avoid naming real commercial and cultural artifacts, -so their references land in a timeless, unplaceable nowhere. -Fix: addition — only when in character: name the actual thing the writer would -name, the specific show or product, instead of "a popular streaming series". -Outside fiction: a post about consumer software that never names a competitor. - -### Use of irony and humor -Looks like: nothing in the piece is meant other than literally — no joke, no -sarcasm, no dry undercut anywhere in it. -Base rate: any discernible humor or irony (inverting the straight-faced value) — -AI 62% / human 88% (StoryScope STY_TON_023) -Why it reads as AI: humor is the highest-risk register for a model, so playing -everything straight is the safe move. -Fix: addition — only when in character: one dry line where the writer would -actually be dry. Do not insert jokes into prose whose author has none. -Outside fiction: an incident report or team update with not one wry remark, from -someone whose speech is full of them. - -### Dominant Tonal Register -Looks like: a narrative voice that lingers, reflects, and finds significance, -rather than one that is reportorial, hardboiled, or analytic. -Base rate: lyrical or meditative register — AI 77% / human 52% (StoryScope STY_TON_001) -Why it reads as AI: the reflective register is the model's comfortable default -and gets applied whatever the material is. -Fix: removal — cut the reflective sentences and let the reported facts stand, -then pick the register the material actually demands and hold it. -Outside fiction: a postmortem that keeps pausing to consider what the outage -meant for the team. - -### Dominant Figurative Device Type -Looks like: comparisons asserted rather than proposed — "the deadline was a -wall" rather than "the deadline felt like a wall" — with metaphor crowding out -simile and plain description. -Base rate: metaphor-dominant — AI 73% / human 50% (StoryScope STY_FIG_002) -Why it reads as AI: metaphor is the more literary of the two moves, and models -take the more literary move. -Fix: removal — convert some metaphors back to similes or to literal statement; -the mix, not the metaphor itself, is what reads human. -Outside fiction: business writing where processes are engines, funnels, and -flywheels instead of being described. - -### Parataxis vs Hypotaxis Preference -Looks like: syntax that never commits — some coordination, some subordination, -evenly mixed, with no run of blunt short clauses and no long embedded sentence. -Base rate: balanced parataxis and hypotaxis — AI 85% / human 64% (StoryScope STY_CPX_004) -Why it reads as AI: the balanced middle is the average of all training prose, -and averaging is what the model does. -Fix: removal — take out the balance: write one passage as a chain of short -coordinated clauses and another as a single long subordinated sentence. -Outside fiction: documentation where every sentence has the same clause count -regardless of how simple or complicated the step is. - -### Primary Function of Allusion -Looks like: a reference used to point at the moral — "like Icarus, we flew too -close" — rather than to color a setting or characterize whoever made it. -Base rate: allusion as theme-signposting or moral commentary — AI 46% / human 26% (StoryScope STY_ALL_004) -Why it reads as AI: models use references to make the point legible, where human -writers more often use them for texture, humor, or offhand analogy. -Fix: removal — delete the signposting reference, or repurpose it so it says -something about the person making it rather than about the theme. -Outside fiction: an essay that closes on a myth or a quotation restating the -argument. - -### Sound patterning devices -Looks like: alliteration specifically, as the salient device whenever the prose -pushes — "a steady, stubborn strain", "policy, practice, and posture". -Base rate: alliteration prominent — AI 98% / human 79% (StoryScope STY_TON_025) -Why it reads as AI: alliteration is the sound effect producible by word choice -alone, so it is the one models overuse. -Fix: removal — replace the alliterating word with the accurate one; if the -phrase survives only because it sounds good, cut the phrase. -Outside fiction: alliterative section headings and slogans in a deck or a -README. - -### Use of Sentence Fragments -Looks like: fragments for emphasis, several to a page. Short. Punchy. Like this. -Base rate: fragments present and stylistically significant — AI 85% / human 67% (StoryScope STY_CPX_003) -Why it reads as AI: the direction is counter-intuitive — AI fragments more than -humans do, because the one-beat emphatic fragment is a learned dramatic device, -not a sign of a loose human hand. -Fix: removal — rejoin most fragments into the sentences they broke off from, and -keep at most one where the break carries real emphasis. -Outside fiction: a social post where every third line is a one-word paragraph. - -### Voice Markedness -Looks like: a voice with flavor but no risk — a little rhythm, a few distinctive -turns, no dialect, no verbal tic, nothing a reader could imitate. -Base rate: moderately marked voice, distinct but not extreme — AI 98% / human 83% (StoryScope STY_TON_005) -Why it reads as AI: almost every AI passage lands in this middle band, while -human writing spreads out into plain report and into genuine idiosyncrasy. -Fix: removal — take the voice off the middle: either strip it back to plain -report, or commit to one real mannerism and carry it consistently. -Outside fiction: internal writing that sounds like a company rather than like a -person. - -### Figurative Device Density -Looks like: a metaphor or simile in most paragraphs, including in sentences that -had a job to do and did not need one. -Base rate: AI 3.66 / human 3.00 on a 1–5 scale (StoryScope STY_FIG_001) -Why it reads as AI: models decorate by default, and figurative density is the -most visible form that decoration takes. -Fix: rebalance — cut roughly a third of the figures, starting with those that -explain something already clear, and keep the ones doing work no literal -sentence could do. -Outside fiction: an explainer where every concept arrives with an analogy -attached to it. - -### Rhythmic markedness of prose -Looks like: cadence you can hear — balanced clauses, repeated openings, -sentences resolving on a stressed beat — sustained across the whole piece. -Base rate: AI 3.61 / human 3.17 on a 1–5 scale (StoryScope STY_TON_024) -Why it reads as AI: the model optimizes each sentence for fluency, and sustained -fluency turns into a metronome. -Fix: rebalance — break the meter: let one sentence end awkwardly, run another -past its natural stop, or drop a clause that exists only for balance. -Outside fiction: a speech-shaped post that reads as though written to be read -aloud when nobody will read it aloud. - -### Allusion density -Looks like: prose that refers to almost nothing outside itself — no cited text, -no dated event, no artifact a reader would recognize. -Base rate: AI 2.26 / human 2.59 on a 1–5 scale (StoryScope STY_ALL_017) -Why it reads as AI: models avoid specific external references because those are -the most checkable thing in a text, while humans reach for them constantly. -Fix: rebalance — add the reference the writer would actually have made, and -confirm it is real before keeping it. -Outside fiction: a memo that cites no paper, no prior project, and no names. - -### Latinate vs Anglo-Saxon lexical flavor -Looks like: "utilize", "facilitate", "commence", "demonstrate" where "use", -"help", "start", and "show" would carry the same sense. -Base rate: AI 2.83 / human 2.51 on a 1–5 scale (StoryScope STY_ALL_016) -Why it reads as AI: the formal register dominates the training mass, so the -learned word wins unless the context pushes hard the other way. -Fix: rebalance — swap in the short Germanic word wherever the register does not -require the long one; `surface-tells.md` carries the paired surface check. -Outside fiction: a chat message written in the register of a policy document. words 620 · sentences 34 · paragraphs 12 sentence length: mean 18.24, stdev 3.46, cv 0.19 (min 14, max 27) paragraph length: mean 2.83 sentences, stdev 0.39, cv 0.137 @@ -609,23 +23,6 @@ repetition: 3.2/1k · longest repeat 5 · "across all workstreams and teams"×3 grammar: participial tails 14 (22.6/1k) ", reflecting the late discovery of schema drift in the" · container-of 2 "a sense of" sentence tail: over-30 0.0% · p90 25 · longest flat run 8 nominalization hits: 24 (validation×2, migration×2, implementation×1) · frames: "the implementation of", "the migration of" -discourse { - "summary_closer": true, - "disclaimer_opener": { - "fired": true, - "hits": [ - { - "text": "it's important to approach", - "sentence": 0 - }, - { - "text": "i'm not able to", - "sentence": 1 - } - ] - } -} - grammar { "participial_tail": { "count": 14, @@ -687,7 +84,28 @@ grammar { ] } } - +discourse { + "summary_closer": true, + "disclaimer_opener": { + "fired": true, + "hits": [ + { + "text": "it's important to approach", + "sentence": 0 + }, + { + "text": "i'm not able to", + "sentence": 1 + } + ] + } +} +structures { + "tricolon": 1, + "not_but": 1, + "rhetorical_q": 0, + "parallel_openers": 0 +} repetition { "too_short": false, "repeated_phrase_rate": 3.2, @@ -704,134 +122,39 @@ repetition { } ] } - -structures { - "tricolon": 1, - "not_but": 1, - "rhetorical_q": 0, - "parallel_openers": 0 +sentence_len { + "mean": 18.24, + "stdev": 3.46, + "cv": 0.19, + "min": 14, + "max": 27, + "pct_over_30": 0.0, + "p90": 25, + "longest_flat_run": 8 } - -nominalization { - "count": 24, - "hits": [ - { - "text": "validation", - "count": 2 - }, - { - "text": "migration", - "count": 2 - }, - { - "text": "implementation", - "count": 1 - }, - { - "text": "ingestion", - "count": 1 - }, - { - "text": "alignment", - "count": 1 - }, - { - "text": "adoption", - "count": 1 - }, - { - "text": "completion", - "count": 1 - }, - { - "text": "optimization", - "count": 1 - }, - { - "text": "deflection", - "count": 1 - }, - { - "text": "decision", - "count": 1 - }, - { - "text": "compensation", - "count": 1 - }, - { - "text": "adjustments", - "count": 1 - }, - { - "text": "coordination", - "count": 1 - }, - { - "text": "escalation", - "count": 1 - }, - { - "text": "transformation", - "count": 1 - } - ], - "of_frames": [ - { - "text": "the implementation of", - "sentence": 2 - }, - { - "text": "the migration of", - "sentence": 7 - }, - { - "text": "the completion of", - "sentence": 7 - }, - { - "text": "the optimization of", - "sentence": 8 - }, - { - "text": "the escalation of", - "sentence": 18 - }, - { - "text": "the transformation of", - "sentence": 19 - }, - { - "text": "the remediation of", - "sentence": 20 - }, - { - "text": "the integration of", - "sentence": 26 - } - ] +paragraph_len { + "mean": 2.83, + "stdev": 0.39, + "cv": 0.137, + "min": 2, + "max": 3 } ## Output -I'll invoke the humanize skill and run an audit-only pass on the fixture. -Audit complete. Classified as `expository` (quarterly status report, 620 words, 34 sentences, 12 paragraphs). Scanner ran; the table below covers every block that fired. +I'll invoke the humanize skill in audit-only mode on the fixture. +Audit complete. Class: `expository` (a quarterly status report, 620 words, 34 sentences, 12 paragraphs). | # | Tell | Evidence | Base rate / metric | |---|------|----------|--------------------| -| 1 | Trailing participial clause | ", ensuring alignment across teams before any record reaches" (×14) | scan: 14 tails, 22.6/1k; AI 5.3× human, d = 1.38 [reinhart-2025] | -| 2 | Safety disclaimer opener | "It's important to approach this update carefully" / "I'm not able to share vendor pricing here, but" | scan: fired, 2 hits; 46% vs 0.2% of responses [rudnicka-2026] | -| 3 | Verbatim repetition | "across all workstreams and teams" (×3, sentences 5, 17, 32) | scan: 3.2/1k, longest repeat 5; OR 1.47 [jakesch-2023] | -| 4 | Uniform sentence length | every sentence 14–27 words; none over 30 | scan: cv 0.19, over-30 0.0%, flat run 8; rule of thumb: human 0.5–0.9, AI often < 0.4 | -| 5 | Uniform paragraph length | 12 paragraphs of 2–3 sentences each | scan: paragraph cv 0.137; rule of thumb: < 0.3 across 5+ paragraphs is uniform | -| 6 | Summary closer | "In summary, delivery across all workstreams and teams stayed on plan…" | scan: summary_closer true | -| 7 | Abstract container-noun phrase | "a sense of momentum", "The weight of the decision" (×2) | scan: container-of 2; LAMP Table 8 heads, rare in human seed paragraphs [chakrabarty-2025] | - -Notes on what did not make the table: - -- **Nominalization** is a prompt to look, not a row, in expository prose. The scanner logged 24 hits and eight "the X of" frames ("the implementation of", "the optimization of", "the remediation of"). For a status report this is native register, though eight frames in 620 words is on the heavy side. -- **Not-X-but-Y** counted 1, but the match is the "I'm not able to… but" opener already covered in row 2. -- **Tricolon** counted 1, well under the one-per-150-words rule of thumb. -- Wordlist, hedges, intensifiers, and every punctuation metric came back at zero. Opener diversity is 0.735, above the monotony threshold. -- No style-layer tell fired with enough evidence to rank. The register is plain report throughout, which a quarterly update earns, so tone and voice were not scored against it. - -Stopped after the audit as requested. No rewrite produced. +| 1 | Safety disclaimer opener | "It's important to approach this update carefully…", "I'm not able to share vendor pricing here, but…" (sentences 1–2) | scan: `discourse.disclaimer_opener` fired; per-family range 46% vs 0.2% of responses (Rudnicka & Juzek 2026) | +| 2 | Trailing participial clause | ", reflecting the late discovery of schema drift", ", ensuring alignment across teams", ", creating a sense of momentum", ", allowing designers to retire eleven legacy patterns" (×14) | scan: 14 tails, 22.6/1k; ratio 5.3×, d = 1.38 (Reinhart et al. 2025) | +| 3 | Verbatim repetition | "across all workstreams and teams" (×3, sentences 5, 17, 32) | scan: `repetition.phrases`; repeated phrases OR 1.47 (Jakesch et al. 2023); rate 3.2/1k reported-only | +| 4 | Uniform sentence length | every sentence 14–27 words; longest flat run 8; no sentence over 30 | scan: cv 0.19, pct_over_30 0% (humans 31.2% vs 17.5–21.0%, Muñoz-Ortiz et al. 2024); rule of thumb: human 0.5–0.9, AI often below 0.4 | +| 5 | Uniform paragraph length | 12 paragraphs, every one 2–3 sentences, each opening on a topic and closing on a status | scan: `paragraph_len.cv` 0.137; rule of thumb: below 0.3 across five or more paragraphs is uniform | +| 6 | Summary closer | "In summary, delivery across all workstreams and teams stayed on plan…" | scan: `discourse.summary_closer` yes | +| 7 | Abstract container-noun phrase | "a sense of momentum that was missing last quarter", "The weight of the decision to pause backfills" (×2) | scan: `grammar.container_of` 2; heads attested in LAMP Table 8 (Chakrabarty et al. 2025) | +| 8 | Predominant tonal quality (earnest, no undercut) | "the overall picture is encouraging and the risks are manageable"; no dry line in 620 words | earnest or lyrical tone: AI 71% / human 40% (StoryScope STY_TON_021); any humor or irony: AI 62% / human 88% (STY_TON_023) | + +Two notes on what I did not table. The scanner counted 24 nominalization hits ("the implementation of", "the migration of", "the optimization of", "the remediation of", "the transformation of"), but in report register those are native and never become a row. They are a prompt to look, and the density here is high enough that a rewrite would unbury several. The one `not_but` and one tricolon hit are below any threshold and I treated them as noise. + +Audit only was requested, so I stopped before the voice statement and rewrite. The two strongest signals are the assistant-style disclaimer opener and the participial tail on roughly four of every ten sentences. diff --git a/docs/acceptance/v0.3/codex.md b/docs/acceptance/v0.3/codex.md index feb778e..5e9778c 100644 --- a/docs/acceptance/v0.3/codex.md +++ b/docs/acceptance/v0.3/codex.md @@ -273,26 +273,15 @@ nominalization hits: 24 (validation×2, migration×2, implementation×1) · fram ## Output -I’ll read the humanize skill and audit `ai_report.txt` without changing it. -Audited `ai_report.txt` as **expository prose**. No changes made. These are stylistic findings, not proof of AI authorship. +I’ll use the humanize skill to audit `ai_report.txt` and leave the file unchanged. +Audited `ai_report.txt` as **expository prose**. File unchanged. The strongest patterns are repetition and uniform rhythm; these do not establish AI authorship. | # | Tell | Evidence | Base rate / metric | |---|------|----------|--------------------| -| 1 | Verbatim repetition | “across all workstreams and teams” (×3), without an apparent rhetorical purpose. | Same five-word phrase repeated three times. | -| 2 | Repeated participial tails | “, ensuring alignment across teams before any record reaches”; “, exceeding the target we set in March”; “, allowing designers to retire eleven legacy patterns”. The repeated construction makes results sound mechanically appended. | Scanner: 14 hits; 22.6 per 1,000 words. Legitimate in reports, but conspicuous at this density. | -| 3 | Uniform sentence length | “Two departments asked for a shorter reporting cycle, and that request is under review.” Nearly every sentence has a similar medium-length cadence. | 34 sentences; range 14–27 words; coefficient of variation (CV) 0.19. Skill heuristic flags CV below 0.4 as unusually flat. | -| 4 | Uniform paragraph length | The paragraph beginning “On the reliability side” follows the same three-sentence shape as nearly every substantive update. | All 12 paragraphs contain 2–3 sentences; paragraph CV 0.137, below the skill’s 0.3 uniformity heuristic. | -| 5 | Disclaimer opener | “It's important to approach” and “I'm not able to” delay the substantive update. The vendor-pricing restriction may have a real purpose; the cautionary lead-in is generic. | Scanner: disclaimer opener fired, with two matching phrases. | -| 6 | Summary closer | “In summary, delivery across all workstreams and teams stayed on plan” repeats the broad reassurance. “Stayed on plan” also needs reconciling with the reported two-week delay. | Scanner: summary closer detected. | -| 7 | Abstract container phrases — weak signal | “a sense of” momentum; “The weight of” the decision. These make the account less concrete, but are ordinary report language at this frequency. | Scanner: 2 container-noun hits; insufficient density to treat as a strong tell. | -Audited `ai_report.txt` as **expository prose**. No changes made. These are stylistic findings, not proof of AI authorship. - -| # | Tell | Evidence | Base rate / metric | -|---|------|----------|--------------------| -| 1 | Verbatim repetition | “across all workstreams and teams” (×3), without an apparent rhetorical purpose. | Same five-word phrase repeated three times. | -| 2 | Repeated participial tails | “, ensuring alignment across teams before any record reaches”; “, exceeding the target we set in March”; “, allowing designers to retire eleven legacy patterns”. The repeated construction makes results sound mechanically appended. | Scanner: 14 hits; 22.6 per 1,000 words. Legitimate in reports, but conspicuous at this density. | -| 3 | Uniform sentence length | “Two departments asked for a shorter reporting cycle, and that request is under review.” Nearly every sentence has a similar medium-length cadence. | 34 sentences; range 14–27 words; coefficient of variation (CV) 0.19. Skill heuristic flags CV below 0.4 as unusually flat. | -| 4 | Uniform paragraph length | The paragraph beginning “On the reliability side” follows the same three-sentence shape as nearly every substantive update. | All 12 paragraphs contain 2–3 sentences; paragraph CV 0.137, below the skill’s 0.3 uniformity heuristic. | -| 5 | Disclaimer opener | “It's important to approach” and “I'm not able to” delay the substantive update. The vendor-pricing restriction may have a real purpose; the cautionary lead-in is generic. | Scanner: disclaimer opener fired, with two matching phrases. | -| 6 | Summary closer | “In summary, delivery across all workstreams and teams stayed on plan” repeats the broad reassurance. “Stayed on plan” also needs reconciling with the reported two-week delay. | Scanner: summary closer detected. | -| 7 | Abstract container phrases — weak signal | “a sense of” momentum; “The weight of” the decision. These make the account less concrete, but are ordinary report language at this frequency. | Scanner: 2 container-noun hits; insufficient density to treat as a strong tell. | +| 1 | Verbatim repetition | “across all workstreams and teams” (×3) adds repetitive wording without technical necessity. | 3 occurrences of the same five-word phrase | +| 2 | Trailing participial clauses | “, ensuring alignment across teams before any record reaches”; “, allowing designers to retire eleven legacy patterns” | 14 hits; 22.6 per 1,000 words. Common in reports, but frequent here. | +| 3 | Uniform sentence length | “Four product surfaces migrated to the new components, allowing designers to retire eleven legacy patterns.” exemplifies the steady cadence. | All 34 sentences span 14–27 words; coefficient of variation (CV) 0.19. Skill heuristic: below 0.4 is unusually uniform. | +| 4 | Uniform paragraph length | The paragraph beginning “On the reliability side” follows the recurring three-sentence format. | All 12 paragraphs contain 2–3 sentences; CV 0.137. Skill heuristic: below 0.3 is uniform. | +| 5 | Disclaimer opener | “It's important to approach” and “I'm not able to” delay the substantive update. The vendor-pricing restriction may be justified. | Scanner fired on 2 opening phrases. | +| 6 | Summary closer | “In summary, delivery across all workstreams and teams stayed on plan” repeats the broad reassurance. “Stayed on plan” also needs reconciling with the reported two-week delay. | Summary-closer flag: true | +| 7 | Abstract container phrases — weak signal | “a sense of” momentum; “The weight of” the decision. These obscure the specific improvement or burden. | 2 hits; ordinary report language at this density, so not strong evidence. | diff --git a/tools/smoke_harnesses.sh b/tools/smoke_harnesses.sh index 4e7817a..1bb8845 100755 --- a/tools/smoke_harnesses.sh +++ b/tools/smoke_harnesses.sh @@ -16,21 +16,19 @@ OUT="$ROOT/docs/acceptance/v0.3" REQUEST="Use the humanize skill on $FIXTURE. Audit only — do not rewrite." mkdir -p "$OUT" -# Expected rows (case-insensitive regex, matched by concept rather than by our own -# reference titles) and two scanner numbers that must appear verbatim. CONTAINER_ALT is -# derived from the scanner's own container_of hits on the fixture so a model that quotes -# the hit text verbatim instead of the word "container" still matches. -CONTAINER_ALT="$(python3 "$ROOT/skills/humanize/scripts/surface_scan.py" "$ROOT/$FIXTURE" \ - | python3 -c 'import json,sys; h=json.load(sys.stdin)["grammar"]["container_of"]["hits"]; print("|".join(x["text"].lower() for x in h))')" -ROWS=('participial' 'repetit|repeated' "container|${CONTAINER_ALT:-container}" 'disclaimer') +# Two scanner numbers that must appear verbatim. Row concepts (matched against the +# third |-separated field — the Tell cell — of each distinct table row under +# "## Output", rather than by our own reference titles) are checked in judge()'s +# row_check awk block below. SCAN="$(python3 "$ROOT/skills/humanize/scripts/surface_scan.py" --text "$ROOT/$FIXTURE")" NUM_TAILS="$(printf '%s\n' "$SCAN" | sed -n 's/^grammar: participial tails \([0-9]*\).*/\1/p')" NUM_REP="$(printf '%s\n' "$SCAN" | sed -n 's/^repetition: \([0-9.]*\)\/1k.*/\1/p')" +[ -n "$NUM_TAILS" ] && [ -n "$NUM_REP" ] || { echo "FAIL all (scanner summary format changed; cannot derive expected numbers)"; exit 1; } # check_auth reads a transcript (or raw CLI output) on stdin and prints the first # line matching a known authentication-failure signature, or nothing. check_auth() { - grep -m1 -E "Authentication required|AuthError|token refresh failed|Please run 'agent login'" + grep -m1 -E "^(Error: )?Authentication required|^\S*AuthError|token refresh failed|Please run 'agent login'" } # section_* extract one H2 section's body from a transcript file, bounded by the @@ -47,7 +45,26 @@ judge() { # judge out="$(section_output "$f")" scan="$(section_scanner_output "$f")" calls="$(section_tool_calls "$f")" - for r in "${ROWS[@]}"; do printf '%s\n' "$out" | grep -qiE "$r" || { echo " missing row: $r"; ok=0; }; done + local row_check + # Match each concept against the third |-separated field (the Tell cell) of every + # distinct table row under ## Output, taking the first matching row per concept. + # Header/separator rows (" Tell ", "------") match none of the four regexes, so + # they need no special case. All four concepts must match, on four distinct rows. + row_check="$(printf '%s\n' "$out" | awk -F'|' ' + /^\|/ { + n++ + tell = tolower($3) + if (!l1 && tell ~ /participial/) l1 = n + if (!l2 && tell ~ /repetit/) l2 = n + if (!l3 && tell ~ /container/) l3 = n + if (!l4 && tell ~ /disclaimer/) l4 = n + } + END { + if (!l1 || !l2 || !l3 || !l4) { print "missing concept row (participial/repetition/container/disclaimer)"; exit } + if (l1 == l2 || l1 == l3 || l1 == l4 || l2 == l3 || l2 == l4 || l3 == l4) print "concept rows not distinct" + } + ')" + [ -n "$row_check" ] && { echo " $row_check"; ok=0; } printf '%s\n' "$out" | grep -iE '^\|' | grep -qi 'nominalization' && { echo " nominalization row present"; ok=0; } printf '%s\n%s\n' "$scan" "$out" | grep -qE "participial tails[^0-9]{0,20}${NUM_TAILS}([^0-9]|$)" || { echo " scanner count $NUM_TAILS absent"; ok=0; } printf '%s\n%s\n' "$scan" "$out" | grep -qF "${NUM_REP}/1k" || { echo " scanner rate ${NUM_REP}/1k absent"; ok=0; } @@ -58,25 +75,32 @@ judge() { # judge run_claude() { command -v claude >/dev/null || { echo "SKIP claude (not installed)"; return; } - local f="$OUT/claude-code.md" + local f="$OUT/claude-code.md" tmp raw + tmp="$(mktemp -d)" + [ -n "$tmp" ] && [ -d "$tmp" ] || { echo "FAIL claude-code (mktemp failed)"; return; } + raw="$tmp/claude-code.md.jsonl" claude plugin disable humanize@humanize >/dev/null 2>&1 || true - # A ^C or kill mid-run must not leave the user's installed plugin disabled. - trap 'claude plugin enable humanize@humanize >/dev/null 2>&1 || true' EXIT INT TERM + # A ^C or kill mid-run must not leave the user's installed plugin disabled or + # the raw-stream temp directory behind. + trap 'claude plugin enable humanize@humanize >/dev/null 2>&1 || true; rm -rf "$tmp"' EXIT + trap 'claude plugin enable humanize@humanize >/dev/null 2>&1 || true; rm -rf "$tmp"; trap - EXIT INT TERM; exit 130' INT TERM (cd "$ROOT" && claude -p "$REQUEST" --plugin-dir . --output-format stream-json --verbose \ - --allowedTools "Bash,Read,Glob,Grep" > "$f.jsonl" 2>&1) + --allowedTools "Bash,Read,Glob,Grep" > "$raw" 2>&1) claude plugin enable humanize@humanize >/dev/null 2>&1 || true trap - EXIT INT TERM local auth_line - auth_line="$(check_auth < "$f.jsonl")" + auth_line="$(check_auth < "$raw")" if [ -n "$auth_line" ]; then - cp "$f.jsonl" "$f" + cp "$raw" "$f" echo "SKIP claude-code (not authenticated: $auth_line)" + rm -rf "$tmp" return fi - python3 - "$f.jsonl" "$f" <<'EOF' + python3 - "$raw" "$f" <<'EOF' import json, sys src, dst = sys.argv[1], sys.argv[2] calls, scanner, output = [], [], [] +scan_ids = set() for line in open(src, encoding="utf-8"): line = line.strip() if not line: @@ -96,8 +120,19 @@ for line in open(src, encoding="utf-8"): continue btype = blk.get("type") if btype == "tool_use": - calls.append(json.dumps(blk.get("input"), indent=2)) + # Only the scanner invocation itself becomes tool-call/scanner-output + # evidence. A tool call that merely names surface_scan.py in a read + # (cat/grep) must not satisfy the tool-evidence rule; only genuine + # scanner invocations do, mirroring the Codex parser's filter. + serialized = json.dumps(blk.get("input")) + if "surface_scan.py" in serialized: + calls.append(json.dumps(blk.get("input"), indent=2)) + tool_id = blk.get("id") + if tool_id: + scan_ids.add(tool_id) elif btype == "tool_result": + if blk.get("tool_use_id") not in scan_ids: + continue c = blk.get("content") if isinstance(c, str): scanner.append(c) @@ -122,6 +157,7 @@ open(dst, "w", encoding="utf-8").write( "\n\n## Output\n" + "\n".join(output) ) EOF + rm -rf "$tmp" judge "claude-code" "$f" 'surface_scan\.py' } @@ -129,7 +165,10 @@ run_codex() { command -v codex >/dev/null || { echo "SKIP codex (not installed)"; return; } local tmp f="$OUT/codex.md"; tmp="$(mktemp -d)" [ -n "$tmp" ] && [ -d "$tmp" ] || { echo "FAIL codex (mktemp failed)"; return; } + trap 'rm -rf "$tmp"' EXIT + trap 'rm -rf "$tmp"; trap - EXIT INT TERM; exit 130' INT TERM mkdir -p "$tmp/.agents/skills" && cp -R "$ROOT/skills/humanize" "$tmp/.agents/skills/" && cp "$ROOT/$FIXTURE" "$tmp/" + find "$tmp/.agents/skills/humanize" -name __pycache__ -type d -exec rm -rf {} + (cd "$tmp" && codex debug prompt-input "hi" 2>/dev/null | grep -qi humanize) || echo " warning: skill not listed by codex debug prompt-input" (cd "$tmp" && codex exec -C "$tmp" --skip-git-repo-check --ephemeral -s read-only --json \ -o "$tmp/out.md" "Use the humanize skill on ai_report.txt. Audit only — do not rewrite." "$tmp/events.jsonl" 2>&1) @@ -139,6 +178,7 @@ run_codex() { cp "$tmp/events.jsonl" "$f" echo "SKIP codex (not authenticated: $auth_line)" rm -rf "$tmp" + trap - EXIT INT TERM return fi python3 - "$tmp/events.jsonl" "$tmp/out.md" "$f" <<'EOF' @@ -173,7 +213,11 @@ for line in open(events_path, encoding="utf-8"): output.append(text) try: with open(out_md_path, encoding="utf-8") as fh: - output.append(fh.read()) + out_md_text = fh.read() + # out.md commonly repeats the last agent_message text verbatim; only append + # it when it actually differs, or ## Output carries the audit table twice. + if not output or output[-1] != out_md_text: + output.append(out_md_text) except OSError: pass open(dst, "w", encoding="utf-8").write( @@ -184,6 +228,7 @@ open(dst, "w", encoding="utf-8").write( EOF judge "codex" "$f" 'surface_scan\.py' rm -rf "$tmp" + trap - EXIT INT TERM } run_cursor() { @@ -191,12 +236,16 @@ run_cursor() { agent --help 2>/dev/null | grep -qE -- '(^| )-p[ ,]|--print' || { echo "SKIP cursor (agent $(agent --version 2>/dev/null | head -1) has no headless flag)"; return; } local tmp f="$OUT/cursor.md" raw; tmp="$(mktemp -d)" [ -n "$tmp" ] && [ -d "$tmp" ] || { echo "FAIL cursor (mktemp failed)"; return; } + trap 'rm -rf "$tmp"' EXIT + trap 'rm -rf "$tmp"; trap - EXIT INT TERM; exit 130' INT TERM mkdir -p "$tmp/.cursor/skills" && cp -R "$ROOT/skills/humanize" "$tmp/.cursor/skills/" && cp "$ROOT/$FIXTURE" "$tmp/" + find "$tmp/.cursor/skills/humanize" -name __pycache__ -type d -exec rm -rf {} + # Deviation from the brief: the installed `agent` CLI's approvalMode is `allowlist` # with only Shell(ls) allowed, so a headless run cannot execute the scanner # without --force. raw="$(cd "$tmp" && agent -p --force --output-format text "Use the humanize skill on ai_report.txt. Audit only — do not rewrite." 2>&1)" rm -rf "$tmp" + trap - EXIT INT TERM local auth_line auth_line="$(printf '%s\n' "$raw" | check_auth)" if [ -n "$auth_line" ]; then @@ -222,6 +271,11 @@ run_hermes() { local dest="$HOME/.hermes/skills/writing/humanize" f="$OUT/hermes.md" raw [ -e "$dest" ] && { echo "SKIP hermes ($dest already exists; not touching it)"; return; } mkdir -p "$(dirname "$dest")" && cp -R "$ROOT/skills/humanize" "$dest" + # A ^C or kill mid-run must not leave this copy installed in the user's home + # directory — a leftover copy makes every later run print SKIP hermes forever. + trap 'rm -rf "$dest"' EXIT + trap 'rm -rf "$dest"; trap - EXIT INT TERM; exit 130' INT TERM + find "$dest" -name __pycache__ -type d -exec rm -rf {} + # $REQUEST names the fixture by a project-relative path; run from $ROOT so it resolves # no matter which directory this script itself is invoked from. if hermes --help 2>/dev/null | grep -q -- ' -z'; then @@ -230,6 +284,7 @@ run_hermes() { raw="$(cd "$ROOT" && hermes chat -q "$REQUEST" -Q 2>&1)" fi rm -rf "$dest" + trap - EXIT INT TERM local auth_line auth_line="$(printf '%s\n' "$raw" | check_auth)" if [ -n "$auth_line" ]; then @@ -252,4 +307,7 @@ run_hermes() { HARNESSES=("$@") [ "${#HARNESSES[@]}" -eq 0 ] && HARNESSES=(claude codex cursor hermes) -for h in "${HARNESSES[@]}"; do "run_$h"; done +for h in "${HARNESSES[@]}"; do + type "run_$h" >/dev/null 2>&1 || { echo "SKIP $h (unknown harness)"; continue; } + "run_$h" +done From 0585210095de0804cdd4dbdb9c7edd17984fbcf7 Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 12:13:39 -0400 Subject: [PATCH 20/22] SKILL.md: scan-path fallback; invocation heading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Step 2 now tells the model to search for scripts/surface_scan.py when a harness doesn't show the skill's path, and to skip the scan (saying so) if it still can't find it — insurance for Cursor, Hermes, Cowork, and claude.ai, none of which have exercised this step yet (I6). The compensating -1 line drops the now-redundant document-skills plugin pointer from the Non-text sources paragraph; it already lives in the README's "Word, PDF, PowerPoint inputs" note. Net 0 lines: SKILL.md stays at 150; curly-character count unchanged at 11. - "## Invocation (`/humanize` only)" -> "## Invocation (explicit only)": the old heading contradicted the very next sentence, which also names Codex's $humanize form (minor). uv run pytest -q -W error: 108 passed (3.13 and --python 3.9). claude plugin validate --strict .: Validation passed. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- skills/humanize/SKILL.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/skills/humanize/SKILL.md b/skills/humanize/SKILL.md index b0f6136..3b1377b 100644 --- a/skills/humanize/SKILL.md +++ b/skills/humanize/SKILL.md @@ -21,7 +21,7 @@ the text. No audit table, no commentary about tells. **Audit mode** — the user asks you to humanize existing text, or `/humanize` was invoked. Follow all six steps below. -## Invocation (`/humanize` only) +## Invocation (explicit only) Applies only when the user typed `/humanize …` (Codex: `$humanize …`). On auto-invoke there are no arguments: skip this section. Options below also @@ -70,17 +70,17 @@ If the text is 80 words or longer, run the scanner and keep the output: python3 /scripts/surface_scan.py --text ``` -`` is the directory holding this SKILL.md; keep the working -directory on the user's project so `` resolves. Pasted text goes to a -temp file first; drop `--text` for full JSON. Under 80 words, skip this step. +`` is the directory holding this SKILL.md; if its path was not +shown, search for `scripts/surface_scan.py`, and if it is not found skip the +scan and say so. Keep the working directory on the user's project so `` +resolves. Pasted text goes to a temp file first. Under 80 words, skip this step. **Non-text sources** (`.docx`, `.pdf`, `.pptx`, `.odt`, `.rtf`): the scanner reads -plain text only. Extract first with the harness's document skills (Anthropic's -`docx`/`pdf` skills ship with Claude Code's `document-skills` plugin and with -Claude Desktop) to write a temp `.md`, then scan and audit that. In claude.ai the -file is an attachment: convert it in the sandbox (pandoc, pdfplumber). Without -document skills, read the file yourself if you can and write the text to a temp -file; otherwise ask for a text export. Deliver Markdown; a Word file via `docx`. +plain text only. Extract first with the harness's document skills (`docx`, `pdf`) +to write a temp `.md`, then scan and audit that; in claude.ai convert the +attachment in the sandbox. Without document skills, read the file yourself if +you can and write the text to a temp file; otherwise ask for a text export. +Deliver Markdown; a Word file via `docx`. ### 3. Audit From 2a3983588fd896618eb10f64a932ada8f294d324 Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 12:16:59 -0400 Subject: [PATCH 21/22] Docs: verified install commands, acceptance record, spec amendments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README.md: stop claiming every install command was verified. `npx skills --help` / `npx skills add --help` (skills.sh CLI 1.5.26) confirm `add ` accepts the owner/repo shorthand the skills.sh row uses, so that row stays; the sentence now names what was actually checked and points installs that fetch from GitHub (Codex, Hermes) at the acceptance record instead (C1). `claude plugin validate .` -> `--strict` to match CLAUDE.md and CI (minor). - docs/acceptance/v0.3/README.md: the "## Tool calls" sentence is now true for both harnesses (after the smoke-script fix). Renamed the misnumbered "Acceptance step 4" reference to "Acceptance 3 (spec §8)" and added a section recording its verbatim outcome from task-7-report.md (marketplace add from ccf/humanize@feat/v0.3, plugin add resolving to 0.3.0, skill listed, plugin and marketplace removed, list back to the pre-run baseline). Added an "Acceptance 7 (spec §8)" checklist recording this run's results: the storyscope grep over the manifests/pyproject/SKILL.md is empty, the README's seven StoryScope mentions are all sourced positions, every README command was checked (per C1), and the scanner run on the README grounding paragraph shows no wordlist hits. Added a "Pre-merge status" paragraph: Claude Code and Codex PASS, Cursor and Hermes SKIP (unauthenticated on this machine), both moving to phase 2 and re-run after login (C2/C3, I7). - docs/design/2026-09-14-humanize-v0.3-design.md: status line now names the pre-merge acceptance items actually recorded (1, 2 for Claude Code/Codex, 3, 7) and states plainly that Cursor and Hermes move to phase 2 unauthenticated, matching what the acceptance README records. Appended four Post-review amendments: the phase-2 move for Cursor/Hermes, the narrowed acceptance-7 grep scope with the "sourced positions" README check, the Claude parser/row-matching/trap/temp-dir hardening from this wave, and the SKILL.md scan-path fallback (C3, I8). Gate: uv run pytest -q -W error (108, both 3.13 and --python 3.9); bash -n tools/smoke_harnesses.sh; wc -l skills/humanize/SKILL.md (150); claude plugin validate --strict . (passed); claude plugin list shows humanize@humanize enabled; no .jsonl under docs/acceptance/. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- README.md | 8 +- docs/acceptance/v0.3/README.md | 86 ++++++++++++++++++- .../design/2026-09-14-humanize-v0.3-design.md | 13 ++- 3 files changed, 100 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9bbf570..6e730cc 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,10 @@ resolves in `references/SOURCES.md`. ## Install -Requires Python 3.9+ on `PATH` for the scanner. No other dependencies. Every -command below was checked against its CLI's `--help` for this release. +Requires Python 3.9+ on `PATH` for the scanner. No other dependencies. +Commands were checked against each CLI's `--help` for this release; installs +that fetch from GitHub (Codex, Hermes) are exercised at the acceptance steps +recorded in `docs/acceptance/`. | Harness | Install | Verify | Invoke | |---|---|---|---| @@ -137,7 +139,7 @@ tests/ pytest; no network, no LLM calls ``` uv sync uv run pytest -q -claude plugin validate . +claude plugin validate --strict . uv run python skills/humanize/scripts/surface_scan.py --text some.txt ``` diff --git a/docs/acceptance/v0.3/README.md b/docs/acceptance/v0.3/README.md index 714e801..7bbedfc 100644 --- a/docs/acceptance/v0.3/README.md +++ b/docs/acceptance/v0.3/README.md @@ -105,7 +105,87 @@ config. `skills/humanize` into `~/.hermes/skills/writing/humanize` for the run and removes only that directory afterward. If that path already exists, the run is SKIPped and nothing is touched. -- Acceptance step 4 (`codex plugin marketplace add ccf/humanize@feat/v0.3`) adds a - marketplace to the user's Codex config; the same run removes the plugin and the - marketplace afterward and verifies with `codex plugin marketplace list`. +- Acceptance 3 (spec §8; `codex plugin marketplace add ccf/humanize@feat/v0.3`, see + below) adds a marketplace to the user's Codex config; the same run removes the + plugin and the marketplace afterward and verifies with `codex plugin marketplace + list`. - The script never updates or reconfigures any harness CLI. + +## Acceptance 3 (spec §8) + +Spec §8.3: `codex plugin marketplace add ccf/humanize@feat/v0.3 && codex plugin add +humanize@humanize` installs and the skill surfaces in `codex debug prompt-input "hi"` +(pre-merge); the same without `@ref` after the merge. Run 2026-09-14, verbatim: + +``` +$ git push origin feat/v0.3 +Everything up-to-date + +$ codex plugin marketplace add ccf/humanize@feat/v0.3 +Added marketplace `humanize` from https://github.com/ccf/humanize.git#feat/v0.3. +Installed marketplace root: /Users/ccf/.codex/.tmp/marketplaces/humanize + +$ codex plugin add humanize@humanize +Added plugin `humanize` from marketplace `humanize`. +Installed plugin root: /Users/ccf/.codex/plugins/cache/humanize/humanize/0.3.0 + +$ codex debug prompt-input "hi" | grep -i humanize +- humanize:humanize: Use when drafting or editing any prose — email, essay, + documentation, blog post, story, chat reply — or when asked to "humanize" text, + make it "sound less like AI", "more natural", "less robotic", or remove AI tells. + Also use when reviewing prose someone else wrote. Not for code, config, or commit + messages. (file: r5/humanize/0.3.0/skills/humanize/SKILL.md) + +$ codex plugin remove humanize@humanize +Removed plugin `humanize` from marketplace `humanize`. + +$ codex plugin marketplace remove humanize +Removed marketplace `humanize`. +Removed installed marketplace root: /Users/ccf/.codex/.tmp/marketplaces/humanize + +$ codex plugin marketplace list +MARKETPLACE ROOT +openai-primary-runtime /Users/ccf/.cache/codex-runtimes/codex-primary-runtime/plugins/openai-primary-runtime +openai-bundled /Users/ccf/.codex/.tmp/bundled-marketplaces/openai-bundled +openai-curated /Users/ccf/.codex/.tmp/plugins +agentcairn /Users/ccf/git/agentcairn +``` + +Outcome: install resolved cleanly to `0.3.0`, matching the version pinned across the +repo's manifests; the skill was listed by `codex debug prompt-input`; the plugin and +marketplace were both removed afterward, and the final `codex plugin marketplace +list` matches the pre-run baseline exactly (no `humanize` row). PASS. + +## Acceptance 7 (spec §8) + +Spec §8.7 (amended — see the design doc's Post-review amendments): `grep -ri +storyscope` over the manifests, `pyproject.toml`, and SKILL.md returns nothing; the +README's StoryScope mentions occur only at sourced positions; every README command +was checked against its CLI's `--help`; the README grounding paragraph scans clean +of wordlist hits. Run 2026-09-14: + +- `grep -ri storyscope plugin.json .codex-plugin/plugin.json .claude-plugin/*.json + pyproject.toml skills/humanize/SKILL.md` — no output (empty). +- README StoryScope positions (`grep -n -i storyscope README.md`): lines 7 (the + grounding paragraph, sourced explicitly), 124–125 (`style-tells.md` / + `narrative-tells.md` file descriptions — source of the base rates), 132 (`data/` + tree line — the directory holds StoryScope's taxonomy), 155–156 (the license/credit + paragraph — attribution is required there), 160 (the AI fiction fixture's + provenance). All seven are sourced positions per spec §9; none are general + positioning copy. +- Every README command checked against its CLI's `--help`: Claude Code, Codex, and + Cursor rows verified directly; `hermes skills install --help` confirms the Hermes + row's `identifier` (`owner/repo/path`) and `--category`; `npx skills --help` / + `npx skills add --help` (skills.sh CLI 1.5.26) confirm `add ` accepts the + `owner/repo` shorthand the skills.sh row uses. +- `python3 skills/humanize/scripts/surface_scan.py --text` on the README grounding + paragraph (`README.md:7-17`): `wordlist: 0.0/1k — none`. No wordlist hits. + +PASS. + +## Pre-merge status + +Claude Code: PASS. Codex: PASS. Cursor: SKIP (not authenticated on this machine). +Hermes: SKIP (not authenticated on this machine). Both SKIPs move to phase 2 +(post-merge, pre-tag) per the design doc's Post-review amendments, and are re-run +after login, with transcripts committed to this directory. diff --git a/docs/design/2026-09-14-humanize-v0.3-design.md b/docs/design/2026-09-14-humanize-v0.3-design.md index 85b524f..391dee0 100644 --- a/docs/design/2026-09-14-humanize-v0.3-design.md +++ b/docs/design/2026-09-14-humanize-v0.3-design.md @@ -1,7 +1,7 @@ # humanize v0.3 — portable skill (design) Date: 2026-09-14 -Status: implemented on feat/v0.3 as 0.3.0 (revision 2 + post-review amendments); phased acceptance per §7 — pre-merge items recorded in docs/acceptance/v0.3/. Revision 1 had a three-lens adversarial review (harness +Status: implemented on feat/v0.3 as 0.3.0 (revision 2 + post-review amendments); pre-merge acceptance 1, 2 (Claude Code, Codex), 3, 7 recorded in docs/acceptance/v0.3/; Cursor and Hermes smoke runs moved to phase 2 (unauthenticated at pre-merge). Revision 1 had a three-lens adversarial review (harness facts, repo mechanics, tests/acceptance): 3 blockers, 16 important, 2 minor, all adjudicated below; a `claude plugin validate` dry run and a full pytest run on a rearranged scratch copy confirmed the layout. @@ -446,3 +446,14 @@ is the specific source of a number or a file; the tool is described as grounded unauthenticated harness prints `SKIP` with the reason rather than `FAIL`. - §8 acceptance 2 — Cursor and Hermes were SKIP (not authenticated on this machine) at the pre-merge run; they are re-run post-login and recorded in `docs/acceptance/v0.3/`. +- §7/§8 — at the pre-merge run Cursor and Hermes were SKIP (not authenticated on this machine); their + smoke runs move to phase 2 (post-merge, pre-tag), re-run after login and committed to + `docs/acceptance/v0.3/`. Acceptance 3 and 7 outcomes are recorded in `docs/acceptance/v0.3/README.md`. +- §8 acceptance 7 — the `grep -ri storyscope` scope is the manifests, `pyproject.toml`, and SKILL.md; + README lines 1–20 contain the one sourced StoryScope sentence §9 prescribes, so the README check is + "StoryScope appears only at sourced positions" instead. +- §5 — the Claude transcript parser keeps only the scanner invocation and its result (as the Codex + parser already did); row concepts match the Tell cell of distinct table rows; traps clean up every + side effect on interrupt; the raw Claude stream is written to a temp directory. +- §2B — if a harness does not show the skill's path, the model searches for `scripts/surface_scan.py` + and, failing that, skips the scan and says so. From a517f7408a429563a16997dc24f48a880a26c55f Mon Sep 17 00:00:00 2001 From: "Charles C. Figueiredo" Date: Mon, 14 Sep 2026 12:29:18 -0400 Subject: [PATCH 22/22] Smoke matrix: arm the Hermes trap before the copy; Claude evidence requires a python call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - run_hermes now sets its EXIT/INT/TERM trap before mkdir/cp -R instead of after, so a ^C or kill during the copy itself can't leave a partial install stuck in $HOME (rm -rf on a $dest that doesn't exist yet, or exists only partially, is safe). The pre-existence SKIP check stays where it was, before the trap is armed (Bugbot). - The Claude parser's tool-evidence filter now requires name == "Bash" and a command containing both "python" and "surface_scan.py", matching the Codex parser's rule exactly. Matching on the serialized input alone let a Read/Glob/Grep search that merely names surface_scan.py — invited by SKILL.md's new scan-path fallback — satisfy the tool-evidence rule without the scanner ever running (Bugbot). Re-ran `tools/smoke_harnesses.sh claude`: PASS claude-code on the first attempt; transcript regenerated (## Tool calls now holds only the Bash scanner calls). `claude plugin list` shows humanize@humanize enabled before and after. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KTKYvLEVY4mStJ3iPaB1Mh --- docs/acceptance/v0.3/claude-code.md | 261 +++++++++++++++------------- tools/smoke_harnesses.sh | 22 ++- 2 files changed, 151 insertions(+), 132 deletions(-) diff --git a/docs/acceptance/v0.3/claude-code.md b/docs/acceptance/v0.3/claude-code.md index 1c65550..f62439d 100644 --- a/docs/acceptance/v0.3/claude-code.md +++ b/docs/acceptance/v0.3/claude-code.md @@ -6,8 +6,12 @@ "description": "Run the surface scanner on the fixture" } { - "command": "python3 skills/humanize/scripts/surface_scan.py tests/fixtures/ai_report.txt | python3 -c '\nimport json,sys\nd=json.load(sys.stdin)\nfor k in (\"grammar\",\"discourse\",\"structures\",\"repetition\",\"sentence_len\",\"paragraph_len\"):\n print(k, json.dumps(d.get(k), indent=1)[:3000])\n'", - "description": "Print scanner JSON blocks with verbatim hits" + "command": "python3 skills/humanize/scripts/surface_scan.py --text tests/fixtures/ai_report.txt --json 2>/dev/null | python3 -c \"\nimport json,sys\nd=json.load(sys.stdin)\nfor k in ('discourse','grammar','repetition','structures'):\n print(k, json.dumps(d.get(k), indent=1)[:3000])\n\" || python3 skills/humanize/scripts/surface_scan.py --help", + "description": "Dump scanner JSON blocks for discourse, grammar, repetition, structures" +} +{ + "command": "python3 skills/humanize/scripts/surface_scan.py tests/fixtures/ai_report.txt | python3 -c \"\nimport json,sys\nd=json.load(sys.stdin)\nprint(json.dumps({k:d.get(k) for k in ('discourse','grammar','repetition','structures')}, indent=1))\n\"", + "description": "Dump scanner JSON blocks for discourse, grammar, repetition, structures" } ## Scanner output @@ -23,138 +27,149 @@ repetition: 3.2/1k · longest repeat 5 · "across all workstreams and teams"×3 grammar: participial tails 14 (22.6/1k) ", reflecting the late discovery of schema drift in the" · container-of 2 "a sense of" sentence tail: over-30 0.0% · p90 25 · longest flat run 8 nominalization hits: 24 (validation×2, migration×2, implementation×1) · frames: "the implementation of", "the migration of" -grammar { - "participial_tail": { - "count": 14, - "rate": 22.6, - "hits": [ - { - "text": ", reflecting the late discovery of schema drift in the", - "sentence": 2 - }, - { - "text": ", ensuring alignment across teams before any record reaches", - "sentence": 3 - }, - { - "text": ", exceeding the target we set in March", - "sentence": 4 - }, - { - "text": ", creating a sense of momentum that was missing last quarter", - "sentence": 5 - }, - { - "text": ", allowing designers to retire eleven legacy patterns", - "sentence": 6 - }, - { - "text": ", giving on-call staff a clearer signal during incidents", - "sentence": 9 - }, - { - "text": ", matching the level the platform group had proposed", - "sentence": 10 - }, - { - "text": ", resolving most of them within the first business day", - "sentence": 11 - }, - { - "text": ", leaving two senior positions unfilled going into the next", - "sentence": 14 - }, - { - "text": ", assuming the compensation adjustments are approved", - "sentence": 16 - } - ] +Traceback (most recent call last): + File "", line 3, in + File "/Users/ccf/.pyenv/versions/3.12.2/lib/python3.12/json/__init__.py", line 293, in load + return loads(fp.read(), + ^^^^^^^^^^^^^^^^ + File "/Users/ccf/.pyenv/versions/3.12.2/lib/python3.12/json/__init__.py", line 346, in loads + return _default_decoder.decode(s) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/ccf/.pyenv/versions/3.12.2/lib/python3.12/json/decoder.py", line 337, in decode + obj, end = self.raw_decode(s, idx=_w(s, 0).end()) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/Users/ccf/.pyenv/versions/3.12.2/lib/python3.12/json/decoder.py", line 355, in raw_decode + raise JSONDecodeError("Expecting value", s, err.value) from None +json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) +usage: surface_scan.py [-h] [--text] [path] + +Surface-level prose metrics (stdlib only). + +positional arguments: + path file to scan; reads stdin if omitted + +options: + -h, --help show this help message and exit + --text print a short summary instead of JSON +{ + "discourse": { + "summary_closer": true, + "disclaimer_opener": { + "fired": true, + "hits": [ + { + "text": "it's important to approach", + "sentence": 0 + }, + { + "text": "i'm not able to", + "sentence": 1 + } + ] + } }, - "container_of": { - "count": 2, - "hits": [ - { - "text": "a sense of", - "sentence": 5 - }, - { - "text": "The weight of", - "sentence": 15 - } - ] - } -} -discourse { - "summary_closer": true, - "disclaimer_opener": { - "fired": true, - "hits": [ - { - "text": "it's important to approach", - "sentence": 0 - }, + "grammar": { + "participial_tail": { + "count": 14, + "rate": 22.6, + "hits": [ + { + "text": ", reflecting the late discovery of schema drift in the", + "sentence": 2 + }, + { + "text": ", ensuring alignment across teams before any record reaches", + "sentence": 3 + }, + { + "text": ", exceeding the target we set in March", + "sentence": 4 + }, + { + "text": ", creating a sense of momentum that was missing last quarter", + "sentence": 5 + }, + { + "text": ", allowing designers to retire eleven legacy patterns", + "sentence": 6 + }, + { + "text": ", giving on-call staff a clearer signal during incidents", + "sentence": 9 + }, + { + "text": ", matching the level the platform group had proposed", + "sentence": 10 + }, + { + "text": ", resolving most of them within the first business day", + "sentence": 11 + }, + { + "text": ", leaving two senior positions unfilled going into the next", + "sentence": 14 + }, + { + "text": ", assuming the compensation adjustments are approved", + "sentence": 16 + } + ] + }, + "container_of": { + "count": 2, + "hits": [ + { + "text": "a sense of", + "sentence": 5 + }, + { + "text": "The weight of", + "sentence": 15 + } + ] + } + }, + "repetition": { + "too_short": false, + "repeated_phrase_rate": 3.2, + "longest_repeat": 5, + "phrases": [ { - "text": "i'm not able to", - "sentence": 1 + "text": "across all workstreams and teams", + "count": 3, + "sentences": [ + 5, + 17, + 32 + ] } ] + }, + "structures": { + "tricolon": 1, + "not_but": 1, + "rhetorical_q": 0, + "parallel_openers": 0 } } -structures { - "tricolon": 1, - "not_but": 1, - "rhetorical_q": 0, - "parallel_openers": 0 -} -repetition { - "too_short": false, - "repeated_phrase_rate": 3.2, - "longest_repeat": 5, - "phrases": [ - { - "text": "across all workstreams and teams", - "count": 3, - "sentences": [ - 5, - 17, - 32 - ] - } - ] -} -sentence_len { - "mean": 18.24, - "stdev": 3.46, - "cv": 0.19, - "min": 14, - "max": 27, - "pct_over_30": 0.0, - "p90": 25, - "longest_flat_run": 8 -} -paragraph_len { - "mean": 2.83, - "stdev": 0.39, - "cv": 0.137, - "min": 2, - "max": 3 -} ## Output I'll invoke the humanize skill in audit-only mode on the fixture. -Audit complete. Class: `expository` (a quarterly status report, 620 words, 34 sentences, 12 paragraphs). +Scan and references are all in hand. Writing up the audit now. + +Class: expository (quarterly status report, 620 words, 34 sentences, 12 paragraphs). Scanner ran on the file. Audit only, so no rewrite follows. | # | Tell | Evidence | Base rate / metric | |---|------|----------|--------------------| -| 1 | Safety disclaimer opener | "It's important to approach this update carefully…", "I'm not able to share vendor pricing here, but…" (sentences 1–2) | scan: `discourse.disclaimer_opener` fired; per-family range 46% vs 0.2% of responses (Rudnicka & Juzek 2026) | -| 2 | Trailing participial clause | ", reflecting the late discovery of schema drift", ", ensuring alignment across teams", ", creating a sense of momentum", ", allowing designers to retire eleven legacy patterns" (×14) | scan: 14 tails, 22.6/1k; ratio 5.3×, d = 1.38 (Reinhart et al. 2025) | -| 3 | Verbatim repetition | "across all workstreams and teams" (×3, sentences 5, 17, 32) | scan: `repetition.phrases`; repeated phrases OR 1.47 (Jakesch et al. 2023); rate 3.2/1k reported-only | -| 4 | Uniform sentence length | every sentence 14–27 words; longest flat run 8; no sentence over 30 | scan: cv 0.19, pct_over_30 0% (humans 31.2% vs 17.5–21.0%, Muñoz-Ortiz et al. 2024); rule of thumb: human 0.5–0.9, AI often below 0.4 | -| 5 | Uniform paragraph length | 12 paragraphs, every one 2–3 sentences, each opening on a topic and closing on a status | scan: `paragraph_len.cv` 0.137; rule of thumb: below 0.3 across five or more paragraphs is uniform | -| 6 | Summary closer | "In summary, delivery across all workstreams and teams stayed on plan…" | scan: `discourse.summary_closer` yes | -| 7 | Abstract container-noun phrase | "a sense of momentum that was missing last quarter", "The weight of the decision to pause backfills" (×2) | scan: `grammar.container_of` 2; heads attested in LAMP Table 8 (Chakrabarty et al. 2025) | -| 8 | Predominant tonal quality (earnest, no undercut) | "the overall picture is encouraging and the risks are manageable"; no dry line in 620 words | earnest or lyrical tone: AI 71% / human 40% (StoryScope STY_TON_021); any humor or irony: AI 62% / human 88% (STY_TON_023) | +| 1 | Trailing participial clause | ", ensuring alignment across teams before any record reaches" (×14) | scan: 14 tails, 22.6/1k; ratio 5.3×, d = 1.38 [reinhart-2025] | +| 2 | Safety disclaimer opener | "It's important to approach this update carefully" / "I'm not able to share vendor pricing here, but" | scan: fired, 2 hits; 46% vs 0.2% of responses [rudnicka-2026] | +| 3 | Verbatim repetition | "across all workstreams and teams" (×3, sentences 5, 17, 32) | scan: 3.2/1k, longest repeat 5; OR 1.47 [jakesch-2023] | +| 4 | Uniform sentence length | every sentence 14–27 words; 0% over 30; longest flat run 8 | scan: cv 0.19; rule of thumb human 0.5–0.9, AI often below 0.4; humans 31.2% over 30 vs 17.5–21.0% [munoz-ortiz-2024] | +| 5 | Uniform paragraph length | 12 paragraphs of 2–3 sentences, topic sentence then two supports | scan: cv 0.137; rule of thumb below 0.3 is uniform | +| 6 | Summary closer | "In summary, delivery across all workstreams and teams stayed on plan despite the schema issue" | scan: summary_closer true | +| 7 | Abstract container-noun phrase | "creating a sense of momentum" / "The weight of the decision to pause backfills fell mostly on" | scan: container_of 2 (LAMP Table 8 heads) [chakrabarty-2025] | +| 8 | Predominant tonal quality (earnest) | "the overall picture is encouraging and the risks are manageable"; no dry line anywhere | AI 71% / human 40% (StoryScope STY_TON_021) | -Two notes on what I did not table. The scanner counted 24 nominalization hits ("the implementation of", "the migration of", "the optimization of", "the remediation of", "the transformation of"), but in report register those are native and never become a row. They are a prompt to look, and the density here is high enough that a rewrite would unbury several. The one `not_but` and one tricolon hit are below any threshold and I treated them as noise. +Two notes outside the table. The scanner's nominalization block is a prompt to look, not a row, and in a report it is native register. It still stands out here for the length: seven "the X of" frames open sentences ("the implementation of", "the migration of", "the optimization of", "the escalation of", "the transformation of", "the remediation of", "the integration of"). The flat sentence profile in row 4 is also the natural shape of plain-language status writing, so it is supporting evidence rather than authorship evidence on its own. -Audit only was requested, so I stopped before the voice statement and rewrite. The two strongest signals are the assistant-style disclaimer opener and the participial tail on roughly four of every ten sentences. +Everything else stayed quiet: no wordlist hits, no hedges or intensifiers, no em-dashes, one tricolon, no rhetorical questions, opener variety at 0.735. diff --git a/tools/smoke_harnesses.sh b/tools/smoke_harnesses.sh index 1bb8845..8dd6b90 100755 --- a/tools/smoke_harnesses.sh +++ b/tools/smoke_harnesses.sh @@ -120,13 +120,15 @@ for line in open(src, encoding="utf-8"): continue btype = blk.get("type") if btype == "tool_use": - # Only the scanner invocation itself becomes tool-call/scanner-output - # evidence. A tool call that merely names surface_scan.py in a read - # (cat/grep) must not satisfy the tool-evidence rule; only genuine - # scanner invocations do, mirroring the Codex parser's filter. - serialized = json.dumps(blk.get("input")) - if "surface_scan.py" in serialized: - calls.append(json.dumps(blk.get("input"), indent=2)) + # Only a genuine scanner invocation becomes tool-call/scanner-output + # evidence: a Bash call whose command names both python and + # surface_scan.py, mirroring the Codex parser's rule. A Read/Glob/Grep + # search that merely names surface_scan.py — invited by SKILL.md's + # scan-path fallback — must not satisfy the tool-evidence rule. + inp = blk.get("input") + cmd = inp.get("command", "") if isinstance(inp, dict) else "" + if blk.get("name") == "Bash" and "python" in cmd and "surface_scan.py" in cmd: + calls.append(json.dumps(inp, indent=2)) tool_id = blk.get("id") if tool_id: scan_ids.add(tool_id) @@ -270,11 +272,13 @@ run_hermes() { command -v hermes >/dev/null || { echo "SKIP hermes (not installed)"; return; } local dest="$HOME/.hermes/skills/writing/humanize" f="$OUT/hermes.md" raw [ -e "$dest" ] && { echo "SKIP hermes ($dest already exists; not touching it)"; return; } - mkdir -p "$(dirname "$dest")" && cp -R "$ROOT/skills/humanize" "$dest" - # A ^C or kill mid-run must not leave this copy installed in the user's home + # A ^C or kill mid-run must not leave a copy installed in the user's home # directory — a leftover copy makes every later run print SKIP hermes forever. + # Armed before the copy starts: rm -rf on a $dest that doesn't exist yet, or + # exists only partially, is safe. trap 'rm -rf "$dest"' EXIT trap 'rm -rf "$dest"; trap - EXIT INT TERM; exit 130' INT TERM + mkdir -p "$(dirname "$dest")" && cp -R "$ROOT/skills/humanize" "$dest" find "$dest" -name __pycache__ -type d -exec rm -rf {} + # $REQUEST names the fixture by a project-relative path; run from $ROOT so it resolves # no matter which directory this script itself is invoked from.