Copy Craft
LLM-judgment critique of prose-in-code across six surfaces: error messages, log lines, CLI output strings, commit subjects, PR descriptions, and code comments. Primary domain is error messages (universally bad in most codebases). Third member of the craft-pipeline initiative. NO rule-based floor exists — pure ceiling. Emits 3-axis findings (tier × impact × confidence per ADR 0019).
When to Use
- During PR review on code that adds or changes error messages, log lines, or CLI output
- After a feature ships, to audit error-message quality across the changed surfaces
- Periodically (per-release) to catch accumulated noise in log lines + comment rot
- As the user-facing-copy critic alongside design-craft (which owns UI copy)
- NOT for UI copy in components (use design-craft)
- NOT for prose documentation in
docs/ (use docs-craft when it ships)
- NOT for autofix / rewriting (this is judgment-only; v2 may ship
align-copy)
- NOT for JSDoc / TSDoc structured API docs (docs-craft territory)
- NOT for non-TS/JS languages in v1 (v1.x)
Capability Roles
- Defines (Service Definition): the shared craft critique contract (
packages/cli/src/shared/craft/) — LlmProvider + finding/axes schema + run store — shared across all *-craft skills. This skill implements, and does not own, that contract.
- Provides (Provider): this skill — a prose-in-code critique implemented over the shared contract (
packages/cli/src/copy-craft/).
- Consumes (Consumer):
craft-fleet (the craft-pipeline elevation sweep) and the harness natural-language router, which invoke every *-craft provider uniformly through the shared critique/finding shape
Process
Phase 1: EXTRACT — Six surfaces, three infrastructures
Read project configuration. Check harness.config.json for:
craft.copy.enabled — gate (default true)
craft.copy.surfaces — restrict to specific surfaces (default: all 6)
craft.copy.maxFiles (default 100), craft.copy.maxItemsPerFile (default 20)
craft.copy.commitsSince (default '1 month ago'), craft.copy.prLimit (default 20)
Source-side surfaces (errors / logs / CLI output / comments): single TS Compiler API walk per source file. Amortizes parse cost across surfaces.
- errors:
throw new <X>Error("...") where the constructor name ends in Error; also Err({ message: "..." }) for Result-style returns
- logs:
console.log/info/warn/error/debug and logger.X / log.X / pino.X / winston.X where X is a known level
- cli-output: strings inside files under
packages/*/src/commands/ (configurable via cliOutputPaths); takes precedence over log for files matching the glob
- comments:
ts.getLeadingCommentRanges() + getTrailingCommentRanges(); excludes JSDoc and license banners
Git surface (commits): shell-out to git log --pretty=format:'%H%x09%s' --since=.... Skip silently when not in a git repo.
GitHub surface (PR descriptions): shell-out to gh pr list --json number,title,body. Skip silently when gh binary missing or gh auth status fails.
Skipped surfaces recorded in summary.skippedSurfaces with the reason — visible in the report, not a failure.
Phase 2: CRITIQUE — Per (item, rubric) loop, surface-filtered
8 seed rubrics, each declares which surfaces it applies to:
| Rubric |
Surfaces |
COPY-R001 WHAT/WHY/HOW-TO-FIX |
error |
COPY-R002 calm-not-panicky |
error, log |
COPY-R003 specific-not-generic |
error, log, cli-output |
COPY-R004 signal-not-noise |
log |
COPY-R005 grep-survives |
log, cli-output |
COPY-R006 describes-change-not-work |
commit, pr-description |
COPY-R007 stranger-in-6-months |
commit, pr-description, comment |
COPY-R008 WHY-not-WHAT |
comment |
For each (item, rubric) where the rubric applies to the item's surface:
- Build prompt with rubric description + surface + context (errorType / logLevel / ref) + snippet (truncated to 1500 chars).
- LLM returns fenced JSON:
null (rubric doesn't apply / copy is fine) OR { tier, impact, confidence, message }.
- On non-null: emit
CopyFinding with cite.rubricId for ADR 0020 traceability.
Phase 3: REPORT — Aggregate + cost telemetry
Emit CopyCraftOutput:
{
findings: CopyFinding[];
summary: {
phaseRun: ['critique'];
durationMs: number;
llmCalls: { provider, model, count, costUsd };
catalog: { rubricsApplied: string[]; surfacesScanned: CopySurface[] };
counts: Record<CopySurface, number>;
skippedSurfaces: Array<{ surface, reason }>;
runId: string;
}
}
Harness Integration
harness copy-craft — CLI entry. --files / --surfaces / --max-files / --max-items-per-file / --commits-since / --pr-limit / --json / --verbose.
mcp__harness__copy_craft — MCP tool. Same input/output. Consumed by agents.
- Cross-cutting API:
critiqueCopyInFile(file, opts) exported. Source-side surfaces only; git surfaces are project-scoped.
- Shared craft infrastructure: imports
LlmProvider + 3-axis types + derivePriority from packages/cli/src/shared/craft/ (extracted by spec-craft).
Success Criteria
See docs/changes/craft-pipeline/copy-craft/proposal.md for the full 39 success criteria. Highlights:
- 8 seed rubrics ship in
catalog/rubrics/<id>.ts (file-per-rubric, matches naming/spec-craft)
- 3-axis output preserved (tier × impact × confidence, never collapsed)
cite.rubricId populated on every finding (ADR 0020)
- Single TS AST walk amortizes parse cost across 4 source surfaces
- Graceful degradation:
summary.skippedSurfaces records when git/gh surfaces couldn't run
- Cross-cutting
critiqueCopyInFile exported (source surfaces only)
Rationalizations to Reject
These are common rationalizations that sound reasonable but lead to incorrect results. When you catch yourself thinking any of these, stop and follow the documented process instead.
| Rationalization |
Why It Is Wrong |
| "This error message is a clean, grammatical sentence, so it passes COPY-R001." |
Grammar is not the rubric. COPY-R001 asks for WHAT failed, WHY, and HOW to fix. A fluent sentence that names no artifact and offers no recovery path still fails — emit the finding. |
"It's just a debug-level log, so COPY-R004 (signal-not-noise) doesn't really apply." |
signal-not-noise applies to every log line regardless of level. A debug log that fires on every invocation carrying no state or decision is exactly the noise the rubric exists for. |
| "The commit subject accurately describes what the author did, so COPY-R006 passes." |
COPY-R006 is describes-change-not-work. "update tests" describes the activity, not the behaviour that changed. Describing the work performed IS the failure mode, not the pass. |
| "The git/gh surface was skipped, so I'll infer commit or PR quality from the diff." |
Skipped surfaces are recorded in summary.skippedSurfaces with a reason — never backfilled by inference. Fabricating findings for a surface that did not execute corrupts the report. |
| "This copy is clearly bad, so I'll mark it high confidence and foundational." |
tier, impact, and confidence are three independent axes (ADR 0019). A genuinely bad string can be low-impact, and you may only be medium-confident. Collapsing the axes defeats derivePriority. |
Examples
Example: Generic error message
Input: src/parse.ts:
throw new Error('parse error');
Output (mock LLM):
[error]
COPY-R001 [foundational/large/medium] src/parse.ts:14 error
"parse error"
Doesn't tell WHAT was being parsed, WHY it failed, or HOW the user can
recover. Try: "Failed to parse design-system/tokens.json at line 12: ..."
COPY-R003 [polish/medium/high] src/parse.ts:14 error
"parse error"
Generic — no operation, no artifact. Name the file being parsed and the
specific failure mode.
Example: Noisy log line
Input:
console.log('entered function');
Output:
[log]
COPY-R004 [foundational/medium/high] src/handler.ts:23 log
"entered function"
Pure noise — fires on every invocation; carries no state or decision.
Either remove or replace with a state-transition log at the relevant
boundary.
Example: Work-not-change commit subject
Input: A commit with subject "update tests".
Output:
[commit]
COPY-R006 [polish/medium/medium] git:abc1234 commit
"update tests"
Describes the work, not the change. A reader six months from now needs
to know what behaviour changed. Try: "ratchet drift threshold to 0.5%
after Hermes Phase 4 baseline reset" or similar.
Example: Missing prerequisites — graceful skip
When not in a git repo OR gh is missing/unauthenticated, those surfaces silently skip and the report shows:
Skipped surfaces:
- commit: not a git repo
- pr-description: gh binary not found
Gates
- No autofix. v2's
align-copy may add safe rewrites.
- No JSDoc / TSDoc. docs-craft territory.
- No PR / review comments. v1.x.
- No commit BODY critique. Subjects only in v1.
- No B' bootstrap. Same posture as naming/spec-craft.
- No graph persistence. Phase 1 MVP.
- No non-TS/JS language support. v1.x.
- No author-attributed signals. v1.x telemetry.
Escalation
- When LLM cost is too high: drop
maxItemsPerFile (default 20) or scope to specific surfaces with --surfaces error. Cost ≈ items × applicable rubrics × per-call.
- When intentionally-bad test fixtures get flagged: scope via
--files to exclude fixtures. v1.x adds <!-- copy-craft:skip --> annotation + JSDoc tag.
- When git surface skips though you ARE in a repo: the walk goes up 10 levels looking for
.git. If you're deeper than that, run from a closer cwd.
- When PR surface skips with
gh auth status failing: run gh auth login first. Or scope away with --surfaces error,log,comment.
- When
console.log in a CLI file gets surface='log' instead of 'cli-output': verify the file path contains packages/cli/src/commands/ (or a similar default substring). Override with craft.copy.cliOutputGlobs config in v1.x.
Status
v1 — in implementation. See:
- Spec:
docs/changes/craft-pipeline/copy-craft/proposal.md
- Roadmap entry: part of the
craft-pipeline initiative
- Sibling craft skills:
naming-craft, spec-craft
- Shared infrastructure:
packages/cli/src/shared/craft/ (extracted by spec-craft)
- Future:
align-copy (FIX side, v2), docs-craft (prose docs), test-craft, code-craft
1---2name: copy-craft3description: Copy Craft4---5# Copy Craft67> LLM-judgment critique of prose-in-code across six surfaces: error messages, log lines, CLI output strings, commit subjects, PR descriptions, and code comments. Primary domain is error messages (universally bad in most codebases). Third member of the craft-pipeline initiative. NO rule-based floor exists — pure ceiling. Emits 3-axis findings (tier × impact × confidence per ADR 0019).89## When to Use1011- During PR review on code that adds or changes error messages, log lines, or CLI output12- After a feature ships, to audit error-message quality across the changed surfaces13- Periodically (per-release) to catch accumulated noise in log lines + comment rot14- As the user-facing-copy critic alongside design-craft (which owns UI copy)15- NOT for UI copy in components (use design-craft)16- NOT for prose documentation in `docs/` (use docs-craft when it ships)17- NOT for autofix / rewriting (this is judgment-only; v2 may ship `align-copy`)18- NOT for JSDoc / TSDoc structured API docs (docs-craft territory)19- NOT for non-TS/JS languages in v1 (v1.x)2021## Capability Roles2223<!-- Capability seam: this skill participates in a real extension point whose three roles are named and concrete. A seam with only one role filled is accidental single-implementation lock-in. See harness-skill-authoring Phase 1C. -->2425- **Defines (Service Definition):** the shared craft critique contract (`packages/cli/src/shared/craft/`) — `LlmProvider` + finding/axes schema + run store — shared across all `*-craft` skills. This skill implements, and does not own, that contract.26- **Provides (Provider):** **this skill** — a prose-in-code critique implemented over the shared contract (`packages/cli/src/copy-craft/`).27- **Consumes (Consumer):** `craft-fleet` (the craft-pipeline elevation sweep) and the `harness` natural-language router, which invoke every `*-craft` provider uniformly through the shared critique/finding shape2829## Process3031### Phase 1: EXTRACT — Six surfaces, three infrastructures32331. **Read project configuration.** Check `harness.config.json` for:34 - `craft.copy.enabled` — gate (default `true`)35 - `craft.copy.surfaces` — restrict to specific surfaces (default: all 6)36 - `craft.copy.maxFiles` (default 100), `craft.copy.maxItemsPerFile` (default 20)37 - `craft.copy.commitsSince` (default `'1 month ago'`), `craft.copy.prLimit` (default 20)38392. **Source-side surfaces** (errors / logs / CLI output / comments): single TS Compiler API walk per source file. Amortizes parse cost across surfaces.40 - **errors:** `throw new <X>Error("...")` where the constructor name ends in `Error`; also `Err({ message: "..." })` for Result-style returns41 - **logs:** `console.log/info/warn/error/debug` and `logger.X` / `log.X` / `pino.X` / `winston.X` where X is a known level42 - **cli-output:** strings inside files under `packages/*/src/commands/` (configurable via `cliOutputPaths`); takes precedence over `log` for files matching the glob43 - **comments:** `ts.getLeadingCommentRanges()` + `getTrailingCommentRanges()`; excludes JSDoc and license banners44453. **Git surface** (commits): shell-out to `git log --pretty=format:'%H%x09%s' --since=...`. Skip silently when not in a git repo.46474. **GitHub surface** (PR descriptions): shell-out to `gh pr list --json number,title,body`. Skip silently when `gh` binary missing or `gh auth status` fails.48495. **Skipped surfaces** recorded in `summary.skippedSurfaces` with the reason — visible in the report, not a failure.5051### Phase 2: CRITIQUE — Per (item, rubric) loop, surface-filtered52538 seed rubrics, each declares which surfaces it applies to:5455| Rubric | Surfaces |56| ------------------------------------- | ------------------------------- |57| `COPY-R001` WHAT/WHY/HOW-TO-FIX | error |58| `COPY-R002` calm-not-panicky | error, log |59| `COPY-R003` specific-not-generic | error, log, cli-output |60| `COPY-R004` signal-not-noise | log |61| `COPY-R005` grep-survives | log, cli-output |62| `COPY-R006` describes-change-not-work | commit, pr-description |63| `COPY-R007` stranger-in-6-months | commit, pr-description, comment |64| `COPY-R008` WHY-not-WHAT | comment |6566For each (item, rubric) where the rubric applies to the item's surface:67681. Build prompt with rubric description + surface + context (errorType / logLevel / ref) + snippet (truncated to 1500 chars).692. LLM returns fenced JSON: `null` (rubric doesn't apply / copy is fine) OR `{ tier, impact, confidence, message }`.703. On non-null: emit `CopyFinding` with `cite.rubricId` for ADR 0020 traceability.7172### Phase 3: REPORT — Aggregate + cost telemetry7374Emit `CopyCraftOutput`:7576```ts77{78 findings: CopyFinding[];79 summary: {80 phaseRun: ['critique'];81 durationMs: number;82 llmCalls: { provider, model, count, costUsd };83 catalog: { rubricsApplied: string[]; surfacesScanned: CopySurface[] };84 counts: Record<CopySurface, number>;85 skippedSurfaces: Array<{ surface, reason }>;86 runId: string;87 }88}89```9091## Harness Integration9293- **`harness copy-craft`** — CLI entry. `--files` / `--surfaces` / `--max-files` / `--max-items-per-file` / `--commits-since` / `--pr-limit` / `--json` / `--verbose`.94- **`mcp__harness__copy_craft`** — MCP tool. Same input/output. Consumed by agents.95- **Cross-cutting API:** `critiqueCopyInFile(file, opts)` exported. Source-side surfaces only; git surfaces are project-scoped.96- **Shared craft infrastructure:** imports `LlmProvider` + 3-axis types + `derivePriority` from `packages/cli/src/shared/craft/` (extracted by spec-craft).9798## Success Criteria99100See `docs/changes/craft-pipeline/copy-craft/proposal.md` for the full 39 success criteria. Highlights:101102- 8 seed rubrics ship in `catalog/rubrics/<id>.ts` (file-per-rubric, matches naming/spec-craft)103- 3-axis output preserved (tier × impact × confidence, never collapsed)104- `cite.rubricId` populated on every finding (ADR 0020)105- Single TS AST walk amortizes parse cost across 4 source surfaces106- Graceful degradation: `summary.skippedSurfaces` records when git/gh surfaces couldn't run107- Cross-cutting `critiqueCopyInFile` exported (source surfaces only)108109## Rationalizations to Reject110111These are common rationalizations that sound reasonable but lead to incorrect results. When you catch yourself thinking any of these, stop and follow the documented process instead.112113| Rationalization | Why It Is Wrong |114| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |115| "This error message is a clean, grammatical sentence, so it passes COPY-R001." | Grammar is not the rubric. COPY-R001 asks for WHAT failed, WHY, and HOW to fix. A fluent sentence that names no artifact and offers no recovery path still fails — emit the finding. |116| "It's just a `debug`-level log, so COPY-R004 (signal-not-noise) doesn't really apply." | signal-not-noise applies to every log line regardless of level. A debug log that fires on every invocation carrying no state or decision is exactly the noise the rubric exists for. |117| "The commit subject accurately describes what the author did, so COPY-R006 passes." | COPY-R006 is describes-change-not-work. "update tests" describes the activity, not the behaviour that changed. Describing the work performed IS the failure mode, not the pass. |118| "The git/gh surface was skipped, so I'll infer commit or PR quality from the diff." | Skipped surfaces are recorded in `summary.skippedSurfaces` with a reason — never backfilled by inference. Fabricating findings for a surface that did not execute corrupts the report. |119| "This copy is clearly bad, so I'll mark it high confidence and foundational." | tier, impact, and confidence are three independent axes (ADR 0019). A genuinely bad string can be low-impact, and you may only be medium-confident. Collapsing the axes defeats `derivePriority`. |120121## Examples122123### Example: Generic error message124125**Input:** `src/parse.ts`:126127```ts128throw new Error('parse error');129```130131**Output (mock LLM):**132133```134[error]135 COPY-R001 [foundational/large/medium] src/parse.ts:14 error136 "parse error"137 Doesn't tell WHAT was being parsed, WHY it failed, or HOW the user can138 recover. Try: "Failed to parse design-system/tokens.json at line 12: ..."139 COPY-R003 [polish/medium/high] src/parse.ts:14 error140 "parse error"141 Generic — no operation, no artifact. Name the file being parsed and the142 specific failure mode.143```144145### Example: Noisy log line146147**Input:**148149```ts150console.log('entered function');151```152153**Output:**154155```156[log]157 COPY-R004 [foundational/medium/high] src/handler.ts:23 log158 "entered function"159 Pure noise — fires on every invocation; carries no state or decision.160 Either remove or replace with a state-transition log at the relevant161 boundary.162```163164### Example: Work-not-change commit subject165166**Input:** A commit with subject `"update tests"`.167168**Output:**169170```171[commit]172 COPY-R006 [polish/medium/medium] git:abc1234 commit173 "update tests"174 Describes the work, not the change. A reader six months from now needs175 to know what behaviour changed. Try: "ratchet drift threshold to 0.5%176 after Hermes Phase 4 baseline reset" or similar.177```178179### Example: Missing prerequisites — graceful skip180181When not in a git repo OR `gh` is missing/unauthenticated, those surfaces silently skip and the report shows:182183```184Skipped surfaces:185 - commit: not a git repo186 - pr-description: gh binary not found187```188189## Gates190191- **No autofix.** v2's `align-copy` may add safe rewrites.192- **No JSDoc / TSDoc.** docs-craft territory.193- **No PR / review comments.** v1.x.194- **No commit BODY critique.** Subjects only in v1.195- **No B' bootstrap.** Same posture as naming/spec-craft.196- **No graph persistence.** Phase 1 MVP.197- **No non-TS/JS language support.** v1.x.198- **No author-attributed signals.** v1.x telemetry.199200## Escalation201202- **When LLM cost is too high:** drop `maxItemsPerFile` (default 20) or scope to specific surfaces with `--surfaces error`. Cost ≈ items × applicable rubrics × per-call.203- **When intentionally-bad test fixtures get flagged:** scope via `--files` to exclude fixtures. v1.x adds `<!-- copy-craft:skip -->` annotation + JSDoc tag.204- **When git surface skips though you ARE in a repo:** the walk goes up 10 levels looking for `.git`. If you're deeper than that, run from a closer cwd.205- **When PR surface skips with `gh auth status` failing:** run `gh auth login` first. Or scope away with `--surfaces error,log,comment`.206- **When `console.log` in a CLI file gets surface='log' instead of 'cli-output':** verify the file path contains `packages/cli/src/commands/` (or a similar default substring). Override with `craft.copy.cliOutputGlobs` config in v1.x.207208## Status209210**v1 — in implementation.** See:211212- Spec: `docs/changes/craft-pipeline/copy-craft/proposal.md`213- Roadmap entry: part of the `craft-pipeline` initiative214- Sibling craft skills: `naming-craft`, `spec-craft`215- Shared infrastructure: `packages/cli/src/shared/craft/` (extracted by spec-craft)216- Future: `align-copy` (FIX side, v2), docs-craft (prose docs), test-craft, code-craft