vinta-ai-workflows 0.7.0-alpha18 → 0.7.0-alpha19

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,7 @@ 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-alpha19 on 2026-09-19 -->
10
11
  <!-- pre-release: 0.7.0-alpha18 on 2026-09-17 -->
11
12
  <!-- pre-release: 0.7.0-alpha17 on 2026-09-17 -->
12
13
  <!-- pre-release: 0.7.0-alpha16 on 2026-09-16 -->
@@ -28,6 +29,74 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
28
29
 
29
30
  ### Added
30
31
 
32
+ - **Gate durations are now kept, not just displayed.** Every `gate_result` has
33
+ carried the runner's own `duration_ms` since gates were journalled, and
34
+ nothing read it back: the figure went to a live panel and was afterwards
35
+ reachable only by hand-parsing event payloads, so the one measurement that
36
+ answers "is the suite why this run took an hour" could not be asked of a
37
+ finished run. `analyzeRun` now rolls it up per gate — runs, cache hits,
38
+ summed runtime, the slowest single run, verdicts by kind — and each phase's
39
+ time split carries `gateRunMs` beside `gatePoolQueueMs`, so a run says how
40
+ much of its working time was the gate queue and how much was the gate. The
41
+ post-mortem carries the same as a `gate_costs` finding, which is what makes
42
+ it durable: `plan-feature` reads these artifacts when sizing the next plan,
43
+ and a gate pool's capacity and `max_parallel_lanes` are one decision made
44
+ with half the numbers. Cache hits are counted and their time kept in its own
45
+ `cache_saved_ms` column — a hit's duration is what the gate cost when it last
46
+ ran, and adding it to the runtime would report time the run specifically did
47
+ not spend. A verdict written before the runner measured itself is counted and
48
+ its milliseconds are not: both reports emit a `gate_durations_unrecorded` gap
49
+ naming the gates, because a gate reported as free is the number a planner
50
+ would act on hardest.
51
+
52
+ - **Phases can run chores: declared agent turns that change the diff rather than
53
+ judge it.** A gate is a shell command that says pass or fail; a chore is an
54
+ agent asked to do something to the work — rewrite the comments this phase
55
+ wrote, add the changelog entry, extract the strings that need translating. It
56
+ runs on the implementer's own session, so the agent that wrote the diff is the
57
+ one asked to act on it. Declare them in `chores.<id>` (a `prompt` or a
58
+ `prompt_ref` into the plan, plus an optional `skill` the prompt names) and
59
+ pick them with `defaults.chores` for the run or `nodes[].chores` for one
60
+ phase — a node's list replaces the default rather than adding to it, so `[]`
61
+ opts a phase out. In `standard-phase` they run in a new `polish` state,
62
+ between a passing review and the gate: a chore edits the tree, so running it
63
+ later would merge a diff the gates never ran against, and running it earlier
64
+ would have the fixer rewrite what it just did. A chore that fails, or that the
65
+ harness had no capacity for, is journalled and the phase continues to its
66
+ gates — `on_failure: "fail"` is for one the phase is not correct without.
67
+
68
+ - **The post-mortem now reports what the schedule cost.** Two findings, both
69
+ derived from events the journal already carried. `critical_path` is the
70
+ dependency chain that decided the wall clock — each phase's span, the total,
71
+ and its share of elapsed — because every phase *off* that chain could be made
72
+ instant without moving the run, and nothing said which phases those were.
73
+ `idle_capacity` reports the width the graph actually reached against the lanes
74
+ it asked for: a plan declaring three lanes that never runs more than two
75
+ phases at once pays for a third worktree, a third forked database and a third
76
+ desk, and gets none of them back. Run against a real fourteen-hour build, the
77
+ two say its critical path was **99% of elapsed time** — six phases, effectively
78
+ serial — with peak concurrency 2 against 3 lanes and **55% of lane time idle**.
79
+ `plan-feature` already reads these artifacts before drawing the next set of
80
+ dependency lines; until now neither depth nor width was in them. A
81
+ `blocking_cause_unrecorded` gap ships alongside, because the chain is what the
82
+ *graph* forced and a phase can also wait on a busy lane or an unanswered
83
+ question — which the journal cannot currently tell apart.
84
+
85
+ - **`--retry-after <15m>`: an unanswered failure question eventually answers
86
+ itself.** An observed fourteen-hour run spent 4h07m — 29% of its wall clock —
87
+ parked on "This phase failed. Try it again?" with nobody at the keyboard; the
88
+ answer, when it came, was `retry`, and it worked. Unset by default, so a run
89
+ behaves exactly as it did. Accepts minutes bare or a unit (`15`, `15m`, `90s`,
90
+ `2h`) on both `run` and `serve`. It is deliberately unbounded and fires again
91
+ on each new question: a version capped by `--retries` would stall at the cap
92
+ and idle for the rest of the night, which is the failure it exists for. Every
93
+ firing is a full phase attempt, so the interval is the throttle. It reaches
94
+ the failure question and nothing else — a plan's own `await_human` gate asked
95
+ for a person, and both park through the same code, so answering those would be
96
+ the orchestrator overruling the plan. An unattended answer is journalled with
97
+ `unattended: true`, because "a human said try again" and "nobody was here" are
98
+ the same answer with very different meanings.
99
+
31
100
  - **The daemon now has a log of its own, and a Logs view to read it in.**
32
101
  Transcripts said what the agents did; nothing said what the *daemon* did, so
33
102
  "the run stopped and I don't know why" had no evidence behind it at all.
@@ -75,6 +144,82 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
75
144
  is already a copy of the repository, and excluding it cost the one string that
76
145
  most often explains a failure. `--log-detail kind` narrows to the kind and the
77
146
  stack frames for a checkout under a stricter obligation than this store's own.
147
+ - **A run that is dragging can now tune itself, within bounds somebody wrote
148
+ down.** A run can be mis-*configured* rather than broken — every phase doing
149
+ what it was asked, every gate reporting honestly, and the whole thing paying
150
+ for a test database it rebuilds on every gate run in every lane. Nobody needs
151
+ waking for that, and the post-mortem found it out an afternoon too late to
152
+ help. Now a watchdog wakes the run's monitor when a phase passes an hour or a
153
+ gate's uncached cost passes thirty minutes, the monitor reads the gate logs
154
+ and the durations, and it answers with a **proposal** that the daemon
155
+ validates and applies through the existing amend path. `--no-intervene` turns
156
+ it off.
157
+
158
+ It proposes; it never writes. Its authority is four verbs — a gate's command,
159
+ a gate's timeout, a phase's fix budget, a phase's model — and the line they
160
+ draw is that it may change **how** the run executes and never **what** it
161
+ builds. There is no way to express a change to a dependency, a phase brief, a
162
+ touch list, the base branch, or the set of phases: not refused at runtime,
163
+ unrepresentable.
164
+
165
+ The one field that can do real damage is a gate's command, because
166
+ `--reuse-db` and `-k not_slow` are the same edit to the same string, and a
167
+ gate narrowed wrongly goes green and looks like success in every log there
168
+ is. So a command may only be changed when the gate itself declares
169
+ `tuning.allowed_flags` — a list a person wrote in the committed plan, under
170
+ review, before the run started — and the proposed command must be the current
171
+ one's argv tokens, in order, plus additions from that list. A gate that
172
+ declares no `tuning` block is not tunable, which is the default.
173
+
174
+ A run gets three of these in its life, at most one per gate and per phase.
175
+ What it changed is journalled as identifiers; why it changed it is the
176
+ monitor's prose and goes in `runs/<run-id>/interventions.jsonl` beside the
177
+ run, including every attempt that was refused — an agent trying to do
178
+ something it may not do, with nobody in the room, is the record most worth
179
+ keeping. See `packages/vinta-ai-maestro/docs/monitor-intervention.md`.
180
+
181
+ - **`plan-feature` is told what the newer post-mortem findings mean.** The
182
+ skill documents each finding and what to *do* about it, and three had been
183
+ added to the artifact without reaching it: `gate_costs`, `critical_path` and
184
+ `idle_capacity`. A finding a planning agent cannot interpret is a finding
185
+ that changes no plan, which is the whole return on emitting it — and the
186
+ three are among the most actionable there are. `gate_costs` sizes the gate
187
+ pool capacities a plan declares, and separates a slow suite from a fix loop
188
+ re-paying for the same one. `critical_path` is the chain to re-draw the graph
189
+ against: shortening a phase that is not on it changes nothing, so a plan that
190
+ parallelises harder without touching that chain buys nothing at all.
191
+ `idle_capacity` says the graph never got as wide as the lanes it asked for,
192
+ which is a fact about the plan rather than about the machine. Each now has a
193
+ row beside the findings that already had one.
194
+
195
+ - **The post-mortem says what a run changed about itself, and whether it
196
+ helped.** `postmortem.v1` gains an `interventions[]` finding: one entry per
197
+ amendment the run made to itself, with the effect on the thing it changed. A
198
+ gate is scored against its own uncached durations either side of the
199
+ amendment — mean before, mean after, `cheaper` / `dearer` / `unchanged`
200
+ outside a ten-per-cent band. Cached hits are excluded, because a hit reports
201
+ the duration of the run that filled the cache and would drag the mean toward
202
+ whichever side of the boundary that run fell on.
203
+
204
+ It is a comparison and not a claim of cause, and the schema says so: a gate
205
+ that got cheaper did so while phases were finishing and caches warming. It is
206
+ worth carrying anyway, because without it an autonomous editor is one nobody
207
+ can tell is making runs worse — and because `plan-feature` now reads it, so a
208
+ project whose `unit` gate wants `--reuse-db` pays to find that out once
209
+ instead of every run.
210
+
211
+ A phase-level change (a fix budget, a model) is reported `unmeasured` rather
212
+ than estimated: a phase runs once, so there is no before to compare an after
213
+ against, and the only candidate baseline is a different phase doing different
214
+ work. An operator's amendment is not scored at all — a person deciding
215
+ something is not the run choosing it.
216
+
217
+ - **`schemas/intervention.v1.schema.json`** — the document the monitor answers
218
+ with, generated from `src/intervention/intervention.ts` the way the workflow
219
+ and post-mortem schemas are generated from theirs. It is the odd one in
220
+ `schemas/`: it validates something a *model* writes rather than a skill or a
221
+ person, and it is the boundary deciding what an unattended run may change
222
+ about itself.
78
223
 
79
224
  - **A run no longer dies with the terminal that started it.** `vinta-ai-maestro
80
225
  serve` accepts `POST /api/runs`, so a run can be submitted to a daemon that
@@ -419,6 +564,28 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
419
564
 
420
565
  ### Changed
421
566
 
567
+ - **The node view's gate panel is a list of verdicts you can open, not a stack
568
+ of scrolling boxes.** Every gate used to render its whole log at once, each
569
+ in its own fixed-height box with its own scrollbar — so three gates in a
570
+ third of a grid row was a column of letterbox slots, and a wheel gesture
571
+ over one of them was swallowed by that box instead of scrolling the page.
572
+ Gates are now an accordion: one row per gate, only one open at a time, and
573
+ the open log has no scroller of its own (it is tail-truncated at 64 KiB
574
+ already, and long lines wrap the way the diff panel's do). The panel opens
575
+ on the gate a human question points at, or on the first that is not passing.
576
+
577
+ - **Each gate row says what happened and what it cost.** The verdict —
578
+ `running`, `passed`, `failed`, `timed out` — is on the wire now, so a phase
579
+ whose lint gate passed and whose unit gate failed reads as that instead of
580
+ as two identical rows; previously the UI could only name the single gate
581
+ §9.1's question happened to point at, because `NodeDetail` carried an id and
582
+ a log and nothing else. A running gate shows a clock counting from the
583
+ daemon's own start time, so a tab opened halfway through a slow suite shows
584
+ the true elapsed figure rather than starting from zero; a finished one shows
585
+ the runner's measurement of its latest attempt, with `cached` where the
586
+ cache served the verdict and a `×N` count where the gate has run more than
587
+ once.
588
+
422
589
  - **Crew members are named after their tier, not after a seniority.** A roster
423
590
  reads `tier1`, `tier2-1`, `tier2-2`, `tier4` and `reviewer` where it used to
424
591
  read `junior`, `mid-1`, `mid-2` and `senior`. Nothing about the data model
@@ -440,6 +607,27 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
440
607
  which the schema requires to be lowercase kebab-case, so the date prefix is
441
608
  the only part the three can share. Existing dateless workflows are unaffected
442
609
  — this is the emitter's convention, not a schema rule.
610
+ - **A gate's command can be retuned while the phase that runs it is still
611
+ running.** §9's amend refused any change reaching a node in flight, which is
612
+ exactly the node anyone is looking at when a run is dragging: a `unit` gate
613
+ missing `--reuse-db` could not be fixed until the run it was costing hours
614
+ had finished. The refusal now reads a narrower set. A node's harness, model,
615
+ pipeline, body and base are each read at a moment that has already passed, so
616
+ changing them is refused as before; a gate's *command* is resolved per gate
617
+ run by every reader of it, so a running phase simply runs the new command at
618
+ its next gate. Nothing downstream is blocked either, because a command moves
619
+ no branch's base. Changing **which** gates a node declares is still refused
620
+ mid-flight — a phase that gained a gate would finish without ever running it —
621
+ so that now counts as part of the phase body.
622
+
623
+ - **A gate result is cached against the command that produced it.** The key was
624
+ `(gate id, tree hash)`, on the assumption that a gate id names a command.
625
+ Amending a live run makes that false, and an unchanged tree would have been
626
+ served the old command's verdict. The command is now part of the key, hashed
627
+ rather than stored — a command line is repository content. A cache database
628
+ written before this is dropped when it is opened: its rows cannot say which
629
+ command produced them, and one re-run per stale entry is the price a lost row
630
+ already costs.
443
631
 
444
632
  - **A session cannot inherit a machine's decision to stop compacting.** All
445
633
  three harnesses compact automatically when their context fills — there was
@@ -548,6 +736,75 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
548
736
 
549
737
  ### Fixed
550
738
 
739
+ - **`vinta-ai-maestro gate` gave up on the slowest gate after five minutes and
740
+ called it a daemon it could not reach.** The command awaited one `fetch`, on
741
+ the stated reasoning that a gate is slow for reasons the client cannot shorten
742
+ and so the client should simply wait — while its own comment claimed it
743
+ imposed no deadline of its own. It imposed 300 seconds, because that is where
744
+ Node's `fetch` abandons a response whose headers have not arrived, and a gate
745
+ that queues for a capacity-1 semaphore and then runs a test suite is routinely
746
+ slower. In a measured fourteen-hour run this fired **33 times, every one of
747
+ them on the `unit` gate and none on any other** — a perfect correlation with
748
+ duration, not a flaky daemon. The gates were running fine and cached green
749
+ results minutes later; only the answer was lost. Told a transport lie about
750
+ work in progress, agents wrote polling loops around a command documented as
751
+ needing none: in one phase, **24 of 108 shell turns were pure waiting, six of
752
+ them burning a full ten-minute ceiling — 60 minutes, 31% of that phase.** The
753
+ wait is a `202` hop loop now, which is what `with` already does, having found
754
+ the same bug the same way and fixed it first.
755
+
756
+ - **A gate command cut short by the harness started the suite over.** An agent
757
+ harness caps how long one command may run, and a gate may exceed any such cap
758
+ — a plan declaring `timeout_s: 2400` is asking for forty minutes against a
759
+ ten-minute budget. So being killed partway is the *ordinary* ending, and what
760
+ an agent does next is run the command again. That used to start a second full
761
+ suite beside the first, queued behind the very semaphore its own predecessor
762
+ was still holding. The daemon now keys a running gate on `(phase, gate id)`
763
+ and the re-invocation attaches to it, which makes re-running the command the
764
+ correct move rather than a wasteful one — worth having, because it is the move
765
+ an agent makes anyway. There is no token to carry, deliberately: a client that
766
+ was killed has nothing in hand, and needing something would defeat the point.
767
+
768
+ - **A pipeline that could not progress was journalled as the word "Error".** The
769
+ interpreter composes an id-safe reason for exactly this case — which state it
770
+ could not leave, and which trigger failed to match — and the scheduler threw
771
+ it as a bare `Error`. `failureReason` reports an unrecognised error by its
772
+ name, so the sentence was discarded at the one moment it was worth keeping.
773
+ It is a `PipelineStuckError` now, carrying the state, and the reason survives
774
+ into the journal. The same shape as the non-zero git exit, fixed the same way.
775
+
776
+ - **A pull request said nothing but the plan anchor.** The daemon opened PRs
777
+ with `title: node.name` and `body: node.prompt_ref` — one line, a link to a
778
+ heading. It had no idea the `prs-context` mechanism existed: there was not a
779
+ single reference to it in the package, so the rich path the skills use
780
+ (`open-pr-from-context`, the project's own PR template, inline review
781
+ comments) was never reached from a run. `open_pr` now reads the phase's
782
+ `.vinta-ai-workflows/prs-context/<plan>/phase-<node>.md` when the agent wrote
783
+ one, and the implementer is asked to write it — the 5–15 line summary it
784
+ already produces, put where a human will read it instead of only the
785
+ transcript. With no context file the body is composed from the run's own
786
+ record: the brief, what the phase is based on and why that is not the default
787
+ branch, its declared surface, its gates, how many attempts it took, and any
788
+ merge conflicts an agent resolved on the way in — which nobody reviews.
789
+
790
+ - **A phase with two dependencies could never open a pull request.** Its base is
791
+ an `integ-<id>` branch and `git_push` pushes only the node's own branch, so
792
+ `gh` was asked to open against a ref the forge had never seen. It refused, the
793
+ refusal was discarded, and the phase completed with no PR and nothing saying
794
+ so — in one observed run, the single node with two dependencies was the single
795
+ node with no PR. The integration base is pushed before the PR is opened, and
796
+ every outcome is journalled as `node_pr`, so a PR that did not open is now as
797
+ visible as one that did.
798
+
799
+ - **A conflict resolution was never gated.** `IntegratorOptions.verify` has
800
+ existed as long as the conflict loop, documented as "a resolution that does
801
+ not build is not a resolution", and nothing ever supplied it. Six merges in
802
+ one run were resolved by an agent and went straight in — ungated — to become
803
+ the base the next phase built on, while the phases either side were gated to
804
+ the hilt. The union of the conflicting nodes' declared gates now runs in the
805
+ integration worktree, with that worktree's environment, and a red gate spends
806
+ a fix round instead of shipping.
807
+
551
808
  - **A conflict fixer that committed its own resolution failed the phase.** An
552
809
  agent told to resolve a merge conflict reaches for the sequence a person
553
810
  would — abort, merge again, resolve, commit — and the orchestrator then
@@ -629,6 +886,35 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
629
886
  actually ran is someone else — so a phase declared for one tier and covered by
630
887
  a higher one offered that higher one as its escalation, and a third of the
631
888
  menu did nothing distinguishable from plain retry.
889
+ - **An amended workflow now reaches the integrator too, and a wave merge is
890
+ pinned to the membership it was decided with.** Fixing the executor without
891
+ fixing the integrator is what made this reachable: `#lastOfWave` decides a
892
+ wave is complete by counting the executor's node set and `mergeWave` decided
893
+ what to merge by reading the integrator's, so while both were stale they
894
+ agreed with each other and nothing showed. One fresh and one stale is a wave
895
+ whose merge silently omits the branch of a phase that ran.
896
+
897
+ The case that gets there is a phase nobody depends on. An amendment is
898
+ refused while a node it *blocks* is in flight, and such a phase blocks
899
+ nothing, so it lands freely beside the wave-mates it will be merged with.
900
+
901
+ A wave's membership is now computed once, synchronously, by the code that
902
+ decides the wave is complete, and handed to the merge — a wave merge runs
903
+ behind the integration worktree's queue and an amendment can land in between,
904
+ so a merge that re-derived its own membership would merge a phase that never
905
+ ran. §9's rebase goes through that same queue now, since "the nodes this
906
+ amendment blocks are idle" says nothing about whether another wave's merge is
907
+ writing in that directory.
908
+
909
+ - **An amended workflow now reaches the code that runs the gate.** `adopt`
910
+ replaced the scheduler's copy of the run's definition and nothing else, while
911
+ the effect executor and the agent-gate broker each held the snapshot they were
912
+ constructed with — and the command a gate actually runs is read from the
913
+ executor's. So an operator who edited a gate command mid-run moved the
914
+ snapshot, the journal and the pool reservations, watched the amendment be
915
+ accepted, and went on getting the old command. All three take the amendment
916
+ now, fanned out from one place, host side before the scheduler so that no
917
+ phase is dispatched against a definition half the system has not seen yet.
632
918
 
633
919
  - **Closing a terminal on a run now records the interruption.** `run` and
634
920
  `serve` handle SIGHUP as well as SIGINT and SIGTERM — SIGHUP is what a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vinta-ai-workflows",
3
- "version": "0.7.0-alpha18",
3
+ "version": "0.7.0-alpha19",
4
4
  "description": "Bootstrap AI tooling (AGENTS.md, skills, sub-agents) across Claude Code, Codex, Cursor, and VS Code + GitHub Copilot.",
5
5
  "type": "module",
6
6
  "bin": {
package/schemas/README.md CHANGED
@@ -14,8 +14,11 @@ JSON Schema (Draft 2020-12) contracts for every YAML or JSON payload the vinta-a
14
14
  | AI model tier table | `ai-tools/skills/plan-feature/resources/ai-models.yaml` (shipped verbatim with the `plan-feature` foundation skill; source: `skills/vinta-derive-skills/resources/foundation-skills/plan-feature/resources/ai-models.yaml`) | [`ai-models.v1.schema.json`](ai-models.v1.schema.json) | nightly `check-ai-models` job ([`scripts/check-ai-models.mjs`](../scripts/check-ai-models.mjs)) → auto-PR | `plan-feature` (per-phase model suggestion) |
15
15
  | Workflow (executable plan) | `ai-plans/YYYY-MM-DD-<feature-kebab>.workflow.json` — **JSON, not YAML** | [`workflow.v1.schema.json`](workflow.v1.schema.json) | `plan-feature` (emitted alongside the human-readable plan) | `vinta-ai-maestro` (the orchestrator daemon) |
16
16
  | Plan post-mortem | `.vinta-ai-maestro/runs/<run-id>/postmortem.json` — **JSON, not YAML**; per-run state beside the frozen workflow, copied to `ai-plans/<feature-kebab>.postmortem.json` when a team wants it committed | [`postmortem.v1.schema.json`](postmortem.v1.schema.json) | `vinta-ai-maestro` (once a run has ended) | `plan-feature` (when planning the next feature in the same repo) |
17
+ | Monitor intervention | not a file the user writes — the document the run's monitor answers with when a watchdog wakes it, recorded per attempt at `.vinta-ai-maestro/runs/<run-id>/interventions.jsonl` | [`intervention.v1.schema.json`](intervention.v1.schema.json) | the run monitor (`vinta-ai-maestro`, when a phase or a gate crosses a threshold) | `vinta-ai-maestro` (validates it, then applies it through §9's amend path) |
17
18
 
18
- > `workflow.v1.schema.json` and `postmortem.v1.schema.json` are the two schemas here that are **generated, not hand-written**. The workflow's source of truth is `packages/vinta-ai-maestro/src/types.ts`; regenerate with `pnpm --filter vinta-ai-maestro schema:gen` and verify with `schema:check`. The post-mortem's is `packages/vinta-ai-maestro/src/postmortem/postmortem.ts`; regenerate with `node --experimental-strip-types src/postmortem/generate.ts` from `packages/vinta-ai-maestro`, verify with the same command plus `--check` — `tests/postmortem.test.ts` runs that byte comparison too. Edit the zod schemas, never the JSON.
19
+ > **Three of these are generated, not hand-written**: `workflow.v1`, `postmortem.v1` and `intervention.v1`. The workflow's source of truth is `packages/vinta-ai-maestro/src/types.ts`; regenerate with `pnpm --filter vinta-ai-maestro schema:gen` and verify with `schema:check`. The post-mortem's is `src/postmortem/postmortem.ts` and the intervention's is `src/intervention/intervention.ts`; regenerate either with `pnpm --filter vinta-ai-maestro <postmortem|intervention>:schema:gen`, verify with `:schema:check` — `tests/postmortem.test.ts` and `tests/intervention.test.ts` run those byte comparisons too. Edit the zod schemas, never the JSON.
20
+ >
21
+ > The intervention schema is the odd one: it validates a document a *model* writes rather than one a skill or a person does, and it is the boundary that decides what an unattended run may change about itself. Its verbs are deliberately few and deliberately cannot express a change to what a phase builds — see [`docs/monitor-intervention.md`](../packages/vinta-ai-maestro/docs/monitor-intervention.md).
19
22
 
20
23
  ## Versioning rules
21
24
 
@@ -0,0 +1,170 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/vintasoftware/vinta-ai-workflows/schemas/intervention.v1.schema.json",
4
+ "title": "vinta-ai-maestro monitor intervention",
5
+ "description": "The monitor’s proposal for a run it was woken up about: a closed set of execution-tuning verbs, applied by the host through §9’s amend path. It cannot express a change to what a phase builds — only to how the run executes it. Written by the run monitor when a watchdog wakes it about a phase that has outrun its threshold, and applied by `vinta-ai-maestro` through §9’s amend path. GENERATED from packages/vinta-ai-maestro/src/intervention/intervention.ts — edit there, not here. See schemas/README.md for versioning rules.",
6
+ "type": "object",
7
+ "properties": {
8
+ "$schema": {
9
+ "description": "Optional URL of this schema, for editor validation. Ignored at runtime.",
10
+ "type": "string"
11
+ },
12
+ "schema_version": {
13
+ "type": "number",
14
+ "const": 1,
15
+ "description": "Schema major version. Bumped only on breaking changes."
16
+ },
17
+ "changes": {
18
+ "maxItems": 4,
19
+ "type": "array",
20
+ "items": {
21
+ "oneOf": [
22
+ {
23
+ "type": "object",
24
+ "properties": {
25
+ "verb": {
26
+ "type": "string",
27
+ "const": "retune_gate"
28
+ },
29
+ "gate": {
30
+ "type": "string",
31
+ "minLength": 1,
32
+ "pattern": "^[a-z0-9][a-z0-9-]*$",
33
+ "description": "The gate id, as the workflow declares it."
34
+ },
35
+ "cmd": {
36
+ "type": "string",
37
+ "minLength": 1,
38
+ "description": "The proposed command. Must be the current command’s argv tokens, in order, plus tokens drawn from this gate’s `tuning.allowed_flags`. Nothing may be removed or rewritten."
39
+ },
40
+ "evidence": {
41
+ "type": "string",
42
+ "minLength": 1,
43
+ "maxLength": 600,
44
+ "description": "What was observed and where — the gate log line, the durations, the transcript. Unverifiable by construction and required regardless: it is what an intervention is audited and scored against, and a change made with nobody watching that cannot say why it was made is not auditable at all."
45
+ }
46
+ },
47
+ "required": [
48
+ "verb",
49
+ "gate",
50
+ "cmd",
51
+ "evidence"
52
+ ],
53
+ "additionalProperties": false
54
+ },
55
+ {
56
+ "type": "object",
57
+ "properties": {
58
+ "verb": {
59
+ "type": "string",
60
+ "const": "retime_gate"
61
+ },
62
+ "gate": {
63
+ "type": "string",
64
+ "minLength": 1,
65
+ "pattern": "^[a-z0-9][a-z0-9-]*$"
66
+ },
67
+ "timeout_s": {
68
+ "type": "integer",
69
+ "minimum": 1,
70
+ "maximum": 9007199254740991,
71
+ "description": "The proposed timeout, in seconds."
72
+ },
73
+ "evidence": {
74
+ "type": "string",
75
+ "minLength": 1,
76
+ "maxLength": 600,
77
+ "description": "What was observed and where — the gate log line, the durations, the transcript. Unverifiable by construction and required regardless: it is what an intervention is audited and scored against, and a change made with nobody watching that cannot say why it was made is not auditable at all."
78
+ }
79
+ },
80
+ "required": [
81
+ "verb",
82
+ "gate",
83
+ "timeout_s",
84
+ "evidence"
85
+ ],
86
+ "additionalProperties": false
87
+ },
88
+ {
89
+ "type": "object",
90
+ "properties": {
91
+ "verb": {
92
+ "type": "string",
93
+ "const": "rebudget_fixes"
94
+ },
95
+ "node": {
96
+ "type": "string",
97
+ "minLength": 1,
98
+ "pattern": "^[a-z0-9][a-z0-9-]*$",
99
+ "description": "The phase id."
100
+ },
101
+ "max_fix_rounds": {
102
+ "type": "integer",
103
+ "minimum": 0,
104
+ "maximum": 10
105
+ },
106
+ "evidence": {
107
+ "type": "string",
108
+ "minLength": 1,
109
+ "maxLength": 600,
110
+ "description": "What was observed and where — the gate log line, the durations, the transcript. Unverifiable by construction and required regardless: it is what an intervention is audited and scored against, and a change made with nobody watching that cannot say why it was made is not auditable at all."
111
+ }
112
+ },
113
+ "required": [
114
+ "verb",
115
+ "node",
116
+ "max_fix_rounds",
117
+ "evidence"
118
+ ],
119
+ "additionalProperties": false
120
+ },
121
+ {
122
+ "type": "object",
123
+ "properties": {
124
+ "verb": {
125
+ "type": "string",
126
+ "const": "retier_phase"
127
+ },
128
+ "node": {
129
+ "type": "string",
130
+ "minLength": 1,
131
+ "pattern": "^[a-z0-9][a-z0-9-]*$"
132
+ },
133
+ "model": {
134
+ "type": "string",
135
+ "minLength": 1,
136
+ "description": "The model to hand this phase. Must already be on the run’s roster — a monitor that could name any string could spend money on a model the plan never staffed, and could name one that does not exist."
137
+ },
138
+ "evidence": {
139
+ "type": "string",
140
+ "minLength": 1,
141
+ "maxLength": 600,
142
+ "description": "What was observed and where — the gate log line, the durations, the transcript. Unverifiable by construction and required regardless: it is what an intervention is audited and scored against, and a change made with nobody watching that cannot say why it was made is not auditable at all."
143
+ }
144
+ },
145
+ "required": [
146
+ "verb",
147
+ "node",
148
+ "model",
149
+ "evidence"
150
+ ],
151
+ "additionalProperties": false
152
+ }
153
+ ]
154
+ },
155
+ "description": "What to change, or nothing. Empty is a real and expected answer: a phase that is slow because its work is hard needs no amendment, and a monitor that cannot say so will invent one."
156
+ },
157
+ "summary": {
158
+ "type": "string",
159
+ "minLength": 1,
160
+ "maxLength": 1000,
161
+ "description": "One paragraph on what was found, whether or not anything is being changed."
162
+ }
163
+ },
164
+ "required": [
165
+ "schema_version",
166
+ "changes",
167
+ "summary"
168
+ ],
169
+ "additionalProperties": false
170
+ }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://github.com/vintasoftware/vinta-ai-workflows/schemas/postmortem.v1.schema.json",
4
4
  "title": "vinta-ai-maestro plan post-mortem",
5
- "description": "What one finished `vinta-ai-maestro` run learned about the plan that produced it: dependencies that were never used, dependencies discovered missing, same-wave phases that conflicted, and phases whose duration diverged from their wave. Ids, waves, paths, durations and counts only. Lives at `.vinta-ai-maestro/runs/<run-id>/postmortem.json` in a target project, emitted by `vinta-ai-maestro` after a run ends and read by `plan-feature` when planning the next feature in the same repo. GENERATED from packages/vinta-ai-maestro/src/postmortem/postmortem.ts — edit there, not here. See schemas/README.md for versioning rules.",
5
+ "description": "What one finished `vinta-ai-maestro` run learned about the plan that produced it: dependencies that were never used, dependencies discovered missing, same-wave phases that conflicted, phases whose duration diverged from their wave, what each gate cost, and changes the run made to its own plan while it ran. Ids, waves, paths, durations and counts only. Lives at `.vinta-ai-maestro/runs/<run-id>/postmortem.json` in a target project, emitted by `vinta-ai-maestro` after a run ends and read by `plan-feature` when planning the next feature in the same repo. GENERATED from packages/vinta-ai-maestro/src/postmortem/postmortem.ts — edit there, not here. See schemas/README.md for versioning rules.",
6
6
  "type": "object",
7
7
  "properties": {
8
8
  "$schema": {
@@ -203,6 +203,73 @@
203
203
  "description": "Two same-wave phases that actually fought over the same code at integration."
204
204
  }
205
205
  },
206
+ "gate_costs": {
207
+ "type": "array",
208
+ "items": {
209
+ "type": "object",
210
+ "properties": {
211
+ "gate": {
212
+ "type": "string",
213
+ "minLength": 1,
214
+ "pattern": "^[a-z0-9][a-z0-9-]*$"
215
+ },
216
+ "runs": {
217
+ "type": "integer",
218
+ "minimum": 1,
219
+ "maximum": 9007199254740991,
220
+ "description": "Verdicts recorded for this gate, over every phase and every fix round."
221
+ },
222
+ "cached_runs": {
223
+ "type": "integer",
224
+ "minimum": 0,
225
+ "maximum": 9007199254740991,
226
+ "description": "Of those, the ones the result cache served. They cost this run nothing."
227
+ },
228
+ "ran_ms": {
229
+ "type": "integer",
230
+ "minimum": 0,
231
+ "maximum": 9007199254740991,
232
+ "description": "Summed runtime of the verdicts the run actually paid for — the runner’s own measurement, not the distance between two journal rows. Cache hits are excluded."
233
+ },
234
+ "slowest_ms": {
235
+ "type": "integer",
236
+ "minimum": 0,
237
+ "maximum": 9007199254740991,
238
+ "description": "The slowest single paid run. What a `timeout_s` is set against."
239
+ },
240
+ "cache_saved_ms": {
241
+ "type": "integer",
242
+ "minimum": 0,
243
+ "maximum": 9007199254740991,
244
+ "description": "Summed runtime of the cache hits: what those gates cost the last time they really ran. An estimate of time saved, never additive with `ran_ms`."
245
+ },
246
+ "failed_runs": {
247
+ "type": "integer",
248
+ "minimum": 0,
249
+ "maximum": 9007199254740991,
250
+ "description": "Verdicts that came back red."
251
+ },
252
+ "timed_out_runs": {
253
+ "type": "integer",
254
+ "minimum": 0,
255
+ "maximum": 9007199254740991,
256
+ "description": "Verdicts killed at `timeout_s`. The most expensive kind of run there is."
257
+ }
258
+ },
259
+ "required": [
260
+ "gate",
261
+ "runs",
262
+ "cached_runs",
263
+ "ran_ms",
264
+ "slowest_ms",
265
+ "cache_saved_ms",
266
+ "failed_runs",
267
+ "timed_out_runs"
268
+ ],
269
+ "additionalProperties": false,
270
+ "description": "What one gate cost the run. A plan is sized against this: the gate that dominates `ran_ms` is the one whose pool capacity decides the wall clock, and a gate whose `runs` far exceeds its phases is a fix loop re-paying for the same suite."
271
+ }
272
+ },
206
273
  "duration_divergences": {
207
274
  "type": "array",
208
275
  "items": {
@@ -254,13 +321,199 @@
254
321
  "additionalProperties": false,
255
322
  "description": "A phase whose real duration was wildly out of line with its wave placement — which is exactly what makes the next plan’s parallelism estimate wrong."
256
323
  }
324
+ },
325
+ "critical_path": {
326
+ "anyOf": [
327
+ {
328
+ "type": "object",
329
+ "properties": {
330
+ "nodes": {
331
+ "type": "array",
332
+ "items": {
333
+ "type": "object",
334
+ "properties": {
335
+ "node": {
336
+ "type": "string",
337
+ "minLength": 1,
338
+ "pattern": "^[a-z0-9][a-z0-9-]*$"
339
+ },
340
+ "wave": {
341
+ "type": "integer",
342
+ "minimum": -9007199254740991,
343
+ "maximum": 9007199254740991
344
+ },
345
+ "span_ms": {
346
+ "type": "integer",
347
+ "minimum": 0,
348
+ "maximum": 9007199254740991
349
+ }
350
+ },
351
+ "required": [
352
+ "node",
353
+ "wave",
354
+ "span_ms"
355
+ ],
356
+ "additionalProperties": false
357
+ },
358
+ "description": "The chain in the order it ran, each with the time it was running."
359
+ },
360
+ "span_ms": {
361
+ "type": "integer",
362
+ "minimum": 0,
363
+ "maximum": 9007199254740991,
364
+ "description": "The chain’s total. The floor this graph could reach."
365
+ },
366
+ "share_of_elapsed": {
367
+ "type": "number",
368
+ "minimum": 0,
369
+ "maximum": 1,
370
+ "description": "How much of the run’s wall clock this one chain accounts for."
371
+ }
372
+ },
373
+ "required": [
374
+ "nodes",
375
+ "span_ms",
376
+ "share_of_elapsed"
377
+ ],
378
+ "additionalProperties": false,
379
+ "description": "The dependency chain that decided how long the run took. A plan is re-drawn against this: shortening any other phase changes nothing, and the only way to a faster run is a shallower graph or a cheaper phase on this list."
380
+ },
381
+ {
382
+ "type": "null"
383
+ }
384
+ ]
385
+ },
386
+ "idle_capacity": {
387
+ "anyOf": [
388
+ {
389
+ "type": "object",
390
+ "properties": {
391
+ "lane_capacity": {
392
+ "type": "integer",
393
+ "minimum": 1,
394
+ "maximum": 9007199254740991,
395
+ "description": "Lanes the plan asked to be provisioned."
396
+ },
397
+ "peak_concurrency": {
398
+ "type": "integer",
399
+ "minimum": 0,
400
+ "maximum": 9007199254740991,
401
+ "description": "The most phases that were ever running at once. The width the graph reached."
402
+ },
403
+ "lane_ms_provisioned": {
404
+ "type": "integer",
405
+ "minimum": 0,
406
+ "maximum": 9007199254740991
407
+ },
408
+ "lane_ms_used": {
409
+ "type": "integer",
410
+ "minimum": 0,
411
+ "maximum": 9007199254740991,
412
+ "description": "Summed phase spans — one lane each, while running."
413
+ },
414
+ "idle_share": {
415
+ "type": "number",
416
+ "minimum": 0,
417
+ "maximum": 1,
418
+ "description": "Provisioned lane time no phase occupied."
419
+ }
420
+ },
421
+ "required": [
422
+ "lane_capacity",
423
+ "peak_concurrency",
424
+ "lane_ms_provisioned",
425
+ "lane_ms_used",
426
+ "idle_share"
427
+ ],
428
+ "additionalProperties": false,
429
+ "description": "What the run paid for and did not use. A lane is a worktree, a forked database and a desk for a crew member, and a plan that is never as wide as its lane count bought all three for nothing — which is a fact about the graph, not about the machine."
430
+ },
431
+ {
432
+ "type": "null"
433
+ }
434
+ ]
435
+ },
436
+ "interventions": {
437
+ "type": "array",
438
+ "items": {
439
+ "type": "object",
440
+ "properties": {
441
+ "amendment": {
442
+ "type": "integer",
443
+ "minimum": 1,
444
+ "maximum": 9007199254740991,
445
+ "description": "The amendment ordinal, matching the run’s `workflow_amended` history."
446
+ },
447
+ "at_ms": {
448
+ "type": "integer",
449
+ "minimum": -9007199254740991,
450
+ "maximum": 9007199254740991,
451
+ "description": "When the run amended itself."
452
+ },
453
+ "target": {
454
+ "type": "string",
455
+ "minLength": 1,
456
+ "description": "What it changed, as the ledger keys it — `gate:<id>` or `node:<id>`."
457
+ },
458
+ "effect": {
459
+ "type": "string",
460
+ "enum": [
461
+ "cheaper",
462
+ "dearer",
463
+ "unchanged",
464
+ "unmeasured"
465
+ ],
466
+ "description": "`cheaper` / `dearer` / `unchanged` compare the gate’s mean uncached duration before and after. `unmeasured` means the journal cannot answer — a phase-level change, or a gate with no runs on one side of the amendment."
467
+ },
468
+ "before_ms": {
469
+ "description": "Mean uncached duration of this gate before the amendment. Absent when unmeasured.",
470
+ "type": "integer",
471
+ "minimum": -9007199254740991,
472
+ "maximum": 9007199254740991
473
+ },
474
+ "after_ms": {
475
+ "description": "The same, after. Absent when unmeasured.",
476
+ "type": "integer",
477
+ "minimum": -9007199254740991,
478
+ "maximum": 9007199254740991
479
+ },
480
+ "runs_before": {
481
+ "type": "integer",
482
+ "minimum": 0,
483
+ "maximum": 9007199254740991
484
+ },
485
+ "runs_after": {
486
+ "type": "integer",
487
+ "minimum": 0,
488
+ "maximum": 9007199254740991
489
+ },
490
+ "record_ref": {
491
+ "type": "string",
492
+ "description": "Where the monitor’s own account of this change lives, relative to the run directory. The reasoning, the evidence and the command are prose about a repository and stay there; this artifact carries what can be counted."
493
+ }
494
+ },
495
+ "required": [
496
+ "amendment",
497
+ "at_ms",
498
+ "target",
499
+ "effect",
500
+ "record_ref"
501
+ ],
502
+ "additionalProperties": false,
503
+ "description": "One amendment a run made to itself, and what happened to the thing it changed. The effect is a comparison of measured durations, not a claim of cause: a gate that got cheaper across an amendment did so while other things were also changing."
504
+ },
505
+ "description": "Amendments the run made to itself (§9.2), oldest first. Empty means it made none, which is a fact rather than a gap: `workflow_amended` rows say who wrote them."
257
506
  }
258
507
  },
259
508
  "required": [
260
509
  "unused_dependencies",
261
510
  "missing_dependencies",
262
511
  "wave_conflicts",
263
- "duration_divergences"
512
+ "gate_costs",
513
+ "duration_divergences",
514
+ "critical_path",
515
+ "idle_capacity",
516
+ "interventions"
264
517
  ],
265
518
  "additionalProperties": false
266
519
  },
@@ -272,9 +525,12 @@
272
525
  "kind": {
273
526
  "type": "string",
274
527
  "enum": [
528
+ "blocking_cause_unrecorded",
275
529
  "dependency_use_unrecorded",
276
530
  "gate_result_unrecorded",
277
- "integration_record_unavailable"
531
+ "gate_durations_unrecorded",
532
+ "integration_record_unavailable",
533
+ "intervention_effect_unmeasured"
278
534
  ]
279
535
  },
280
536
  "needs": {
@@ -316,6 +572,15 @@
316
572
  "minLength": 1,
317
573
  "pattern": "^[a-z0-9][a-z0-9-]*$"
318
574
  }
575
+ },
576
+ "gates": {
577
+ "description": "The gates the gap applies to.",
578
+ "type": "array",
579
+ "items": {
580
+ "type": "string",
581
+ "minLength": 1,
582
+ "pattern": "^[a-z0-9][a-z0-9-]*$"
583
+ }
319
584
  }
320
585
  },
321
586
  "required": [
@@ -368,6 +368,16 @@
368
368
  "minLength": 1,
369
369
  "pattern": "^[a-z0-9][a-z0-9-]*$"
370
370
  },
371
+ "chores": {
372
+ "default": [],
373
+ "description": "The chores every phase runs unless it names its own. Most chores are run-wide — a comment pass, a changelog entry — and repeating them on every node is how one phase ends up quietly missing one.",
374
+ "type": "array",
375
+ "items": {
376
+ "type": "string",
377
+ "minLength": 1,
378
+ "pattern": "^[a-z0-9][a-z0-9-]*$"
379
+ }
380
+ },
371
381
  "max_session_turns": {
372
382
  "default": 12,
373
383
  "description": "Turns one reused session slot may take before the next spawn starts fresh (§15.5). A shared session only ever grows; without a ceiling a long node eventually dies on a context-window error that reads as a broken harness.",
@@ -504,6 +514,25 @@
504
514
  "minimum": 1,
505
515
  "maximum": 9007199254740991
506
516
  },
517
+ "tuning": {
518
+ "type": "object",
519
+ "properties": {
520
+ "allowed_flags": {
521
+ "minItems": 1,
522
+ "type": "array",
523
+ "items": {
524
+ "type": "string",
525
+ "minLength": 1
526
+ },
527
+ "description": "Flags the monitor may ADD to this gate’s command, verbatim and whole. Matched as exact argv tokens, so `--reuse-db` permits `--reuse-db` and nothing else — not `--reuse-db-and-more`, and not a flag that merely starts the same way. A flag taking a separate value (`-n auto`) is written as the two tokens it is. Removing or rewriting an existing token is never permitted, whatever is listed here: the proposed command must be this one’s tokens, in order, plus additions."
528
+ }
529
+ },
530
+ "required": [
531
+ "allowed_flags"
532
+ ],
533
+ "additionalProperties": false,
534
+ "description": "What an autonomous retune of this gate may do. Absent — the default — means the command is not the monitor’s to touch."
535
+ },
507
536
  "description": {
508
537
  "type": "string"
509
538
  }
@@ -514,6 +543,60 @@
514
543
  "additionalProperties": false
515
544
  }
516
545
  },
546
+ "chores": {
547
+ "default": {},
548
+ "description": "Agent turns a phase runs beside its gates, by id. A gate judges and a chore changes the tree, so they are separate registries — see ChoreSchema.",
549
+ "type": "object",
550
+ "propertyNames": {
551
+ "type": "string",
552
+ "minLength": 1,
553
+ "pattern": "^[a-z0-9][a-z0-9-]*$"
554
+ },
555
+ "additionalProperties": {
556
+ "type": "object",
557
+ "properties": {
558
+ "prompt": {
559
+ "description": "The instruction, inline. Exactly one of `prompt` and `prompt_ref` is required.",
560
+ "type": "string",
561
+ "minLength": 1
562
+ },
563
+ "prompt_ref": {
564
+ "description": "Where the instruction lives, in `prompt_ref` form (`ai-plans/PLAN.md#deslop`), resolved by the same resolver as a phase brief. Prefer it over `prompt` for anything longer than a sentence: it keeps the text reviewable in the plan rather than in a JSON string.",
565
+ "type": "string",
566
+ "minLength": 1
567
+ },
568
+ "skill": {
569
+ "description": "A skill the agent should use, named in the prompt rather than passed as a harness flag — codex and opencode have no skills, and a chore that only works on one harness is a chore that silently does nothing on the other two.",
570
+ "type": "string",
571
+ "minLength": 1
572
+ },
573
+ "session": {
574
+ "default": "main",
575
+ "description": "The session slot this turn continues (§15). `main` — the default — is the implementer’s: the agent that wrote the phase already holds the brief, the diff and its own reasons, and that is most of what makes a chore cheap. A slot no other effect names starts a fresh session every time and pays for a cold context.",
576
+ "type": "string",
577
+ "minLength": 1
578
+ },
579
+ "model": {
580
+ "description": "Overrides the model the phase’s implementer would otherwise run at. A mechanical pass does not need the tier the phase was staffed at.",
581
+ "type": "string",
582
+ "minLength": 1
583
+ },
584
+ "on_failure": {
585
+ "default": "continue",
586
+ "description": "`continue` — the default — journals a failed chore and moves on to the gates. A chore is polish, and losing an implemented phase to one that timed out is the worse trade. `fail` is for a chore whose output the phase is not correct without.",
587
+ "type": "string",
588
+ "enum": [
589
+ "continue",
590
+ "fail"
591
+ ]
592
+ },
593
+ "description": {
594
+ "type": "string"
595
+ }
596
+ },
597
+ "additionalProperties": false
598
+ }
599
+ },
517
600
  "nodes": {
518
601
  "minItems": 1,
519
602
  "type": "array",
@@ -583,6 +666,15 @@
583
666
  "pattern": "^[a-z0-9][a-z0-9-]*$"
584
667
  }
585
668
  },
669
+ "chores": {
670
+ "description": "The chores this phase runs, in order. Optional rather than defaulted, and that is the whole of its override rule: absent takes `defaults.chores`, and a list — `[]` included — replaces it. Merging instead would leave no way to skip a run-wide chore on the one phase it makes no sense for.",
671
+ "type": "array",
672
+ "items": {
673
+ "type": "string",
674
+ "minLength": 1,
675
+ "pattern": "^[a-z0-9][a-z0-9-]*$"
676
+ }
677
+ },
586
678
  "harness": {
587
679
  "description": "Overrides defaults.harness.",
588
680
  "type": "string",
@@ -680,6 +772,7 @@
680
772
  "enum": [
681
773
  "spawn_agent",
682
774
  "run_gate",
775
+ "run_chore",
683
776
  "git_branch",
684
777
  "git_merge",
685
778
  "git_push",
@@ -744,6 +837,7 @@
744
837
  "enum": [
745
838
  "spawn_agent",
746
839
  "run_gate",
840
+ "run_chore",
747
841
  "git_branch",
748
842
  "git_merge",
749
843
  "git_push",
@@ -864,6 +958,7 @@
864
958
  "enum": [
865
959
  "spawn_agent",
866
960
  "run_gate",
961
+ "run_chore",
867
962
  "git_branch",
868
963
  "git_merge",
869
964
  "git_push",
@@ -274,6 +274,10 @@ Each file carries `findings` and `gaps`. Use them like this:
274
274
  - **`wave_conflicts`** — two same-wave phases that actually fought, with the contested `paths`. Cross-check those paths against this plan's **Touch List**: two phases of yours touching one of them in the same wave is the same defect repeating. Add the edge, or split so only one phase owns the file.
275
275
  - **`duration_divergences`** — `direction: "longer"` means that phase set its wave's wall clock alone, so every peer you parallelised it with bought nothing; keep comparable-size work together and let the long pole start in wave 1. `"shorter"` means a small phase sat behind a long one and could have been folded in or moved earlier. Sizing, never time estimates in the plan body.
276
276
  - **`unused_dependencies`** — edges the run proved nobody needed. Drop the equivalent edge here. **Empty is not evidence of a tight graph**: check `gaps` first, because a `dependency_use_unrecorded` entry means dependency use was never measured on that run, not that every edge earned its place.
277
+ - **`gate_costs`** — what each gate actually cost, per gate rather than per phase: `runs`, `cached_runs`, `ran_ms`, `slowest_ms`, `cache_saved_ms`, and the failed and timed-out counts. This is what sizes the **gate pool capacities** you declare. The gate that dominates `ran_ms` is the one whose pool decides the run's wall clock — give that one the capacity and stop widening the others. A gate whose `runs` far exceeds the number of phases that declare it is a fix loop re-paying for the same suite: that is a signal about `max_fix_rounds` or about phases too large to get right in two rounds, not about the gate.
278
+ - **`critical_path`** — the dependency chain that decided how long the run took, each phase with the time it was running, plus `span_ms` and `share_of_elapsed`. **Re-draw the graph against this one.** Shortening any phase *not* on this list changes nothing at all, so a plan that parallelises harder without touching this chain buys nothing. A high `share_of_elapsed` means the graph is deep rather than wide: look for an edge on that chain that is convention rather than a real artifact dependency, and cut it. This is the finding most likely to change a plan's shape.
279
+ - **`idle_capacity`** — what the run paid for and did not use: `lane_capacity` against `peak_concurrency`, and `idle_share` of the provisioned lane time. A lane is a worktree, a forked database and a crew member's desk, so a plan that never got as wide as its lane count bought all three for nothing. `peak_concurrency` well under `lane_capacity` means **this graph cannot use that many lanes** — either declare fewer next time, or find the edges that are serialising work that did not need to be serial. It is a fact about the graph, not about the machine.
280
+ - **`interventions`** — changes the run made to *itself* while it ran, because a watchdog found it paying for something the plan could have stopped it paying for. A `gate:<id>` entry with `effect: "cheaper"` is the useful one: that gate's command or timeout was wrong in the workflow this project keeps emitting, and the run had to discover it at its own expense. **Carry the change forward** — the gate block you emit for this feature should already have it, so the next run does not pay again. The reasoning and the actual command are not in this file (it carries counts, not prose); they are in `interventions.jsonl` beside it in the run directory, which is where to look before copying anything. An `effect: "dearer"` entry is the opposite signal and is worth saying out loud to the human: the run tuned itself in the wrong direction, and whatever it changed should probably not be in the next plan. `unmeasured` means exactly that — the change may have been right or wrong and the run could not tell, so treat it as no evidence rather than as weak evidence.
277
281
 
278
282
  Rules for using them:
279
283
 
@@ -603,6 +607,9 @@ First key in the file is `"$schema"`, pointing at `https://github.com/vintasoftw
603
607
  | `resources.lane` | `{"capacity": N, "kind": "worktree"}`. **Required** — a lane pool is where phases are dispatched, and a workflow without one has nowhere to run. `N` = the number of **implementers** on the roster, because each keeps one worktree for the whole run. Reviewers add nothing: they read the lane under review. How many run *at once* is still capped by the graph and by the project's parallel-lane budget (3 when unstated); an implementer idle in wave 1 still has a desk. |
604
608
  | `resources.<pool>` | One `{"kind": "semaphore"}` pool per expensive shared thing a gate contends for — the test database, the e2e browser grid, a staging deploy slot. `capacity: 1` when only one can run at a time. |
605
609
  | `gates.<id>` | The checks a phase must pass, as **shell commands run in the phase's lane** — the project's real typecheck / test / lint invocations, not an agent and not prose. Give the slow ones `requires` naming the pool they contend for, and a `timeout_s` that is generous rather than tight. |
610
+ | `chores.<id>` | Agent turns a phase runs beside its gates, for work that changes the diff rather than judging it. **Always emit the `deslop` chore below** — it is the comment-hygiene pass, and without it a maestro run has none. Add another only when the plan genuinely needs one (a changelog entry, a translation extraction); a chore is a model turn per phase, so each one has to earn it. |
611
+ | `defaults.chores` | `["deslop"]`. The chores every phase runs unless it names its own. |
612
+ | `nodes[].chores` | **Omit** on almost every phase — absent means the run-wide default. Name a list only to give one phase a different set, and `[]` to opt one out. A list *replaces* the default rather than adding to it. |
606
613
  | `nodes[]` | One per phase, in plan order. |
607
614
  | `nodes[].id` | `p` + the phase number, lowercased: `Phase 1` → `p1`, `Phase 4a` → `p4a`, `Phase 1b` → `p1b`. |
608
615
  | `nodes[].name` | The phase title without its `Phase N —` prefix. |
@@ -622,6 +629,7 @@ Rules the mapping depends on:
622
629
  - **`touches` is what the same-wave overlap check reads.** Executors *warn* on two same-wave nodes declaring the same path rather than refusing, so an incomplete Touch List doesn't fail loudly — it fails at merge. Transcribe every file the phase creates or edits, including tests.
623
630
  - **Never invent a model id.** Pick the tier from the rubric under "Staff the plan", then read the id out of [resources/ai-models.yaml](resources/ai-models.yaml). Ids drift; tiers don't.
624
631
  - **The roster is the same decision as the Crew table.** `crew` transcribes it: one entry per row, `tier` from the Tier column, `model` from that tier in `ai-models.yaml`. A member the table does not list, or a table row with no `crew` entry, means the two were edited separately.
632
+ - **A gate is a command; a chore is an agent.** Both run per phase and that is where the resemblance stops. A gate is a shell line that says pass or fail and decides whether the phase merges. A chore is a turn that *changes* the diff — the comment pass, a changelog entry — and it is never what stands between a phase and its merge: one that fails is recorded and the phase carries on to its gates. Anything you can express as a command belongs in `gates`, where it is cached and queued and costs no model time.
625
633
  - **Gate commands must be commands the repo actually runs today.** Read them out of the project's task runner (`package.json` scripts, `Makefile`, `pyproject.toml`, CI config) rather than guessing a conventional one. A gate that doesn't exist fails every phase identically, and looks like a code problem.
626
634
  - **The graph must agree with the Execution graph table.** Same nodes, same edges, same waves — they are two renderings of one set of `**Depends on**:` lines, so derive both from the lines rather than transcribing one from the other. A disagreement means one was hand-edited, and the executor flags it.
627
635
  - **`plan_context_refs` is anchors, never prose.** It names sections of the plan; it never restates them. A summary written into the JSON is a second copy that drifts the first time someone edits the plan, and the whole point of the field is that the implementer reads what the plan actually says.
@@ -815,7 +823,10 @@ and this `ai-plans/2026-03-04-bookmark-folders.workflow.json`:
815
823
  "defaults": {
816
824
  "harness": "claude-code",
817
825
  "model": "claude-sonnet-5",
818
- "pipeline": "standard-phase"
826
+ "pipeline": "standard-phase",
827
+ "chores": [
828
+ "deslop"
829
+ ]
819
830
  },
820
831
  "resources": {
821
832
  "lane": {
@@ -842,6 +853,13 @@ and this `ai-plans/2026-03-04-bookmark-folders.workflow.json`:
842
853
  "timeout_s": 1800
843
854
  }
844
855
  },
856
+ "chores": {
857
+ "deslop": {
858
+ "skill": "deslop-comments",
859
+ "prompt": "Rewrite the comments and doc blocks this phase wrote into Simple English, and delete the ones that should not be there. Comment-only: no renames, no logic changes.",
860
+ "description": "The comment-hygiene pass, on the diff that is about to merge."
861
+ }
862
+ },
845
863
  "nodes": [
846
864
  {
847
865
  "id": "p1",
@@ -1041,4 +1059,5 @@ When in doubt, model the plan after a recent example in `ai-plans/` — look for
1041
1059
  - [ ] **`project` decided, not defaulted** — asked via `AskUserQuestion`, then either written (roles `dev` / `test`, each naming its own database, engine fields filled from the project, `migrate_cmd` read out of its task runner) or deliberately omitted because lanes share the main checkout's database. No `reset_cmd`, no compose project name, no seed command, no env-file strategy — those are the worktree's, not the plan's.
1042
1060
  - [ ] No credential anywhere in the workflow file: `connection_url_var` is a variable name, and `server_url` is a host and port.
1043
1061
  - [ ] Model ids come from [resources/ai-models.yaml](resources/ai-models.yaml) and appear **only** in the `crew` block — no node carries a `model`, and `crew` transcribes the Crew table row for row.
1062
+ - [ ] `chores` declares `deslop` and `defaults.chores` names it, so every phase's diff gets the comment pass before it is gated. No node carries its own `chores` unless that phase genuinely needs a different set.
1044
1063
  - [ ] `pipelines` is omitted — `defaults.pipeline: standard-phase` is enough, and the executor supplies it.