vinta-ai-workflows 0.7.0-alpha2 → 0.7.0-alpha21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +1704 -3
  2. package/README.md +16 -0
  3. package/package.json +11 -9
  4. package/schemas/README.md +9 -2
  5. package/schemas/intervention.v1.schema.json +170 -0
  6. package/schemas/postmortem.v1.schema.json +268 -3
  7. package/schemas/prs-context-frontmatter.v1.schema.json +18 -7
  8. package/schemas/vinta-ai-workflows-config.v1.schema.json +12 -0
  9. package/schemas/workflow.v1.schema.json +388 -24
  10. package/skills/vinta-analyze-codebase/SKILL.md +1 -0
  11. package/skills/vinta-bootstrap-ai-tools/SKILL.md +18 -2
  12. package/skills/vinta-derive-skills/SKILL.md +5 -0
  13. package/skills/vinta-derive-skills/resources/foundation-skills/open-pr-from-context/scripts/open-pr.sh +0 -0
  14. package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/SKILL.md +100 -50
  15. package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/resources/ai-models.yaml +9 -10
  16. package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/SKILL.md +10 -2
  17. package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/scripts/claude-worktree-write-guard.py +0 -0
  18. package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/scripts/gen-claude-sandbox-settings.sh +0 -0
  19. package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/scripts/gen-compose-worktree-override.sh +0 -0
  20. package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/scripts/sandbox-run.sh +0 -0
  21. package/skills/vinta-derive-skills/resources/plan-execution/README.md +11 -0
  22. package/skills/vinta-derive-skills/resources/plan-execution/partials/commit-strategy/stacked.md +18 -0
  23. package/skills/vinta-derive-skills/resources/plan-execution/partials/dispatched-agent.md +31 -0
  24. package/skills/vinta-derive-skills/resources/plan-execution/partials/implementer-prompt.md +2 -0
  25. package/skills/vinta-derive-skills/resources/plan-execution/partials/parallel-lanes.md +10 -3
  26. package/skills/vinta-derive-skills/resources/plan-execution/partials/pr-context.md +3 -1
  27. package/skills/vinta-derive-skills/resources/plan-execution/shell/amend-plan-template.md +3 -0
  28. package/skills/vinta-derive-skills/resources/plan-execution/shell/implement-phase-template.md +3 -0
  29. package/skills/vinta-derive-skills/resources/plan-execution/shell/implement-plan-template.md +7 -3
  30. package/skills/vinta-derive-skills/resources/plan-execution/shell/integrate-phase-template.md +2 -1
  31. package/skills/vinta-derive-skills/resources/plan-execution/shell/review-phase-template.md +3 -0
  32. package/skills/vinta-derive-skills/resources/prs-context-template.md +5 -4
  33. package/skills/vinta-install-ai-tools-setup/SKILL.md +28 -1
  34. package/skills/vinta-install-ai-tools-setup/resources/setup-ai-tools.mjs +0 -0
  35. package/skills/vinta-sync-ai-tools/SKILL.md +2 -1
  36. package/skills/vinta-update-project-skills/SKILL.md +1 -0
@@ -7,7 +7,7 @@ description: Author a phased implementation plan for a new feature following the
7
7
 
8
8
  Plans live in `ai-plans/` as `YYYY-MM-DD-FEATURE_NAME_IMPLEMENTATION_PLAN.md` (uppercase + underscores). `..._SPEC.md` sibling exists → **read first**. Plan translates spec into phased delivery, doesn't re-derive requirements. No spec? Point at [create-spec](../create-spec/SKILL.md) first; plan without spec = plausible-sounding but unverified. Spec/plan pair share `YYYY-MM-DD-FEATURE_NAME` prefix.
9
9
 
10
- Every plan ships **two** files: the markdown above, and its executable sibling `ai-plans/<feature-kebab>.workflow.json` — the same phase graph in the form an orchestrator runs. See "Emit the executable workflow". Written every time; never gated on a question.
10
+ Every plan ships **two** files: the markdown above, and its executable sibling `ai-plans/{TODAY}-<feature-kebab>.workflow.json` — same date prefix, the same phase graph in the form an orchestrator runs. See "Emit the executable workflow". Written every time; never gated on a question.
11
11
 
12
12
  ## Step 0 — Interrogate before drafting (NON-NEGOTIABLE)
13
13
 
@@ -226,15 +226,15 @@ dependency on each other and are implemented concurrently.
226
226
 
227
227
  | Wave | Phases | Agent | Depends on |
228
228
  |---|---|---|---|
229
- | 1 | Phase 0, Phase 1b | `junior`, `mid-2` | — |
230
- | 2 | Phase 1, Phase 2 | `mid-1`, `mid-2` | Phase 0 |
231
- | 3 | Phase 3 | `senior` | Phase 1, Phase 2 |
232
- | 4 | Phase 4 — remove the `bookmarks-v2` flag | `junior` | Phase 3 (deferred — soak-gated) |
229
+ | 1 | Phase 0, Phase 1b | `tier1`, `tier2-2` | — |
230
+ | 2 | Phase 1, Phase 2 | `tier2-1`, `tier2-2` | Phase 0 |
231
+ | 3 | Phase 3 | `tier4` | Phase 1, Phase 2 |
232
+ | 4 | Phase 4 — remove the `bookmarks-v2` flag | `tier1` | Phase 3 (deferred — soak-gated) |
233
233
 
234
234
  **File overlap:** phases in the same wave touch disjoint files, with one exception —
235
235
  Phase 1 and Phase 2 both export from `@app/bookmarks/__init__.py`. Trivial merge.
236
236
 
237
- **Idle:** `senior` has nothing until wave 3 and `mid-1` nothing until wave 2 — the
237
+ **Idle:** `tier4` has nothing until wave 3 and `tier2-1` nothing until wave 2 — the
238
238
  foundation phase is the whole of wave 1 and only one agent can write it.
239
239
  ```
240
240
 
@@ -262,7 +262,7 @@ Don't contort the plan for concurrency, though. A genuinely sequential feature i
262
262
 
263
263
  ### Read the previous runs' post-mortems before drawing the graph
264
264
 
265
- Every plan you write is a guess about coupling. Every plan the orchestrator *ran* turned that guess into evidence, and it wrote the evidence down: one `postmortem.json` per finished run under `.vinta-ai-maestro/runs/<run-id>/`, plus any copy the team committed beside its plan as `ai-plans/<feature-kebab>.postmortem.json`. **Read them before the `**Depends on**:` lines, not after.** Newest first, and all of them — one run is an anecdote, three runs saying the same thing about the same layer is a rule about this codebase.
265
+ Every plan you write is a guess about coupling. Every plan the orchestrator *ran* turned that guess into evidence, and it wrote the evidence down: one `postmortem.json` per finished run under `.vinta-ai-maestro/runs/<run-id>/`, plus any copy the team committed beside its plan as `ai-plans/{DATE}-<feature-kebab>.postmortem.json`. **Read them before the `**Depends on**:` lines, not after.** Newest first, and all of them — one run is an anecdote, three runs saying the same thing about the same layer is a rule about this codebase.
266
266
 
267
267
  ```bash
268
268
  ls -t .vinta-ai-maestro/runs/*/postmortem.json ai-plans/*.postmortem.json 2>/dev/null | head -5
@@ -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
 
@@ -430,7 +434,7 @@ The mandatory final flag-removal phase depends on **every** gated phase — it d
430
434
 
431
435
  A plan does not pick a model per phase. It **staffs a team**, then assigns each phase to somebody on it.
432
436
 
433
- The difference is not cosmetic. Choosing a tier phase by phase answers "what should run this one" ten times and never adds it up, so the two questions that actually decide what a feature costs go unasked: **how many agents does this need at once, and is any of them too junior for what it was handed.** A roster asks both before a line is written, and it is falsifiable — you can look at it and say "nobody needs three seniors here".
437
+ The difference is not cosmetic. Choosing a tier phase by phase answers "what should run this one" ten times and never adds it up, so the two questions that actually decide what a feature costs go unasked: **how many agents does this need at once, and is any of them below the tier of what it was handed.** A roster asks both before a line is written, and it is falsifiable — you can look at it and say "nobody needs three Tier 4 members here".
434
438
 
435
439
  ### The Crew table
436
440
 
@@ -441,13 +445,17 @@ First thing under **Phased Rollout**, before the **Execution graph**:
441
445
 
442
446
  | Agent | Role | Tier | Takes | Why this tier |
443
447
  |---|---|---|---|---|
444
- | `junior` | implementer | 1 | Phase 0, Phase 4 | An empty module and a flag deletion — exact precedent, both of them. |
445
- | `mid-1` | implementer | 2 | Phase 1 | A DRF viewset mirroring `@app/tags/api/views.py` almost line for line. |
446
- | `mid-2` | implementer | 2 | Phase 1b, Phase 2 | Serializer with cross-field validation; the producer stub is the same shape. |
447
- | `senior` | implementer | 4 | Phase 3 | Cycle detection over a user-mutable tree — no precedent in this repo. |
448
+ | `tier1` | implementer | 1 | Phase 0, Phase 4 | An empty module and a flag deletion — exact precedent, both of them. |
449
+ | `tier2-1` | implementer | 2 | Phase 1 | A DRF viewset mirroring `@app/tags/api/views.py` almost line for line. |
450
+ | `tier2-2` | implementer | 2 | Phase 1b, Phase 2 | Serializer with cross-field validation; the producer stub is the same shape. |
451
+ | `tier4` | implementer | 4 | Phase 3 | Cycle detection over a user-mutable tree — no precedent in this repo. |
448
452
  | `reviewer` | reviewer | 4 | — reviews every phase | Reads the tree phase too; a reviewer below the hardest phase can never take it. |
449
453
  ```
450
454
 
455
+ **Name members after their tier, never after a seniority.** An implementer is `tier<N>`, or `tier<N>-<k>` when the roster carries more than one at that tier; a reviewer is `reviewer`, or `reviewer-<k>` when there are several. Ids are lowercase kebab-case — the schema rejects anything else.
456
+
457
+ The rule exists because the tier *is* the whole of what the orchestrator knows about a member: it is the floor on what they may be handed and the only thing compared when one covers for another. A roster that names members after seniority grades names people instead, and invites two mistakes — reading a capability ranking as a career one, and letting the label drift from the `tier` beside it, which is the number that actually decides anything.
458
+
451
459
  **Implementers write; reviewers read; nobody does both.** That is not a convention, it is the shape of the document — an implementer cannot be assigned a review and a reviewer cannot be assigned a phase, so an agent grading its own work is not something a plan can express. It replaces the older "review one tier above the author" rule, which was a proxy for independence and broke in both directions: a phase covered by the top-tier implementer had nobody above it, and a tier says nothing about *who* now that a member is a durable agent rather than a model id.
452
460
 
453
461
  Staff **at least one reviewer** on every plan. A roster of implementers only still runs — reviews fall back to the project's `agent_models.reviewer`, cold, one session per phase — but the reviewer then never accumulates anything about the codebase, which is most of what makes review cheap by phase three.
@@ -456,25 +464,25 @@ Staff **at least one reviewer** on every plan. A roster of implementers only sti
456
464
 
457
465
  **Every implementer has to earn their place, and there are exactly two ways to do it:**
458
466
 
459
- - **Concurrency.** A wave of three phases needs three pairs of hands, and they must be hands *at or above* each of those phases' tiers — two mids and a junior cannot run three Tier 2 phases two-wide, because the junior is not allowed to take one. So go wave by wave: sort the wave's phase tiers, sort the roster's tiers, and check the roster covers them one for one.
460
- - **Cheapness.** A member below the roster's other tiers earns their place by taking work the dearer members would otherwise do. A junior who runs the migration and the flag deletion is worth having even in a graph that is never two phases wide, because those two phases run at Tier 1 instead of Tier 2.
467
+ - **Concurrency.** A wave of three phases needs three pairs of hands, and they must be hands *at or above* each of those phases' tiers — two Tier 2 members and a Tier 1 cannot run three Tier 2 phases two-wide, because the Tier 1 member is not allowed to take one. So go wave by wave: sort the wave's phase tiers, sort the roster's tiers, and check the roster covers them one for one.
468
+ - **Cheapness.** A member below the roster's other tiers earns their place by taking work the dearer members would otherwise do. A Tier 1 member who runs the migration and the flag deletion is worth having even in a graph that is never two phases wide, because those two phases run at Tier 1 instead of Tier 2.
461
469
 
462
- **A member who does neither should not be on the roster** — a second mid in a graph that is never two wide adds no concurrency and saves nothing, and the executor refuses a member assigned no phase at all.
470
+ **A member who does neither should not be on the roster** — a second Tier 2 member in a graph that is never two wide adds no concurrency and saves nothing, and the executor refuses a member assigned no phase at all.
463
471
 
464
472
  Note what this does *not* say: the roster is not capped at the widest wave. Concurrency is capped there, but a cheaper member is bought with money, not with parallelism, and adding one to a narrow graph is a perfectly good trade — it now costs a worktree, which is the honest price (see "Every member gets a desk").
465
473
 
466
474
  Two more rules:
467
475
 
468
- - **Prefer fewer tiers over more.** Two mids beat one mid and one "upper-mid": a tier is a floor on what a member may be handed, so a roster that draws fine distinctions only makes it harder to cover for a busy peer.
476
+ - **Prefer fewer tiers over more.** Two members at Tier 2 beat one at Tier 2 and one at Tier 3 when both phases are Tier 2 work: a tier is a floor on what a member may be handed, so a roster that draws fine distinctions only makes it harder to cover for a busy peer.
469
477
  - **The `Takes` column is the assignment**, and it must agree with every phase's `**Assigned to**:` line. They are two renderings of one decision, so write the phases and let the table fall out — never the reverse.
470
478
 
471
479
  ### Assigning a phase
472
480
 
473
481
  Two constraints, and they pull against each other. That is what makes this a judgement rather than a lookup.
474
482
 
475
- **The rubric tier is a floor.** Never hand a Tier 3 phase to a junior to save money. A cheap model on work above its tier does not fail cleanly — it produces plausible code that fails review two rounds later, and by then nothing points back at the staffing decision. Default to the cheapest tier that plausibly works; that is not the same as the cheapest tier available.
483
+ **The rubric tier is a floor.** Never hand a Tier 3 phase to a Tier 1 member to save money. A cheap model on work above its tier does not fail cleanly — it produces plausible code that fails review two rounds later, and by then nothing points back at the staffing decision. Default to the cheapest tier that plausibly works; that is not the same as the cheapest tier available.
476
484
 
477
- **Nobody should be idle while work they could do is queued.** If the senior is busy from wave 1 to wave 4 and the two juniors are idle after wave 1, the plan is a senior-shaped chain with decoration. Rebalance by splitting a phase or moving a dependency, not by handing the senior's work to a junior.
485
+ **Nobody should be idle while work they could do is queued.** If the Tier 4 member is busy from wave 1 to wave 4 and the two Tier 1 members are idle after wave 1, the plan is a Tier 4-shaped chain with decoration. Rebalance by splitting a phase or moving a dependency, not by handing Tier 4 work down to Tier 1.
478
486
 
479
487
  When those two genuinely cannot both hold, **the floor wins and the plan says so** in the **Idle** note. A wave that serializes is a schedule; a phase run below its tier is a defect.
480
488
 
@@ -562,7 +570,17 @@ Don't mix styles within one sentence. In **Touch List**, use `@path` for new fil
562
570
 
563
571
  ## Emit the executable workflow
564
572
 
565
- Alongside the markdown plan, write `ai-plans/<feature-kebab>.workflow.json` — same directory, feature name lowercased with hyphens (`BOOKMARK_FOLDERS` → `ai-plans/bookmark-folders.workflow.json`), no date prefix. The markdown is what humans review; the JSON is the same phase graph in the form an orchestrator runs — one worktree lane per phase, branches cut from each phase's dependencies, gates queued behind capacity limits instead of stampeding.
573
+ Alongside the markdown plan, write `ai-plans/{TODAY}-<feature-kebab>.workflow.json` — same directory, same date prefix as the plan and the spec, feature name lowercased with hyphens (`2026-03-04` + `BOOKMARK_FOLDERS` → `ai-plans/2026-03-04-bookmark-folders.workflow.json`). The markdown is what humans review; the JSON is the same phase graph in the form an orchestrator runs — one worktree lane per phase, branches cut from each phase's dependencies, gates queued behind capacity limits instead of stampeding.
574
+
575
+ **The date prefix is what keeps the three files together.** `ai-plans/` accumulates every feature this repo has ever planned, and a dateless `bookmark-folders.workflow.json` sorts into a different part of the directory from the `2026-03-04-BOOKMARK_FOLDERS_PLAN.md` it belongs to — so the one file you need when the plan is in front of you is the one you have to search for. With the prefix a feature's three files land adjacent:
576
+
577
+ ```
578
+ ai-plans/2026-03-04-BOOKMARK_FOLDERS_PLAN.md
579
+ ai-plans/2026-03-04-BOOKMARK_FOLDERS_SPEC.md
580
+ ai-plans/2026-03-04-bookmark-folders.workflow.json
581
+ ```
582
+
583
+ The case difference is not an inconsistency to fix: the markdown convention is `UPPERCASE_WITH_UNDERSCORES`, and the workflow's stem **is** its `id`, which the schema requires to be lowercase kebab-case. They cannot be spelled the same way, so they share the one part that can be shared.
566
584
 
567
585
  **Unconditional.** Write it on every plan. Don't ask, don't gate it on a config field, don't skip it because the project has no orchestrator installed — a project without one carries a few KB it never reads, and a project that installs one later finds its plans already executable. The one thing that is *not* free is emitting it inconsistently: a half-populated `ai-plans/` teaches the team the file is optional.
568
586
 
@@ -576,7 +594,7 @@ First key in the file is `"$schema"`, pointing at `https://github.com/vintasoftw
576
594
  |---|---|
577
595
  | `$schema` | The URL above, literally. |
578
596
  | `schema_version` | `1`. |
579
- | `id` | The feature kebab — same slug as the filename. It lands in branch names (`plan/{id}/wave-2`), so kebab-case only, no dates, no underscores. |
597
+ | `id` | `{TODAY}-<feature-kebab>` — the filename's stem, exactly. Digits and hyphens only, no underscores and no uppercase: it is both the filename the daemon resolves and the slug in every branch name (`plan/2026-03-04-bookmark-folders/wave-2`), and the daemon refuses a workflow whose `id` and filename disagree. |
580
598
  | `plan_ref` | Repo-relative path of the markdown plan, e.g. `ai-plans/2026-03-04-BOOKMARK_FOLDERS_IMPLEMENTATION_PLAN.md`. |
581
599
  | `plan_context_refs` | The two plan sections that bound **every** phase, as anchors into the plan you just wrote: `<plan_ref>#1-goals` (which carries Non-goals with it) and `<plan_ref>#2-guiding-decisions`, in that order. Same file-and-anchor form as `prompt_ref`. See "Plan-level context". |
582
600
  | `base_branch` | What `**Depends on**: nothing — starts from the base branch` means concretely: the repo's default branch, unless **Guiding Decisions** names a long-lived feature branch. |
@@ -589,6 +607,10 @@ First key in the file is `"$schema"`, pointing at `https://github.com/vintasoftw
589
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. |
590
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. |
591
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
+ | `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. |
612
+ | `defaults.chores` | `["deslop"]`, plus `"review-canvas"` when that chore is emitted. The chores every phase runs unless it names its own. |
613
+ | `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. |
592
614
  | `nodes[]` | One per phase, in plan order. |
593
615
  | `nodes[].id` | `p` + the phase number, lowercased: `Phase 1` → `p1`, `Phase 4a` → `p4a`, `Phase 1b` → `p1b`. |
594
616
  | `nodes[].name` | The phase title without its `Phase N —` prefix. |
@@ -598,7 +620,7 @@ First key in the file is `"$schema"`, pointing at `https://github.com/vintasoftw
598
620
  | `nodes[].gates` | The gate ids this phase must pass, in the order they should run. |
599
621
  | `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. |
600
622
  | `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`. |
601
- | `nodes[].max_fix_rounds` | Omit (defaults to 2). Set it higher only on a phase whose review you expect to iterate — a delicate migration, a concurrency protocol. |
623
+ | `nodes[].max_fix_rounds` | Omit (defaults to 4). When a phase spends it, the executor asks the operator whether to continue rather than failing the phase, so this is how often somebody is asked, not a hard cap. Set it higher only on a phase whose review you expect to iterate — a delicate migration, a concurrency protocol. |
602
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. |
603
625
  | `pipelines` | **Omit.** The executor ships `standard-phase` — see "The pipeline block". |
604
626
 
@@ -608,6 +630,7 @@ Rules the mapping depends on:
608
630
  - **`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.
609
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.
610
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 table row with no `crew` entry, means the two were edited separately.
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 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.
611
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.
612
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.
613
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.
@@ -679,6 +702,21 @@ The remaining values — `migrate_cmd`, the database names, the server URL, the
679
702
 
680
703
  Author a `pipelines` block only when a project genuinely needs a *different* lifecycle. That is an executor-configuration decision made once per project, not a per-plan choice, and a declared id shadows the shipped pipeline of the same name.
681
704
 
705
+ ### The review-canvas chore
706
+
707
+ Emit this chore only when `.vinta-ai-workflows.yaml` has `integrations.pr-review-canvas: enabled`. Then add `"review-canvas"` to `defaults.chores` after `"deslop"`:
708
+
709
+ ```json
710
+ "review-canvas": {
711
+ "skill": "pr-review-canvas",
712
+ "when": "after_pr",
713
+ "prompt": "Generate the review canvas for this phase's pull request: run `/pr-review-canvas` with the PR number named above. Report the local review URL and the PR comment link it returns. Do not change any file in the repository.",
714
+ "description": "A topic-grouped review canvas on each phase PR, for the human reviewer."
715
+ }
716
+ ```
717
+
718
+ `"when": "after_pr"` is what makes this work. Maestro runs the chore at the end of `integrate`, once the PR is open, and tells the agent the PR's URL and number. When no PR opened, it skips the chore. Do not give it `on_failure: "fail"`; maestro refuses that for an `after_pr` chore, because the phase is already merged. The integration never changes how the plan's phases are written, so the worked example below leaves this chore out.
719
+
682
720
  ### Worked example
683
721
 
684
722
  A five-phase plan whose `**Depends on**:` lines are:
@@ -705,18 +743,18 @@ staffed by this **Crew** table:
705
743
  ```markdown
706
744
  | Agent | Role | Tier | Takes | Why this tier |
707
745
  |---|---|---|---|---|
708
- | `junior` | implementer | 1 | Phase 1, Phase 5 | A model plus its migration, and a flag deletion. Exact precedent, both. |
709
- | `mid-1` | implementer | 2 | Phase 2 | A DRF viewset mirroring the tags viewset almost line for line. |
710
- | `mid-2` | implementer | 2 | Phase 3, Phase 4 | Tree serializer and the list action that returns it — same shape twice. |
746
+ | `tier1` | implementer | 1 | Phase 1, Phase 5 | A model plus its migration, and a flag deletion. Exact precedent, both. |
747
+ | `tier2-1` | implementer | 2 | Phase 2 | A DRF viewset mirroring the tags viewset almost line for line. |
748
+ | `tier2-2` | implementer | 2 | Phase 3, Phase 4 | Tree serializer and the list action that returns it — same shape twice. |
711
749
  | `reviewer` | reviewer | 2 | — reviews every phase | Tier 2 is the hardest phase here, so one reviewer covers the plan. |
712
750
  ```
713
751
 
714
752
  Three implementers for a graph that is never more than **two** phases wide,
715
- which is the case worth reading closely. The two mids are there for concurrency:
716
- wave 2 is two Tier 2 phases, and two hands at Tier 2 is the only way to run it
717
- two-wide — a junior cannot take one of them. `junior` is there for cheapness:
718
- without them, Phase 1 and Phase 5 would run on a mid's model for work that has
719
- exact precedent. `junior` is idle in waves 2 and 3 and that is not a defect; a
753
+ which is the case worth reading closely. The two Tier 2 members are there for
754
+ concurrency: wave 2 is two Tier 2 phases, and two hands at Tier 2 is the only
755
+ way to run it two-wide — `tier1` cannot take one of them. `tier1` is there for
756
+ cheapness: without them, Phase 1 and Phase 5 would run on a Tier 2 model for
757
+ work that has exact precedent. `tier1` is idle in waves 2 and 3 and that is not a defect; a
720
758
  fourth implementer would be, because there would be nothing left for them to
721
759
  make cheaper and no third phase for them to run alongside.
722
760
 
@@ -731,22 +769,22 @@ and this **Execution graph** table:
731
769
  ```markdown
732
770
  | Wave | Phases | Agent | Depends on |
733
771
  |---|---|---|---|
734
- | 1 | Phase 1 | `junior` | — |
735
- | 2 | Phase 2, Phase 3 | `mid-1`, `mid-2` | Phase 1 |
736
- | 3 | Phase 4 | `mid-2` | Phase 2, Phase 3 |
737
- | 4 | Phase 5 — remove the `bookmark-folders` flag | `junior` | Phase 2, Phase 3, Phase 4 (deferred — soak-gated) |
772
+ | 1 | Phase 1 | `tier1` | — |
773
+ | 2 | Phase 2, Phase 3 | `tier2-1`, `tier2-2` | Phase 1 |
774
+ | 3 | Phase 4 | `tier2-2` | Phase 2, Phase 3 |
775
+ | 4 | Phase 5 — remove the `bookmark-folders` flag | `tier1` | Phase 2, Phase 3, Phase 4 (deferred — soak-gated) |
738
776
 
739
- **Idle:** both mids in wave 1 — the model has to exist before anything reads it.
740
- `mid-1` in wave 3, `junior` in waves 2 and 3.
777
+ **Idle:** both Tier 2 members in wave 1 — the model has to exist before anything
778
+ reads it. `tier2-1` in wave 3, `tier1` in waves 2 and 3.
741
779
  ```
742
780
 
743
- and this `ai-plans/bookmark-folders.workflow.json`:
781
+ and this `ai-plans/2026-03-04-bookmark-folders.workflow.json`:
744
782
 
745
783
  ```json
746
784
  {
747
785
  "$schema": "https://github.com/vintasoftware/vinta-ai-workflows/schemas/workflow.v1.schema.json",
748
786
  "schema_version": 1,
749
- "id": "bookmark-folders",
787
+ "id": "2026-03-04-bookmark-folders",
750
788
  "plan_ref": "ai-plans/2026-03-04-BOOKMARK_FOLDERS_IMPLEMENTATION_PLAN.md",
751
789
  "plan_context_refs": [
752
790
  "ai-plans/2026-03-04-BOOKMARK_FOLDERS_IMPLEMENTATION_PLAN.md#1-goals",
@@ -773,19 +811,19 @@ and this `ai-plans/bookmark-folders.workflow.json`:
773
811
  }
774
812
  },
775
813
  "crew": {
776
- "junior": {
814
+ "tier1": {
777
815
  "role": "implementer",
778
816
  "tier": 1,
779
817
  "model": "claude-haiku-4-5",
780
818
  "description": "A model plus its migration, and a flag deletion."
781
819
  },
782
- "mid-1": {
820
+ "tier2-1": {
783
821
  "role": "implementer",
784
822
  "tier": 2,
785
823
  "model": "claude-sonnet-5",
786
824
  "description": "A DRF viewset mirroring the tags viewset."
787
825
  },
788
- "mid-2": {
826
+ "tier2-2": {
789
827
  "role": "implementer",
790
828
  "tier": 2,
791
829
  "model": "claude-sonnet-5",
@@ -801,7 +839,10 @@ and this `ai-plans/bookmark-folders.workflow.json`:
801
839
  "defaults": {
802
840
  "harness": "claude-code",
803
841
  "model": "claude-sonnet-5",
804
- "pipeline": "standard-phase"
842
+ "pipeline": "standard-phase",
843
+ "chores": [
844
+ "deslop"
845
+ ]
805
846
  },
806
847
  "resources": {
807
848
  "lane": {
@@ -828,6 +869,13 @@ and this `ai-plans/bookmark-folders.workflow.json`:
828
869
  "timeout_s": 1800
829
870
  }
830
871
  },
872
+ "chores": {
873
+ "deslop": {
874
+ "skill": "deslop-comments",
875
+ "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.",
876
+ "description": "The comment-hygiene pass, on the diff that is about to merge."
877
+ }
878
+ },
831
879
  "nodes": [
832
880
  {
833
881
  "id": "p1",
@@ -843,7 +891,7 @@ and this `ai-plans/bookmark-folders.workflow.json`:
843
891
  "types",
844
892
  "unit"
845
893
  ],
846
- "crew": "junior"
894
+ "crew": "tier1"
847
895
  },
848
896
  {
849
897
  "id": "p2",
@@ -864,7 +912,7 @@ and this `ai-plans/bookmark-folders.workflow.json`:
864
912
  "types",
865
913
  "unit"
866
914
  ],
867
- "crew": "mid-1"
915
+ "crew": "tier2-1"
868
916
  },
869
917
  {
870
918
  "id": "p3",
@@ -884,7 +932,7 @@ and this `ai-plans/bookmark-folders.workflow.json`:
884
932
  "types",
885
933
  "unit"
886
934
  ],
887
- "crew": "mid-2"
935
+ "crew": "tier2-2"
888
936
  },
889
937
  {
890
938
  "id": "p4",
@@ -909,7 +957,7 @@ and this `ai-plans/bookmark-folders.workflow.json`:
909
957
  "types",
910
958
  "unit"
911
959
  ],
912
- "crew": "mid-2"
960
+ "crew": "tier2-2"
913
961
  },
914
962
  {
915
963
  "id": "p5",
@@ -940,7 +988,7 @@ and this `ai-plans/bookmark-folders.workflow.json`:
940
988
  "types",
941
989
  "unit"
942
990
  ],
943
- "crew": "junior"
991
+ "crew": "tier1"
944
992
  }
945
993
  ]
946
994
  }
@@ -948,15 +996,15 @@ and this `ai-plans/bookmark-folders.workflow.json`:
948
996
 
949
997
  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.
950
998
 
951
- No node carries a `model`. `p1` and `p5` are the Tier 1 phases and run on `junior`'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.
999
+ 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.
952
1000
 
953
- `mid-2` takes `p3` and then `p4`, which is the pairing worth seeing: the same agent writes the tree serializer and the action that returns it, in the same worktree, **continuing the same session**. Its second phase does not pay to work out where the serializers live or how the suite is run — it did that in `p3`. What it *is* told, because the tree moved underneath it, is that it is now on `p4`'s branch, that `p3`'s work is in this tree (`p4` depends on it), and exactly which files differ from what it last saw.
1001
+ `tier2-2` takes `p3` and then `p4`, which is the pairing worth seeing: the same agent writes the tree serializer and the action that returns it, in the same worktree, **continuing the same session**. Its second phase does not pay to work out where the serializers live or how the suite is run — it did that in `p3`. What it *is* told, because the tree moved underneath it, is that it is now on `p4`'s branch, that `p3`'s work is in this tree (`p4` depends on it), and exactly which files differ from what it last saw.
954
1002
 
955
1003
  No node names a reviewer. Every phase is read by `reviewer`, the only member on the roster whose role is to read them — so no agent ever reads its own diff. Its session carries only between reviews that land in the same lane, because it goes where the work is rather than keeping a tree of its own.
956
1004
 
957
1005
  `plan_context_refs` points at the same plan file the `prompt_ref`s do, at its **Goals** and **Guiding Decisions** headings. Each of the five phases is handed those two sections whole, so the implementer of `p3` knows that the tree serializer is deliberately not paginated if the plan's Non-goals said so, and the reviewer of `p3` can call a paginated one scope creep instead of a bonus.
958
1006
 
959
- The lane pool is **three**: one desk per implementer, kept for the whole run so each one's session has a directory to come back to. Only two are ever busy at once — the graph is never wider than that — and the third idle desk is the price of `junior`'s session. `reviewer` has no desk at all; it reads whichever lane it is reviewing, changes still uncommitted, which is what lets a finding be fixed before the commit rather than after it. The `project` block is what lets those worktrees exist at once. `bookmarks_test` is forked per lane from a template that `uv run python manage.py migrate` builds once, so `p2` and `p3` run `uv run pytest` against separate rows instead of the same ones; `test-suite` stays at capacity 1 because three suites at once melt the machine, not because they would corrupt each other. `dev` and `test` name two different databases, which is what keeps their forks from being the same database under two roles.
1007
+ The lane pool is **three**: one desk per implementer, kept for the whole run so each one's session has a directory to come back to. Only two are ever busy at once — the graph is never wider than that — and the third idle desk is the price of `tier1`'s session. `reviewer` has no desk at all; it reads whichever lane it is reviewing, changes still uncommitted, which is what lets a finding be fixed before the commit rather than after it. The `project` block is what lets those worktrees exist at once. `bookmarks_test` is forked per lane from a template that `uv run python manage.py migrate` builds once, so `p2` and `p3` run `uv run pytest` against separate rows instead of the same ones; `test-suite` stays at capacity 1 because three suites at once melt the machine, not because they would corrupt each other. `dev` and `test` name two different databases, which is what keeps their forks from being the same database under two roles.
960
1008
 
961
1009
  ## What to avoid
962
1010
 
@@ -1019,7 +1067,7 @@ When in doubt, model the plan after a recent example in `ai-plans/` — look for
1019
1067
  - [ ] Open Questions lists what couldn't resolve, with recommended default.
1020
1068
  - [ ] Touch List groups files by phase.
1021
1069
  - [ ] All file references use `@path/to/file.py` or `[name](relative-path#Lline)`.
1022
- - [ ] **`ai-plans/<feature-kebab>.workflow.json` written** — every plan, no exceptions — with `$schema` set to the canonical URL and `schema_version: 1`.
1070
+ - [ ] **`ai-plans/{TODAY}-<feature-kebab>.workflow.json` written** — every plan, no exceptions — with `$schema` set to the canonical URL and `schema_version: 1`.
1023
1071
  - [ ] Workflow graph matches the **Execution graph** table: one node per phase, one `depends_on` entry per `**Depends on**:` clause carrying both the node id and the artifact, same waves.
1024
1072
  - [ ] Every node has `prompt_ref` (`<plan_ref>#phase-<number>`), `touches` from its **Touch List** block, and the `gates` it must pass.
1025
1073
  - [ ] **`plan_context_refs` names the Goals and Guiding Decisions anchors** (`<plan_ref>#1-goals`, `<plan_ref>#2-guiding-decisions`), matching the headings as written — references, never a summary of them. Every phase's implementer and reviewer read them; a phase that doesn't know the non-goals is a phase that scope-creeps.
@@ -1027,4 +1075,6 @@ When in doubt, model the plan after a recent example in `ai-plans/` — look for
1027
1075
  - [ ] **`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.
1028
1076
  - [ ] No credential anywhere in the workflow file: `connection_url_var` is a variable name, and `server_url` is a host and port.
1029
1077
  - [ ] 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.
1078
+ - [ ] `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.
1079
+ - [ ] `chores.review-canvas` (with `"when": "after_pr"`) is declared and in `defaults.chores` **if and only if** `.vinta-ai-workflows.yaml` has `integrations.pr-review-canvas: enabled`.
1030
1080
  - [ ] `pipelines` is omitted — `defaults.pipeline: standard-phase` is enough, and the executor supplies it.
@@ -11,7 +11,7 @@
11
11
  # Tier *placement* is human/LLM judgement — the job flags drift and proposes
12
12
  # edits, it does not blindly reassign tiers. See scripts/check-ai-models.mjs.
13
13
  schema_version: 1
14
- last_verified: 2026-07-13
14
+ last_verified: 2026-09-16
15
15
 
16
16
  # Drift is checked against a free, no-key community aggregator (models.dev,
17
17
  # vendor-keyed with release dates + pricing), cross-checked against LiteLLM's
@@ -37,7 +37,7 @@ tiers:
37
37
  anthropic:
38
38
  - id: claude-haiku-4-5
39
39
  openai:
40
- - id: gpt-5.6-luna
40
+ - id: gpt-6-luna
41
41
  note: optimized for cost-sensitive workloads
42
42
  google:
43
43
  - id: gemini-3.1-flash-lite
@@ -50,13 +50,12 @@ tiers:
50
50
  fixtures, simple HStore/ArrayField additions.
51
51
  models:
52
52
  anthropic:
53
- - id: claude-haiku-4-5
54
53
  - id: claude-sonnet-5
55
54
  note: step-up target when phase touches >3 files
56
55
  openai:
57
- - id: gpt-5-mini
56
+ - id: gpt-6-luna
58
57
  google:
59
- - id: gemini-3.5-flash
58
+ - id: gemini-3.8-flash
60
59
 
61
60
  - tier: 3
62
61
  label: multi-file orchestration, business logic, SQL views
@@ -67,9 +66,9 @@ tiers:
67
66
  covering concurrency edges.
68
67
  models:
69
68
  anthropic:
70
- - id: claude-sonnet-5
69
+ - id: claude-opus-5-5
71
70
  openai:
72
- - id: gpt-5.6-terra
71
+ - id: gpt-6-sol
73
72
  google:
74
73
  - id: gemini-3-pro
75
74
  note: stronger code recall
@@ -82,10 +81,10 @@ tiers:
82
81
  tuning slow query against partitioned hot table, debugging heisenbug.
83
82
  models:
84
83
  anthropic:
85
- - id: claude-opus-4-8
84
+ - id: claude-fable-5-1
86
85
  openai:
87
- - id: gpt-5.6-sol
88
- note: Frontier model for complex professional work
86
+ - id: gpt-6-astra
87
+ note: frontier model; roughly twice sol's price, so reserve it for tier 4
89
88
  google:
90
89
  - id: gemini-3-pro
91
90
  note: with thinking
@@ -355,7 +355,7 @@ state:
355
355
  files: [.env, .envrc, ...]
356
356
  dev_db:
357
357
  engine: postgres | mysql | sqlite | mongo | redis
358
- strategy: fork | share | stub
358
+ strategy: fork | share | stub # closed set — see "The summary is read by machine"
359
359
  forked_name: <forked db name> # null when share / stub
360
360
  connection_url_var: DATABASE_URL
361
361
  reset_cmd: <shell command that returns this DB to the state of a fresh checkout>
@@ -365,7 +365,7 @@ state:
365
365
  # instead of reusing this worktree across a migration boundary.
366
366
  test_db:
367
367
  engine: ...
368
- strategy: fork | share
368
+ strategy: fork | share # closed set, as above
369
369
  forked_name: ...
370
370
  connection_url_var: TEST_DATABASE_URL
371
371
  reset_cmd: <same idea, for the test DB> # null when not resettable
@@ -467,6 +467,14 @@ Every step gated on user confirmation when the worktree has un-pushed branches.
467
467
 
468
468
  - **Symlink for reads, copy for writes, fork for state.** This is the only mental model that scales. Default to fork when unsure — disk is cheap, corrupted main-checkout DBs are not.
469
469
  - **Never share a writable DB across worktrees by default.** The race conditions are subtle and the failure mode is silent data corruption.
470
+ ### The summary is read by machine
471
+
472
+ This file is not a note to the next human. [vinta-ai-maestro](https://github.com/vintasoftware/vinta-ai-workflows/tree/main/packages/vinta-ai-maestro) parses it to decide whether a lane can be handed to another phase, so **every `|` above is a closed set and every key is required** — `null` is how you say "none", and omitting the key is not the same thing.
473
+
474
+ A value outside the set makes the lane unusable by the daemon. It does not fail loudly at write time; it fails later, as `doctor` reporting the lane and the field it could not accept, long after the worktree and its forked databases exist. Seen in the wild: a `strategy: create-empty-and-migrate` invented to describe a database that had been created empty and migrated by hand, with `reset_cmd` left off entirely. Neither is in the spec, and the pair cost eleven lanes their readability.
475
+
476
+ If none of the listed strategies describes what you did, **the honest record is the closest one plus a `note:`** — the schema keeps `note` free-form for exactly this. A database created empty and migrated is a `fork` whose `reset_cmd` re-runs that same creation; a database you could not provision at all is a `stub`. What it must never be is a fourth word.
477
+
470
478
  - **`COMPOSE_PROJECT_NAME` does not isolate `external:`/fixed-`name:` volumes or fixed host ports — always run the [4a](#4a--neutralize-compose-isolation-leaks) generator.** For a compose-delivered DB the data volumes are forked *unconditionally* (independent of `schema_change` and `test_db_strategy`): the worktree runs its own server, and two servers on one volume is corruption. "Share the DB" only ever means share a connection to a single already-running server, never a second server on shared storage. The fix is a generated, out-of-tree override + a `COMPOSE_FILE` line in the copied `.env` — never an edit to any tracked compose file. Verify the volume-differs invariant before declaring the worktree runnable.
471
479
  - **Every fork decision lands in `.vinta-ai-workflows/worktrees/<name>.yaml`.** Teardown reads it; humans grep it; agents resuming a stalled plan read it. No decision lives only in conversation memory.
472
480
  - **Worktree root governed by runtime conventions.** claude-code uses `.claude/worktrees/`; other harnesses use sibling dirs. Don't fight the harness — match it.
@@ -41,6 +41,17 @@ The conductor resolves three values **once per lane** (see `partials/worktree-se
41
41
 
42
42
  Every `git` / lint / test / build call in every sub-skill uses `git -C <WORKROOT>` **uniformly** — no `if use_worktree` inside them. Only two genuine conditionals remain, each local and data-driven: the `SANDBOX_TIER`-gated spawn wrap in `implement-phase`, and the `WORKROOT != main_checkout`-gated stray-write check in `review-phase`.
43
43
 
44
+ ## The dispatched-agent seam (`partials/dispatched-agent.md`)
45
+
46
+ One rule, two shapes, because it has to hold at both ends of the unit: **the agent doing a phase never dispatches.**
47
+
48
+ | Block | Consumed by | What it owns |
49
+ |---|---|---|
50
+ | `CONDUCTOR_ENTRY_GUARD` | `implement-plan`, `implement-phase`, `review-phase`, `amend-plan` | Refuses entry to an agent that was itself handed one phase — by another conductor, or by an external orchestrator such as [vinta-ai-maestro](https://github.com/vintasoftware/vinta-ai-workflows/tree/main/packages/vinta-ai-maestro). The conductor is already running; it is what spawned the reader. |
51
+ | `NO_NESTED_DISPATCH` | `implementer-prompt.md#INNER_OUTER_LOOP` → the composed prompt in `implement-phase` and `amend-plan` 4b | Tells the spawned implementer to do the work in its own session. |
52
+
53
+ The cost this prevents is not duplicated orchestration — it is lost context. A phase agent is reused (review findings, a chore over its own diff, often the next phase), and that reuse only buys anything because the session that read the codebase is the session that gets the next turn. A sub-agent's reading dies with the sub-agent, leaving the parent holding a summary and every later turn starting cold.
54
+
44
55
  ## The parallel-lanes seam (`partials/parallel-lanes.md`)
45
56
 
46
57
  The plan gives every phase a `**Depends on**:` line. The conductor turns those into a DAG and dispatches a phase the moment its dependencies are green and a lane is free. Six blocks:
@@ -17,6 +17,18 @@ A chain-shaped plan reproduces the classic stack exactly — each phase depends
17
17
  **PR base per phase** (the `base` field written into the prs-context frontmatter — this is what `gh pr create --base` / `glab mr create --target-branch` opens the PR against; getting it wrong makes the PR diff include every upstream phase and the review unusable):
18
18
 
19
19
  - `base = <phase.base_branch>`, always. Never `<BASE_BRANCH>` for a phase that has dependencies.
20
+
21
+ **Every branch the plan lands through gets a PR.** Phase PRs alone do not reach `<BASE_BRANCH>`: a phase based on `integ-{phase.id}` targets a branch that nothing else targets, and the conflict resolutions made during wave merges are on no phase branch at all. So a stacked run opens three kinds of PR:
22
+
23
+ | PR | `branch` (head) | `base` | prs-context file | Opened |
24
+ |---|---|---|---|---|
25
+ | Phase | `plan/{plan-id-kebab}/phase-{phase.id}` | `<phase.base_branch>` | `phase-{phase.id}.md` | when the phase passes review |
26
+ | Integration | `plan/{plan-id-kebab}/integ-{phase.id}` | `<BASE_BRANCH>` | `integ-{phase.id}.md` (`kind: integration`) | just before the phase PR, only for a phase with two or more dependencies |
27
+ | Plan | the final `plan/{plan-id-kebab}/wave-{N}` | `<BASE_BRANCH>` | `plan.md` (`kind: plan`) | once, at run end, after the final wave branch is built |
28
+
29
+ The **integration PR** is based on `<BASE_BRANCH>` because the `integ-` branch has several parents and none of them is "below" it. Its diff starts as all of its dependencies and shrinks, as their PRs merge, to the merge commits and any conflict resolution the fixer made. Its description says: merge the dependencies' PRs first, then this one, then retarget the phase PR to `<BASE_BRANCH>`.
30
+
31
+ The **plan PR** is the one branch known to hold the whole plan. Its description lists every phase and integration PR in an order that merges (wave by wave; inside a wave, plan order; each integration PR right before its phase PR) and offers two ways to land: merge the plan PR alone, or merge the listed PRs in order and the plan PR last — by then its diff is only what no other PR carries. Either way, **merge commits, not squash**: a squash makes every PR stacked above it repeat the changes below it.
20
32
  <!-- block-end: BRANCH_NAMING -->
21
33
 
22
34
  <!-- block-begin: PER_PHASE_COMMIT -->
@@ -28,11 +40,17 @@ A chain-shaped plan reproduces the classic stack exactly — each phase depends
28
40
 
29
41
  <!-- block-begin: PR_OPEN_TIMING -->
30
42
  One PR per phase — the [Open PR via context file](#open-pr-via-context-file) step runs after this phase passes review, writing `.vinta-ai-workflows/prs-context/{feature-kebab}/phase-{phase.id}.md`.
43
+
44
+ **When `<phase.base_branch>` is an `integ-` branch, run that step twice — integration PR first.** Push `plan/{plan-id-kebab}/integ-{phase.id}`, then write `.vinta-ai-workflows/prs-context/{feature-kebab}/integ-{phase.id}.md` with `kind: integration`, `branch: plan/{plan-id-kebab}/integ-{phase.id}`, `base: <BASE_BRANCH>`, a `# Title` of `Integrate <dep ids> for <phase title>`, and a `# Description` naming the dependencies and the merge order (dependencies' PRs → this PR → retarget the phase PR to `<BASE_BRANCH>`). Run `open-pr.sh` on it, then on the phase file. Skip the inline-comment pass for the integration file — its diff is other phases' work.
45
+
46
+ The **plan PR** is not this step's job — the conductor opens it at run end, once the final wave branch exists.
31
47
  <!-- block-end: PR_OPEN_TIMING -->
32
48
 
33
49
  <!-- block-begin: CHECKLIST -->
34
50
  - [ ] Phase branch created from `<phase.base_branch>` (dependency-derived), not from plan order.
35
51
  - [ ] PR `base` in the prs-context frontmatter equals `<phase.base_branch>`.
52
+ - [ ] Phase based on an `integ-` branch: the `integ-` branch pushed and its integration PR (`integ-{phase.id}.md`, `base: <BASE_BRANCH>`) opened before the phase PR.
53
+ - [ ] Run end: plan PR (`plan.md`, final wave branch → `<BASE_BRANCH>`) opened, listing every phase and integration PR in merge order.
36
54
  <!-- block-end: CHECKLIST -->
37
55
 
38
56
  <!-- Single-line values (derive-skills substitutes these into the shells directly):
@@ -0,0 +1,31 @@
1
+ <!-- Partial: dispatched-agent — the two halves of "the agent doing one phase does not dispatch". CONDUCTOR_ENTRY_GUARD stops a dispatched phase agent from re-entering a conductor skill one level down (implement-plan / implement-phase / review-phase / amend-plan). NO_NESTED_DISPATCH is prompt text: it goes inside the composed implementer prompt, via implementer-prompt.md#INNER_OUTER_LOOP, so both implement-phase's prompt and amend-plan's 4b rewrite prompt carry it. Project-agnostic — no `{{...}}` placeholders of its own. -->
2
+
3
+ <!-- block-begin: CONDUCTOR_ENTRY_GUARD -->
4
+ ## Not for an agent that was handed one phase
5
+
6
+ This skill **orchestrates**: it composes prompts, picks models and spawns other agents. Run it only when you are the session a user or a scheduler invoked to drive a plan.
7
+
8
+ If you are reading it because something handed you a single phase — a prompt naming your phase id, a branch already cut for you, a worktree you were told to stay inside, or an orchestrator such as [vinta-ai-maestro](https://github.com/vintasoftware/vinta-ai-workflows/tree/main/packages/vinta-ai-maestro) that spawned you — then the conductor this skill describes **is already running, and it is what spawned you**. Do not start a second one underneath it. Do the work in your own session, report back the way your prompt asked, and take from here only what it says about this repository's conventions, gates and commit rules.
9
+
10
+ The duplication is the smaller cost. A dispatched agent is deliberately reused — the same session takes the review findings, the chore over its own diff, and often the next phase — and that reuse is worth something only because the session that read the codebase is the session that gets the next turn. Hand your phase to a sub-agent and its reading of the code dies with it: you are left holding a summary, and every turn after yours starts cold.
11
+ <!-- block-end: CONDUCTOR_ENTRY_GUARD -->
12
+
13
+ <!-- block-begin: NO_NESTED_DISPATCH -->
14
+ ## Do this work yourself
15
+
16
+ You are the agent that implements this phase, not an orchestrator for one. Do not spawn,
17
+ dispatch or delegate to a sub-agent (claude-code's Task/Agent tool, or whatever your
18
+ runtime calls the same thing) for any part of it: not the implementation, not a search of
19
+ the codebase, not a second opinion on your own output. Read, run and write yourself.
20
+
21
+ The orchestrator reuses this session — for the review findings, for a chore over your own
22
+ diff, often for the next phase — precisely because by then you know where this codebase
23
+ keeps things and how its suite is run. A sub-agent's reading of the code ends when the
24
+ sub-agent does, so a delegated phase leaves you holding its summary and nothing else, and
25
+ every turn after this one starts cold.
26
+
27
+ A project skill that tells you to spawn an implementer, reviewer or fixer —
28
+ `implement-plan`, `implement-phase`, `review-phase`, `amend-plan`, anything shaped like
29
+ them — is written for the orchestrator that dispatched you, not for you. Take what it says
30
+ about conventions, gates and commit rules; never follow its spawn steps.
31
+ <!-- block-end: NO_NESTED_DISPATCH -->
@@ -85,4 +85,6 @@ Project skills available: {{PROJECT_SKILLS_LIST}}
85
85
  {If run_options.full_test_suite = true:} run the **full test suite** `{{TEST_CMD}}` instead of the scoped suite — this phase guards against regressions in untouched code too.
86
86
  {{E2E_OUTER_GATE_LINE}}
87
87
  6. Outer gate fails → return step 2 (fix regression), re-run inner loop, then 5a/5b/5c. **Never** commit, push, or proceed while any gate is red.
88
+
89
+ <!-- include: partials/dispatched-agent.md#NO_NESTED_DISPATCH -->
88
90
  <!-- block-end: INNER_OUTER_LOOP -->