vinta-ai-workflows 0.7.0-alpha2 → 0.7.0-alpha20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/CHANGELOG.md +1441 -3
  2. package/package.json +8 -9
  3. package/schemas/README.md +5 -2
  4. package/schemas/intervention.v1.schema.json +170 -0
  5. package/schemas/postmortem.v1.schema.json +268 -3
  6. package/schemas/workflow.v1.schema.json +245 -0
  7. package/skills/vinta-derive-skills/SKILL.md +2 -0
  8. package/skills/vinta-derive-skills/resources/foundation-skills/open-pr-from-context/scripts/open-pr.sh +0 -0
  9. package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/SKILL.md +82 -49
  10. package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/resources/ai-models.yaml +9 -10
  11. package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/SKILL.md +10 -2
  12. package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/scripts/claude-worktree-write-guard.py +0 -0
  13. package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/scripts/gen-claude-sandbox-settings.sh +0 -0
  14. package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/scripts/gen-compose-worktree-override.sh +0 -0
  15. package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/scripts/sandbox-run.sh +0 -0
  16. package/skills/vinta-derive-skills/resources/plan-execution/README.md +11 -0
  17. package/skills/vinta-derive-skills/resources/plan-execution/partials/dispatched-agent.md +31 -0
  18. package/skills/vinta-derive-skills/resources/plan-execution/partials/implementer-prompt.md +2 -0
  19. package/skills/vinta-derive-skills/resources/plan-execution/partials/parallel-lanes.md +2 -2
  20. package/skills/vinta-derive-skills/resources/plan-execution/shell/amend-plan-template.md +3 -0
  21. package/skills/vinta-derive-skills/resources/plan-execution/shell/implement-phase-template.md +3 -0
  22. package/skills/vinta-derive-skills/resources/plan-execution/shell/implement-plan-template.md +3 -0
  23. package/skills/vinta-derive-skills/resources/plan-execution/shell/integrate-phase-template.md +1 -0
  24. package/skills/vinta-derive-skills/resources/plan-execution/shell/review-phase-template.md +3 -0
  25. package/skills/vinta-install-ai-tools-setup/resources/setup-ai-tools.mjs +0 -0
package/CHANGELOG.md CHANGED
@@ -7,11 +7,360 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [0.7.0] — YYYY-MM-DD
9
9
 
10
+ <!-- pre-release: 0.7.0-alpha20 on 2026-09-24 -->
11
+ <!-- pre-release: 0.7.0-alpha19 on 2026-09-19 -->
12
+ <!-- pre-release: 0.7.0-alpha18 on 2026-09-17 -->
13
+ <!-- pre-release: 0.7.0-alpha17 on 2026-09-17 -->
14
+ <!-- pre-release: 0.7.0-alpha16 on 2026-09-16 -->
15
+ <!-- pre-release: 0.7.0-alpha15 on 2026-09-16 -->
16
+ <!-- pre-release: 0.7.0-alpha14 on 2026-09-16 -->
17
+ <!-- pre-release: 0.7.0-alpha13 on 2026-09-16 -->
18
+ <!-- pre-release: 0.7.0-alpha12 on 2026-09-15 -->
19
+ <!-- pre-release: 0.7.0-alpha11 on 2026-09-15 -->
20
+ <!-- pre-release: 0.7.0-alpha10 on 2026-09-15 -->
21
+ <!-- pre-release: 0.7.0-alpha9 on 2026-09-14 -->
22
+ <!-- pre-release: 0.7.0-alpha8 on 2026-09-14 -->
23
+ <!-- pre-release: 0.7.0-alpha7 on 2026-09-14 -->
24
+ <!-- pre-release: 0.7.0-alpha6 on 2026-09-13 -->
25
+ <!-- pre-release: 0.7.0-alpha5 on 2026-09-13 -->
26
+ <!-- pre-release: 0.7.0-alpha4 on 2026-09-13 -->
27
+ <!-- pre-release: 0.7.0-alpha3 on 2026-09-12 -->
10
28
  <!-- pre-release: 0.7.0-alpha2 on 2026-09-12 -->
11
29
  <!-- pre-release: 0.7.0-alpha1 on 2026-09-11 -->
12
30
 
13
31
  ### Added
14
32
 
33
+ - **A timed-out gate says whether it hung or was slow.** A `gate_result` with
34
+ `status: timed_out` used to carry only the status, so a run with nine
35
+ timeouts could not say whether any of them was a stuck suite or a host too
36
+ busy to finish one. It now carries `timeout`: `quiet_ms` (how long the gate
37
+ had gone without writing anything when it was killed), `output_bytes`, and
38
+ the host's `load_1m` beside `cpus`. `quiet_ms` near the timeout is a gate
39
+ waiting on something; a small one with `load_1m` well above `cpus` is
40
+ contention. Numbers only, as before: nothing the gate printed reaches the
41
+ row. The monitor's brief points at the new field. Older rows have no
42
+ `timeout`.
43
+
44
+ - **Gate durations are now kept, not just displayed.** Every `gate_result` has
45
+ carried the runner's own `duration_ms` since gates were journalled, and
46
+ nothing read it back: the figure went to a live panel and was afterwards
47
+ reachable only by hand-parsing event payloads, so the one measurement that
48
+ answers "is the suite why this run took an hour" could not be asked of a
49
+ finished run. `analyzeRun` now rolls it up per gate — runs, cache hits,
50
+ summed runtime, the slowest single run, verdicts by kind — and each phase's
51
+ time split carries `gateRunMs` beside `gatePoolQueueMs`, so a run says how
52
+ much of its working time was the gate queue and how much was the gate. The
53
+ post-mortem carries the same as a `gate_costs` finding, which is what makes
54
+ it durable: `plan-feature` reads these artifacts when sizing the next plan,
55
+ and a gate pool's capacity and `max_parallel_lanes` are one decision made
56
+ with half the numbers. Cache hits are counted and their time kept in its own
57
+ `cache_saved_ms` column — a hit's duration is what the gate cost when it last
58
+ ran, and adding it to the runtime would report time the run specifically did
59
+ not spend. A verdict written before the runner measured itself is counted and
60
+ its milliseconds are not: both reports emit a `gate_durations_unrecorded` gap
61
+ naming the gates, because a gate reported as free is the number a planner
62
+ would act on hardest.
63
+
64
+ - **Phases can run chores: declared agent turns that change the diff rather than
65
+ judge it.** A gate is a shell command that says pass or fail; a chore is an
66
+ agent asked to do something to the work — rewrite the comments this phase
67
+ wrote, add the changelog entry, extract the strings that need translating. It
68
+ runs on the implementer's own session, so the agent that wrote the diff is the
69
+ one asked to act on it. Declare them in `chores.<id>` (a `prompt` or a
70
+ `prompt_ref` into the plan, plus an optional `skill` the prompt names) and
71
+ pick them with `defaults.chores` for the run or `nodes[].chores` for one
72
+ phase — a node's list replaces the default rather than adding to it, so `[]`
73
+ opts a phase out. In `standard-phase` they run in a new `polish` state,
74
+ between a passing review and the gate: a chore edits the tree, so running it
75
+ later would merge a diff the gates never ran against, and running it earlier
76
+ would have the fixer rewrite what it just did. A chore that fails, or that the
77
+ harness had no capacity for, is journalled and the phase continues to its
78
+ gates — `on_failure: "fail"` is for one the phase is not correct without.
79
+
80
+ - **The post-mortem now reports what the schedule cost.** Two findings, both
81
+ derived from events the journal already carried. `critical_path` is the
82
+ dependency chain that decided the wall clock — each phase's span, the total,
83
+ and its share of elapsed — because every phase *off* that chain could be made
84
+ instant without moving the run, and nothing said which phases those were.
85
+ `idle_capacity` reports the width the graph actually reached against the lanes
86
+ it asked for: a plan declaring three lanes that never runs more than two
87
+ phases at once pays for a third worktree, a third forked database and a third
88
+ desk, and gets none of them back. Run against a real fourteen-hour build, the
89
+ two say its critical path was **99% of elapsed time** — six phases, effectively
90
+ serial — with peak concurrency 2 against 3 lanes and **55% of lane time idle**.
91
+ `plan-feature` already reads these artifacts before drawing the next set of
92
+ dependency lines; until now neither depth nor width was in them. A
93
+ `blocking_cause_unrecorded` gap ships alongside, because the chain is what the
94
+ *graph* forced and a phase can also wait on a busy lane or an unanswered
95
+ question — which the journal cannot currently tell apart.
96
+
97
+ - **`--retry-after <15m>`: an unanswered failure question eventually answers
98
+ itself.** An observed fourteen-hour run spent 4h07m — 29% of its wall clock —
99
+ parked on "This phase failed. Try it again?" with nobody at the keyboard; the
100
+ answer, when it came, was `retry`, and it worked. Unset by default, so a run
101
+ behaves exactly as it did. Accepts minutes bare or a unit (`15`, `15m`, `90s`,
102
+ `2h`) on both `run` and `serve`. It is deliberately unbounded and fires again
103
+ on each new question: a version capped by `--retries` would stall at the cap
104
+ and idle for the rest of the night, which is the failure it exists for. Every
105
+ firing is a full phase attempt, so the interval is the throttle. It reaches
106
+ the failure question and nothing else — a plan's own `await_human` gate asked
107
+ for a person, and both park through the same code, so answering those would be
108
+ the orchestrator overruling the plan. An unattended answer is journalled with
109
+ `unattended: true`, because "a human said try again" and "nobody was here" are
110
+ the same answer with very different meanings.
111
+
112
+ - **The daemon now has a log of its own, and a Logs view to read it in.**
113
+ Transcripts said what the agents did; nothing said what the *daemon* did, so
114
+ "the run stopped and I don't know why" had no evidence behind it at all.
115
+ `.vinta-ai-maestro/logs/daemon.ndjson` now records what the process bound,
116
+ every HTTP request and socket upgrade it refused and why, every start request
117
+ that produced no run, every scheduler dispatch and node transition, and every
118
+ deadlock — on one clock, so a node failure can be read beside the refusal
119
+ thirty seconds before it. `GET /api/logs` serves it as a tail or a follow,
120
+ with filters for level, run, node and a substring of any event or field; the
121
+ UI's new **Logs** section follows the tail until you scroll up. The file
122
+ rotates at 8 MiB and keeps five rotations, so it is bounded whatever the
123
+ daemon's uptime, and `purge` leaves it alone for the same reason it leaves
124
+ `flow.db` alone — it holds no repository contents, and it is the record of the
125
+ failure somebody is about to ask about.
126
+
127
+ - **A crash is no longer silent.** An exception escaping a node's own handling
128
+ used to become an unhandled rejection on a promise the scheduler does not
129
+ hold, and an unhandled rejection ends the process: hours of agent turns and a
130
+ real amount of money, gone with no journal row, no transcript entry and
131
+ nothing on disk saying why — because the thing that would have said it was the
132
+ thing that died. Three changes. A node that throws is now **contained** by the
133
+ rule §6 already states for failures: it fails, its transitive dependents
134
+ block, and everything independent of it keeps going. A crash that is still
135
+ fatal is **recorded first** — `daemon.uncaught_exception` /
136
+ `daemon.unhandled_rejection`, with the error's kind, its stack frames and the
137
+ ids of every run in flight. And the in-flight runs are **journalled as ended**
138
+ before the process goes, so `run --resume <run-id>` picks them up instead of
139
+ finding a row that says `running` for ever. `serve` and `run` both take
140
+ `--log-level`, `--log-stderr` and `--log-detail`.
141
+
142
+ - **The log is identifiers only, enforced rather than asked for — plus the one
143
+ field that is deliberately prose.** A log is called from anywhere, and "please
144
+ do not log repository contents" is a hope rather than a control. So a field
145
+ may only hold a string, a number, a boolean or null — an object is dropped,
146
+ never stringified, which is the way a diff or a file read becomes a log line —
147
+ identifier values are capped at 200 characters, field names that are secrets
148
+ by their name are redacted, and the daemon's token is registered at boot so
149
+ any value containing it is written `<redacted>`.
150
+
151
+ The exception is an error's **`message`**, which every failure record carries
152
+ beside its kind, capped at 2000 characters and redacted like anything else. It
153
+ is the default because `.vinta-ai-maestro/runs/` already holds every agent
154
+ transcript and gate log *verbatim*, in the same gitignored store and under the
155
+ same `purge` — an error message is a rounding error against a directory that
156
+ is already a copy of the repository, and excluding it cost the one string that
157
+ most often explains a failure. `--log-detail kind` narrows to the kind and the
158
+ stack frames for a checkout under a stricter obligation than this store's own.
159
+ - **A run that is dragging can now tune itself, within bounds somebody wrote
160
+ down.** A run can be mis-*configured* rather than broken — every phase doing
161
+ what it was asked, every gate reporting honestly, and the whole thing paying
162
+ for a test database it rebuilds on every gate run in every lane. Nobody needs
163
+ waking for that, and the post-mortem found it out an afternoon too late to
164
+ help. Now a watchdog wakes the run's monitor when a phase passes an hour or a
165
+ gate's uncached cost passes thirty minutes, the monitor reads the gate logs
166
+ and the durations, and it answers with a **proposal** that the daemon
167
+ validates and applies through the existing amend path. `--no-intervene` turns
168
+ it off.
169
+
170
+ It proposes; it never writes. Its authority is four verbs — a gate's command,
171
+ a gate's timeout, a phase's fix budget, a phase's model — and the line they
172
+ draw is that it may change **how** the run executes and never **what** it
173
+ builds. There is no way to express a change to a dependency, a phase brief, a
174
+ touch list, the base branch, or the set of phases: not refused at runtime,
175
+ unrepresentable.
176
+
177
+ The one field that can do real damage is a gate's command, because
178
+ `--reuse-db` and `-k not_slow` are the same edit to the same string, and a
179
+ gate narrowed wrongly goes green and looks like success in every log there
180
+ is. So a command may only be changed when the gate itself declares
181
+ `tuning.allowed_flags` — a list a person wrote in the committed plan, under
182
+ review, before the run started — and the proposed command must be the current
183
+ one's argv tokens, in order, plus additions from that list. A gate that
184
+ declares no `tuning` block is not tunable, which is the default.
185
+
186
+ A run gets three of these in its life, at most one per gate and per phase.
187
+ What it changed is journalled as identifiers; why it changed it is the
188
+ monitor's prose and goes in `runs/<run-id>/interventions.jsonl` beside the
189
+ run, including every attempt that was refused — an agent trying to do
190
+ something it may not do, with nobody in the room, is the record most worth
191
+ keeping. See `packages/vinta-ai-maestro/docs/monitor-intervention.md`.
192
+
193
+ - **`plan-feature` is told what the newer post-mortem findings mean.** The
194
+ skill documents each finding and what to *do* about it, and three had been
195
+ added to the artifact without reaching it: `gate_costs`, `critical_path` and
196
+ `idle_capacity`. A finding a planning agent cannot interpret is a finding
197
+ that changes no plan, which is the whole return on emitting it — and the
198
+ three are among the most actionable there are. `gate_costs` sizes the gate
199
+ pool capacities a plan declares, and separates a slow suite from a fix loop
200
+ re-paying for the same one. `critical_path` is the chain to re-draw the graph
201
+ against: shortening a phase that is not on it changes nothing, so a plan that
202
+ parallelises harder without touching that chain buys nothing at all.
203
+ `idle_capacity` says the graph never got as wide as the lanes it asked for,
204
+ which is a fact about the plan rather than about the machine. Each now has a
205
+ row beside the findings that already had one.
206
+
207
+ - **The post-mortem says what a run changed about itself, and whether it
208
+ helped.** `postmortem.v1` gains an `interventions[]` finding: one entry per
209
+ amendment the run made to itself, with the effect on the thing it changed. A
210
+ gate is scored against its own uncached durations either side of the
211
+ amendment — mean before, mean after, `cheaper` / `dearer` / `unchanged`
212
+ outside a ten-per-cent band. Cached hits are excluded, because a hit reports
213
+ the duration of the run that filled the cache and would drag the mean toward
214
+ whichever side of the boundary that run fell on.
215
+
216
+ It is a comparison and not a claim of cause, and the schema says so: a gate
217
+ that got cheaper did so while phases were finishing and caches warming. It is
218
+ worth carrying anyway, because without it an autonomous editor is one nobody
219
+ can tell is making runs worse — and because `plan-feature` now reads it, so a
220
+ project whose `unit` gate wants `--reuse-db` pays to find that out once
221
+ instead of every run.
222
+
223
+ A phase-level change (a fix budget, a model) is reported `unmeasured` rather
224
+ than estimated: a phase runs once, so there is no before to compare an after
225
+ against, and the only candidate baseline is a different phase doing different
226
+ work. An operator's amendment is not scored at all — a person deciding
227
+ something is not the run choosing it.
228
+
229
+ - **`schemas/intervention.v1.schema.json`** — the document the monitor answers
230
+ with, generated from `src/intervention/intervention.ts` the way the workflow
231
+ and post-mortem schemas are generated from theirs. It is the odd one in
232
+ `schemas/`: it validates something a *model* writes rather than a skill or a
233
+ person, and it is the boundary deciding what an unattended run may change
234
+ about itself.
235
+
236
+ - **A run no longer dies with the terminal that started it.** `vinta-ai-maestro
237
+ serve` accepts `POST /api/runs`, so a run can be submitted to a daemon that
238
+ was already listening and belongs to that daemon rather than to the shell that
239
+ asked for it; the request answers with the run id as soon as the run is
240
+ registered, not when it finishes. Previously `run` was the only way to start a
241
+ run and it hosted its own daemon, so closing the window killed the scheduler
242
+ and every agent under it — and left the journal claiming `running` for ever,
243
+ with nothing able to tell that row apart from a run still in flight.
244
+
245
+ - **`vinta-ai-maestro run --resume <run-id>` picks an interrupted run back up.**
246
+ Phases already `done` stay done and are not dispatched again, their lane
247
+ worktrees are adopted rather than re-provisioned — so whatever an agent had
248
+ written and not committed is still there — and a phase that was mid-turn when
249
+ the process died runs again from the top of its pipeline, because the turn it
250
+ was in belonged to a process that is gone. The plan comes from the run's
251
+ frozen snapshot, never from the document it was written from, so editing that
252
+ file in the meantime cannot switch plans under a run already under way. A new
253
+ `run_resumed` event records each hand-over; it is deliberately not a second
254
+ `run_started`, which would have moved the run's `started_at` to whenever it
255
+ was last picked up and made every duration short by the length of the outage.
256
+
257
+ - **`vinta-ai-maestro gate <gate-id>` runs a declared gate from inside an agent
258
+ turn.** A phase used to run its full gate suite five or six times a round —
259
+ implementer, reviewer, fixer, reviewer again, then the authoritative `gate`
260
+ node — and only the last of those consulted the gate cache or the resource
261
+ pool. Agents now ask the daemon for a gate by id: the result is cached on the
262
+ same `(gate id, lane tree hash)` key the `gate` node reads, the gate's
263
+ resources are held by the daemon rather than by whichever agent remembered to
264
+ wrap the command, and the id resolves to the plan's own command so a scoped
265
+ approximation cannot be reported as the gate. Each run is journalled as a
266
+ `gate_result` event and a `gate_run` transcript entry under the GATE band,
267
+ whoever asked for it.
268
+
269
+ - **A project declares what makes a lane runnable.** `project.env_files` copies
270
+ ignored configuration into each worktree, `project.setup_cmd` runs an
271
+ idempotent setup hook on provisioning and recycle, and the fixed
272
+ `project.commands` vocabulary tells implementers, reviewers and fixers the
273
+ project's real lint, typecheck, test and migration entry points.
274
+
275
+ - **Compose-backed lanes are isolated past `COMPOSE_PROJECT_NAME`.** The daemon
276
+ now generates an out-of-tree Compose override per lane, strips fixed host
277
+ ports, re-pins fixed and external volumes, and carries the complete lane
278
+ environment into agent and takeover processes as well as gates. Explicitly
279
+ published services receive probed per-lane ports recorded in the teardown
280
+ summary.
281
+
282
+ - **Shared services get one namespace per lane.** `project.services` models a
283
+ fixed index or lane-derived name inside one Redis, RabbitMQ, object-storage,
284
+ or similar server, with project-owned create/reset commands and capacity
285
+ validation before provisioning creates templates or worktrees.
286
+
287
+ - **Agents can take the semaphore a workflow declares.**
288
+ `vinta-ai-maestro with <resource> -- <cmd>` waits on the run's existing
289
+ `ResourcePools`, runs the heavy inner-loop command, and releases on exit.
290
+ Renewable leases expire when their client disappears, are visible in the
291
+ live journal, and are surfaced in implementer, reviewer and fixer prompts.
292
+
293
+ - **A failed phase retries itself, and then asks.** `--on-failure` defaults to
294
+ `retry`: one cold re-attempt (`--retries <n>`, 0–5), and then the node parks
295
+ on a question — retry, retry with another member of the crew, or stop. The
296
+ failures this system produces are overwhelmingly environmental and are gone by
297
+ the second attempt; the ones that survive it are the ones worth a person.
298
+ `ask` skips the automatic attempt. `stop` is the old behaviour and remains the
299
+ right choice for CI, because everything else eventually **waits**.
300
+
301
+ A failure that survives its retries says so: the reason carries
302
+ `(after N attempts)`, which no event otherwise records.
303
+
304
+ - **A failed phase can be retried by hand.** A **Retry phase** button, and
305
+ `POST /api/runs/:runId/nodes/:nodeId/retry`. It returns the node to the ready
306
+ set and unblocks the subtree its failure blocked — a phase that died of
307
+ something environmental holds up work that was never broken. It needs the run
308
+ to still be in flight, which with the new default it usually is; a run that
309
+ has genuinely ended is re-run, not retried, and says so rather than doing
310
+ nothing.
311
+
312
+ - **The monitor can go and look.** It was given a digest and nothing else, and
313
+ read exactly as thin as that sounds — it could say a phase had failed and
314
+ never say what the phase had written. It is briefed as a technical project
315
+ manager now, with the map: the plan document, each phase's brief, each lane's
316
+ worktree path, each branch and its base, and where the journal and transcripts
317
+ live. It has a shell and its working directory is the repository, so it can
318
+ read the diff before drawing a conclusion. The digest stays as the index —
319
+ bounded, cheap, enough to know which phase is worth a closer look.
320
+
321
+ - **The monitor conversation is kept.** It lived in component state, so a reload
322
+ threw away every question and every answer — a worse record than the run it
323
+ describes. It is written to the transcript store under a reserved node id and
324
+ read back on load, so it outlives the tab, the daemon and the run.
325
+
326
+ - **Panels expand.** Anything holding content that is only nominally
327
+ summarisable — an agent's transcript, a gate log, the monitor conversation,
328
+ the steering box — takes the full container width and about a screen of
329
+ height on demand. The height travels as a CSS variable, because the element
330
+ that has to grow is a scroller several components below the panel.
331
+
332
+ - **A run has a spokesperson you can ask.** `serve` grows a **Monitor** panel on
333
+ the run view: one agent that reads the journal and answers in prose — what is
334
+ blocked, why a phase failed, what it would take to move on. It runs on the
335
+ dearest tier on the crew, because it is read by a person deciding what to do
336
+ about a failing run, and it is asked a handful of times rather than hundreds.
337
+
338
+ Three things it deliberately is not.
339
+
340
+ **It is not in the permission path.** The obvious thought, once an agent has
341
+ been refused something, is to put a smarter agent in front of the refusals —
342
+ but a phase makes hundreds of tool calls (one node in one real run made 179)
343
+ and a model turn before each is latency and cost spent on questions like "may
344
+ I run ruff", which should never have been questions. Permissions are settled
345
+ structurally, once per spawn, for free.
346
+
347
+ **It does not read the transcripts.** Piping every agent's output into a
348
+ second agent would make the monitor the most expensive thing in the run and
349
+ tie its cost to the work rather than to the questions. It reads a bounded
350
+ digest — statuses, failure reasons, pending questions, the last few refusals
351
+ with their sentences, one trimmed last word per phase — so a one-hour run and
352
+ a one-day run cost about the same to ask about.
353
+
354
+ **It has no authority.** It can explain a §9.1 pause; it cannot answer one.
355
+ An agent that could quietly approve its colleagues' work would turn a
356
+ checkpoint into a formality, and the checkpoint is the point.
357
+
358
+ It answers about **finished runs too**, which is when "why did this fail" is
359
+ usually asked, and it needs no lane: it writes nothing.
360
+
361
+ - **`serve` reads `--permission`.** The flag has been accepted and ignored since
362
+ it existed. It decides how the monitor is spawned, so it is read now.
363
+
15
364
  - **A plan staffs a team, instead of picking a model per phase.** Choosing a tier
16
365
  phase by phase answers "what runs this one" ten times and never adds it up, so the
17
366
  two questions that decide what a feature costs go unasked: how many agents does
@@ -79,10 +428,33 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
79
428
  claimed per review turn rather than per phase — it has one session ledger and
80
429
  two concurrent reviews would collide over it — so one reviewer on a three-lane
81
430
  plan is a queue at the review step and not a serialised run.
431
+ - **"Cheapest qualified" has one exception, and it is about sessions.** A
432
+ member who already holds a session this phase would genuinely resume takes
433
+ it ahead of a cheaper member who would have to start cold — even when they
434
+ are more senior than the phase needs, and even when the member the plan
435
+ named is free. What it buys is the context a cold session spends its first
436
+ turn rebuilding; what it costs is the dearer model for that phase, which is
437
+ the smaller of the two bills. **The tier floor is untouched**: warmth
438
+ reorders the members who already qualify and never widens them, so a warm
439
+ junior still cannot take a Tier 3 phase.
440
+ - **Only when a session is genuinely already up.** "Warm" means the scheduler
441
+ asked its own session-reuse rules and got a yes — same pinned lane, same
442
+ harness, under the turn ceiling, not poisoned by a failed phase. A member
443
+ who has merely run before is not warm, because a promotion bought on a
444
+ resume that is then refused pays the senior's rate *and* cold-starts
445
+ anyway. With nobody warm, staffing is exactly what it was: a session is
446
+ being opened either way, and the plan's own level wins.
447
+ - An operator who picks the member by hand on a retry gets that member. A
448
+ saving nobody asked for does not overrule an explicit answer.
82
449
  - The run view reports who actually worked against who the plan said would.
83
450
  `substituted` is the figure to read beside a cost that overran: every
84
451
  substitution ran at or above the budgeted tier, so a run can be entirely green
85
- and still have been staffed dearer than planned.
452
+ and still have been staffed dearer than planned. The phases promoted to reuse
453
+ a warm session are counted **within** that figure and reported apart from it —
454
+ a peer covering for a busy member costs what the plan budgeted, while a warm
455
+ promotion is the scheduler deliberately trading model rate against cold
456
+ starts, and folded together a run that promoted everything to the top tier
457
+ would read as an ordinary busy wave.
86
458
 
87
459
  - **`implement-plan` runs independent phases in parallel.** The plan now carries a
88
460
  dependency graph and the conductor schedules against it: a phase starts as soon as
@@ -177,8 +549,206 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
177
549
  **generated** from a zod source rather than hand-written — see
178
550
  [`schemas/README.md`](schemas/README.md).
179
551
 
552
+ - **The monitor keeps thinking when you look away, and thinks out loud.**
553
+ Asking used to be one long HTTP request that produced the whole answer inside
554
+ it, which made the turn's lifetime the browser's connection: switching view,
555
+ reloading or letting a laptop sleep killed the monitor mid-thought and left a
556
+ question with no answer and no record that one had been attempted. The daemon
557
+ owns the turn now — `POST` answers `202`, and the monitor journals each
558
+ thought and each sentence as it produces them, so the conversation is
559
+ readable while it is being written and by any tab that opens it. The panel
560
+ renders through the same rows a phase's transcript uses, which is what the
561
+ API always claimed and the UI never did.
562
+
563
+ - **A phase's transcript says which agent wrote each line, and holds the gates.**
564
+ Every spawn on a node appends to one file whatever its role, so a phase that
565
+ took two fix rounds held five agents' output in one undifferentiated stream —
566
+ the role was in scope at the append and simply not written down. Each line now
567
+ carries the role and the session slot that produced it, the view bands the
568
+ list where one agent stops and the next starts, and gate runs are entries in
569
+ it rather than only in a log beside it. The operator's own steering stays the
570
+ operator's (§7), even though the adapter echoes it back on the agent's event
571
+ stream. Transcripts written before this render exactly as they did, with no
572
+ bands. The band stays pinned to the top of the scroller while its own run of
573
+ rows goes past, so the agent you are reading is named without scrolling back
574
+ to where it started — in the phase transcript and in the monitor conversation
575
+ alike.
576
+
180
577
  ### Changed
181
578
 
579
+ - **Updated the OpenAI and Anthropic models to their latest version.**
580
+
581
+ - **Updated the generated skills to have `disable-model-invocation: true` set.**
582
+
583
+ - **An expanded panel reads in a centred column.** Full page used to mean
584
+ full width too, so on a large monitor a transcript or gate log ran in lines
585
+ too long to follow. The overlay still covers the window; its header and
586
+ content now sit in a centred column capped at `max-w-6xl`.
587
+
588
+ - **The node view reads like a chat.** The transcript now sits above the
589
+ steering box instead of below it, so the conversation comes first and the
590
+ box you answer it in is under its newest turn. A terminal opened by *Take
591
+ over* still appears under the steering box.
592
+
593
+ - **`--retry-after` backs off when attempts fail faster than it.** An
594
+ unanswered failure question used to answer itself on a flat interval
595
+ forever, so a phase whose attempts died within seconds was retried every
596
+ fifteen minutes for hours, against the same failure, at the price of a cold
597
+ lane each time. Each attempt that fails in less time than the interval now
598
+ doubles the next wait, up to 8× (15m becomes 2h); one attempt that runs at
599
+ least the interval long puts it back. It still never stops on its own: a
600
+ run whose cause goes away overnight is moving again before morning. The
601
+ `node.unattended_retry` log line reports the actual wait and the streak.
602
+
603
+ - **Notifications have an inbox, and are heard from every view.** The UI's
604
+ notifications used to be a row of banners in the top bar, shown only when
605
+ browser notifications were refused. They piled up there and could be read
606
+ only one at a time, by dismissing whichever was in front. Every notification
607
+ now lands in an inbox behind a bell with an unread count: newest first,
608
+ filterable by unread and by still-waiting pauses, marked read one at a time
609
+ or all at once, dismissed singly or cleared. A pause that gets answered stays
610
+ listed and says *Answered*. The inbox is kept in `localStorage` and shared by
611
+ every tab, so a reload no longer loses it. It stores what a notification
612
+ body already carries — a run id, a node id, a reason and the journal's
613
+ timestamp — and no question text, output or diff. Browser notifications also
614
+ reach the operator more often now. They used to fire only for the run open
615
+ on screen; the UI now follows every running run in the background, and a run
616
+ view takes over that run's stream instead of opening a second one. They also
617
+ depended on a permission prompt raised by the first pause, which Firefox and
618
+ Safari ignore when no click raised it, so on those browsers they could never
619
+ turn on. An *Enable notifications* button now sits in the top bar until the
620
+ browser has been asked (and in the inbox too), and confirms with one test
621
+ notification. It also says why the channel is off
622
+ when it is: blocked in the browser's site settings, or served over plain
623
+ HTTP on a remote `--host`, which browsers do not allow notifications from.
624
+ The reminder interval moved into the inbox.
625
+
626
+ - **The run view's Nodes table names a lane by what distinguishes it.**
627
+ Every lane in a run is named `${runId}-...`, so the column spelled them in
628
+ full as `2026-09-11-graphql-aggregations-mubou9ar-crew-3-tier4` on every
629
+ row, phase after phase — the same forty characters of prefix repeated down
630
+ the table, wide enough to push the whole table into a horizontal scroll for
631
+ the sake of a string that was identical everywhere it appeared. The cell now
632
+ shows the tail that actually differs (`crew-3-tier4`, `lane-2`, `integ`) and
633
+ keeps the full name in its `title`, because that is what the worktree
634
+ directory and the branch are called. The phase description beside the node
635
+ id is bounded and wraps for the same reason — table cells are
636
+ `whitespace-nowrap`, so one long phase name was setting the width of the
637
+ whole table.
638
+
639
+ - **A dispatched phase agent does the work itself, instead of spawning an
640
+ implementer under it.** claude-code sessions run by the orchestrator were
641
+ handing their phase to a sub-agent and reporting its summary back, and the
642
+ pull was not laziness — `implement-phase` ships into the same repositories,
643
+ its description matches "you are implementing P3 of plan X", and its content
644
+ is "spawn exactly one implementer subagent". So a session was loading a
645
+ conductor skill on its own and correctly following it, one level below the
646
+ conductor that had already spawned it. The cost is the whole point of session
647
+ reuse: a sub-agent's reading of the codebase ends with the sub-agent, so the
648
+ warm session that every cross-phase turn greets with "everything you learned
649
+ still holds" was left holding a paragraph, and each phase paid a cold agent's
650
+ first turn again. Both halves are now closed. Every `vinta-ai-maestro` role
651
+ prompt (implementer, reviewer, fixer, chore — cold and continued) carries a
652
+ no-delegation section that names those skills and says what delegating costs,
653
+ and the plan-execution unit grows a `dispatched-agent` partial: a guard at the
654
+ top of all four conductors refusing entry to an agent that was itself handed
655
+ one phase, plus the same rule inside every composed implementer prompt.
656
+
657
+ - **The node view's gate panel is a list of verdicts you can open, not a stack
658
+ of scrolling boxes.** Every gate used to render its whole log at once, each
659
+ in its own fixed-height box with its own scrollbar — so three gates in a
660
+ third of a grid row was a column of letterbox slots, and a wheel gesture
661
+ over one of them was swallowed by that box instead of scrolling the page.
662
+ Gates are now an accordion: one row per gate, only one open at a time, and
663
+ the open log has no scroller of its own (it is tail-truncated at 64 KiB
664
+ already, and long lines wrap the way the diff panel's do). The panel opens
665
+ on the gate a human question points at, or on the first that is not passing.
666
+
667
+ - **Each gate row says what happened and what it cost.** The verdict —
668
+ `running`, `passed`, `failed`, `timed out` — is on the wire now, so a phase
669
+ whose lint gate passed and whose unit gate failed reads as that instead of
670
+ as two identical rows; previously the UI could only name the single gate
671
+ §9.1's question happened to point at, because `NodeDetail` carried an id and
672
+ a log and nothing else. A running gate shows a clock counting from the
673
+ daemon's own start time, so a tab opened halfway through a slow suite shows
674
+ the true elapsed figure rather than starting from zero; a finished one shows
675
+ the runner's measurement of its latest attempt, with `cached` where the
676
+ cache served the verdict and a `×N` count where the gate has run more than
677
+ once.
678
+
679
+ - **Crew members are named after their tier, not after a seniority.** A roster
680
+ reads `tier1`, `tier2-1`, `tier2-2`, `tier4` and `reviewer` where it used to
681
+ read `junior`, `mid-1`, `mid-2` and `senior`. Nothing about the data model
682
+ moved — `tier` was already the 1-4 number the orchestrator staffs on, and it
683
+ is still the only thing compared when one member covers for another. What
684
+ changed is that the label beside it now says the same thing, instead of
685
+ offering a second vocabulary that could drift from it and that reads as a
686
+ ranking of people rather than of difficulty. Workflows using the old ids keep
687
+ running: the ids are free-form and nothing resolves them by name.
688
+
689
+ - **`plan-feature` emits its workflow with the plan's date prefix.** The
690
+ executable sibling is now
691
+ `ai-plans/YYYY-MM-DD-<feature-kebab>.workflow.json` — previously it carried
692
+ no date, which sorted it away from the `YYYY-MM-DD-FEATURE_NAME_PLAN.md` and
693
+ `_SPEC.md` it belongs to, so the one file you want with the plan open was the
694
+ one you had to go looking for. A feature's three files now land adjacent. The
695
+ case difference between them is not an oversight: the markdown convention is
696
+ `UPPERCASE_WITH_UNDERSCORES` and a workflow's filename stem *is* its `id`,
697
+ which the schema requires to be lowercase kebab-case, so the date prefix is
698
+ the only part the three can share. Existing dateless workflows are unaffected
699
+ — this is the emitter's convention, not a schema rule.
700
+ - **A gate's command can be retuned while the phase that runs it is still
701
+ running.** §9's amend refused any change reaching a node in flight, which is
702
+ exactly the node anyone is looking at when a run is dragging: a `unit` gate
703
+ missing `--reuse-db` could not be fixed until the run it was costing hours
704
+ had finished. The refusal now reads a narrower set. A node's harness, model,
705
+ pipeline, body and base are each read at a moment that has already passed, so
706
+ changing them is refused as before; a gate's *command* is resolved per gate
707
+ run by every reader of it, so a running phase simply runs the new command at
708
+ its next gate. Nothing downstream is blocked either, because a command moves
709
+ no branch's base. Changing **which** gates a node declares is still refused
710
+ mid-flight — a phase that gained a gate would finish without ever running it —
711
+ so that now counts as part of the phase body.
712
+
713
+ - **A gate result is cached against the command that produced it.** The key was
714
+ `(gate id, tree hash)`, on the assumption that a gate id names a command.
715
+ Amending a live run makes that false, and an unchanged tree would have been
716
+ served the old command's verdict. The command is now part of the key, hashed
717
+ rather than stored — a command line is repository content. A cache database
718
+ written before this is dropped when it is opened: its rows cannot say which
719
+ command produced them, and one re-run per stale entry is the price a lost row
720
+ already costs.
721
+
722
+ - **A session cannot inherit a machine's decision to stop compacting.** All
723
+ three harnesses compact automatically when their context fills — there was
724
+ never a flag to turn on — but each honours an environment variable that turns
725
+ it off, and an agent spawned on a machine carrying one would run a long phase
726
+ straight into a wall it was supposed to be able to survive. Those variables
727
+ are now stripped from every child the daemon spawns, and claude-code's own
728
+ setting is reasserted in the per-lane file beside its permission rules. The
729
+ window itself is left alone: an operator who narrowed theirs compacts earlier,
730
+ which is not the failure this guards against. A harness that compacts declares
731
+ it, and a compaction the harness reports is a transcript row like any other.
732
+
733
+ One consequence worth knowing: `max_session_turns` used to be a proxy for "this
734
+ session is nearing a context window it will die on". That death no longer
735
+ happens, so the ceiling now guards a different thing — a session whose memory
736
+ was replaced by a summary of itself, and which answers just as confidently.
737
+
738
+ - **Agent prompts name gates by id rather than printing their commands.** The
739
+ implementer's step 4 was descriptive — "Outer gate… These run against your
740
+ lane:" followed by the gate commands — and implementers were observed reading
741
+ it as a note about what would run later, finishing the scoped inner loop and
742
+ never running a gate at all. It is imperative now, and every role that runs a
743
+ gate (implementer, reviewer, fixer, and the continuation of each) is pointed
744
+ at `vinta-ai-maestro gate <id>`. The fixer prompts no longer say to re-run the
745
+ gates "until they are green", which contradicted the commit protocol directly
746
+ below them and left fixers running out the turn with the work uncommitted;
747
+ they now say to commit and report FAILURE when a gate cannot be turned green.
748
+ Implementers and fixers report which gates they ran and what each returned —
749
+ a record for the reviewer to check against, explicitly not a licence for the
750
+ reviewer to skip running them itself.
751
+
182
752
  - **Phase branches base on their dependencies, not on the previous phase.** A phase
183
753
  with no dependencies cuts from the default branch; one with a single dependency
184
754
  cuts from that phase's branch; one with several cuts from a
@@ -215,8 +785,878 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
215
785
  `plan/{plan-id}/lane-{phase-id}` branches, merged `--no-ff` into the single plan
216
786
  branch at each wave boundary, so the unit commits survive.
217
787
 
788
+ - **`project.prepare_cmd`, for the shared servers a run depends on.** A project
789
+ whose databases are `delivery: external` and whose services point at one Redis
790
+ has no long-lived containers of its own — the shape these fields recommend —
791
+ and that makes some *other* stack a hard dependency of every run. Nothing in
792
+ the package could bring it up. `setup_cmd` looks like the place and is not: it
793
+ runs per lane, long after the template database was created on a server that
794
+ had to be up for `createdb` to work at all.
795
+
796
+ It runs in the repository root **before anything else a run does** — before
797
+ the preflight, before the templates, before the first worktree — and again
798
+ before every lane recycle, so a server that dies mid-run is restored at the
799
+ next hand-off rather than failing every phase after it. Failure aborts with
800
+ the exit code and a bounded stderr tail, and says what the failure means
801
+ rather than only that a command exited. It must be idempotent, and `--wait`
802
+ is only as good as the `healthcheck` a service declares: without one, compose
803
+ returns as soon as the container starts and `createdb` races the server.
804
+
805
+ - **A merge conflict goes to the most senior member who wrote one of its
806
+ sides.** The conflict fixer was staffed from `defaults` and ignored the roster
807
+ entirely, so on a tiered crew a collision between two Tier 4 phases was handed
808
+ to a default-tier agent that had read neither of them — the hardest merge in
809
+ the run going to the least contextualised agent available, and resolved by
810
+ guessing which side looked more finished.
811
+
812
+ `Integrator` already knows which nodes own the contested paths, and `node_crew`
813
+ already records who took each one; this is the join. Highest tier among them
814
+ wins, on that member's own `model` and `harness`, and a tier tie breaks on
815
+ member id the way roster substitution breaks its own. An unstaffed workflow
816
+ still resolves through `defaults`, unchanged, and so does a conflict whose
817
+ nodes cannot be resolved to a member.
818
+
819
+ What is carried is the member's **capability, not their session**. A phase's
820
+ session ran in that phase's lane; the fixer runs in the integration worktree,
821
+ where the files in dispute are half-merged and unlike anything that session
822
+ saw — resuming there would give the agent confident, false memories of exactly
823
+ the files it is merging. The prompt tells the fixer which side is its own, and
824
+ in the same breath that it is a fresh session and must read rather than
825
+ recall.
826
+
218
827
  ### Fixed
219
828
 
829
+ - **A failed Claude Code turn no longer reads as `claude-code result:
830
+ success`.** When the CLI ends a turn with `is_error: true` beside
831
+ `subtype: "success"` (how a plan limit arrives, for one), the recorded
832
+ error printed the subtype and said "success" — 189 times in one observed
833
+ run. It now reads `claude-code result: is_error`. Other subtypes
834
+ (`error_max_turns`, …) are unchanged.
835
+
836
+ - **The monitor's conversation was mostly unformatted JSON.** Not its answers —
837
+ its *watchdog's*. An intervention turn must reply with one document matching
838
+ `intervention.v1.schema.json`, which is what makes the monitor's authority
839
+ over a live run a closed set of verbs rather than an editor over
840
+ `workflow.json`; and that turn is journalled into the conversation a person
841
+ reads, because a run that retuned itself should say so where somebody is
842
+ already looking. Both of those are right. Rendering the document verbatim was
843
+ not, and the summary paragraph the operator wanted arrived a thousand
844
+ characters into one unwrapped line of braces. A proposal now renders as what
845
+ it says: the summary, then one line per proposed change with its evidence
846
+ under it, and a tone dot only when something was actually proposed — a turn
847
+ that looked and changed nothing is the expected outcome and should not shout.
848
+ The verb table is keyed on the daemon's own union, so a fifth verb fails the
849
+ UI build rather than shipping as a bare identifier. The journal still holds
850
+ the bytes the model wrote — only the row changed, so conversations already on
851
+ disk read as well as new ones. Any other whole-JSON answer is at least
852
+ indented.
853
+
854
+ - **The monitor's conversation opened at its oldest entry and never scrolled.**
855
+ It followed the newest row *only while a turn was running*, on the reasoning
856
+ that the list is short and the operator is watching the answer they just
857
+ asked for. Neither half held: the conversation is the journal's, so it holds
858
+ every question ever asked about the run plus every proposal the run's own
859
+ watchdog made with nobody asking anything — a hundred entries is ordinary.
860
+ Opening the panel therefore put the reader at the oldest of them. It now
861
+ follows the way a phase's transcript does: stuck to the newest row, letting
862
+ go the moment the reader scrolls up to read, with a "Jump to latest" as the
863
+ way back. That rule is one module now rather than a well-tested copy in the
864
+ transcript and none in the monitor, which is how the panel came to be missing
865
+ three of its parts — including the one that survives a panel being expanded
866
+ to full page, where the portal hands the view a brand new list scrolled to
867
+ the top.
868
+
869
+ - **Hitting the plan's session or weekly limit failed the node instead of
870
+ waiting for the window to end.** `claude-code`'s refusal table recognised
871
+ "usage limit reached" and nothing else, and the classifier's default is
872
+ `fatal` — correctly, since an unrecognised failure must not become an
873
+ unbounded wait. But the CLI announces a plan window in its own words:
874
+ "You've hit your session limit", "Session limit reached · resets 3am",
875
+ "5-hour limit reached", "You've reached your weekly limit". None of those
876
+ contain "usage limit", so every one of them fell past the table to `fatal`,
877
+ which fails the node and blocks its whole dependent subtree — for a window
878
+ that ends by itself in a few hours. The machinery to do the right thing was
879
+ already there and simply never reached: a `quota` refusal parks the harness,
880
+ not the node, so every later node joins one wait instead of discovering the
881
+ limit for itself; the wake time is journalled and survives a daemon restart;
882
+ and the AIMD ceiling is left alone, because a plan window says nothing about
883
+ how many agents may run at once. Those wordings are now `quota`. The new row
884
+ sits *below* `concurrency` and `rate_limit` so that "concurrent session limit
885
+ reached" keeps the kind that halves the ceiling, and "approaching session
886
+ limit" — a warning from a harness that is still working — matches nothing.
887
+
888
+ - **A window that closed *under* a running turn was an ordinary turn failure.**
889
+ §6.1 is written about a spawn, because that is where a vendor usually says
890
+ "not right now" — and everything that follows from it, parking the harness so
891
+ every later node joins one wait, journalling the wake time so a daemon
892
+ restart does not forget it, leaving the AIMD ceiling alone for a window that
893
+ says nothing about concurrency, hung off `admit`. So the other arrival of the
894
+ same fact had none of it: a plan window that closes while an agent is twenty
895
+ minutes into a phase ends the turn, and the node then burnt its retries
896
+ against a closed window and failed, blocking its whole dependent subtree —
897
+ the exact outcome §6.1 exists to prevent, reached by the one path that did not
898
+ go through admission control. A session now reports a `TurnRefusal`, and the
899
+ caller answers it with the `CapacityRetry` a refused spawn raises: the lane
900
+ goes back, the harness parks until the stated reset, and the node re-drives
901
+ when the window opens.
902
+
903
+ Each adapter reads it where its own failure prose is, which is not where its
904
+ events are: `claude-code`'s terminal `result` frame carries the sentence in
905
+ the `result` string that `mapResult` deliberately drops (it is agent output,
906
+ §11), `codex` states a failure as prose on three different frames, and
907
+ `opencode`'s `session.error` carries it under `data`, which `#errorName`
908
+ refuses to put in an event. All three read that prose, match it against the
909
+ table they already had, and drop it — what reaches the caller is a kind, a
910
+ fixed reason token and a reset time, and what reaches the transcript is one
911
+ token-only line so the record says why the turn stopped. The stderr-and-exit
912
+ shape of the same event is read too, from the diagnostics buffer that is the
913
+ only place that sentence exists.
914
+
915
+ The re-drive is a full one, and that is deliberate: the session the turn was
916
+ using is gone, and resuming from a memory of files a recycle has removed is
917
+ worse than redoing the work. Nothing is destroyed by it — `recycle` sets
918
+ aside whatever the turn left uncommitted as a WIP ref first. What it costs is
919
+ the turn. What it used to cost was the node.
920
+
921
+ - **A stated reset time was only read when it was an hour on the same day.**
922
+ `parseRetryAfter` prefers a reset the vendor reported over a guessed backoff,
923
+ and its wall-clock pattern wanted `am`/`pm` immediately after "reset", so
924
+ "resets at 15:00", "resets tomorrow at 2am" and the weekly window's "resets
925
+ on Monday at 12am" all reported nothing — falling back to the five-minute
926
+ re-probe interval, which across a week-long window is two thousand spawns to
927
+ discover what the first sentence said. It now reads 24-hour times, `today`,
928
+ `tomorrow` and a named weekday (the next occurrence of it, which is today
929
+ only while the hour is still ahead). A reset named by date, "resets Nov 3 at
930
+ 9am", still reports nothing on purpose: a month with no year is a guess, and
931
+ the re-probe is a better answer than a wait that is wrong by a year. So are
932
+ "reset 3", "resets at 25:00" and "resets at 13pm" — an hour with neither
933
+ minutes nor a meridiem is as likely to be a version or a retry count, and
934
+ everything this function returns becomes a wait.
935
+
936
+ - **`run --resume` could not provision a pool at all on a project that
937
+ disables hooks.** `#configureHooks` enables `extensions.worktreeConfig`
938
+ before it can set `core.hooksPath --worktree`, and that first key is
939
+ repository-wide: the write lands in the shared `.git/config`, which git takes
940
+ `.git/config.lock` to edit. Called once per lane out of the provisioning
941
+ `Promise.all`, the lanes collided on that lock — `could not lock config file
942
+ …: File exists`. A fresh provision never showed it, because `worktree add`
943
+ goes through the pool's serialized git turn and staggered the lanes apart by
944
+ the time they reached the shared write; `adopt` skips `worktree add`, so every
945
+ lane arrived in the same tick and **every** resume of a `hooks: false` project
946
+ failed, reproducibly, before a single phase was scheduled. The extension now
947
+ goes through the same serialized turn. The `--worktree` write stays parallel:
948
+ it lands in `.git/worktrees/<name>/config.worktree`, one file per lane.
949
+
950
+ - **A provision failure that was not one of three enumerated kinds said nothing
951
+ about itself.** `refusal` detailed `DiskProbeError`, `LaneSetupError` and
952
+ `LaneEnvFileError`; everything else — including the lock collision above and
953
+ `LaneAdoptError`, the one error a resume is most likely to hit — collapsed to
954
+ "could not provision the lane pool under …", and the daemon log recorded that
955
+ same sentence as `run.provision_failed`'s `reason` with no `error` field and
956
+ no stack. So the run that could not start left behind no record of why, in
957
+ either place an operator looks. The exclusion was a §11 reading
958
+ `LaneSetupError` had already revised for this exact reason: §11 keeps
959
+ repository *contents* out of the record — diffs, file bodies, gate output —
960
+ and an error's own complaint about a lock file or a ref is not that. An
961
+ unenumerated error's message is now carried, capped at 500 characters and run
962
+ through the log's redaction set; `LaneAdoptError` speaks for itself like the
963
+ other lane errors; and the failure is logged with its kind and stack frames.
964
+
965
+ - **`doctor` reported the resumed run's own lanes as leftovers blocking it, and
966
+ offered to destroy them.** The held-branch check fails a run whose phase
967
+ branches are checked out in another worktree, which is right for a fresh run
968
+ and exactly wrong for a resume: the integrator puts a lane on
969
+ `plan/<id>/phase-<n>` for the duration of a phase, so a run killed mid-phase
970
+ — the kill a resume is *for* — leaves its own lanes holding precisely those
971
+ branches. Every `run --resume` therefore refused preflight with one failure
972
+ per phase in flight, each remedied by `git worktree remove --force <lane>`,
973
+ which deletes the uncommitted work the resume existed to adopt. The check now
974
+ takes the run being resumed and excludes lanes belonging to it, identified by
975
+ reading the lane root and asking each worktree which branch it holds — never
976
+ by comparing git's printed paths with this process's, the platform trap
977
+ `reap` and `LanePool` both document. `doctor` takes `--resume <run-id>` to ask
978
+ the same question before a resume, and a lane of *another* run still fails.
979
+
980
+ - **`vinta-ai-maestro gate` gave up on the slowest gate after five minutes and
981
+ called it a daemon it could not reach.** The command awaited one `fetch`, on
982
+ the stated reasoning that a gate is slow for reasons the client cannot shorten
983
+ and so the client should simply wait — while its own comment claimed it
984
+ imposed no deadline of its own. It imposed 300 seconds, because that is where
985
+ Node's `fetch` abandons a response whose headers have not arrived, and a gate
986
+ that queues for a capacity-1 semaphore and then runs a test suite is routinely
987
+ slower. In a measured fourteen-hour run this fired **33 times, every one of
988
+ them on the `unit` gate and none on any other** — a perfect correlation with
989
+ duration, not a flaky daemon. The gates were running fine and cached green
990
+ results minutes later; only the answer was lost. Told a transport lie about
991
+ work in progress, agents wrote polling loops around a command documented as
992
+ needing none: in one phase, **24 of 108 shell turns were pure waiting, six of
993
+ them burning a full ten-minute ceiling — 60 minutes, 31% of that phase.** The
994
+ wait is a `202` hop loop now, which is what `with` already does, having found
995
+ the same bug the same way and fixed it first.
996
+
997
+ - **A gate command cut short by the harness started the suite over.** An agent
998
+ harness caps how long one command may run, and a gate may exceed any such cap
999
+ — a plan declaring `timeout_s: 2400` is asking for forty minutes against a
1000
+ ten-minute budget. So being killed partway is the *ordinary* ending, and what
1001
+ an agent does next is run the command again. That used to start a second full
1002
+ suite beside the first, queued behind the very semaphore its own predecessor
1003
+ was still holding. The daemon now keys a running gate on `(phase, gate id)`
1004
+ and the re-invocation attaches to it, which makes re-running the command the
1005
+ correct move rather than a wasteful one — worth having, because it is the move
1006
+ an agent makes anyway. There is no token to carry, deliberately: a client that
1007
+ was killed has nothing in hand, and needing something would defeat the point.
1008
+
1009
+ - **A pipeline that could not progress was journalled as the word "Error".** The
1010
+ interpreter composes an id-safe reason for exactly this case — which state it
1011
+ could not leave, and which trigger failed to match — and the scheduler threw
1012
+ it as a bare `Error`. `failureReason` reports an unrecognised error by its
1013
+ name, so the sentence was discarded at the one moment it was worth keeping.
1014
+ It is a `PipelineStuckError` now, carrying the state, and the reason survives
1015
+ into the journal. The same shape as the non-zero git exit, fixed the same way.
1016
+
1017
+ - **A pull request said nothing but the plan anchor.** The daemon opened PRs
1018
+ with `title: node.name` and `body: node.prompt_ref` — one line, a link to a
1019
+ heading. It had no idea the `prs-context` mechanism existed: there was not a
1020
+ single reference to it in the package, so the rich path the skills use
1021
+ (`open-pr-from-context`, the project's own PR template, inline review
1022
+ comments) was never reached from a run. `open_pr` now reads the phase's
1023
+ `.vinta-ai-workflows/prs-context/<plan>/phase-<node>.md` when the agent wrote
1024
+ one, and the implementer is asked to write it — the 5–15 line summary it
1025
+ already produces, put where a human will read it instead of only the
1026
+ transcript. With no context file the body is composed from the run's own
1027
+ record: the brief, what the phase is based on and why that is not the default
1028
+ branch, its declared surface, its gates, how many attempts it took, and any
1029
+ merge conflicts an agent resolved on the way in — which nobody reviews.
1030
+
1031
+ - **A phase with two dependencies could never open a pull request.** Its base is
1032
+ an `integ-<id>` branch and `git_push` pushes only the node's own branch, so
1033
+ `gh` was asked to open against a ref the forge had never seen. It refused, the
1034
+ refusal was discarded, and the phase completed with no PR and nothing saying
1035
+ so — in one observed run, the single node with two dependencies was the single
1036
+ node with no PR. The integration base is pushed before the PR is opened, and
1037
+ every outcome is journalled as `node_pr`, so a PR that did not open is now as
1038
+ visible as one that did.
1039
+
1040
+ - **A conflict resolution was never gated.** `IntegratorOptions.verify` has
1041
+ existed as long as the conflict loop, documented as "a resolution that does
1042
+ not build is not a resolution", and nothing ever supplied it. Six merges in
1043
+ one run were resolved by an agent and went straight in — ungated — to become
1044
+ the base the next phase built on, while the phases either side were gated to
1045
+ the hilt. The union of the conflicting nodes' declared gates now runs in the
1046
+ integration worktree, with that worktree's environment, and a red gate spends
1047
+ a fix round instead of shipping.
1048
+
1049
+ - **A conflict fixer that committed its own resolution failed the phase.** An
1050
+ agent told to resolve a merge conflict reaches for the sequence a person
1051
+ would — abort, merge again, resolve, commit — and the orchestrator then
1052
+ committed unconditionally on top of it. Against an already-committed
1053
+ resolution `git add --all` is a no-op that exits 0 and `git commit --no-edit`
1054
+ exits 1 with "nothing to commit", which is a throw. A successful resolution
1055
+ was reported as a failed phase, deterministically, on **every wave after a
1056
+ parallel one**: a multi-dependency node is the only thing that merges here,
1057
+ and two sibling phases both creating one file is the common case rather than
1058
+ the rare one. The phase died before its branch was cut, so it left no
1059
+ transcript, no gate log and no branch — only a resolved merge commit sitting
1060
+ in the integration worktree. The commit is now conditional on there being
1061
+ something to commit, and a fixer that discarded the merge instead of resolving
1062
+ it is refused rather than recorded as integrated.
1063
+
1064
+ - **The conflict fixer's turn is in the transcript now.** Its event stream was
1065
+ drained and every event dropped — an unread stream never ends — so the one
1066
+ agent turn in a run nobody could watch live was also the one nobody could read
1067
+ afterwards: a resolved merge, a row saying it took two rounds, and no record of
1068
+ what was decided. It lands in the incoming phase's own transcript, beside the
1069
+ implementer and reviewer turns that produced the branches being merged, under
1070
+ a `conflict-fixer` role that keeps it distinguishable from the review fixer —
1071
+ a different job, in a different worktree, on a different thing. The UI has had
1072
+ a band for that role since transcripts learned to name their authors; it had
1073
+ simply never been given one to draw.
1074
+
1075
+ - **`PlanDefectError` is now `UnresolvedConflictError`.** The old name made a
1076
+ judgement the orchestrator is not entitled to make. Two same-wave phases
1077
+ touching one file is not a broken plan — the plan's file-overlap analysis is a
1078
+ guess made before a line was written, and the fixer exists because of it. What
1079
+ reaching that error means is narrower: *this* conflict outlasted its rounds and
1080
+ needs a person. The message says so, and now leads with finishing the merge by
1081
+ hand in the integration worktree — which, since a prepared base is reused,
1082
+ survives into the retry — rather than with re-cutting the plan.
1083
+
1084
+ - **A conflict was resolved again on every retry, and recorded nowhere.**
1085
+ Conflicts are an ordinary outcome — the plan's file-overlap analysis is a
1086
+ guess and two sibling phases legitimately edit one file — but `prepareBase`
1087
+ reset and re-merged its `integ-<id>` branch unconditionally, so each retry
1088
+ spawned a fresh fixer agent to redo work already done, and discarded any
1089
+ resolution a person had finished by hand in that worktree. One observed run
1090
+ paid for the same resolution three times. A base that already integrates every
1091
+ dependency's current tip is now kept; one built before a dependency moved is
1092
+ still rebuilt, because reuse is about reachability of the tips rather than
1093
+ about the branch existing. Every settled conflict is also journalled as a
1094
+ `node_conflict` event naming both participants, the paths and the rounds it
1095
+ took — previously a wave conflict reached only the post-mortem and a *base*
1096
+ conflict was discarded entirely, so a phase could sit in `running` for minutes
1097
+ while an agent merged in a worktree nobody could see.
1098
+
1099
+ - **A failed attempt now says why, wherever it goes next.** The reason was
1100
+ computed at the failure site and handed to the path that gives up — which the
1101
+ default `onFailure: retry` does not take: it retries automatically, then parks
1102
+ on "This phase failed. Try it again?". Both paths dropped the reason, and
1103
+ `#offerRetry` carried a comment asserting the opposite. A phase that failed
1104
+ three times before any agent ran was undiagnosable after the fact, and the
1105
+ only offered action was to repeat it. Every attempt now writes a `node_error`
1106
+ event carrying its reason and attempt number, and a `node.attempt_failed`
1107
+ record to the daemon log.
1108
+
1109
+ - **A git command that exited non-zero was persisted as the word "Error".**
1110
+ `failureReason` reports an unrecognised error by its name plus a *string*
1111
+ `code`, and an `execFile` rejection carries `name: 'Error'` with a *numeric*
1112
+ one — so the most common real failure in this package said nothing at all.
1113
+ Git failures now carry their subcommand and exit status (`git commit exited
1114
+ 1`), which is a command name and an exit code rather than anything §11 keeps
1115
+ out of the journal.
1116
+
1117
+ - **The conflict fixer ran without its worktree's environment.** It was the one
1118
+ agent spawn not given `AgentTask.env`, whose own docstring records this exact
1119
+ bug class. With the daemon's bare environment its `docker compose` resolved to
1120
+ a project named after the directory rather than the isolated one, ignored the
1121
+ `compose.publish: []` override that rides in `COMPOSE_FILE`, and published the
1122
+ project's fixed ports on the host — colliding with the developer's own stack
1123
+ and outliving the run.
1124
+
1125
+ - **"Retry with <member>" offered the member that had just failed.** The menu
1126
+ excluded the member the *plan* named, but a substitution means the member that
1127
+ actually ran is someone else — so a phase declared for one tier and covered by
1128
+ a higher one offered that higher one as its escalation, and a third of the
1129
+ menu did nothing distinguishable from plain retry.
1130
+ - **An amended workflow now reaches the integrator too, and a wave merge is
1131
+ pinned to the membership it was decided with.** Fixing the executor without
1132
+ fixing the integrator is what made this reachable: `#lastOfWave` decides a
1133
+ wave is complete by counting the executor's node set and `mergeWave` decided
1134
+ what to merge by reading the integrator's, so while both were stale they
1135
+ agreed with each other and nothing showed. One fresh and one stale is a wave
1136
+ whose merge silently omits the branch of a phase that ran.
1137
+
1138
+ The case that gets there is a phase nobody depends on. An amendment is
1139
+ refused while a node it *blocks* is in flight, and such a phase blocks
1140
+ nothing, so it lands freely beside the wave-mates it will be merged with.
1141
+
1142
+ A wave's membership is now computed once, synchronously, by the code that
1143
+ decides the wave is complete, and handed to the merge — a wave merge runs
1144
+ behind the integration worktree's queue and an amendment can land in between,
1145
+ so a merge that re-derived its own membership would merge a phase that never
1146
+ ran. §9's rebase goes through that same queue now, since "the nodes this
1147
+ amendment blocks are idle" says nothing about whether another wave's merge is
1148
+ writing in that directory.
1149
+
1150
+ - **An amended workflow now reaches the code that runs the gate.** `adopt`
1151
+ replaced the scheduler's copy of the run's definition and nothing else, while
1152
+ the effect executor and the agent-gate broker each held the snapshot they were
1153
+ constructed with — and the command a gate actually runs is read from the
1154
+ executor's. So an operator who edited a gate command mid-run moved the
1155
+ snapshot, the journal and the pool reservations, watched the amendment be
1156
+ accepted, and went on getting the old command. All three take the amendment
1157
+ now, fanned out from one place, host side before the scheduler so that no
1158
+ phase is dispatched against a definition half the system has not seen yet.
1159
+
1160
+ - **Closing a terminal on a run now records the interruption.** `run` and
1161
+ `serve` handle SIGHUP as well as SIGINT and SIGTERM — SIGHUP is what a
1162
+ terminal sends when its window closes, and with no handler Node's default was
1163
+ to die on the spot, running no teardown and writing no `run_ended`. Both
1164
+ commands now mark their live runs interrupted before exiting and print the
1165
+ `--resume` line for each, so a closed window leaves something recoverable
1166
+ instead of a row nothing will ever move again.
1167
+
1168
+ - **A member's reviews were counted as phases they took.** `node_crew` is
1169
+ written for both seats — the reviewer's claim carries `role: 'reviewer'`, the
1170
+ implementer's omits it — and the staffing rollup read neither, so every phase
1171
+ a member reviewed was folded into the count of phases they implemented. On any
1172
+ run with reviewers that inflated the number the rollup exists to put beside
1173
+ the plan's estimate, and inflated `asPlanned`, which the run view divides by.
1174
+ The two seats are counted apart now rather than one of them dropped: a review
1175
+ is real work by a real member, and a member who only reviewed is neither a
1176
+ phase-taker nor idle.
1177
+
1178
+ - **An agent taking a lease left the resource panel showing the one before it.**
1179
+ The scheduler's own gate-pool transitions are journalled as `gate_pool`
1180
+ events, so the browser re-reads the snapshot when one lands. An agent's lease,
1181
+ taken through `vinta-ai-maestro with`, was only a row in the `leases` table —
1182
+ correct in the daemon's projection and announced to nobody. A projection
1183
+ nobody is told changed is a projection nobody sees change, so the panel showed
1184
+ whatever holders it had last time some unrelated event happened to arrive, and
1185
+ the waits it was stalest about were the agent ones, which are the long ones.
1186
+ The broker now journals `agent_lease` on both edges. A renewal writes nothing:
1187
+ it is a heartbeat on a lease already reported, and journalling it would bury
1188
+ the two rows that are transitions under a liveness log.
1189
+
1190
+ - **An agent waiting for a semaphore could be overtaken without bound.** A
1191
+ `vinta-ai-maestro with` wait is served in fifteen-second hops so no client
1192
+ timeout has an opinion about it, and each hop used to abort the underlying
1193
+ `pools.acquire`. Aborting leaves the pool queue, and the next POST re-entered
1194
+ it — at the tail, behind every waiter that had arrived in the meantime. Since
1195
+ a gate run holds a capacity-1 semaphore for minutes, an agent lost that race
1196
+ on hop after hop while the scheduler's own in-process waiters, which never
1197
+ leave the queue, did not. The pool's aging rule could not help: aging reserves
1198
+ against waiters *behind* the aged one, and the waiters that overtook it are in
1199
+ front of it. A `202` now carries a `waitToken` naming the wait, the client
1200
+ sends it back, and the daemon keeps the one queue entry — one arrival time,
1201
+ one place in line — across the whole wait. A parked wait whose client stops
1202
+ hopping is reaped after a minute, releasing the slot if one was granted to it
1203
+ while nobody was watching.
1204
+
1205
+ - **The monitor's conversation could never be written on Windows.** It is kept
1206
+ under a reserved node id that becomes a directory, and that id was
1207
+ `monitor:conversation` — a colon, chosen because no phase id may contain one,
1208
+ and the drive separator on Windows. `mkdir` failed with `ENOENT`, so every
1209
+ question threw on its first append and the endpoint reported the monitor
1210
+ unavailable. A colon is legal in a macOS or Linux filename, which is why no
1211
+ machine the feature was built on ever saw it. The id is now
1212
+ `_monitor-conversation`: still unrepresentable as a phase id, and legal
1213
+ everywhere. Conversations recorded under the old id are not migrated.
1214
+
1215
+ - **An implementer was told both to commit and never to commit.** The prompt
1216
+ carried "never commit while a gate is red" beside "the turn is not complete
1217
+ until `git status --porcelain` is empty of your work" — a contradiction
1218
+ whenever the gate could not be turned green inside the turn, which is most of
1219
+ why fix rounds exist. Agents obeyed the `never`, and the reviewer one node
1220
+ later raised the BLOCKER it is told to raise for uncommitted work, spending a
1221
+ fix round re-implementing code that was already on disk. A red gate now
1222
+ changes the report and never the decision to commit: commit, and report
1223
+ FAILURE saying what is still red.
1224
+
1225
+ The reviewer's side was a false BLOCKER independently of that. Any unclean
1226
+ tree was a finding, and a lane's tree is never clean — the pool copies
1227
+ configuration in, links dependency trees, and the gates leave build output
1228
+ behind, none of which the implementer is allowed to stage. It is scoped to
1229
+ the failure it was written for now: work that is in the tree and missing from
1230
+ the diff.
1231
+ - **A live transcript stops following at entry sixty.** The effect that kept the
1232
+ box on the newest row was keyed on how many rows were *shown*, which is
1233
+ `min(entries, window)` — so it stopped changing the moment a transcript
1234
+ outgrew one window, and the following died silently and permanently, including
1235
+ after the operator scrolled back to the bottom to ask for it. Which is roughly
1236
+ minute two of any real phase. It follows on the entry count now, growing the
1237
+ window keeps the reader on the row they were reading instead of teleporting
1238
+ them, and a **Jump to latest** button says so out loud rather than leaving a
1239
+ 60px band at the bottom of a scroller as the only way back in.
1240
+
1241
+ - **The transcript spends its space on the noisiest rows.** An agent's answer,
1242
+ its private reasoning and four hundred characters of tool-call JSON all
1243
+ rendered as the same two lines at the same weight. Thinking is now small and
1244
+ quiet, and a streamed thought is one row rather than the dozen events it
1245
+ arrived as; tool calls and their results fold to the argument that says what
1246
+ they did — a command, a path — and open individually or a whole kind at a time
1247
+ from the panel header. Prose is untouched and never folds.
1248
+
1249
+ - **A panel's expand control did nothing on the node view.** It expanded by
1250
+ taking `col-span-full`, which on the run view bought a strip a third wider and
1251
+ on the node view matched nothing at all — those panels sit in a flex column,
1252
+ not a grid, so the only thing the button changed was the height of the
1253
+ scroller inside it. It now opens the panel over the whole page through a
1254
+ portal, so no scrolling ancestor can clip it, with Escape to leave and the
1255
+ page behind it held still. A transcript that was following stays on its newest
1256
+ row across the change.
1257
+
1258
+ - **`doctor` checks that the shared servers answer.** It verified that binaries
1259
+ existed and disk fitted, and never looked at a `server_url` — so a project
1260
+ whose Postgres lives in another checkout's compose stack passed the preflight
1261
+ with that stack down and died on the first `createdb`, before a worktree
1262
+ existed. Each external database's server and each service URL is now probed
1263
+ with a TCP connect, and a failure names the address that did not answer. A
1264
+ compose-delivered database is deliberately not probed: the lane starts it.
1265
+
1266
+ The order matters as much as the check. `prepare_cmd` runs *before* the
1267
+ preflight, so the check reports the world that hook just made rather than the
1268
+ one in front of it.
1269
+
1270
+ - **A service's `create_cmd` runs with the lane's environment.** `reset_cmd`
1271
+ always had it and `create_cmd` did not, so a `create_cmd` that reached for
1272
+ `docker compose` ran against the lane's own compose file with no project name
1273
+ and no override — booting a stack on the project's fixed host ports, which is
1274
+ the collision the override exists to prevent, caused by the step that sets a
1275
+ lane up.
1276
+
1277
+ - **`vinta-ai-maestro with` waits for the lock instead of giving up on it.** Its
1278
+ usage said it blocked until the resource was granted, and it did — for about
1279
+ five minutes, which is where Node's own `fetch` stops waiting for a response.
1280
+ A test suite behind a capacity-1 semaphore beats that regularly. What the
1281
+ agent saw was "could not reach the lease daemon", and what agents did with
1282
+ that, repeatedly, was run the command without a lease: exactly the stampede
1283
+ the pool exists to prevent, arrived at by an agent behaving reasonably.
1284
+
1285
+ The wait is the client's now. The daemon answers `202 still queued` within
1286
+ seconds and **leaves its own queue behind it**, so nothing is granted to a
1287
+ request that has gone away; the client loops until it is granted, saying so
1288
+ once and then occasionally, so a transcript shows a wait rather than a
1289
+ silence. Only answers that waiting cannot change — a resource this run does
1290
+ not declare, a run that is no longer live — end it.
1291
+
1292
+ And when it does end that way, the refusal says what not to do about it.
1293
+ "The resource lease was not granted" is an error with no alternative in it;
1294
+ the rule against working around it now travels with the message rather than
1295
+ sitting only in a prompt the agent read twenty minutes earlier. The prompt
1296
+ says it too, including that waiting is the expected outcome and not a failure.
1297
+
1298
+ - **A retry gets a fresh fix budget.** `fix_rounds` was set to 0 when a node was
1299
+ created and never again, so a phase that spent its whole budget on attempt 1
1300
+ began attempt 2 already exhausted — one review, one fix, and the exhaustion
1301
+ transition fired straight back. Seen with `max_fix_rounds: 4`: the retry got a
1302
+ single round before the operator was asked again, forty seconds of work
1303
+ standing in for four rounds of it. A retry that inherits the reason the last
1304
+ attempt ran out is not a retry. It resets wherever a node is re-driven from
1305
+ its pipeline's initial state — a capacity refusal, an automatic retry, the
1306
+ operator's answer, the `retry` verb — which is one place rather than four.
1307
+
1308
+ - **The last fix is reviewed rather than failed unread.** `standard-phase`
1309
+ checked the budget on the way *out* of `fix`, so the final fixer's work went
1310
+ straight to `failed` with nothing looking at it. Two phases in one run ended
1311
+ on a fixer reporting "all gates green, committed, tree clean" and were failed
1312
+ anyway. The check now sits in front of the fixer, on both doors into it: every
1313
+ fixer that runs is reviewed, and the review after the last one can still pass
1314
+ the phase. Guarding only the review side left a red gate under a passing
1315
+ reviewer looping forever, which is the other half of the same change.
1316
+
1317
+ `max_fix_rounds` still means the number of fixers a phase may spend. `0` now
1318
+ means none, where before it let one run and failed the phase regardless.
1319
+
1320
+ - **Agents are told not to background their work.** An implementer started five
1321
+ commands with `run_in_background: true` and closed its turn with "I'll wait for
1322
+ the test result notification before continuing to the outer gate". There is no
1323
+ notification: a headless session ends when the turn ends and whatever it
1324
+ backgrounded is killed with it. Three sessions ended with no report and no
1325
+ commit, and the reviewer failed each for uncommitted work — true, and not the
1326
+ cause. Nothing had said so, and believing a tool that offers backgrounding will
1327
+ still be there afterwards is not unreasonable.
1328
+
1329
+ - **The reviewer runs the gates instead of taking their word for it.** The prompt
1330
+ asked it to confirm the outer gate was green, which an agent can do by reading
1331
+ the implementer's report — one reused reviewer session reached a verdict in
1332
+ under four minutes without running the project's tests at all. It is now asked
1333
+ to run them, to say what they returned, and to treat being unable to run them
1334
+ as a finding. Re-review rounds are asked again, because a session that
1335
+ remembers running them last round is remembering a different tree.
1336
+
1337
+ - **A retried phase keeps the commits the attempt before it made.** A failed
1338
+ phase re-enters its pipeline at the initial state, which runs `git_branch`
1339
+ again — and `checkout -B` moves an existing branch unconditionally, so attempt
1340
+ 2 began by resetting attempt 1's work to base. The files left the worktree,
1341
+ the next reviewer correctly reported "phase not implemented at all", and the
1342
+ fix budget burned re-implementing from nothing until the operator was asked. A
1343
+ phase that produced real commits was guaranteed to lose them.
1344
+
1345
+ "Cold" was always meant to describe the agent's *session*, not its branch —
1346
+ and the default policy targets environmental failures, which is exactly when
1347
+ the previous attempt's code is worth keeping. A resumed branch is checked out,
1348
+ never rebased onto a base that moved: a conflict there would fail the retry
1349
+ during its setup, before the agent that might resolve it has run. A branch
1350
+ that committed nothing is still moved up. Whether this is a second attempt is
1351
+ read from the journal rather than from the ref, because a phase branch's name
1352
+ carries the plan id and not the run id — an identically named branch left by
1353
+ an earlier run must still be cut fresh. `node_assigned` now records
1354
+ `previous_head`, so a reset that does happen is visible in the run rather than
1355
+ only in a reflog.
1356
+
1357
+ - **A phase is judged on commits, and the prompts finally say so.** An
1358
+ implementer ran four sessions, reported `SUCCESS` with a green inner loop, and
1359
+ never ran `git commit`: its deliverables were untracked files. The reviewer
1360
+ reads the committed diff, so it saw an empty one and reported "not implemented
1361
+ at all"; the fixer re-implemented, also without committing; the rounds ran out
1362
+ and the lane was recycled over the files. Every instruction that agent had was
1363
+ *about* committing — "never commit while a gate is red" — and none of them
1364
+ said it had to.
1365
+
1366
+ Implementers and fixers, cold and continued, are now told that the phase is
1367
+ reviewed and merged from commits on its branch, that an uncommitted file does
1368
+ not exist as far as the run is concerned, and that the turn is not over until
1369
+ `git status --porcelain` is empty of their work — staged by explicit path,
1370
+ never `git add -A`, because a lane holds local files that are not theirs to
1371
+ commit. The reviewer reads the working tree as well as the diff: a full tree
1372
+ with an empty diff is a real failure and is *not* "not implemented", and the
1373
+ difference decides whether the fixer writes the code again or simply commits
1374
+ it.
1375
+
1376
+ - **A recycled lane no longer deletes work nobody committed.** Both recycle
1377
+ paths throw the working tree away. The tree is now committed first — with
1378
+ plumbing, and **off the branch**, under `refs/vinta-ai-maestro/wip/`. On the
1379
+ branch was the first implementation, and it was wrong: a lane's dirty tree
1380
+ holds gate artifacts as often as deliverables, and a phase branch is the base
1381
+ of its dependents, so one gate's scratch file reached the next phase and
1382
+ failed a gate for a reason nothing in that phase caused. Nothing merges,
1383
+ nothing reaches a dependent, and one `git restore --source <ref>` gets a file
1384
+ back. Where the rescue itself fails, the recycle refuses and names the paths.
1385
+
1386
+ The retry question says how much is at stake: commits are kept, and *N*
1387
+ uncommitted files in the lane will be set aside.
1388
+
1389
+ - **`project.hooks: skip`.** Committing is not optional any more, and a
1390
+ `language: system` pre-commit chain reads a never-committed-in worktree as a
1391
+ fresh machine — one project's hooks built a 510 MB virtualenv before allowing
1392
+ a first commit, per lane, after four attempts and a two-minute timeout. Set
1393
+ per worktree, so the operator's own checkout keeps its hooks, and opt-in,
1394
+ because hooks are usually there for a reason.
1395
+
1396
+ - **`doctor` knows when a project needs docker compose.** It assembled its
1397
+ options without a `project`, so `needsCompose` was handed `undefined` on every
1398
+ invocation and reported "not required by this project" for a
1399
+ compose-delivered database — and `run`'s own preflight did the same. A check
1400
+ nothing can reach is worse than an absent one: it reads as a pass.
1401
+
1402
+ - **A failing `setup_cmd` says why.** It carried a lane name and nothing else,
1403
+ so the operator saw "could not provision the lane pool under …" and had to
1404
+ replay the command by hand with the lane environment rebuilt to find a missing
1405
+ settings module. It carries the exit code and a bounded stderr tail now — the
1406
+ same class of thing as a harness refusal's own explanation, which this package
1407
+ already decided to carry after an afternoon lost to the same silence.
1408
+
1409
+ - **A lane the executor was not given is refused, not guessed.** It fell back to
1410
+ the right directory with an *empty environment* — a gate running against the
1411
+ wrong compose project and no forked connection string, with nothing anywhere
1412
+ saying a lane was missing.
1413
+
1414
+ - **The monitor reads, and does not run.** Its shell has no lane environment, so
1415
+ the project's own commands run from it contend with the lanes instead of
1416
+ observing them. One reported "the final gate fails on a port conflict" when no
1417
+ gate had run at all, and the operator believed it. Gate results come from the
1418
+ journal and the gate logs.
1419
+
1420
+ - **`serve --host 0.0.0.0` prints a URL another machine can open.** Binding
1421
+ every interface made the server report the wildcard back, so the line printed
1422
+ for the operator to open — and share — was `http://0.0.0.0:<port>`, which
1423
+ resolves for nobody. The bind is unchanged; the printed host is now this
1424
+ machine's LAN address. The daemon still warns, because the token in that URL
1425
+ is the only thing between the run and anyone who can reach the port.
1426
+
1427
+ - **An agent can run a shell command.** `auto` mapped to the vendor's
1428
+ `acceptEdits`, which accepts file *edits* and nothing else — a shell command
1429
+ still goes to a permission prompt, and in `-p` there is nobody to answer one.
1430
+ A run of eight phases died of it: the agents wrote their code and were then
1431
+ refused `ruff`, `pytest`, `git add` and `docker compose`, **69 denials across
1432
+ two lanes**, every gate failing on work that was never allowed to be checked.
1433
+ `auto` now allows `Bash` in the policy file it already writes.
1434
+
1435
+ What that costs is worth stating plainly: **`Bash` was never confined to the
1436
+ lane.** The deny list covers the file-editing tools and a shell redirection
1437
+ walks through it, so allowing the shell allows commands on the machine.
1438
+ `auto` has always promised exactly this in words — "the agent works unattended
1439
+ inside its lane" — and this is the first version where the words are true.
1440
+ `ask` still does not allow it: that mode exists for a human at the browser,
1441
+ and the prompt is its purpose.
1442
+
1443
+ - **A refusal says why, in the words the harness used.** `permission_denied`
1444
+ carried a decision token and deliberately dropped the sentence beside it, on
1445
+ §11 grounds. Then a run failed with forty-five denials reading
1446
+ `reason: "other"`, and finding out what that meant cost an afternoon and a
1447
+ rebuilt reproduction — the sentence said "this Bash command contains multiple
1448
+ operations; the following parts require approval". §11 keeps repository
1449
+ *contents* out of the record; a refusal's own explanation is not contents, and
1450
+ the `tool_use` row above it already carries the path and the command verbatim.
1451
+ Withholding it protected nothing and hid the one fact worth having.
1452
+
1453
+ - **`serve` opens a run that has already finished.** The API resolved a run from
1454
+ the in-memory registry *and* the journal and required both, so a daemon
1455
+ started with nothing running listed the operator's history — the list reads
1456
+ the journal — and then answered 404 for every run in it, under a message
1457
+ saying the daemon was not running. Reads now need only the journal, which is
1458
+ where a run's whole record lives and which outlives the daemon by design
1459
+ (§5.3). Steering a finished run is still refused, with `409` rather than
1460
+ `404`: the run exists, and saying it does not sends the operator hunting for a
1461
+ typo instead of reading the status in front of them.
1462
+
1463
+ - **Four places the UI asked the operator to work around it.** The transcript
1464
+ opened at the *oldest* row it held, so a live agent's newest line — the one
1465
+ the panel was opened to read — sat below the fold and every arriving entry
1466
+ pushed it further down; it now opens at the newest and stays there, but only
1467
+ while the operator is already at the bottom, because scrolling up is reading.
1468
+ The `git diff` command overflowed its panel, uncopyable without selecting
1469
+ blind, and now wraps. The shell's bounded column went from 1280px to 1600px:
1470
+ 1280 is a reading measure, and the Nodes table's columns are ids, lane names
1471
+ and branch names, none of which can be abbreviated without losing what
1472
+ identifies them. And the notification interval said "Remind every 5 min"
1473
+ without ever saying what it would remind anyone about — it repeats the
1474
+ notification for a phase that stopped to ask the operator something, until
1475
+ they answer, which is what the options say now.
1476
+
1477
+ - **A killed terminal releases its directory on Windows.** Two PTY tests failed
1478
+ teardown with `EBUSY: rmdir`: killing a process there is not synchronous with
1479
+ releasing what it held, so the conpty and its background child are gone as far
1480
+ as the test is concerned while the OS still has the working directory open.
1481
+
1482
+ - **A failed phase can be retried instead of ending the run.** `run` takes
1483
+ `--on-failure <stop|ask>`. `stop` is the default and is what every run has
1484
+ always done — the phase fails, its dependents block, the run finishes. `ask`
1485
+ parks the node on a question instead: retry, retry with another member of the
1486
+ crew, or stop.
1487
+
1488
+ It exists because the failures worth retrying are overwhelmingly
1489
+ environmental — a permission wall, a grant nobody made, a gate whose service
1490
+ was not up — and the only recovery was to start the whole plan again, running
1491
+ every phase that had already succeeded a second time. Only pass `ask` when
1492
+ somebody is watching: the run waits, which is exactly why it is not the
1493
+ default.
1494
+
1495
+ The alternatives offered are **members of the crew, not models**. A staffed
1496
+ run has no free-floating models in it — a phase is taken by a member, and the
1497
+ tier floor that decides who may take it is the assigned member's own — so a
1498
+ model is something the plan cannot express and nobody could be found to hold.
1499
+ A member below the phase's tier is never offered: an operator answering a
1500
+ question is not a reason to hand a phase to somebody the plan judged too
1501
+ junior for it.
1502
+
1503
+ The retry starts **cold**, and everything the node held goes back before it
1504
+ waits (§6.1) — an operator at lunch must not be holding a lane another phase
1505
+ could use.
1506
+
1507
+ - **A refusal nobody was asked about is now said out loud.** The adapter knew
1508
+ about `permission_request`, a question waiting for an answer, and nothing
1509
+ else. The refusal an operator actually hits is not a question: a read outside
1510
+ the working directory is decided by the CLI and announced as
1511
+ `permission_denied`, with no request to reply to. That frame was dropped at
1512
+ the first `switch` — no event, no journal row, no transcript line. The new
1513
+ `permission_denied` event carries the decision token (`workingDir`, `mode`)
1514
+ and never the vendor's prose, which names the file being read (§11).
1515
+
1516
+ - **A turn that ended blocked is no longer a turn that succeeded.** The CLI
1517
+ says `status_category: "blocked"` one frame before reporting
1518
+ `is_error: false`, and both are true from its side: it was asked for
1519
+ something it could not do and said so. Taking the second at its word is how a
1520
+ phase passed having written no code and then failed two steps later under
1521
+ another name. A session that reported an error no longer ends `ok` — which
1522
+ also makes a reviewer fail closed, since its verdict is read back out of a
1523
+ transcript whose session did not end cleanly.
1524
+
1525
+ - **An agent still could not write in its own lane.** The entry below passed
1526
+ claude-code `--permission-mode auto`, which matched our own vocabulary for
1527
+ "works unattended" and is not what the vendor means by the word: `auto` still
1528
+ routes a write to a permission prompt, and in `-p` there is nobody to answer
1529
+ one. Run against the CLI, a `Write` to the agent's *own working directory*
1530
+ comes back denied with no reason attached — the same dead end, wearing the
1531
+ word that made it look closed. `acceptEdits` is the mode under which that
1532
+ write succeeds, and is what `auto` now maps to. The test covering this
1533
+ asserted the word rather than the behaviour, so it pinned the bug in place.
1534
+
1535
+ - **A phase can read the operator's checkout.** A lane is a worktree cut from a
1536
+ branch, so anything uncommitted — a plan written this morning, a spec that
1537
+ never leaves the operator's machine — is present where they are and absent
1538
+ from every lane. Reaching for it was refused before the model saw a byte, with
1539
+ a message that reads like a prompt awaiting an answer ("Claude requested
1540
+ permissions to read from …, but you haven't granted it yet") when in fact
1541
+ nothing is asking: the CLI denies it outright, emits no permission request,
1542
+ and the session then ends *successfully*, having written nothing.
1543
+
1544
+ The repository root is now granted to every claude-code agent for **reading**.
1545
+ Granting a directory also grants writing in it, and the lane sits inside the
1546
+ directory being granted, so the grant is paired with a deny list that keeps
1547
+ the lane the only writable place under it — the operator's source and every
1548
+ sibling lane stay refused. That list names the *siblings* at each level down
1549
+ to the lane rather than the root itself, because the vendor resolves deny
1550
+ before allow with no carve-out: `deny: <repo>/**` plus `allow: <lane>/**`
1551
+ refuses the lane too. Each of those facts was established by running the CLI.
1552
+
1553
+ **This covers the file-editing tools and not `Bash`.** A shell redirection was
1554
+ never confined to the working directory and is not confined now; that boundary
1555
+ is the OS's, and the vendor's sandbox that enforces it also switches off the
1556
+ network a phase needs to install anything.
1557
+
1558
+ codex needed nothing: under `--approve-for-me` it already reads anywhere on
1559
+ disk, which was confirmed the same way.
1560
+
1561
+ - **An agent can write in its own lane.** Every adapter declared
1562
+ `permissionControl: true` and passed no policy at all: claude-code was spawned
1563
+ as `-p --output-format stream-json …` with no `--permission-mode`, and codex as
1564
+ `exec --json` with no sandbox and no approval flag, under a comment saying it
1565
+ took both on the command line. So a headless run asked before its first write,
1566
+ emitted a `permission_request`, rendered it in the transcript — and nothing
1567
+ anywhere answered. The phase failed reporting a permission system it could not
1568
+ see, having written nothing.
1569
+
1570
+ `run` and `serve` now take `--permission <ask|auto|full>`, defaulting to
1571
+ `auto`. **The operator sets it, never the workflow document**: the document is
1572
+ committed and shared, and a file in a repository should not be able to tell
1573
+ someone else's machine to run agents without approvals.
1574
+
1575
+ Both CLIs refuse combinations that looked reasonable, and both were found by
1576
+ running them rather than reading them: codex rejects `--sandbox` alongside
1577
+ `--approve-for-me`, and `codex exec resume` accepts neither — passing the
1578
+ fresh-spawn policy there turned a `stale_session` refusal, which the scheduler
1579
+ retries cold, into a `fatal` one that fails the node.
1580
+
1581
+ This reverses a documented decision. The README said the daemon passed no
1582
+ permission flags and that a committed `.claude/settings.json` was what made a
1583
+ run able to write; that is now the narrowing layer on top of a mode, not the
1584
+ only thing standing between an agent and a blocked lane.
1585
+ - **`purge --lanes --branches` clears what a failed run leaves behind.** A run's
1586
+ lane worktrees are not under `runs/`, so no purge ever reached them: three
1587
+ failed attempts at one plan left twelve worktrees, twelve summaries, and a set
1588
+ of `plan/<id>/phase-*` branches still checked out — which is what makes the
1589
+ *next* run of that plan impossible, since git refuses to check out a branch a
1590
+ second worktree holds.
1591
+
1592
+ One rule decides both: **never delete the only copy of work.** A phase can
1593
+ leave work committed on its branch or uncommitted in its worktree, and both
1594
+ count — so a branch carrying commits no other branch has is kept, a lane with
1595
+ a dirty tree is kept, and each is listed with the command to remove it by
1596
+ hand. Untracked files count as work: a phase that wrote three new modules and
1597
+ never committed them is exactly the case worth protecting.
1598
+
1599
+ Emptiness is measured as "carries no commit another branch does not already
1600
+ have", not as "merged into HEAD". The latter is relative to wherever the
1601
+ operator is standing, so on a feature branch — the checkout this is most often
1602
+ run from — every empty phase branch looked unmerged and nothing was ever
1603
+ cleaned up.
1604
+
1605
+ `--dry-run` and `--yes` work as they already did, and a worktree that will not
1606
+ go is named rather than counted.
1607
+
1608
+ - **`doctor` now catches, before a lane exists, the failures that used to take a
1609
+ run each.** Four consecutive runs of one plan failed four different ways, and
1610
+ every one of them was knowable at minute zero from the workflow and the repo.
1611
+ - **Every `prompt_ref` and `plan_context_ref` is resolved against
1612
+ `base_branch`**, with `git cat-file -e`, which reads the branch's tree and
1613
+ so cannot be satisfied by a working-tree copy — the working tree being
1614
+ exactly what a lane will not have. It distinguishes the three cases, because
1615
+ they have three different fixes: never committed, **staged but never
1616
+ committed** (`git add` alone does not put a file in a branch), and committed
1617
+ on a different branch. The last offers both routes out — merge it, or point
1618
+ `base_branch` at the branch that has it — since only the operator knows
1619
+ which they want.
1620
+ - **Phase branches held by another worktree** are reported with the directory
1621
+ to remove. Lane directories are named per run and phase branches per
1622
+ workflow, so a failed run leaves worktrees holding the branch names the next
1623
+ run will try to cut, and git refuses to check out a branch twice. Nothing
1624
+ cleans those up: `purge` deletes run state under `.vinta-ai-maestro/runs/`,
1625
+ and lane worktrees are not there. `doctor` does not delete them either — a
1626
+ failed lane's worktree is the only place its state survives — but it now
1627
+ names each one and the exact command.
1628
+
1629
+ - **A failed node records why.** `node_status` carried `{"status":"failed"}` and
1630
+ nothing else, so two runs failing for two unrelated reasons produced identical
1631
+ journal rows and the cause survived only in the operator's terminal. The
1632
+ reason is now persisted — **sanitized**, not raw: errors the package builds
1633
+ itself are identifiers by construction and are kept verbatim, and anything
1634
+ else is reduced to its kind, the rule `recycleStage` already followed. The
1635
+ journal is durable and API-served, and an exception message from a dependency
1636
+ is exactly how repository content gets into one. Blocked dependents record
1637
+ which node blocked them.
1638
+
1639
+ - **Two diagnostics that named the symptom and hid the cause.** Both came out of
1640
+ one real run, and neither was a wrong answer — just an unusable one.
1641
+ - **A phase whose plan is not committed** failed with `prompt_ref "…" names no
1642
+ readable file`, against a path that was spelled correctly. A lane is a fresh
1643
+ worktree of the base branch, so a plan sitting untracked in the operator's
1644
+ checkout is in the one place the run cannot look. The message now names the
1645
+ directory it resolved against and says why a plan is often not in it.
1646
+ - **A lane summary the daemon could not accept** reported `summary unreadable`
1647
+ and advised deleting the lane — which takes its forked databases with it,
1648
+ over what turned out to be two wrong fields. `doctor` now names the fields
1649
+ and what was expected, and offers repair before deletion. A file that does
1650
+ not parse at all still reports as unreadable, because there is no field to
1651
+ name. Neither message carries the offending *value*: a summary holds
1652
+ database names and connection variables, and the value that failed
1653
+ validation is the likeliest thing in it to be one.
1654
+
1655
+ `prepare-worktree` gained the rule that would have prevented the summary in
1656
+ the first place — its `|` alternatives are closed sets read by machine, `null`
1657
+ is how you say "none", and a strategy outside the set should be the closest
1658
+ one plus a `note:` rather than a fourth word.
1659
+
220
1660
  - **`npx vinta-ai-maestro` now runs.** The published package pointed its `bin` at
221
1661
  `src/cli/bin.ts` and relied on Node stripping the types at startup — which Node
222
1662
  refuses to do anywhere under `node_modules`, unconditionally and with no flag to
@@ -654,7 +2094,6 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
654
2094
  **Consumers**: re-sync to pick up the opt-in questions. E2E-disabled projects
655
2095
  are unaffected (the e2e regions strip out as before).
656
2096
 
657
-
658
2097
  <!-- pre-release: 0.2.0-alpha6 on 2026-07-13 -->
659
2098
 
660
2099
  ### Changed
@@ -691,7 +2130,6 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
691
2130
 
692
2131
  ### Added
693
2132
 
694
-
695
2133
  - **`handoff` foundation skill — session-continuation handoff docs between
696
2134
  agents.** Write mode captures the current task (goal, verified-vs-unverified
697
2135
  state, decisions + rejected alternatives, landmines, single concrete next