Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
88bdf89
SW-25: feat: enhance agent documentation with machine-readable input …
developzoneio Jul 28, 2026
a5cb04b
SW-26: add Check 8 cross-file contract lint (wave 1) (#25)
developzoneio Jul 29, 2026
eef37dc
SW-32: add Check 8 CL1xx invocation contract (wave 2)
developzoneio Jul 29, 2026
7abf1d8
SW-33: add Check 8 CL2xx role and tool integrity (wave 3a)
developzoneio Jul 29, 2026
15f121a
SW-34: add Check 8 CL4xx stack-agnostic prose + CL306 (wave 3b)
developzoneio Jul 30, 2026
0a6c837
SW-35: add Check 8 CL5xx file budgets (wave 4)
developzoneio Jul 30, 2026
81758a6
SW-26: promote CL200/CL306/CL400 from WARN to BLOCK
developzoneio Jul 31, 2026
9a43dcc
SW-30: add examples/fixture-project - runnable non-.NET example
developzoneio Jul 31, 2026
1c140a2
SW-27: add tests/e2e headless behavioral eval harness
developzoneio Aug 1, 2026
e4a11f9
SW-28: close Check 7 blind spot on version/release-state claims
developzoneio Aug 3, 2026
c6695f1
SW-29: stamp the installed engine version at install time
developzoneio Aug 6, 2026
af12c9f
SW-29: report engine-version drift in /sd:setup, drop dead $schema claim
developzoneio Aug 9, 2026
fd04863
SW-31: ship threshold-calibration machinery, verdict insufficient data
developzoneio Aug 9, 2026
81b3fd3
SW-37: add sd-port-fidelity skill, raise agents byte ratchet
developzoneio Aug 9, 2026
d165296
SW-38: add PORT spec prefix, port spec template, snapshot artifact la…
developzoneio Aug 9, 2026
0d0c7f9
SW-39: add port-extract TASK mode to sd-code-explorer
developzoneio Aug 9, 2026
d0e81b3
SW-40: add port-parity adjudication gate to sd-reviewer
developzoneio Aug 9, 2026
78a766d
SW-41: add /sd:port pipeline - 10 phases, 6 gates, 4 hard
developzoneio Aug 9, 2026
b93ea31
Add Ko-fi sponsor badge and support link to README
developzoneio Aug 9, 2026
bfd4d14
Use friendlier Ko-fi badge and wording in README
developzoneio Aug 9, 2026
f2b4596
Fix oversized Ko-fi badge to match other badge sizes
developzoneio Aug 9, 2026
fa6ea38
SW-42: add Spawned specs section to feature/bug/refactor/perf specs
developzoneio Aug 9, 2026
4bc97d5
Restructure README to lead with evidence
developzoneio Aug 9, 2026
dcce3e9
Trim README from 390 to 263 lines
developzoneio Aug 9, 2026
ab66a2c
Split install blocks back out per platform
developzoneio Aug 9, 2026
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
21 changes: 21 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 All @@ -131,6 +144,12 @@ jobs:
for area in commands agents hooks templates skills; do
test ! -d "$base/$area/sd" || { echo "FAIL: $area/sd survived uninstall"; exit 1; }
done
remaining="$(find "$base" -type f)"
if [ -n "$remaining" ]; then
echo "FAIL: files survived uninstall:"
echo "$remaining"
exit 1
fi
rm -rf "$base"
echo "round-trip OK"

Expand All @@ -151,5 +170,7 @@ jobs:
foreach ($area in 'commands','agents','hooks','templates','skills') {
if (Test-Path "$base/$area/sd") { Write-Error "FAIL: $area/sd survived uninstall"; exit 1 }
}
$remaining = Get-ChildItem -LiteralPath $base -Recurse -File -Force
if ($remaining) { Write-Error "FAIL: files survived uninstall:`n$($remaining.FullName -join "`n")"; exit 1 }
Remove-Item -Recurse -Force $base
Write-Host 'round-trip OK'
43 changes: 43 additions & 0 deletions .github/workflows/e2e-nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: E2E behavioral eval (nightly)

# Headless behavioral eval harness for commands and gates (SW-27). Drives real
# `claude -p` sessions - real API cost and multi-minute wall clock - so this
# is deliberately NOT wired into ci.yml's push/pull_request job. See
# tests/e2e/README.md for what each scenario asserts, the isolation model,
# and the permission-mode pitfall (acceptEdits silently ignores a hook's
# deny - see that file before changing --permission-mode here).

on:
schedule:
- cron: '17 3 * * *' # 03:17 UTC daily - avoid the top-of-hour stampede
workflow_dispatch: {}

jobs:
e2e:
name: e2e (ubuntu-latest)
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install claude CLI
run: npm install -g @anthropic-ai/claude-code

- name: Run e2e suite
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
shell: pwsh
run: ./tests/e2e/run-e2e.ps1

- name: Run e2e self-test (guard-neutering detection)
if: always()
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
shell: pwsh
run: ./tests/e2e/run-e2e.ps1 -SelfTest
474 changes: 474 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

22 changes: 17 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Note: `templates/CLAUDE.template.md` is the template `/sd:setup` scaffolds into

# Sandbox install test (run before any PR touching install/hooks/commands/agents)
.\install\install.ps1 -BasePath C:\temp\sd-test
Get-ChildItem C:\temp\sd-test\commands\sd\ # expect 13 .md files
Get-ChildItem C:\temp\sd-test\commands\sd\ # expect 14 .md files
.\install\uninstall.ps1 -BasePath C:\temp\sd-test -Force # round-trip: removes the 5 sd\ dirs
Remove-Item -Recurse -Force C:\temp\sd-test # cleanup
```
Expand All @@ -38,17 +38,27 @@ 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

| Source | Installs to | Contents |
|---|---|---|
| `commands/` | `~/.claude/commands/sd/` | 13 slash commands (`/sd:feature`, `/sd:bug`, `/sd:rca`, `/sd:refactor`, `/sd:perf`, `/sd:spec`, `/sd:explore`, `/sd:review`, `/sd:setup`, `/sd:release`, `/sd:adr`, `/sd:verify`, `/sd:status`) |
| `commands/` | `~/.claude/commands/sd/` | 14 slash commands (`/sd:feature`, `/sd:bug`, `/sd:rca`, `/sd:refactor`, `/sd:perf`, `/sd:port`, `/sd:spec`, `/sd:explore`, `/sd:review`, `/sd:setup`, `/sd:release`, `/sd:adr`, `/sd:verify`, `/sd:status`) |
| `agents/` | `~/.claude/agents/sd/` | 6 subagents (`sd-spec-architect`, `sd-code-explorer`, `sd-debugger`, `sd-implementer`, `sd-reviewer`, `sd-docs-writer`) |
| `hooks/powershell/` + `hooks/bash/` | `~/.claude/hooks/sd/` | 3 hooks × 2 platforms (`prompt-router`, `spec-gate`, `subagent-retro`) |
| `templates/` | `~/.claude/templates/sd/` | 4 setup templates + 5 spec templates in `specs/` |
| `skills/` | `~/.claude/skills/sd/` | 8 rule packs, one folder per skill with `SKILL.md` |
| `templates/` | `~/.claude/templates/sd/` | 4 setup templates + 6 spec templates in `specs/` |
| `skills/` | `~/.claude/skills/sd/` | 9 rule packs, one folder per skill with `SKILL.md` |

Source filenames are unprefixed (`agents/reviewer.md`); the `sd-`/`sd:` namespace comes from frontmatter `name:` and the `sd/` install subfolder. The namespace exists for collision avoidance and clean uninstall — never use bare names when assets reference each other.

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
106 changes: 103 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ per-file-type guidelines, and how to test changes locally.
- [Project goals and non-goals](#project-goals-and-non-goals)
- [Repo layout](#repo-layout)
- [The manifest](#the-manifest)
- [Threshold re-calibration](#threshold-re-calibration)
- [PR process](#pr-process)
- [Per-file-type guidelines](#per-file-type-guidelines)
- [Commands (`commands/*.md`)](#commands-commandsmd)
Expand Down Expand Up @@ -41,13 +42,13 @@ per-file-type guidelines, and how to test changes locally.

```
specwright/
commands/ # 13 slash commands (markdown with frontmatter)
commands/ # 14 slash commands (markdown with frontmatter)
agents/ # 6 subagent definitions (markdown with frontmatter)
hooks/
powershell/ # 3 PowerShell hooks
bash/ # 3 bash hooks (parity with PowerShell)
templates/ # 4 setup templates
specs/ # 5 spec templates
specs/ # 6 spec templates
install/ # install.ps1 + install.sh + install/README.md
docs/ # architecture, usage, walkthrough, troubleshooting
examples/ # demo references
Expand Down Expand Up @@ -79,10 +80,16 @@ What this means in practice:
- **Writing intentionally historical docs** (superseded counts as a past-state record): put the
path in `historicalExclusions`. `docs/history/`, `docs/superpowers/` and `CHANGELOG.md` are
already excluded. Never "fix" their numbers to match today's disk state.
- **Publishing the current released version**: add a `versionClaims` entry (`file` + a `pattern`
with one capture group, no `equals`). It is checked against the newest dated
`## [x.y.z] - <date>` heading in `CHANGELOG.md`, not against a manifest quantity - CHANGELOG is
the single source of truth for "what version is released." Unlike `docClaims`, there is no
undeclared-claim scan for version strings yet: a version claim in a doc that is not listed here
is not caught.

Two constraints on `pattern`: it must be valid in **both** POSIX ERE (bash `[[ =~ ]]`) and .NET
(PowerShell), so use `[0-9]` rather than `\d` and avoid lookarounds; and it is matched
**case-sensitively** on both platforms.
**case-sensitively** on both platforms. This applies to `versionClaims` patterns too.

`scripts/selftest-docs.{ps1,sh}` proves Check 7 still bites, by corrupting a throwaway copy of the
repo and asserting the validator catches it. CI runs it on all three OSes.
Expand All @@ -98,6 +105,73 @@ 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.

---

## Threshold re-calibration

Every hardcoded threshold in this repo (Gate Complexity's tasks/layers/files limits,
`retroStaleMinutes`, `debounceMinutes`, `maxLessons`, `metrics.maxSizeKb`, the perf gate's noise
floor) started as an estimate, not a measurement - see `docs/adr/0004-threshold-calibration.md`.
Re-run the calibration pass **every 20 closed specs, or at each minor release, whichever comes
first**:

1. Run `/sd:status --calibration` against the accumulated `.specs/index.md` and
`.specs/_metrics/events.jsonl`.
2. For each threshold, record the verdict - keep, change, or insufficient data - in a new ADR under
`docs/adr/`. "Insufficient data" is a legitimate, expected outcome at a thin corpus size; do not
change a threshold without a stated measurement behind it.
3. Where a threshold's rationale in `templates/project-config.template.json` is still a judgement
call (no measured basis), leave its `_..._use` caveat in place rather than removing it.

---

## PR process
Expand Down Expand Up @@ -189,6 +263,32 @@ skills:
- Agent must read `CLAUDE.md` and `constitution.md` at runtime. No hardcoded stack assumptions (no `cs`, `csproj`, `dotnet`, etc. literal references unless they come from project config).
- Every finding cites `file:line`. No prose without citations.

**Machine-readable input declarations.** Under every heading that selects a distinct agent
behavior by field value (`## Mode N: TASK = <mode>`, `## Task type: `<type>``, `### `TASK =
<type>``, `### `WORKFLOW_TYPE = <type>``), add two lines immediately before the existing prose:

```
Inputs (required): SPEC, IMPACT
Inputs (optional): MODE, REPLAN_SCOPE
```

- Tokens are `UPPER_SNAKE`, comma-separated, exactly the identifiers the calling command sets -
never a paraphrase.
- Write `none` explicitly when a mode has no required (or no optional) inputs. Silence is not an
assertion - an omitted line reads as "not yet documented," not as "empty."
- The existing prose `Inputs: ...` line (with parenthetical caveats, cross-references, etc.) stays
below unchanged - the two new lines are additive, for tooling to grep, not a replacement for the
explanatory prose.
- When adding or changing an agent invocation in `commands/*.md`, cross-check it against the
target mode's declared inputs: a token the command passes that the mode doesn't declare, or a
required token the mode declares that the command omits, is a real defect - fix the mismatch (add
the missing token to the declaration or the invocation, whichever is actually correct) rather than
leaving the two out of sync.
- This cross-check is machine-enforced: Check 8's `CL100`-`CL104` rules (`docs/contract-lint.md`)
parse both sides and BLOCK on a mode mismatch or a missing required input. A legitimate mismatch
the declaration can't express (an either/or required set, for example) gets a
`<!-- contract-lint: allow CLxxx - <reason> -->` suppression, not a silent gap.

### Hooks

Hooks come in pairs. If you change `hooks/powershell/foo.ps1`, you also update `hooks/bash/foo.sh`. The repo CI runs `scripts/validate.{ps1,sh}`, which refuses PRs where the pair drifts (along with the other engine-invariant checks).
Expand Down
Loading
Loading