Skip to content

Repository contract

When steer manages a repo, it expects a known shape. /steer:setup init and /steer:setup adopt install it; /steer:setup sync keeps it current. The scaffold is bundled in plugins/steer/templates/scaffold/ and mapped to install paths by its MANIFEST.md.

What a managed repo carries

flowchart TD
    ROOT[Repo root] --> SPEC["/spec spine<br/>intent · contract · vision · glossary · history/ · tracker · ADRs · design · sources · .version"]
    ROOT --> MISE[mise.toml<br/>toolchain + tasks]
    ROOT --> CI[.github/ workflows + PR template]
    ROOT --> COMPOSE[compose.yaml]
    ROOT --> CLAUDE[CLAUDE.md<br/>product-specific context only]
    ROOT --> ARCH[ARCHITECTURE.md<br/>as-built system model]
    ROOT --> CODE[/apps · /packages - implementation/]
Element Source Notes
/spec spine templates/spec/ Product truth. See Product spine.
mise.toml scaffold Toolchain pins + dev-loop tasks. mise is the single task entry surface: tasks declare ordering with depends (never run = ["mise run ..."] chains), and [deps.pnpm]/[deps.uv] (auto = true, gated by [settings] experimental) auto-install workspace deps on lockfile change - no hand-rolled install task (so never run a bare pnpm install; route a manual one through mise exec -- pnpm install). Because that runs non-interactively, the bundled pnpm-workspace.yaml sets confirmModulesPurge: false; and for the pinned pnpm to win, mise activate must be sourced after any nvm/asdf/volta in your shell rc - otherwise a global copy shadows it (/steer:setup doctor flags this). App-level Node scripts stay in package.json; a mise task may delegate to them, but delegation is one-way - a package.json script never shells out to uv/Python nor re-defines a mise task, and no task lives in both files. A polyglot app's Python backend (e.g. apps/api) is a mise/uv run task, composed with a [tasks.dev] depends = ["dev:*"] fan-out so mise stays the single entry point. Three verification tiers share one definition with CI: mise run pre-commit (hygiene + lint - wired to .git/hooks/pre-commit at bootstrap via mise generate git-pre-commit - unless the repo already owns a commit gate of its own, in which case /steer:setup init//steer:setup adopt report the collision and leave it alone - and per-clone state rather than a committed file, so a teammate's fresh clone has no hook until /steer:setup sync re-establishes it), mise run check (adds typecheck), and mise run ci (everything the required check runs). The full task model is in the conventions reference (/steer:reference conventions, which Claude loads on request).
mise.lock created at pin time The real version pin. The scaffold ships no lock - /steer:setup init//steer:setup adopt create it when they pin the toolchain (touch mise.lock, mise install, then mise lock --platform linux-x64,macos-arm64 so the lock carries per-platform URLs + checksums - CI runs mise install --locked on linux-x64, which fails on a host-only lock). Until a populated lock is committed, CI runs a plain unlocked install; never commit an empty / comment-only lock. The full toolchain rationale is in the conventions reference (/steer:reference conventions).
CI workflows + PR template scaffold Quality gates and review template. The required ci check is defined once, run two ways: every step in .github/workflows/ci.yml is a single mise run ci:<stage>, so the same code gates a PR and a laptop. Reproduce any CI failure with mise run ci - it is the whole check, not an approximation, which is also what keeps a repo verifiable when no runner is available (an org out of Actions minutes, an air-gapped checkout). Three advisory jobs (design-lint, spec-drift, ai-slop) stay inline in the workflow and are not part of the local gate. Minute discipline: every job is guarded on github.event.pull_request.draft == false and the trigger carries ready_for_review, so a PR costs nothing while it is a draft; a concurrency: block cancels a PR's in-flight run when it is pushed again, but never on main. Both are safe against branch protection because a skipped job satisfies a required check - an absent one does not, which is why no paths-ignore filter is used. The corollary matters on the skill side: a skipped check reads as green to gh pr checks, so /steer:work finish runs gh pr ready before it watches CI.
scripts/ci-*.sh scaffold The body of the required check, one POSIX-sh script per stage (ci-hygiene, ci-deps, ci-lint, ci-typecheck, ci-test, ci-iac, ci-image, ci-coverage, ci-changelog, ci-spec) plus the shared ci-lib.sh. Each self-detects its stack and no-ops with a ::notice:: when it is absent, so pruning mise.toml to the product's stack never orphans a depends; ci-lib.sh's predicates are kept in lockstep with the plugin's hooks/lib/scope.sh, so CI and the always-on rules agree on what stack a repo is. Three stages fail closed on a real defect and open otherwise: ci-test rejects a detected stack whose packages define no real test script or whose pytest run collects nothing (exit 5), and ci-coverage rejects changed lines below COVERAGE_DIFF_MIN (80) while skipping when no report exists or the base ref is unresolvable. ci-changelog rejects a change that touches shipping code without adding a changelog fragment, and skips when no base ref resolves or the repo has no .changie.yaml. It is deliberately delivery-mode-blind, so it is also part of the solo-trunk push-time floor. Its base comes from steer_ci_base() in ci-lib.sh; ci-coverage still inlines its own equivalent, because its push branch carries coverage-specific solo-trunk policy. ci-spec enforces the open-question contract on every push and PR: it fails on a bare - [ ] question under ## Open questions, an unfilled placeholder seed in a feature past draft, a malformed ### Q-NNN block, or a blocking question still open at a gate its feature has passed. It reads the questions with spec-questions.sh, a verbatim copy of the session hook's parser, and no-ops without spec/.version. A product adapts the stage scripts to its toolchain; ci-lib.sh and spec-questions.sh are copied verbatim.
.gitattributes scaffold Normalizes line endings to LF (* text=auto eol=lf plus per-extension pins) so a Windows contributor's core.autocrlf=true can't check the repo out - or commit into it - with CRLF. That matters more than whitespace: a CRLF shell script does not warn, it fails to parse, which takes out scripts/*.sh and every CI step that runs them, and makes a Docker image's entrypoint unrunnable. Marks binaries (images, fonts) binary so they are never newline-normalized, and lockfiles (pnpm-lock.yaml, uv.lock, mise.lock) -diff - so a dependency-bump PR shows Binary files differ rather than thousands of lines; that suppresses only the rendered diff, never the content, so any gate reading committed state is unaffected and a reviewer can still run git diff --text. CHANGELOG.md deliberately carries no attribute: it is generated by changie merge and its entries live one-per-file under .changes/unreleased/, so concurrent PRs write different paths and never conflict - the merge=union row this file used to carry is retired from the scaffold, because union is line-based and splices multi-line entries together wrongly. Reconcile is additive and never deletes, so an already-adopted repo keeps its copy until /steer:setup sync applies the migration that drops it. /steer:setup init and /steer:setup adopt install it where absent and reconcile it additively where one already exists. /steer:setup sync covers both cases by different routes: its step-5 additive reconcile splices missing pins into a .gitattributes the repo already has, and its step-6 capability repair (line-ending-normalization) detects the file being absent entirely and proposes creating it from the scaffold, waiting for a yes - it never creates it unasked and never runs git add --renormalize .. See Windows setup. Adding it does not convert CRLF already committed to history; that needs a one-shot git add --renormalize ..
CHANGELOG.md, .changie.yaml, .changes/ scaffold (+ generated) The curated changelog. Entries are written as one YAML fragment per change under .changes/unreleased/ (mise run changelog:new); changie merge assembles them into CHANGELOG.md, which is generated - never edited by hand. Curated, not commit-derived: Conventional Commits buy readable history, not release notes (see /steer:reference conventions -> Changelog). One file per change is what makes concurrent PRs conflict-free, the same reason /spec/history/ is a directory. The ci:changelog stage fails a PR that changes shipping code without adding a fragment. Release cut: library/cli run changie batch auto (semver from each fragment's kind); app/service deploy continuously and have no artifact version, so they cut a CalVer ship date at the prod promotion. .changie.yaml is seeded once and then the product's - tune kinds, or enable replacements to stamp a version into package.json. .changie.yaml and .changes/ come from the scaffold; CHANGELOG.md itself appears at the first changelog:merge. /steer:setup sync proposes the repair when they are missing (changelog-fragments capability), waiting for a yes, and never parses a pre-existing hand-written CHANGELOG.md - it renames it to CHANGELOG-archive.md and starts fresh.
compose.yaml, README quickstart scaffold Local run + onboarding. Host ports are env-overridable so they don't collide across products or worktrees.
.worktreeinclude scaffold Carries git-ignored local config (.env, .mise.local.toml, .claude/settings.local.json) into each claude --worktree (Orca honours it too) - worktrees start from git refs only, so without it the app can't boot there. Its header also documents worktree mise trust: trust is path-keyed, so a new worktree starts untrusted and every mise run ... there fails on trust until someone trusts it. A Claude Code session inherits the primary checkout's trust automatically - at SessionStart for a session started in the worktree, and on CwdChanged for one it enters mid-session (the check-worktree-trust script, registered twice - see Hooks). A plain terminal, where no session is watching, is not reached by either registration: run mise trust there once. Nor is any Copilot surface, which has no trust hook at all.
scripts/worktree-env.sh scaffold Sourced by mise.toml ([env]._.source) so parallel Claude Code worktrees of the same repo don't collide at runtime: it gives each worktree a unique COMPOSE_PROJECT_NAME and a stable per-worktree host-port offset (POSTGRES_PORT, WEB_PORT, DATABASE_URL). The primary checkout gets offset 0 (ports unchanged) and keeps its bare directory name; a linked worktree's project name is <repo>-<worktree>, because a worktree basename alone is not unique across repos. Re-taking this file renames an existing linked worktree's stack, so tear a running one down first - under the new project name Compose no longer sees the old containers or volumes (recover with docker compose -p <old-name> down -v) - in a polyrepo the same feature branch runs in several members, and a shared project name meant one member's docker:clean tore down another's containers and volumes. mise run docker:clean tears down a worktree's services + volumes before it is removed, scoped to that worktree - spelled mise run ws:docker:clean in a workspace repo, whose profile replaces core mise.toml and prefixes every whole-product task. This [env]._.source line is also what makes a new worktree need mise trust: mise loads a data-only config untrusted but refuses one that executes code at load time (see the .worktreeinclude row above). See the Parallel worktrees section of rule 45-delivery.
CLAUDE.md product Only product-specific context - standards prose is never duplicated here. Carries the <!-- steer:profile=... --> marker (see Repo profiles).
/apps, /packages product Empty at bootstrap - the scaffold ships no starter app. The bundled scaffold deliberately carries no placeholder to delete: the first real app is created for the chosen stack, by /steer:build step 5 in a PO build or by the dev following the spec-first loop. pnpm dev / db:migrate / db:seed no-op harmlessly until it exists, and apps/README.md says the folder starts empty until the app that fills it lands. The one exception is a fork of the retired static repository-template, which does carry apps/web + packages/core - /steer:setup init Path A swaps or removes it.
ARCHITECTURE.md scaffold The as-built system model at the repo root - narrative and tables only, linking rather than inlining the rendered diagram at spec/design/architecture-diagram.md. Its staleness is checked by /steer:audit code (the DX & docs dimension), not by /steer:audit spec - that mode is spec-vs-spec, diffing the as-built /spec spine against the tracker spec export and reading neither the code nor this file. Required at the root by rule 30-spec § Living documentation (and the layout conventions in /steer:reference conventions), and allowlisted there by the housekeeping standard (HOUSEKEEPING.md) so /steer:work tidy never proposes relocating it.

Repo profiles

Not every managed repo is an app monorepo. A repo carries a profile - app (default), infra, service, library, cli, or workspace - recorded as a <!-- steer:profile=... --> marker on the CLAUDE.md ## Profile section (a sibling of the delivery-mode marker; absent => app, for back-compat).

workspace is the odd one out: it hosts a polyrepo product's /spec spine and owns no application code. Its topology is derived from disk (spec/workspace.yml at the host, spec/PRODUCT.md at each member), never from this marker - so the marker follows the topology rather than declaring it.

The profile is a bootstrap-time choice that selects an additive set of scaffold layers /steer:setup init / /steer:setup adopt lay down (later layers only add):

  • Layer 0 - Core (every profile): mise.toml toolchain pinning (node/python/uv mandatory - agent tooling needs them), the /spec spine, stack-agnostic CI hygiene, dotfiles, policy/, the version-pin scripts, and - deliberately for every profile - compose.yaml + scripts/worktree-env.sh (the containerize-by-default surface, so devs run backing services in Docker rather than on the host).
  • Layer 1 - Node baseline (profiles/_node/, Node-stack profiles only): package.json, pnpm-workspace.yaml, biome.json, configs/, packages/. Every Node profile is a pnpm workspace (monorepo-by-default). The root package.json ships a packageManager placeholder that /steer:setup init stamps with the mise-pinned pnpm version, so corepack (e.g. in a Docker build) uses the same pnpm that wrote pnpm-lock.yaml. Skipped for infra, and replaced by pyproject.toml/Ruff for a Python-only product.
  • Layer 2 - Profile extras (profiles/<profile>/): app adds apps/, DESIGN.md and .claude/launch.json (the Claude Desktop Code tab preview-server config - convenience only, never overwritten if the repo already has one); service adds apps/; library/cli add nothing (the skill adapts package.json); infra substitutes a tofu/terragrunt/ansible-flavored root mise.toml (which still pins node, sources worktree-env.sh, and defines the core docker:up/docker:down/docker:clean tasks the always-on worktree rules mandate - it keeps the core compose.yaml) and gets CI that auto-detects *.tf/Ansible and runs tofu fmt / ansible-lint; workspace replaces the core README.md, mise.toml and compose.yaml and adds scripts/ws.sh plus a .gitignore fragment. Its mise.toml drops pnpm/biome (no code here), keeps the agent-runtime baseline and convert:doc (PO documents land at the spine host), and adds the ws:* member tasks - including ws:dev, which as shipped boots the members' backing services via Compose include:; the app half (each member's own dev server) needs mise monorepo mode enabled plus one depends entry per member with a dev task, which /steer:setup init resolves from the manifest. Every task the workspace profile defines is ws:-prefixed (bar convert:doc) so that it cannot shadow a member's own task; its compose.yaml declares no services and include:s each member's file. The member checkouts are git-ignored clones, not submodules, so nothing pins a member SHA.

So a non-app repo is never skipped at bootstrap - it shares all of Core, and an infra, library or cli repo that genuinely runs no local services simply deletes the core compose.yaml. That deletion is what licenses pruning the docker:*/db:* tasks: keep the compose file and you keep the tasks, because steer's teardown hooks resolve docker:down / docker:clean (or their ws:-prefixed forms) by name from mise tasks ls, so pruning them while a stack still runs silently disables both hooks - and the always-on rules then leave mise run docker:clean entirely in your hands. An infra repo may drop the paired scripts/worktree-env.sh (and its mise.toml [env]._.source line) too; a library/cli should keep it, since worktree-port-isolation reports n/a only when the stack is none. The installed repo layout is unchanged by this organization; only the plugin's bundle and the init/adopt composition differ.

Always-on rules do not read the marker - they self-gate on filesystem traits and on what policy/ declares, via the inject-when mechanism, so the injected rule context always matches what is on disk. Six expressions gate a shipped rule today:

Expression Rules
code-project the code-loop rules, enumerated in Configuration & rules
org-e22 10-stack, 15-commands
has-iac&org-e22 12-stack-infra
has-openspec 33-spec-workflow-openspec
tracker-github 36-issue-first
automation-optin 53-autonomous-loops

lib/scope.sh also defines has-apps, has-compose, has-infra, polyrepo, has-workspace-manifest and has-product-pointer, all of which are available but carry no rule today - the polyrepo topology is deliberately delivered by a SessionStart note rather than an always-on rule, so the existence of the polyrepo token is not evidence that a 21-polyrepo rule exists. Nor is any shipped rule currently composed with |; the one composite in use is the & above.

A monorepo that also has a nested /infra dir stays profile app and gets the infra-stack rule when it is on the e22 org pack, since has-iac holds and org-e22 is what an absent policy/org.yml means. Deployment guidance reaches it regardless: since the 6.6 rule diet that is a section of 45-delivery, which is code-project, and a repo that deploys nowhere says so in policy/delivery.yml (environments: []) rather than being skipped. The profile is read by /steer:setup sync and scripts/scan-capabilities.sh (an informational profile fingerprint) for reporting and overlay decisions.

Org packs - which house defaults reach the repo

A profile says what shape the repo is (app, infra, library, cli, workspace). An org pack says whose defaults it follows, and the two are independent: an infra repo on the e22 pack gets OpenTofu + Terragrunt, an infra repo on no pack gets the vendor-neutral core and picks its own.

The repo declares its pack in policy/org.yml:

schema: 1
pack: e22   # any other value drops the pack

An absent file reads as e22. That is the upgrade contract, not a fallback: every repo bootstrapped before packs existed was built against these defaults, so absence has to mean the status quo and opting out has to be the deliberate edit. /steer:setup sync seeds the file from a migration-ledger entry, changing nothing, so the choice becomes visible and editable.

pack: e22 any other value
Stack defaults (10-stack) Next.js + TS + Tailwind, Node + PostgreSQL + Drizzle, pnpm/uv, Biome/Ruff, Vitest/pytest, Better Auth, Sentry not delivered
Useful commands (15-commands) mise run dev:setup, pnpm dev, uv run, the profile task map not delivered
IaC stack (12-stack-infra) OpenTofu + Terragrunt on AWS, delivered when the repo also does IaC not delivered
Secrets at rest SSM Parameter Store SecureString, Secrets Manager for rotation rule 60-high-risk says "the declared store" and asks
Baseline patterns (85-practices) stated as principles, with the pack naming each instance stated as principles, unchanged

Dropping the pack does not relax anything: the patterns rule, the Definition of Done, testing, spec coupling and the high-risk gates are all pack-independent, and choosing a different stack is still an ADR. The pack decides which defaults are delivered, never whether a choice needs recording. Full prose: /steer:reference conventions -> "Org packs".

Root housekeeping

The root holds scaffolding, config, and the four standing documents the rules require there - README.md, CLAUDE.md, ARCHITECTURE.md and DESIGN.md - not the spreadsheets, decks, diagrams, and specification / requirements documents (.pdf, .docx, decks - specs, briefs, RFP/SOW) that feed the spec. Those are source material: their home is /spec/reference/; architecture and flow diagrams go to /spec/design/.

DESIGN.md is not documentation for humans only - it is read. A skill that renders a shareable Claude Artifact styles the page from the tokens DESIGN.md declares (root, or apps/<app>/DESIGN.md), falling back to the house default only when the repo declares none - the Artifact standard (/steer:reference artifacts) and, in practice, /steer:status feature <id>. Populate it and stakeholder-facing pages carry the product's own palette, type scale, and spacing; leave it empty and they carry the generic look. Never an invented brand either way.

Steer keeps the root clean as it works. When a session notices a loose root file it can confidently classify, it moves it to the right home immediately (git mv, filename preserved) - no confirmation for a move that was never in doubt. Confirmation is reserved for where judgment or loss is at stake: renaming a cryptic name to a cleaner one is proposed (the file still moves now, under its existing name); a file whose purpose or correct home is ambiguous - or a Copy of ... / look-alike pair - is asked about before anything happens; and deletion is never automatic, always waits for a yes, and covers only two cases - true OS junk like .DS_Store (which also gets a .gitignore pattern so it can't return), and an already-absorbed source, a spec/requirements doc whose bytes match a committed spec/sources/**/original.*, where deleting the redundant duplicate beats filing a second copy (no .gitignore pattern there - it isn't junk, and a later version is expected). Run /steer:work tidy for a full sweep of an accumulated pile.

Scaffold storage convention

Scaffold dotfiles are stored in the plugin without the leading dot (gitignore, env.example, github/, claude/, ...) so they don't act on the plugin repo itself. MANIFEST.md maps each stored file to its installed path (adding the dot back). When a standard implies concrete scaffolding, the scaffold bundle is updated in the same change as the rule.

When /steer:setup init, /steer:setup adopt, or /steer:setup sync install a scaffold file that already exists in the target repo, they merge additively and never clobber: Markdown spec files reconcile on heading/checklist anchors (template-reconcile.sh), and the structured-config files - the line-based .gitignore / .gitattributes / .worktreeinclude and the JSON configs .claude/settings.json, biome.json and tsconfig - reconcile with scaffold_reconcile.py, which unions JSON arrays and adds missing keys/lines without overwriting, reordering, or removing any existing value.

.changie.yaml and .changes/ follow the same dot-stripping (changie.yaml, changes/ in the plugin - changes/ being the first stripped directory beyond github/ and claude/). .changie.yaml is not in the scaffold_reconcile.py set: it is seeded once and then the product's to tune, so an existing one is left exactly as it is rather than merged into.

The committed editor configs (.vscode/extensions.json, .vscode/settings.json, and .vscode/mcp.json - the last being how Copilot/VS Code teammates get the same MCP servers) are merged by hand, not by that script: all three templates carry // comments, and scaffold_reconcile.py parses with strict JSON, so it refuses them. They are yours once installed - merge additively, drop what you don't use. VS Code is the default editor; see the Stack rule / /steer:reference conventions.

The one exception is the .claude/settings.json permissions block, which Claude Code evaluates by precedence deny > ask > allow. There, the same pattern in two tiers is a contradiction rather than a choice (the lower-precedence copy never governs), so after merging, the reconcile keeps each permission pattern only in its most-restrictive tier and drops the others - preventing a sync from leaving, say, Bash(git push) in both allow and ask, and healing a repo already in that state. Because the surviving tier is the one that already governed, effective behavior is unchanged.

Versioning the contract

/spec/.version records the plugin version the spine was last reconciled against. After a plugin release, /steer:setup sync applies pending structural migrations from the ledger, reconciles additively, and re-stamps .version. Ledger migrations cover the non-additive changes reconciliation cannot express - renames and moves (git mv), deletions (git rm), in-file token rewrites (replacing a string that already exists in a materialized file, e.g. the e22-standards -> steer rebrand), and whole-file or whole-section re-takes (the file's content, or one bounded region of it, has moved past any enumerable set of old->new pairs, so the current template replaces it; a section re-take states its region boundaries) - each applied read-then-propose, never clobbering filled-in content, and each carrying the consumer's own edits forward rather than discarding them.

Two ledger entries landed in 3.23.0: the living global architecture diagram is renamed spec/design/architecture.md -> spec/design/architecture-diagram.md (a git mv plus an enumerated in-file token rewrite, so history follows the file and the links to it are updated), and the retired markitdown MCP server is cleared from .mcp.json / .vscode/mcp.json (harmless until the migration runs - the converter is now the on-demand mise run convert:doc task). Neither requires manual work; /steer:setup sync proposes both.

Six further entries landed in 3.24.0. Four are non-additive edits to materialized files that reconciliation cannot carry: scripts/worktree-env.sh gains a repo prefix on COMPOSE_PROJECT_NAME in a linked worktree (a whole-section re-take - and tear any running linked-worktree stack down first, or its containers and volumes are orphaned under the new project name); spec/tracker.md's promoted- question rule is reversed (the ### Q-NNN block now stays, with the ref in its tracker: field); a polyrepo member's spec/PRODUCT.md spine-resolution ladder now requires spec/workspace.yml to be present at workspace.path rather than merely a directory (resolved against the primary checkout - from a linked worktree the recommended relative .. otherwise lands on a real but empty directory and the product's specs read as absent); and spec/PRODUCTIONIZATION.md's open-question seed becomes a ### Q-NNN field block, because the SessionStart hook and /steer:spec questions count only those, so the old bullet seed modelled a shape neither one sees. The fifth covers the workspace task rename. The workspace profile's whole-product tasks are now ws:-prefixed (ws:dev, ws:docker:up / down / clean), because an unprefixed name in the workspace's mise.toml is an ancestor config in every member cloned inside it and shadows any member that does not define that name. mise.toml is materialized and product-owned, so additive reconciliation cannot carry a rename - it splices in what is missing and would leave both the old and the new names in place. That is exactly the case a ledger entry exists for, so the rename ships as one: /steer:setup sync proposes the four task headers, repoints every reference to a renamed task (including the live ws:dev depends, which resolves in the caller's task set and would otherwise bind to a member's docker:up), re-takes scripts/ws.sh whole - the new script carries the preflight subcommand the rename points a task at, plus its own stale mise run dev header comment and ws:-prefixed failure messages, so no enumerable pair set describes it; a consumer's added ws: subcommands carry forward - replaces ws:docker:up's first run element with that preflight guard while leaving the line that boots the stack alone, and relocates the whole commented monorepo section above [settings] where mise will actually accept the key. The entry is precondition-gated to workspace-profile repos, so member repos and non-workspace profiles are untouched.

The sixth is an in-file token rewrite in the scaffold's infra/README.md - the copy materialized into a monorepo with a nested /infra dir, which stays profile app, not the infra profile (a root-level infra repo keeps these conventions in its own README, which this entry leaves alone). Its state-backend prose named S3 + DynamoDB locking and told the reader to bootstrap a bucket and lock table, while rule 12-stack-infra mandates S3 with the native use_lockfile lock (S3 conditional writes replace the table). Both lines are procedural - a human follows one to bootstrap an environment and the other to write root.hcl - so a repo still carrying them provisions a lock table the standard no longer wants. The entry rewrites only those two lines and is precondition-gated on one of the stale tokens still being present. It deliberately stops at the prose: moving a live state backend off a DynamoDB lock table is an infrastructure change with its own plan, review, and blast radius, so if the repo's root.hcl still configures dynamodb_table, /steer:setup sync lands the prose fix, says so, and hands the backend migration to a dev as separate work.

One further entry landed in 4.0.0, and it makes the action history a directory of immutable per-entry files, spec/history/YYYY-MM-DD-HHMM-<slug>.md, replacing the single append-only spec/HISTORY.md. It is the most consequential entry for an adopted repo, and it is deliberately not a move: the old file is frozen in place as the pre-migration archive and is never split into per-entry records, because those entries are immutable review evidence that a bulk rewrite would re-date and risk mangling. The entry creates the directory (materializing spec/history/README.md, the format doc), adds the frozen banner to the archive's header prose without touching a single entry below ## Entries, and rewrites the old path in the live instruction surfaces reconciliation cannot reach - ci.yml's spec-drift filter (which matches date-named entries only, so editing the directory's README.md format doc does not clear the gate, and keeps ^spec/HISTORY\.md$ alongside the new pattern so a repo mid-migration is not flagged), the PR template's living-docs checkbox, README.md, CLAUDE.md, spec/tracker.md, each spec/sources/*/source.md, and a polyrepo member's spec/PRODUCT.md. It leaves .github/copilot-instructions.md and .agents/skills/* alone - those are re-copied from the plugin on the same sync - and a false-positive guard keeps it away from provenance prose, where a mention of spec/HISTORY.md is a legitimate record of where something was written at the time. In a polyrepo the history belongs to the workspace, so a member gets no local spec/history/ and only four of those rewrites - CLAUDE.md, spec/PRODUCT.md, the PR template and ci.yml. Finally the migration logs itself as the directory's first entry, which both satisfies the living-docs rule for the migration PR and proves the new path works.

The entry keyed 5.0.0 narrows a feature's spec > Status: from five values to draft · approved · live. implemented and validated are retired - they were pure mirrors of the issue's validate/done, a derived value stored in a second file that nobody recomputed. The entry applies only if some spec/features/*/intent.md still carries a retired value or still prints the five-value enum hint; a spine already on three values is skipped. It rewrites implemented -> approved (the build is the issue's business; scope approval is what the spec holds) and validated -> approved unless the feature is genuinely released, in which case live - and where release state is not evident from the repo, it deliberately takes approved and says so in the sync PR for a human to promote, rather than guessing live. It touches no issue: an intent reading approved beside an issue reading done is the intended pairing, not drift. The PO-acceptance checkboxes, including PO validated the working demo, are left alone - that record now carries the acceptance that Status: validated used to imply. In a polyrepo spec/features/** belongs to the workspace, so a member applies none of it locally.

The two 6.0.0 entries are both changes reconciliation cannot complete on its own. The first retires the Copilot prompt-file surface in favour of the cross-tool .agents/skills/ tree: it copies templates/agents/skills/ in verbatim (that tree is Verbatim: yes under agent-surface-current, so it is copied, never reconciled - the migration exists only to create it the first time, after which /steer:setup sync keeps it current), then deletes the steer-*.prompt.md files and the .github/prompts/ directory only if nothing else remains - a prompt file the team wrote themselves is theirs and stays, directory and all. Two live pointers are rewritten because additive reconciliation cannot reach them: the comment above the Copilot toggles in .vscode/settings.json (the chat.promptFiles setting itself stays on - it governs any prompt files the team keeps, which this migration does not touch), and a .agents/ line in .gitignore if an earlier cleanup added one, since the skill tree is committed rather than local state. A repo that never installed the Copilot surface is n/a and skips the entry, and it earns no history entry - a surface swap is not a repo-level event.

The second rewrites the GitHub PAT instruction in README.md, and it is the one entry in this ledger whose completion is not visible in the tree. The bundled github MCP server used to authenticate from a shell-exported GITHUB_PAT; it now reads the plugin's own github_pat config value, so a materialized README still saying "export it from your shell rc" is instructing the reader to do the thing that stopped working on update. Three token rewrites apply, and two of them sit outside the "GitHub MCP server" section - the quickstart line and the GitHub-Actions paragraph - so re-taking that one section leaves the precondition firing forever; the plugin's own scaffold README.md already carries the post-change wording for all three. The precondition is case-sensitive on purpose, so the post-migration github_pat spelling cannot re-trigger it, and the guard leaves GITHUB_PAT alone in provenance prose and in any Actions workflow, where a same-named CI secret is untouched by this change. Applying it does not restore anyone's access: the human must supply the token to the plugin once per machine, and the entry says so explicitly rather than implying the file edit was the whole job.

Three entries landed in 6.2.0, all on the shipped CI: the workflow templates are hardened (actions pinned to full SHAs, least-privilege permissions:, no persisted checkout credential, Dependabot auto-merge gated on the PR author rather than github.actor); ci.yml skips draft PRs and cancels a PR's own superseded runs (never main's); and the ci job's inlined steps move into scripts/ci-*.sh behind mise run ci:* tasks, so a contributor can run the gate that decides their PR. 6.3.0 installs the curated changelog the standard always named - .changie.yaml plus .changes/, with CHANGELOG.md generated - and retires the merge=union line. 6.4.0 adds two: dependabot-auto-merge.yml gains the checks: read / statuses: read scopes its gh pr checks step needs, and an OpenSpec repo moves steer's own artifacts (ADRs, tracker, app guide) under openspec/steer/, leaving native repos untouched. 6.5.0 seeds policy/delivery.yml, which declares how the repo delivers instead of the rule imposing one model, and 6.6.0 seeds policy/org.yml, whose pack: key selects the org pack.

The 7.0.0 entry follows the skill-surface cut to seven front doors: it rewrites every live invocation of an absorbed skill (/steer:init, /steer:questions, /steer:issues, ...) to its front-door mode, reaching the files the standing invocation-hygiene scan does not - feature intent.md files, mise.toml, policy/, spec/tracker.md. Append-only prose (history, ADRs, absorbed sources, audit reports) keeps the spelling it was written in. The full mapping is Invocation changes.

Ledger entries are keyed by the release that introduced them, and /steer:setup sync skips every entry at or below a repo's spec/.version stamp. An entry authored but not yet cut is keyed [Unreleased] - never a guessed number, since an implementation PR merges before the release that names it - and the release PR renames it. A [Unreleased] heading is never "at or below" any stamp, so such an entry is always walked by its precondition rather than silently skipped.

Because the history is now append-only per file, a correction is a new entry carrying - **Corrects:** <filename> - never an edit to the entry it corrects.