vinta-ai-workflows 0.2.0-alpha7 → 0.3.0
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 +120 -2
- package/README.md +42 -25
- package/package.json +1 -1
- package/schemas/vinta-ai-workflows-config.v1.schema.json +38 -1
- package/skills/vinta-bootstrap-ai-tools/SKILL.md +15 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/SKILL.md +20 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/SKILL.md +84 -12
- package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/scripts/gen-compose-worktree-override.sh +211 -0
- package/skills/vinta-derive-skills/resources/plan-execution/partials/agent-models.md +29 -0
- package/skills/vinta-derive-skills/resources/plan-execution/partials/model-pick.md +1 -1
- package/skills/vinta-derive-skills/resources/plan-execution/partials/review-layers.md +2 -2
- package/skills/vinta-derive-skills/resources/plan-execution/partials/worktree-seam.md +1 -1
- package/skills/vinta-derive-skills/resources/plan-execution/shell/amend-plan-template.md +2 -2
- package/skills/vinta-derive-skills/resources/plan-execution/shell/implement-plan-template.md +14 -5
- package/skills/vinta-derive-skills/resources/plan-execution/shell/review-phase-template.md +14 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,125 @@ All notable changes to `vinta-ai-workflows` are documented in this file.
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
|
|
6
6
|
the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [0.
|
|
8
|
+
## [0.3.0] — 2026-07-17
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`prepare-worktree` — data-corruption bug: `COMPOSE_PROJECT_NAME` alone does
|
|
13
|
+
NOT isolate a per-worktree docker-compose stack, so worktrees could run a
|
|
14
|
+
second database server against the main checkout's data volume.** Step 4
|
|
15
|
+
assumed `COMPOSE_PROJECT_NAME` fully isolates a compose stack. It does not for
|
|
16
|
+
two very common patterns: (1) volumes declared `external: true` and/or pinned
|
|
17
|
+
to a fixed top-level `name:` — the project name namespaces containers and
|
|
18
|
+
networks but NOT these volumes, so every worktree's `db` service mounted the
|
|
19
|
+
**same physical volume**; and (2) fixed host port bindings
|
|
20
|
+
(`ports: - "5432:5432"`) — published to the same host port across compose
|
|
21
|
+
projects. On a Django + docker-compose project (2026-07-17) this ran two
|
|
22
|
+
Postgres postmasters against one PGDATA for a whole test run; when the
|
|
23
|
+
worktree stack was `docker compose down`'d, its Postgres removed the shared
|
|
24
|
+
`postmaster.pid` and the **main checkout's database self-terminated**
|
|
25
|
+
(recovered on restart — luck, not safety). The fix also corrects a deeper
|
|
26
|
+
conceptual error: Step 3a's "share the DB (don't fork)" is now explicit that
|
|
27
|
+
*share* means share a **connection to a single already-running server** — for
|
|
28
|
+
a compose-delivered DB the data volumes are **always forked, independent of
|
|
29
|
+
`schema_change` / `test_db_strategy`**, because the worktree runs its own
|
|
30
|
+
server and two servers on one volume is corruption.
|
|
31
|
+
|
|
32
|
+
**User impact / migration:** existing worktrees provisioned before this fix
|
|
33
|
+
are **unsafe to boot their DB compose stack while the main stack is up** —
|
|
34
|
+
they share main's data volume. For an already-provisioned worktree, regenerate
|
|
35
|
+
the override without re-running the whole skill:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
ai-tools/skills/prepare-worktree/scripts/gen-compose-worktree-override.sh \
|
|
39
|
+
--main <main-checkout-root> --worktree <worktree-path> --name <name> \
|
|
40
|
+
--out <summary_dir>/<name>.docker-compose.override.yml
|
|
41
|
+
echo "COMPOSE_FILE=<base-compose-file>:<override-path>" >> <worktree>/.env
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then confirm the worktree's resolved DB volume name differs from main's before
|
|
45
|
+
booting its stack. **Consumers**: re-sync (`vinta-sync-ai-tools`) to pick up
|
|
46
|
+
the new script, the reworked Step 4/Step 3a, and the `shared_volumes` config.
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- **`prepare-worktree` bundled script
|
|
51
|
+
`scripts/gen-compose-worktree-override.sh`.** Detects the compose isolation
|
|
52
|
+
leaks above entirely from `docker compose config --format json` (no project /
|
|
53
|
+
service / volume name hardcoded) and emits a **generated, out-of-tree**
|
|
54
|
+
compose override that re-pins every `external:`/fixed-`name:` volume to a
|
|
55
|
+
non-external, worktree-namespaced volume (`<project>_wt_<name>_<volkey>`) and
|
|
56
|
+
strips fixed host-port publishing (via compose's `!override []` — a plain
|
|
57
|
+
`ports: []` merges/appends in Compose v2+; verified on Docker Compose v5.1.1).
|
|
58
|
+
The override is written under `<summary_dir>/` and wired in via
|
|
59
|
+
`COMPOSE_FILE=<base>:<override>` appended to the worktree's **copied** `.env`,
|
|
60
|
+
so **no tracked compose file is ever mutated**. Volumes safe to keep shared
|
|
61
|
+
(a read-only dep/venv cache) are exempted via `--share-volume`.
|
|
62
|
+
- **`skills.prepare-worktree.shared_volumes` config field** (array of compose
|
|
63
|
+
volume keys, default `[]`). Lists volumes safe to keep shared across worktrees
|
|
64
|
+
— read-only dep/venv caches only, never a data/DB volume. Feeds
|
|
65
|
+
`gen-compose-worktree-override.sh --share-volume`. Added to the config schema
|
|
66
|
+
and the bootstrap template's `prepare-worktree` block.
|
|
67
|
+
- **`prepare-worktree` Step 4a "Neutralize compose isolation leaks"**, a
|
|
68
|
+
Verification invariant that boots the worktree's DB service and asserts its
|
|
69
|
+
resolved data volume name differs from main's (failing provisioning loudly
|
|
70
|
+
otherwise), a matching Pitfall + Rules entry, and a `state.compose` summary
|
|
71
|
+
schema extension (per-volume fork decisions, override path, stripped ports)
|
|
72
|
+
so teardown is mechanical (`docker volume rm` the forked names; `rm` the
|
|
73
|
+
override — never `down -v`, which would nuke shared volumes).
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
|
|
77
|
+
- **`prepare-worktree` foundation skill — rebalanced Step 3b (test database)
|
|
78
|
+
to be stack-agnostic.** The section previously led with a detailed
|
|
79
|
+
`pytest-django` recipe (with `vitest`/`jest` and Rails as one-liners),
|
|
80
|
+
which read as Django-centric. It now opens with the stack-agnostic
|
|
81
|
+
principle — give the worktree its own test DB name, inject it through
|
|
82
|
+
whatever channel the runner already reads (env var or per-worktree config
|
|
83
|
+
override), and never edit tracked test config in place — then lists the
|
|
84
|
+
runners as equal-weight illustrations grouped by *how they take config*:
|
|
85
|
+
env-var runners (`vitest`/`jest`, `go test`, `cargo test`), config-file
|
|
86
|
+
runners (`pytest-django`), convention-based runners (Rails), and
|
|
87
|
+
compose-based test DBs. No behavior change; the pytest-django mechanics
|
|
88
|
+
(`conftest_worktree.py`, don't touch the tracked `conftest.py`) are
|
|
89
|
+
preserved. **Consumers**: re-sync to pick up the reworded skill body.
|
|
90
|
+
|
|
91
|
+
## [0.2.0] — 2026-07-15
|
|
92
|
+
|
|
93
|
+
### Added
|
|
94
|
+
|
|
95
|
+
- **Project-wide model selection for the review sub-agents and mechanical
|
|
96
|
+
steps, via a new optional `agent_models` section in
|
|
97
|
+
`.vinta-ai-workflows.yaml`.** Until now only the per-phase *implementer*
|
|
98
|
+
model was configurable (each plan phase's `**Suggested AI model**:` line);
|
|
99
|
+
the reviewer, the fixer, and the mechanical steps (worktree prep, opening
|
|
100
|
+
the PR / integrate) always inherited the runtime default. `agent_models`
|
|
101
|
+
maps each of `reviewer` / `fixer` / `worktree_prep` / `integrate` to a
|
|
102
|
+
**tier (1–4)** into the same `plan-feature/resources/ai-models.yaml` table
|
|
103
|
+
the implementer uses — so no model IDs are hard-coded and the nightly
|
|
104
|
+
`check-ai-models` job keeps them fresh. Behavior:
|
|
105
|
+
- `review-phase` spawns the reviewer at `agent_models.reviewer` and the
|
|
106
|
+
fixer at `agent_models.fixer` **as the project default** — a plan phase
|
|
107
|
+
can override either for that phase via an optional `**Review models**:`
|
|
108
|
+
line (per-phase override wins → else `agent_models` → else runtime
|
|
109
|
+
default). Lets a plan raise the review model on its few critical /
|
|
110
|
+
high-blast-radius phases without changing the project-wide setting.
|
|
111
|
+
- `implement-plan` **delegates** the mechanical steps to a cheap subagent
|
|
112
|
+
when `agent_models.worktree_prep` / `agent_models.integrate` is set —
|
|
113
|
+
provisioning the worktree and pushing + opening the PR run on the
|
|
114
|
+
configured model instead of the conductor's (usually pricier) session.
|
|
115
|
+
The PR-context file + `open-pr.sh` stays the only PR-creation path.
|
|
116
|
+
- `plan-feature` phases always pick the implementer model
|
|
117
|
+
(`**Suggested AI model**:`) and MAY add an optional `**Review models**:`
|
|
118
|
+
line to override the reviewer / fixer tier for that phase; the mechanical
|
|
119
|
+
models stay config-only.
|
|
120
|
+
Every key is optional and backward-compatible: an unset key (or the whole
|
|
121
|
+
section absent) means that spawn keeps using the runtime default.
|
|
122
|
+
**Consumers**: re-sync to pick up the `agent_models` bootstrap question and
|
|
123
|
+
the updated plan-execution skills; existing configs are unaffected until
|
|
124
|
+
they add the section.
|
|
125
|
+
|
|
126
|
+
<!-- pre-release: 0.2.0-alpha7 on 2026-07-13 -->
|
|
9
127
|
|
|
10
128
|
### Changed
|
|
11
129
|
|
|
@@ -30,7 +148,7 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
30
148
|
are unaffected (the e2e regions strip out as before).
|
|
31
149
|
|
|
32
150
|
|
|
33
|
-
<!-- pre-release: 0.2.0-
|
|
151
|
+
<!-- pre-release: 0.2.0-alpha6 on 2026-07-13 -->
|
|
34
152
|
|
|
35
153
|
### Changed
|
|
36
154
|
|
package/README.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Bootstrap AI tooling — AGENTS.md, sub-agents, project skills, multi-vendor wiring — into any project, then get out of the way.
|
|
4
4
|
|
|
5
|
-
Distributed as
|
|
5
|
+
Distributed as an npm package (`vinta-ai-workflows`) with a CLI that installs / updates / uninstalls a set of one-shot bootstrap skills into the project's vendor-specific skill directories (Claude Code, Codex, Cursor, VS Code + GitHub Copilot).
|
|
6
6
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
# 1. Add the package to the target project.
|
|
11
11
|
cd ~/code/my-project
|
|
12
|
-
npm install -D
|
|
12
|
+
npm install -D vinta-ai-workflows
|
|
13
13
|
|
|
14
14
|
# 2. Install the bootstrap skills for your AI tool.
|
|
15
15
|
npx vinta-ai-workflows install --tool claude-code
|
|
@@ -28,7 +28,7 @@ npx vinta-ai-workflows uninstall --tool claude-code
|
|
|
28
28
|
npm uninstall vinta-ai-workflows
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Pin a version for determinism with `npm install -D vinta-ai-workflows@0.2.0`. Prerequisites: Node ≥ 18.
|
|
32
32
|
|
|
33
33
|
## Why this is useful
|
|
34
34
|
|
|
@@ -208,10 +208,22 @@ Standalone re-invocation works at every step — kick `create-spec` again when t
|
|
|
208
208
|
|
|
209
209
|
Quick inventory of what `vinta-bootstrap-ai-tools` writes into your repo's `ai-tools/` layout (and exposes via per-vendor wiring at `.claude/skills/`, `.agents/skills/`, etc.). Two disclaimers up front:
|
|
210
210
|
|
|
211
|
-
> **Optional foundation skills are gated by the bootstrap interview.** `systematic-debugging`, `add-e2e-test`, `add-env-var`,
|
|
211
|
+
> **Optional foundation skills are gated by the bootstrap interview.** `systematic-debugging`, `add-e2e-test`, `add-env-var`, `add-one-off-script`, `prepare-worktree`, `thermo-nuclear-code-quality-review`, and `handoff-to-client` ship **only** if the user says yes during `vinta-bootstrap-ai-tools` Step 0 (or later opts in via `vinta-sync-ai-tools`). They are not in every bootstrapped project. The opt-in is recorded in `.vinta-ai-workflows.yaml` under `foundation_skills.*.enabled` and is sticky across syncs.
|
|
212
212
|
>
|
|
213
213
|
> **Stack-specific skills and sub-agents are user-supplied.** This package ships **detection signals + category lists** per stack ([resources/stacks/](skills/vinta-bootstrap-ai-tools/resources/stacks/) — Django, Medplum, Next.js App Router, Python package, TypeScript monorepo), not the bodies. When a stack is detected, the orchestrator asks the user for a path / URL to their team's existing template. If no template exists, the category is recorded as a gap in the final summary, not auto-drafted. Bodies are project- and team-specific; one shared template doesn't fit every team's conventions.
|
|
214
214
|
|
|
215
|
+
### New in the 0.2.0 releases
|
|
216
|
+
|
|
217
|
+
The 0.2.0 alpha line adds five foundation skills and restructures plan execution. Two ship in every bootstrapped project, three are opt-in:
|
|
218
|
+
|
|
219
|
+
- **`handoff` (always)** — writes a session-continuation doc to `.vinta-ai-workflows/handoffs/` so a fresh session, a different agent, or a teammate can resume in-flight work without re-deriving context. Write mode gathers the goal, what's verified vs. still assumed, decisions and the alternatives rejected, landmines, and the single concrete next step **from the repo, never from memory**. Resume mode reads a handoff and re-checks its claims against the repo before trusting them.
|
|
220
|
+
- **`deslop-comments` (always)** — rewrites the comments and doc blocks a task touched into Simple English, stripping AI-slop vocabulary and negative framing. Comment-only: no renames, no behavior change. It's wired into the review flow — `review-phase` Layer 2 gained a comment-hygiene check and dispatches a `fixer` to run this skill on any comment-slop finding — and is also invokable standalone ("deslop these comments").
|
|
221
|
+
- **`prepare-worktree` (opt-in)** — provisions a fully-runnable git worktree for parallel plan work so a long-running plan can build, test, migrate, and hit databases without disturbing the main checkout. It reads the plan plus `.gitignore` / manifests / env / docker config and decides per ignored path whether to symlink, copy, or fork (dev DB, env files, compose project name). It also ships an OS-level write-guard — `sandbox-run.sh` (`sandbox-exec` / `bwrap`) for subprocess runtimes and a `PreToolUse` hook for Claude Code's in-process subagents — so a stray write back to the main checkout fails at the kernel layer instead of relying on a cooperative "stay in the worktree" instruction. `implement-plan` Step 0 question (c) opts a run into it.
|
|
222
|
+
- **`thermo-nuclear-code-quality-review` (opt-in)** — a deliberately harsh, on-demand structural-maintainability audit of a diff (abstraction quality, giant files, spaghetti-condition growth) that hunts for "code-judo" reframes collapsing whole branches / helpers / modes / layers rather than polishing them. Read-only — it reports findings and hands each fix to a `fixer`. `review-phase` Layer 3 now applies a condensed version of this lens on every phase and escalates to the full audit only when a phase touches core architecture, crosses ~1,000 lines, or surfaces a structural smell too big to fix inline.
|
|
223
|
+
- **`handoff-to-client` (opt-in, API-only repos)** — generates a self-contained markdown document for the client teams consuming the repo's API: every endpoint / operation added, changed, deprecated, or removed on the current branch vs. the default branch, with request/response shapes derived from the code, auth + error changes, breaking-change flags judged from the strictest plausible client, one realistic example per operation, and per-platform migration notes. Template-rendered from `skills.handoff-to-client.*` config (required `client_platforms`, plus `api_style` / `api_spec_path` / `output_dir`).
|
|
224
|
+
|
|
225
|
+
Alongside the new skills, **`implement-plan` was decomposed into a modular plan-execution unit** — a thin conductor plus three co-shipped single-purpose sub-skills (`implement-phase`, `review-phase`, `integrate-phase`). `review-phase` is now the one review implementation shared by `implement-plan`, `amend-plan`, and `systematic-debugging`. The behavior is unchanged; the split is for reviewability and determinism. Two more knobs land in `.vinta-ai-workflows.yaml`: **`agent_models`** (set a tier per `reviewer` / `fixer` / `worktree_prep` / `integrate` so the review sub-agents and mechanical steps aren't stuck on the runtime default), and **E2E tests are now opt-in** in both `plan-feature` and `implement-plan` (they make runs materially slower; default off, re-enable per-run or per-project).
|
|
226
|
+
|
|
215
227
|
### Foundation skills (project-agnostic)
|
|
216
228
|
|
|
217
229
|
Land at `ai-tools/skills/<name>/SKILL.md`. Always-on unless flagged optional.
|
|
@@ -227,10 +239,15 @@ Land at `ai-tools/skills/<name>/SKILL.md`. Always-on unless flagged optional.
|
|
|
227
239
|
| `review-phase` | always (unit) | Plan-execution sub-skill: the three-layer review (mechanical / plan-compliance / independent reviewer) + fix loop. Shared by `implement-plan`, `amend-plan`, and `systematic-debugging`. |
|
|
228
240
|
| `integrate-phase` | always (unit) | Plan-execution sub-skill: push the reviewed phase along the project's commit strategy + open the PR via context file. Under `commit_strategy = ask`, ships as `integrate-phase-stacked` + `integrate-phase-modular`. |
|
|
229
241
|
| `amend-plan` | always (generated) | History-rewriting companion to `implement-plan` — revises in-flight plans, amends prior-phase commits, force-pushes, rebases stacked downstream branches. Reuses `review-phase` + `implement-phase`'s shared prompt loop. |
|
|
242
|
+
| [`handoff`](skills/vinta-derive-skills/resources/foundation-skills/handoff/SKILL.md) | always | Session-continuation handoff doc → `.vinta-ai-workflows/handoffs/`. **Write** captures goal, verified-vs-unverified state, decisions + rejected alternatives, landmines, and the single next step — gathered from the repo, never memory. **Resume** verifies a handoff's claims against the repo before continuing from its next step. |
|
|
243
|
+
| [`deslop-comments`](skills/vinta-derive-skills/resources/foundation-skills/deslop-comments/SKILL.md) | always | Rewrite comments + doc blocks touched during a task into Simple English (strips AI-slop vocabulary + negative framing; comment-only — no renames, no behavior change). `review-phase` Layer 2 depends on it; also invokable standalone. |
|
|
230
244
|
| `systematic-debugging` | **opt-in** | Root-cause-first debugging with project-specific repro commands + MCP evidence-gathering (error tracking, traces, logs, metrics, alerts). Renders from a catalogue of observability MCP servers the user declares. |
|
|
231
245
|
| `add-e2e-test` | **opt-in** | Add an e2e test. Body covers e2e framework, page-object pattern, auth/storage-state, seed helpers, tenant scoping, screenshot conventions. |
|
|
232
246
|
| `add-env-var` | **opt-in** | Propagate a new env var through every layer (`.env.example`, build tool envPrefix, build cache hash, app config, AGENTS.md, CI, deploy injection). |
|
|
233
247
|
| [`add-one-off-script`](skills/vinta-derive-skills/resources/foundation-skills/add-one-off-script/SKILL.md) | **opt-in** | Author one-off operational scripts (backfills, cleanups, ad-hoc fixes). Ships a `BaseOneOffScript` class (Python + TS) enforcing dry-run default, idempotency, batched DB ops, segmented CSV backups, signal-safe interruption, multi-sink logging. |
|
|
248
|
+
| [`prepare-worktree`](skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/SKILL.md) | **opt-in** | Provision a fully-runnable git worktree for parallel plan work — reads the plan + `.gitignore` + manifests + env / docker config and decides per ignored path whether to **symlink / copy / fork** (dev DB, env files, compose project name). Ships an OS-level write-guard (`sandbox-run.sh` + a Claude Code `PreToolUse` hook) so stray writes to the main checkout fail at the kernel layer. `implement-plan` Step 0 question (c) opts a run into it. |
|
|
249
|
+
| [`thermo-nuclear-code-quality-review`](skills/vinta-derive-skills/resources/foundation-skills/thermo-nuclear-code-quality-review/SKILL.md) | **opt-in** | Deliberately harsh structural-maintainability audit of a diff — abstraction quality, giant files, spaghetti-condition growth — hunting "code-judo" reframes that collapse whole branches / helpers / modes / layers. Read-only; hands each fix to `fixer`. `review-phase` Layer 3 runs a condensed lens every phase and escalates to this full audit on core-architecture / ~1000-line / structurally-smelly phases. |
|
|
250
|
+
| `handoff-to-client` | **opt-in** | (API-only repos) Generate a self-contained API-change handoff doc for the client teams consuming the repo's API — every endpoint / operation added / changed / deprecated / removed vs the default branch, with request/response shapes, auth + error changes, breaking-change flags, one example per operation, and per-platform migration notes. Template-rendered from `skills.handoff-to-client.*` config (client platforms, API style, spec path). |
|
|
234
251
|
|
|
235
252
|
### Foundation sub-agents (project-agnostic)
|
|
236
253
|
|
|
@@ -251,7 +268,7 @@ Bootstrap is a snapshot. `vinta-ai-workflows` keeps shipping — new foundation
|
|
|
251
268
|
### The flow at a glance
|
|
252
269
|
|
|
253
270
|
```bash
|
|
254
|
-
# 1. Pull the new package version
|
|
271
|
+
# 1. Pull the new package version.
|
|
255
272
|
npm update vinta-ai-workflows
|
|
256
273
|
|
|
257
274
|
# 2. Run the sync skill from your AI tool.
|
|
@@ -314,48 +331,48 @@ To bring a previously-bootstrapped project up to date with a newer package versi
|
|
|
314
331
|
|
|
315
332
|
## Install variants
|
|
316
333
|
|
|
317
|
-
The package is
|
|
318
|
-
|
|
319
|
-
### A. From git+ssh (no registry needed)
|
|
334
|
+
The package is published to the public npm registry. Three install paths.
|
|
320
335
|
|
|
321
|
-
|
|
336
|
+
### A. From the npm registry (default)
|
|
322
337
|
|
|
323
338
|
```bash
|
|
324
339
|
# inside the target project
|
|
325
|
-
npm install -D
|
|
326
|
-
pnpm add -D
|
|
327
|
-
yarn add -D
|
|
328
|
-
bun add -d
|
|
340
|
+
npm install -D vinta-ai-workflows
|
|
341
|
+
pnpm add -D vinta-ai-workflows
|
|
342
|
+
yarn add -D vinta-ai-workflows
|
|
343
|
+
bun add -d vinta-ai-workflows
|
|
329
344
|
```
|
|
330
345
|
|
|
331
|
-
Pin a
|
|
346
|
+
Pin a version when you want determinism:
|
|
332
347
|
|
|
333
348
|
```bash
|
|
334
|
-
npm install -D
|
|
349
|
+
npm install -D vinta-ai-workflows@0.2.0
|
|
335
350
|
```
|
|
336
351
|
|
|
337
|
-
### B. From
|
|
352
|
+
### B. From git+ssh (an unreleased ref)
|
|
338
353
|
|
|
339
|
-
|
|
354
|
+
Use this to install a branch or commit that isn't published yet — each developer's GitHub SSH key needs read access to the repo:
|
|
340
355
|
|
|
341
|
-
```
|
|
342
|
-
|
|
343
|
-
|
|
356
|
+
```bash
|
|
357
|
+
# inside the target project
|
|
358
|
+
npm install -D git+ssh://git@github.com:vintasoftware/vinta-ai-workflows.git
|
|
359
|
+
pnpm add -D git+ssh://git@github.com:vintasoftware/vinta-ai-workflows.git
|
|
360
|
+
yarn add -D git+ssh://git@github.com:vintasoftware/vinta-ai-workflows.git
|
|
361
|
+
bun add -d git+ssh://git@github.com:vintasoftware/vinta-ai-workflows.git
|
|
344
362
|
```
|
|
345
363
|
|
|
346
|
-
|
|
364
|
+
Pin a tag/commit when you want determinism:
|
|
347
365
|
|
|
348
366
|
```bash
|
|
349
|
-
npm install -D vinta-ai-workflows
|
|
367
|
+
npm install -D git+ssh://git@github.com:vintasoftware/vinta-ai-workflows.git#0.1.0
|
|
350
368
|
```
|
|
351
369
|
|
|
352
|
-
### One-shot via npx (no install)
|
|
370
|
+
### C. One-shot via npx (no install)
|
|
353
371
|
|
|
354
372
|
If you don't want the dep tracked in `package.json`:
|
|
355
373
|
|
|
356
374
|
```bash
|
|
357
|
-
npx -y -p
|
|
358
|
-
vinta-ai-workflows install --tool all
|
|
375
|
+
npx -y -p vinta-ai-workflows vinta-ai-workflows install --tool all
|
|
359
376
|
```
|
|
360
377
|
|
|
361
378
|
### Picking a tool
|
package/package.json
CHANGED
|
@@ -245,6 +245,37 @@
|
|
|
245
245
|
"fixer": { "type": "string", "enum": ["enabled", "disabled"] }
|
|
246
246
|
}
|
|
247
247
|
},
|
|
248
|
+
"agent_models": {
|
|
249
|
+
"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.",
|
|
250
|
+
"type": "object",
|
|
251
|
+
"additionalProperties": false,
|
|
252
|
+
"properties": {
|
|
253
|
+
"reviewer": {
|
|
254
|
+
"description": "Default tier for the independent reviewer subagent spawned by `review-phase` (Layer 3). Reviewing wants a capable model — Tier 3 is the usual default. A plan phase's `**Review models**:` line can override this for that phase. Unset (and no phase override) → runtime default.",
|
|
255
|
+
"type": "integer",
|
|
256
|
+
"minimum": 1,
|
|
257
|
+
"maximum": 4
|
|
258
|
+
},
|
|
259
|
+
"fixer": {
|
|
260
|
+
"description": "Default tier for the fixer subagent spawned by `review-phase`'s fix loop to apply one reviewer finding. Narrow, well-specified edits — Tier 2 is the usual default. A plan phase's `**Review models**:` line can override this for that phase. Unset (and no phase override) → runtime default.",
|
|
261
|
+
"type": "integer",
|
|
262
|
+
"minimum": 1,
|
|
263
|
+
"maximum": 4
|
|
264
|
+
},
|
|
265
|
+
"worktree_prep": {
|
|
266
|
+
"description": "Tier for the mechanical worktree-provisioning step (`prepare-worktree`), which `implement-plan` / `amend-plan` run once at the start of a worktree-enabled run. When set, the conductor delegates the step to a subagent at this tier instead of running it inline in its own (usually pricier) session. Mechanical, precedent-driven work — Tier 1 is the usual default. Unset → conductor runs it inline. Irrelevant when `foundation_skills.prepare-worktree` is `disabled`.",
|
|
267
|
+
"type": "integer",
|
|
268
|
+
"minimum": 1,
|
|
269
|
+
"maximum": 4
|
|
270
|
+
},
|
|
271
|
+
"integrate": {
|
|
272
|
+
"description": "Tier for the mechanical integrate step (`integrate-phase`: push the phase branch + open/update the PR via the bundled `open-pr.sh`) that `implement-plan` runs once per phase. When set, the conductor delegates the step to a subagent at this tier instead of running it inline. Mechanical git + PR-context work — Tier 1 is the usual default. Unset → conductor runs it inline. The PR-context file + `open-pr.sh` remains the only PR-creation path regardless of who runs the step.",
|
|
273
|
+
"type": "integer",
|
|
274
|
+
"minimum": 1,
|
|
275
|
+
"maximum": 4
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
},
|
|
248
279
|
"stacks": {
|
|
249
280
|
"description": "Stack templates applied at bootstrap (e.g. `medplum`, `django`, `nextjs-app-router`).",
|
|
250
281
|
"type": "array",
|
|
@@ -389,10 +420,16 @@
|
|
|
389
420
|
"default": "fork-on-schema-change"
|
|
390
421
|
},
|
|
391
422
|
"summary_dir": {
|
|
392
|
-
"description": "Where per-worktree summary YAMLs land. Default `.vinta-ai-workflows/worktrees` — covered by the umbrella gitignore added in 0.1.3. The skill writes `<summary_dir>/<name>.yaml` per provisioned worktree; teardown reads it.",
|
|
423
|
+
"description": "Where per-worktree summary YAMLs land. Default `.vinta-ai-workflows/worktrees` — covered by the umbrella gitignore added in 0.1.3. The skill writes `<summary_dir>/<name>.yaml` per provisioned worktree; teardown reads it. The generated compose override (see `shared_volumes`) also lands here as `<summary_dir>/<name>.docker-compose.override.yml`.",
|
|
393
424
|
"type": "string",
|
|
394
425
|
"minLength": 1,
|
|
395
426
|
"default": ".vinta-ai-workflows/worktrees"
|
|
427
|
+
},
|
|
428
|
+
"shared_volumes": {
|
|
429
|
+
"description": "Compose volume KEYS (as they appear under top-level `volumes:` in the project's compose config) that are safe to keep SHARED across worktrees — a read-only dep/venv/asset cache with no write churn. The skill's compose-isolation step (Step 4a) forks EVERY `external:`/fixed-`name:` volume into a worktree-namespaced copy by default (COMPOSE_PROJECT_NAME does not isolate those, and two servers on one DATA volume corrupts the store); volumes listed here are exempted and stay shared. NEVER list a database/data volume here. Passed through to `scripts/gen-compose-worktree-override.sh --share-volume`. Empty/omitted = fork every leaky volume (safest).",
|
|
430
|
+
"type": "array",
|
|
431
|
+
"items": { "type": "string", "minLength": 1 },
|
|
432
|
+
"default": []
|
|
396
433
|
}
|
|
397
434
|
}
|
|
398
435
|
},
|
|
@@ -81,6 +81,8 @@ These bleed across sub-skills, so capture once now:
|
|
|
81
81
|
|
|
82
82
|
Lands in `.vinta-ai-workflows.yaml` `policies.commit_strategy`. Drives the `implement-plan` template's branch / commit / PR-opening flow + the `amend-plan` skill's supported-strategies check.
|
|
83
83
|
|
|
84
|
+
9. **Agent model tiers (optional).** The plan-execution family picks the **implementer** model per phase from the plan itself, but the **reviewer**, **fixer**, and the mechanical steps (**worktree prep**, **opening the PR / integrate**) have no model input unless the project sets one. Offer to pin each to a **tier** (1–4) into the same table `plan-feature` uses — `ai-tools/skills/plan-feature/resources/ai-models.yaml` — so review runs on a capable model while cheap mechanical work runs on a cheap one. `AskUserQuestion`: `Use recommended defaults (reviewer 3, fixer 2, worktree prep 1, integrate 1)`, `Customize each`, `Leave unset — every spawn uses the runtime default`. On `Customize each`, ask a per-role tier (1–4) for `reviewer` / `fixer` / `worktree_prep` / `integrate`. Land the chosen tiers in `.vinta-ai-workflows.yaml` `agent_models`; omit any role the user leaves unset (unset → runtime default). Recommend the defaults for most teams; recommend `Leave unset` only when the runtime exposes a single model anyway. Note `worktree_prep` is a no-op unless `prepare-worktree` is enabled, and setting `worktree_prep` / `integrate` makes `implement-plan` delegate those steps to a cheap subagent instead of running them inline.
|
|
85
|
+
|
|
84
86
|
### D. Optional foundation skills
|
|
85
87
|
|
|
86
88
|
Seven skills are part of the foundation set but aren't always needed. Ask explicitly:
|
|
@@ -270,6 +272,15 @@ foundation_agents:
|
|
|
270
272
|
reviewer: enabled
|
|
271
273
|
fixer: enabled
|
|
272
274
|
|
|
275
|
+
# Only emit the roles the user chose in the "Agent model tiers" question (C.9).
|
|
276
|
+
# Omit the whole block on "Leave unset"; omit any individual role left unset.
|
|
277
|
+
# Each value is a tier (1–4) into ai-tools/skills/plan-feature/resources/ai-models.yaml.
|
|
278
|
+
agent_models:
|
|
279
|
+
reviewer: <C.9 → reviewer tier, default 3>
|
|
280
|
+
fixer: <C.9 → fixer tier, default 2>
|
|
281
|
+
worktree_prep: <C.9 → worktree_prep tier, default 1; omit when prepare-worktree disabled>
|
|
282
|
+
integrate: <C.9 → integrate tier, default 1>
|
|
283
|
+
|
|
273
284
|
stacks: <Stack detection → matched stacks>
|
|
274
285
|
stack_specialist_agents: <empty unless user supplied templates>
|
|
275
286
|
|
|
@@ -301,6 +312,10 @@ skills:
|
|
|
301
312
|
compose_network: per-worktree
|
|
302
313
|
test_db_strategy: <prepare-worktree follow-up — fork-on-schema-change | always-fork | share>
|
|
303
314
|
summary_dir: .vinta-ai-workflows/worktrees
|
|
315
|
+
# Compose volume keys safe to keep shared across worktrees (read-only dep/venv
|
|
316
|
+
# caches only — NEVER a data/DB volume). Empty = fork every external/named
|
|
317
|
+
# volume (safest). See prepare-worktree Step 4a.
|
|
318
|
+
shared_volumes: []
|
|
304
319
|
|
|
305
320
|
# Only emit this block when foundation_skills.add-one-off-script = enabled.
|
|
306
321
|
add-one-off-script:
|
|
@@ -245,6 +245,8 @@ Tests:
|
|
|
245
245
|
|
|
246
246
|
**Suggested AI model**: {tier choice + why}. See "AI model selection".
|
|
247
247
|
|
|
248
|
+
**Review models** (optional — omit for the project defaults): reviewer Tier {N}, fixer Tier {N} — {why this phase warrants a non-default review model}. See "AI model selection".
|
|
249
|
+
|
|
248
250
|
**Reusable skills**: {invoke `Skill(name)` — see "Project skills"}.
|
|
249
251
|
|
|
250
252
|
Acceptance: {one literal statement true after merge + deploy of this phase, only this phase}.
|
|
@@ -318,6 +320,15 @@ Common mistake: leave cross-repo producer wiring for last, then discover the ups
|
|
|
318
320
|
|
|
319
321
|
For each phase, suggest **cheapest/fastest model likely to one-shot work**. Iterating with cheap model usually beats burning Opus tokens on CRUD scaffold.
|
|
320
322
|
|
|
323
|
+
**Scope: the plan always picks the *implementer* model, and MAY override the reviewer / fixer models per phase.**
|
|
324
|
+
|
|
325
|
+
- `**Suggested AI model**:` drives the implementer subagent that writes the phase — **required on every phase**.
|
|
326
|
+
- `**Review models**:` (optional) overrides the reviewer and/or fixer tier **for this phase only**. Use it when a phase is riskier than average — high blast-radius change, subtle concurrency / transaction logic, security-sensitive surface, a migration that's hard to undo — and you want a more capable reviewer or fixer than the project default. Name a tier for reviewer, fixer, or both; omit either to leave that role on the default.
|
|
327
|
+
- **Precedence** (resolved by `implement-plan` / `review-phase`): a phase's `**Review models**:` override wins → else the project-wide `agent_models.reviewer` / `agent_models.fixer` tier in `.vinta-ai-workflows.yaml` → else the runtime default. So the project keeps sane defaults and the plan only speaks up for the phases that need a different review model.
|
|
328
|
+
- The mechanical-step models (worktree prep, opening the PR / integrate) are **not** plan-owned — they stay under `agent_models` in `.vinta-ai-workflows.yaml`. Don't add worktree/PR model hints to a phase; they'd be ignored.
|
|
329
|
+
|
|
330
|
+
**Most phases carry only the implementer line.** Add `**Review models**:` deliberately, for the few phases that earn it — not by default on every phase.
|
|
331
|
+
|
|
321
332
|
**Concrete model IDs per tier live in [resources/ai-models.yaml](resources/ai-models.yaml) — read that file when writing each suggestion. Never recall model names from memory; they go stale as vendors ship.** The tiers below define *when* each applies (stable judgement); the IDs drift, and a nightly job keeps the resource current. Note the file's `last_verified` date — if it's far in the past, the IDs may be stale; flag that rather than trusting them blindly.
|
|
322
333
|
|
|
323
334
|
### Tier 1 — cheapest/fastest (boilerplate, exact-precedent edits)
|
|
@@ -342,6 +353,14 @@ When one tier doesn't fit, name both:
|
|
|
342
353
|
|
|
343
354
|
> **Suggested AI model**: Tier 2 for repository + serializer; step up to Tier 3 for the integration test spanning upsert → routing → reprocess. IDs per tier in [resources/ai-models.yaml](resources/ai-models.yaml).
|
|
344
355
|
|
|
356
|
+
### Overriding the review models on a critical phase (optional)
|
|
357
|
+
|
|
358
|
+
Add a `**Review models**:` line **only** when the phase justifies a non-default reviewer / fixer. Pick the tier from the same rubric — a higher tier for the *review* of a delicate change, not for its authoring:
|
|
359
|
+
|
|
360
|
+
> **Review models**: reviewer Tier 4 — this phase rewrites the transactional batch-apply protocol with deferred constraints; a subtle ordering bug here corrupts data, so the independent review runs on the most capable model. Fixer left on the project default.
|
|
361
|
+
|
|
362
|
+
Name only the role you're changing (`reviewer`, `fixer`, or both). Omitting the line entirely — the common case — leaves both roles on the project's `agent_models` defaults.
|
|
363
|
+
|
|
345
364
|
**Default to cheapest tier that plausibly works, not safest.** Cheap models failing fast beats expensive succeeding slowly.
|
|
346
365
|
|
|
347
366
|
## Project skills to leverage
|
|
@@ -406,6 +425,7 @@ When in doubt, model the plan after a recent example in `ai-plans/` — look for
|
|
|
406
425
|
- [ ] Phase numbering uses numbers + letters consistently.
|
|
407
426
|
- [ ] **Phase granularity matches the Step 0 answer.** Default (one-use-case-per-phase): at least one phase per spec use-case, no phase implements two use-cases. If bundling was chosen: grouped phases stay MR-sized, one concern, independently mergeable. Cross-cutting scaffolding is its own foundation phase either way.
|
|
408
427
|
- [ ] Each phase has Goal / Spec use-case / Feature flag (or explicit waiver) / Changes / Tests / Suggested AI model / Reusable skills / Acceptance.
|
|
428
|
+
- [ ] `**Review models**:` appears **only** on phases that justify a non-default reviewer / fixer (not on every phase); each such line names a tier + why. Phases without it inherit the project's `agent_models` defaults.
|
|
409
429
|
<!-- e2e:start -->
|
|
410
430
|
- [ ] **If e2e coverage was opted into at Step 0:** every phase introducing a new UI flow has an **E2E happy-path test** in its Tests block, with screenshot output to `pr-screenshots/`. If it was not opted into (default), **no phase carries an e2e spec** and there is no `QA_USE_CASES.md` / `pr-screenshots/` reference.
|
|
411
431
|
<!-- e2e:end -->
|
|
@@ -109,6 +109,8 @@ Default: **copy** for pnpm + npm + cargo + go; **reinstall** for poetry + venv +
|
|
|
109
109
|
|
|
110
110
|
`env_change = true` → `cp <main>/.env <worktree>/.env`. Then mutate per the **Database fork** step (DB URL) + the **Docker / compose isolation** step (compose project name).
|
|
111
111
|
|
|
112
|
+
**Compose forces a copy.** If the project uses compose, the [4a](#4a--neutralize-compose-isolation-leaks) sub-step appends `COMPOSE_FILE=…` to the worktree's `.env` — a mutation. So a compose project ALWAYS needs a copied `.env` (or a copied `.envrc`) even when `env_change = false`; a symlinked `.env` would leak the `COMPOSE_FILE` line into main. Treat "project uses compose" as an independent reason to copy `.env`.
|
|
113
|
+
|
|
112
114
|
`.env.example` is **always** symlinked — it's committed; the worktree should keep tracking it.
|
|
113
115
|
|
|
114
116
|
### 2c — Skip rules
|
|
@@ -129,6 +131,11 @@ Whatever the app reads + writes during local dev. Detect the connection string s
|
|
|
129
131
|
2. Project config files: `config/database.yml` (Rails), `database.ini`, `prisma/schema.prisma`'s `datasource`, `knexfile.js`, Django `settings.py`'s `DATABASES`.
|
|
130
132
|
3. Docker compose service env: `services.db.environment.POSTGRES_DB`.
|
|
131
133
|
|
|
134
|
+
**First, determine HOW the DB is delivered — this changes what "share" even means.** Two deliveries:
|
|
135
|
+
|
|
136
|
+
- **External server** — the DB runs on a server the worktree only *connects* to (a system Postgres, an RDS instance, a compose service the worktree does NOT boot its own copy of). Here "share" = point the worktree's connection string at the one already-running server. Fork = clone into a new DB *on that same server*.
|
|
137
|
+
- **Per-worktree compose stack** — the DB is delivered *through the worktree's own `docker compose up`* (a `db` service the worktree boots). Here the worktree runs its **own server process**. "Share" the way the connection-string sense means is impossible without corruption: booting a second server against the same data volume is two postmasters on one PGDATA. **For compose-delivered DBs the data volumes are ALWAYS forked, independent of `schema_change`** — see the [Neutralize compose isolation leaks](#4a--neutralize-compose-isolation-leaks) sub-step, which does the volume fork mechanically. `schema_change = false` may still let you skip *cloning main's data* into the forked volume (boot empty + migrate), but it never authorizes a shared data volume.
|
|
138
|
+
|
|
132
139
|
For each detected DB, ask the user (`AskUserQuestion`) once:
|
|
133
140
|
|
|
134
141
|
- **Fork the DB** — recommended when `schema_change = true` or when the plan's phases run destructive migrations. Strategy depends on the engine:
|
|
@@ -137,7 +144,7 @@ For each detected DB, ask the user (`AskUserQuestion`) once:
|
|
|
137
144
|
- **SQLite (file-based)** — `cp <main>/db.sqlite3 <worktree>/db.sqlite3`. Symlink would defeat the point.
|
|
138
145
|
- **Mongo / Redis / Elasticsearch** — engine-specific clone, OR per-worktree DB name / key prefix (`REDIS_URL=redis://localhost:6379/<index>` with a free index; `MONGODB_URI=mongodb://localhost:27017/<db>_wt_<name>`).
|
|
139
146
|
|
|
140
|
-
- **Share the DB (don't fork)** —
|
|
147
|
+
- **Share the DB (don't fork)** — means share a *connection to a single already-running server*, nothing more. Only safe when the DB is an **external server** (NOT compose-delivered — see the delivery check above) AND `schema_change = false` AND there's no risk of conflicting writes. Symlink the SQLite file or point the worktree's env at the same URL. Warn explicitly: "DB shared with main checkout — destructive ops here will be visible in main." **Never read this bullet as permission to point a second per-worktree compose server at a shared data volume** — that is not sharing a connection, it is running two servers on one data directory, and it corrupts the store regardless of `schema_change`.
|
|
141
148
|
|
|
142
149
|
- **Stub the DB** — point the worktree at a fresh empty DB (`createdb <main_db>_wt_<name>` + run all migrations + seed if a seed exists). Best when the feature needs schema parity but no production data.
|
|
143
150
|
|
|
@@ -147,10 +154,12 @@ Record the chosen strategy + the forked DB name in the worktree summary (written
|
|
|
147
154
|
|
|
148
155
|
A different beast — tests on the same engine but a different DB name (`<main_db>_test`, `test_<repo>`). Parallel worktrees running tests against the same `<main_db>_test` will overwrite each other's fixtures and produce flaky failures.
|
|
149
156
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
- **
|
|
153
|
-
- **
|
|
157
|
+
**The principle (stack-agnostic):** give the worktree its own test DB name (`<main_db>_test_wt_<name>`), inject it through whatever channel the test runner already reads (an env var or a per-worktree config override), and **never edit tracked test config in place** — a worktree-local override or an env var is safe; a change to a committed config file is one `git add -A` away from leaking into main. How each runner exposes that channel:
|
|
158
|
+
|
|
159
|
+
- **Env-var runners (`vitest` / `jest`, `go test`, `cargo test`, most CI-shaped suites)** — set `TEST_DATABASE_URL` (or the suite's equivalent) per worktree and confirm the test setup reads it. Simplest case; no file edits at all.
|
|
160
|
+
- **Config-file runners (`pytest-django` / `pytest`)** — set `--reuse-db` per worktree via a per-worktree `DJANGO_SETTINGS_MODULE`, OR override `DATABASES['default']['NAME']` to `<main_db>_test_wt_<name>` guarded on a `WORKTREE_NAME` env var. Put the override in a `conftest_worktree.py` in the worktree — don't touch the tracked `conftest.py`.
|
|
161
|
+
- **Convention-based runners (Rails)** — point the `test` env's `DATABASE_URL` at `<main_db>_test_wt_<name>`.
|
|
162
|
+
- **Docker-compose-based test DB (any stack)** — see the **Docker / compose isolation** step below; a per-worktree compose project name fixes it.
|
|
154
163
|
|
|
155
164
|
If `test_infra_change = true` → fork the test DB unconditionally. If `false` and the user says "share is fine" → still flag the race risk; offer a one-line fix to switch later.
|
|
156
165
|
|
|
@@ -166,10 +175,15 @@ If the project has a `docker-compose.yml` / `compose.yaml` / `docker-compose.ove
|
|
|
166
175
|
|
|
167
176
|
1. Set `COMPOSE_PROJECT_NAME=<repo>_<worktree-name>` in the worktree's `.env` (or in a `.envrc` if the project uses direnv). Then `docker compose up` inside the worktree spins a fresh container set.
|
|
168
177
|
|
|
169
|
-
|
|
178
|
+
**`COMPOSE_PROJECT_NAME` is necessary but NOT sufficient.** It namespaces containers and networks, and auto-named volumes — but it does **not** isolate two very common compose patterns: volumes declared `external: true` and/or pinned to a fixed top-level `name:`, and services with fixed host port bindings (`ports: - "5432:5432"`). A shared *database* volume is the dangerous one: two worktree stacks (or a worktree stack + main's stack) mount the same physical volume, so two server processes run against one data directory → corruption. The next sub-step closes both leaks mechanically.
|
|
179
|
+
|
|
180
|
+
2. **Neutralize compose isolation leaks** — see [4a](#4a--neutralize-compose-isolation-leaks) below. Run this whenever the project has compose, before ever booting the worktree's stack.
|
|
181
|
+
|
|
182
|
+
3. **Shared network** — if the worktree's app needs to reach services running in the *main* checkout's compose stack (a queue, a cache, a search index that's expensive to spin twice), put them on an external network:
|
|
170
183
|
|
|
171
184
|
```yaml
|
|
172
|
-
#
|
|
185
|
+
# Add to the SAME out-of-tree override the 4a sub-step generates — NOT a
|
|
186
|
+
# tracked docker-compose.override.yml at the worktree root (see 4a for why).
|
|
173
187
|
networks:
|
|
174
188
|
shared:
|
|
175
189
|
external: true
|
|
@@ -182,9 +196,44 @@ If the project has a `docker-compose.yml` / `compose.yaml` / `docker-compose.ove
|
|
|
182
196
|
|
|
183
197
|
Or use `network_mode: host` if the project uses that pattern. Decide once per project and record in `.vinta-ai-workflows.yaml` → `run_options.prepare-worktree.compose_network: per-worktree | shared-external | host`.
|
|
184
198
|
|
|
185
|
-
|
|
199
|
+
4. **Linters / formatters / test runners that run inside docker** (a Dockerfile-based `pnpm test`, a `lint` target that mounts source into a sidecar container) — these MUST run with the worktree's `COMPOSE_PROJECT_NAME`. Otherwise concurrent runs hit the same container and one wins. The standard fix: ensure the project's lint/test scripts read `COMPOSE_PROJECT_NAME` from env instead of hard-coding it.
|
|
200
|
+
|
|
201
|
+
5. **Per-worktree volumes** — for a volume the project leaves un-named, compose auto-namespaces it by project name and it's already isolated. But projects routinely pin data volumes with `external: true` or a fixed `name:` (for good reasons of their own — you must NOT edit their committed compose to "fix" that). Those are the leaks the [4a](#4a--neutralize-compose-isolation-leaks) sub-step re-pins in a generated override. Never touch the tracked compose file to change a volume's isolation; the override is the only lever.
|
|
202
|
+
|
|
203
|
+
### 4a — Neutralize compose isolation leaks
|
|
204
|
+
|
|
205
|
+
Runs whenever the project has any compose file, before the worktree's stack is ever booted. Fully project-agnostic — every decision is driven by `docker compose config --format json`, no project / service / volume name is hardcoded.
|
|
206
|
+
|
|
207
|
+
**1. Generate the override.** Run the bundled [scripts/gen-compose-worktree-override.sh](scripts/gen-compose-worktree-override.sh):
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
override=$(scripts/gen-compose-worktree-override.sh \
|
|
211
|
+
--main <main-checkout-root> \
|
|
212
|
+
--worktree <worktree-path> \
|
|
213
|
+
--name <worktree-name> \
|
|
214
|
+
--out <summary_dir>/<name>.docker-compose.override.yml \
|
|
215
|
+
--share-volume <volkey>...) # 0+ volumes safe to keep shared (see below)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The script renders the resolved config from the main checkout and, for the worktree:
|
|
219
|
+
- re-pins every volume that is `external: true` OR carries a fixed top-level `name:` to a **non-external, worktree-namespaced** volume (`<project>_wt_<name>_<volkey>`), so the worktree's server writes to its OWN physical volume;
|
|
220
|
+
- strips host port publishing from every service that publishes a fixed port (via compose's `!override []` — a plain `ports: []` **merges/appends** in Compose v2+ and would NOT strip it; verified on Docker Compose v5.1.1);
|
|
221
|
+
- leaves alone any volume already namespaced by project name (auto-named), and any volume the caller passes via `--share-volume` (a read-only dep/venv cache with no dep churn is safe to keep shared — the config's `shared_volumes` list feeds this arg).
|
|
222
|
+
|
|
223
|
+
The script exits non-zero with a clear message if `docker compose` is unavailable or the config doesn't parse; surface that and stop (don't boot a stack you couldn't prove is isolated).
|
|
224
|
+
|
|
225
|
+
**2. Write it OUT OF TREE and wire it in via the copied `.env`.** The override goes to `<summary_dir>/<name>.docker-compose.override.yml` (under the already-ignored `.vinta-ai-workflows/` umbrella) — **NOT** a `docker-compose.override.yml` at the worktree root. Rationale: a root `docker-compose.override.yml` is auto-loaded by compose, but it is frequently a **tracked** file (it was in the project that hit the 2026-07-17 incident); writing the generated override there pollutes the worktree diff and risks `git add -A` leaking it into main. Instead, wire it in by appending to the worktree's **copied** `.env`:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# The generator prints exactly this line (base:override) on stderr — use it verbatim.
|
|
229
|
+
echo "COMPOSE_FILE=<base-compose-file>:<ABSOLUTE-override-path>" >> <worktree>/.env
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`COMPOSE_FILE` makes compose load base + override for every `docker compose` call in the worktree without touching any tracked file. The **override path must be absolute** — it lives under `<summary_dir>` (outside the worktree), so a bare basename wouldn't resolve from the worktree's compose project dir; the generator prints the absolute path for you (and on stdout, for `$(...)` capture). The base file stays relative — the worktree carries its own tracked copy. **This requires `.env` to be COPIED, not symlinked** — appending to a symlinked `.env` leaks the line into main. If the **Env files** step decided `env_change = false` (symlink), flip `.env` to a copy here (compose isolation is itself an env change) and note it in the summary. Use the exact base compose filename the generator reports (`compose.yaml` vs `docker-compose.yml`).
|
|
233
|
+
|
|
234
|
+
**3. Record the decision.** Write the per-volume fork decisions and the override path into the summary YAML's `state.compose` (see the **Write the summary file** step). The override's own `volumes:` block is the teardown manifest — each pinned `name:` is a `docker volume rm` target.
|
|
186
235
|
|
|
187
|
-
|
|
236
|
+
**Relation to `test_db_strategy`.** The `test_db_strategy: fork-on-schema-change` config governs the **test-DB axis only** (Step 3b — which test DB *name* the runner targets). It says nothing about compose data volumes and must **never** be read as permission to share the compose stack's volumes. Volume forking here is unconditional for compose-delivered DBs, independent of `schema_change` and independent of `test_db_strategy`.
|
|
188
237
|
|
|
189
238
|
## Step 5 — Other shared infra to fork
|
|
190
239
|
|
|
@@ -303,6 +352,14 @@ state:
|
|
|
303
352
|
compose:
|
|
304
353
|
project_name: <repo>_<worktree-name>
|
|
305
354
|
network_strategy: per-worktree | shared-external | host
|
|
355
|
+
override_path: <summary_dir>/<name>.docker-compose.override.yml # null when no compose
|
|
356
|
+
base_compose_file: docker-compose.yml # the base file COMPOSE_FILE prepends
|
|
357
|
+
forked_volumes: # [] when nothing leaked; teardown removes each name
|
|
358
|
+
- volume_key: pgdata # the compose key
|
|
359
|
+
reason: external | fixed-name # why it leaked past COMPOSE_PROJECT_NAME
|
|
360
|
+
forked_name: <repo>_wt_<name>_pgdata # the worktree-namespaced physical volume
|
|
361
|
+
shared_volumes: [virtualenv] # leaky volumes deliberately kept shared (--share-volume)
|
|
362
|
+
ports_stripped_from: [db, cache] # services whose fixed host ports the override removed
|
|
306
363
|
other:
|
|
307
364
|
redis_db_index: <int>
|
|
308
365
|
s3_prefix: wt-<name>/
|
|
@@ -346,7 +403,12 @@ guard is active; the orchestrator's post-run stray-write check is the backstop.
|
|
|
346
403
|
When the plan is merged / abandoned:
|
|
347
404
|
git worktree remove <path>
|
|
348
405
|
<drop-db command> # if fork strategy was `fork`
|
|
349
|
-
|
|
406
|
+
# Compose (if forked). Do NOT use `down -v` — it can also nuke SHARED volumes
|
|
407
|
+
# (a fixed-name dep/venv cache). Bring the stack down, then remove only the
|
|
408
|
+
# worktree-namespaced volumes the override created:
|
|
409
|
+
docker compose -p <project_name> down
|
|
410
|
+
docker volume rm <each state.compose.forked_volumes[].forked_name>
|
|
411
|
+
rm <override_path> # the generated out-of-tree compose override
|
|
350
412
|
|
|
351
413
|
The summary file at `.vinta-ai-workflows/worktrees/<name>.yaml` records every
|
|
352
414
|
decision for mechanical teardown.
|
|
@@ -371,7 +433,10 @@ A sibling skill / explicit command — not auto-run by this skill. Steps:
|
|
|
371
433
|
1. Confirm no uncommitted changes in the worktree (`git -C <path> status`).
|
|
372
434
|
2. `git worktree remove <path>` (or `--force` after explicit confirmation if the user is fine losing work).
|
|
373
435
|
3. Drop the forked DB (`dropdb <forked_name>`, `DROP DATABASE`, `rm <sqlite-file>`, …) — read the strategy from the summary YAML.
|
|
374
|
-
4.
|
|
436
|
+
4. Compose teardown (read `state.compose` from the summary YAML):
|
|
437
|
+
- `docker compose -p <project_name> down` — **not `down -v`**; a bare `down -v` also removes shared volumes (a fixed-name dep/venv cache in `state.compose.shared_volumes`), which belong to main.
|
|
438
|
+
- `docker volume rm <forked_name>` for each entry in `state.compose.forked_volumes` — these are the worktree-only volumes the generated override created; removing them is safe and leaves main's / shared volumes untouched.
|
|
439
|
+
- `rm <state.compose.override_path>` — delete the generated out-of-tree override.
|
|
375
440
|
5. Remove `.vinta-ai-workflows/worktrees/<name>.yaml`.
|
|
376
441
|
|
|
377
442
|
Every step gated on user confirmation when the worktree has un-pushed branches.
|
|
@@ -380,6 +445,7 @@ Every step gated on user confirmation when the worktree has un-pushed branches.
|
|
|
380
445
|
|
|
381
446
|
- **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.
|
|
382
447
|
- **Never share a writable DB across worktrees by default.** The race conditions are subtle and the failure mode is silent data corruption.
|
|
448
|
+
- **`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.
|
|
383
449
|
- **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.
|
|
384
450
|
- **Worktree root governed by runtime conventions.** claude-code uses `.claude/worktrees/`; other harnesses use sibling dirs. Don't fight the harness — match it.
|
|
385
451
|
- **Don't mutate the main checkout from this skill.** Every write goes to the worktree or to `.vinta-ai-workflows/`. Forking a Postgres DB is the one exception (the new DB lives in the same server) — document it loudly in the summary.
|
|
@@ -395,6 +461,9 @@ Every step gated on user confirmation when the worktree has un-pushed branches.
|
|
|
395
461
|
- **Symlinking `node_modules` for a `pnpm add` phase.** The new package writes back through the symlink into the main checkout's store. Detect dep churn in the **Plan inspection** step and copy/reinstall instead.
|
|
396
462
|
- **Forking the dev DB but forgetting the test DB.** Tests still hit the shared test DB and stomp on parallel worktrees' fixtures. Both axes need their own decision in the **Database fork** step.
|
|
397
463
|
- **Forgetting `COMPOSE_PROJECT_NAME`.** Containers from worktree-A overwrite worktree-B's containers; volumes get nuked. Set it in `.env` so every `docker compose` call inherits.
|
|
464
|
+
- **Trusting `COMPOSE_PROJECT_NAME` to isolate `external:`/fixed-`name:` volumes and fixed host ports — it does NOT.** These defeat the project-name namespace: every worktree's compose stack mounts the SAME external/named volume and binds the SAME host port. For a DB volume that means two server processes on one data directory — the 2026-07-17 corruption incident (a worktree's `docker compose down` deleted the shared `postmaster.pid` and the main checkout's Postgres self-terminated). The fix is the [4a](#4a--neutralize-compose-isolation-leaks) generated out-of-tree override, never an edit to the tracked compose file. Verify the invariant (Verification step 6), don't assume it.
|
|
465
|
+
- **Confusing "share the DB" (a connection to one running server) with "share the data volume" (a second server on shared storage).** The first can be safe when `schema_change = false`; the second is always corruption. `schema_change = false` and `test_db_strategy` never authorize a shared compose data volume — see [3a](#3a--dev--app-database).
|
|
466
|
+
- **Writing the compose override to the worktree-root `docker-compose.override.yml`.** It's auto-loaded (convenient) but often a *tracked* file — the generated override then shows up in the diff and one `git add -A` leaks it into main. Write it under `<summary_dir>/` and wire it in via `COMPOSE_FILE` in the COPIED `.env` ([4a](#4a--neutralize-compose-isolation-leaks)).
|
|
398
467
|
- **Sharing a Redis DB without per-worktree prefix.** Tests writing `user:123` collide across worktrees. Pick an index OR a key prefix.
|
|
399
468
|
- **Copying `node_modules` for yarn PnP / absolute-path setups.** The copy carries baked-in paths from the main checkout. Reinstall instead — pnpm's relative-symlink store is the safe-to-copy exception.
|
|
400
469
|
- **Forgetting to `--allow` the `.vinta-ai-workflows` dir under sandbox.** It's shared/symlinked but the run *writes* it (summary YAMLs, prs-context). If it's not an `--allow` exception, those writes hit the read-only main subtree and fail mid-run. Pass it alongside the worktree path.
|
|
@@ -413,4 +482,7 @@ After the **Write the summary file** step writes the summary:
|
|
|
413
482
|
3. `git -C <worktree-path> status` is clean (no accidental file additions from the prep step).
|
|
414
483
|
4. The summary YAML parses (`python3 -c "import yaml; yaml.safe_load(open('.vinta-ai-workflows/worktrees/<name>.yaml'))"`).
|
|
415
484
|
5. `WORKTREE.md` exists at the worktree root with accurate fork / share annotations.
|
|
416
|
-
6.
|
|
485
|
+
6. **Compose volume-isolation invariant (when the DB is compose-delivered).** This is the checked form of the footgun that caused the 2026-07-17 incident — do not skip it when the project boots its DB via compose. From the worktree:
|
|
486
|
+
- Resolve the worktree's DB data volume: `docker compose config --format json | <read the db service's data volume `source`, then that volume's `.name`>`. Assert it **differs** from the main checkout's resolved DB volume name (run the same read in `<main-checkout-root>`). If they're equal, the override didn't take — **fail provisioning loudly**; do not proceed.
|
|
487
|
+
- Boot only the DB service in the worktree (`docker compose up -d <db-service>`), then confirm the **main checkout's** DB container is still healthy/running afterward (`docker inspect`/`pg_isready` against main's DB). Bring the worktree DB back down. A main-DB disturbance here means isolation failed — fail provisioning.
|
|
488
|
+
7. Optional smoke test: run a single new-test command in the worktree (e.g. `pytest -x tests/health.py`) — confirms env vars resolved, DB reachable, deps importable.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# gen-compose-worktree-override.sh — generate an out-of-tree docker-compose
|
|
3
|
+
# override that neutralizes the isolation leaks COMPOSE_PROJECT_NAME does NOT
|
|
4
|
+
# cover, so a per-worktree compose stack cannot collide with the main checkout's.
|
|
5
|
+
#
|
|
6
|
+
# Why this exists: COMPOSE_PROJECT_NAME namespaces containers and networks, but
|
|
7
|
+
# NOT two very common compose patterns —
|
|
8
|
+
#
|
|
9
|
+
# 1. Volumes declared `external: true` and/or pinned to a fixed top-level
|
|
10
|
+
# `name:`. These are addressed by name, so EVERY worktree's compose stack
|
|
11
|
+
# mounts the SAME physical volume. For a database volume that means two
|
|
12
|
+
# postmasters on one PGDATA → data corruption (the 2026-07-17 incident).
|
|
13
|
+
# 2. Fixed host port bindings (`ports: - "5432:5432"`). These publish to a
|
|
14
|
+
# fixed host port that collides across compose projects regardless of name.
|
|
15
|
+
#
|
|
16
|
+
# The fix is a GENERATED, out-of-tree override (never a mutation of the project's
|
|
17
|
+
# committed compose files) that, for the worktree:
|
|
18
|
+
# - re-pins every leaky volume to a NON-external, worktree-namespaced volume
|
|
19
|
+
# (`<project>_wt_<name>_<volkey>`), EXCEPT volumes the caller marks shareable
|
|
20
|
+
# (a read-only dep/venv cache with no dep churn is safe to keep shared);
|
|
21
|
+
# - strips host port publishing from every service that has it, using compose's
|
|
22
|
+
# `!override []` tag (a plain `ports: []` MERGES/appends in Compose v2+ and
|
|
23
|
+
# would NOT strip the port — verified on Docker Compose v5.1.1).
|
|
24
|
+
#
|
|
25
|
+
# Detection is 100% generic — no project / service / volume name is hardcoded.
|
|
26
|
+
# Everything comes from `docker compose config --format json` run in the main
|
|
27
|
+
# checkout. A volume leaks when `.external == true` OR its resolved `.name`
|
|
28
|
+
# differs from the auto-namespaced form `<project>_<volkey>` (i.e. it carries a
|
|
29
|
+
# fixed top-level `name:`). Setting `external: false` in the override is
|
|
30
|
+
# mandatory — without it the base `external: true` merges through and the volume
|
|
31
|
+
# stays externally-pinned (verified).
|
|
32
|
+
#
|
|
33
|
+
# Usage:
|
|
34
|
+
# gen-compose-worktree-override.sh \
|
|
35
|
+
# --main <main-checkout-root> \
|
|
36
|
+
# --worktree <worktree-path> \
|
|
37
|
+
# --name <worktree-name> \
|
|
38
|
+
# --out <override-output-path> \
|
|
39
|
+
# [--share-volume <volkey>]... # volumes safe to keep shared (repeatable)
|
|
40
|
+
#
|
|
41
|
+
# Output:
|
|
42
|
+
# stdout — the override path (only, on success) so it is safe to capture with
|
|
43
|
+
# $(...). stderr carries a human-readable decision report + the
|
|
44
|
+
# suggested `COMPOSE_FILE=<base>:<override>` wiring line.
|
|
45
|
+
# The generated override's `volumes:` block lists the forked volumes by their
|
|
46
|
+
# pinned name — that IS the teardown manifest (`docker volume rm <name>`).
|
|
47
|
+
#
|
|
48
|
+
# Exit: 0 on success; 2 on argument / setup errors (no `docker compose`, config
|
|
49
|
+
# does not parse, no base compose file found).
|
|
50
|
+
|
|
51
|
+
set -u
|
|
52
|
+
|
|
53
|
+
die() { echo "gen-compose-worktree-override: $*" >&2; exit 2; }
|
|
54
|
+
|
|
55
|
+
MAIN=""; WORKTREE=""; NAME=""; OUT=""
|
|
56
|
+
SHARE=()
|
|
57
|
+
while [ $# -gt 0 ]; do
|
|
58
|
+
case "$1" in
|
|
59
|
+
--main) shift; [ $# -gt 0 ] || die "--main needs a path"; MAIN="$1"; shift ;;
|
|
60
|
+
--worktree) shift; [ $# -gt 0 ] || die "--worktree needs a path"; WORKTREE="$1"; shift ;;
|
|
61
|
+
--name) shift; [ $# -gt 0 ] || die "--name needs a value"; NAME="$1"; shift ;;
|
|
62
|
+
--out) shift; [ $# -gt 0 ] || die "--out needs a path"; OUT="$1"; shift ;;
|
|
63
|
+
--share-volume) shift; [ $# -gt 0 ] || die "--share-volume needs a volume key"; SHARE+=("$1"); shift ;;
|
|
64
|
+
*) die "unknown arg: $1" ;;
|
|
65
|
+
esac
|
|
66
|
+
done
|
|
67
|
+
|
|
68
|
+
[ -n "$MAIN" ] || die "--main <main-checkout-root> required"
|
|
69
|
+
[ -n "$WORKTREE" ] || die "--worktree <path> required"
|
|
70
|
+
[ -n "$NAME" ] || die "--name <worktree-name> required"
|
|
71
|
+
[ -n "$OUT" ] || die "--out <override-path> required"
|
|
72
|
+
|
|
73
|
+
command -v docker >/dev/null 2>&1 || die "docker not found on PATH — cannot generate compose override"
|
|
74
|
+
docker compose version >/dev/null 2>&1 || die "'docker compose' unavailable — is the Compose v2 plugin installed?"
|
|
75
|
+
|
|
76
|
+
# Canonicalize the main checkout (must exist).
|
|
77
|
+
[ -d "$MAIN" ] || die "main checkout path is not a directory: $MAIN"
|
|
78
|
+
MAIN=$(cd "$MAIN" && pwd -P)
|
|
79
|
+
|
|
80
|
+
# Locate the base compose file compose will auto-load (standard search order).
|
|
81
|
+
BASE=""
|
|
82
|
+
for f in compose.yaml compose.yml docker-compose.yaml docker-compose.yml; do
|
|
83
|
+
if [ -f "$MAIN/$f" ]; then BASE="$f"; break; fi
|
|
84
|
+
done
|
|
85
|
+
[ -n "$BASE" ] || die "no compose file (compose.yaml / docker-compose.yml) found in $MAIN"
|
|
86
|
+
|
|
87
|
+
# Render the resolved config from the MAIN checkout. Do not force
|
|
88
|
+
# COMPOSE_PROJECT_NAME: let it default exactly as the project normally resolves
|
|
89
|
+
# it, so the auto-namespace comparison below reflects reality.
|
|
90
|
+
CONFIG_JSON=$(cd "$MAIN" && docker compose config --format json 2>/dev/null) \
|
|
91
|
+
|| die "'docker compose config' failed in $MAIN — the compose config does not parse"
|
|
92
|
+
|
|
93
|
+
mkdir -p "$(dirname "$OUT")" || die "cannot create output dir for $OUT"
|
|
94
|
+
|
|
95
|
+
CONFIG_JSON="$CONFIG_JSON" OUT="$OUT" NAME="$NAME" BASE="$BASE" \
|
|
96
|
+
python3 - "${SHARE[@]:-}" <<'PY' || die "failed to generate override"
|
|
97
|
+
import json, os, sys
|
|
98
|
+
|
|
99
|
+
cfg = json.loads(os.environ["CONFIG_JSON"])
|
|
100
|
+
out = os.environ["OUT"]
|
|
101
|
+
wt_name = os.environ["NAME"]
|
|
102
|
+
base = os.environ["BASE"]
|
|
103
|
+
share = {s for s in sys.argv[1:] if s}
|
|
104
|
+
|
|
105
|
+
# The override lives OUT of the worktree (under <summary_dir>), so the COMPOSE_FILE
|
|
106
|
+
# entry must reference it by ABSOLUTE path — a basename would only resolve if the
|
|
107
|
+
# file sat in the compose project dir. The base file is worktree-local (the
|
|
108
|
+
# worktree carries its own tracked copy), so it stays relative.
|
|
109
|
+
out_abs = os.path.abspath(out)
|
|
110
|
+
|
|
111
|
+
project = cfg.get("name") or "compose"
|
|
112
|
+
volumes = cfg.get("volumes") or {}
|
|
113
|
+
services = cfg.get("services") or {}
|
|
114
|
+
|
|
115
|
+
# ---- Detect leaky volumes -------------------------------------------------
|
|
116
|
+
# A volume leaks past COMPOSE_PROJECT_NAME when it is external, or carries a
|
|
117
|
+
# fixed top-level name (resolved name != the auto-namespaced <project>_<key>).
|
|
118
|
+
forked = [] # (volkey, reason, pinned_name)
|
|
119
|
+
shared_leaky = [] # (volkey, reason) — leaky but caller marked shareable
|
|
120
|
+
for key, vol in volumes.items():
|
|
121
|
+
vol = vol or {}
|
|
122
|
+
external = bool(vol.get("external"))
|
|
123
|
+
resolved = vol.get("name", "")
|
|
124
|
+
auto = f"{project}_{key}"
|
|
125
|
+
if external:
|
|
126
|
+
reason = "external:true"
|
|
127
|
+
elif resolved and resolved != auto:
|
|
128
|
+
reason = f"fixed name '{resolved}'"
|
|
129
|
+
else:
|
|
130
|
+
continue # already namespaced by project name — safe
|
|
131
|
+
if key in share:
|
|
132
|
+
shared_leaky.append((key, reason))
|
|
133
|
+
continue
|
|
134
|
+
pinned = f"{project}_wt_{wt_name}_{key}"
|
|
135
|
+
forked.append((key, reason, pinned))
|
|
136
|
+
|
|
137
|
+
# ---- Detect services publishing fixed host ports --------------------------
|
|
138
|
+
stripped = [] # service names whose host port publishing we drop
|
|
139
|
+
for svc, spec in services.items():
|
|
140
|
+
spec = spec or {}
|
|
141
|
+
ports = spec.get("ports") or []
|
|
142
|
+
if any((p or {}).get("published") for p in ports):
|
|
143
|
+
stripped.append(svc)
|
|
144
|
+
|
|
145
|
+
# ---- Emit the override YAML (hand-rolled; no yaml dependency) --------------
|
|
146
|
+
def y(s):
|
|
147
|
+
# Quote scalars that YAML could misread; safe for our name/key charset.
|
|
148
|
+
return json.dumps(s)
|
|
149
|
+
|
|
150
|
+
lines = []
|
|
151
|
+
lines.append("# GENERATED by gen-compose-worktree-override.sh — do not edit by hand.")
|
|
152
|
+
lines.append("# Out-of-tree compose override that neutralizes the isolation leaks")
|
|
153
|
+
lines.append("# COMPOSE_PROJECT_NAME does not cover (external / fixed-name volumes,")
|
|
154
|
+
lines.append("# fixed host port bindings). Wire in without touching tracked files via:")
|
|
155
|
+
lines.append(f"# COMPOSE_FILE={base}:{out_abs} (in the worktree's .env)")
|
|
156
|
+
lines.append(f"# Source project: {project} Worktree: {wt_name}")
|
|
157
|
+
if shared_leaky:
|
|
158
|
+
for key, reason in shared_leaky:
|
|
159
|
+
lines.append(f"# KEPT SHARED (caller --share-volume): {key} ({reason})")
|
|
160
|
+
lines.append("")
|
|
161
|
+
|
|
162
|
+
if stripped:
|
|
163
|
+
lines.append("services:")
|
|
164
|
+
for svc in sorted(stripped):
|
|
165
|
+
lines.append(f" {y(svc)}:")
|
|
166
|
+
# `!override []` REPLACES the base list (plain [] would append/merge).
|
|
167
|
+
lines.append(" ports: !override []")
|
|
168
|
+
lines.append("")
|
|
169
|
+
|
|
170
|
+
if forked:
|
|
171
|
+
lines.append("volumes:")
|
|
172
|
+
for key, _reason, pinned in forked:
|
|
173
|
+
lines.append(f" {y(key)}:")
|
|
174
|
+
lines.append(f" name: {y(pinned)}")
|
|
175
|
+
lines.append(" external: false")
|
|
176
|
+
lines.append("")
|
|
177
|
+
|
|
178
|
+
if not stripped and not forked:
|
|
179
|
+
lines.append("# No isolation leaks detected: every volume is already namespaced by")
|
|
180
|
+
lines.append("# COMPOSE_PROJECT_NAME and no service publishes a fixed host port.")
|
|
181
|
+
lines.append("# This override is a valid no-op; wiring it in is harmless.")
|
|
182
|
+
lines.append("")
|
|
183
|
+
|
|
184
|
+
with open(out, "w") as f:
|
|
185
|
+
f.write("\n".join(lines).rstrip("\n") + "\n")
|
|
186
|
+
|
|
187
|
+
# ---- Report to stderr (human + agent readable) ----------------------------
|
|
188
|
+
def err(msg):
|
|
189
|
+
print(msg, file=sys.stderr)
|
|
190
|
+
|
|
191
|
+
err(f"gen-compose-worktree-override: wrote {out}")
|
|
192
|
+
err(f" base compose file: {base} project: {project}")
|
|
193
|
+
if forked:
|
|
194
|
+
err(" forked volumes (were shared → now worktree-namespaced, non-external):")
|
|
195
|
+
for key, reason, pinned in forked:
|
|
196
|
+
err(f" - {key} ({reason}) -> {pinned}")
|
|
197
|
+
else:
|
|
198
|
+
err(" forked volumes: none")
|
|
199
|
+
if shared_leaky:
|
|
200
|
+
err(" KEPT SHARED (caller --share-volume) — verify these are read-only caches:")
|
|
201
|
+
for key, reason in shared_leaky:
|
|
202
|
+
err(f" - {key} ({reason})")
|
|
203
|
+
if stripped:
|
|
204
|
+
err(f" stripped host-port publishing from: {', '.join(sorted(stripped))}")
|
|
205
|
+
else:
|
|
206
|
+
err(" stripped host-port publishing from: none")
|
|
207
|
+
err(f" wire in: COMPOSE_FILE={base}:{out_abs} (append to the worktree's copied .env)")
|
|
208
|
+
|
|
209
|
+
# stdout = override path ONLY (safe for $(...) capture).
|
|
210
|
+
print(out_abs)
|
|
211
|
+
PY
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!-- Partial: agent-models — resolves the `.vinta-ai-workflows.yaml` `agent_models` section (tier per role/task) to a concrete spawn model, and the mechanical-step delegation pattern. Blocks: TIER_RESOLVE (review-phase reviewer/fixer + implement-plan mechanical steps), MECHANICAL_DELEGATION (implement-plan worktree/integrate steps). The implementer model is NOT here — that stays plan-owned via model-pick. -->
|
|
2
|
+
|
|
3
|
+
<!-- block-begin: TIER_RESOLVE -->
|
|
4
|
+
## Resolve an `agent_models` tier to a spawn model
|
|
5
|
+
|
|
6
|
+
`.vinta-ai-workflows.yaml` may carry an `agent_models` section mapping a role/task (`reviewer`, `fixer`, `worktree_prep`, `integrate`) to a **tier** (1–4) into the same table the per-phase implementer suggestion uses — [`ai-tools/skills/plan-feature/resources/ai-models.yaml`](../plan-feature/resources/ai-models.yaml). `agent_models` is the **project default** for these roles; for `reviewer` / `fixer`, a plan phase's optional `**Review models**:` line may override the tier for that one phase (the caller resolves that precedence and hands this block the effective tier). The mechanical steps (`worktree_prep`, `integrate`) are never plan-named. To turn a tier into the model a spawn actually uses:
|
|
7
|
+
|
|
8
|
+
1. Determine the **effective tier** for the role: for `reviewer` / `fixer`, a per-phase override the conductor passed wins over `agent_models.<role>`; for the mechanical steps, it's simply `agent_models.<role>`.
|
|
9
|
+
2. **No effective tier (override absent AND key unset, or the whole `agent_models` section absent) → do not force a model.** Spawn with the runtime's default model (today's behavior). Skip the rest.
|
|
10
|
+
3. Open [`ai-tools/skills/plan-feature/resources/ai-models.yaml`](../plan-feature/resources/ai-models.yaml), take that tier's `models`, **filter to the vendors the runtime actually exposes**, pick the cheapest/fastest survivor, and translate it to the runner's spawn form — the same resolution [implement-phase](../implement-phase/SKILL.md) runs for the implementer, only keyed by a config tier instead of a plan line.
|
|
11
|
+
4. `ai-models.yaml` missing, or the tier has no runtime-available vendor → fall back to the runtime default and surface the fallback once. Never hard-fail a phase over a model-selection miss.
|
|
12
|
+
|
|
13
|
+
Record the **model actually used** in tracking: for `reviewer` / `fixer`, alongside the review note; for the mechanical steps, in the phase's tracking row next to the branch/PR fields.
|
|
14
|
+
<!-- block-end: TIER_RESOLVE -->
|
|
15
|
+
|
|
16
|
+
<!-- block-begin: MECHANICAL_DELEGATION -->
|
|
17
|
+
## Delegate a mechanical step to a configured model
|
|
18
|
+
|
|
19
|
+
Two steps the conductor would otherwise run **inline in its own (usually pricier) session** — provisioning the worktree ([prepare-worktree](../prepare-worktree/SKILL.md)) and integrating a phase ([integrate-phase](../integrate-phase/SKILL.md): push the branch + open/update the PR through the bundled `open-pr.sh`) — are mechanical, precedent-driven work that a cheap model handles fine. The `agent_models.worktree_prep` / `agent_models.integrate` tiers let a project push that work down.
|
|
20
|
+
|
|
21
|
+
- **Tier set** (`worktree_prep` / `integrate`) → **spawn exactly one subagent** at the [resolved model](#resolve-an-agent_models-tier-to-a-spawn-model), hand it the step's SKILL.md plus the same inputs the conductor would use, and consume its returned report exactly as if the conductor had done the work inline. This subagent is a **labor delegate, not a decision-maker**: the conductor still owns git topology (which branch stacks on which base) and still holds every value the step returns (`WORKROOT` / `BASE_BRANCH` / worktree summary for `worktree_prep`; branch + PR-context path + `status` for `integrate`). The delegate executes and reports those back.
|
|
22
|
+
- **Tier unset** → run the step inline in the conductor's own session — today's behavior, no subagent.
|
|
23
|
+
|
|
24
|
+
Rules that hold **regardless of who runs the step**:
|
|
25
|
+
|
|
26
|
+
- The **PR-context file + `open-pr.sh` is still the only PR-creation path.** An `integrate` delegate uses the bundled script; it never calls raw `gh pr create` / `glab mr create`.
|
|
27
|
+
- The delegate is `read-write` (worktree provisioning writes dirs/DBs; integrate pushes + writes the PR-context file) but **makes no plan or code decisions** — a malformed or failed delegate report is surfaced to the user, never worked around.
|
|
28
|
+
- This delegation is **separate from the phase-work sub-agents** (implementer / reviewer / fixer). Those still never branch, push, or open PRs — that prohibition is about code-authoring agents, not the dedicated mechanical delegate the conductor spawns to run the integrate step itself.
|
|
29
|
+
<!-- block-end: MECHANICAL_DELEGATION -->
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<!-- block-begin: MODEL_PICK -->
|
|
3
3
|
## Pick the model from the plan's per-phase suggestion
|
|
4
4
|
|
|
5
|
-
**The plan owns model
|
|
5
|
+
**The plan owns the *implementer* model — this skill does not re-derive tiers and doesn't assume a vendor.** Each phase carries a `**Suggested AI model**:` line listing one model per vendor. (The reviewer / fixer models default to `.vinta-ai-workflows.yaml`'s `agent_models` section, though a phase's optional `**Review models**:` line can override them for that phase; the mechanical-step models are `agent_models`-only. All of that is handled by `review-phase` / the conductor, not this implementer step.)
|
|
6
6
|
|
|
7
7
|
Pick:
|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ Open the phase body alongside the diff and walk:
|
|
|
30
30
|
|
|
31
31
|
## Layer 3 — Independent reviewer subagent
|
|
32
32
|
|
|
33
|
-
After Layers 1–2 pass, spawn a **separate** subagent (different session, no implementation context) using the project's `reviewer` agent type ([ai-tools/agents/reviewer.md](ai-tools/agents/reviewer.md)). Read-only by design.
|
|
33
|
+
After Layers 1–2 pass, spawn a **separate** subagent (different session, no implementation context) using the project's `reviewer` agent type ([ai-tools/agents/reviewer.md](ai-tools/agents/reviewer.md)) at the model resolved from `agent_models.reviewer` (see the [Resolve the reviewer + fixer model](#resolve-the-reviewer--fixer-model) step; unset → runtime default). Read-only by design.
|
|
34
34
|
|
|
35
35
|
Reviewer prompt template — see the reviewer agent's body for the standard form. Triage findings:
|
|
36
36
|
- **BLOCKER**: must fix before the phase is pushed (the conductor's integrate step).
|
|
@@ -43,7 +43,7 @@ The reviewer finds nothing on a >300-LoC multi-file phase → suspicious. Read o
|
|
|
43
43
|
|
|
44
44
|
## Fix loop
|
|
45
45
|
|
|
46
|
-
1. Spawn a **new** subagent — the project's `fixer` agent type ([ai-tools/agents/fixer.md](ai-tools/agents/fixer.md)). The fix prompt quotes the finding verbatim. For comment-hygiene findings (Layer 2 item 8), the fix prompt tells the fixer to run the `deslop-comments` skill ([ai-tools/skills/deslop-comments/SKILL.md](ai-tools/skills/deslop-comments/SKILL.md)) scoped to the phase's touched files — comment-only edits, no behavior change.
|
|
46
|
+
1. Spawn a **new** subagent — the project's `fixer` agent type ([ai-tools/agents/fixer.md](ai-tools/agents/fixer.md)) at the model resolved from `agent_models.fixer` (see the [Resolve the reviewer + fixer model](#resolve-the-reviewer--fixer-model) step; unset → runtime default). The fix prompt quotes the finding verbatim. For comment-hygiene findings (Layer 2 item 8), the fix prompt tells the fixer to run the `deslop-comments` skill ([ai-tools/skills/deslop-comments/SKILL.md](ai-tools/skills/deslop-comments/SKILL.md)) scoped to the phase's touched files — comment-only edits, no behavior change.
|
|
47
47
|
2. The `fixer`'s system prompt mandates re-running the inner loop + outer gate (in `<WORKROOT>`).
|
|
48
48
|
3. After the fixer returns, redo Layer 1 in full + the affected portion of Layer 2.
|
|
49
49
|
4. Loop until Layers 1, 2, 3 are all clean.
|
|
@@ -13,7 +13,7 @@ Resolve three values **once**, before any phase runs, and record them in trackin
|
|
|
13
13
|
|
|
14
14
|
**When `use_worktree = false`:** set `WORKROOT` = main checkout, `BASE_BRANCH = {{DEFAULT_BRANCH}}`, `SANDBOX_TIER = none`. Make `BASE_BRANCH` current + up to date: `git -C <WORKROOT> checkout {{DEFAULT_BRANCH}} && git -C <WORKROOT> pull --ff-only`. Jump to Step 1.
|
|
15
15
|
|
|
16
|
-
**When `use_worktree = true`:**
|
|
16
|
+
**When `use_worktree = true`:** run [prepare-worktree](../prepare-worktree/SKILL.md) **once**. This is a mechanical step: when `agent_models.worktree_prep` is set, **delegate it to a subagent** per the [Delegate a mechanical step to a configured model](#delegate-a-mechanical-step-to-a-configured-model) pattern (hand the subagent prepare-worktree's SKILL.md + the inputs below; consume its returned `worktree_path` / `worktree_branch` / `worktree_summary` / `sandbox_tier` report). When the tier is unset, run it inline — the steps below read the same either way:
|
|
17
17
|
|
|
18
18
|
1. **Inputs.** Plan path (so prepare-worktree can read it for deps / migrations / env / compose churn — see prepare-worktree's **Plan inspection** step), suggested worktree name = `plan-{plan-id-kebab}`, plan-driven mode.
|
|
19
19
|
2. **Pre-run sanity.** Confirm no existing worktree at the target path (`git worktree list | grep <name>` — refuse if collision). Confirm `git -C <main_checkout> status` of the main checkout (warn if dirty; defer to prepare-worktree's **Sanity checks** step for the call).
|
|
@@ -126,7 +126,7 @@ Always the first write. Plan file is durable; commits get rewritten next.
|
|
|
126
126
|
- Letter: `1b` between `1` (relabeled `1a`) and `2`. Requires renaming `1` → `1a` inside **Phased Rollout** + updating downstream references.
|
|
127
127
|
Ask the user. Default: decimal — no rename of existing ids.
|
|
128
128
|
|
|
129
|
-
3. **Appends** — new `## Phase N+1` block at end of **Phased Rollout**. Same shape as siblings: Goal, Suggested AI model, reusable_skills, Changes, Tests, Acceptance.
|
|
129
|
+
3. **Appends** — new `## Phase N+1` block at end of **Phased Rollout**. Same shape as siblings: Goal, Suggested AI model, optional Review models, reusable_skills, Changes, Tests, Acceptance.
|
|
130
130
|
|
|
131
131
|
4. **Guiding Decisions changes** — rewrite the affected row. Add a one-line note at the top of **Guiding Decisions** ("**Amended YYYY-MM-DD**: replaced storage shape from X to Y; affects phases 2, 3, 4.") so reviewers see what shifted. Reference the changed row by its **Decision** column name, not by a `§N.M` shorthand.
|
|
132
132
|
|
|
@@ -241,7 +241,7 @@ For `change_kind = rebase-only` (downstream phase whose parent moved): skip the
|
|
|
241
241
|
|
|
242
242
|
### 4c. Run the three-layer review
|
|
243
243
|
|
|
244
|
-
Invoke [review-phase](../review-phase/SKILL.md) against the rewritten branch, passing the **new** phase body to walk against and `
|
|
244
|
+
Invoke [review-phase](../review-phase/SKILL.md) against the rewritten branch, passing the **new** phase body to walk against, `WORKROOT`, and the `reviewer` / `fixer` agent types with their `agent_models` tiers plus this phase's `reviewer_model_tier` / `fixer_model_tier` overrides (parsed from the rewritten body's `**Review models**:` line, null when absent). Layer 2 walks: every "Changes" item in the new body, every "Tests" entry, the new acceptance line.
|
|
245
245
|
|
|
246
246
|
Skip this step only when `change_kind = rebase-only` (no body change → no compliance walk). Even then, spot-run review-phase's Layer 1 mechanical checks to verify the rebase didn't lose unrelated work.
|
|
247
247
|
|
package/skills/vinta-derive-skills/resources/plan-execution/shell/implement-plan-template.md
CHANGED
|
@@ -37,7 +37,7 @@ Parse once, reuse for every phase:
|
|
|
37
37
|
- **Goals + Non-goals** section — verbatim, used in every phase prompt.
|
|
38
38
|
- **Guiding Decisions** section — verbatim. Pay attention to: feature flag (key, scope, default, flip-on criterion), storage shape, tenant scoping, API contract decisions.
|
|
39
39
|
- **Data Model Changes** section — keep full body; later phases reference earlier subsections.
|
|
40
|
-
- **Phased Rollout** section — parse into phase records: `{ id, title, goal, body, spec_use_case, suggested_model_tier, reusable_skills, has_e2e, acceptance, is_cross_repo, is_flag_removal }`.
|
|
40
|
+
- **Phased Rollout** section — parse into phase records: `{ id, title, goal, body, spec_use_case, suggested_model_tier, reviewer_model_tier, fixer_model_tier, reusable_skills, has_e2e, acceptance, is_cross_repo, is_flag_removal }`. `reviewer_model_tier` / `fixer_model_tier` come from the phase's optional `**Review models**:` line (null when the phase doesn't override — most phases).
|
|
41
41
|
- **Risk & Rollout Notes**, **Open Questions**, **Touch List** sections — keep available; include in phase prompts only when relevant.
|
|
42
42
|
3. **Classify each phase**: `is_cross_repo`, `is_flag_removal` — the conductor does NOT auto-execute these (see [Cross-repo phases](#cross-repo-phases) + [Flag-removal phase](#flag-removal-phase-always-out-of-scope)).
|
|
43
43
|
4. **Ask the user three opt-in questions** via `AskUserQuestion`. Defaults are project-specific (see below); record every answer in tracking under `run_options`:
|
|
@@ -60,6 +60,14 @@ Parse once, reuse for every phase:
|
|
|
60
60
|
|
|
61
61
|
Wait for "go". After that, the per-phase pause behavior follows `run_options.pause_between_phases`. Inline-comment drafting follows `run_options.generate_inline_comments`. Worktree isolation follows `run_options.use_worktree`. Outer-gate test scope follows `run_options.full_test_suite`.{{E2E_RUN_OPTION_TRAILER}}{{COMMIT_STRATEGY_STEP0_TRAILER}}
|
|
62
62
|
|
|
63
|
+
## Agent models — reviewer, fixer, and mechanical steps
|
|
64
|
+
|
|
65
|
+
The per-phase **implementer** model stays plan-owned (each phase's `**Suggested AI model**:` line — see [implement-phase](../implement-phase/SKILL.md)). Every *other* model this conductor spawns — the review sub-agents and the mechanical steps — is chosen from `.vinta-ai-workflows.yaml`'s `agent_models` section, never from the plan. Read that section once in [Step 0](#step-0--locate--parse-plan) alongside `run_options`.
|
|
66
|
+
|
|
67
|
+
<!-- include: partials/agent-models.md#TIER_RESOLVE -->
|
|
68
|
+
|
|
69
|
+
<!-- include: partials/agent-models.md#MECHANICAL_DELEGATION -->
|
|
70
|
+
|
|
63
71
|
<!-- include: partials/worktree-seam.md#WORKROOT_RESOLUTION -->
|
|
64
72
|
|
|
65
73
|
<!-- include: partials/worktree-seam.md#WORKROOT_TOPOLOGY_RULE -->
|
|
@@ -76,11 +84,11 @@ Invoke [implement-phase](../implement-phase/SKILL.md), passing the phase record,
|
|
|
76
84
|
|
|
77
85
|
### 1b. Review
|
|
78
86
|
|
|
79
|
-
Invoke [review-phase](../review-phase/SKILL.md) against the phase diff, passing the phase body to walk, `WORKROOT`, `main_checkout`, `run_options.full_test_suite`{{E2E_RUN_OPTION_TRACKING}}, and the `reviewer` / `fixer` agent types. It loops its three layers + fix loop until clean, then returns `PASS` (or the surfaced findings). Do not proceed to integrate while any layer is red.
|
|
87
|
+
Invoke [review-phase](../review-phase/SKILL.md) against the phase diff, passing the phase body to walk, `WORKROOT`, `main_checkout`, `run_options.full_test_suite`{{E2E_RUN_OPTION_TRACKING}}, and the `reviewer` / `fixer` agent types with their `agent_models.reviewer` / `agent_models.fixer` tiers **plus this phase's `reviewer_model_tier` / `fixer_model_tier` overrides (null when the phase didn't set a `**Review models**:` line)**. review-phase prefers a phase override over the `agent_models` default. It loops its three layers + fix loop until clean, then returns `PASS` (or the surfaced findings). Do not proceed to integrate while any layer is red.
|
|
80
88
|
|
|
81
89
|
### 1c. Integrate
|
|
82
90
|
|
|
83
|
-
Invoke {{INTEGRATE_PHASE_DISPATCH}}, passing `WORKROOT` / `BASE_BRANCH`, the `{{PR_POLICY_BLOCK}}` policy, and `run_options.generate_inline_comments`. It pushes the branch and routes the PR through the context file, returning the branch + PR-context path + status.
|
|
91
|
+
Invoke {{INTEGRATE_PHASE_DISPATCH}}, passing `WORKROOT` / `BASE_BRANCH`, the `{{PR_POLICY_BLOCK}}` policy, and `run_options.generate_inline_comments`. It pushes the branch and routes the PR through the context file, returning the branch + PR-context path + status. This is a mechanical step: when `agent_models.integrate` is set, run it as a delegated subagent per the [Delegate a mechanical step to a configured model](#delegate-a-mechanical-step-to-a-configured-model) pattern (the delegate pushes + writes the PR-context file + runs `open-pr.sh`, then reports the branch / path / status back); when unset, run it inline. Either way the PR-context file + `open-pr.sh` is the only PR-creation path.
|
|
84
92
|
|
|
85
93
|
### 1d. Update tracking file
|
|
86
94
|
|
|
@@ -151,9 +159,10 @@ After all executable phases complete:
|
|
|
151
159
|
- **Read AGENTS.md** in every phase prompt.
|
|
152
160
|
- **Stage explicitly.** No `git add -A`.
|
|
153
161
|
- **Subagents work in fresh sessions.** Each phase = a new subagent. Tracking + plan files = the context handoff.
|
|
154
|
-
- **Conductor owns git topology.**
|
|
162
|
+
- **Conductor owns git topology.** Phase-work subagents (implementer / reviewer / fixer) commit but never branch, push, {{PR_RULE_TAIL}}. The one exception is a **mechanical `integrate` delegate** spawned per `agent_models.integrate` — it exists precisely to run the conductor's integrate step (push + PR via `open-pr.sh`) on a cheaper model, and the conductor still dictates the branch/base topology it uses.
|
|
155
163
|
{{COAUTHOR_RULE_LINE}}
|
|
156
|
-
- **Trust the plan's per-phase model suggestion.**
|
|
164
|
+
- **Trust the plan's per-phase model suggestion.** Implementer model selection lives in [implement-phase](../implement-phase/SKILL.md); the conductor never re-derives tiers.
|
|
165
|
+
- **Reviewer / fixer / mechanical-step models come from `agent_models`, not the plan.** Resolve each configured tier via the [Agent models](#agent-models--reviewer-fixer-and-mechanical-steps) step; an unset key means the spawn uses the runtime default. The plan never names these models.
|
|
157
166
|
- **Don't re-implement what a project skill encodes.**
|
|
158
167
|
{{UI_E2E_RULE_LINE}}
|
|
159
168
|
- **Two-tier verification, in order, every phase.** Inner scoped, then the outer gate — enforced inside [implement-phase](../implement-phase/SKILL.md). The outer gate always runs the repo-wide type/build gate; its test scope follows `run_options.full_test_suite` (scoped suite by default, full repo suite when opted in).
|
|
@@ -14,7 +14,20 @@ The single review implementation shared by every plan-execution conductor: [impl
|
|
|
14
14
|
- `WORKROOT`, `SANDBOX_TIER` — resolved once by the conductor.
|
|
15
15
|
- `main_checkout` — the repo root the run was invoked from (equals `WORKROOT` when no worktree).
|
|
16
16
|
- `run_options.full_test_suite` — resolves which outer gate Layer 1 item 3 verifies ran (false = scoped suite; true = full repo suite).
|
|
17
|
-
- The project's `reviewer` + `fixer` agent types.
|
|
17
|
+
- The project's `reviewer` + `fixer` agent types, plus their `agent_models.reviewer` / `agent_models.fixer` tiers (when set in `.vinta-ai-workflows.yaml`).
|
|
18
|
+
- Optional per-phase `reviewer_model_tier` / `fixer_model_tier` overrides — the tiers parsed from this phase's `**Review models**:` line in the plan (null when the phase didn't set one, which is the common case).
|
|
19
|
+
|
|
20
|
+
## Resolve the reviewer + fixer model
|
|
21
|
+
|
|
22
|
+
Each of `reviewer` and `fixer` spawns at an **effective tier**, resolved per role with this precedence:
|
|
23
|
+
|
|
24
|
+
1. The phase's `reviewer_model_tier` / `fixer_model_tier` override, when the conductor passed one (the plan chose a non-default review model for this critical phase).
|
|
25
|
+
2. Else the project-wide `agent_models.reviewer` / `agent_models.fixer` tier from `.vinta-ai-workflows.yaml`.
|
|
26
|
+
3. Else unset → the runtime default model.
|
|
27
|
+
|
|
28
|
+
Feed that effective tier into the resolution below (it turns a tier into a concrete spawn model). A phase override applies to that phase only; the next phase falls back to the `agent_models` default unless it too overrides.
|
|
29
|
+
|
|
30
|
+
<!-- include: partials/agent-models.md#TIER_RESOLVE -->
|
|
18
31
|
|
|
19
32
|
## Review
|
|
20
33
|
|