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

SKILL.md

GitHub

User 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.mjs and artifact-lint.mjs report zero ERRORs, or each survivor is explained in the report
  • jest --onlyChanged, lint and typecheck green 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.