Keep AI coding agents on rails. Every change starts from a written spec with testable acceptance criteria, gets a design that two reviewer agents have argued with, and cannot be marked done until the criteria are ticked and the evidence is recorded β enforced by git hooks, not good intentions.
The whole workflow is agent-agnostic and packaged to be compiled by
Microsoft APM (Agent Package Manager) into whichever coding-agent
harness your project uses β .claude/, .opencode/, .cursor/, .github/, β¦ A static website and a
complex information-management system use the exact same workflow. Stack- and design-specific
guidance is optional and lives in separate injectable bundles; where your
governing context lives β architecture, security rules, ADRs, UX β is likewise pluggable, through a
context map of files and optional MCP providers.
One spec per unit of work. A spec is a folder β specs/SPEC-N-name/ holding spec.md (the
contract), tech-design.md (the how), audit-trail.md (the verification record), and any attachments
the spec refers to (mockups, imported source). Each step advances that spec's status, which must
stay identical in spec.md and in specs/INDEX.md β the hooks fail your commit if they drift.
| What happens | You run | Status becomes |
|---|---|---|
| Define what β user stories, acceptance criteria, edge cases, BDD scenarios | /requirements <your idea> |
π΅ In Planning |
| Design how β schema, API contracts, components | /technical-design SPEC-1 |
π£ Planned |
| Two rounds of independent architecture + security review | (automatic) | π£ Planned |
| Build it, on the spec's branch | β | π‘ In Progress |
| Prove it against the acceptance criteria and BDD scenarios | /write-tests SPEC-1 |
π In Review |
| Tick every criterion, record the test evidence | β | π’ Validated |
A spec whose feature no longer exists becomes β« Deprecated and stays as a tombstone β dropped, never rewritten, so the history stays traceable. Every spec is versioned with an inline changelog, so you can see why it changed and which other spec forced it.
π See a finished spec β one worked example, Validated and closed out,
showing the status header, changelog, ticked acceptance criteria, a descoped one, BDD scenarios, and the tech design.
specs/INDEX.mdβ one table with every spec's status, version, folder, and the recommended build order, kept in lockstep with the specs themselves.- Slash commands β
/requirements,/technical-design,/write-tests,/frontend-architecture. - Reviewer agents β
architecture-reviewer,security-reviewer, driving the Plan Review Workflow. Pick a model per agent (any model your harness understands, including local ones) β see Choosing models for the reviewer agents. - Security rules β auto-injected when you touch
.env*or**/api/**, from wherever your context map points (kind=security; defaults todocs/SECURITY-RULES.md). - General engineering rules β
engineering-practices(discovery before assumption, KISS/DRY/YAGNI, what not to introduce),code-hygiene(no zombie code; docs and your architecture source kept true), andtesting(universal, stack-agnostic testing law). These are the rules you would otherwise hand-write intoCLAUDE.md/AGENTS.md; here they ship as managed rules that refresh onapm update. - Self-enforcing checks β spec status, acceptance-criteria close-out, spec versioning, and unanswered questions are enforced two ways (see Enforcement below).
- An unattended mode β the commands can write their questions into the spec instead of asking the terminal, and the next phase stays locked until you answer. See Running unattended.
What it deliberately does not contain: language/framework/design specifics (those live in optional bundles, discovered through a frozen profile extension point), or any hardcoded assumption about where your governing context lives β architecture docs, security rules, ADRs, and UX guidelines are located through the context map, so they can sit in the repo or in Confluence/Notion/an MCP provider.
- APM installed (
apm --version). git(for the enforcement floor).jqβ required. The project config (.spec-workflow/config.json) is JSON, andjqis what reads and writes it and merges agent hooks safely. The setup script checks for it up front and stops before touching anything if it's missing.
APM deploys the shared primitives automatically, but β by design β it does not run a dependency's setup script in your project (that would be a supply-chain footgun). So setup is an explicit second step you can read before running.
1. Install the primitives. Pin by immutable commit SHA (not a floating tag) so installs are reproducible and can't be silently moved under you:
apm install mode41/sd-workflow#<commit-sha>This deploys the rules, commands, and reviewer agents to whichever harness markers your repo has β
.claude/, .opencode/, .cursor/, .github/, β¦ (auto-detected; nothing pinned). Verified:
instructions β each harness's rules dir, commands β its commands dir, agents β its agents dir.
2. Run the one-time setup (idempotent; re-run after every apm update). Read it first if you
like β it's short, does no network access, and shells out to nothing but git and jq:
bash "$(find apm_modules -path '*/.apm/scripts/init-and-wire.sh' | head -1)"This:
- deploys the enforcement machinery into a committed
.spec-workflow/directory, - seeds the living files only if absent β
specs/INDEX.md,docs/PRD.md,docs/SECURITY-RULES.md,.spec-workflow/context-map.md,AGENTS.md,spec-workflow.supplemental.md,.spec-workflow/config.jsonβ never clobbering your data, - adopts an existing
AGENTS.md: if you already have one (most existing projects do), the installer never rewrites it β it offers to append a marked workflow section (interactively), or records it as a manual step if you decline or run non-interactively. Opt out withtouch .spec-workflow/.no-agents-merge, - merges the config forward (adds entries for newly-shipped reviewer agents, retires dropped ones) and prompts for any model you haven't chosen β see Choosing models below,
- wires
git config core.hooksPath(detect-and-preserve β see below), - reconciles each present harness's native hooks (today that means removing the Claude
Stophook older versions wired β see Enforcement below) and stamps each agent's configured model into the deployed agent files, - writes
.spec-workflow/MANUAL-STEPS.mdβ a regenerated checklist of everything the installer could not do for you (an existingAGENTS.mdto extend, a foreigncore.hooksPathto chain, an unsupported harness, etc.). Check the file after each run; resolved items drop off automatically.
Update later with apm update, then re-run the step-2 command: managed primitives and templates
refresh; your seeded living files are left untouched (a drift notice prints if your
docs/SECURITY-RULES.md diverges from upstream), and .spec-workflow/MANUAL-STEPS.md is
regenerated. In CI, use apm install --frozen and run apm audit.
Verified against APM 0.25.0. Auto-detect deploy works; a dependency's
lifecycle:block is project-scoped and does not auto-run in a consumer (hence the explicit step 2); project lifecycle scripts are untrusted by default (apm lifecycle trustrequired) and executable primitives are gated byapm approveβ so nothing of ours runs without your explicit action.
In your coding agent, not the shell:
/requirements a CLI that converts CSV files to Parquet
That interviews you about the project, fills in docs/PRD.md, and splits the work into
specs/SPEC-N-name/ folders (each with a spec.md) with a recommended build order. Then, per spec:
/technical-design SPEC-1 β design reviewed by both reviewer agents, written as tech-design.md
β implement it on the spec's branch
/write-tests SPEC-1 β tests written and run against the acceptance criteria and BDD scenarios
β tick the criteria, record the evidence in audit-trail.md, done
Adding a feature to a project that already has a PRD? Same entry point β /requirements <the feature>
adds a single spec instead of bootstrapping everything.
Not sure what you're aiming at? docs/example-spec.md is a complete spec at
the end of that journey, annotated with which hook enforces which convention.
.spec-workflow/ # this package's namespace β commit this. Mixed: most of it is managed
# (refreshed every install), the files marked β YOURS are seeded once.
hooks/ # managed: check-*.sh, pre-commit, checks.sha256 (the git enforcement)
templates/ # managed: current spec/INDEX/PRD/context-map/audit-trail templates (migration reference)
checks.spec.json # managed: neutral metadata for the checks (where they live)
config.schema.json # managed: describes config.json (drives editor validation)
config.json # β YOURS. Seeded once; values never overwritten. See below.
context-map.md # β YOURS. Seeded once β where governing context lives
# (files + optional MCP providers). Never clobbered.
profiles/ # optional, owned by a stack/design bundle if you install one:
stack.md # stack specifics (consumed by /write-tests, /technical-design)
design.md # design specifics (consumed by /frontend-architecture)
MANUAL-STEPS.md # generated per-machine checklist of what the installer couldn't do
# (git-ignored via a managed .spec-workflow/.gitignore β don't commit)
specs/ # your specs (SPEC-N-name/ folders) + INDEX.md β living data, seeded once
# each folder: spec.md + tech-design.md + audit-trail.md + attachments
# a parked spec may sit under specs/backlog/SPEC-N-name/ instead
docs/PRD.md # living data, seeded once (default `business` context source)
docs/SECURITY-RULES.md # living data, seeded once (default `security` source; drift-notified on update)
ARCHITECTURE.md # optional β default `architecture` source (create as the project takes shape)
AGENTS.md # neutral project memory, seeded once (or workflow section appended if it exists)
spec-workflow.supplemental.md # your workflow tuning, seeded once, never overwritten
.claude/ .opencode/ ... # APM-deployed rules/commands/agents + our agent-model stamps
The five checks (check-ac-closeout, check-bdd-closeout, check-status-sync, check-spec-version,
check-open-questions) run at one boundary: the git pre-commit hook. It works for any agent
(or none), because it's git, not an agent feature β and a commit is a real workflow boundary.
It runs whatever check-*.sh the installer deployed β there is no list to keep in sync, and a check a
newer version drops is pruned rather than left behind. A spec parked under
specs/backlog/SPEC-N-name/ is held to all five checks, exactly like one in the main tree.
Why there is no session-end hook. Earlier versions also merged the checks into Claude Code's
Stop hook, calling it a "finish boundary". It isn't one: Stop fires when the agent finishes
responding β the end of every turn, including one that only answered a question or talked through an
open Q-N. No harness exposes a "the next workflow step was invoked" event. Worse, a Stop hook that
exits 2 doesn't warn β it blocks the agent from stopping and feeds stderr back as an instruction to
continue, so an ordinary conversational turn became a forced continuation. The wiring is gone, and
apm install removes it from projects that still have it. The check scripts additionally honor the
harness's stop_hook_active flag, so a stale hook left over anywhere can't loop.
Known, accepted asymmetries β documented, not hidden:
git commit --no-verifybypasses the git gate. It is the only enforcement layer, so a bypassed commit is unchecked.core.hooksPathis per-clone.git clonedoes not copy.git/config, so a fresh clone has no git enforcement until someone re-runsapm install. (The.spec-workflow/scripts are committed and travel with the repo; only the activation is local.)core.hooksPathdetect-and-preserve. If your repo already setscore.hooksPath(Husky, lefthook, an org secret-scanner), the installer will not overwrite it β it warns and prints how to chain the spec-workflow checks from your existing hook. Opt out permanently withtouch .spec-workflow/.no-git-hooks.
The checks are the part of this package that can silently break a consumer's workflow, so they have a
behavioural test suite. It runs the shipped scripts in .apm/scripts/ against throwaway git
fixtures in a temp dir β nothing is installed and your repo is untouched:
bash tests/enforcement.test.sh # exit 0 = all passedIt covers the status / AC / open-question state machine, the spec-versioning substantive-change rules
and every documented exemption, the stop_hook_active safety net, the pre-commit wrapper (blocking,
--no-verify, and the checks.sha256 integrity gate), and the installer's hook reconciliation and
retired-file pruning. Only git and bash are needed; the sections that need jq skip themselves
without it. This is repo-only tooling β it lives outside .apm/, so it is never packed or deployed.
Run it before publishing a version. If you change a check's behaviour on purpose, update the matching assertion in the same commit β an assertion that no longer describes the intended contract is worse than no assertion.
/requirements and /technical-design are written to interview you, which is useless when nobody is
at the terminal. Flip one key in .spec-workflow/config.json:
{ "interaction": { "mode": "file" } }Now a command that would have asked instead writes the question into the file it owns, records the answer it assumed, and keeps going β so an unattended run finishes with a real PRD, real specs, and a written list of everything it had to decide for you:
## Open Questions
### Q-1: Which compression codec is the default?
- (a) snappy β faster writes, ~30% larger files
- (b) zstd β ~2Γ slower, ~50% smaller
**Assumed:** (b) zstd β AC-2 prioritises storage cost over write latency.
**Answer:**You answer by filling in **Answer:** β your choice, or confirmed to accept the assumption.
Nothing built on a guess can move forward. An empty **Answer:** is a ceiling on the spec's
status, enforced by check-open-questions.sh at the same pre-commit gate as every other check:
| An unanswered question in | holds the spec at |
|---|---|
docs/PRD.md |
π΅ In Planning β for every spec, since project-level questions invalidate anything beneath them |
specs/SPEC-N-*/spec.md |
π΅ In Planning β so /technical-design refuses the spec |
specs/SPEC-N-*/tech-design.md |
π£ Planned β so implementation cannot start |
Two details worth knowing:
- The ledger is not a queue. Answered questions stay in the file, so
## Open Questionsbecomes the record of why a decision went the way it did β the one thing the format previously threw away. Editing it never demands a version bump; whatever the answer then changes in the contract does. - It works in
askmode too. The check does not care how a question got there, so you can park a spec by hand by dropping aQ-Ninto it. Unresolved critical findings from the plan-review pipeline land here as well, which means "accept this risk" has to be written down by a human rather than agreed to in a chat window that nobody keeps.
.spec-workflow/config.json decides which model each reviewer subagent runs on:
{
"$schema": "./config.schema.json",
"schemaVersion": 2,
"interaction": { "mode": "ask" },
"agents": {
"architecture-reviewer": { "model": { "default": "opus" } },
"security-reviewer": { "model": { "claude": "sonnet", "opencode": "ollama/qwen2.5-coder" } }
}
}- Values are written verbatim into the deployed agent file's
model:frontmatter β the package never translates them, so any model your harness understands works, local ones included (ollama/qwen2.5-coder,devstral, β¦). defaultcovers every harness without its own key; add a per-harness key only where they differ.- Omit a model (or set it to
null) and that agent just inherits the harness default. - The setup script prompts for anything unset when run interactively, and simply leaves it unset
in CI or with no TTY β
apm install --frozennever blocks. Re-run withSPEC_WORKFLOW_CONFIGURE=1to change your answers, or edit the JSON directly (the$schemapointer gives you editor validation and completion).
Stamped today: Claude Code (.claude/agents/) and opencode (.opencode/agents/). Copilot's
model: is a list of UI display names and Codex agents are TOML without a model field, so those
harnesses are reported as unsupported and their agents inherit the harness default.
Why step 2 re-runs every time: apm update overwrites the deployed agent files, so the setup
command re-applies your models afterwards. That is also why your choices live in config.json rather
than in the agent files themselves.
Your settings survive package updates: when a new version adds a reviewer agent, its entry is added
(and prompted for) with your existing values untouched; when one is dropped, its entry moves to
retiredAgents rather than being deleted, and is restored if the agent ever returns. When a new
version changes the config format, the setup script migrates schemaVersion forward additively β
new keys get their defaults, nothing you set is touched. Going the other way, a config newer than the
installed package makes it skip model stamping and tell you to update, rather than mis-parse your
settings.
- Setup runs no code without your action. Unlike npm-style postinstall hooks, APM does not
auto-run a dependency's script in your project β you invoke the step-2 bootstrap explicitly, it is
short, and it does no network access (it shells out only to
gitandjq), so you can read.apm/scripts/init-and-wire.sh(orapm_modules/.../init-and-wire.sh) before running it. If you ever add it to your ownapm.ymllifecycle:block, APM keeps it untrusted until you runapm lifecycle trust. Still: pin by commit SHA, and use--frozen+apm auditin CI. docs/SECURITY-RULES.mdis yours. It is seeded once and never overwritten, so org-specific rules survive updates; the installer prints a drift notice when it diverges from upstream so you can pull in improvements deliberately. It is the defaultsecuritysource β repointkind=securityin.spec-workflow/context-map.mdat a Confluence/Notion page or an MCP provider if your rules live there instead.- Check-script integrity. The pre-commit hook verifies every deployed check script against a
pinned
checks.sha256before running them, and fails loudly if one was tampered with. The installer also prunes checks a newer version no longer ships, so a dropped check cannot linger and get re-blessed by the regenerated hash.
The core is bundle-ready via a frozen profile extension point: /write-tests and
/technical-design consult .spec-workflow/profiles/stack.md; /frontend-architecture consults
.spec-workflow/profiles/design.md. If no profile is present, commands run on discovery of your
project's own patterns. See docs/extending-with-bundles.md.
Frozen describes the contract's shape β fixed paths, a closed kind vocabulary, schema-versioned
files β not the release. The package is pre-1.0 (0.MINOR.PATCH), so until a stable major exists a
breaking change bumps the minor. Pin by commit SHA and read the diff before updating.
The workflow reads your project's governing context β architecture, security rules, business context,
ADRs, UX guidelines β through a single seeded manifest, .spec-workflow/context-map.md. Each entry is
either a file (a repo path, resolved from the repo root, or an external URL β Confluence, Notion, a
wiki) or a provider: an MCP tool
that serves context scoped to the part of the system you are working on, queried live. For any
kind, the commands query a connected provider, else read the listed file, else discover from the repo β
so it degrades cleanly and needs no new dependency (the manifest is plain markdown; MCP is
optional). Architecture docs and security rules are located this way too, so they can live in the repo
or in an org-wide tool β your choice, not a hardcoded path.
The extension point is vendor-neutral: any MCP tool matching the contract works. The reference
provider is architrace.io, which serves your org's governing context
(Confluence, Notion, git, β¦) scoped to each system. Connect its MCP server in your harness settings
and add a row to the manifest's Providers table. See
docs/extending-with-bundles.md for the frozen contract.
MIT β see LICENSE.