Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,19 @@ jobs:
shell: pwsh
run: ./tests/hooks/run-conformance.ps1 -SelfTest

# --- Contract-lint fixtures: same posture as the hook conformance pair
# above. One pwsh runner drives BOTH linter implementations per case, so
# parity is asserted rather than inferred from two green jobs. No `if:`
# guard - every OS runs both, which is what makes windows-latest cover the
# CRLF path (*.md is deliberately not pinned to LF at the repo root) -----
- name: Contract lint fixtures (bash vs PowerShell)
shell: pwsh
run: ./tests/contract-lint/run-selftest.ps1

- name: Contract lint self-test (dead-linter detection)
shell: pwsh
run: ./tests/contract-lint/run-selftest.ps1 -SelfTest

# --- Install -> uninstall round-trip (CLAUDE.md sandbox recipe) --------
- name: Install -> uninstall round-trip (bash)
if: runner.os == 'Linux' || runner.os == 'macOS'
Expand Down
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **Check 8: cross-file contract lint** (`scripts/contract-lint.ps1` / `scripts/contract-lint.sh`,
SW-26 wave 1). Where Check 7 guards inventory, Check 8 guards the relationships between commands,
agents and skills. 17 rules across three bands: `CL0xx` reference resolution, `CL3xx` gate
integrity, `CL9xx` suppression hygiene. Deterministic file ops, no subagent, TSV on stdout, exit
`2` when it cannot run. Wired into both validators and into CI on all three OSes.
- `contractLint` subtree in `specwright.manifest.json`: scan scope, the rule registry (the single
source of every rule's severity, so a BLOCK/WARN divergence between the twins is structurally
impossible), declared gate contracts, spec artifact names, skill-consumer escapes and the CL305
override vocabulary. Each linter carries a registry parity guard that exits `2` when the rules it
dispatches and the registry disagree.
- Gate counts are now published claims: `contractLint.gates.<file>.hard` seeds Check 7 quantities,
giving `README <- manifest` there and `manifest <- disk` in CL302, hence transitively
`README == disk`. 21 new `docClaims` plus a `N hard gates` claim phrase.
- `tests/contract-lint/` fixture suite - a minimal valid mini-engine plus one overlay per rule, five
false-positive guards and one must-still-bite case. Goldens pin a seed marker, never a line
number. `run-selftest.ps1` drives both implementations in one process so parity is asserted, and
`-SelfTest` proves the harness detects a linter that reports nothing.
- `docs/contract-lint.md` - rule catalogue, suppression syntax, manifest surface, and why a declared
gate count belongs in a manifest that otherwise stores no counts. `CONTRIBUTING.md` gained a
matching section.
- Machine-readable `Inputs (required): ...` / `Inputs (optional): ...` declarations under every
TASK/mode/workflow-type heading in `agents/*.md` (22 sections across `code-explorer`, `debugger`,
`implementer`, `reviewer`, `spec-architect`; `docs-writer` has no mode dispatch) - prerequisite
for the invocation-contract validator in SW-26 (SW-25). Format documented in `CONTRIBUTING.md`
under "Agents". No agent behaviour changed.

### Fixed
- `docs/architecture.md` listed four items against a gate count of three for `/sd:feature`, one of
them naming a per-task review gate removed when the workflow moved to batch review. Found by
writing CL302, fixed before the linter landed.
- `commands/setup.md`'s detected-facts gate had no literal `STOP` (the nearest one belonged to the
migration gate above it), and neither setup gate offered a machine-readable option set. Both were
real CL300/CL301 violations on disk.
- Audit of every `commands/*.md` invocation site against the new declarations turned up three
drifted contracts, now corrected: `/sd:bug`'s hypothesis-verify loop omitted `EVIDENCE_DIR` from
its `sd-debugger` `TASK = verify` call, so verification evidence had nowhere to be saved
Expand Down
14 changes: 13 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ bash -n hooks/bash/spec-gate.sh
grep -nP "[^\x00-\x7F]" hooks/powershell/*.ps1 install/*.ps1
```

```powershell
# Cross-file contract lint (Check 8) - run it directly while editing prompts
bash scripts/contract-lint.sh --root . # exit 0 clean, 1 on BLOCK, 2 cannot run
.\scripts\contract-lint.ps1 -Root .

# Fixture suite. Drives BOTH implementations in one process, so parity is asserted
.\tests\contract-lint\run-selftest.ps1
.\tests\contract-lint\run-selftest.ps1 -SelfTest # proves the harness notices a dead linter
```

Every PR adds a line under `## [Unreleased]` in `CHANGELOG.md` (Keep a Changelog / SemVer).

## Repo structure → install targets
Expand All @@ -58,6 +68,7 @@ Source filenames are unprefixed (`agents/reviewer.md`); the `sd-`/`sd:` namespac
- **Agents** declare frontmatter: `name`, `description`, `color`, `model`, minimal `tools` allowlist, and a `skills:` list. Tool allowlists enforce roles structurally — the reviewer has no write tools, so it *cannot* auto-fix. Heavy reasoning agents (architect, debugger, reviewer) use `sonnet`; mechanical agents (explorer, implementer) use `haiku`.
- **Skills** are shared rule packs loaded into agent context via frontmatter reference. A rule used by multiple agents (e.g. `sd-evidence-citation`, used by 3) lives in one `SKILL.md`, never copy-pasted into agent bodies.
- **Hooks** inject context (`prompt-router` on UserPromptSubmit, `subagent-retro` on SubagentStop) or guard edits (`spec-gate` on PreToolUse blocks code edits with no in-progress spec). `spec-gate` denials emit a dual-format JSON object carrying both the new schema (`hookSpecificOutput.permissionDecision: "deny"`) and the legacy schema (`decision: "block"`) for CLI version compatibility. `spec-gate` and `subagent-retro` also *record*: metadata-only events (spec ID, phase, decision - never a path) appended to `.specs/_metrics/events.jsonl`, opt-out via `hooks.metrics.enabled: false`.
- **The manifest guards two different things.** `specwright.manifest.json`'s `areas`/`docClaims` guard *inventory* (Check 7: does a number in the docs match disk?) and derive every count from disk. Its `contractLint` subtree guards *relationships* (Check 8: does this command invoke an agent that exists, does this gate halt, does this workflow declare the gate count it has?). Inventory is always derived; a gate count is a declared contract and is written down on purpose — `docs/contract-lint.md` states the test that separates the two. Adding a lint rule means four edits (registry, both linters, a fixture, the doc table), and each edge is guarded by a different mechanism, so it cannot be half-done.
- **Spec artifacts** (`.specs/<ID>/00-spec.md` … `05-retro.md`) are the input contract between agents, not after-the-fact docs. Spec templates intentionally leave cross-phase fields empty, marked with a `<<PHASE-N: ...>>` token (plus an explanatory `<!-- ... -->` comment) — workflows enforce sequencing through those empty fields. Do not pre-fill them.

## Hard rules when editing
Expand All @@ -67,7 +78,8 @@ Source filenames are unprefixed (`agents/reviewer.md`); the `sd-`/`sd:` namespac
3. **Model fields are aliases only** (`sonnet`, `haiku`, `opus`, `inherit`) — never full model IDs.
4. **Stack-agnostic, no exceptions.** Commands and agents must not contain hardcoded stack commands (`dotnet test`, `npm test`) or language assumptions; reference `commands.test` etc. from `project-config.json`. An agent that hardcodes a stack is a bug.
5. **Minimal tool allowlists.** Read-only agents never get `Write`; add a tool only if the role requires it.
6. **Templates** use `<<placeholder>>` for user-filled fields and stay short. Spec templates also
6. **Gates are machine-checked.** A gate heading must halt (a literal `STOP` inside its block) and offer a machine-readable option set — a slash-separated parenthetical like `(yes / revise / abort)`, or two or more top-level `- ` bullets. A HARD gate must not *list* an override as a choice; describing one in prose is fine. Changing how many gates a workflow has is a deliberate two-file edit: the heading and `contractLint.gates` in the manifest.
7. **Templates** use `<<placeholder>>` for user-filled fields and stay short. Spec templates also
use `<<PHASE-N: ...>>` for cross-phase fields that Phase N must fill from measured evidence —
the two forms have opposite rules (author-fill must be gone by `approved`; phase-deferred must
still be there), and `/sd:spec validate` enforces both. Never pre-fill a `<<PHASE-N: ...>>`.
Expand Down
49 changes: 49 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,55 @@ Check 7 needs `jq` on Unix and **fails loudly without it**. This is the opposite
below (hooks exit `0` silently when `jq` is missing so they never block a user on their own bugs) -
a validator that skipped itself for a missing tool would turn CI green while checking nothing.

### Contract lint (Check 8)

Where Check 7 guards *inventory*, Check 8 guards the **relationships between** the prompt files:
which agent a command invokes, which skill an agent loads, which template a prompt reads, how many
hard gates a workflow declares. It is a script, not a prompt - `scripts/contract-lint.{ps1,sh}`,
configured entirely from the manifest's `contractLint` subtree. Full rule catalogue and rationale:
[`docs/contract-lint.md`](docs/contract-lint.md).

Run it directly while iterating:

```bash
bash scripts/contract-lint.sh --root .
```
```powershell
.\scripts\contract-lint.ps1 -Root .
```

Exit `0` means no BLOCK findings, `1` means at least one, and **`2` means it could not run at all**
(missing manifest, missing `jq`, or the registry parity guard tripped). Check 8 treats `2` as a
failure for the same reason Check 7 refuses to skip itself.

**Suppressing a finding.** Rarely, a violation is correct on purpose. Put a comment on the offending
line or the line above it, naming the rule and giving a real reason:

```text
<!-- contract-lint: allow CL305 - the option here buys a logged constitution exception rather than a way past the requirement -->
```

Three things constrain that escape hatch, and all three are enforced:

- **The reason is mandatory.** Under ten non-separator characters fails as CL900. "`- x`" is not a
reason.
- **The rule id must exist.** A typo fails as CL901 rather than silently suppressing nothing.
- **It must actually suppress something.** A suppression that outlives the finding it was written
for fails as CL902 - the same anti-rot posture as Check 7's vacuous-claim rule.

A suppression can never suppress CL900, CL901 or CL902; that would be a self-authorizing loophole.

**Adding a rule** means four edits, and skipping any one of them fails CI: a `contractLint.rules`
registry entry, a rule function in *both* implementations, a fixture case under
`tests/contract-lint/` whose `expected.json` names the rule, and a row in `docs/contract-lint.md`.
Each edge of that square is guarded by a different mechanism - the linters' own registry parity
guard, and invariants C and D in `tests/contract-lint/run-selftest.ps1`.

`tests/contract-lint/run-selftest.ps1` is the fixture suite. Like the hook conformance harness it is
a single pwsh script by design: it runs both implementations in one process, so parity is asserted
rather than inferred. `-SelfTest` swaps in a linter that reports nothing and asserts the harness
notices.

---

## PR process
Expand Down
4 changes: 2 additions & 2 deletions commands/adr.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: Author an Architecture Decision Record (ADR) from a spec's decisions via sd-docs-writer. One hard gate before keeping the file.
description: Author an Architecture Decision Record (ADR) from a spec's decisions via sd-docs-writer. 1 hard gate before keeping the file.
argument-hint: <spec-ID | "decision title">
---

Expand Down Expand Up @@ -61,7 +61,7 @@ the ADR stays `proposed` until they change its status to `accepted`.
## Rules (hard constraints)

- **Decisions come from the source, never invented.** Empty or absent decision content aborts the command.
- **One hard gate.** Nothing is kept on disk without explicit approval.
- **1 hard gate.** Nothing is kept on disk without explicit approval.
- **ADRs are not specs.** No `.specs/index.md` lifecycle entry; ADRs live under `.specs/_adr/` with their
own numbering.
- **The constitution is never edited here.** Amending a rule is a separate `/sd:refactor` or a manual ADR
Expand Down
2 changes: 2 additions & 0 deletions commands/perf.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,11 @@ STOP. Display Target + Methodology. Ask:
STOP. Two cases:

**Case A: baseline already meets SLA goal.**
<!-- contract-lint: allow CL305 - Case A is the already-at-goal branch; 'proceed anyway' there buys a logged constitution exception, not a way past the baseline requirement, which stays unconditional -->
> Baseline p95=<X> already meets SLA goal p95<<goal>. No optimization needed. Close PERF-<slug> as 'done' with no changes? (yes / proceed anyway / abort)
- `yes` -> set status=`in-progress` (no hotspot work occurs, but the state machine has no
approved -> done shortcut), then jump to Phase 6 close-out with summary "no work needed".
<!-- contract-lint: allow CL305 - same exception as the option line above; the bullet only explains what selecting it costs -->
- `proceed anyway` -> requires explicit constitution exception ("optimizing past SLA"). Log to retro.

**Case B: baseline below SLA goal.**
Expand Down
9 changes: 5 additions & 4 deletions commands/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ and continue (in `complete` -> clean early exit; in `partial` -> continue fillin
Otherwise print ONE table grouped by file (each line showing exact before -> after with a
HIGH/MED/LOW tag), then STOP for explicit approval:

> Reply "go" to apply (each file backed up first), "skip" to leave `.claude/*` untouched, or name
> specific lines to exclude (e.g. "skip the models lines").
> Reply to apply every change (each file backed up first), leave `.claude/*` untouched, or name the
> specific lines to exclude, e.g. "skip the models lines". (go / skip / <lines to exclude>)

This is a confirmation of a batch, not a 4th interrogation question - the 3-question rule still holds.

Expand Down Expand Up @@ -186,7 +186,7 @@ constitution rules.

### Gate - confirm detected facts (one batch confirmation, not a question)

Print a single table of detected facts and ask the user to confirm before writing:
Print a single table of detected facts, then STOP for explicit approval before writing:

```
Detected (edit any before I write, or say "go"):
Expand All @@ -196,7 +196,8 @@ Detected (edit any before I write, or say "go"):
Commands build/test/lint/run/coverage : <values or "<<placeholder>>">
```

> Review these. Reply with corrections (e.g. "tests = test, drop the Infrastructure layer") or "go".
> Review these. Reply to accept them as-is, or send corrections such as "tests = test, drop the
> Infrastructure layer". (go / <corrections>)

This is a confirmation of a batch, not a 4th interrogation question - the 3-question rule still holds.
Anything the user does not correct is used as-is; anything still unknown stays `<<placeholder>>`.
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ The 5 workflow commands have these gate counts:

| Workflow | Gates | Why |
|---|---|---|
| `/sd:feature` | 3 | spec, plan, per-task review, integration |
| `/sd:feature` | 3 | spec, plan, integration + review |
| `/sd:bug` | 5 | symptom, reproduction (HARD), root cause, failing test, regression |
| `/sd:rca` | 3 | evidence, hypotheses, root cause |
| `/sd:refactor` | 6 | spec, coverage, post-test, plan, per-batch tests, holistic review |
Expand Down
Loading
Loading