speckit-harden
Quality gate for the code a feature produced: runs the mechanical audits, judges durability and bloat against a fixed rubric, fixes what it finds, and re-verifies. Runs between /speckit-implement and
Files2
View on GitHubSKILL.md.claude/skills/speckit-harden/SKILL.md
GitHubUser Input
$ARGUMENTS
You MUST consider the user input before proceeding (if not empty). It narrows scope (a package, a directory). Empty means the whole branch.
Mutation testing is not part of this command or of any task: Stryker runs
only in the Mutation workflow (.github/workflows/mutation.yml, every 3 days
on main and by workflow_dispatch, sharded, never on this machine), and a
surviving mutant is picked from the Project like any other task.
Goal
/speckit-analyze judges the artifacts. spec-reviewer judges the diff against
the spec. Neither asks the question this command exists for: is the code that
shipped durable, and is any of it unnecessary?
Those two are one question. Code nothing needs is code that will rot — an export with no importer, a layer with one implementation, a test that passes with the implementation deleted. Principle I is not a style preference here; it is the cheapest reliability mechanism available, because the most reliable line is the one that was never written.
Operating Constraints
- Fixes, not just findings. Unlike
spec-reviewer, this command edits. Every fix it makes is verified by the suite before it reports. - Never weaken a test to make a check pass. Deleting a test,
.skipping it, loosening an assertion, or adding a suppression to clear a finding is the one thing this command must not do. If a finding cannot be fixed honestly, report it. - No new abstraction. A quality pass that introduces a helper "for the next case" has failed at its own job.
- Behavior stays put. Every change here is a refactor, a deletion, or a test addition. If something needs a behavior change, that is a finding for the user, not an edit.
Execution Steps
0. Size the job
git diff --stat $(git merge-base HEAD main)..HEAD -- apps libs e2e | tail -1
Under ~200 changed lines with no new files, the built-in /simplify skill is
the whole of step 3 — run steps 1 and 2, then /simplify, then step 4. Above
that, every step below.
1. Mechanical audits
node .claude/scripts/artifact-lint.mjs # spec/tasks defects + untestable/compound/uncovered FRs
node .claude/scripts/diff-audit.mjs # dead surface, build-time traps + unjustified dependencies
npm run lint && npm run typecheck
Then, when the IDE is open, the second static analyzer this repo has and never
runs: mcp__webstorm__get_file_problems on each changed source file
(projectPath = repo root, errorsOnly: false). IntelliJ's inspections
overlap biome very little — unreachable code, suspicious promise handling,
redundant awaits, unused type parameters. An MCP connection error means the
IDE is closed; note "inspections: IDE not running" and continue, never retry.
If the diff touches libs/contracts, anything under auth, secret material,
or an input boundary (HTTP, filesystem, env), run the built-in /security-review
skill here and fold its findings in. This repo stores crypto inventory with a
CHECK constraint against persisting secret values; a feature near it earns
the pass.
diff-audit.mjs covers only what biome and tsc structurally cannot see:
cross-file dead exports, the per-app relative-import extension rule (which fails
at build time, not typecheck), new dependencies, added suppressions,
and new source files no test mentions.
Fix every ERROR. Judge each WARN — several are legitimate by design (a test seam,
a deliberate Stryker disable on an equivalent mutant); say which you kept and
why.
1b. Design audit
git diff --name-only <base>..HEAD -- apps/web libs/ui-cockpit
When that lists a file, run the design-audit skill on the changed component
folders and on libs/ui-cockpit/src/styles/cockpit.css, with its scan.mjs
and contrast.mjs helpers. Fix every high finding, run it again, and list
what is left on a "Design audit" line in the report. When it lists nothing,
the line reads "Design audit: skipped, no web file changed".
2. Adversarial tests
A delta run, dispatched once per change beside the two reviewers, in
/speckit-auto phase 14's one message; at levels 2–3 harden reads the
adversary's result there and does not dispatch it here. That dispatch passes
the adversary specs already on the branch:
git diff --name-only --diff-filter=A <merge-base> -- '*.adversary.spec.*'
with the feature directory and the diff's public surface, asking for the surface those files do not cover yet. An empty list means a full run over the whole surface. The adversary has not seen the implementation and must not; it writes the tests the author's model of the code did not suggest. Every failing test it leaves is either a defect or a spec gap — you decide which, and a defect is fixed under step 4's rules while a spec gap goes to "Needs you". Its report is not shown to the user; relay the failing rows.
3. No durability read here
The code-reviewer subagent runs once per change, in the review
(/speckit-review, or /speckit-auto phase 14), over the whole diff: a read
here would judge the same lines twice. Every adversary defect is fixed in
step 4, pre-existing or adjacent included: fix every verified finding in this
PR; an owner decision is a needs-confirmation question
(speckit-tracker-sync blocked <question>).
4. Fix, verify, repeat
Apply the fixes in one pass, then:
npx jest --onlyChanged && npm run lint && npm run typecheck
Each of these: run it into a log and read only the exit code, the summary and the failures (AGENTS.md "Agent replies": tail -n 40, then grep -nE '✕|●|FAIL|Error' on a failure).
Re-run the audits from step 1. Loop at most twice; a third round means the finding needs the user, not another attempt.
5. Report
## Harden: <feature> (<range>)
| Check | Before | After |
|-------|--------|-------|
| diff-audit errors | n | n |
| artifact-lint errors | n | n |
| tests | n | n |
Design audit: <high n → 0, medium n | skipped, no web file changed>
Fixed: <one line each>
Kept deliberately: <finding — why it is correct as it stands>
Needs you: <finding this command must not decide>
Done When
-
diff-audit.mjsandartifact-lint.mjsreport zero ERRORs, or each survivor is explained in the report -
jest --onlyChanged,lintandtypecheckgreen after the last edit - Every fix is a refactor, deletion, or added test — no behavior changed, no test weakened
- Report delivered with before/after numbers, not adjectives
Agent Execution Rules: harden deltas
The constitution's Agent Execution Rules apply in full. Specific to this command:
- A finding you cannot fix honestly is reported, never suppressed. "Needs you" is a legitimate and expected section of the report.
- No internal identifiers in anything you write — no FR ids, task ids, or ticket
keys in comments or test titles (Constitution II). The one allowed form is a
whole-line
// @traces <feature>-FR-<n>comment in a test file. - Deletion is the preferred fix. Reach for it before refactoring, and refactor before adding.
The repair loop has a cap
Every fix-and-re-verify lap is counted:
node .claude/scripts/run-state.mjs repair
It exits 1 at the tenth lap (SPECKIT_MAX_REPAIR_ITERATIONS) and leaves
.specify/run-state.json at status: blocked, blocking_condition: repair-loop-exceeded. Borrowed from BMAD's autonomous build, whose blocking
conditions include "review repair loop exceeded 5 iterations".
When it fires, stop and report. A cycle that has not converged in ten laps is not one lap from converging: either a finding is wrong, or the requirement is, and both are decisions rather than another edit.