vinta-ai-workflows 0.7.0-alpha20 → 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.
package/CHANGELOG.md CHANGED
@@ -30,6 +30,202 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
30
30
 
31
31
  ### Added
32
32
 
33
+ - **PR Review Canvas integration (`integrations.pr-review-canvas`).** The
34
+ bootstrap now offers [PR Review Canvas](https://github.com/vintasoftware/pr-review-canvas)
35
+ as its first *integration*: an external tool that installs its own skill.
36
+ Nothing from it is bundled here. A new optional top-level `integrations`
37
+ object in `vinta-ai-workflows-config.v1.schema.json` records the opt-in
38
+ (`enabled` / `disabled`). It is additive, so no schema major bump. The
39
+ question is interview group D.2. It is asked only when `code_host` is
40
+ GitHub or GitLab and `pr_creation` is `agents-create`.
41
+ When it is enabled:
42
+ - `vinta-install-ai-tools-setup` step 7b runs
43
+ `pr-review install-skill --claude-dir ai-tools/skills --codex-dir ai-tools/skills`.
44
+ It writes one tool-owned copy that every vendor symlink reaches, then
45
+ runs `pr-review doctor`. The CLI is never auto-installed; the step prints
46
+ `npm install -g @vintasoftware/pr-review-canvas` instead.
47
+ - The `integrate-phase` PR step runs `/pr-review-canvas <n>` after
48
+ `open-pr.sh` publishes. This is a runtime gate in `partials/pr-context.md`,
49
+ so enabling the integration later needs no re-derive.
50
+ - `plan-feature` adds an `after_pr` `review-canvas` chore to the maestro
51
+ workflow it writes.
52
+
53
+ `vinta-analyze-codebase`, `vinta-derive-skills` and
54
+ `vinta-update-project-skills` treat an integration-owned skill (marker
55
+ `.pr-review-install`) as the tool's and never diff or edit it. Refresh it
56
+ with `pr-review upgrade`. `vinta-sync-ai-tools` offers the integration to
57
+ existing projects as an `opt-in-offer`.
58
+ - **Maestro: System One classifiers (SPEC §17).** A run can now consult a
59
+ fast classifier: one that answers yes/no or scores a fixed set of labels.
60
+ Three things use it.
61
+ - **Configuration.** `run`, `serve` and `doctor` take
62
+ `--system-one <config.json>`. The file lives on the operator's machine and
63
+ is never part of the plan. It names the classifier adapter: `http` (POST,
64
+ bearer key read from the env var you name) or `command` (a local process,
65
+ JSON on stdin and stdout). Out-of-tree adapters register through
66
+ `registerSystemOneAdapter`. The file also turns on the built-in judges.
67
+ This is the only API key the package uses; LLM harnesses still run on your
68
+ logged-in CLIs.
69
+ - **Judge gates.** A workflow gate can be
70
+ `{ "judge": { "question" | "question_ref", "labels", "fail_on",
71
+ "threshold", "on_unavailable" } }` instead of `{ "cmd": … }`. It asks the
72
+ plan's question about the lane's diff and reports exit 0 or 1, so
73
+ pipelines, the fix loop and the fixer prompt are unchanged. A judge gate
74
+ can only fail a phase, never pass one that a command gate failed. Agents
75
+ are not told to run judge gates. Without `--system-one`, a judge gate
76
+ falls back to its `on_unavailable` setting (`pass` by default).
77
+ `schemas/workflow.v1.schema.json` is regenerated (`gates` values are now
78
+ `anyOf` command | judge). The change is additive: existing gates are
79
+ unchanged.
80
+ - **Gate triage** (`judges.gate_triage`). When a command gate fails, the
81
+ classifier reads the end of its log. If it calls the failure flaky or
82
+ environmental, the gate is rerun once before a fixer is spent. The result
83
+ is exposed as `gate.triage`.
84
+ - **`--permission judged`** (claude-code only). Agents run without vendor
85
+ prompts. A `PreToolUse` hook asks the classifier about each judged tool
86
+ call (default `Bash`) through a new `POST /api/runs/:runId/permission`
87
+ endpoint. It fails closed at every step. It needs `judges.permission` in
88
+ the config. `doctor` refuses it for codex or opencode phases. This is a
89
+ speed trade-off, not a sandbox.
90
+
91
+ Every judgement is journalled as `system_one_judged`, with labels and
92
+ scores only, never the diff, log or command. With a hosted classifier,
93
+ diffs, gate logs and shell commands leave the machine. Use the `command`
94
+ adapter with a local model where that is not acceptable.
95
+ - **Maestro: chores that run after the PR opens (`when: "after_pr"`).** A
96
+ chore can now declare `when`. The default, `before_gate`, keeps today's
97
+ `polish` timing. `after_pr` runs the chore at the end of `standard-phase`'s
98
+ `integrate` state, after `open_pr`, for work about the PR rather than the
99
+ diff, such as a review canvas. `open_pr` now states `pr.opened` / `pr.url` /
100
+ `pr.number` as facts, so a guard can read them, and the `pr` root joins the
101
+ guard context. An `after_pr` chore's prompt names the PR and forbids
102
+ editing, committing or pushing. It is skipped when no PR opened. The
103
+ validator refuses `after_pr` with `on_failure: "fail"`, because the phase is
104
+ already merged. `run_chore` takes a `when` param. Custom pipelines that
105
+ call it without one keep running the `before_gate` chores.
106
+ `schemas/workflow.v1.schema.json` is regenerated, and the change is
107
+ additive.
108
+ - **Maestro: the node view links the phase's PR.** `GET
109
+ /api/runs/:runId/nodes/:nodeId` now carries `pullRequest` (`opened`, `url`,
110
+ `number`, `reason`), read from the latest `node_pr` event. The **Changes**
111
+ card shows `#<n>` as a link, or says why no PR opened (`gh` missing or
112
+ failed). Only an `https://` URL becomes a link. The link carries
113
+ `rel="noreferrer noopener"` because the page URL holds the daemon token.
114
+ - **The node view shows what a phase changed, and the whole diff on request.**
115
+ The *Diff* panel used to print `git diff base...branch` for the operator to
116
+ paste into a terminal. It is now a *Changes* card: how many files changed
117
+ and how many lines were added and removed, then each file — up to ten, the
118
+ rest as a count — with its status mark (`A`, `M`, `D`, `R`, `?` for a file
119
+ not yet added to git), its own `+N −M`, and a five-block bar that compares
120
+ the files to each other at a glance. *View full diff* opens a new route,
121
+ `#/runs/<run>/nodes/<node>/changes`, that renders the unified diff with
122
+ syntax highlighting (shiki, GitHub light and dark following the theme),
123
+ old and new line numbers, hunk headers with their function context, a
124
+ sticky file list that scrolls to each file, and a fold on any file past 400
125
+ changed lines. Within a removed line and the added line it pairs with, the
126
+ words that differ are marked, so a flipped operator or a renamed variable
127
+ reads at a glance; a line rewritten rather than edited keeps the row colour
128
+ alone. *Unified* or *Split* — the old file beside the new — is a toggle in
129
+ the header, remembered per browser. The daemon serves it from a new endpoint,
130
+ `GET /api/runs/:runId/nodes/:nodeId/changes`, answered by the git unit: from
131
+ the lane's **working tree** while the lane still holds the phase's branch —
132
+ so an agent's uncommitted edits and untracked files are visible while it is
133
+ working — and from the branch itself once the lane has moved on. The card
134
+ polls the counts on the node view's cadence and never the patch; the diff
135
+ view reads the patch once, and again on a button or when the run moves. A
136
+ patch over 2 MiB is cut at a file boundary and says so; the per-file counts
137
+ are complete whatever its size. A daemon older than this browser gets the
138
+ reference — branch, base, lane — as before. (`vinta-ai-maestro`)
139
+
140
+ - **The transcript reads like the harnesses do.** An agent's prose is
141
+ rendered as the markdown it is — headings, lists, inline code and fenced
142
+ blocks, the blocks highlighted like the diff — instead of as its asterisks;
143
+ raw HTML in it stays text. Tool calls are a verb and a target on one line —
144
+ `Read` and a path, `Shell` and a command, `Edit` and a path with `+N −M`
145
+ beside it — whatever the harness named the tool (`Bash`, `bash`,
146
+ `command_execution` and an MCP server's `read_file` all read as what they
147
+ are), with the result's verdict as a dot at the end of the row because the
148
+ result now sits under the call it answers. Open, an edit is a diff of its
149
+ two sides, a write is the file, a shell call is the command and what it
150
+ printed, a failed one with a red edge. A stretch of consecutive reads,
151
+ searches and listings is one row — *Explored · 3 reads, 2 searches* — that
152
+ opens into the calls. The operator's own message is set apart in a tinted
153
+ block. The transcript panel also takes the height the window leaves it
154
+ rather than a fixed 480px. (`vinta-ai-maestro`)
155
+
156
+ - **A plan's PRs now reach `main`.** Phase PRs alone did not: a phase with
157
+ several dependencies targets its `integ-<id>` branch, and nothing ever
158
+ targeted that branch, so every PR stacked above it was stuck. Conflict
159
+ resolutions from wave merges were on no phase branch at all. maestro and the
160
+ stacked-branches `implement-plan` now open two more kinds of PR:
161
+ - an **integration PR** per `integ-<id>` branch, into `base_branch`, opened
162
+ by that phase just before its own PR;
163
+ - a **plan PR** from the final wave branch into `base_branch`, opened once
164
+ the last wave merges. Its body lists every phase and integration PR in an
165
+ order that merges, and the two ways to land the plan: merge it alone, or
166
+ merge the listed PRs in order and this one last.
167
+
168
+ maestro also pushes every wave branch, journals the new PRs (`node_pr` gains
169
+ `kind`, and a new `run_pr` event), and treats `gh`'s "a pull request already
170
+ exists" as opened, with that PR's URL, so a retry no longer reports a failure.
171
+ `prs-context-frontmatter.v1` gains an optional `kind`
172
+ (`phase` | `integration` | `plan`); `phase_id` and `phase_title` are no
173
+ longer required for `kind: plan`.
174
+ - **The schemas are checked.** `npm run validate-schemas` (and `npm test`)
175
+ compiles every schema under `schemas/` with Ajv in strict Draft 2020-12 mode
176
+ and checks the fixtures in `tests/schema-fixtures/`: `valid/` must pass,
177
+ `invalid/` must fail. Source-side only — `ajv` and `ajv-formats` are root
178
+ devDependencies, and nothing new ships.
179
+ - **The plan graph is readable, and says where you are in it.** The canvas
180
+ `vinta-ai-maestro`'s run view and workflow editor share (`vinta-dag-editor`)
181
+ was a strip of six-pixel cards: a five-wave plan was framed into a 380px box
182
+ at half scale, every dependency label was a bordered box truncated to "the
183
+ BookmarkFolder…" piled on the line it labelled, edges had no direction, and
184
+ nothing changed when a node was picked. Redrawn: edges carry arrowheads and
185
+ the ones touching the selected or hovered node are pulled forward while the
186
+ rest recede; cards show a status dot and label (running pulses; failed and
187
+ awaiting-human tint the whole card) and clamp a long name to two lines;
188
+ labels are quiet pills in the gap between waves and spread apart when two
189
+ share one; the zoom and fit controls are icons with tooltips; an empty canvas
190
+ says so. **Long text is reachable three ways:** an instant tooltip on hover
191
+ or keyboard focus for a clamped name or truncated artifact, a label that
192
+ grows to its full text when hovered or selected, and a details strip under
193
+ the canvas — in the run view too — printing the selected node's name and
194
+ status or the selected dependency as a sentence. Keyboard: `+`/`−`/`0` zoom
195
+ and fit, Enter on the selected node opens it. Double-clicking a node in the
196
+ run view opens its node view (`vinta-dag-node-activate`).
197
+ - **Dependencies are drawn by dragging.** Drag from a node's handle onto
198
+ another node; a dashed preview follows the pointer, the source is ringed, and
199
+ every card says whether it would take the drop — a cycle, a duplicate, or the
200
+ source itself is dimmed and refused before the gesture is spent. Click-then-
201
+ pick and the `e` key still work. A refused dependency now says which rule it
202
+ broke (`vinta-dag-refuse`), and the editor shows that where the gesture
203
+ happened instead of nothing.
204
+
205
+ ### Changed
206
+
207
+ - **The canvas no longer swallows the page's scroll.** A bare wheel over the
208
+ graph used to zoom it and block the page from scrolling; it now scrolls the
209
+ page, and zoom is ⌘ or Ctrl + scroll (which is also how a trackpad pinch
210
+ arrives), the buttons, or the keys. Dragging the canvas shows a grab cursor
211
+ and never selects text. The host box follows the window's height instead of
212
+ being fixed at 380px.
213
+
214
+ ### Fixed
215
+
216
+ - **The editor's canvas inspector no longer offers fields it then discards.**
217
+ Its Status and Wave fields were folded back into the workflow and silently
218
+ dropped — status is run state and wave is computed from dependencies — and
219
+ Name duplicated the field in the panel beside the graph. They are hidden in
220
+ the editor through the parts the component now exposes, leaving the
221
+ inspector the one thing only it can do: a dependency's artifact and the
222
+ delete actions.
223
+ - **Picking a dependency after a node no longer loses it on the next frame.**
224
+ The host reported an edge selection as "no node", pushed that back into the
225
+ canvas, and cleared the edge the moment it was picked; the edge's inspector
226
+ flashed and vanished. A cleared node selection now leaves an edge selection
227
+ alone.
228
+ - **Pressing on the canvas's toolbar or inspector no longer pans the graph.**
33
229
  - **A timed-out gate says whether it hung or was slow.** A `gate_result` with
34
230
  `status: timed_out` used to carry only the status, so a run with nine
35
231
  timeouts could not say whether any of them was a stuck suite or a host too
@@ -576,6 +772,58 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
576
772
 
577
773
  ### Changed
578
774
 
775
+ - **maestro's review/fix loop is now the thermo-nuclear review loop.** The
776
+ shipped `standard-phase` changes for every run that uses it:
777
+ - **The reviewer** keeps its session across rounds. It is held to a shipped
778
+ Review Standard (evidence bar, priority order, approval bar), or to the
779
+ project's `REVIEW.md`, which replaces that standard when the lane has one.
780
+ `VERDICT: pass` is now an explicit approval. A re-review gets the fixer's
781
+ report, every finding already rejected or settled, and the pass-two rules.
782
+ - **The orchestrator checks that a review turn changed nothing.** If `HEAD`
783
+ or a tracked file moved, the node stops and asks whether to keep the edits
784
+ or stop the phase.
785
+ - **The fixer** (the implementer continuing its session) verifies each
786
+ finding before acting on it and rejects unsupported ones with
787
+ counter-evidence. It sends scope decisions to the operator instead of
788
+ making them: unreachable scenarios, defensive checks, requirements
789
+ ambiguities, destructive operations. The new `consult` state asks about the
790
+ whole batch at once and does not count as a fix round.
791
+ - **The review ledger.** Rejections and the operator's answers are kept in
792
+ `runs/<run-id>/nodes/<node-id>/review-ledger.jsonl` and shown to every
793
+ later review and fix.
794
+ - **The budget.** `max_fix_rounds` now defaults to **4** (was 2). Running out
795
+ asks `continue` / `stop` instead of failing the phase.
796
+ - **Unattended runs.** Under `--retry-after`, a scope question nobody answers
797
+ takes each item's default, and an exhausted budget nobody answers takes
798
+ `stop`, which fails into `--on-failure` as before. Without `--retry-after`
799
+ both questions wait for a person.
800
+ - **The reviewer's model.** An unstaffed review runs on the new optional
801
+ `defaults.reviewer_model`, then the harness's top tier (`opus` on
802
+ claude-code), then the phase's model.
803
+ - **New pieces.** `await_human` gains an optional `unattended_answer`, and
804
+ there are two new effects, `record_decision` and `grant_fix_rounds`; the
805
+ schema stays at `workflow.v1`.
806
+ - **Migration.** A workflow that pins `max_fix_rounds` keeps its number but
807
+ now gets asked at the end of it. One that ships its own `standard-phase`
808
+ shadows the built-in and is unaffected. The skills-path review partials
809
+ are not updated yet (SPEC §16.6). See
810
+ [SPEC §16](packages/vinta-ai-maestro/SPEC.md#16-the-review-loop).
811
+
812
+ - **maestro starts one session at a time per harness, and remembers the
813
+ account's limit.** Sessions used to boot simultaneously up to the harness
814
+ ceiling, and N Claude Code CLIs starting together contend for the same local
815
+ config and OAuth token: a probe against 2.1.274 measured time to `init`
816
+ rising from 1.2 s with two simultaneous starts to 4.8 s with sixteen, against
817
+ a flat 0.7 s when each waited for the previous one. A spawn now waits until
818
+ the one ahead of it has a session or a refusal, so a refusal also parks every
819
+ queued spawn before it spends one. The ceiling AIMD discovers after a
820
+ `concurrency` or `rate_limit` refusal is kept per harness in `flow.db` and a
821
+ run started within six hours opens at it instead of re-learning the limit by
822
+ being refused; clean spawns still probe back up to the configured value, and
823
+ the hint is dropped once the ceiling recovers or goes stale. When a harness
824
+ is throttled, the freed slot goes to the node with the longest chain of work
825
+ still in front of it rather than to whichever queued first.
826
+
579
827
  - **Updated the OpenAI and Anthropic models to their latest version.**
580
828
 
581
829
  - **Updated the generated skills to have `disable-model-invocation: true` set.**
@@ -826,6 +1074,21 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
826
1074
 
827
1075
  ### Fixed
828
1076
 
1077
+ - **maestro asks you to log in instead of failing on a logged-out harness.**
1078
+ A CLI that was not logged in (or whose token was rejected) was `fatal`: the
1079
+ node failed and its whole subtree blocked. It now parks in `awaiting_human`
1080
+ on a "not logged in" question answered with `logged in` or `stop`, holding
1081
+ its lane. Nothing answers it for you — no automatic or unattended retry,
1082
+ regardless of `onFailure` — and one `logged in` resumes every node that
1083
+ harness refused. Adapters report it as the new `unauthenticated` refusal
1084
+ kind (Claude Code, Codex and opencode).
1085
+ - **maestro no longer fails a node on an organization spend cap.** Claude
1086
+ Code's `billing_error` "spend limit reached (daily; resets …)", the
1087
+ `org_spend_cap_reached` overage reason and "usage credit limit reached"
1088
+ matched no refusal pattern and were classified `fatal`, failing the node and
1089
+ blocking its subtree. They are now `quota` waits, and the stated
1090
+ `resets YYYY-MM-DD HH:MM UTC` time is honored as the wake time.
1091
+
829
1092
  - **A failed Claude Code turn no longer reads as `claude-code result:
830
1093
  success`.** When the CLI ends a turn with `is_error: true` beside
831
1094
  `subtype: "success"` (how a plan limit arrives, for one), the recorded
package/README.md CHANGED
@@ -292,6 +292,22 @@ Land at `ai-tools/agents/<name>.yaml` (canonical YAML; `setup-ai-tools.mjs` emit
292
292
 
293
293
  Stack templates may add specialists like `migration-author` (Django) or `deploy-author` (Medplum) — see disclaimer above.
294
294
 
295
+ ### Integrations (external tools)
296
+
297
+ Integrations are external tools that ship their own skill. The bootstrap asks about each one. When one is enabled, the tool's own CLI installs and updates its skill. This package never copies, renders or edits that skill. Enabling one is recorded under `integrations` in `.vinta-ai-workflows.yaml`.
298
+
299
+ | Integration | What it adds |
300
+ |---|---|
301
+ | [`pr-review-canvas`](https://github.com/vintasoftware/pr-review-canvas) | A review canvas for every PR an agent opens: the diff grouped by topic, with attention points for the reviewer, shared as a PR comment and read in a local app (`pr-review serve`). `integrate-phase` runs `/pr-review-canvas <n>` after `open-pr.sh` publishes the PR. `plan-feature` adds an `after_pr` `review-canvas` chore so that [vinta-ai-maestro](packages/vinta-ai-maestro/README.md#chores-about-the-pr--when-after_pr) does the same on each phase PR. Offered only for GitHub / GitLab projects where agents open PRs. |
302
+
303
+ The tool itself is a machine-level install, like `gh`, and the bootstrap never installs it:
304
+
305
+ ```bash
306
+ npm install -g @vintasoftware/pr-review-canvas
307
+ ```
308
+
309
+ When the CLI is present, the bootstrap runs `pr-review install-skill --claude-dir ai-tools/skills --codex-dir ai-tools/skills`, so one copy in `ai-tools/skills/pr-review-canvas/` serves every vendor. Refresh it with `pr-review upgrade`, and check it with `pr-review doctor`. Do not edit the copy: the tool stamps a hash into it and reports an edited copy as stale. Sharing and other settings live in the tool's own `pr-review.config.yml`.
310
+
295
311
  ## Staying in sync with upstream
296
312
 
297
313
  Bootstrap is a snapshot. `vinta-ai-workflows` keeps shipping — new foundation skills, refined templates, sharper agent prompts, schema additions, stack support, best-practice updates lifted from real projects. Pulling those into a previously-bootstrapped repo is a first-class flow, not an afterthought. **This is one of the most important capabilities of the package.**
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "vinta-ai-workflows",
3
- "version": "0.7.0-alpha20",
3
+ "version": "0.7.0-alpha21",
4
4
  "description": "Bootstrap AI tooling (AGENTS.md, skills, sub-agents) across Claude Code, Codex, Cursor, and VS Code + GitHub Copilot.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "vinta-ai-workflows": "./vinta-ai-workflows.mjs"
8
8
  },
9
9
  "devDependencies": {
10
+ "ajv": "^8.20.0",
11
+ "ajv-formats": "^3.0.1",
10
12
  "yaml": "^2.4.0"
11
13
  },
12
14
  "files": [
@@ -29,6 +31,7 @@
29
31
  "license": "MIT",
30
32
  "scripts": {
31
33
  "check-ai-models": "node scripts/check-ai-models.mjs",
34
+ "validate-schemas": "node scripts/validate-schemas.mjs",
32
35
  "test": "node --test tests/*.test.mjs"
33
36
  }
34
37
  }
package/schemas/README.md CHANGED
@@ -90,3 +90,7 @@ done
90
90
  ```
91
91
 
92
92
  For PR-context frontmatter, extract the YAML block first (`yq` does this cleanly), then validate.
93
+
94
+ ## Checking the schemas themselves (this repo only)
95
+
96
+ `npm run validate-schemas` (also part of `npm test`) compiles every schema here under Ajv's strict Draft 2020-12 mode and checks the fixtures in `tests/schema-fixtures/<schema-file-stem>/`: everything under `valid/` must pass, everything under `invalid/` must fail. A fixture is `.json`, `.yaml` / `.yml`, or a `.md` file whose YAML frontmatter is the payload. **A conditional rule (`if` / `then`) needs an `invalid/` fixture for each branch it enforces** — a lenient validator ignores a broken one silently. Pass file paths instead to validate payloads against the schema their `$schema` key or `yaml-language-server` comment names.
@@ -8,8 +8,6 @@
8
8
  "schema_version",
9
9
  "plan_id",
10
10
  "feature_name",
11
- "phase_id",
12
- "phase_title",
13
11
  "branch",
14
12
  "base",
15
13
  "created_at",
@@ -34,24 +32,30 @@
34
32
  "pattern": "^[A-Z][A-Z0-9_]*$",
35
33
  "minLength": 1
36
34
  },
35
+ "kind": {
36
+ "description": "Which PR this file opens. `phase` (the default when absent): one phase's branch into its dependency-derived base. `integration`: a multi-dependency phase's `plan/<plan-id>/integ-<phase-id>` branch into the default branch, so the phase PR stacked on it can land. `plan`: the final wave branch (or, under modular commits, the plan branch) into the default branch — the PR that lands the whole plan.",
37
+ "type": "string",
38
+ "enum": ["phase", "integration", "plan"],
39
+ "default": "phase"
40
+ },
37
41
  "phase_id": {
38
- "description": "Phase id verbatim from the plan's `Phased Rollout` section (e.g. `1`, `4a`, `2.5`).",
42
+ "description": "Phase id verbatim from the plan's `Phased Rollout` section (e.g. `1`, `4a`, `2.5`). Required unless `kind` is `plan`; for `kind: integration` it is the phase whose `integ-` branch this is.",
39
43
  "type": "string",
40
44
  "pattern": "^[0-9]+(\\.[0-9]+)?[a-z]?$",
41
45
  "minLength": 1
42
46
  },
43
47
  "phase_title": {
44
- "description": "Phase title verbatim from the plan's `Phased Rollout` section.",
48
+ "description": "Phase title verbatim from the plan's `Phased Rollout` section. Required unless `kind` is `plan`.",
45
49
  "type": "string",
46
50
  "minLength": 1
47
51
  },
48
52
  "branch": {
49
- "description": "Source branch the PR opens from. Default convention `plan/<plan-id-kebab>/phase-<phase.id>`.",
53
+ "description": "Source branch the PR opens from. `plan/<plan-id-kebab>/phase-<phase.id>` for a phase, `plan/<plan-id-kebab>/integ-<phase.id>` for an integration PR, the final `plan/<plan-id-kebab>/wave-<N>` (or `plan/<plan-id-kebab>` under modular commits) for the plan PR.",
50
54
  "type": "string",
51
55
  "minLength": 1
52
56
  },
53
57
  "base": {
54
- "description": "Target branch. Either the project's default branch (`main`) or the previous phase's stacked branch.",
58
+ "description": "Target branch. A phase PR targets its dependency-derived base (the default branch, a dependency's phase branch, or its `integ-` branch); integration and plan PRs target the default branch.",
55
59
  "type": "string",
56
60
  "minLength": 1
57
61
  },
@@ -73,5 +77,12 @@
73
77
  { "const": "" }
74
78
  ]
75
79
  }
76
- }
80
+ },
81
+ "allOf": [
82
+ {
83
+ "description": "Every PR but the plan PR belongs to one phase.",
84
+ "if": { "not": { "properties": { "kind": { "const": "plan" } }, "required": ["kind"] } },
85
+ "then": { "required": ["phase_id", "phase_title"] }
86
+ }
87
+ ]
77
88
  }
@@ -250,6 +250,18 @@
250
250
  "fixer": { "type": "string", "enum": ["enabled", "disabled"] }
251
251
  }
252
252
  },
253
+ "integrations": {
254
+ "description": "Per-integration opt-in for external tools that ship their own skills, as opposed to foundation skills this package bundles. An integration's skill is installed by that tool's own CLI into `ai-tools/skills/<name>/`, carries the tool's own ownership marker, and is never copied, rendered or refreshed by `vinta-derive-skills` / `vinta-update-project-skills`. Use `enabled` / `disabled`; `disabled` is a sticky opt-out, as for `foundation_skills`. Absent means the integration was never offered (projects bootstrapped before it existed).",
255
+ "type": "object",
256
+ "additionalProperties": { "type": "string", "enum": ["enabled", "disabled"] },
257
+ "properties": {
258
+ "pr-review-canvas": {
259
+ "description": "PR Review Canvas (`@vintasoftware/pr-review-canvas`, CLI `pr-review`): a topic-grouped review canvas for each PR, posted as a PR comment and read in a local review app. When `enabled`: `vinta-install-ai-tools-setup` runs `pr-review install-skill` into `ai-tools/skills/` (the CLI itself is a global install the user does, never auto-installed); the `integrate-phase` PR step runs the `pr-review-canvas` skill after `open-pr.sh` publishes a PR; and `plan-feature` adds an `after_pr` `review-canvas` chore to the maestro workflow it writes. Posting the canvas comment follows the tool's own `sharing.canvasComment` setting in `pr-review.config.yml`.",
260
+ "type": "string",
261
+ "enum": ["enabled", "disabled"]
262
+ }
263
+ }
264
+ },
253
265
  "agent_models": {
254
266
  "description": "Per-role / per-task model selection for the plan-execution family, expressed as **tiers** (1–4) into the same table `plan-feature` uses — `ai-tools/skills/plan-feature/resources/ai-models.yaml`. Each consuming skill resolves a tier to a concrete model at runtime: take that tier's `models`, filter to the vendors the runtime exposes, pick the cheapest survivor. This keeps the config free of hard-coded model IDs (which go stale) — the nightly `check-ai-models` job keeps the tier table fresh. Every key is optional; an unset key (or the whole section absent) means the spawn uses the runtime's default model, i.e. today's behavior. These are the **project defaults**: the `reviewer` / `fixer` tiers here can be overridden per phase by a plan's optional `**Review models**:` line (a phase override wins over the value here for that phase only — see `plan-feature`). The per-phase implementer model is NOT here — that stays owned by each plan phase's `**Suggested AI model**:` line. The mechanical steps (`worktree_prep`, `integrate`) are never plan-named.",
255
267
  "type": "object",
@@ -378,6 +378,11 @@
378
378
  "pattern": "^[a-z0-9][a-z0-9-]*$"
379
379
  }
380
380
  },
381
+ "reviewer_model": {
382
+ "description": "The model review turns run on when no reviewer is staffed on the roster (§16.4). A review is the step standing between a plan and its merge, so it is not tied to the tier the phase was written at. Absent takes the harness’s own top tier where it names one, and the node’s model where it does not.",
383
+ "type": "string",
384
+ "minLength": 1
385
+ },
381
386
  "max_session_turns": {
382
387
  "default": 12,
383
388
  "description": "Turns one reused session slot may take before the next spawn starts fresh (§15.5). A shared session only ever grows; without a ceiling a long node eventually dies on a context-window error that reads as a broken harness.",
@@ -491,56 +496,155 @@
491
496
  "pattern": "^[a-z0-9][a-z0-9-]*$"
492
497
  },
493
498
  "additionalProperties": {
494
- "type": "object",
495
- "properties": {
496
- "cmd": {
497
- "type": "string",
498
- "minLength": 1,
499
- "description": "Shell command run in the node’s lane. Not an agent."
500
- },
501
- "requires": {
502
- "default": [],
503
- "description": "Resource pool ids acquired before the gate runs, in canonical order.",
504
- "type": "array",
505
- "items": {
506
- "type": "string",
507
- "minLength": 1,
508
- "pattern": "^[a-z0-9][a-z0-9-]*$"
509
- }
510
- },
511
- "timeout_s": {
512
- "default": 1800,
513
- "type": "integer",
514
- "minimum": 1,
515
- "maximum": 9007199254740991
516
- },
517
- "tuning": {
499
+ "anyOf": [
500
+ {
518
501
  "type": "object",
519
502
  "properties": {
520
- "allowed_flags": {
521
- "minItems": 1,
503
+ "cmd": {
504
+ "type": "string",
505
+ "minLength": 1,
506
+ "description": "Shell command run in the node’s lane. Not an agent."
507
+ },
508
+ "requires": {
509
+ "default": [],
510
+ "description": "Resource pool ids acquired before the gate runs, in canonical order.",
522
511
  "type": "array",
523
512
  "items": {
524
513
  "type": "string",
525
- "minLength": 1
514
+ "minLength": 1,
515
+ "pattern": "^[a-z0-9][a-z0-9-]*$"
516
+ }
517
+ },
518
+ "timeout_s": {
519
+ "default": 1800,
520
+ "type": "integer",
521
+ "minimum": 1,
522
+ "maximum": 9007199254740991
523
+ },
524
+ "tuning": {
525
+ "type": "object",
526
+ "properties": {
527
+ "allowed_flags": {
528
+ "minItems": 1,
529
+ "type": "array",
530
+ "items": {
531
+ "type": "string",
532
+ "minLength": 1
533
+ },
534
+ "description": "Flags the monitor may ADD to this gate’s command, verbatim and whole. Matched as exact argv tokens, so `--reuse-db` permits `--reuse-db` and nothing else — not `--reuse-db-and-more`, and not a flag that merely starts the same way. A flag taking a separate value (`-n auto`) is written as the two tokens it is. Removing or rewriting an existing token is never permitted, whatever is listed here: the proposed command must be this one’s tokens, in order, plus additions."
535
+ }
526
536
  },
527
- "description": "Flags the monitor may ADD to this gate’s command, verbatim and whole. Matched as exact argv tokens, so `--reuse-db` permits `--reuse-db` and nothing else — not `--reuse-db-and-more`, and not a flag that merely starts the same way. A flag taking a separate value (`-n auto`) is written as the two tokens it is. Removing or rewriting an existing token is never permitted, whatever is listed here: the proposed command must be this one’s tokens, in order, plus additions."
537
+ "required": [
538
+ "allowed_flags"
539
+ ],
540
+ "additionalProperties": false,
541
+ "description": "What an autonomous retune of this gate may do. Absent — the default — means the command is not the monitor’s to touch."
542
+ },
543
+ "description": {
544
+ "type": "string"
528
545
  }
529
546
  },
530
547
  "required": [
531
- "allowed_flags"
548
+ "cmd"
532
549
  ],
533
- "additionalProperties": false,
534
- "description": "What an autonomous retune of this gate may do. Absent — the default — means the command is not the monitor’s to touch."
550
+ "additionalProperties": false
535
551
  },
536
- "description": {
537
- "type": "string"
552
+ {
553
+ "type": "object",
554
+ "properties": {
555
+ "judge": {
556
+ "type": "object",
557
+ "properties": {
558
+ "question": {
559
+ "description": "The question, inline. Exactly one of `question` and `question_ref` is required.",
560
+ "type": "string",
561
+ "minLength": 1
562
+ },
563
+ "question_ref": {
564
+ "description": "Where the question lives, in `prompt_ref` form (`ai-plans/PLAN.md#no-phi-in-logs`), resolved off the lane like a phase brief.",
565
+ "type": "string",
566
+ "minLength": 1
567
+ },
568
+ "labels": {
569
+ "default": [
570
+ "yes",
571
+ "no"
572
+ ],
573
+ "description": "The closed set the classifier chooses among. The default makes it a yes/no question.",
574
+ "minItems": 2,
575
+ "type": "array",
576
+ "items": {
577
+ "type": "string",
578
+ "minLength": 1
579
+ }
580
+ },
581
+ "fail_on": {
582
+ "minItems": 1,
583
+ "type": "array",
584
+ "items": {
585
+ "type": "string",
586
+ "minLength": 1
587
+ },
588
+ "description": "The labels that fail the gate. Their combined score is compared against `threshold`, so `[\"architectural\", \"schema\"]` fails on the two together and not on either alone."
589
+ },
590
+ "threshold": {
591
+ "default": 0.5,
592
+ "description": "The gate fails when the `fail_on` labels together score at least this much.",
593
+ "type": "number",
594
+ "minimum": 0,
595
+ "maximum": 1
596
+ },
597
+ "input": {
598
+ "default": "diff",
599
+ "description": "What is judged. `diff` is the lane against the phase’s base, uncommitted work included.",
600
+ "type": "string",
601
+ "enum": [
602
+ "diff"
603
+ ]
604
+ },
605
+ "max_input_bytes": {
606
+ "default": 204800,
607
+ "description": "A larger input is not sent. The gate then counts as unavailable — a truncated diff would be judged on the part that happened to fit.",
608
+ "type": "integer",
609
+ "minimum": 1024,
610
+ "maximum": 9007199254740991
611
+ },
612
+ "on_unavailable": {
613
+ "default": "pass",
614
+ "description": "What a question nobody answered means: no classifier configured, unreachable, or an oversized input. `pass` — the default — keeps a judge gate advisory; `fail` is for a check the phase must not merge without.",
615
+ "type": "string",
616
+ "enum": [
617
+ "pass",
618
+ "fail"
619
+ ]
620
+ }
621
+ },
622
+ "required": [
623
+ "fail_on"
624
+ ],
625
+ "additionalProperties": false
626
+ },
627
+ "requires": {
628
+ "default": [],
629
+ "description": "Resource pool ids acquired before the gate runs. Rarely needed: a judge holds no lane resource.",
630
+ "type": "array",
631
+ "items": {
632
+ "type": "string",
633
+ "minLength": 1,
634
+ "pattern": "^[a-z0-9][a-z0-9-]*$"
635
+ }
636
+ },
637
+ "description": {
638
+ "type": "string"
639
+ }
640
+ },
641
+ "required": [
642
+ "judge"
643
+ ],
644
+ "additionalProperties": false
538
645
  }
539
- },
540
- "required": [
541
- "cmd"
542
646
  ],
543
- "additionalProperties": false
647
+ "description": "A command gate (`cmd`) or a judge gate (`judge`)."
544
648
  }
545
649
  },
546
650
  "chores": {
@@ -581,6 +685,15 @@
581
685
  "type": "string",
582
686
  "minLength": 1
583
687
  },
688
+ "when": {
689
+ "default": "before_gate",
690
+ "description": "When the chore runs. `before_gate` — the default — runs it between a passing review and the gates, on the diff that is about to merge. `after_pr` runs it once the phase is merged, pushed and its pull request is open, for work about the PR rather than the diff (a review canvas, a PR comment). An `after_pr` chore must not edit the tree, is skipped when no PR opened, and cannot be `on_failure: fail`.",
691
+ "type": "string",
692
+ "enum": [
693
+ "before_gate",
694
+ "after_pr"
695
+ ]
696
+ },
584
697
  "on_failure": {
585
698
  "default": "continue",
586
699
  "description": "`continue` — the default — journals a failed chore and moves on to the gates. A chore is polish, and losing an implemented phase to one that timed out is the worse trade. `fail` is for a chore whose output the phase is not correct without.",
@@ -695,8 +808,8 @@
695
808
  "pattern": "^[a-z0-9][a-z0-9-]*$"
696
809
  },
697
810
  "max_fix_rounds": {
698
- "default": 2,
699
- "description": "Rounds the fixer gets to clear a review’s findings before the phase fails. Two is a budget, not a target, and it is measured in *rounds* rather than findings — a first review raising four legitimate blockers can exhaust it while every round is making progress. Raise it on a phase you expect to be argued over; it is the knob that decides how much a phase gets to be wrong before it is handed to `--on-failure`.",
811
+ "default": 4,
812
+ "description": "Fix rounds a phase may spend before the operator is asked whether to keep going (§16). A round is a fixer turn — one answer to a review that returned blockers or a gate that went red. It is measured in *rounds* rather than findings, so a first review raising four legitimate blockers spends one. When it 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`.",
700
813
  "type": "integer",
701
814
  "minimum": 0,
702
815
  "maximum": 9007199254740991
@@ -779,7 +892,9 @@
779
892
  "open_pr",
780
893
  "write_tracking",
781
894
  "await_human",
782
- "notify"
895
+ "notify",
896
+ "record_decision",
897
+ "grant_fix_rounds"
783
898
  ],
784
899
  "description": "Which catalog verb this effect invokes."
785
900
  },
@@ -844,7 +959,9 @@
844
959
  "open_pr",
845
960
  "write_tracking",
846
961
  "await_human",
847
- "notify"
962
+ "notify",
963
+ "record_decision",
964
+ "grant_fix_rounds"
848
965
  ],
849
966
  "description": "Which catalog verb this effect invokes."
850
967
  },
@@ -965,7 +1082,9 @@
965
1082
  "open_pr",
966
1083
  "write_tracking",
967
1084
  "await_human",
968
- "notify"
1085
+ "notify",
1086
+ "record_decision",
1087
+ "grant_fix_rounds"
969
1088
  ],
970
1089
  "description": "Which catalog verb this effect invokes."
971
1090
  },
@@ -164,6 +164,7 @@ For each skill, also classify:
164
164
 
165
165
  - `vinta-managed` — directory name starts with `vinta-` (installed by `vinta-ai-workflows`; can be left alone or refreshed via that CLI).
166
166
  - `foundation-shape` — name matches the Vinta foundation set (`plan-feature`, `create-spec`, `create-qa-use-cases`, `implement-plan`, `implement-phase`, `review-phase`, `integrate-phase`, `amend-plan`, `add-e2e-test`, `add-env-var`, `write-unit-test`). `implement-phase` / `review-phase` / `integrate-phase` are the plan-execution sub-skills co-shipped with `implement-plan`.
167
+ - `integration` — installed by an external tool that the bootstrap's **Integrations** group knows about. The directory carries that tool's marker, such as `pr-review-canvas/` with a `.pr-review-install` file (installed by `pr-review install-skill`). The tool owns it. Record it, and also record `pr-review.config.yml` at the repo root when present, so the bootstrap can default the integration question to `Yes`.
167
168
  - `project-custom` — anything else; written by the team.
168
169
 
169
170
  **Sub-agents** — list every agent file with its name + description:
@@ -23,7 +23,7 @@ This orchestrator runs six sub-skills in order. Each is its own SKILL.md so it c
23
23
  2. [vinta-write-agents-md](../vinta-write-agents-md/SKILL.md) — synthesize the root `AGENTS.md` from the inventory + a focused interview for what the analysis can't see.
24
24
  3. [vinta-derive-subagents](../vinta-derive-subagents/SKILL.md) — author `ai-tools/agents/*.yaml`. Always emits the foundation trio (`implementer`, `reviewer`, `fixer`); adds stack-specific specialists when the user supplies a template for a matched stack.
25
25
  4. [vinta-derive-skills](../vinta-derive-skills/SKILL.md) — author `ai-tools/skills/*/SKILL.md`. Always copies the project-agnostic foundation set (`plan-feature`, `create-spec`, `create-qa-use-cases`) verbatim from its bundled resources. Generates `implement-plan` from a parameterized template using project specifics. Asks the user whether the optional `add-e2e-test` and `add-env-var` skills are needed. Asks for stack-specific templates per matched stack.
26
- 5. [vinta-install-ai-tools-setup](../vinta-install-ai-tools-setup/SKILL.md) — copy the canonical `setup-ai-tools.mjs` into `ai-tools/scripts/`, wire the package script alias, run setup, verify all vendor paths resolve.
26
+ 5. [vinta-install-ai-tools-setup](../vinta-install-ai-tools-setup/SKILL.md) — copy the canonical `setup-ai-tools.mjs` into `ai-tools/scripts/`, wire the package script alias, run setup, verify all vendor paths resolve. Then install the skills of any enabled integration (D.2) through that tool's own CLI.
27
27
  6. [vinta-migrate-plans-specs](../vinta-migrate-plans-specs/SKILL.md) — find any pre-existing implementation plans / feature specs scattered across the repo (`docs/`, `specs/`, `plans/`, root markdown, etc.) and propose moving them to the canonical layout `ai-plans/YYYY-MM-DD-{FEATURE_NAME}_{PLAN|SPEC}.md`. Read-only by default; every rename is gated on per-file user approval. Skipped automatically when the analysis finds no candidates.
28
28
 
29
29
  Each sub-skill returns a short status report. Don't run the next sub-skill until the previous finished cleanly. If a sub-skill fails or surfaces ambiguity, surface that to the user and resolve before continuing.
@@ -144,6 +144,16 @@ Seven skills are part of the foundation set but aren't always needed. Ask explic
144
144
 
145
145
  If the user answers "No" to any of the seven, that skill won't ship. If the user answers "Yes" to `add-e2e-test` / `add-env-var` but doesn't have a template, derive-skills drafts one via interview. `systematic-debugging` is always template-rendered — no per-project drafting interview, just the MCP-server inventory above. `add-one-off-script` is copied verbatim from [vinta-derive-skills/resources/foundation-skills/add-one-off-script/](../vinta-derive-skills/resources/foundation-skills/add-one-off-script/) — its body is project-agnostic; the per-project variability lives in the `skills.add-one-off-script.*` config block above and in env vars consumed at runtime. `prepare-worktree` is also copied verbatim from [vinta-derive-skills/resources/foundation-skills/prepare-worktree/](../vinta-derive-skills/resources/foundation-skills/prepare-worktree/) — same pattern: project-agnostic body, per-project defaults under `skills.prepare-worktree.*`. `thermo-nuclear-code-quality-review` is copied verbatim from [vinta-derive-skills/resources/foundation-skills/thermo-nuclear-code-quality-review/](../vinta-derive-skills/resources/foundation-skills/thermo-nuclear-code-quality-review/) with no follow-up config — its body is fully project-agnostic. `handoff-to-client` is always template-rendered from [vinta-derive-skills/resources/handoff-to-client-template.md](../vinta-derive-skills/resources/handoff-to-client-template.md) — no per-project drafting interview, just the client-handoff config follow-up above.
146
146
 
147
+ ### D.2 Integrations (external tools)
148
+
149
+ Integrations are external tools that ship their own skill. They are not foundation skills: this package bundles none of their content. When one is enabled, the tool's own CLI installs its skill, and this flow only wires that skill in. Ask once per integration:
150
+
151
+ 1. **`pr-review-canvas`** ([PR Review Canvas](https://github.com/vintasoftware/pr-review-canvas), CLI `pr-review`). Should agents generate a review canvas for every PR they open? A canvas is the PR's diff grouped by topic, with attention points for the reviewer. It is shared as a PR comment and read in a local review app (`pr-review serve`). Ask **only when** `project.code_host` is `github` or `gitlab` **and** the `pr_creation` policy (C.4) is `agents-create`. Otherwise set `disabled` without asking: the tool supports only those two hosts, and with `branches-only` no agent opens a PR to attach a canvas to. `AskUserQuestion` options: `Yes — enable`, `No — skip`. Recommend `Yes` for teams that review agent-written PRs, where a large multi-phase diff is the normal case. Default to `Yes` when [vinta-analyze-codebase](../vinta-analyze-codebase/SKILL.md) already found the tool in use: a `pr-review-canvas` skill classified `integration`, or a `pr-review.config.yml`.
152
+
153
+ Mention the requirements in the question: Node.js 22+, a logged-in `gh` or `glab`, and Claude Code or Codex as the agent. Do **not** install the CLI. Like `open-pr.sh`'s `yq` / `jq` / `gh`, it is a machine-level dependency the user installs. [vinta-install-ai-tools-setup](../vinta-install-ai-tools-setup/SKILL.md) checks for it and prints `npm install -g @vintasoftware/pr-review-canvas` when it is missing.
154
+
155
+ No follow-up config. The tool's own settings (sharing on/off, high-risk paths, prompt overrides) live in its `pr-review.config.yml` at the repo root, and the tool owns that file.
156
+
147
157
  ### E. Existing AI artifacts (per-artifact disposition)
148
158
 
149
159
  [vinta-analyze-codebase](../vinta-analyze-codebase/SKILL.md#11-existing-ai-tooling-artifacts)'s **Existing AI-tooling artifacts** scan produces an inventory of **every** AI-tooling artifact already in the repo: instruction docs (AGENTS.md / CLAUDE.md / .cursorrules / .github/copilot-instructions.md / ...), skills under `.claude/skills/`, `.cursor/skills/`, `.codex/skills/`, `.github/skills/`, `.agents/skills/`, `ai-tools/skills/`, and sub-agent files under `.claude/agents/`, `.cursor/agents/`, `.codex/agents/`, `.github/agents/`, `ai-tools/agents/`.
@@ -162,6 +172,8 @@ For each artifact, read it (frontmatter + body), then ask the user via `AskUserQ
162
172
  - `Keep in current vendor path, don't touch` — leaves it where it is. AGENTS.md may reference it; downstream skill setup won't manage it.
163
173
  - `Drop` — delete (rare; usually the user wants to migrate).
164
174
 
175
+ **Integration-owned skills** (name matches an integration under D.2 — today `pr-review-canvas`) get no question. Leave them where they are. Their tool owns them through its own marker file (`.pr-review-install`), and its `doctor` / `upgrade` commands manage them. When the integration is `enabled` and the existing copy sits in a real `.claude/skills/` or `.agents/skills/` directory rather than in `ai-tools/skills/`, tell the user once. [vinta-install-ai-tools-setup](../vinta-install-ai-tools-setup/SKILL.md) installs a fresh copy into `ai-tools/skills/`, so the old copy becomes redundant once the vendor directory is a symlink.
176
+
165
177
  Foundation-shape skills (name matches `plan-feature`, `create-spec`, `create-qa-use-cases`, `implement-plan`, `implement-phase`, `review-phase`, `integrate-phase`, `amend-plan`, `add-e2e-test`, `add-env-var`, `add-one-off-script`, `prepare-worktree`, `thermo-nuclear-code-quality-review`, `deslop-comments`, `handoff`, `handoff-to-client`, `write-unit-test`) get an extra option: `Replace with Vinta foundation version` — overwrites with the canonical foundation content, preserving the user's name. Useful when the existing version is stale. (`implement-phase` / `review-phase` / `integrate-phase` are the plan-execution sub-skills co-shipped with `implement-plan`; replace them as a unit.)
166
178
 
167
179
  - **Sub-agents** (each under any vendor `agents/` dir):
@@ -220,7 +232,7 @@ Captured interview state lands in **one** YAML file at the repo root: `.vinta-ai
220
232
 
221
233
  This file is the **only** source of truth for project-wide settings. Every downstream sub-skill (4 → 6 below) reads from it instead of receiving values via in-conversation state. Every meta-skill ([vinta-sync-ai-tools](../vinta-sync-ai-tools/SKILL.md), [vinta-update-project-skills](../vinta-update-project-skills/SKILL.md)) reads + rewrites it.
222
234
 
223
- Write the file now (before any sub-skill runs). Populate from the interview groups **Scope**, **Stack detection**, **Project conventions**, **Optional foundation skills**, and **Existing AI artifacts**:
235
+ Write the file now (before any sub-skill runs). Populate from the interview groups **Scope**, **Stack detection**, **Project conventions**, **Optional foundation skills**, **Integrations**, and **Existing AI artifacts**:
224
236
 
225
237
  ```yaml
226
238
  # yaml-language-server: $schema=./node_modules/vinta-ai-workflows/schemas/vinta-ai-workflows-config.v1.schema.json
@@ -291,6 +303,9 @@ foundation_agents:
291
303
  reviewer: enabled
292
304
  fixer: enabled
293
305
 
306
+ integrations:
307
+ pr-review-canvas: <Integrations → pr-review-canvas answer → enabled | disabled; disabled without asking when code_host ∉ {github, gitlab} or pr_creation = branches-only>
308
+
294
309
  # Only emit the roles the user chose in the "Agent model tiers" question (C.10).
295
310
  # Omit the whole block on "Leave unset"; omit any individual role left unset.
296
311
  # Each value is a tier (1–4) into ai-tools/skills/plan-feature/resources/ai-models.yaml.
@@ -434,6 +449,7 @@ ai-tools/
434
449
  │ │ └── one_off_script_base.ts
435
450
  │ ├── prepare-worktree/SKILL.md ← optional — only if user opts in (verbatim copy; project-agnostic body, defaults under skills.prepare-worktree.*)
436
451
  │ ├── thermo-nuclear-code-quality-review/SKILL.md ← optional — only if user opts in (verbatim copy; deep structural-maintainability audit, escalation target for review-phase Layer 3)
452
+ │ ├── pr-review-canvas/ ← integration — only when integrations.pr-review-canvas = enabled and `pr-review` is installed. Written by `pr-review install-skill`, never by this flow; owned through its `.pr-review-install` marker, refreshed by `pr-review upgrade`
437
453
  │ ├── run-one-off-script-django/ ← optional sister skill — only when stack matches + user supplies template (authors Jupyter notebook / mgmt command runner + JupyterRuntime / DjangoMgmtRuntime adapter in the per-script folder)
438
454
  │ ├── run-one-off-script-medplum/ ← optional sister skill — only when stack matches + user supplies template (authors Medplum bot + MedplumBotRuntime adapter in the per-script folder)
439
455
  │ └── <stack-specific skills>/SKILL.md ← only if user supplied templates
@@ -142,6 +142,8 @@ Shells under `plan-execution/shell/` are thin — most content lives in `plan-ex
142
142
 
143
143
  **When `foundation_skills.prepare-worktree` is `disabled`**, parallel execution is impossible (several agents cannot share one working tree). Render the plan-execution unit sequential-only: strip the Step 0 (e) question and the `{If run_options.parallel_phases = true:}` gate from the implementer prompt, and keep the `parallel-lanes` blocks — the scheduler with `max_parallel_lanes = 1` **is** the sequential path, and the DAG still orders the phases correctly. Only the pool provisioning collapses to the single-worktree branch of `WORKROOT_RESOLUTION`.
144
144
 
145
+ **The review-canvas gate stays, whatever the config says.** The PR-context block carries a `{If integrations.pr-review-canvas = enabled in .vinta-ai-workflows.yaml:}` paragraph. Render it verbatim, including the gate. It is a runtime gate that the integrating agent reads, so a team that enables the integration later through [vinta-sync-ai-tools](../vinta-sync-ai-tools/SKILL.md) needs no re-derive. Strip it only when `policies.pr_creation = branches-only`, because then no agent ever runs `open-pr.sh`.
146
+
145
147
  **`ask` → two `integrate-phase` variants, no dual-render.** When `policies.commit_strategy = ask`, render `integrate-phase-template.md` **twice**:
146
148
  - `ai-tools/skills/integrate-phase-stacked/SKILL.md` — `<RESOLVED>` = `stacked`, `{{INTEGRATE_PHASE_NAME}}` = `integrate-phase-stacked`.
147
149
  - `ai-tools/skills/integrate-phase-modular/SKILL.md` — `<RESOLVED>` = `modular`, `{{INTEGRATE_PHASE_NAME}}` = `integrate-phase-modular`.
@@ -345,6 +347,7 @@ Length: 100–300 lines. Shorter = under-specified. Longer = should probably spl
345
347
  - **Foundation set is a unit.** Always copy `plan-feature` + `create-spec` + `open-pr-from-context` together — they reference each other. `create-qa-use-cases` joins only when `add-e2e-test` is enabled; when it's not, strip `plan-feature`'s e2e regions (the `<!-- e2e:start/end -->` pass) so no cross-link dangles.
346
348
  - **The plan-execution unit is generated, not copied.** `implement-plan` + `implement-phase` + `review-phase` + `integrate-phase` + `amend-plan` all have too much project-specific content for verbatim shipping. They are separate files but a **co-shipped unit** — never emit the conductors without the three sub-skills; a conductor whose `[implement-phase]` / `[review-phase]` / integrate link dangles is a broken skill.
347
349
  - **Optional skills (`add-e2e-test`, `add-env-var`, `add-one-off-script`, `prepare-worktree`, `thermo-nuclear-code-quality-review`, `handoff-to-client`) are gated by user answer.** Don't ship them by default. `handoff-to-client` is additionally template-rendered (bucket B) — its config lives under `skills.handoff-to-client.*`.
350
+ - **Integration skills are not yours.** `ai-tools/skills/pr-review-canvas/` (and any other skill an `integrations.*` tool installs) is written and refreshed by that tool's CLI. It carries the tool's own marker. Never copy, render, scrub or delete it here. The tool's `doctor` treats any edit as a stale copy.
348
351
  - **Each skill solves one job.** Two unrelated checklists → split.
349
352
  - **Reference real files in the target.** Skill links must point to existing paths.
350
353
  - **Skills auto-load by description.** Specific triggers, specific outcomes.
@@ -608,7 +608,8 @@ First key in the file is `"$schema"`, pointing at `https://github.com/vintasoftw
608
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. |
609
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
610
  | `chores.<id>` | Agent turns a phase runs beside its gates, for work that changes the diff rather than judging it. **Always emit the `deslop` chore below** — it is the comment-hygiene pass, and without it a maestro run has none. Add another only when the plan genuinely needs one (a changelog entry, a translation extraction); a chore is a model turn per phase, so each one has to earn it. |
611
- | `defaults.chores` | `["deslop"]`. The chores every phase runs unless it names its own. |
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. |
612
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. |
613
614
  | `nodes[]` | One per phase, in plan order. |
614
615
  | `nodes[].id` | `p` + the phase number, lowercased: `Phase 1` → `p1`, `Phase 4a` → `p4a`, `Phase 1b` → `p1b`. |
@@ -619,7 +620,7 @@ First key in the file is `"$schema"`, pointing at `https://github.com/vintasoftw
619
620
  | `nodes[].gates` | The gate ids this phase must pass, in the order they should run. |
620
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. |
621
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`. |
622
- | `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. |
623
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. |
624
625
  | `pipelines` | **Omit.** The executor ships `standard-phase` — see "The pipeline block". |
625
626
 
@@ -701,6 +702,21 @@ The remaining values — `migrate_cmd`, the database names, the server URL, the
701
702
 
702
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.
703
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
+
704
720
  ### Worked example
705
721
 
706
722
  A five-phase plan whose `**Depends on**:` lines are:
@@ -1060,4 +1076,5 @@ When in doubt, model the plan after a recent example in `ai-plans/` — look for
1060
1076
  - [ ] No credential anywhere in the workflow file: `connection_url_var` is a variable name, and `server_url` is a host and port.
1061
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.
1062
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`.
1063
1080
  - [ ] `pipelines` is omitted — `defaults.pipeline: standard-phase` is enough, and the executor supplies it.
@@ -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):
@@ -124,9 +124,16 @@ The budget is an **integration-level** setting, not either phase's `max_fix_roun
124
124
 
125
125
  **Confirming a fix requires reading the files, not asking git.** `git add` clears a path's unmerged flag whether or not `<<<<<<<` is still sitting in it, so git cannot tell you whether the fixer actually resolved anything. Scan the conflicted paths for conflict markers before committing the merge. Skip this and a fixer that did nothing produces a merge commit full of markers that passes straight into the wave branch.
126
126
 
127
- ### One PR per phase, based on the phase's base
127
+ ### PRs: one per phase, plus what it takes to reach `<BASE_BRANCH>`
128
128
 
129
129
  The PR `base` written into the prs-context frontmatter is the phase's **computed `base_branch`** — `<BASE_BRANCH>`, a single dependency's branch, or the `integ-{P.id}` branch. Never `<BASE_BRANCH>` for a phase that has dependencies; a wrong base makes the PR diff include every upstream phase and the review is unusable.
130
+
131
+ Phase PRs are review units, and on their own they do not land the plan. Under a per-phase PR strategy the conductor also opens:
132
+
133
+ - **An integration PR per `integ-{P.id}` branch**, into `<BASE_BRANCH>`, just before `P`'s own PR. Without it `P`'s PR targets a branch nothing targets, and nothing stacked on `P` can reach `<BASE_BRANCH>`.
134
+ - **A plan PR at run end**, from the final `wave-{N}` branch into `<BASE_BRANCH>`. It is the only branch carrying the conflict resolutions made during wave merges, so it is merged last whichever way the plan lands. Its description lists every phase and integration PR in merge order.
135
+
136
+ The commit strategy's PR-open timing says which files to write and when.
130
137
  <!-- block-end: LANE_TOPOLOGY -->
131
138
 
132
139
  <!-- block-begin: LANE_SCHEDULER -->
@@ -39,7 +39,7 @@ Two project-level signals decide the actual behavior:
39
39
 
40
40
  When `generate_inline_comments = false`: skip this step. The file's `# Comments` block stays empty.
41
41
 
42
- 4. **Write the prs-context file** at {{PRS_CONTEXT_FILE_PATH}}, following [resources/prs-context-template.md](../../prs-context-template.md). Frontmatter: `plan_id`, `feature_name`, `phase_id`, `phase_title`, `branch`, `base`, `created_at`, `status: pending`, empty `pr_url`. **`base` is the branch the PR opens against — resolve it per the commit strategy, never default it to `<BASE_BRANCH>` blindly:** for stacked branches only the first executed phase bases on `<BASE_BRANCH>`; every subsequent phase bases on the **previous phase's branch** (see the PR-base rule under the {{BRANCH_PUSH_HEADING}} step above). For a single plan-level PR (modular / one-PR strategies) `base = <BASE_BRANCH>`. Body sections: `# Title` (single-line PR title), `# Description` (Markdown body — uses the project's PR template structure from step 2 when one exists), `# Comments` (YAML list of `{file, start_line, end_line?, side, body}` — empty list when comments are off).
42
+ 4. **Write the prs-context file** at {{PRS_CONTEXT_FILE_PATH}}, following [resources/prs-context-template.md](../../prs-context-template.md). Frontmatter: `plan_id`, `feature_name`, `phase_id`, `phase_title`, `branch`, `base`, `created_at`, `status: pending`, empty `pr_url`. **`base` is the branch the PR opens against — resolve it per the commit strategy, never default it to `<BASE_BRANCH>` blindly:** for stacked branches a phase's PR bases on its dependency-derived `<phase.base_branch>` — `<BASE_BRANCH>` only for a phase with no dependencies (see the PR-base rule under the {{BRANCH_PUSH_HEADING}} step above); an integration PR (`kind: integration`) and the plan PR (`kind: plan`) base on `<BASE_BRANCH>`. For a single plan-level PR (modular / one-PR strategies) `base = <BASE_BRANCH>`. Body sections: `# Title` (single-line PR title), `# Description` (Markdown body — uses the project's PR template structure from step 2 when one exists), `# Comments` (YAML list of `{file, start_line, end_line?, side, body}` — empty list when comments are off).
43
43
 
44
44
  5. **Deslop the prose.** Run the `deslop-comments` skill ([deslop-comments](../deslop-comments/SKILL.md)) over the file you just wrote, passing {{PRS_CONTEXT_FILE_PATH}} as the explicit scope. `# Title`, the `# Description` body, and every `# Comments` `body` must read as Simple English: one idea per sentence, current behavior stated directly instead of "not X" framing, and no AI-slop vocabulary (`gate`, `guard`, `backstop`, `surface`, `leverage`, `plumb`, `canonical`, …). Keep precise domain terms, and keep the project's PR-template structure intact — section headings, `<!-- HTML comment -->` placeholders, and checklist lines stay exactly as step 2 wrote them. This pass rewrites prose only: it never touches the frontmatter, the section layout, or which file and lines a comment targets. Do it before `open-pr.sh` runs, since the published PR body and comments come straight from this file.
45
45
 
@@ -59,5 +59,7 @@ Two project-level signals decide the actual behavior:
59
59
 
60
60
  When policy = "branches only": **don't run the script.** File stays `status: pending`.
61
61
 
62
+ {If integrations.pr-review-canvas = enabled in `.vinta-ai-workflows.yaml`:} **Review canvas.** After exit `0` or `1`, the PR is up. Generate its review canvas with the `pr-review-canvas` skill ([pr-review-canvas](../pr-review-canvas/SKILL.md)). Pass the PR number from the end of `pr_url` (`…/pull/<n>` or `…/merge_requests/<n>`), as in `/pr-review-canvas <n>`. Run it again each time this step re-runs `open-pr.sh` on the same PR, for example after each phase under one plan-level PR. The tool updates the canvas incrementally. The skill posts the canvas as a PR comment unless the project's `pr-review.config.yml` turns sharing off. Add its local review URL and the comment link to the user update. A canvas that fails never fails the phase. Report the skill's error, and point at `pr-review doctor` when the error suggests setup. When the `pr-review-canvas` skill is not installed, say so once, point at `vinta-install-ai-tools-setup`, and skip.
63
+
62
64
  8. **Skill wrapper** — [open-pr-from-context](../open-pr-from-context/SKILL.md) is available for ad-hoc invocation (after the run, on a different machine, etc.). The orchestrator can call the script directly here; the skill is for humans.
63
65
  <!-- block-end: PR_CONTEXT -->
@@ -181,9 +181,10 @@ After the scheduler loop exits — every executable phase is `done`, `failed`, o
181
181
 
182
182
  1. **Build the final wave branch** if the last completed wave has no branch yet, so one branch carries the whole plan.
183
183
  2. **Delete the `TRACKING_{plan-id}/` directory** (`git rm -r`) on that final wave branch, in one commit. The plan file stays.
184
- 3. Send the user a final summary: {{FINAL_REPORT_BRANCH_SUMMARY}}; the **wave branches** in order, and which phase branches merged into each; phases that **failed** and the dependents each one **blocked**; for UI-flow phases — list of `pr-screenshots/` files (if applicable); deferred phases (cross-repo + flag-removal); next steps for the human. When `run_options.use_worktree = true`: include **every lane's** path + branch + summary file path + teardown command (`git worktree remove <path>` + the per-engine drop-db / `docker compose -p <project> down -v` lines from that lane's `<worktree_summary>`), and the integration worktree's. Do NOT auto-run teardown — the user may still want a lane to debug review feedback or land follow-ups, and a failed phase's lane is the only place its state survives.
184
+ 3. **Open the plan PR** (per-phase PR strategies only — skip when the commit strategy already keeps one plan-level PR). Only when every phase is `done`: a plan with failed or blocked phases is not ready to land, so list the gap instead. Push the final wave branch, write `.vinta-ai-workflows/prs-context/{feature-kebab}/plan.md` with `kind: plan`, `branch: plan/{plan-id-kebab}/wave-{N}` (the final wave), `base: <BASE_BRANCH>`, and a `# Description` listing every phase and integration PR in merge order (wave by wave; plan order inside a wave; each integration PR right before its phase PR), with the two ways to land it — merge this PR alone, or merge the listed PRs in order and this one last — and the rule to use merge commits, not squash. Run `open-pr.sh` on it under the same PR policy as the phase PRs. This is the PR that lands the plan; a run that ends without it leaves the user to rebuild the merges by hand.
185
+ 4. Send the user a final summary: the **plan PR URL** first (or why it was not opened — failed phases, PR policy, missing deps); {{FINAL_REPORT_BRANCH_SUMMARY}}; the **wave branches** in order, and which phase branches merged into each; phases that **failed** and the dependents each one **blocked**; for UI-flow phases — list of `pr-screenshots/` files (if applicable); deferred phases (cross-repo + flag-removal); next steps for the human. When `run_options.use_worktree = true`: include **every lane's** path + branch + summary file path + teardown command (`git worktree remove <path>` + the per-engine drop-db / `docker compose -p <project> down -v` lines from that lane's `<worktree_summary>`), and the integration worktree's. Do NOT auto-run teardown — the user may still want a lane to debug review feedback or land follow-ups, and a failed phase's lane is the only place its state survives.
185
186
  {{FINAL_REPORT_PR_NOTE}}
186
- 4. Flag-removal phase deferred → end with `/schedule` offer for the dedicated flag-removal skill.
187
+ 5. Flag-removal phase deferred → end with `/schedule` offer for the dedicated flag-removal skill.
187
188
 
188
189
  ## Important rules
189
190
 
@@ -232,4 +233,4 @@ After the scheduler loop exits — every executable phase is `done`, `failed`, o
232
233
  - [ ] Wave branch built + `waves/wave-{N}.md` written when this phase completed its wave; merge conflicts resolved by a fixer, never by the conductor, and the outer gate re-run on the merged tree.
233
234
  - [ ] One-paragraph user update sent per phase completion (PR URL or pending-file path included; lane named; what the scheduler picked up next).
234
235
  - [ ] If `run_options.pause_between_phases = true`: stopped dispatching, let in-flight lanes drain, prompted user (`Continue` / `Pause` / `Stop`); honored answer. Else: next ready phase dispatched immediately.
235
- - [ ] On run end: final wave branch built; tracking directory deleted; final summary lists wave + phase branches{{FINAL_CHECKLIST_PR_NOTE}}, failed phases with the dependents they blocked, every lane's teardown command; any `status: pending` PR-context files listed with publish command; `/schedule` offer for flag-removal if applicable.
236
+ - [ ] On run end: final wave branch built; tracking directory deleted; plan PR opened (final wave branch → `<BASE_BRANCH>`, every phase + integration PR listed in merge order) when every phase is done and the strategy opens per-phase PRs; final summary leads with the plan PR URL and lists wave + phase branches{{FINAL_CHECKLIST_PR_NOTE}}, failed phases with the dependents they blocked, every lane's teardown command; any `status: pending` PR-context files listed with publish command; `/schedule` offer for flag-removal if applicable.
@@ -29,4 +29,4 @@ Invoked by [implement-plan](../implement-plan/SKILL.md) after [review-phase](../
29
29
 
30
30
  ## Output
31
31
 
32
- Return to the conductor: the branch pushed, and the PR-context file path with its `status` (`published` + `pr_url` when `open-pr.sh` ran; `pending` otherwise) plus the publish command when `pending`.
32
+ Return to the conductor: the branch pushed (plus the `integ-` branch when one was pushed), and each PR-context file path written — the integration file first when there is one — with its `status` (`published` + `pr_url` when `open-pr.sh` ran; `pending` otherwise) plus the publish command when `pending`.
@@ -3,10 +3,11 @@
3
3
  schema_version: 1 # see schemas/prs-context-frontmatter.v1.schema.json
4
4
  plan_id: <plan-id> # filename feature portion, kebab-case
5
5
  feature_name: <FEATURE_NAME> # UPPERCASE_WITH_UNDERSCORES, matches plan/spec
6
- phase_id: <phase-id> # e.g. "1", "4a"
7
- phase_title: <phase title> # verbatim from the plan's Phased Rollout section
8
- branch: plan/<feature-kebab>/phase-<id> # branch the PR opens from
9
- base: <main | plan/<feature-kebab>/phase-<prev-id>> # PR target branch. Stacked: FIRST phase = default branch; every LATER phase = the PREVIOUS phase's branch (not the default branch). Modular / single-PR: default branch.
6
+ kind: phase # phase (default) | integration (a phase's integ- branch) | plan (the PR that lands the whole plan)
7
+ phase_id: <phase-id> # e.g. "1", "4a". Omit for kind: plan
8
+ phase_title: <phase title> # verbatim from the plan's Phased Rollout section. Omit for kind: plan
9
+ branch: plan/<feature-kebab>/phase-<id> # branch the PR opens from (integ-<id> for integration; final wave-<N> for a stacked plan PR)
10
+ base: <main | plan/<feature-kebab>/phase-<dep-id> | plan/<feature-kebab>/integ-<id>> # PR target branch. Stacked phase: its dependency-derived base (default branch only with no dependencies). Integration, plan, modular / single-PR: default branch.
10
11
  created_at: <ISO 8601 timestamp>
11
12
  status: pending # `pending` until published; `published` after CLI run
12
13
  pr_url: # set by open-pr-from-context after publishing
@@ -101,10 +101,37 @@ For each vendor in the user's selection:
101
101
 
102
102
  Any mismatches → re-run the script. If still wrong → inspect the script's output for warnings (it warns when a target path exists as a non-symlink real file).
103
103
 
104
+ ### 7b. Install integration skills
105
+
106
+ Read `integrations` from `.vinta-ai-workflows.yaml`. Skip this step when nothing there is `enabled`. Run it **after** step 6: the vendor skill paths must already be symlinks into `ai-tools/skills/`, or the tool would create real vendor directories that block those symlinks.
107
+
108
+ **`pr-review-canvas` = enabled:**
109
+
110
+ 1. Check for the CLI with `pr-review --version`. If it is missing, print the install command and skip the rest of this integration. **Do not install it yourself.** It is a global, machine-level tool, like `gh`:
111
+
112
+ ```bash
113
+ npm install -g @vintasoftware/pr-review-canvas
114
+ ```
115
+
116
+ Tell the user to install it, then re-run this step (or run `pr-review install-skill --claude-dir ai-tools/skills --codex-dir ai-tools/skills` themselves).
117
+ 2. Install the skill into the canonical directory:
118
+
119
+ ```bash
120
+ pr-review install-skill --claude-dir ai-tools/skills --codex-dir ai-tools/skills --json
121
+ ```
122
+
123
+ Both flags point at `ai-tools/skills/`, so the tool writes one copy, `ai-tools/skills/pr-review-canvas/`. Every vendor then reaches that copy through its symlink. The tool also adds `.pr-review/settings.yml` to `.gitignore`.
124
+ 3. **Never edit `ai-tools/skills/pr-review-canvas/SKILL.md`.** The tool stamps a body hash into its frontmatter, and `pr-review doctor` and `pr-review serve` flag a modified copy as stale. To refresh the skill, run `pr-review upgrade`, not this flow.
125
+ 4. Run `pr-review doctor`. Its `skill` check must pass, and it reads the copy through `.agents/skills/`, which step 6 always links. Show any other failed checks to the user with the hints the tool prints, most often `gh auth login` / `glab auth login` or a missing `origin` remote. These do not block the bootstrap.
126
+ 5. Do not write `pr-review.config.yml`. Every key in it is optional, and the defaults are correct. Mention that the team can copy the tool's example config to change sharing or mark high-risk paths.
127
+
128
+ On `SKILL_DIR_EXISTS` (exit 1), a hand-made `ai-tools/skills/pr-review-canvas/` without the tool's marker is in the way. Show it to the user and ask before re-running with `--force`. `--force` replaces that directory.
129
+
104
130
  ### 8. Commit
105
131
 
106
132
  Stage:
107
- - `ai-tools/` (canonical sources + script)
133
+ - `ai-tools/` (canonical sources + script, plus any integration skill from step 7b)
134
+ - `.gitignore` (when step 7b's `pr-review install-skill` added `.pr-review/settings.yml`)
108
135
  - `package.json` (script alias + yaml dep)
109
136
  - `pnpm-lock.yaml` / `package-lock.json` / `yarn.lock` (after install)
110
137
  - `AGENTS.md` (regular file at the repo root)
@@ -109,7 +109,7 @@ For each `affects-project` and `opt-in-offer` and `config-schema-change` entry,
109
109
 
110
110
  `tooling` changes batch under one prompt: `Apply all tooling changes?` — they're typically idempotent and small.
111
111
 
112
- `opt-in-offer` `Skip` writes the corresponding entry to `foundation_skills` (or wherever it lives) as `disabled` so the next sync respects the choice.
112
+ `opt-in-offer` `Skip` writes the corresponding entry to `foundation_skills` (or `integrations`, or wherever it lives) as `disabled` so the next sync respects the choice.
113
113
 
114
114
  Don't batch `affects-project` decisions. Each is a separate decision so the user can keep their hand-tuned wording for one skill while accepting another.
115
115
 
@@ -124,6 +124,7 @@ In order:
124
124
  - Bump `schema_version` if the major changed.
125
125
 
126
126
  2. **`opt-in-offer` accepts** — flip the corresponding entry to `enabled` in the config; copy / render the artifact (foundation skill, agent, etc.).
127
+ - **An integration** (`integrations.<name>` in the config, such as `pr-review-canvas`) has no bundled artifact to copy. Flip it to `enabled`, then run [vinta-install-ai-tools-setup](../vinta-install-ai-tools-setup/SKILL.md)'s **Install integration skills** step, which uses the tool's own CLI. The plan-execution skills and `plan-feature` read the flag at runtime, so nothing needs re-deriving. Offer it only when its preconditions hold. For `pr-review-canvas`, that means `project.code_host` is `github` / `gitlab` and `policies.pr_creation` is `agents-create`. `Skip` writes `disabled` under `integrations`.
127
128
 
128
129
  3. **`affects-project` accepts** — re-render the relevant template (for `implement-plan` / `amend-plan`) or re-copy the foundation skill body (for verbatim ones — delegated to [vinta-update-project-skills](../vinta-update-project-skills/SKILL.md)). Use the values in `.vinta-ai-workflows.yaml` as the substitution source. Validate every `{{PLACEHOLDER}}` is resolved.
129
130
 
@@ -65,6 +65,7 @@ For each project skill, decide its bucket:
65
65
  | `verbatim` | The skill name matches a directory under `vinta-derive-skills/resources/foundation-skills/` (e.g. `plan-feature`, `create-spec`, `create-qa-use-cases`). | Diff against source. |
66
66
  | `templated` | The skill name is `implement-plan` (or any other future templated skill listed under `vinta-derive-skills/resources/*-template.md`). | **Don't diff.** Surface as "regenerate via [vinta-derive-skills](../vinta-derive-skills/SKILL.md)". |
67
67
  | `stack-derived` | The skill maps to a stack-template prompt under `vinta-bootstrap-ai-tools/resources/stacks/<stack>/notes.md`. | Same as `templated` — surface, don't diff. |
68
+ | `integration` | The skill dir carries an external tool's ownership marker, such as `.pr-review-install` for `pr-review-canvas`. | **Never diff or edit** — the tool stamps a content hash and treats any edit as stale. Surface as "refresh with the tool's own updater" (`pr-review upgrade` for `pr-review-canvas`). |
68
69
  | `user-authored` | None of the above. | Skip silently. |
69
70
 
70
71
  For `templated` and `stack-derived`, in-place diffing is misleading because the project version contains interpolated values (commands, branch names, etc.) that won't appear in the template. The right refresh path for those is to re-run [vinta-derive-skills](../vinta-derive-skills/SKILL.md) so the user can re-supply the inputs.