Repository navigation
teamwright 0.12.0
Added
- Stages: the lifecycle runs one stage per session when you want it to. Until now
/teamwright:flowdrove a task through analysis, red tests, implementation, gate, review and verification in one session: an autonomous job that was cut lost the whole task, and the context grew with every role. The lifecycle is now five stages — analyze (readyTODO,ANALYSIS,VERIFY_FAIL→IN_PROGRESS), red-tests, implement (green tests, the test-writer's failure-branch bite andtw-mutants,tw-gate --for-task+tw-gate <area>, technical-writer, commit,IN_PROGRESS → NEEDS_REVIEW; a failed check is fixed in the same session), review, verify — and push (DRAIN) stays a separate owner decision. New commands:/teamwright:analyze,/teamwright:red-tests,/teamwright:implement,/teamwright:review,/teamwright:verifyrun a named stage and first ask the router, refusing with the right command when the task is at another stage (/teamwright:implement T-12on a task without red tests →/teamwright:red-tests T-12);/teamwright:routerasks the router and runs exactly one stage, then stops at the barrier./teamwright:flowstays, as a loop of router steps in one session; its lifecycle text moved into the stage commands, and its resume heuristic into the router. - The
stage:task field (red-tests | implement, the sub-stage ofIN_PROGRESS), written by the stage that finished its work and committed with it. The router checks it against the evidence (## Testsfilled = the red tests are written; analysis complete): a field behind the evidence wins (red-testswith the tests filled — a send-back to the tests), a field ahead of it loses (implementwithout tests → red-tests), with a warning; a task without the field (started by an older flow) is routed by the evidence.IN_PROGRESSmay now cross a session boundary when its stage is recorded and the tree is clean (lifecycle.md,sessions.md,AGENTS.md). Task template: astage: ""line. - Stage gate (analysis gate, check 4): entering
IN_PROGRESSsets the stage in the same edit (red-testsfromTODO,ANALYSIS,VERIFY_FAIL; a send-back may setimplement), the field only movesred-tests → implement(back tored-testsfrom implement, or when review or verification return the task), is never removed from anIN_PROGRESStask,stage: implementneeds## Testsfilled, andIN_PROGRESS → NEEDS_REVIEWneedsstage: implement, the tests and — for a task that changes code (_task.code_changed: a commit naming the task, or a tracked uncommitted change, touches a code file) — the### Failure branchestable: only the implement stage hands a task to review. Shell rewrites ofstage:in a record are refused likestatus:, recognised by the values too. Tests intests/test-analysis-gate.sh. - Router: stages, per-stage batches and thresholds, candidates, models, headless output (
scripts/tw-next.py). Every task-bound decision carriesstage; the output addsbatch(the router's default) andbatch_cap,candidatesfor the chosen stage — every waiting task in router order with priority, effective priority, how many tasks it unblocks,depends_on, seam paths and the areas they fall in, size hints, the candidates itoverlaps(tw-parallel.py's seam rule) and those it depends on — so a session picks its batch without reading task files;stages(per stage: waiting, due, why),work_available(the one predicate for "what runs" and "nothing to do"),model,command,selectandheadless(run,prompt,model, a note) for one stage per CI job.--stage S [--task IDS]answers a stage command (request: ok, batch, candidates, hint; an explicit stage ignoresmin). New settings, validated by the installer:process.stages.<stage>.batch(1–10, default 1),.min(1–10, default 1) and.max_wait_hours(0–168, default 0) — a stage that is not due (fewer thanminwaiting, no P0/P1 task, the oldest younger thanmax_wait_hours) moves behind the due work, and runs when nothing else is due;process.models.<stage>(opus,sonnet,haiku,fableor aclaude-…id; unset = the role files'model:), passed as the agent tool'smodelon the stage's role spawns;process.router.select: script | agent(defaultscript: the script selects and counts, the session picks the batch from the candidates;agent: the session reads the task records itself — more tokens, for records the script cannot read). Defaults behave as before stages. Tests intests/test-tw-next.sh(stage field vs evidence, legacy tasks, resume rules, thresholds, batches, candidates, requests, models, headless) andtests/test-installer.sh. - The journal records the stage and the chosen batch:
outcomes.jsonlgetsstage(from aStage: analyze|red-tests|implement|review|verify|flowline),batchandbatch_reason(fromBatch: T-3 T-7 - <why>). Tests intests/test-journal.sh. - Tokens per stage.
StopandSubagentStoplines inoutcomes.jsonlcarrytokens: the model usage of the agent's transcript so far, per model (input,output,cache_read,cache_write) — the session's own lines fromtranscript_path(sidechains left out), a subagent's fromagent_transcript_path; a message the transcript repeats per content block is counted once, and the transcript is read incrementally (offset under.teamwright/logs/.tokens/).teamwright-metricsadds per stage (journal.stages, a Per stage table in Markdown): sessions grouped by the stage they declared, duration, tool calls of every agent, outcomes, tasks taken, tokens (per session, per task, per model) and, with the new--prices <json>($ per million tokens per model regex), a cost; a session that sent no tokens isno data, never 0./teamwright:improvereads it as a signal (model or batch per stage).docs/process/metrics.md,gates.md(journal). Tests intests/test-journal.sh,tests/test-metrics.sh. - Review gate check 7: one reviewed task spawns at most two new tasks (
lifecycle.md§6 — until now only a line in the review skill). A newdocs/tasks/<ID>.mdcreated while the session's latest spawn by its main agent is acode-reviewerfor task R counts toward R's review (the reviewer's rca task and the orchestrator's follow-up task alike; a later spawn of another role closes the window); the third is refused with the exit — add to an existing record, one umbrella task, or ask the owner. Gate-first warns. LimitTEAMWRIGHT_REVIEW_NEW_TASKS(2); ledger.teamwright/sessions/<R>.new-tasks.jsonl.gates.md, the review skill. Tests intests/test-review-gate.sh. - The recommended models per stage and stage thresholds are offered.
tw-next.py --jsonreportsrecommended(_config.PROCESS_RECOMMENDED, now withstages: batch 1, min 1, max_wait_hours 0 per stage, beside the models analyze/implement/review/verifyopus, red-testssonnet). adopt and init ask about the models and thresholds in their settings block,/teamwright:settingsshows the recommended column from it and offers the keys still at a different default,/teamwright:statusnames them. Tests intests/test-tw-next.sh. tests/test-usage-example.sh: the autonomous-run workflow indocs/usage.mdis parsed (PyYAML, or a subset parser where it is missing), its route step is run against a real project (tw-next.py --json, theheadlessandreasonfields it reads,run=falsewith nothing to do), everysteps.route.outputs.*another step reads is printed, the dispatched file is the one the docs name, and theclaudeflags exist when the CLI is installed. It found a real error:run: echo "router: …"is not valid YAML — the step now passes the reason throughenv(also no template expansion inside the shell).- Release channels: stable and experimental. The branch
nextis published as a second marketplace,teamwright-next(/plugin marketplace add smykov/teamwright#next,/plugin install teamwright@teamwright-next); it carries no version, so every pushed commit is an update.scripts/check-versions.py(inmake lint, andmake release-check) applies the rule by the marketplace name, not the branch:teamwright— the versions agree and have a changelog section (unchanged),teamwright-<channel>— no version anywhere. The installer records a channel kit's version as the 12-hex commit Claude Code computes (from the git checkout, orinstalled_plugins.jsonfor an installed copy), and enables its channel in.claude/settings.jsononly when no other channel of teamwright is enabled there or insettings.local.json— two enabled copies with one name do not mix. README (Channels), CONTRIBUTING (Branches and channels),docs/usage.md. Tests:tests/test-release-check.sh,tests/test-installer.sh.
Changed
resumemeans unfinished work in the tree: tracked changes, or a session that endedinterrupted. A current task on a clean tree no longer forcesresume: its stage is recorded and committed, and it is routed to that stage (continue). InIN_PROGRESSthe router's role follows the stage (test-writer at red-tests, developer at implement).tests/test-tw-next.shupdated.docs/process/sessions.md§8 (stages, the stage field, selection, batches, thresholds, models, the stage-command protocol, autonomous runs), §5 (model per stage);lifecycle.md,gates.md,plugin.md,AGENTS.md, the flow, parallel, settings and status skills,design-decisions.md,docs/usage.md(an example workflow, one stage per job) and the README describe it.tw-next.pyno longer prints aBrokenPipeErrorwhen its reader stops early (| grep -q).
Fixed
- Router: only a linked worktree is a per-task tree. An autonomous run checks out the whole project on a task branch (
task/T-12-run3); the router took the branch name for a parallel worktree of T-12, refused verify for it ("merge it from the main tree") and did not see the other task of the batch. The per-task view now needs a linked worktree (its git dir differs from the common dir). - Session check: a plugin loaded from a directory.
claude --plugin-dirand the Agent SDK load the plugin without the registry, so the SessionStart check told an autonomous session the plugin was not installed.TEAMWRIGHT_PLUGIN_DIRnames that directory; the check takes the plugin from there. - Headless: one owner decision no longer stops the chain of jobs (roast B1). Owner decisions (a
BLOCKEDquestion, an escalated review,NEEDS_OWNER) come before new work in the router's order, andheadless.runwas false for anydecide— one open question stopped every autonomous job althoughwork_availablewas true.headlessnow takes the first piece of work in router order and lists the pending decisions (decisions, also top level);runis false only when no work is left, the team is paused, the tree holds uncommitted changes of no task, or a pass ended. Astartwithprocess.autonomy.start: askis skipped (nobody answers the start question in CI;run=falsewith that reason when nothing else is left), andheadless.promptcarries no task ids: the stage session asks the router and picks its batch from the candidates, within the stage's cap. The interactive answer is unchanged. Tests intests/test-tw-next.sh(headless),tests/test-usage-example.sh. - Gate evidence survives a job boundary (roast B2). The review gate needed
.teamwright/sessions/<ID>.reviews.jsonland the reviewer's spawn intools.jsonl, the verify gate<ID>.verify.jsonl/<ID>.validation.jsonl— all local and gitignored, cleaned by every CI checkout: a job cut after the reviewer's APPROVE (or the validator's PASS) was committed but before the status move made the next job's gate refuse, and the router sent the same stage again, forever. When a gate lets a review, verify or validation record through it now also writes a stamp next to it,docs/tasks/<ID>.<reviews|verify|validation>/<n>.gate.json(writer's session, agent and type, round, verdict, kind/result/commit, whether the reviewer spawn was journaled, the record's sha256), committed with the record; a status move on a checkout without the task's ledger checks the stamp. A record changed after its stamp does not count; only the gates write stamps (state guard, the gates' shell checks); the trade-off — committed and reviewable, but no more tamper-proof than the local ledgers — is ingates.md, Gate stamps.docs/usage.md: the example workflow checks out withclean: false, caches.teamwright/logs(per-stage tokens, outcomes; the ledgers in.teamwright/sessionsdeliberately not), createswip/teamwrightwhen it is missing, and stops the chain when a stage made no new commit. Tests intests/test-review-gate.sh,tests/test-verify-gate.sh(B2),tests/test-usage-example.sh(cache, branch, the no-commit guard run). - The
stage:field can send a task back to red-tests (roast M1). The router trusted## Testsbeing filled over the field, sostage: red-testsset by review or verify ("the tests themselves are wrong") was routed to implement, where the stage gate then refusedNEEDS_REVIEW— a path to ESCALATE. The field now wins when it is behind the evidence; the evidence wins only when the field is ahead of it (implementwithout tests), with a warning. Tests intests/test-tw-next.sh(stages). - The stage gate is no longer opt-in or evidence-free (roast M2).
TODO → IN_PROGRESSwent straight toimplement, a record without the field was never checked, a send-back could clear it,→ implementpassed with## Testsempty,perl -pi -e 's/red-tests/implement/'named no key and passed, andfailure_brancheswas computed but never checked. Now: a stage is required on enteringIN_PROGRESS(red-testsfrom the pre-analysis statuses), never cleared,implementneedstests_written,NEEDS_REVIEWneedsstage: implement+ tests + the failure-branch table when the task changes code, and shell rewrites are recognised by the stage and status values. A task started before stages setsstage: implementwith its move to review. Gate-first warns, enforce blocks. The demo walks the stage field and shows the failure-branch denial. Tests intests/test-analysis-gate.sh(4. the stage field),tests/test-demo.sh. - A task that deploys is verified after DRAIN, and the push decision exists on a wip branch (roast M3). The router offered verify for a
CODE_COMPLETEtask withdeploys: truebefore its code was pushed, and in CI counted unpushed commits against thewip/branch's own upstream — always 0 after the job's push, so no push decision ever came up. Verify for such a task is now offered only once its last code commit is on the delivery branch (origin/HEAD, elseorigin/main/origin/master; without a remote nothing waits); the push reason names the tasks waiting for it. Off the delivery branch, unpushed commits are counted against it (state.delivery_ref).docs/usage.md,sessions.md§7–§8,plugin.md. Tests intests/test-tw-next.sh(M3). - The router's answer fits a stage session (roast M4). On a project with 116 tasks
tw-next.py --jsonprinted 183 KB, withrequestand the candidates last — cut off in a truncated preview — and every stage command also readsessions.md§8 (21 KB): the token saving of script selection was eaten by the reading. New--brief(implies--json):requestfirst, the action, stage, batch, model, command, compact candidates (at most max(8, 2 × the batch cap), overlaps among the shown ones,candidates_total), per-stage counts,countsby status, decisions, warnings, headless, alternatives, settings, a compact state — no task list; about 5 KB for 120 tasks. The full JSON putsrequestfirst and no longer lists closed tasks (DONE, DEFERRED) intasks;countscounts every status. The stage commands,/teamwright:routerand/teamwright:flowask with--briefand read the newdocs/process/stage-protocol.md(under 4 KB: ask, choose the batch, per task, end block);sessions.md§8 keeps the long explanation and links to it. Tests intests/test-tw-next.sh(M4: size, order, closed tasks, the skills). - Router: a plugin updated without
applyno longer misroutes silently (roast M5). The router preferred the project's installed_task.py; one from before 0.12 has notests_written/task_stage, so everyIN_PROGRESStask went to red-tests without a word. Every helper the project's copy lacks is now taken from the plugin's copy (as_configalready was), andwarningsnames the drift andtw-install.py apply.tw-progress.pyandtw-blockers.pyshare the loader.plugin.md. Tests intests/test-tw-next.sh(M5). - Explicit
--taskids are capped and checked (roast M6, minor 4). A stage command with named tasks ran all of them, past the stage's batch, in the given order. The router now puts a named task after the one it depends on, keeps at mostprocess.stages.<stage>.batch(the rest is left for the next session, with a warning), and names pairs whose seams overlap (request.warnings). With B1 (prompt without ids, a start that asks the owner skipped) and B2 (the usage example createswip/teamwright) this closes M6. Tests intests/test-tw-next.sh(M6). - Work in progress stays bounded with analyze batches (roast m1). With
stages.analyze.batchabove 1 an analyze session moved every task of its batch intoIN_PROGRESS, although one.teamwright/current-taskper tree serves one at a time, andsessions.md§3 said "the next task starts when the previous one reached a barrier". Analysis may still run ahead, but an analysed task waits inANALYSISwhile another task of the tree isIN_PROGRESS: the router does not offer it (state.wip_waiting; an explicit--taskis refused with the reason). Send-backs are not held.sessions.md§3 and §8,lifecycle.md, the analyze skill, the stage protocol. Tests intests/test-tw-next.sh(m1). max_wait_hoursfires for an uncommitted record (roast m2). A waiting task's age came only from its record's last commit, so a record with its move not yet committed never aged andmax_wait_hoursnever fired; it now falls back to the file's modification time. Documented: thresholds hold a stage back, they do not rank due stages by queue length (sessions.md§8,plugin.md). Tests intests/test-tw-next.sh(m2).- Review gate check 7 cannot be bypassed through the shell (roast m3). A task record created by a shell command without
status:passed the analysis gate's shell check, and the review gate only counted records created by Edit/Write — the bypass was two commands. Now the analysis gate refuses any shell write that creates or replaces a task record (>,tee,cp/mv/installonto it), and the review gate counts again, by git, when the reviewed task's status leavesNEEDS_REVIEW: every record added in a commit since the reviewer's spawn, or new in the tree since then.gates.md. Tests intests/test-review-gate.sh(m3),tests/test-analysis-gate.sh. - Only
<ID>.mdis a task record (roast m5). A side file such asdocs/tasks/T-089.history.mdwas parsed as a task (idT-089.history) by the router, the progress report, the parallel and process scripts andtw-req, and its path matched the gates' task-record pattern. Every reader now takesdocs/tasks/<ID>.mdwith<ID>matchingTEAMWRIGHT_TASK_ID_RE(_task.is_task_file,_hookio.TASK_FILE_REand the review/verify/validation patterns). Tests intests/test-tw-next.sh(m5). - The journal takes the end block's
Stage:andBatch:(roast m6). The Stop hook took the firstStage:/Batch:line of the final message, so a message that quoted the router's answer or an earlier step above its own end block was journaled under the wrong stage. It now takes the last. Documented why a flow run is journaled asflow(one session, cumulative tokens, no boundary between its stages: per-stage numbers come from stage sessions).sessions.md§2,journal.sh. Tests intests/test-journal.sh. - Session check: a plugin directory still has a version (roast m7). With
TEAMWRIGHT_PLUGIN_DIRset the check skipped the registry and with it the version comparison, so an autonomous run on an old plugin checkout against newer project files said nothing. The directory's version —plugin.jsonversion, else its git commit (12 hex, as Claude Code versions a channel build) — is compared with@kitlike the registry's; a channel build (a commit, no number) is reported when it differs.docs/usage.md,gates.md. Tests intests/test-session-check.sh. process.router.select: agentchanges what the router prints (roast m8). The setting was only reported: the router listed candidates and a batch either way, so "agent mode" was the script's choice with extra reading. Withagentthe router now leavescandidatesandbatchempty (also inrequest, unless--tasknames the tasks) andcommandcarries no ids; it still decides pause, resume, the stage and whether there is work.sessions.md§8,plugin.md, the stage protocol. Tests intests/test-tw-next.sh(m8), together with the tests listed under each finding above.- An approval cannot be copied to another task (second roast N1). Copying T-1's
reviews/1.mdand1.gate.jsonintoT-2.reviews/let T-2 move toCODE_COMPLETEwith nobody reviewing it — on a fresh checkout, and also with a local ledger (it has no entry for T-2, so the stamp stood in): the stamp did not say which record it belongs to, and the gate never read the record'stask:. Stamps now carrytask,pathandroundand count only for that record; the review gate refuses a review record whosetask:is not its directory's task when it is written and at the status move, and the verify gate checks an acceptance record'stask:atDONE(verify records already were).gates.md(review check 1–2, Gate stamps). Tests intests/test-review-gate.sh,tests/test-verify-gate.sh(N1). - A stamp is no way around a local ledger, and nobody but a gate writes one (second roast N2). A stamp counted whenever the ledger merely lacked that round, so a record and stamp written by
python3passed on the machine that has the ledger; piping a crafted payload intoscripts/hooks/review-gate.shproduced a genuine-looking stamp withspawned: true; andcd docs/tasks && cp …,printf > 1.gate.jsonafter acd,git checkout|restore -- <record>,rsyncanddd of=got past the shell check. Now: the stamps are consulted only when this checkout has no ledger of that kind for the task at all (a fresh clone) — where it exists it alone decides, as in 0.11 — and a stamp'sspawned: truestands in for the journal only then; the state guard refuses running a hook (bash scripts/hooks/…,./scripts/hooks/…,cd scripts/hooks && …, an interpreter importing_hookio); the review gate refuses any shell write into a review, verify or validation record directory or a stamp (_hookio.shell_writes_records: followscd/pushd, redirects,cp/mv/install/rsync/lnincluding a directory copied intodocs/tasks/,rm,dd of=,sed|perl -i,tar|unzipinto them,find -delete|-exec,git checkout|restore|mv|rm,git stash pop|applyandgit apply|amof changes touching them,sh -c, and python/node/perl/ruby one-liners that name them and write — best effort, documented); the analysis gate also refuses putting back an old version of a task record (git checkout <rev> -- <record>,git restore,git stash pop|apply,git apply|am),dd of=and interpreter one-liners that write one; and the gitpre-commithook refuses a staged new or changed stamp that matches no line of the task's local ledger, where that ledger exists.gates.mdsays plainly that stamps are not signed and what limits a forged one.docs/usage.md: the example workflow caches.teamwright/logsonly — a cache restored from an older job would hold a task's ledger without the round a cut job wrote and refuse it. Tests intests/test-review-gate.sh,tests/test-verify-gate.sh,tests/test-pre-commit.sh,tests/test-analysis-gate.sh(N2),tests/test-usage-example.sh. - Install drift stops autonomous runs; a process document missing the kit's sections is a conflict (second roast N3). The router warned "install drift: _task.py predates the plugin" but
headless.runstayed true, so CI ran the new stage commands on 0.11 hooks — no stage gate, no stamps.tw-next.pynow also compares the project'sscripts/hooks/_hookio.pywith the plugin's (public helpers it lacks, read withast) and the@kitversion ininstalled.jsonwith the plugin's; any drift is listed ininstall_driftand the warning, andheadless.runis false with the reason. Andtw-install.py applyon a 0.11 project kept a locally editeddocs/process/sessions.md(no stages section) askept-localand reported "0 conflicts": a locally editeddocs/process/*.mdthat lacks headings of the kit's version is now a conflict naming them (merge, thenapply --accept).plugin.md,docs/usage.md. Tests intests/test-tw-next.sh,tests/test-installer.sh(N3). - A squash merge of
wip/teamwrightis a delivery; the delivery remote need not beorigin(second roast N4). The M3 check used ancestry, so after a squash merge (or a rebase) of thewip/branch into main its tasks were never seen as delivered — no verify for a task that deploys, and the push decision stayed forever — and a remote not namedoriginmeant no delivery branch at all. A task's code now counts as delivered when its last code commit is on the delivery branch, or its changes are: the same patch id (git cherry: rebase, cherry-pick), or every file the commit touches has there its content in the commit or atHEAD(squash); unpushed commits on awip/branch are counted the same way. The delivery remote is the current branch's upstream remote, else the only remote, elseorigin.docs/usage.mdsays to merge or resetwip/teamwrightafter a delivery;plugin.md,sessions.md. Tests intests/test-tw-next.sh(N4: squash, cherry-pick, a remote namedupstream). - A record write that never lands leaves no evidence (second roast, minor). The review and verify gates wrote the ledger line and the stamp in
PreToolUse, before the write; a write another hook then refused, the person cancelled or that failed left an orphan stamp (and a ledger round) that blockedDONE("written but its file is gone"), and agents cannot remove either. The gates now keep the line and stamp pending (.teamwright/sessions/.pending/) and write them inPostToolUsewhen the record on disk is what they checked;.claude/settings.jsonregisters both gates forPostToolUseon Edit|Write (merged bytw-install.py apply). A stamp without its record is ignored and removed.gates.md(Gate stamps), the demo. Tests intests/test-review-gate.sh,tests/test-verify-gate.sh. - A trivial change to code needs no failure-branch table (second roast, minor). A version bump or a typo fix in code (
analysis: skipped:trivial) could not reachNEEDS_REVIEWwithout a### Failure branchestable — there is no failure branch to break. The stage gate now skips that requirement foranalysis: skipped:trivial(never on an rca task, as for the analysis exit);## Testsis still required.gates.md(analysis gate check 4),lifecycle.md,sessions.md, the implement skill. Tests intests/test-analysis-gate.sh. - Records approved before 0.12 count on a fresh checkout (second roast, minor). A review, verify or acceptance record written by a 0.11 kit has no stamp, so on a fresh checkout (CI) a task approved or accepted before the upgrade could not move on. Such a record now counts when it was last changed in a commit before the one that moved
.teamwright/installed.jsonfrom a pre-0.12 kit to a stamping one (0.12.0 or later, or a channel build) and the working copy is that version (_hookio.legacy_record); a record without a stamp committed after the upgrade, edited since, or in a project installed at 0.12 from the start is not.gates.md(Gate stamps, Known limitations). Tests intests/test-review-gate.sh,tests/test-verify-gate.sh. --briefcarriesstate.wip_waiting(second roast, minor). The analyze skill reads which analysed task waits for whichIN_PROGRESSone fromstate.wip_waiting, but--brief— what stage sessions read — left it out of its compact state.plugin.md. Tests intests/test-tw-next.sh(m1).- An approval from before a
VERIFY_FAILdoes not carry over (found in an autonomous run). A task that failed verification went back through analysis and, with no code change, flow moved itNEEDS_REVIEW → CODE_COMPLETEon the review round written before the failure; the review gate let it through. The gate now compares the latest review record with the task's latest failed check (aFAILverify or acceptance record, by the commit that added it, else the file time) and asks for a fresh round. Tests intests/test-review-gate.sh. - Verify and acceptance records stay append-only in
gate-firsttoo (found in an autonomous run). A validator edited its verify record after writing it; ingate-firstthe gate only warned, and the edited record kept no stamp and no ledger line. Editing or overwriting an existing verify or acceptance round is now denied in every mode. Tests intests/test-verify-gate.sh. tw-gate: the temporary index keeps the index's mtime. The tree id of a dirty working tree was computed on a copy of the index with a fresh mtime, which switched off git's "racy git" re-check: a same-size edit in the second the index was written could look unchanged and reuse the previous PASS. The copy now keeps the original mtime (cp -p). Seen once as a flakytests/test-tw-gate.shrun in CI.
Full changelog: CHANGELOG.md