Status: implemented. Stages 1–5 below are live as of v0.3.0:
init.shwrites the provenance stamp and defaults--updateflags from it;--updateis a triage tool (summary-first,--diff/--only, exit 0/1);CHANGELOG.mdcarries impact-tagged entries; the first real migration lives inmigrations/01-delivery-contract.md; and/toolkit-updateand$toolkit-updateare rendered into scaffolded projects for Claude and Codex respectively. This file is now reference documentation for how it works and why.
bin/init.sh --update already does the mechanical half: render the
current templates, diff -u each one against the live file in the target,
write nothing. What is missing is everything around it — which is why
downstream projects drift and stay drifted.
An update command cannot bootstrap itself into a project that does not have it yet. After pulling the toolkit checkout, an existing stamped project can add newly introduced template files without repeating its original flags:
<toolkit-checkout>/bin/init.sh --update --target <project> # inspect first
<toolkit-checkout>/bin/init.sh --target <project> # missing files onlyThe plain run now loads values from .agents/.toolkit-version and retains
render()'s skip-if-exists guarantee. A Claude user whose older scaffold
already has /toolkit-update can let that command perform these steps. A
Codex-only user upgrading from before Codex support must run the shell bootstrap
once; afterward $toolkit-update is installed and handles future updates. A
pre-v0.3.0 scaffold has no stamp, so it still follows README's one-time flag
recovery procedure.
This is the plan to close that, in five stages. Stages 1–2 are the ones worth doing first; each stage is useful on its own and none blocks the next.
Run --update against a project scaffolded three months ago and you hit
four problems in a row:
- You must remember the original flags. Pass a different
--builder-modelthan the original init and every substituted line in every file shows up as diff noise. Nothing recorded what was used. - No baseline. There is no record of which version of the toolkit the project was scaffolded from, so "what changed since I took this" is unanswerable — you get a diff against your local edits, which conflates upstream changes with your own customizations.
- No signal. Twelve files of raw
diff -uwith no indication that one hunk is a permission-block change you must merge today and another is a typo fix in a comment. Every upstream change looks equally urgent, so none of them get merged. - No migrations. When the state-file contract gains a field or a
section — as it has now, substantially (the acceptance-criteria ledger,
three budget counters, a split blocked status) — existing
.agents/T-*.mdfiles in flight silently lack it, and nothing says so. Stage 4 below now carries the real migration note for that change.
Consequence: --update is technically correct and practically unused. A
project scaffolds once and never upgrades, which means every hardening
lesson this toolkit earns after the scaffold date stays upstream.
The single highest-value change. init.sh writes, on a real scaffold:
.agents/.toolkit-version
toolkit_sha: 79f0e14
toolkit_tag: v0.2.0
scaffolded: 2026-08-26
project_name: my-project
claude_model: sonnet
builder_model: hcnsec/kimi-k2.7-code
reviewer_model: hcnsec/glm-5.2
reviewer_fallback_model: sonnet
tester_model: hcnsec/deepseek-v4-flash
test_dir: e2e
Then:
--updatedefaults every flag from this file.bin/init.sh --update --target .becomes the whole command. Problem 1 disappears, and with it the diff noise that makes the output unreadable.--updateprints the baseline — "scaffolded from79f0e14(2026-08-26); toolkit is now ata1b2c3d(14 commits ahead)". Problem 2 disappears.- A successful merge updates the stamp;
--updatealone never writes it. - It is committed (unlike
.agents/.oc-port) — it describes the project, not the laptop.
Follow the existing conventions: written only when FRESH_SCAFFOLD was
true before any render() ran, same ordering rule as
.needs-customization; never written by --update. Add one smoke case.
Cost: ~30 lines in init.sh, one smoke case.
There are currently zero git tags. Downstream has nothing to pin to.
Tag releases, with the version number meaning something specific to this toolkit rather than to code:
| Bump | Means | Downstream obligation |
|---|---|---|
| MAJOR | The state-file contract, a role's authority, or a script's interface changed | Must merge deliberately; may need a migration (Stage 4) |
| MINOR | New template, script, flag, or role rule | Should merge at the next task boundary |
| PATCH | Prose, docs, comments, formatting | Merge whenever, or never |
CHANGELOG.md, with an impact tag per entry — this is what makes a
diff triage-able:
## v0.3.0 — 2026-09-xx
- `[contract]` state file: added *Reviewer for this task* / *Tester for
this task*. Migration: 03-reviewer-tester-fields.md
- `[safety]` builder: removed blanket `git *: allow`; enumerated
read-only git verbs. Merge this one now.
- `[process]` feature.md: ask before splitting a very large task
- `[docs]` README: quick-start blocks mergedFour tags, in descending order of "drop what you are doing":
contract › safety › process › docs. A downstream operator (or
their AI lead) reads the changelog first and the diffs second — which
inverts today's experience, where the diff is all you get.
Cost: a file plus the discipline to append to it. It is the same "write down why" discipline the toolkit already applies everywhere else.
With Stages 1–2 in place, --update gets three cheap upgrades:
-
Summary first. List changed files with their changelog impact tag before printing any hunks:
init.sh: 3 of 12 files differ from toolkit v0.3.0 (you are on v0.2.0) [safety] .opencode/agent/builder.md — permission block [contract] .agents/TEMPLATE.md — 2 new fields [docs] scripts/team.sh — comment only run with --diff to see hunks, or --diff <path> for one file -
--only <path>to diff one file, so merging can be done one file at a time rather than all-or-nothing. -
Meaningful exit codes:
0clean,1drift. That is all a downstream project needs to runinit.sh --updatein its own CI weekly (or in/loop) and open an issue when the toolkit moves — the difference between pull-when-you-remember and being told.
Cost: ~40 lines, all in the existing render() update branch.
Only [contract] changes need this, which is rare by design. Ship a note
per contract change under migrations/. There is now a real one to
write — the delivery-contract work applied from REVIEW-2.md changed the
state file more than anything since the toolkit was extracted:
# 01 — Delivery contract (ledger, budgets, split blocked status)
Applies to: .agents/TEMPLATE.md, scripts/verify-state.sh, every role file,
and any in-flight .agents/T-*.md
New header fields (add under **Implementer for this task:**):
**Reviewer for this task:** / **Tester for this task:**
**Test-fix loops:** 0 / 2 **Spec bounces:** 0 / 1
**Blocked since:** —
New section, under Acceptance criteria:
### Acceptance criteria ledger — one row per AC, columns:
| AC | Met? | Reviewer evidence | Test evidence |
Status enum: `blocked` split into `blocked:question` / `blocked:spec`.
Bare `blocked` still validates — it is kept as the legacy value precisely
so in-flight files don't break. Nothing to do for existing tasks.
New script: scripts/verify-spec.sh (run at the end of step 1).
Safe to skip for tasks already at Status `done`.Backward compatibility, deliberately: the new verify-state.sh treats
every new field as optional — an absent budget counter is skipped rather
than failed, an absent ledger means the done-gate has nothing to check, and
bare blocked still validates. So a downstream project can take the new
script before it takes the new template, and its in-flight tasks keep
passing. What it does not get until it takes the template is the
guarantee: no ledger means nothing refuses a premature done. That is the
difference between the checks being installed and the checks being load-
bearing, and it is worth saying out loud in the changelog entry.
Short, literal, and hand-appliable. Do not build a migration runner —
these are two-line edits a few times a year, and a runner is exactly the
kind of machinery this toolkit is right to refuse. The value is the note
existing at all, so an in-flight task does not silently lack a field that
verify-state.sh will later check for.
The AI-assisted merge path the README already gestures at ("hand the diff
to your AI lead and ask it to reconcile"), made into a real, templated
command/skill rendered by init.sh alongside each lead's feature entry point:
- Read
.agents/.toolkit-versionfor the baseline. - Run
bin/init.sh --update(flags come from the stamp). - Read the toolkit's
CHANGELOG.mdfor entries after the baseline. - Work the diffs in impact order —
contract, thensafety, thenprocess, thendocs. - For each: show the user what changed upstream, what this project
customized locally, and the proposed merge. Never silently
overwrite a local customization — that is the whole reason
render()is skip-if-exists. - Hard stop on permission blocks. Any hunk touching a
permission:ortools:block is presented, never auto-applied, and carries the toolkit's standing rule: verify it live against the real server before trusting it. Treat a new or changed structural script (verify-state.sh,verify-spec.sh,promote-findings.sh) as[safety]too: these are what the pipeline's guarantees actually rest on, and a project running an oldverify-state.shagainst a new template silently loses the done-gate rather than failing loudly about it. This toolkit's own history is the argument — a blanket-deny that read correctly and did not enforce. - Update the stamp to the new SHA/tag when the merge is accepted.
- Report merged / skipped / deferred, and record it in the project's Decisions log.
Run it at a task boundary, never mid-task — the state file contract is
live while a task is in flight, and changing the template under a running
task is exactly how an in-flight T-<id> ends up half on each contract.
A downstream project can only merge what upstream shipped coherently. This
toolkit keeps several hand-synced copies of the same rules (three of the
lead's flow, two of the state-file contract), and hand-syncing failed twice
on consecutive commits before test/invariants.sh existed to check it —
including one rule that a review file recorded as applied and that was not
actually in the file.
That test is the upstream-side counterpart to everything above: it asserts one grep per (rule, file) pair and runs in CI, so a release cannot ship a rule that reached only two of its three homes. Add a rule to a flow copy and you add one line to its table in the same commit. Without that, a downstream project merging faithfully still inherits whichever copy happened to be stale.
Considered and deliberately not recommended.
A git submodule or subtree would make updates trivial (git pull) — and
would break the property the whole design rests on: every generated file
is meant to be edited locally. Projects are expected to widen the
reviewer's permission block after verifying it live, fill role files with
their own project-specific pitfalls, and add steps to feature.md. A
submodule makes those edits either impossible or upstream-hostile.
Copy-with-provenance is the correct model for generated-then-customized files. What is missing is not a different distribution mechanism — it is the provenance stamp and the changelog that make merging a copy tractable. Stages 1–2 supply exactly that.
| When | Do |
|---|---|
Weekly, in CI or a /loop |
bin/init.sh --update --target . — exit 1 means the toolkit moved |
On a [safety] entry |
Merge at the next task boundary, verify permission changes live |
On a [contract] entry |
Merge before starting the next task; apply the migration to in-flight state files |
On [process] / [docs] |
Batch them; merge whenever convenient |
| Never | Mid-task |
Stage 1 → Stage 2 → Stage 3 → Stage 5 → Stage 4 as needed.
Stages 1 and 2 alone remove the two things that actually stop people from upgrading (remembering the flags, and not knowing what matters). Stages 3 and 5 are convenience on top of a mechanism that already works. Stage 4 is paperwork that only earns its keep when a contract change actually ships.