AGENTS.md · git:20261005.2e4c89b · 2026-10-05 · sha256 fa0d25ab11b1ab54

AGENTS.md git:20261005.2e4c89bA

Immutable. This exact content never changes and is served at /api/v1/blob/fa0d25ab11b1ab54.

# AGENTS.md

Guidance for coding agents working on this repository. Claude Code reads this via the
`@AGENTS.md` import in [CLAUDE.md](CLAUDE.md); Codex, Copilot, Cursor, Gemini and others
read it directly.

**This file is always loaded. Keep it short enough to be read, and put nothing here that a
command already prints.** The pre-commit hook names every guard it runs, the rule it
enforces, the offending line and the fix, so this file does not restate them.

## Start here, every session

Do these four before acting, including after a context reset or compaction. They take
under a minute and each one has cost a real sprint when skipped.

1. `sdlc-studio/reviews/LATEST.md` - what the last run landed and what it left owed.
2. `/sdlc-studio status` - pipeline state and the next step.
3. `.claude/skills/sdlc-studio/lessons/_index.md` - the cross-project lessons. Read them
   before any design decision, not after.
4. `git fetch` - a stale clone mints ids the remote already used and plans over artefacts
   somebody else changed.

## The failure mode this repo actually has

Not ignorance. **Rules that were read and then not followed.** One session produced five
instances: the lessons index went unread while the plan printed its digest; review prompts
were hand-written though the shipped seat briefs exist; findings were not sorted into
regression versus pre-existing; a backlog census was hand-scripted rather than taken from
the tooling; and the full-suite-before-commit rule was broken one commit after being
shipped, leaving `main` red for six commits.

So: **when a rule matters, gate it in the command people actually run** - LL0027 in the
[lessons registry](.claude/skills/sdlc-studio/lessons/_index.md). Adding a line here is the
weakest available fix, and a rule stated here with no gate behind it should be read as a
known-weak one.

## What will refuse you

These block. Everything else in this file is guidance.

| Gate | Refuses |
| --- | --- |
| pre-commit + commit-msg hooks | any lane `.githooks/pre-commit --list` or `.githooks/commit-msg --list` prints; the message rule (a multi-id subject with no `Refs:` trailer) and the collapsed-suite check are lanes there too |
| pre-push hook | a red boundary gate (`gate.py --boundary push`, or `release` for a tag): a push runs the full suite once plus the core gate lanes, about five minutes, and a tag adds the slow release lanes; `python3 tools/gate_timing.py estimate --suite boundary-push --warn-seconds 0` prints the current figure |
| `sprint plan` | a batch whose units lack `Affects:` or `Points:`, or exceed the split threshold |
| `transition -> Done` | a story whose executable ACs have not passed |
| `transition -> Fixed` | a bug nothing speaks for (no ticked criterion, no `Verify:` line), or whose recorded `verify_ac` run is red |
| `sprint close` | an uncommitted change to a file a batch unit declares (uncovered units, unanswered items and failing lanes are recorded as known issues on the report, not refused) |
| `sprint sign` | a report that is not the run's, a page that no longer re-derives to what was filed, a tree changed since the close, or a principal who is a unit's author or a reviewer recorded on it |

**Enable the hooks once per clone: `bash tools/enable-hooks.sh`.** It installs the three tracked hooks
(pre-commit, commit-msg, pre-push), names each, and sets this clone's `core.sshCommand` to an ssh
carrying a keepalive - the pre-push gate runs for minutes before git writes a byte, and a connection
idle that long is dropped before the push starts, so the gate passes, the push fails and the retry
pays the gate again. A `core.sshCommand` already in force at ANY scope - local, global or system - is left as you set
it, and the script names the scope it found it in; if yours carries no keepalive, add one, because
the script will not write over it. Without them you are running with the gates off.
Emergency bypass is `git commit --no-verify` for a commit and `git push --no-verify` for a push, and
nothing else.

**Pushes go straight to main and a red main is read before the next push (D0181).** The pre-push
hook asks the forge for the latest push-triggered Lint run on main before it runs the gate; on any
conclusion but success it refuses until that run's id is acknowledged once per clone with
`SDLC_PUSH_ACK_RED=<run id> git push`. A red answer is read once more before it refuses, and the
second answer is judged: the forge's ordering is eventually consistent, and a stale first row once
named a two-month-old red (BG0709). An unreadable or empty answer is named and never read as
green, and it does not block an offline push. The required status check a direct push can never
satisfy was removed from branch protection under the same ruling on 2026-09-06, and the
protection read-back shows none.

**Each hook lists its own lanes; this file does not.** `.githooks/pre-commit --list` and
`.githooks/commit-msg --list` print one lane per line, its key and the rule it enforces, read from
the hook's own `run` calls, and run nothing; every refusal either hook makes is such a lane.
`.githooks/pre-push` has no `--list`: a push runs `gate.py --boundary push`, the full suite once,
which catches what per-commit selection cannot reach, plus the core gate lanes, and a tag runs
`--boundary release`, which adds the slow release lanes. The gate prints each lane it runs and its
cost.

## Non-negotiable rules

**No ad-hoc coding.** Work becomes a story or a bug with acceptance criteria before it
becomes a diff. A CR or RFC is not work until `refine` decomposes it into sized units.
This repo is held to it like any consuming project.

**Review is independent of the author.** Each unit gets one independent reviewer, a fresh
context that did not write the code, briefed with `critic.py brief` and recording its verdict
with `critic.py record`. The operator signs the run once, after the close, with `sprint sign`.
A reviewer the author controls does not satisfy this.

**Brief a reviewer with the shipped tool, never by hand.** `critic.py brief --unit <id>
--seat engineering|product|qa` carries the seat charter, the bounded diff scope (the unit's
declared `Affects`), the canonical acceptance criteria as law, and the claim-inventory pass.
A hand-written prompt carries none of them and silently substitutes an unbounded surface for
a unit review.

**A review judges the unit's own diff.** Scope is that unit's `Affects` against the run's
base ref. Only a regression or a newly introduced defect blocks; anything already true of
the tree, or already recorded in an open Bug or CR, is reported with its id and does not
hold the gate. Decide which by execution (`git log -S`), not by impression.

**Always look for a tool before doing anything by hand.** The
[toolchain runbook](.claude/skills/sdlc-studio/reference-sprint-toolchain.md) is ordered by
SPRINT STEP - plan, groom, batch, deliver, review, in-flight, close - and each row names the one
command that performs the step beside the hand-rolled shape it replaces. Read the row for the
step you are on BEFORE you start it, not after. If a step there has no command, that is a
finding to file, not permission to hand-roll it.

RUN-01KYZKY5 paid for this line: the close was hand-authored first, and running the shipped
commands afterwards caught four defects the hand version had missed - example scaffold rows left
in a retro, a Batch field that parsed to zero units, a header claiming a delivered count the
batch contradicted, and a lessons digest that no longer matched its log. The runbook already
named the command that would have caught them.

**Use the deterministic tooling; never hand-roll what it wires.** Create artefacts with
`artifact.py new` / `batch`, allocate ids with `next_id.py`, file findings with
`file_finding.py`, change status with `transition.py`. Never hand-author `_index.md` - it is
derived, and `reconcile` syncs it. **Read `reference-scripts.md` before hand-doing anything
mechanical**; it is the catalogue, and the answer is usually already there.

**Exercise every claim through the shipped entry point before asking for review.** A review
should CONFIRM your work, not discover that it does not run. Take each factual claim in the
changelog fragment and the criteria and put it through the CLI in a throwaway fixture -
refusals included, with the positive control beside each one. A library test cannot see a
missing lane, because the wiring is the part it does not exercise: `brief_fingerprint(brief(
...))` passed in-process for a whole sprint while `critic.py brief` printed nothing and the
paperwork said it did. Needing a second review round to learn that is not thoroughness, it is
verification handed to somebody else. Ungated until CR0520 ships `verify_ac lane-check`, so
read it as a known-weak rule and do it anyway.

**Ship the paperwork in the same commit as the code.** Every behaviour or doc change carries
its `changelog.d/<UNIT-ID>.md` fragment (`lessons/LL0004`).

**Run the full suite before a commit that touches shared surface.** The gate runs a selected
subset, so a green commit is not a green tree. This rule has been broken by the agent that
shipped it.

## Project overview

SDLC Studio is an agent skill (the [Agent Skills](https://agentskills.io) open format) for
managing the full software development lifecycle - PRD, epics, stories, planning, tests.
It enables **Goal-Driven Development**: set a goal and acceptance criteria, and the agent
drives the proven lifecycle to it (TDD, BDD, Eval-Driven, Goal-Driven). `sprint` runs it and
closes every run with a reconcile and review.

**The skill source lives at `.claude/skills/sdlc-studio/`** and installs to each tool's
skill directory (`install.sh --list-targets`). This repo dogfoods the skill against its own
source. Durable guidance lives here; volatile state lives in `sdlc-studio/reviews/LATEST.md`,
so the two do not drift.

## Where things live

| Path | Purpose |
| --- | --- |
| `.claude/skills/sdlc-studio/SKILL.md` | Always-loaded router (~300 lines, CI-budgeted under 500) |
| `.claude/skills/sdlc-studio/reference-philosophy.md` | Create vs Generate modes - read first |
| `.claude/skills/sdlc-studio/reference-doctrine.md` | Project-agnostic operating doctrine |
| `.claude/skills/sdlc-studio/reference-scripts.md` | **The script catalogue. Read before hand-doing a mechanical task** |
| `.claude/skills/sdlc-studio/reference-*.md` | Domain workflows (50+ files); `help/references.md` indexes them |
| `.claude/skills/sdlc-studio/help/` | Type-specific help (~45 files) |
| `.claude/skills/sdlc-studio/lessons/` | Cross-project lessons registry |
| `.claude/skills/sdlc-studio/templates/personas/amigos/` | The three shipped amigo seats, in work and review renders; this repo's own cards in `sdlc-studio/personas/seats/` override them (`persona_resolve.py resolve --seat <s> --path-only` names the one in force) |
| `.claude/skills/sdlc-studio/scripts/` | 40+ helpers sharing `lib/sdlc_md.py` |
| `.claude/skills/sdlc-studio/templates/` | Documents and code, incl. `agent-instructions.md` (the starter this file's shape follows) |
| `.claude/skills/sdlc-studio/best-practices/` | Quality guidelines (19 files) |
| `tools/` | Repo-only CI guards, not shipped |

## Forward-porting to the installed copy

Never run `install.sh` from the dev repo - its sweep clobbers the git-tracked tree. Mirror
with the guarded wrapper instead:

```bash
bash tools/forward-port.sh          # itemised diff, dry-run
bash tools/forward-port.sh --yes    # apply (.local and __pycache__ untouched)
bash tools/forward-port.sh --check  # drift gate; non-zero when the copy has drifted
```

The installed copy is what every other project on this machine loads, so the window between
a fix landing here and the mirror running is a window in which a fix believed shipped is in
force nowhere. Two states are reported rather than failed: no installed copy, and a copy
holding a `.local/forward-port.pin` marker.

## Testing

`npm run lint` (markdown plus all guards) and `npm test` (the script suite). **Without npm,
every check except markdownlint is a plain Python or bash command** - do not skip the gate
because npm is missing.

**Budget the time.** A commit runs only the test modules its staged change imports, loads or is
named for, in parallel, and reports its time against a 90-second budget; a docs or artefact commit
runs none. A change to a hub script still selects large modules, so give a commit a 10-minute
timeout. The full suite runs at push.

**The gate spans two hooks, cheapest first.** `pre-commit` runs the cheap guards and decides
whether the suites are needed; `commit-msg` checks the message rules and only then runs them.
Git creates the commit message after `pre-commit`, so this is the only order in which a
message defect is refused before the suites run rather than after them.

One lane is deliberately outside the gate: `npm run lint:corpus` lints every tracked markdown
file under the strict root rules, attributing findings against the latest tag. It runs from a
scheduled job and by hand before a release, because a guard whose cost is paid on every commit
gets switched off.

Manual verification: `./install.sh --local`, then `/sdlc-studio help`, `status`,
`repo map build`, and `reconcile --verify --dry-run` against a fixture story.

## Soft dependencies

| Feature | Requires |
| --- | --- |
| `cr sync`, `story sync`, `project sync` | `gh` CLI, authenticated |
| `reconcile --verify` | whichever of `pytest`, `jest`, `vitest`, `go`, `curl`, `jq`, `rg` your Verify lines invoke |
| `repo map build` | Python 3.10+ (pure stdlib) |
| `verify_ac run --coverage` and the `review.line_coverage` gate | `coverage` 7.10 or later on the interpreter that runs the selectors (`SDLC_COVERAGE_PYTHON`, else the current one); absent, the run refuses under `block` and reports under `report` |

## Style

Enforced by `tools/lint-style.sh`, with no code-span or "only quoting the rule" exception:
British English; no em dashes (use a spaced hyphen, or restructure); no corporate jargon
(allowlist in `tools/style-allowlist.txt`); no internal provenance tags in consuming-facing
`reference-*.md`, `help/` or `scripts/`; dense, economical writing; `{{placeholder}}` syntax
in templates.

## Before you write

| Creating | Read |
| --- | --- |
| Python script | `best-practices/python.md`, then `script.md` |
| Bash script | `best-practices/script.md` |
| Documentation | `best-practices/documentation.md` |
| Agent skill | `best-practices/claude-skill.md` |
| A test | `best-practices/testing.md#name-the-mutant-first` - state the production change the test must fail on, before writing it |

When modifying the skill: keep SKILL.md a router (philosophy gates, the loading guide,
pointers); add a `help/` file for a new command; update the matching `reference-*.md` for a
workflow change.

## Related

- [README.md](README.md) - installation and quick start
- [CONTRIBUTING.md](CONTRIBUTING.md) - contribution guidelines