Spec Craft
LLM-judgment critique of spec quality (proposals + ADRs) against a curated rubric catalog from the spec-quality canon. Per-section critique with rubric-to-section mapping. Second member of the craft-pipeline initiative; highest-leverage craft skill because spec quality compounds across the entire planning → implementation → review lifecycle below it. Emits 3-axis findings (tier × impact × confidence per ADR 0019).
When to Use
- During PR review on a new or substantially-rewritten spec
- After authoring a proposal, before circulating it for review
- When onboarding a new contributor (audit specs they introduced)
- Periodically (per-sprint or per-release) to catch spec drift
- As the cross-cutting spec critic for harness-brainstorming (when newly-authored specs land)
- NOT for spec-structure enforcement (use
harness-soundness-review — that's the rule-based floor)
- NOT for README / general-doc critique (use
docs-craft)
- NOT for autofix / spec rewriting (this is judgment-only; v1.x may add
align-spec sibling)
- NOT for source-code comment critique (use
code-craft or docs-craft)
- NOT for RFCs 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 spec/proposal and ADR quality critique implemented over the shared contract (
packages/cli/src/spec-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: DISCOVER — Find spec files
Read project configuration. Check harness.config.json for:
craft.spec.enabled — gate (default true)
craft.spec.maxFiles — doc count cap (default 50)
craft.spec.maxSectionsPerFile — per-doc section cap (default 10)
Discover spec files:
- proposals:
docs/changes/*/proposal.md and docs/changes/*/<sub>/proposal.md (one level of nesting, for initiatives with sub-projects)
- ADRs:
docs/knowledge/decisions/*.md (excluding README)
- Restrict via
--kinds proposal or --kinds adr for single-kind runs.
Phase 2: PARSE — Split into named sections
For each spec file:
- Strip YAML frontmatter (
--- ... --- block at top, if present).
- Split by H2 (
## ...) into named sections. Each section captures:
heading — original H2 text (e.g., "Decisions", "Out-of-scope (v1)")
canonical — normalized form for rubric matching ("decisions", "out-of-scope-v1")
body — content between this H2 and the next
line / endLine — line range
- Subsections (H3) stay part of the parent H2's body. v1.x may add per-H3 critique for sections like Decisions that have one row per H3.
Phase 3: CRITIQUE — Per (file, section, rubric) loop
7 seed rubrics:
| Rubric |
Title |
Applies to |
SPEC-R001 |
Sharpness vs vagueness |
* (all sections) |
SPEC-R002 |
Cuts at the joints |
decisions, scope, technical-design |
SPEC-R003 |
Two readers, same understanding |
decisions, success-criteria |
SPEC-R004 |
Load-bearing decision vs ambient context |
decisions, overview |
SPEC-R005 |
Honest rationalizations |
rationalizations* (regex) |
SPEC-R006 |
Non-goals are non-goals |
out-of-scope*, non-goals* (regex) |
SPEC-R007 |
Stranger in 6 months |
* (all sections) |
For each eligible (section, rubric) pair:
- Build prompt with rubric description + spec file path + section heading + section body (truncated to 2000 chars if longer for cost control).
- LLM returns fenced JSON:
null (rubric doesn't apply / section is fine) OR { tier, impact, confidence, message }.
- On non-null: emit a
SpecFinding with cite.rubricId populated for ADR 0020 traceability.
Phase 4: REPORT — Aggregate + cost telemetry
Emit SpecCraftOutput:
{
findings: SpecFinding[];
summary: {
phaseRun: ['critique'];
durationMs: number;
llmCalls: { provider, model, count, costUsd };
catalog: { rubricsApplied: string[] };
docsScanned: number;
sectionsScanned: number;
runId: string;
}
}
Harness Integration
harness spec-craft — CLI entry. --files <glob> / --kinds proposal,adr / --sections decisions,scope / --max-files <n> / --max-sections-per-file <n> / --json / --verbose.
mcp__harness__spec_craft — MCP tool. Same input/output. Consumed by agents.
- Cross-cutting API:
critiqueSpecFile(file, opts) exported from packages/cli/src/spec-craft/index.ts. Future craft skills (or harness-brainstorming) can call this when they have a doc in hand without re-walking the project.
- Shared craft infrastructure (extracted on this PR):
LlmProvider, MockLlmProvider, derivePriority, 3-axis types all live in packages/cli/src/shared/craft/. design-craft + naming-craft + spec-craft import from there; design-craft + naming-craft keep their old import paths via re-export shims.
Success Criteria
See docs/changes/craft-pipeline/spec-craft/proposal.md for the full 34 success criteria. Highlights:
- 7 seed rubrics ship at
catalog/rubrics/<id>.ts (file-per-rubric, matches naming-craft)
- 3-axis output preserved (tier × impact × confidence, never collapsed)
cite.rubricId populated on every finding (ADR 0020)
- Section parser strips frontmatter; splits by H2 only
- Rubric-to-section mapping skips silently when rubric doesn't apply
critiqueSpecFile cross-cutting API works on a single file without project walk
- All existing design-craft + naming-craft tests still pass after shared/craft extraction (zero behavior change)
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 spec has all the required section headings, so it passes." |
Structural completeness is harness-soundness-review's floor. Spec-craft critiques the ceiling: a fully-structured Decisions section stuffed with vague qualifiers still fails SPEC-R001 and SPEC-R004. |
| "SPEC-R005 only applies to a section literally titled 'Rationalizations to Reject'." |
The mapping is a regex (rationalizations*). A section headed "Objections considered" matches the intent, and a strawmanned rejection fails SPEC-R005 regardless of the exact heading text. |
| "The Overview reads well, so I'll run every rubric against it to be thorough." |
Rubric-to-section mapping means most rubrics do not apply to most sections. SPEC-R002 (cuts-at-joints) targets decisions/scope/technical-design, not Overview. Running an inapplicable rubric manufactures noise. |
| "'A modern, scalable stack' is a fine summary, so SPEC-R001 passes." |
Sharpness-vs-vagueness flags exactly this: no framework named, no scale metric, no operational definition of "modern". A plausible-sounding summary is the vagueness the rubric targets. |
| "This out-of-scope item is really a deferred feature, so SPEC-R006 passes." |
Non-goals-are-non-goals asks whether the item is a true boundary or a disguised requirement. A deferred feature smuggled into Out-of-scope is precisely the failure SPEC-R006 exists to catch. |
Examples
Example: Vague Decisions section
Input: A proposal's ## Decisions section reads:
| Decision | Why |
|----------|-----|
| Use modern stack | scalable and clean |
| Defer auth | not in scope |
Output (mock LLM):
SPEC-R001 [polish/medium/medium] ## Decisions:34
"Modern stack" and "scalable and clean" are vague — no concrete framework
named, no metric for "scalable", no operational definition of "clean".
Sharpen: name the framework, state the scale target (req/sec, team size),
define what "clean" means in observable terms.
SPEC-R004 [polish/medium/medium] ## Decisions:34
The Decisions section pads load-bearing choices with vague qualifiers
rather than naming the trade-off chosen and the rejected alternative.
Example: Strawmanned Rationalizations
Input: A ## Rationalizations to reject section reads:
| "Use a different framework" | Other frameworks are worse |
Output:
SPEC-R005 [foundational/large/high] ## Rationalizations to reject:88
"Other frameworks are worse" is a strawman — not stated charitably, not
paired with a specific reason. Steelman the rejected position: name the
competing framework, name its strongest feature, then explain the specific
trade-off that made it unsuitable here.
Example: Empty project — no specs
Input: Project has no docs/changes/ or docs/knowledge/decisions/ directory.
Output:
No spec findings.
Summary: 0 findings across 0 docs (0 sections, 7 rubrics, 0 LLM calls, $0.0000, 3ms)
Gates
- No autofix. Sibling
align-spec deferred until signal warrants safe-to-apply rewrites.
- No README / general doc critique. docs-craft territory.
- No source-code comment critique. code-craft / docs-craft territory.
- No B' bootstrap. Same posture as naming-craft v1.
- No graph persistence. Phase 1 MVP.
- No vision/deep mode. Specs are text.
- No structural floor enforcement. harness-soundness-review checks the floor; spec-craft assumes the floor is satisfied and critiques the ceiling.
Escalation
- When LLM cost is too high: drop
maxSectionsPerFile to 5 or maxFiles to 25. Per-doc cost = sections × rubrics × per-call. Rubric-to-section mapping already prunes most calls; further: use --sections decisions to target the highest-value section.
- When a rubric produces high false-positive rate: v1 has no per-rubric disable; v1.x adds
craft.spec.disabledRubrics: ['SPEC-R007']. Until then: filter findings by cite.rubricId in your consumer.
- When a spec has intentionally aspirational vagueness (e.g., a manifesto-style Overview): SPEC-R001 will flag it; low-confidence findings are de-emphasized per ADR 0019. v1.x adds per-section opt-out via
<!-- spec-craft:skip --> HTML comment.
- When you want a doc-level summary instead of per-section findings: v1 is per-section only; v1.x adds a
--mode doc opt-in for whole-doc critique.
- When you want to critique RFCs: v1.x. For now, point
--files <rfc.md> at a single file — the section parser works on any markdown.
Status
v1 — in implementation. See:
- Spec:
docs/changes/craft-pipeline/spec-craft/proposal.md
- Roadmap entry: part of the
craft-pipeline initiative (the highest-leverage craft skill)
- Sibling craft skills:
harness-design-craft (design-pipeline), naming-craft (craft-pipeline)
- Shared infrastructure:
packages/cli/src/shared/craft/ (extracted on this PR)
- Future:
align-spec (FIX side), docs-craft, test-craft, code-craft — each can call critiqueSpecFile if they want spec-level critique for a doc they're already processing.
1---2name: spec-craft3description: Spec Craft4---5# Spec Craft67> LLM-judgment critique of spec quality (proposals + ADRs) against a curated rubric catalog from the spec-quality canon. Per-section critique with rubric-to-section mapping. Second member of the craft-pipeline initiative; highest-leverage craft skill because spec quality compounds across the entire planning → implementation → review lifecycle below it. Emits 3-axis findings (tier × impact × confidence per ADR 0019).89## When to Use1011- During PR review on a new or substantially-rewritten spec12- After authoring a proposal, before circulating it for review13- When onboarding a new contributor (audit specs they introduced)14- Periodically (per-sprint or per-release) to catch spec drift15- As the cross-cutting spec critic for harness-brainstorming (when newly-authored specs land)16- NOT for spec-structure enforcement (use `harness-soundness-review` — that's the rule-based floor)17- NOT for README / general-doc critique (use `docs-craft`)18- NOT for autofix / spec rewriting (this is judgment-only; v1.x may add `align-spec` sibling)19- NOT for source-code comment critique (use `code-craft` or `docs-craft`)20- NOT for RFCs in v1 (v1.x)2122## Capability Roles2324<!-- 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. -->2526- **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.27- **Provides (Provider):** **this skill** — a spec/proposal and ADR quality critique implemented over the shared contract (`packages/cli/src/spec-craft/`).28- **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 shape2930## Process3132### Phase 1: DISCOVER — Find spec files33341. **Read project configuration.** Check `harness.config.json` for:35 - `craft.spec.enabled` — gate (default `true`)36 - `craft.spec.maxFiles` — doc count cap (default 50)37 - `craft.spec.maxSectionsPerFile` — per-doc section cap (default 10)38392. **Discover spec files:**40 - **proposals:** `docs/changes/*/proposal.md` and `docs/changes/*/<sub>/proposal.md` (one level of nesting, for initiatives with sub-projects)41 - **ADRs:** `docs/knowledge/decisions/*.md` (excluding README)42 - Restrict via `--kinds proposal` or `--kinds adr` for single-kind runs.4344### Phase 2: PARSE — Split into named sections4546For each spec file:47481. **Strip YAML frontmatter** (`--- ... ---` block at top, if present).492. **Split by H2** (`## ...`) into named sections. Each section captures:50 - `heading` — original H2 text (e.g., `"Decisions"`, `"Out-of-scope (v1)"`)51 - `canonical` — normalized form for rubric matching (`"decisions"`, `"out-of-scope-v1"`)52 - `body` — content between this H2 and the next53 - `line` / `endLine` — line range543. Subsections (H3) stay part of the parent H2's body. v1.x may add per-H3 critique for sections like Decisions that have one row per H3.5556### Phase 3: CRITIQUE — Per (file, section, rubric) loop57587 seed rubrics:5960| Rubric | Title | Applies to |61| ----------- | ---------------------------------------- | ---------------------------------------- |62| `SPEC-R001` | Sharpness vs vagueness | `*` (all sections) |63| `SPEC-R002` | Cuts at the joints | `decisions`, `scope`, `technical-design` |64| `SPEC-R003` | Two readers, same understanding | `decisions`, `success-criteria` |65| `SPEC-R004` | Load-bearing decision vs ambient context | `decisions`, `overview` |66| `SPEC-R005` | Honest rationalizations | `rationalizations*` (regex) |67| `SPEC-R006` | Non-goals are non-goals | `out-of-scope*`, `non-goals*` (regex) |68| `SPEC-R007` | Stranger in 6 months | `*` (all sections) |6970For each eligible (section, rubric) pair:71721. Build prompt with rubric description + spec file path + section heading + section body (truncated to 2000 chars if longer for cost control).732. LLM returns fenced JSON: `null` (rubric doesn't apply / section is fine) OR `{ tier, impact, confidence, message }`.743. On non-null: emit a `SpecFinding` with `cite.rubricId` populated for ADR 0020 traceability.7576### Phase 4: REPORT — Aggregate + cost telemetry7778Emit `SpecCraftOutput`:7980```ts81{82 findings: SpecFinding[];83 summary: {84 phaseRun: ['critique'];85 durationMs: number;86 llmCalls: { provider, model, count, costUsd };87 catalog: { rubricsApplied: string[] };88 docsScanned: number;89 sectionsScanned: number;90 runId: string;91 }92}93```9495## Harness Integration9697- **`harness spec-craft`** — CLI entry. `--files <glob>` / `--kinds proposal,adr` / `--sections decisions,scope` / `--max-files <n>` / `--max-sections-per-file <n>` / `--json` / `--verbose`.98- **`mcp__harness__spec_craft`** — MCP tool. Same input/output. Consumed by agents.99- **Cross-cutting API:** `critiqueSpecFile(file, opts)` exported from `packages/cli/src/spec-craft/index.ts`. Future craft skills (or `harness-brainstorming`) can call this when they have a doc in hand without re-walking the project.100- **Shared craft infrastructure (extracted on this PR):** `LlmProvider`, `MockLlmProvider`, `derivePriority`, 3-axis types all live in `packages/cli/src/shared/craft/`. design-craft + naming-craft + spec-craft import from there; design-craft + naming-craft keep their old import paths via re-export shims.101102## Success Criteria103104See `docs/changes/craft-pipeline/spec-craft/proposal.md` for the full 34 success criteria. Highlights:105106- 7 seed rubrics ship at `catalog/rubrics/<id>.ts` (file-per-rubric, matches naming-craft)107- 3-axis output preserved (tier × impact × confidence, never collapsed)108- `cite.rubricId` populated on every finding (ADR 0020)109- Section parser strips frontmatter; splits by H2 only110- Rubric-to-section mapping skips silently when rubric doesn't apply111- `critiqueSpecFile` cross-cutting API works on a single file without project walk112- All existing design-craft + naming-craft tests still pass after shared/craft extraction (zero behavior change)113114## Rationalizations to Reject115116These 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.117118| Rationalization | Why It Is Wrong |119| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |120| "This spec has all the required section headings, so it passes." | Structural completeness is `harness-soundness-review`'s floor. Spec-craft critiques the ceiling: a fully-structured Decisions section stuffed with vague qualifiers still fails SPEC-R001 and SPEC-R004. |121| "SPEC-R005 only applies to a section literally titled 'Rationalizations to Reject'." | The mapping is a regex (`rationalizations*`). A section headed "Objections considered" matches the intent, and a strawmanned rejection fails SPEC-R005 regardless of the exact heading text. |122| "The Overview reads well, so I'll run every rubric against it to be thorough." | Rubric-to-section mapping means most rubrics do not apply to most sections. SPEC-R002 (cuts-at-joints) targets decisions/scope/technical-design, not Overview. Running an inapplicable rubric manufactures noise. |123| "'A modern, scalable stack' is a fine summary, so SPEC-R001 passes." | Sharpness-vs-vagueness flags exactly this: no framework named, no scale metric, no operational definition of "modern". A plausible-sounding summary is the vagueness the rubric targets. |124| "This out-of-scope item is really a deferred feature, so SPEC-R006 passes." | Non-goals-are-non-goals asks whether the item is a true boundary or a disguised requirement. A deferred feature smuggled into Out-of-scope is precisely the failure SPEC-R006 exists to catch. |125126## Examples127128### Example: Vague Decisions section129130**Input:** A proposal's `## Decisions` section reads:131132```133| Decision | Why |134|----------|-----|135| Use modern stack | scalable and clean |136| Defer auth | not in scope |137```138139**Output (mock LLM):**140141```142SPEC-R001 [polish/medium/medium] ## Decisions:34143 "Modern stack" and "scalable and clean" are vague — no concrete framework144 named, no metric for "scalable", no operational definition of "clean".145 Sharpen: name the framework, state the scale target (req/sec, team size),146 define what "clean" means in observable terms.147SPEC-R004 [polish/medium/medium] ## Decisions:34148 The Decisions section pads load-bearing choices with vague qualifiers149 rather than naming the trade-off chosen and the rejected alternative.150```151152### Example: Strawmanned Rationalizations153154**Input:** A `## Rationalizations to reject` section reads:155156```157| "Use a different framework" | Other frameworks are worse |158```159160**Output:**161162```163SPEC-R005 [foundational/large/high] ## Rationalizations to reject:88164 "Other frameworks are worse" is a strawman — not stated charitably, not165 paired with a specific reason. Steelman the rejected position: name the166 competing framework, name its strongest feature, then explain the specific167 trade-off that made it unsuitable here.168```169170### Example: Empty project — no specs171172**Input:** Project has no `docs/changes/` or `docs/knowledge/decisions/` directory.173174**Output:**175176```177No spec findings.178179Summary: 0 findings across 0 docs (0 sections, 7 rubrics, 0 LLM calls, $0.0000, 3ms)180```181182## Gates183184- **No autofix.** Sibling `align-spec` deferred until signal warrants safe-to-apply rewrites.185- **No README / general doc critique.** docs-craft territory.186- **No source-code comment critique.** code-craft / docs-craft territory.187- **No B' bootstrap.** Same posture as naming-craft v1.188- **No graph persistence.** Phase 1 MVP.189- **No vision/deep mode.** Specs are text.190- **No structural floor enforcement.** harness-soundness-review checks the floor; spec-craft assumes the floor is satisfied and critiques the ceiling.191192## Escalation193194- **When LLM cost is too high:** drop `maxSectionsPerFile` to 5 or `maxFiles` to 25. Per-doc cost = sections × rubrics × per-call. Rubric-to-section mapping already prunes most calls; further: use `--sections decisions` to target the highest-value section.195- **When a rubric produces high false-positive rate:** v1 has no per-rubric disable; v1.x adds `craft.spec.disabledRubrics: ['SPEC-R007']`. Until then: filter findings by `cite.rubricId` in your consumer.196- **When a spec has intentionally aspirational vagueness (e.g., a manifesto-style Overview):** SPEC-R001 will flag it; low-confidence findings are de-emphasized per ADR 0019. v1.x adds per-section opt-out via `<!-- spec-craft:skip -->` HTML comment.197- **When you want a doc-level summary instead of per-section findings:** v1 is per-section only; v1.x adds a `--mode doc` opt-in for whole-doc critique.198- **When you want to critique RFCs:** v1.x. For now, point `--files <rfc.md>` at a single file — the section parser works on any markdown.199200## Status201202**v1 — in implementation.** See:203204- Spec: `docs/changes/craft-pipeline/spec-craft/proposal.md`205- Roadmap entry: part of the `craft-pipeline` initiative (the highest-leverage craft skill)206- Sibling craft skills: `harness-design-craft` (design-pipeline), `naming-craft` (craft-pipeline)207- Shared infrastructure: `packages/cli/src/shared/craft/` (extracted on this PR)208- Future: `align-spec` (FIX side), docs-craft, test-craft, code-craft — each can call `critiqueSpecFile` if they want spec-level critique for a doc they're already processing.