vinta-ai-workflows 0.7.0-alpha27 → 0.7.0-alpha29
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 +155 -0
- package/package.json +1 -1
- package/schemas/README.md +1 -3
- package/schemas/postmortem.v1.schema.json +337 -2
- package/schemas/vinta-ai-workflows-config.v1.schema.json +19 -1
- package/schemas/workflow.v1.schema.json +45 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/SKILL.md +17 -6
- package/schemas/intervention.v1.schema.json +0 -170
package/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,32 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
30
30
|
|
|
31
31
|
### Added
|
|
32
32
|
|
|
33
|
+
- **`vinta-ai-maestro`: `run` now shows what a slow start is doing.** Until
|
|
34
|
+
now `run` printed "starting in the background…" and nothing else until the
|
|
35
|
+
run was up, which can take minutes (`prepare_cmd`, preflight, database
|
|
36
|
+
templates, lane provisioning). The job now prints a timestamped line per
|
|
37
|
+
step — plan branch, `prepare_cmd`, environment checks, each database
|
|
38
|
+
template, each lane ready (`3/5`) — and the launching `run` streams the
|
|
39
|
+
job's log to the terminal as it is written, with a "still starting…" line
|
|
40
|
+
after 15 s of quiet. Lines carry names and counts only, never command
|
|
41
|
+
output. A refused start is no longer printed twice.
|
|
42
|
+
|
|
43
|
+
- **`vinta-ai-maestro`: `createdb_cmd` / `dropdb_cmd` on a Postgres
|
|
44
|
+
`external` database** (`workflow.v1.schema.json`, optional, defaults
|
|
45
|
+
`createdb` / `dropdb`). They replace only the binary name — in the template
|
|
46
|
+
setup, the per-lane clone and the reset — so a project whose Postgres is a
|
|
47
|
+
container with a password the host cannot supply can set
|
|
48
|
+
`docker compose exec -T db createdb`. `-h/-p/-U` from `server_url` still
|
|
49
|
+
follow. Existing workflows are unchanged.
|
|
50
|
+
|
|
51
|
+
- **`vinta-ai-maestro`: a database password prompt no longer hangs `run`.**
|
|
52
|
+
Template, clone and reset commands now run with stdin closed, no controlling
|
|
53
|
+
terminal and a two-minute timeout that ends the whole process tree; a prompt
|
|
54
|
+
becomes a failed run carrying the command's stderr instead of a job stuck at
|
|
55
|
+
`starting`. `doctor` now runs the effective `dropdb_cmd --if-exists` against
|
|
56
|
+
a name that cannot exist and `FAIL`s, naming the missing password, when it
|
|
57
|
+
cannot authenticate — a server that merely answers used to pass.
|
|
58
|
+
|
|
33
59
|
- **New optional foundation skill: `interview-ui`** — run decision-heavy
|
|
34
60
|
interviews as a full-screen browser form instead of a chat exchange. One
|
|
35
61
|
decision per screen with the context the agent already knows, Markdown +
|
|
@@ -658,6 +684,13 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
658
684
|
|
|
659
685
|
### Removed
|
|
660
686
|
|
|
687
|
+
- **Maestro: the monitor's intervention documents.** The watchdog no longer
|
|
688
|
+
asks the monitor for a JSON proposal of four verbs; the coordinator acts
|
|
689
|
+
through the run's API under the same "how, never what" rule, enforced on
|
|
690
|
+
every amendment it makes. `schemas/intervention.v1.schema.json`, the
|
|
691
|
+
`intervention:schema:*` scripts and `runs/<id>/interventions.jsonl` are
|
|
692
|
+
gone; the post-mortem's `interventions` now point at the coordinator's
|
|
693
|
+
conversation.
|
|
661
694
|
- **The three-layer phase review.** `review-phase` no longer runs the
|
|
662
695
|
mechanical checks (including the review-time secret scan, dependency-license
|
|
663
696
|
check and co-author trailer check), the plan-compliance walkthrough
|
|
@@ -679,8 +712,117 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
679
712
|
reviewer members and every `role`, rename `before_gate` to `after_review`,
|
|
680
713
|
delete `reviewer_model`, and add the `review` chore.
|
|
681
714
|
|
|
715
|
+
### Added
|
|
716
|
+
|
|
717
|
+
- **Maestro: `land` and `propagate`.** `land <run-id>` says how a finished
|
|
718
|
+
run lands — the plan PR, with a merge commit, and nothing else — lists every
|
|
719
|
+
phase and integration PR with its state, and marks the ones whose work is
|
|
720
|
+
already in the base (every commit they add is a merge of commits the base
|
|
721
|
+
has, such as a GitHub "Update branch"); `--close` closes those with a
|
|
722
|
+
comment. `propagate <run-id> <phase>` carries a fix made on a phase branch
|
|
723
|
+
after the run into every `integ-*` and `wave-*` branch that contains it,
|
|
724
|
+
keeping the wave chain, and stops at a conflict with the branch untouched.
|
|
725
|
+
- **Maestro: `defaults.substitution`** (`same_tier` | `up_one_tier` | `any`,
|
|
726
|
+
additive, default `up_one_tier`). A busy member's phase may go at most to
|
|
727
|
+
the next tier the roster has, and a warm session no longer moves a phase up
|
|
728
|
+
a tier unless `any`. **Behavior change:** runs that relied on warm-session
|
|
729
|
+
promotion to a higher tier now stay at the planned tier; set `any` to keep
|
|
730
|
+
it. Substitutions above plan are shown in `status`, the run view and the
|
|
731
|
+
post-mortem.
|
|
732
|
+
- **Maestro: post-mortem findings** `failure_causes`, `crew_substitutions`,
|
|
733
|
+
`operations` and `cross_phase_failures`, and `wave_conflicts[].where`
|
|
734
|
+
(additive). `plan-feature` reads them.
|
|
735
|
+
- **Maestro: the run coordinator.** The run's monitor becomes its
|
|
736
|
+
coordinator: one agent per run that is woken when a phase fails, an attempt
|
|
737
|
+
errors, maestro logs an error about itself, a phase waits 20 minutes for
|
|
738
|
+
the integration worktree, or a phase or gate crosses its cost threshold, and
|
|
739
|
+
that can act: steer, redirect, pause, abort or retry a phase, answer a
|
|
740
|
+
question a `--retry-after` timer could answer, amend the live run, and run
|
|
741
|
+
commands in a lane or the integration worktree. It reaches the run with a
|
|
742
|
+
token of its own, everything it does is journalled as its own, and the run
|
|
743
|
+
refuses what it may not do: change what the plan builds, weaken a check
|
|
744
|
+
(drop a gate or chore, rewrite a gate command beyond its
|
|
745
|
+
`tuning.allowed_flags`, edit a judge gate, change `hooks`, lower
|
|
746
|
+
`wave_gates`), answer a question the operator holds, or halt the run. Woken
|
|
747
|
+
at most 24 times per run; `--no-coordinator` turns the wakes off
|
|
748
|
+
(`--no-intervene` still works).
|
|
749
|
+
- **Maestro: `node`, `workflow`, `amend` and `exec` commands.** Steer one
|
|
750
|
+
phase (`context`, `redirect`, `pause`, `abort`, `retry`, `answer`), print or
|
|
751
|
+
amend a live run's workflow, and run a command in a lane or the integration
|
|
752
|
+
worktree with that worktree's environment — its database, compose project
|
|
753
|
+
and ports — from a terminal. `exec … integration` holds the integration
|
|
754
|
+
worktree while the command runs, so no merge or unattended retry checks a
|
|
755
|
+
branch out underneath a manual repair.
|
|
756
|
+
- **Maestro: `defaults.wave_gates`** (`off` | `final` | `every`, additive,
|
|
757
|
+
default `final`). The phases' gates now run in full on the merged tree:
|
|
758
|
+
every gate of the plan on the last wave before the plan PR opens, and a
|
|
759
|
+
wave's gates on any wave whose merge needed a conflict resolution; `every`
|
|
760
|
+
checks every wave. Merged waves were never tested under `gate_scope: full`,
|
|
761
|
+
and two parallel phases that each passed their gates disagreed on a signal's
|
|
762
|
+
keyword arguments, failing 22 tests that only showed up after the plan PR.
|
|
763
|
+
`plan-feature` now puts a contract test for any interface two parallel
|
|
764
|
+
phases share into the foundation phase.
|
|
765
|
+
- **Maestro: pausing a phase parked on a question holds it.** Under
|
|
766
|
+
`--retry-after` the unattended timer no longer answers a question the
|
|
767
|
+
operator paused; a person's answer releases it, and `status` and the node
|
|
768
|
+
view say "paused by operator". The pause used to be recorded as `ignored`
|
|
769
|
+
while the timer fired anyway. Node operations now answer with the delivery
|
|
770
|
+
they recorded, so `POST …/pause` on a node with nothing to pause reports
|
|
771
|
+
`"delivery": "ignored"` instead of a bare `ok`.
|
|
772
|
+
- **Maestro: `nodes[].deferred`** (additive). A phase carrying it — the
|
|
773
|
+
soak-gated flag removal `plan-feature` now emits it on — is not dispatched
|
|
774
|
+
when its dependencies merge; it parks on a question naming the reason, and
|
|
775
|
+
the operator answers `start` or `stop`. The scheduler used to run such a
|
|
776
|
+
phase immediately.
|
|
777
|
+
- **Maestro: `node_wait` and the "waiting for the integration worktree"
|
|
778
|
+
state.** A phase queued behind the integration worktree journals who holds
|
|
779
|
+
it; `status` and the node view show it. It used to read `running` with no
|
|
780
|
+
agent process, which looked like a frozen run.
|
|
781
|
+
- **Maestro: `interrupted` run status.** A signal to the job now kills every
|
|
782
|
+
live agent turn and gate, the fix round in progress and any merge standing
|
|
783
|
+
in the integration worktree, and ends the run `interrupted` (resumable)
|
|
784
|
+
instead of `failed` with orphaned agents still editing the worktree. The
|
|
785
|
+
daemon's close is bounded so an open browser socket cannot hang shutdown.
|
|
786
|
+
- **Maestro: `doctor` warns about `docker compose run`/`exec` without `-T`**
|
|
787
|
+
in gates and the `project` block, and `plan-feature` says so.
|
|
788
|
+
|
|
682
789
|
### Fixed
|
|
683
790
|
|
|
791
|
+
- **Maestro: PRs say how the plan lands.** Every phase and integration PR is
|
|
792
|
+
marked as a review unit that lands through the plan PR, and the plan PR says
|
|
793
|
+
to merge it alone; it used to offer two landing paths, and a 36-PR landing
|
|
794
|
+
resolved the same conflicts several times by hand. A phase PR also names the
|
|
795
|
+
sibling phases its base leaves out, so a CI failure in their area is not
|
|
796
|
+
read as its own.
|
|
797
|
+
- **Maestro: the post-mortem reports what happened.** Conflicts are read from
|
|
798
|
+
the journal, base merges included, so a run that restarted no longer
|
|
799
|
+
reports none; amendments and operator operations, failure causes, crew
|
|
800
|
+
substitutions and cross-phase failures on merged waves are recorded.
|
|
801
|
+
- **Maestro: a finished run stops its compose stacks** (volumes kept) and
|
|
802
|
+
prints how to remove its lanes and volumes; `status` and `doctor` list what
|
|
803
|
+
runs that are over left running or on disk. One run left twenty containers,
|
|
804
|
+
five worktrees and a job process up a day and a half after it was done.
|
|
805
|
+
- **Maestro: wave branches are built across a restart.** Wave completion was
|
|
806
|
+
counted in memory, so a run that restarted with some phases of a wave merged
|
|
807
|
+
before and the rest after never built that wave, and the next wave failed on
|
|
808
|
+
`git checkout` (exit 128) on every retry. A phase the journal calls `done`
|
|
809
|
+
now counts as arrived; a wave the run never built is built before the wave
|
|
810
|
+
on top of it, and a wave that completes before the one under it waits for
|
|
811
|
+
it (`wave_deferred`) instead of failing. A missing wave is named
|
|
812
|
+
("wave 5 was never built") rather than left to git. New `wave_built` event.
|
|
813
|
+
- **Maestro: a phase that moved lanes can check out its branch.** The branch
|
|
814
|
+
is released from the sibling worktree that still had it (detached at its
|
|
815
|
+
commit, tree untouched) before the new lane takes it. Git's "already used
|
|
816
|
+
by worktree" refusal failed one phase 39 attempts in a row.
|
|
817
|
+
- **Maestro: a conflict-fixer round has a time limit** (20 minutes by
|
|
818
|
+
default) and is told to verify only the files it touched; one round spent
|
|
819
|
+
hours re-running a suite that could not reach its database while three
|
|
820
|
+
phases queued behind it.
|
|
821
|
+
- **Maestro: recreated lanes read the amended `env_files`.** The pool takes
|
|
822
|
+
the amended `project` block, so a single-use lane re-provisioned mid-run no
|
|
823
|
+
longer loses files declared after the run started. A compose-delivered
|
|
824
|
+
database whose service is in `compose.publish` also gets
|
|
825
|
+
`<connection_url_var>_HOST`, the same URL on the published host port.
|
|
684
826
|
- **Maestro: a merge commit git refuses no longer leaves the integration
|
|
685
827
|
worktree mid-merge.** A pre-commit hook failing in the integration worktree
|
|
686
828
|
left `MERGE_HEAD` behind, and every phase whose base was built there then
|
|
@@ -2574,6 +2716,19 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
2574
2716
|
nothing produces a merge commit full of markers that passes into the wave
|
|
2575
2717
|
branch unnoticed.
|
|
2576
2718
|
|
|
2719
|
+
### Security
|
|
2720
|
+
|
|
2721
|
+
- **Maestro: lane agents hold a token of their own.** A run's agents used to
|
|
2722
|
+
reach its API with the operator's token (`VINTA_AI_MAESTRO_TOKEN`), so an
|
|
2723
|
+
agent could steer, abort, amend, `exec` in, pause or stop its own run, or
|
|
2724
|
+
save a workflow from the editor, as the operator. They now get an agent
|
|
2725
|
+
token that reaches only their own leases (`with`), `gate`, and the
|
|
2726
|
+
permission-judge and gate-guard hooks; every other route answers
|
|
2727
|
+
`403 agent_forbidden`. It does not open the WebSocket (and so no PTY
|
|
2728
|
+
takeover), `ui` never forwards it to a run's job, and it is redacted from
|
|
2729
|
+
the logs like the other tokens. Nothing to migrate: lanes are handed the new
|
|
2730
|
+
token when a run starts or resumes.
|
|
2731
|
+
|
|
2577
2732
|
> The repository also gained `packages/vinta-ai-maestro/`, the workspace package
|
|
2578
2733
|
> that executes these workflows. It is published as its own npm package,
|
|
2579
2734
|
> `vinta-ai-maestro`, carrying the same version as `vinta-ai-workflows`, and is
|
package/package.json
CHANGED
package/schemas/README.md
CHANGED
|
@@ -17,11 +17,9 @@ JSON Schema (Draft 2020-12) contracts for every YAML or JSON payload the vinta-a
|
|
|
17
17
|
| 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) |
|
|
18
18
|
| 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) |
|
|
19
19
|
| Plan review | `ai-plans/YYYY-MM-DD-<feature-kebab>.review.json` — **JSON, not YAML**; beside the plan and its workflow, committed with them | [`plan-review.v1.schema.json`](plan-review.v1.schema.json) | the `vinta-ai-maestro` review page (a person's comments, messages, approval) and `vinta-ai-maestro review reply` (the agent's answers) | `plan-feature`, through `vinta-ai-maestro review wait` |
|
|
20
|
-
| 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) |
|
|
21
20
|
|
|
22
|
-
> **
|
|
21
|
+
> **Three of these are generated, not hand-written**: `workflow.v1`, `postmortem.v1` and `plan-review.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 plan review's is `src/review/document.ts`; regenerate either with `pnpm --filter vinta-ai-maestro <postmortem|review>:schema:gen`, verify with `:schema:check` — `tests/postmortem.test.ts` and `tests/review.test.ts` run those byte comparisons too. Edit the zod schemas, never the JSON.
|
|
23
22
|
>
|
|
24
|
-
> 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).
|
|
25
23
|
|
|
26
24
|
## Versioning rules
|
|
27
25
|
|
|
@@ -169,6 +169,14 @@
|
|
|
169
169
|
"maximum": 9007199254740991,
|
|
170
170
|
"description": "The wave whose merge produced the conflict."
|
|
171
171
|
},
|
|
172
|
+
"where": {
|
|
173
|
+
"description": "`wave`: the wave merge. `base`: building a phase’s `integ-` base out of its dependencies, a wave earlier. Absent on post-mortems written before base conflicts were counted.",
|
|
174
|
+
"type": "string",
|
|
175
|
+
"enum": [
|
|
176
|
+
"base",
|
|
177
|
+
"wave"
|
|
178
|
+
]
|
|
179
|
+
},
|
|
172
180
|
"nodes": {
|
|
173
181
|
"minItems": 2,
|
|
174
182
|
"type": "array",
|
|
@@ -489,7 +497,7 @@
|
|
|
489
497
|
},
|
|
490
498
|
"record_ref": {
|
|
491
499
|
"type": "string",
|
|
492
|
-
"description": "Where the
|
|
500
|
+
"description": "Where the run coordinator’s own account of this change lives, relative to the run directory: its conversation. The reasoning, the evidence and the commands are prose about a repository and stay there; this artifact carries what can be counted."
|
|
493
501
|
}
|
|
494
502
|
},
|
|
495
503
|
"required": [
|
|
@@ -502,7 +510,334 @@
|
|
|
502
510
|
"additionalProperties": false,
|
|
503
511
|
"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
512
|
},
|
|
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."
|
|
513
|
+
"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. Changes a person made are in `operations`."
|
|
514
|
+
},
|
|
515
|
+
"failure_causes": {
|
|
516
|
+
"description": "Failed attempts grouped by cause, most expensive first.",
|
|
517
|
+
"type": "array",
|
|
518
|
+
"items": {
|
|
519
|
+
"type": "object",
|
|
520
|
+
"properties": {
|
|
521
|
+
"reason": {
|
|
522
|
+
"type": "string",
|
|
523
|
+
"maxLength": 300,
|
|
524
|
+
"description": "The scheduler’s recorded reason, verbatim up to 300 characters."
|
|
525
|
+
},
|
|
526
|
+
"setup": {
|
|
527
|
+
"type": "boolean",
|
|
528
|
+
"description": "Whether these attempts failed before any agent ran — provisioning, a base merge, a checkout. The machinery’s failure rather than the work’s, and the kind a plan cannot fix but a project configuration can."
|
|
529
|
+
},
|
|
530
|
+
"attempts": {
|
|
531
|
+
"type": "integer",
|
|
532
|
+
"minimum": 1,
|
|
533
|
+
"maximum": 9007199254740991,
|
|
534
|
+
"description": "Attempts that failed with this reason, across every phase."
|
|
535
|
+
},
|
|
536
|
+
"nodes": {
|
|
537
|
+
"minItems": 1,
|
|
538
|
+
"type": "array",
|
|
539
|
+
"items": {
|
|
540
|
+
"type": "string",
|
|
541
|
+
"minLength": 1,
|
|
542
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
543
|
+
},
|
|
544
|
+
"description": "The phases it hit."
|
|
545
|
+
}
|
|
546
|
+
},
|
|
547
|
+
"required": [
|
|
548
|
+
"reason",
|
|
549
|
+
"setup",
|
|
550
|
+
"attempts",
|
|
551
|
+
"nodes"
|
|
552
|
+
],
|
|
553
|
+
"additionalProperties": false,
|
|
554
|
+
"description": "One cause of failed attempts, and what it cost: the run’s retries, grouped."
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
"crew_substitutions": {
|
|
558
|
+
"type": "array",
|
|
559
|
+
"items": {
|
|
560
|
+
"type": "object",
|
|
561
|
+
"properties": {
|
|
562
|
+
"node": {
|
|
563
|
+
"type": "string",
|
|
564
|
+
"minLength": 1,
|
|
565
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
566
|
+
},
|
|
567
|
+
"planned": {
|
|
568
|
+
"type": "string",
|
|
569
|
+
"minLength": 1,
|
|
570
|
+
"description": "The member the plan named."
|
|
571
|
+
},
|
|
572
|
+
"planned_tier": {
|
|
573
|
+
"description": "Their tier. Absent on rows written before it was recorded.",
|
|
574
|
+
"type": "integer",
|
|
575
|
+
"minimum": -9007199254740991,
|
|
576
|
+
"maximum": 9007199254740991
|
|
577
|
+
},
|
|
578
|
+
"member": {
|
|
579
|
+
"type": "string",
|
|
580
|
+
"minLength": 1,
|
|
581
|
+
"description": "Who ran it instead."
|
|
582
|
+
},
|
|
583
|
+
"tier": {
|
|
584
|
+
"type": "integer",
|
|
585
|
+
"minimum": -9007199254740991,
|
|
586
|
+
"maximum": 9007199254740991
|
|
587
|
+
},
|
|
588
|
+
"model": {
|
|
589
|
+
"description": "The model that member ran on, from the roster.",
|
|
590
|
+
"type": "string"
|
|
591
|
+
},
|
|
592
|
+
"reason": {
|
|
593
|
+
"type": "string",
|
|
594
|
+
"enum": [
|
|
595
|
+
"peer_busy",
|
|
596
|
+
"warm_session"
|
|
597
|
+
],
|
|
598
|
+
"description": "`peer_busy`: the planned member was working. `warm_session`: promoted to reuse an open session."
|
|
599
|
+
},
|
|
600
|
+
"attempts": {
|
|
601
|
+
"type": "integer",
|
|
602
|
+
"minimum": 1,
|
|
603
|
+
"maximum": 9007199254740991,
|
|
604
|
+
"description": "Attempts this member took the phase for."
|
|
605
|
+
},
|
|
606
|
+
"above_plan": {
|
|
607
|
+
"type": "boolean",
|
|
608
|
+
"description": "Whether this ran at a higher tier than the plan named — dearer than budgeted."
|
|
609
|
+
},
|
|
610
|
+
"node_cost_usd": {
|
|
611
|
+
"description": "What the whole phase cost, every attempt and member included, when its harness reported cost.",
|
|
612
|
+
"type": "number",
|
|
613
|
+
"minimum": 0
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
"required": [
|
|
617
|
+
"node",
|
|
618
|
+
"planned",
|
|
619
|
+
"member",
|
|
620
|
+
"tier",
|
|
621
|
+
"reason",
|
|
622
|
+
"attempts",
|
|
623
|
+
"above_plan"
|
|
624
|
+
],
|
|
625
|
+
"additionalProperties": false,
|
|
626
|
+
"description": "A phase run by somebody other than the member its plan named."
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
"operations": {
|
|
630
|
+
"type": "object",
|
|
631
|
+
"properties": {
|
|
632
|
+
"amendments": {
|
|
633
|
+
"type": "array",
|
|
634
|
+
"items": {
|
|
635
|
+
"type": "object",
|
|
636
|
+
"properties": {
|
|
637
|
+
"amendment": {
|
|
638
|
+
"type": "integer",
|
|
639
|
+
"minimum": 1,
|
|
640
|
+
"maximum": 9007199254740991
|
|
641
|
+
},
|
|
642
|
+
"at_ms": {
|
|
643
|
+
"type": "integer",
|
|
644
|
+
"minimum": -9007199254740991,
|
|
645
|
+
"maximum": 9007199254740991
|
|
646
|
+
},
|
|
647
|
+
"author": {
|
|
648
|
+
"type": "string",
|
|
649
|
+
"enum": [
|
|
650
|
+
"operator",
|
|
651
|
+
"monitor",
|
|
652
|
+
"coordinator",
|
|
653
|
+
"config"
|
|
654
|
+
]
|
|
655
|
+
}
|
|
656
|
+
},
|
|
657
|
+
"required": [
|
|
658
|
+
"amendment",
|
|
659
|
+
"at_ms",
|
|
660
|
+
"author"
|
|
661
|
+
],
|
|
662
|
+
"additionalProperties": false
|
|
663
|
+
},
|
|
664
|
+
"description": "Every change to the run’s definition while it ran, whoever made it."
|
|
665
|
+
},
|
|
666
|
+
"node_operations": {
|
|
667
|
+
"type": "array",
|
|
668
|
+
"items": {
|
|
669
|
+
"type": "object",
|
|
670
|
+
"properties": {
|
|
671
|
+
"node": {
|
|
672
|
+
"type": "string",
|
|
673
|
+
"minLength": 1,
|
|
674
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
675
|
+
},
|
|
676
|
+
"op": {
|
|
677
|
+
"type": "string",
|
|
678
|
+
"enum": [
|
|
679
|
+
"add_context",
|
|
680
|
+
"redirect",
|
|
681
|
+
"pause",
|
|
682
|
+
"abort"
|
|
683
|
+
]
|
|
684
|
+
},
|
|
685
|
+
"by": {
|
|
686
|
+
"type": "string",
|
|
687
|
+
"enum": [
|
|
688
|
+
"operator",
|
|
689
|
+
"coordinator"
|
|
690
|
+
]
|
|
691
|
+
},
|
|
692
|
+
"count": {
|
|
693
|
+
"type": "integer",
|
|
694
|
+
"minimum": 1,
|
|
695
|
+
"maximum": 9007199254740991
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
"required": [
|
|
699
|
+
"node",
|
|
700
|
+
"op",
|
|
701
|
+
"by",
|
|
702
|
+
"count"
|
|
703
|
+
],
|
|
704
|
+
"additionalProperties": false
|
|
705
|
+
},
|
|
706
|
+
"description": "Steering, pauses and aborts, per phase."
|
|
707
|
+
},
|
|
708
|
+
"questions": {
|
|
709
|
+
"type": "object",
|
|
710
|
+
"properties": {
|
|
711
|
+
"asked": {
|
|
712
|
+
"type": "integer",
|
|
713
|
+
"minimum": 0,
|
|
714
|
+
"maximum": 9007199254740991
|
|
715
|
+
},
|
|
716
|
+
"by_operator": {
|
|
717
|
+
"type": "integer",
|
|
718
|
+
"minimum": 0,
|
|
719
|
+
"maximum": 9007199254740991
|
|
720
|
+
},
|
|
721
|
+
"unattended": {
|
|
722
|
+
"type": "integer",
|
|
723
|
+
"minimum": 0,
|
|
724
|
+
"maximum": 9007199254740991,
|
|
725
|
+
"description": "Answered by a `--retry-after` timer."
|
|
726
|
+
},
|
|
727
|
+
"by_coordinator": {
|
|
728
|
+
"type": "integer",
|
|
729
|
+
"minimum": 0,
|
|
730
|
+
"maximum": 9007199254740991
|
|
731
|
+
},
|
|
732
|
+
"unanswered": {
|
|
733
|
+
"type": "integer",
|
|
734
|
+
"minimum": 0,
|
|
735
|
+
"maximum": 9007199254740991
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
"required": [
|
|
739
|
+
"asked",
|
|
740
|
+
"by_operator",
|
|
741
|
+
"unattended",
|
|
742
|
+
"by_coordinator",
|
|
743
|
+
"unanswered"
|
|
744
|
+
],
|
|
745
|
+
"additionalProperties": false,
|
|
746
|
+
"description": "Every question a phase was parked on, and who answered it."
|
|
747
|
+
},
|
|
748
|
+
"exec_runs": {
|
|
749
|
+
"type": "array",
|
|
750
|
+
"items": {
|
|
751
|
+
"type": "object",
|
|
752
|
+
"properties": {
|
|
753
|
+
"target": {
|
|
754
|
+
"type": "string",
|
|
755
|
+
"minLength": 1
|
|
756
|
+
},
|
|
757
|
+
"by": {
|
|
758
|
+
"type": "string",
|
|
759
|
+
"enum": [
|
|
760
|
+
"operator",
|
|
761
|
+
"coordinator"
|
|
762
|
+
]
|
|
763
|
+
},
|
|
764
|
+
"count": {
|
|
765
|
+
"type": "integer",
|
|
766
|
+
"minimum": 1,
|
|
767
|
+
"maximum": 9007199254740991
|
|
768
|
+
},
|
|
769
|
+
"failed": {
|
|
770
|
+
"type": "integer",
|
|
771
|
+
"minimum": 0,
|
|
772
|
+
"maximum": 9007199254740991
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
"required": [
|
|
776
|
+
"target",
|
|
777
|
+
"by",
|
|
778
|
+
"count",
|
|
779
|
+
"failed"
|
|
780
|
+
],
|
|
781
|
+
"additionalProperties": false
|
|
782
|
+
},
|
|
783
|
+
"description": "Commands run by hand in a lane or the integration worktree (`vinta-ai-maestro exec`)."
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
"required": [
|
|
787
|
+
"amendments",
|
|
788
|
+
"node_operations",
|
|
789
|
+
"questions",
|
|
790
|
+
"exec_runs"
|
|
791
|
+
],
|
|
792
|
+
"additionalProperties": false,
|
|
793
|
+
"description": "What was done to the run while it ran, by a person or its coordinator. Repairs made outside maestro — a wave branch rebuilt in another checkout — leave no record and are not counted."
|
|
794
|
+
},
|
|
795
|
+
"cross_phase_failures": {
|
|
796
|
+
"type": "array",
|
|
797
|
+
"items": {
|
|
798
|
+
"type": "object",
|
|
799
|
+
"properties": {
|
|
800
|
+
"wave": {
|
|
801
|
+
"type": "integer",
|
|
802
|
+
"minimum": 1,
|
|
803
|
+
"maximum": 9007199254740991
|
|
804
|
+
},
|
|
805
|
+
"gate": {
|
|
806
|
+
"type": "string",
|
|
807
|
+
"minLength": 1,
|
|
808
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
809
|
+
},
|
|
810
|
+
"members": {
|
|
811
|
+
"minItems": 1,
|
|
812
|
+
"type": "array",
|
|
813
|
+
"items": {
|
|
814
|
+
"type": "string",
|
|
815
|
+
"minLength": 1,
|
|
816
|
+
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
817
|
+
},
|
|
818
|
+
"description": "The phases merged in that wave."
|
|
819
|
+
},
|
|
820
|
+
"at_ms": {
|
|
821
|
+
"type": "integer",
|
|
822
|
+
"minimum": -9007199254740991,
|
|
823
|
+
"maximum": 9007199254740991
|
|
824
|
+
},
|
|
825
|
+
"exit_code": {
|
|
826
|
+
"type": "integer",
|
|
827
|
+
"minimum": -9007199254740991,
|
|
828
|
+
"maximum": 9007199254740991
|
|
829
|
+
}
|
|
830
|
+
},
|
|
831
|
+
"required": [
|
|
832
|
+
"wave",
|
|
833
|
+
"gate",
|
|
834
|
+
"members",
|
|
835
|
+
"at_ms",
|
|
836
|
+
"exit_code"
|
|
837
|
+
],
|
|
838
|
+
"additionalProperties": false,
|
|
839
|
+
"description": "A gate that every phase passed on its own branch and that failed on the merged wave: two of `members` (or one of them and an earlier wave) disagree. The evidence of a missing dependency or a missing contract test that `missing_dependencies`’ ordering cannot see."
|
|
840
|
+
}
|
|
506
841
|
}
|
|
507
842
|
},
|
|
508
843
|
"required": [
|
|
@@ -339,7 +339,7 @@
|
|
|
339
339
|
"additionalProperties": false,
|
|
340
340
|
"properties": {
|
|
341
341
|
"defaults": {
|
|
342
|
-
"description": "Run defaults a plan's `defaults` overrides field by field: `harness`, `model`, `pipeline`, `chores`, `gates` (the gate ids every phase runs unless it names its own), `gate_scope` (`scoped` | `full`), `allow_ungated_phases`, `model_fallbacks`, `max_session_turns`.",
|
|
342
|
+
"description": "Run defaults a plan's `defaults` overrides field by field: `harness`, `model`, `pipeline`, `chores`, `gates` (the gate ids every phase runs unless it names its own), `gate_scope` (`scoped` | `full`), `wave_gates` (`off` | `final` | `every`), `substitution` (`same_tier` | `up_one_tier` | `any`), `allow_ungated_phases`, `model_fallbacks`, `max_session_turns`.",
|
|
343
343
|
"type": "object",
|
|
344
344
|
"properties": {
|
|
345
345
|
"harness": {
|
|
@@ -379,6 +379,24 @@
|
|
|
379
379
|
"full"
|
|
380
380
|
]
|
|
381
381
|
},
|
|
382
|
+
"substitution": {
|
|
383
|
+
"type": "string",
|
|
384
|
+
"enum": [
|
|
385
|
+
"same_tier",
|
|
386
|
+
"up_one_tier",
|
|
387
|
+
"any"
|
|
388
|
+
],
|
|
389
|
+
"description": "How far above the member the plan named a phase may be moved. `up_one_tier` (the default): a busy member's phase may go to the next tier up the roster has, and a warm session never moves a phase up. `same_tier`: a peer at the same tier or wait. `any`: any tier above, warm sessions included."
|
|
390
|
+
},
|
|
391
|
+
"wave_gates": {
|
|
392
|
+
"type": "string",
|
|
393
|
+
"enum": [
|
|
394
|
+
"off",
|
|
395
|
+
"final",
|
|
396
|
+
"every"
|
|
397
|
+
],
|
|
398
|
+
"description": "When the phases' gates run in full on a merged wave, in the integration worktree. `final` (the default): every gate of the plan on the last wave before the plan PR, and a wave's gates on any wave whose merge needed a conflict resolution. `every`: on every wave as well. `off`: none of these (the full form of a `scoped_cmd` gate still runs per wave under `gate_scope: scoped`)."
|
|
399
|
+
},
|
|
382
400
|
"allow_ungated_phases": {
|
|
383
401
|
"type": "boolean",
|
|
384
402
|
"description": "Whether a phase that resolves to no gates is acceptable. Off (the default): `vinta-ai-maestro validate` refuses the workflow and `doctor` warns, because an ungated phase passes its gate step without running anything."
|
|
@@ -106,6 +106,16 @@
|
|
|
106
106
|
"connection_url_var": {
|
|
107
107
|
"type": "string",
|
|
108
108
|
"minLength": 1
|
|
109
|
+
},
|
|
110
|
+
"createdb_cmd": {
|
|
111
|
+
"description": "Replaces the `createdb` binary name (default `createdb`) in every command that creates a lane database. A trusted command prefix, unquoted, like `migrate_cmd`; the `-h`/`-p`/`-U` flags derived from `server_url` still follow it. Only meaningful for `external`. For a Postgres in a container whose password the host cannot supply: `docker compose exec -T db createdb` — the official image trusts connections made from inside the container, so no password is needed there.",
|
|
112
|
+
"type": "string",
|
|
113
|
+
"minLength": 1
|
|
114
|
+
},
|
|
115
|
+
"dropdb_cmd": {
|
|
116
|
+
"description": "Replaces the `dropdb` binary name (default `dropdb`), exactly as `createdb_cmd` does for `createdb`. Set both together: `docker compose exec -T db dropdb`.",
|
|
117
|
+
"type": "string",
|
|
118
|
+
"minLength": 1
|
|
109
119
|
}
|
|
110
120
|
},
|
|
111
121
|
"required": [
|
|
@@ -175,6 +185,16 @@
|
|
|
175
185
|
"connection_url_var": {
|
|
176
186
|
"type": "string",
|
|
177
187
|
"minLength": 1
|
|
188
|
+
},
|
|
189
|
+
"createdb_cmd": {
|
|
190
|
+
"description": "Replaces the `createdb` binary name (default `createdb`) in every command that creates a lane database. A trusted command prefix, unquoted, like `migrate_cmd`; the `-h`/`-p`/`-U` flags derived from `server_url` still follow it. Only meaningful for `external`. For a Postgres in a container whose password the host cannot supply: `docker compose exec -T db createdb` — the official image trusts connections made from inside the container, so no password is needed there.",
|
|
191
|
+
"type": "string",
|
|
192
|
+
"minLength": 1
|
|
193
|
+
},
|
|
194
|
+
"dropdb_cmd": {
|
|
195
|
+
"description": "Replaces the `dropdb` binary name (default `dropdb`), exactly as `createdb_cmd` does for `createdb`. Set both together: `docker compose exec -T db dropdb`.",
|
|
196
|
+
"type": "string",
|
|
197
|
+
"minLength": 1
|
|
178
198
|
}
|
|
179
199
|
},
|
|
180
200
|
"required": [
|
|
@@ -393,6 +413,26 @@
|
|
|
393
413
|
"full"
|
|
394
414
|
]
|
|
395
415
|
},
|
|
416
|
+
"substitution": {
|
|
417
|
+
"default": "up_one_tier",
|
|
418
|
+
"description": "How far above the member the plan named a phase may be moved when that member is busy or another holds a warm session. `same_tier` — only a peer at the same tier; a phase waits rather than run dearer. `up_one_tier` — the default — also the next tier up the roster has, when nobody at the planned tier is free; a warm session never moves a phase up. `any` — any tier above, and a warm session above the plan may take it to avoid a cold start. One observed run moved 23 phases from Tier 3 to a Tier 4 member whose credits then ran out, and nothing showed it.",
|
|
419
|
+
"type": "string",
|
|
420
|
+
"enum": [
|
|
421
|
+
"same_tier",
|
|
422
|
+
"up_one_tier",
|
|
423
|
+
"any"
|
|
424
|
+
]
|
|
425
|
+
},
|
|
426
|
+
"wave_gates": {
|
|
427
|
+
"default": "final",
|
|
428
|
+
"description": "When the phases’ gates run in full on a merged wave, in the integration worktree. Each phase passes its gates on its own branch, and two parallel phases can each be green and disagree with each other — a sender and a receiver of one signal, say — which only a merged tree shows. `final` — the default — runs every gate of the plan once on the last wave before the plan PR opens, and the wave’s phases’ gates on any wave whose merge needed a conflict resolution. `every` runs the wave’s phases’ gates on every wave as well, which costs one full suite per wave and catches a disagreement at the wave that introduced it. `off` runs none of these. Under `gate_scope: scoped` the full form of a narrowed gate still runs on each wave whatever this says: that is where the suite the phases skipped is paid back.",
|
|
429
|
+
"type": "string",
|
|
430
|
+
"enum": [
|
|
431
|
+
"off",
|
|
432
|
+
"final",
|
|
433
|
+
"every"
|
|
434
|
+
]
|
|
435
|
+
},
|
|
396
436
|
"chores": {
|
|
397
437
|
"default": [],
|
|
398
438
|
"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.",
|
|
@@ -850,6 +890,11 @@
|
|
|
850
890
|
"minLength": 1,
|
|
851
891
|
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
852
892
|
},
|
|
893
|
+
"deferred": {
|
|
894
|
+
"description": "Why this phase must not start on its own — \"deferred until the flag has soaked two weeks at 100% in production\". A deferred phase is not dispatched when its dependencies are done: it parks on a question naming this reason, and the operator answers `start` (now) or `stop` (fail it, blocking what depends on it). Without it the scheduler would run a soak-gated flag removal the moment its dependencies merged.",
|
|
895
|
+
"type": "string",
|
|
896
|
+
"minLength": 1
|
|
897
|
+
},
|
|
853
898
|
"max_fix_rounds": {
|
|
854
899
|
"description": "Fix rounds a phase may spend on red gates, and unsuccessful iterations each review turn may run, before the operator is asked whether to keep going (§16). Absent — the default — sets no limit: the fixer works until the gates are green and the review loop until the reviewer approves. A fix round is a fixer turn; a review iteration is one pass of the review loop that returned blockers. When a set budget runs out the shipped pipeline asks rather than fails: `continue` grants the same budget again, `stop` fails the phase and hands it to `--on-failure`.",
|
|
855
900
|
"type": "integer",
|
|
@@ -260,6 +260,7 @@ Before publishing the plan, cross-check the **Touch List**: for every pair of ph
|
|
|
260
260
|
Two habits pay for themselves:
|
|
261
261
|
|
|
262
262
|
- **Front-load shared scaffolding into a wave-1 foundation phase.** Types, the migration, the empty module, the fixture. Every use-case phase then depends only on that one phase, and they all run at once, instead of forming a chain.
|
|
263
|
+
- **Pin every interface two parallel phases share with a contract test in that foundation phase.** A signal payload one phase sends and another receives, a protocol or ABC one implements and another calls, a DI provider one registers and another injects, an event schema, a serializer shape. Write the test that states the contract (the keyword arguments a receiver requires, the methods an implementation must have) into the foundation phase, so each parallel phase's own gates run it on its own branch. Without it, two phases can each be green alone and disagree with each other: one observed run had a sender omit an `organization_id` its sibling's receiver required, and the merged wave failed 22 tests that no phase's gate could have seen. The executor also runs the plan's gates once on the merged tree before the plan PR (`defaults.wave_gates`), but that catches the disagreement at the end, when it costs a rework of both phases.
|
|
263
264
|
- **Split by seam, not by layer, when the seams are independent.** Four use-cases on the same entity are four independent phases if they only share the model. Four layers of one use-case (repository → service → serializer → view) are a chain no matter how you number them.
|
|
264
265
|
|
|
265
266
|
Don't contort the plan for concurrency, though. A genuinely sequential feature is a chain of waves of one, and that is a correct plan.
|
|
@@ -275,13 +276,18 @@ ls -t .vinta-ai-maestro/runs/*/postmortem.json ai-plans/*.postmortem.json 2>/dev
|
|
|
275
276
|
Each file carries `findings` and `gaps`. Use them like this:
|
|
276
277
|
|
|
277
278
|
- **`missing_dependencies`** — a phase failed, a phase it did *not* declare landed, and only then did it pass. The previous plan was missing that edge. If this feature couples the same two layers, **declare the edge here**, naming the artifact. Entry is ordering evidence, not proof (the file's own `gate_result_unrecorded` gap says so) — confirm the coupling exists in the code before you draw it.
|
|
278
|
-
- **`wave_conflicts`** — two
|
|
279
|
+
- **`wave_conflicts`** — two phases that actually fought, with the contested `paths`: at a wave merge (`where: "wave"`), or while a phase's `integ-` base was built out of its dependencies (`where: "base"`), which is the same signal a wave earlier. 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.
|
|
279
280
|
- **`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.
|
|
280
281
|
- **`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.
|
|
281
282
|
- **`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.
|
|
282
283
|
- **`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.
|
|
283
284
|
- **`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.
|
|
284
|
-
- **`interventions`** — changes the run made to
|
|
285
|
+
- **`interventions`** — changes the run's coordinator made to the run while it ran, because it found the run 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 the coordinator's conversation its `record_ref` names, 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.
|
|
286
|
+
|
|
287
|
+
- **`cross_phase_failures`** — a gate every phase passed on its own branch that failed on a merged wave: two of that wave's `members` disagree about something they share (a signal's arguments, a protocol, a provider). This is a missing dependency or a missing contract test that `missing_dependencies` cannot see, because nothing *landed* in between — it only showed up merged. If this feature has phases on the same interface, put a contract test for it in the foundation phase (see "Design *for* parallelism").
|
|
288
|
+
- **`failure_causes`** — failed attempts grouped by reason, with `attempts` and the phases each one hit. An entry with `setup: true` failed before any agent ran — provisioning, a base merge, a checkout — and repeating it is the project configuration's fault, not the plan's: name it to the human (a missing `env_files` entry, a compose service that will not start) rather than reshaping the graph around it. A `setup: false` cause that hit many phases is a gate or a contract the plan should have settled first.
|
|
289
|
+
- **`crew_substitutions`** — phases run by somebody other than the member the plan named. Entries with `above_plan: true` ran dearer than budgeted; many of them on one member means the roster was too thin at that tier for the plan's width. Size the **Crew** table so each wave's phases are covered at their own tier, rather than relying on cover from above.
|
|
290
|
+
- **`operations`** — what people (and the coordinator) did to the run: amendments, steering, pauses, aborts, who answered the questions, commands run by hand. A plan whose runs needed many of these is telling you where it was underspecified: a phase steered or redirected repeatedly needed a sharper brief; the same question answered the same way every time belonged in the plan.
|
|
285
291
|
|
|
286
292
|
Rules for using them:
|
|
287
293
|
|
|
@@ -426,7 +432,7 @@ Watch for the accidental version of this: making an in-repo phase `**Depends on*
|
|
|
426
432
|
|
|
427
433
|
### Flag-removal phase depends on everything
|
|
428
434
|
|
|
429
|
-
The mandatory final flag-removal phase depends on **every** gated phase — it deletes the branches they added. Say so in its `**Depends on**:` line. It sits alone in the deepest wave
|
|
435
|
+
The mandatory final flag-removal phase depends on **every** gated phase — it deletes the branches they added. Say so in its `**Depends on**:` line. It sits alone in the deepest wave, so this edge costs nothing and documents the real constraint. **The executor does not know it is soak-gated unless the workflow says so**: give that node `deferred` (see the mapping table) carrying the soak condition, or the scheduler starts it the moment its dependencies merge.
|
|
430
436
|
|
|
431
437
|
### Never give time estimates
|
|
432
438
|
|
|
@@ -600,6 +606,8 @@ First key in the file is `"$schema"`, pointing at `https://github.com/vintasoftw
|
|
|
600
606
|
| `chores.<id>` | Agent turns a phase runs once its gates are green. **Always emit the `review` and `deslop` chores below unless `maestro.chores.review` / `maestro.chores.deslop` already declare them** — `review` is the phase's code review (see "The review chore") and `deslop` the comment-hygiene pass, and without them a maestro run has neither. 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. |
|
|
601
607
|
| `chores.review-canvas` | **Emit only when `.vinta-ai-workflows.yaml` has `integrations.pr-review-canvas: enabled`.** An `after_pr` chore: maestro runs it once each phase's PR is open, and it posts a review canvas on that PR. Emit it exactly as shown under "The review-canvas chore" below. Never emit it when the integration is `disabled` or absent. The skill it names is installed only when the integration is enabled. |
|
|
602
608
|
| `defaults.chores` | `["review", "deslop"]`, plus `"review-canvas"` when that chore is emitted — or **omit the key** when `maestro.defaults.chores` already names the same list. The chores every phase runs unless it names its own. **Never write `"chores": []` to mean "the project's"**: an empty list *replaces* the project's list with nothing, and the run then runs no chore at all. |
|
|
609
|
+
| `defaults.substitution` | **Omit** — it defaults to `up_one_tier`: a busy member's phase may go to the next tier up the roster has, and a warm session never moves a phase up. Set `same_tier` when the top tier is a limited credit allowance that must not absorb other tiers' work. |
|
|
610
|
+
| `defaults.wave_gates` | **Omit** — it defaults to `final`: every gate of the plan runs once on the merged tree before the plan PR opens, plus a wave's gates on any wave whose merge needed a conflict resolution. Set `every` for a plan with many parallel phases on shared interfaces, where finding a cross-phase break at the wave that introduced it is worth one full suite per wave. Never `off` unless the project's suite cannot run in the integration worktree. |
|
|
603
611
|
| `defaults.gates` | The gate ids every phase runs. **Omit the key** when `maestro.defaults.gates` is already the list this plan wants; set it when most phases share a list the project does not name. A node with no `gates` key takes it. **Never `[]`**: that is a run with no gates. |
|
|
604
612
|
| `nodes[].chores` | **Omit the key** on almost every phase — absent means the run-wide default. Name a list only to give one phase a different set, and `[]` only to deliberately opt one phase out. A list *replaces* the default rather than adding to it; `[]` is "no chores", never "the default". |
|
|
605
613
|
| `nodes[]` | One per phase, in plan order. |
|
|
@@ -611,6 +619,7 @@ First key in the file is `"$schema"`, pointing at `https://github.com/vintasoftw
|
|
|
611
619
|
| `nodes[].gates` | The gate ids this phase must pass, in the order they should run. **Omit the key** when they are the run's `defaults.gates` (the plan's or the project's `maestro.defaults.gates`). **`[]` is an ungated phase** — its gate step passes without running anything — and is for a phase that genuinely has nothing to check, declared with `defaults.allow_ungated_phases: true`; `vinta-ai-maestro validate` refuses a phase that resolves to no gates otherwise. One observed run finished twelve phases without a single check because every node carried `"gates": []` where the author meant "the project's". |
|
|
612
620
|
| `nodes[].crew` | The id from this phase's `**Assigned to**:` line. **Required on every node of a staffed workflow** — a half-staffed document is refused, because the executor would be running two staffing rules at once. |
|
|
613
621
|
| `nodes[].model` / `nodes[].harness` | **Omit `model` entirely on a staffed workflow** — the member carries it, and a node setting both is refused. `harness` only when this one phase runs on a different CLI than `defaults`. |
|
|
622
|
+
| `nodes[].deferred` | The phase's soak or wait condition, as prose, on every phase the Execution graph marks **deferred** — the flag-removal phase above all. A deferred phase is not started when its dependencies merge: the executor parks it on a question naming this text, and a person answers `start` when the condition holds (or `stop`). Omit on every other phase. Without it the scheduler runs a soak-gated flag removal the moment the phases it depends on land. |
|
|
614
623
|
| `nodes[].max_fix_rounds` | Omit. Absent means no limit: the fixer works until the gates are green and the review loop until the reviewer approves. Set it to cap the fix rounds a phase may spend on red gates, and the review iterations each review turn may run, before the operator is asked. When a phase spends a set budget, the executor asks the operator whether to continue rather than failing the phase. Set one only on a phase you want a person to look at if it drags on — an expensive suite, a phase with a known risk of going in circles. |
|
|
615
624
|
| `nodes[].pipeline` | Omit. A per-phase pipeline is for a phase that genuinely runs a different lifecycle, which is rare enough that needing it is a signal to re-read the plan. |
|
|
616
625
|
| `pipelines` | **Omit.** The executor ships `standard-phase` — see "The pipeline block". |
|
|
@@ -622,7 +631,7 @@ Rules the mapping depends on:
|
|
|
622
631
|
- **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.
|
|
623
632
|
- **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 row with no `crew` entry, means the two were edited separately.
|
|
624
633
|
- **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 review loop's fixes, the comment pass, a changelog entry — and it runs only once the phase's gates are green. Only the `review` chore stands between a phase and its merge; any other chore that fails is recorded and the phase carries on to its final gate run. Anything you can express as a command belongs in `gates`, where it is cached and queued and costs no model time.
|
|
625
|
-
- **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.
|
|
634
|
+
- **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. A command that goes through `docker compose run` or `exec` needs `-T`: a gate runs with no terminal, and without it compose fails with "the input device is not a TTY" before the command starts (`doctor` warns about it).
|
|
626
635
|
- **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
636
|
- **`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.
|
|
628
637
|
|
|
@@ -985,6 +994,7 @@ and this `ai-plans/2026-03-04-bookmark-folders.workflow.json`:
|
|
|
985
994
|
{
|
|
986
995
|
"id": "p5",
|
|
987
996
|
"name": "Remove the bookmark-folders feature flag",
|
|
997
|
+
"deferred": "until the bookmark-folders flag has soaked two weeks at 100% in production with no rollback or incident attributed.",
|
|
988
998
|
"depends_on": [
|
|
989
999
|
{
|
|
990
1000
|
"node": "p2",
|
|
@@ -1017,7 +1027,7 @@ and this `ai-plans/2026-03-04-bookmark-folders.workflow.json`:
|
|
|
1017
1027
|
}
|
|
1018
1028
|
```
|
|
1019
1029
|
|
|
1020
|
-
Read the three renderings against each other: `p2` and `p3` both name only `p1`, so they sit in wave 2 and run at once; `p4` names both, so it is wave 3; `p5` names every gated phase, so it is wave 4 and alone there. `p2` and `p4` both touch `apps/bookmarks/api/views.py` — allowed, because the edge between them puts them in different waves; had they been same-wave, that overlap is what "Same-wave phases must not fight over the same files" is about.
|
|
1030
|
+
Read the three renderings against each other: `p2` and `p3` both name only `p1`, so they sit in wave 2 and run at once; `p4` names both, so it is wave 3; `p5` names every gated phase, so it is wave 4 and alone there — and carries `deferred`, so once `p4` merges the executor asks rather than starts it. `p2` and `p4` both touch `apps/bookmarks/api/views.py` — allowed, because the edge between them puts them in different waves; had they been same-wave, that overlap is what "Same-wave phases must not fight over the same files" is about.
|
|
1021
1031
|
|
|
1022
1032
|
No node carries a `model`. `p1` and `p5` are the Tier 1 phases and run on `tier1`'s model because that is who took them — the roster says it once instead of two nodes repeating an id that goes stale on the next model bump.
|
|
1023
1033
|
|
|
@@ -1179,6 +1189,7 @@ When in doubt, model the plan after a recent example in `ai-plans/` — look for
|
|
|
1179
1189
|
- [ ] Same-wave phases checked against the **Touch List** for file overlap; real overlaps either serialized with an edge or called out explicitly under the graph table.
|
|
1180
1190
|
- [ ] Post-mortems from previous runs (`.vinta-ai-maestro/runs/*/postmortem.json`, plus any committed beside a plan) read **before** the graph was drawn; every finding either changed an edge, a wave or a split, or was consciously dismissed as not applying to this feature.
|
|
1181
1191
|
- [ ] Slow-moving / cross-repo work sits in wave 1, and no in-repo phase depends on a cross-repo phase when it only needs the contract.
|
|
1192
|
+
- [ ] Every interface two same-wave phases share (a signal payload, a protocol, a DI provider, an event schema) has a contract test in the foundation phase they both depend on.
|
|
1182
1193
|
- [ ] **Crew** table is the first thing under **Phased Rollout**: one row per implementer and no reviewer rows, every member taking at least one phase, every `Takes` cell agreeing with that phase's `**Assigned to**:` line.
|
|
1183
1194
|
- [ ] **Every member earns their place** — by concurrency (a wave genuinely needs that many hands *at or above* those phases' tiers) or by cheapness (they take work a dearer member would otherwise do). Check it wave by wave: sort the wave's phase tiers against the roster's and confirm the roster covers them one for one.
|
|
1184
1195
|
- [ ] Every phase is assigned to a member whose tier is **at or above** the rubric tier its work implies — no phase handed down a tier to save money.
|
|
@@ -1188,7 +1199,7 @@ When in doubt, model the plan after a recent example in `ai-plans/` — look for
|
|
|
1188
1199
|
<!-- e2e:end -->
|
|
1189
1200
|
- [ ] Feature flag declared in **Guiding Decisions** (key, scope, default, flip-on criterion) **unless** **Guiding Decisions** explicitly justifies "no flag — purely additive surface".
|
|
1190
1201
|
- [ ] ≥1 test per gated phase asserts flag-off behavior unchanged.
|
|
1191
|
-
- [ ] If flag declared, **final entry under Phased Rollout is dedicated flag-removal phase** with prerequisite (soak window), full deletion touch list, `grep` acceptance check.
|
|
1202
|
+
- [ ] If flag declared, **final entry under Phased Rollout is dedicated flag-removal phase** with prerequisite (soak window), full deletion touch list, `grep` acceptance check — and its workflow node carries `deferred` with that prerequisite.
|
|
1192
1203
|
- [ ] No time estimates anywhere.
|
|
1193
1204
|
- [ ] Cross-repo phases labeled `Phase Nb`, deploy ordering called out.
|
|
1194
1205
|
- [ ] Risk & Rollout Notes covers locks, partitions, backfills, rollback.
|
|
@@ -1,170 +0,0 @@
|
|
|
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
|
-
}
|