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 +286 -0
- package/package.json +1 -1
- package/schemas/README.md +4 -1
- package/schemas/intervention.v1.schema.json +170 -0
- package/schemas/postmortem.v1.schema.json +268 -3
- package/schemas/workflow.v1.schema.json +95 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/SKILL.md +20 -1
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
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
|
-
>
|
|
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,
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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.
|