vinta-ai-workflows 0.2.0-alpha2 → 0.2.0-alpha3
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 +75 -0
- package/package.json +7 -1
- package/schemas/README.md +1 -0
- package/schemas/ai-models.v1.schema.json +87 -0
- package/schemas/vinta-ai-workflows-config.v1.schema.json +5 -1
- package/skills/vinta-bootstrap-ai-tools/SKILL.md +1 -1
- package/skills/vinta-derive-skills/SKILL.md +15 -9
- package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/SKILL.md +24 -27
- package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/resources/ai-models.yaml +98 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/SKILL.md +54 -0
- package/skills/vinta-derive-skills/resources/foundation-skills/prepare-worktree/scripts/sandbox-run.sh +104 -0
- package/skills/vinta-derive-skills/resources/implement-plan-template.md +26 -6
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,81 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
## [0.2.0] — YYYY-MM-DD
|
|
9
9
|
|
|
10
|
+
<!-- pre-release: 0.2.0-alpha3 on 2026-06-23 -->
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`plan-feature` AI model tier table extracted to a data resource +
|
|
15
|
+
nightly freshness job.** The per-tier model recommendations that used to
|
|
16
|
+
be hard-coded in `plan-feature/SKILL.md` now live in
|
|
17
|
+
`plan-feature/resources/ai-models.yaml` (ships verbatim with the skill;
|
|
18
|
+
schema: `schemas/ai-models.v1.schema.json`). The SKILL.md body keeps the
|
|
19
|
+
*tier-selection rubric* (stable judgement) and points at the resource for
|
|
20
|
+
concrete model IDs, so the prose no longer goes stale. A source-side
|
|
21
|
+
nightly GitHub Action (`.github/workflows/check-ai-models.yml`, runner
|
|
22
|
+
`scripts/check-ai-models.mjs`) checks the cited IDs against a **free,
|
|
23
|
+
no-key model aggregator** (models.dev, LiteLLM JSON as fallback) — no
|
|
24
|
+
vendor API keys required — flags IDs that disappear or that a newer
|
|
25
|
+
same-family model has superseded, and on drift has an LLM propose an
|
|
26
|
+
updated table that it opens as a reviewable PR (the optional LLM step is
|
|
27
|
+
the only one that uses a key). **Consumers**: re-sync the
|
|
28
|
+
`plan-feature` skill to pick up a refreshed table; the resource's
|
|
29
|
+
`last_verified` date signals how current it is.
|
|
30
|
+
|
|
31
|
+
- **`prepare-worktree` — OS-level filesystem sandbox that *prevents*
|
|
32
|
+
stray main-checkout writes (harness-agnostic).** The prompt instruction
|
|
33
|
+
"stay in the worktree" is cooperative only; a smaller phase subagent can
|
|
34
|
+
resolve a path back to the main checkout and silently write there. A new
|
|
35
|
+
bundled script `prepare-worktree/scripts/sandbox-run.sh` confines the
|
|
36
|
+
*process* (not the agent tool) at the kernel layer — `sandbox-exec` on
|
|
37
|
+
macOS, `bwrap` (bubblewrap) on Linux — using a **deny-main, allow-rest**
|
|
38
|
+
model: the whole filesystem stays writable except the main checkout, with
|
|
39
|
+
the worktree (nested under it) and `.vinta-ai-workflows` punched back to
|
|
40
|
+
writable, so package managers / caches / `$HOME` behave normally and no
|
|
41
|
+
per-stack allowlist tuning is needed. A stray main-checkout write fails
|
|
42
|
+
with `Operation not permitted` / `EROFS` regardless of which harness
|
|
43
|
+
(claude-code, Codex, …) issued it. `prepare-worktree` adds a capability
|
|
44
|
+
probe + a `sandbox` block in its summary YAML recording the achieved tier
|
|
45
|
+
(`enforced` / `none`); `implement-plan` wraps each subprocess subagent
|
|
46
|
+
spawn in the script when `sandbox_tier = enforced`, threads the tier
|
|
47
|
+
through tracking + the re-running-mid-plan resume path, and downgrades its
|
|
48
|
+
existing Layer 1 stray-write check from sole defense to a backstop. On
|
|
49
|
+
machines without a sandbox tool (`tier = none`) the script runs the
|
|
50
|
+
command unsandboxed with a loud warning and the Layer 1 check remains the
|
|
51
|
+
guard. Escape hatch: `VINTA_SANDBOX=off`. **Consumers**: re-sync
|
|
52
|
+
`prepare-worktree` + `implement-plan` to pick up the guard; the whole
|
|
53
|
+
`prepare-worktree/` dir now ships (SKILL.md + `scripts/sandbox-run.sh`),
|
|
54
|
+
so re-run the foundation-skill copy, not just the SKILL.md.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- **`plan-feature` — per-phase PR-size target raised from ~100–300 LoC to
|
|
59
|
+
up to 1500 LoC** (tests included). The old "reviewer reads the diff in
|
|
60
|
+
30 minutes / ~100–300 LoC" guidance forced excessive phase splitting:
|
|
61
|
+
trivial use-cases became standalone PRs and the phase count ballooned
|
|
62
|
+
past what a reviewer wants to track. The new ceiling — "reviewer reads
|
|
63
|
+
≤1500 LoC and understands the phase in isolation" — keeps phases
|
|
64
|
+
MR-sized and single-concern while letting a coherent unit of work land
|
|
65
|
+
in one PR. Pairs with the new **Phase granularity** Step 0 choice
|
|
66
|
+
below: bundled phases also cap at 1500 LoC. Plan-shape checklist
|
|
67
|
+
updated to match.
|
|
68
|
+
|
|
69
|
+
- **`plan-feature` — "one use-case per phase" is now a Step 0 choice, not
|
|
70
|
+
a hard mandate.** Added a **Phase granularity** question to the Scope
|
|
71
|
+
group (default: one use-case per phase). The rule, its apply-even-when
|
|
72
|
+
list, and the checklist item now branch on the answer — opting into
|
|
73
|
+
bundling lets closely-related use-cases share a phase as long as each
|
|
74
|
+
phase stays MR-sized, single-concern, and independently mergeable.
|
|
75
|
+
|
|
76
|
+
- **E2E content is stripped from no-e2e projects.** `create-qa-use-cases`
|
|
77
|
+
now ships **only when `add-e2e-test` is enabled** (it seeds e2e specs),
|
|
78
|
+
and `plan-feature`'s Playwright / `QA_USE_CASES.md` / `pr-screenshots/`
|
|
79
|
+
sections are wrapped in `<!-- e2e:start/end -->` markers that
|
|
80
|
+
`vinta-derive-skills` strips when `add-e2e-test` is disabled. The
|
|
81
|
+
bootstrap `add-e2e-test` answer now also sets
|
|
82
|
+
`foundation_skills.create-qa-use-cases`. Result: projects marked as
|
|
83
|
+
having no e2e tests get foundation skills with zero e2e references.
|
|
84
|
+
|
|
10
85
|
<!-- pre-release: 0.2.0-alpha2 on 2026-06-13 -->
|
|
11
86
|
|
|
12
87
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vinta-ai-workflows",
|
|
3
|
-
"version": "0.2.0-
|
|
3
|
+
"version": "0.2.0-alpha3",
|
|
4
4
|
"description": "Bootstrap AI tooling (AGENTS.md, skills, sub-agents) across Claude Code, Codex, Cursor, and VS Code + GitHub Copilot.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"vinta-ai-workflows": "./vinta-ai-workflows.mjs"
|
|
8
8
|
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"check-ai-models": "node scripts/check-ai-models.mjs"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"yaml": "^2.4.0"
|
|
14
|
+
},
|
|
9
15
|
"files": [
|
|
10
16
|
"vinta-ai-workflows.mjs",
|
|
11
17
|
"skills",
|
package/schemas/README.md
CHANGED
|
@@ -11,6 +11,7 @@ JSON Schema (Draft 2020-12) contracts for every YAML file the vinta-ai-workflows
|
|
|
11
11
|
| PR-context frontmatter | top-of-file YAML in `.vinta-ai-workflows/prs-context/{feature-kebab}/phase-{phase.id}.md` | [`prs-context-frontmatter.v1.schema.json`](prs-context-frontmatter.v1.schema.json) | `implement-plan` / `amend-plan` | `open-pr.sh` |
|
|
12
12
|
| PR-context inline comments | YAML inside the ` ```yaml ... ``` ` fence under `# Comments` of the same file | [`prs-context-comments.v1.schema.json`](prs-context-comments.v1.schema.json) | `implement-plan` / `amend-plan` | `open-pr.sh` |
|
|
13
13
|
| MCP preflight cache | `.vinta-ai-workflows/cache.yaml` (gitignored — per-developer-machine state) | [`mcp-preflight-cache.v1.schema.json`](mcp-preflight-cache.v1.schema.json) | rendered `systematic-debugging` SKILL.md (writes during Phase 0) | rendered `systematic-debugging` SKILL.md (reads at Phase 0 start) |
|
|
14
|
+
| AI model tier table | `ai-tools/skills/plan-feature/resources/ai-models.yaml` (shipped verbatim with the `plan-feature` foundation skill; source: `skills/vinta-derive-skills/resources/foundation-skills/plan-feature/resources/ai-models.yaml`) | [`ai-models.v1.schema.json`](ai-models.v1.schema.json) | nightly `check-ai-models` job ([`scripts/check-ai-models.mjs`](../scripts/check-ai-models.mjs)) → auto-PR | `plan-feature` (per-phase model suggestion) |
|
|
14
15
|
|
|
15
16
|
## Versioning rules
|
|
16
17
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/vintasoftware/vinta-ai-workflows/schemas/ai-models.v1.schema.json",
|
|
4
|
+
"title": "AI model tier table",
|
|
5
|
+
"description": "Per-tier AI model recommendations consumed by the plan-feature skill and maintained by the nightly check-ai-models job. Source of truth for concrete model IDs; the SKILL.md body carries only selection guidance.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schema_version", "last_verified", "source", "tiers"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schema_version": {
|
|
11
|
+
"const": 1,
|
|
12
|
+
"description": "Matches the v1 suffix on this schema filename."
|
|
13
|
+
},
|
|
14
|
+
"last_verified": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"format": "date",
|
|
17
|
+
"pattern": "^\\d{4}-\\d{2}-\\d{2}$",
|
|
18
|
+
"description": "Date every model id below was last confirmed to still exist against the aggregator."
|
|
19
|
+
},
|
|
20
|
+
"source": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"additionalProperties": false,
|
|
23
|
+
"required": ["primary"],
|
|
24
|
+
"description": "Free, no-key model-list aggregators the checker queries. Vendors tracked are derived from the `models` keys under `tiers`.",
|
|
25
|
+
"properties": {
|
|
26
|
+
"primary": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"format": "uri",
|
|
29
|
+
"description": "Primary aggregator JSON (e.g. models.dev/api.json), vendor-keyed with a `.models` map per provider."
|
|
30
|
+
},
|
|
31
|
+
"fallback": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"format": "uri",
|
|
34
|
+
"description": "Fallback aggregator JSON used when the primary is unreachable (e.g. LiteLLM price/context JSON)."
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"tiers": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"minItems": 1,
|
|
41
|
+
"items": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"required": ["tier", "label", "use_for", "models"],
|
|
45
|
+
"properties": {
|
|
46
|
+
"tier": {
|
|
47
|
+
"type": "integer",
|
|
48
|
+
"minimum": 1,
|
|
49
|
+
"description": "Tier number, cheapest/fastest (1) → architectural/hard."
|
|
50
|
+
},
|
|
51
|
+
"label": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "Short tier name."
|
|
54
|
+
},
|
|
55
|
+
"use_for": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "When to reach for this tier."
|
|
58
|
+
},
|
|
59
|
+
"models": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"description": "Vendor key → ordered list of recommended models for this tier. Keys should match entries in the top-level `vendors` map.",
|
|
62
|
+
"minProperties": 1,
|
|
63
|
+
"additionalProperties": {
|
|
64
|
+
"type": "array",
|
|
65
|
+
"minItems": 1,
|
|
66
|
+
"items": {
|
|
67
|
+
"type": "object",
|
|
68
|
+
"additionalProperties": false,
|
|
69
|
+
"required": ["id"],
|
|
70
|
+
"properties": {
|
|
71
|
+
"id": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"description": "Vendor model id as it appears in the vendor's model-list API."
|
|
74
|
+
},
|
|
75
|
+
"note": {
|
|
76
|
+
"type": "string",
|
|
77
|
+
"description": "Optional caveat (fallback, variant, when-to-step-up)."
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -188,7 +188,11 @@
|
|
|
188
188
|
"properties": {
|
|
189
189
|
"plan-feature": { "type": "string", "enum": ["enabled", "disabled"] },
|
|
190
190
|
"create-spec": { "type": "string", "enum": ["enabled", "disabled"] },
|
|
191
|
-
"create-qa-use-cases": {
|
|
191
|
+
"create-qa-use-cases": {
|
|
192
|
+
"description": "Bootstrap `QA_USE_CASES.md` from the active spec/plan. Coupled to `add-e2e-test`: QA use-cases seed e2e specs, so ship (and set `enabled`) only when `add-e2e-test` is `enabled`. When `add-e2e-test` is `disabled`, set this `disabled` too — derive-skills won't ship the skill and strips the e2e regions from `plan-feature`.",
|
|
193
|
+
"type": "string",
|
|
194
|
+
"enum": ["enabled", "disabled"]
|
|
195
|
+
},
|
|
192
196
|
"implement-plan": { "type": "string", "enum": ["enabled", "disabled"] },
|
|
193
197
|
"amend-plan": { "type": "string", "enum": ["enabled", "disabled"] },
|
|
194
198
|
"open-pr-from-context": { "type": "string", "enum": ["enabled", "disabled"] },
|
|
@@ -85,7 +85,7 @@ These bleed across sub-skills, so capture once now:
|
|
|
85
85
|
|
|
86
86
|
Five skills are part of the foundation set but aren't always needed. Ask explicitly:
|
|
87
87
|
|
|
88
|
-
1. **`add-e2e-test`** — does the project have e2e tests (Playwright / Cypress / similar) or plan to add them? `AskUserQuestion` options: `Yes — already has them`, `Yes — planning to add`, `No — skip`. If yes, [vinta-derive-skills](../vinta-derive-skills/SKILL.md)'s **Optional — ask the user** bucket follows up to ask whether the user has a template or wants to draft from scratch.
|
|
88
|
+
1. **`add-e2e-test`** — does the project have e2e tests (Playwright / Cypress / similar) or plan to add them? `AskUserQuestion` options: `Yes — already has them`, `Yes — planning to add`, `No — skip`. If yes, [vinta-derive-skills](../vinta-derive-skills/SKILL.md)'s **Optional — ask the user** bucket follows up to ask whether the user has a template or wants to draft from scratch. **This answer also governs `create-qa-use-cases` and the e2e content inside `plan-feature`**: on `No — skip`, set `foundation_skills.create-qa-use-cases: disabled` too — derive-skills won't ship `create-qa-use-cases` and strips `plan-feature`'s `<!-- e2e:start/end -->` regions so no-e2e projects carry zero Playwright / `QA_USE_CASES.md` / `pr-screenshots/` references. On either `Yes`, set both `add-e2e-test` and `create-qa-use-cases` to `enabled`.
|
|
89
89
|
2. **`add-env-var`** — does the project have a non-trivial env-var propagation flow (multiple files / build configs / CI updates per new var) or a single `.env` file is enough? `AskUserQuestion` options: `Yes — non-trivial flow`, `No — single .env is enough`. Skip if `No`.
|
|
90
90
|
3. **`systematic-debugging`** — should agents follow a root-cause-first debugging flow that mandates pulling evidence from the project's observability MCP servers before proposing any fix? `AskUserQuestion` options: `Yes — enable`, `No — skip`. Recommend enabling whenever any observability MCP server is wired up — the skill bakes the project's real test / lint / build commands into the rendered checklist and instructs the agent to discover available calls on those servers at runtime (so it stays correct even as MCP servers add or rename tools).
|
|
91
91
|
4. **`add-one-off-script`** — does the project ever need one-off operational scripts (backfills, cleanups, ad-hoc data fixes) that mutate production data outside the regular migration / ETL / cron path? `AskUserQuestion` options: `Yes — enable`, `No — skip`. Recommend `Yes` for any project with a relational DB or large user dataset; the bundled `BaseOneOffScript` template enforces the safety contract (dry-run by default, idempotent re-runs, batched DB ops, streamed reads, segmented CSV backups capped at 1M cells per file with one set of files per affected table, interruption-safe signal handlers, console + filesystem + S3 logs that survive an interruption). Skip for read-only projects or projects whose only writes go through a migration tool.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vinta-derive-skills
|
|
3
|
-
description: Author project-specific skills under `ai-tools/skills/`. Always copies the project-agnostic foundation set (`plan-feature`, `create-spec`, `
|
|
3
|
+
description: Author project-specific skills under `ai-tools/skills/`. Always copies the project-agnostic foundation set (`plan-feature`, `create-spec`, `open-pr-from-context`) verbatim from this skill's bundled resources; ships `create-qa-use-cases` and keeps `plan-feature`'s e2e sections only when `add-e2e-test` is enabled. Generates `implement-plan` from a parameterized template using project-specific commands + branch / commit / PR conventions captured in the bootstrap interview. Asks the user whether `add-e2e-test` and `add-env-var` are needed (optional foundation skills — only ship if applicable). Stack-specific skills (Medplum, Django, etc) come from user-supplied templates pointed at by the orchestrator — see [bootstrap-ai-tools/resources/stacks/<stack>/notes.md](../vinta-bootstrap-ai-tools/resources/stacks/) for the categories per stack.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Derive skills
|
|
@@ -45,16 +45,22 @@ These are project-agnostic enough to ship as-is. Copy from this skill's [resourc
|
|
|
45
45
|
|---|---|---|
|
|
46
46
|
| `plan-feature` | [resources/foundation-skills/plan-feature/SKILL.md](resources/foundation-skills/plan-feature/SKILL.md) | Author phased implementation plans for a new feature, with interview-driven scoping. |
|
|
47
47
|
| `create-spec` | [resources/foundation-skills/create-spec/SKILL.md](resources/foundation-skills/create-spec/SKILL.md) | Turn a raw feature prompt into a structured spec doc. |
|
|
48
|
-
| `create-qa-use-cases` | [resources/foundation-skills/create-qa-use-cases/SKILL.md](resources/foundation-skills/create-qa-use-cases/SKILL.md) | Bootstrap a project's `QA_USE_CASES.md` from the active spec/plan. |
|
|
48
|
+
| `create-qa-use-cases` | [resources/foundation-skills/create-qa-use-cases/SKILL.md](resources/foundation-skills/create-qa-use-cases/SKILL.md) | Bootstrap a project's `QA_USE_CASES.md` from the active spec/plan. **E2E-gated** — ship only when `foundation_skills.add-e2e-test` is `enabled`; QA use-cases exist to seed e2e specs, so a no-e2e project has no use for it. |
|
|
49
49
|
| `open-pr-from-context` | [resources/foundation-skills/open-pr-from-context/](resources/foundation-skills/open-pr-from-context/) | Thin SKILL.md wrapper around [scripts/open-pr.sh](resources/foundation-skills/open-pr-from-context/scripts/open-pr.sh) — bash script that publishes one `.vinta-ai-workflows/prs-context/{feature}/{phase}.md` file as a real PR + inline comments via the project's PR CLI (`gh` / `glab`). Used by `implement-plan` (when CLI + `yq`/`jq` are available) and standalone (to publish a `pending` file later). |
|
|
50
|
-
| `prepare-worktree` | [resources/foundation-skills/prepare-worktree/
|
|
50
|
+
| `prepare-worktree` | [resources/foundation-skills/prepare-worktree/](resources/foundation-skills/prepare-worktree/) | Provision a runnable, isolated git worktree for parallel feature work — symlink dep dirs (or copy / reinstall when the plan churns deps), copy + mutate env files, fork dev + test DBs when migrations are in scope, namespace docker-compose project + network, OS-sandbox the worktree so main-checkout writes are blocked (bundled [scripts/sandbox-run.sh](resources/foundation-skills/prepare-worktree/scripts/sandbox-run.sh) — `sandbox-exec` / `bwrap`), write a teardown-ready summary YAML. Used by `implement-plan` when Step 0 question (c) opts in; standalone for ad-hoc isolation. Opt-in (bucket A + ask-first via the bootstrap's **Optional foundation skills** interview). |
|
|
51
51
|
|
|
52
|
-
These
|
|
52
|
+
These reference each other:
|
|
53
53
|
|
|
54
|
-
- `plan-feature` reads spec; dispatches `create-qa-use-cases` when the doc is missing.
|
|
54
|
+
- `plan-feature` reads spec; dispatches `create-qa-use-cases` when the doc is missing (e2e-enabled projects only — see below).
|
|
55
55
|
- `implement-plan` (bucket B below) writes per-phase PR-context files following [resources/prs-context-template.md](resources/prs-context-template.md), then invokes `open-pr-from-context` when a PR CLI is detected.
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
The **always-on unit** is `plan-feature` + `create-spec` + `open-pr-from-context` — copy all three or none. `create-qa-use-cases` joins the unit **only when `foundation_skills.add-e2e-test` is `enabled`**; when add-e2e-test is disabled (or absent), don't ship it and set `foundation_skills.create-qa-use-cases: disabled` in `.vinta-ai-workflows.yaml` to match.
|
|
58
|
+
|
|
59
|
+
**E2E stripping in `plan-feature`.** `plan-feature` ships verbatim but carries e2e-only regions delimited by `<!-- e2e:start -->` / `<!-- e2e:end -->` lines. When copying it, run an e2e pass:
|
|
60
|
+
|
|
61
|
+
- **Always delete the marker lines themselves** (`<!-- e2e:start -->` / `<!-- e2e:end -->`) so they never reach the shipped file.
|
|
62
|
+
- **When `foundation_skills.add-e2e-test` is `disabled` or absent**, also delete everything *between* each marker pair. Result: a no-e2e project gets a `plan-feature` with zero Playwright / `QA_USE_CASES.md` / `pr-screenshots/` / `add-e2e-test` references — no noise in the plans it authors.
|
|
63
|
+
- **When `add-e2e-test` is `enabled`**, keep the enclosed content (strip only the markers).
|
|
58
64
|
|
|
59
65
|
After copying, scan each for project-specific path references that no longer match the target (the bundled copies may still carry source-repo paths such as `<source-repo>/ai-plans/` or `apps/<service>/`). Replace with the target's paths from the inventory.
|
|
60
66
|
|
|
@@ -171,7 +177,7 @@ Two skills depend on whether the project actually does the thing. Ask via `AskUs
|
|
|
171
177
|
| `add-e2e-test` | "Does this project have e2e tests, or plan to add them?" Options: `Yes — already has them`, `Yes — planning to add`, `No — skip` | Ask if user has an existing template (path / URL). If yes → copy + adapt. If no → draft from scratch using the canonical structure (see "Skill SKILL.md structure" below) interviewing user about: e2e framework, page-object pattern, auth/storage-state pattern, seed-helpers location, tenant scoping in seeds, screenshot conventions for PRs. |
|
|
172
178
|
| `add-env-var` | "Does this project have a non-trivial env-var propagation flow (multiple files / build configs / CI to update for one new var)?" Options: `Yes`, `No — single .env file is enough` | Ask if user has an existing template. If yes → copy + adapt. If no → draft from scratch interviewing user about every layer the new var must touch (`.env.example`, build tool envPrefix allowlist, build cache hash inputs, app config module, AGENTS.md env section, CI workflows, deploy-time injection). |
|
|
173
179
|
| `add-one-off-script` | "Does this project ever need one-off operational scripts — backfills, cleanups, ad-hoc data fixes?" Options: `Yes — enable`, `No — skip`. Recommend `Yes` for any project with a relational DB; the skill bundles a `BaseOneOffScript` class enforcing dry-run-by-default, idempotent re-runs, batched DB ops, streamed reads, segmented CSV backups (1M cells/file, never nested across tables), interruption-safe signal handlers, and console + filesystem + S3 logging. | Copy [resources/foundation-skills/add-one-off-script/](resources/foundation-skills/add-one-off-script/) into `ai-tools/skills/add-one-off-script/` (SKILL.md + the bundled `resources/one_off_script_base.{py,ts}` templates). Then ask the user whether they want the base class file pre-staged at `<scripts_dir>/one_off/_base.{py,ts}` — default `Yes` for projects whose primary lang is Python or TS. The skill itself prompts to copy the base class on first use if the user said `No` here. |
|
|
174
|
-
| `prepare-worktree` | "Do you ever want to run plans / experiments inside an isolated git worktree (separate runnable copy of the app with its own DB, env, compose stack) instead of swapping branches in the main checkout?" Options: `Yes — enable`, `No — skip`. Recommend `Yes` for projects with long migrations, multi-day plans, or where the user is likely to want parallel concurrent work. | Copy [resources/foundation-skills/prepare-worktree/
|
|
180
|
+
| `prepare-worktree` | "Do you ever want to run plans / experiments inside an isolated git worktree (separate runnable copy of the app with its own DB, env, compose stack) instead of swapping branches in the main checkout?" Options: `Yes — enable`, `No — skip`. Recommend `Yes` for projects with long migrations, multi-day plans, or where the user is likely to want parallel concurrent work. | Copy the whole [resources/foundation-skills/prepare-worktree/](resources/foundation-skills/prepare-worktree/) dir into `ai-tools/skills/prepare-worktree/` — both `SKILL.md` and `scripts/sandbox-run.sh` (mark the script executable: `chmod +x ai-tools/skills/prepare-worktree/scripts/sandbox-run.sh`). Then ask three short follow-ups landing in `skills.prepare-worktree.*`: worktree root (default `.claude/worktrees` for claude-code projects, `../<repo>-wt-` sibling dirs otherwise), default deps strategy (`symlink` / `copy` / `reinstall`), default test-DB strategy (`always-fork` / `fork-on-schema-change` / `share`). Also ask whether `implement-plan` should default its Step 0 question (c) to `yes` — lands in `run_options.implement-plan.use_worktree`. |
|
|
175
181
|
|
|
176
182
|
If user answers "No" to any: don't ship that skill. Record nothing — the foundation set is just smaller.
|
|
177
183
|
|
|
@@ -232,7 +238,7 @@ Length: 100–300 lines. Shorter = under-specified. Longer = should probably spl
|
|
|
232
238
|
## Rules
|
|
233
239
|
|
|
234
240
|
- **Don't ship copy-pasted source-project SKILL.md files unchanged.** Run search-and-replace for source project names / paths / commands before saving.
|
|
235
|
-
- **Foundation set is a unit.** Always copy `plan-feature` + `create-spec` + `
|
|
241
|
+
- **Foundation set is a unit.** Always copy `plan-feature` + `create-spec` + `open-pr-from-context` together — they reference each other. `create-qa-use-cases` joins only when `add-e2e-test` is enabled; when it's not, strip `plan-feature`'s e2e regions (the `<!-- e2e:start/end -->` pass) so no cross-link dangles.
|
|
236
242
|
- **`implement-plan` is generated, not copied.** Its body has too much project-specific content for verbatim shipping.
|
|
237
243
|
- **Optional skills (`add-e2e-test`, `add-env-var`, `add-one-off-script`, `prepare-worktree`) are gated by user answer.** Don't ship them by default.
|
|
238
244
|
- **Each skill solves one job.** Two unrelated checklists → split.
|
|
@@ -254,6 +260,6 @@ After all skills written:
|
|
|
254
260
|
1. Each `ai-tools/skills/<name>/SKILL.md` has frontmatter `name` + `description`, plus markdown body.
|
|
255
261
|
2. `name` field matches dir name.
|
|
256
262
|
3. No `{{PLACEHOLDER}}` strings survive in any output skill.
|
|
257
|
-
4. Foundation skills (plan-feature, create-spec, create-qa-use-cases) reference each other correctly via `[name](../<name>/SKILL.md)` links.
|
|
263
|
+
4. Foundation skills (plan-feature, create-spec, open-pr-from-context — plus create-qa-use-cases when e2e is enabled) reference each other correctly via `[name](../<name>/SKILL.md)` links. When e2e is disabled, confirm `plan-feature` has **no** surviving `<!-- e2e:start/end -->` markers and no dangling link to `create-qa-use-cases` / `add-e2e-test`.
|
|
258
264
|
5. `implement-plan` body cites real commands + the project's actual code host + branch / PR / co-author policy.
|
|
259
265
|
6. Setup script ([vinta-install-ai-tools-setup](../vinta-install-ai-tools-setup/SKILL.md)) runs cleanly with the new skills in place.
|
|
@@ -55,6 +55,7 @@ Never flatten ten questions into one prose paragraph just because tool unavailab
|
|
|
55
55
|
### B. Scope & non-goals
|
|
56
56
|
1. **Explicitly out of scope** for v1? Force non-goals list — where most plans drift.
|
|
57
57
|
2. v1.x / v2 already implied? Name it so we don't bake assumptions into v1's data model.
|
|
58
|
+
3. **Phase granularity:** one phase per spec use-case (more, smaller PRs) or allow bundling closely-related use-cases (fewer, larger phases)? **Default: one use-case per phase** — confirm or override. Drives the "One use-case per phase" rule under **Phase structure**.
|
|
58
59
|
|
|
59
60
|
### C. Data model & storage
|
|
60
61
|
1. New table, new column on existing, JSONB blob, side table, no persistence?
|
|
@@ -179,25 +180,28 @@ Don't invent new top-level sections. Skip non-applicable (e.g. omit "API Design"
|
|
|
179
180
|
|
|
180
181
|
### Each phase MR-sized
|
|
181
182
|
|
|
182
|
-
Reviewer
|
|
183
|
+
Reviewer should read ≤1500 LoC + understand in isolation. Guidelines:
|
|
183
184
|
|
|
184
|
-
- **Target**:
|
|
185
|
+
- **Target**: Up to 1500 LoC (tests included).
|
|
185
186
|
- **One concern per phase.** "Add field + write migration + wire into 4 use cases + update 3 SQL views" = four phases.
|
|
186
187
|
- **Independently mergeable.** Phase N merged + Phase N+1 stalled → system still working. No half-finished features behind flag with no flag-on path.
|
|
187
188
|
- **Own tests.** Every phase ships unit/integration tests. No "tests come in Phase 8."
|
|
188
189
|
- **Acceptance criterion.** Each phase ends with one-line "Acceptance:" — literally testable.
|
|
189
190
|
|
|
190
|
-
### One use-case per phase (
|
|
191
|
+
### One use-case per phase (default — confirm in Step 0)
|
|
191
192
|
|
|
192
|
-
|
|
193
|
+
**Default ON.** Skip only when the Step 0 **Phase granularity** answer opted into bundling. When on: every spec use-case (entries under **Decisions → Use-cases** in the SPEC) gets **its own phase**. Never bundle two use-cases in one phase even when the diff is tiny. Bundling = larger PR + reviewer needs context for both flows + rollback drags both. Cost of an extra phase = one PR header. Cost of a bundled regression = hotfix + split-after-the-fact.
|
|
193
194
|
|
|
194
|
-
|
|
195
|
+
When on, apply even when:
|
|
195
196
|
- Two use-cases share the same endpoint — split anyway, the second phase is "wire use-case 2 into the existing endpoint." Reviewer reads ≤50 LoC.
|
|
196
197
|
- Use-cases are CRUD on same entity — Create / Read / Update / Delete are four phases, not one.
|
|
197
198
|
- "It's just one extra branch" — that branch hides edge cases. Separate phase forces explicit acceptance + tests for that branch.
|
|
198
199
|
|
|
200
|
+
**If the user opted into bundling** (Step 0): group closely-related use-cases into one phase where it reduces churn, but each phase still stays MR-sized (≤1500 LoC), one concern, independently mergeable, with its own tests + acceptance. Bundling is a granularity dial, not a license for kitchen-sink phases.
|
|
201
|
+
|
|
199
202
|
Cross-cutting infra (shared types, migration, scaffolding) lands in a foundation phase before the use-case phases. Each subsequent use-case phase consumes that scaffolding.
|
|
200
203
|
|
|
204
|
+
<!-- e2e:start -->
|
|
201
205
|
### Phases creating new UI flow need happy-path E2E
|
|
202
206
|
|
|
203
207
|
Every phase that introduces or substantially changes a user-facing flow (new page, new modal, new wizard step, new gated action) **must** ship a Playwright e2e test covering at least the happy path in the same MR. Follow the [add-e2e-test](../add-e2e-test/SKILL.md) skill: pick the next free `PA###` / `PR###` id, update [QA_USE_CASES.md](QA_USE_CASES.md), add the page object + spec. **No `QA_USE_CASES.md` in the project yet?** Run [create-qa-use-cases](../create-qa-use-cases/SKILL.md) first to bootstrap the doc from this plan + spec — add-e2e-test appends, it doesn't create.
|
|
@@ -205,6 +209,7 @@ Every phase that introduces or substantially changes a user-facing flow (new pag
|
|
|
205
209
|
In the phase body, name the spec file path under **Tests → E2E**. If the phase is purely backend (API-only, bot, migration), skip — happy-path E2E only required when the phase reaches the browser.
|
|
206
210
|
|
|
207
211
|
**Capture screenshots on every affected UI**: the e2e spec takes one screenshot per distinct rendered state (landing page, each modal/wizard step, success toast, final state) — not just the final state. Spec writes to Playwright's **default per-test output dir** via `testInfo.outputPath('<id>-<NN>-<view-slug>.png')` (zero-padded step number, kebab-case slug describing what's on screen). Never hard-code `pr-screenshots/` in the spec. After the suite runs, a copy step moves matching files from `test-results/**/` into `pr-screenshots/`. The `pr-screenshots/` directory is gitignored — author drags the files into the PR description in numeric order so reviewers see the journey, not just the destination. See [add-e2e-test](../add-e2e-test/SKILL.md) for the strict filename convention + spec example + copy command.
|
|
212
|
+
<!-- e2e:end -->
|
|
208
213
|
|
|
209
214
|
Doesn't fit → split: `Phase 4a — Static validation`, `Phase 4b — Resolution engine`, `Phase 4c — Apply engine`, `Phase 4d — View wiring`.
|
|
210
215
|
|
|
@@ -229,7 +234,9 @@ Spec use-case: {SPEC **Decisions → Use-cases** id/name this phase implements,
|
|
|
229
234
|
Tests:
|
|
230
235
|
- **Unit**: {file path} — {what it covers}.
|
|
231
236
|
- **Integration**: {file path} — {what it covers, including edge cases user flagged in Step 0, AND flag-off test proving existing callers see no behavior change}.
|
|
237
|
+
<!-- e2e:start -->
|
|
232
238
|
- **E2E** (only when this phase reaches the browser): {e2e/tests/<app>/<id>-<slug>.spec.ts} — happy path covering the new flow. Spec writes screenshots to the Playwright default output dir via `testInfo.outputPath(...)`; post-run copy step moves them into `pr-screenshots/<id>-<step>.png`. Follow [add-e2e-test](../add-e2e-test/SKILL.md).
|
|
239
|
+
<!-- e2e:end -->
|
|
233
240
|
|
|
234
241
|
**Suggested AI model**: {tier choice + why}. See "AI model selection".
|
|
235
242
|
|
|
@@ -285,7 +292,7 @@ Tests:
|
|
|
285
292
|
- Existing test suite passes unchanged on on-branch.
|
|
286
293
|
- Remove flag-parametrized tests no longer make sense.
|
|
287
294
|
|
|
288
|
-
**Suggested AI model**: Tier 1
|
|
295
|
+
**Suggested AI model**: Tier 1 (IDs in `resources/ai-models.yaml`). Mechanical deletion + inlining; cheap models excel.
|
|
289
296
|
|
|
290
297
|
**Reusable skills**: none — pure cleanup.
|
|
291
298
|
|
|
@@ -306,41 +313,29 @@ Common mistake: leave cross-repo producer wiring for last, then discover the ups
|
|
|
306
313
|
|
|
307
314
|
For each phase, suggest **cheapest/fastest model likely to one-shot work**. Iterating with cheap model usually beats burning Opus tokens on CRUD scaffold.
|
|
308
315
|
|
|
316
|
+
**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.
|
|
317
|
+
|
|
309
318
|
### Tier 1 — cheapest/fastest (boilerplate, exact-precedent edits)
|
|
310
319
|
**Use for**: single migration adding column or index, exporting from `__init__.py`, registering admin, scaffolding empty Django app, thin serializer mirroring existing pattern verbatim.
|
|
311
320
|
|
|
312
|
-
- Anthropic: `claude-haiku-4-5`
|
|
313
|
-
- OpenAI: `gpt-5-nano` (fall back to `gpt-5-mini` if file has any branching logic)
|
|
314
|
-
- Google: `gemini-2.5-flash-lite`
|
|
315
|
-
|
|
316
321
|
### Tier 2 — standard pattern application
|
|
317
322
|
**Use for**: repository methods, DRF serializer with non-trivial validation, ViewSet wiring with filterset, pytest unit/integration tests against established fixtures, simple HStore/ArrayField additions.
|
|
318
323
|
|
|
319
|
-
- Anthropic: `claude-haiku-4-5` (with iteration), step up to `claude-sonnet-4-6` if phase touches >3 files
|
|
320
|
-
- OpenAI: `gpt-5-mini`
|
|
321
|
-
- Google: `gemini-2.5-flash`
|
|
322
|
-
|
|
323
324
|
### Tier 3 — multi-file orchestration, business logic, SQL views
|
|
324
325
|
**Use for**: use case coordinating across repositories with non-trivial branching, new `vw_*` view + non-managed model + migration, serializer with cross-field validation affecting use-case behavior, integration tests covering concurrency edges.
|
|
325
326
|
|
|
326
|
-
- Anthropic: `claude-sonnet-4-6`
|
|
327
|
-
- OpenAI: `gpt-5`
|
|
328
|
-
- Google: `gemini-2.5-pro` (or `gemini-3-pro` for stronger code recall)
|
|
329
|
-
|
|
330
327
|
### Tier 4 — architectural / novel / hard
|
|
331
328
|
**Use for**: cycle detection in user-mutable trees, transactional batch protocols with deferred constraints, partitioned-to-partitioned FK design, perf tuning slow query against partitioned hot table, debugging heisenbug.
|
|
332
329
|
|
|
333
|
-
- Anthropic: `claude-opus-4-7` (`[1m]` 1M-context for large codebases)
|
|
334
|
-
- OpenAI: `gpt-5` with extended thinking, or `o3` / `o4` for pure reasoning
|
|
335
|
-
- Google: `gemini-3-pro` with thinking
|
|
336
|
-
|
|
337
330
|
### Writing the suggestion
|
|
338
331
|
|
|
339
|
-
|
|
332
|
+
Pick the tier from the rubric above, then pull the matching vendor IDs out of [resources/ai-models.yaml](resources/ai-models.yaml):
|
|
333
|
+
|
|
334
|
+
> **Suggested AI model**: Tier 1 (IDs in [resources/ai-models.yaml](resources/ai-models.yaml)). Single-field migration + model export, exact precedent in `@<app>/<module>/models/<file>.py`.
|
|
340
335
|
|
|
341
|
-
When one tier doesn't fit:
|
|
336
|
+
When one tier doesn't fit, name both:
|
|
342
337
|
|
|
343
|
-
> **Suggested AI model**: Tier 2 for repository + serializer
|
|
338
|
+
> **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
339
|
|
|
345
340
|
**Default to cheapest tier that plausibly works, not safest.** Cheap models failing fast beats expensive succeeding slowly.
|
|
346
341
|
|
|
@@ -402,11 +397,13 @@ When in doubt, model the plan after a recent example in `ai-plans/` — look for
|
|
|
402
397
|
- [ ] Filename: `ai-plans/{TODAY}-{FEATURE_NAME}_IMPLEMENTATION_PLAN.md`.
|
|
403
398
|
- [ ] **Goals + Non-goals** section present.
|
|
404
399
|
- [ ] **Guiding Decisions** table — each row has *why*.
|
|
405
|
-
- [ ] Phases MR-sized (
|
|
400
|
+
- [ ] Phases MR-sized (≤1500 LoC) + independently mergeable.
|
|
406
401
|
- [ ] Phase numbering uses numbers + letters consistently.
|
|
407
|
-
- [ ] **
|
|
402
|
+
- [ ] **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
403
|
- [ ] Each phase has Goal / Spec use-case / Feature flag (or explicit waiver) / Changes / Tests / Suggested AI model / Reusable skills / Acceptance.
|
|
404
|
+
<!-- e2e:start -->
|
|
409
405
|
- [ ] Every phase introducing a new UI flow has an **E2E happy-path test** in its Tests block, with screenshot output to `pr-screenshots/`.
|
|
406
|
+
<!-- e2e:end -->
|
|
410
407
|
- [ ] Feature flag declared in **Guiding Decisions** (key, scope, default, flip-on criterion) **unless** **Guiding Decisions** explicitly justifies "no flag — purely additive surface".
|
|
411
408
|
- [ ] ≥1 test per gated phase asserts flag-off behavior unchanged.
|
|
412
409
|
- [ ] If flag declared, **final entry under Phased Rollout is dedicated flag-removal phase** with prerequisite (soak window), full deletion touch list, `grep` acceptance check.
|
package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/resources/ai-models.yaml
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# yaml-language-server: $schema=https://github.com/vintasoftware/vinta-ai-workflows/schemas/ai-models.v1.schema.json
|
|
2
|
+
#
|
|
3
|
+
# Per-tier AI model recommendations consumed by the `plan-feature` skill's
|
|
4
|
+
# "AI model selection per phase" section. This file is the single source of
|
|
5
|
+
# truth for concrete model IDs — the SKILL.md body carries the *selection
|
|
6
|
+
# guidance* (when each tier applies, how to write the suggestion) but never
|
|
7
|
+
# hard-codes model IDs, so the prose never goes stale.
|
|
8
|
+
#
|
|
9
|
+
# `last_verified` is the date a maintainer (or the nightly `check-ai-models`
|
|
10
|
+
# job in vinta-ai-workflows) last confirmed every `id` below still exists.
|
|
11
|
+
# Tier *placement* is human/LLM judgement — the job flags drift and proposes
|
|
12
|
+
# edits, it does not blindly reassign tiers. See scripts/check-ai-models.mjs.
|
|
13
|
+
schema_version: 1
|
|
14
|
+
last_verified: 2026-06-22
|
|
15
|
+
|
|
16
|
+
# Drift is checked against a free, no-key community aggregator (models.dev,
|
|
17
|
+
# vendor-keyed with release dates + pricing), cross-checked against LiteLLM's
|
|
18
|
+
# price/context JSON. No vendor API keys are needed to detect staleness — the
|
|
19
|
+
# checker only needs network access. The vendors tracked are derived from the
|
|
20
|
+
# `models:` keys under `tiers` below (anthropic / openai / google), which must
|
|
21
|
+
# match the provider keys used by the aggregator.
|
|
22
|
+
source:
|
|
23
|
+
primary: https://models.dev/api.json
|
|
24
|
+
fallback: https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json
|
|
25
|
+
|
|
26
|
+
# Tiers run cheapest/fastest (1) → architectural/hard (4). Default to the
|
|
27
|
+
# cheapest tier that plausibly one-shots the work; a cheap model failing fast
|
|
28
|
+
# beats an expensive one succeeding slowly.
|
|
29
|
+
tiers:
|
|
30
|
+
- tier: 1
|
|
31
|
+
label: cheapest/fastest (boilerplate, exact-precedent edits)
|
|
32
|
+
use_for: >-
|
|
33
|
+
single migration adding column or index, exporting from __init__.py,
|
|
34
|
+
registering admin, scaffolding empty Django app, thin serializer
|
|
35
|
+
mirroring existing pattern verbatim.
|
|
36
|
+
models:
|
|
37
|
+
anthropic:
|
|
38
|
+
- id: claude-haiku-4-5
|
|
39
|
+
openai:
|
|
40
|
+
- id: gpt-5-nano
|
|
41
|
+
note: fall back to gpt-5-mini if file has any branching logic
|
|
42
|
+
google:
|
|
43
|
+
- id: gemini-2.5-flash-lite
|
|
44
|
+
|
|
45
|
+
- tier: 2
|
|
46
|
+
label: standard pattern application
|
|
47
|
+
use_for: >-
|
|
48
|
+
repository methods, DRF serializer with non-trivial validation, ViewSet
|
|
49
|
+
wiring with filterset, pytest unit/integration tests against established
|
|
50
|
+
fixtures, simple HStore/ArrayField additions.
|
|
51
|
+
models:
|
|
52
|
+
anthropic:
|
|
53
|
+
- id: claude-haiku-4-5
|
|
54
|
+
note: with iteration; step up to claude-sonnet-4-6 if phase touches >3 files
|
|
55
|
+
- id: claude-sonnet-4-6
|
|
56
|
+
note: step-up target when phase touches >3 files
|
|
57
|
+
openai:
|
|
58
|
+
- id: gpt-5-mini
|
|
59
|
+
google:
|
|
60
|
+
- id: gemini-2.5-flash
|
|
61
|
+
|
|
62
|
+
- tier: 3
|
|
63
|
+
label: multi-file orchestration, business logic, SQL views
|
|
64
|
+
use_for: >-
|
|
65
|
+
use case coordinating across repositories with non-trivial branching,
|
|
66
|
+
new vw_* view + non-managed model + migration, serializer with
|
|
67
|
+
cross-field validation affecting use-case behavior, integration tests
|
|
68
|
+
covering concurrency edges.
|
|
69
|
+
models:
|
|
70
|
+
anthropic:
|
|
71
|
+
- id: claude-sonnet-4-6
|
|
72
|
+
openai:
|
|
73
|
+
- id: gpt-5
|
|
74
|
+
google:
|
|
75
|
+
- id: gemini-2.5-pro
|
|
76
|
+
- id: gemini-3-pro
|
|
77
|
+
note: stronger code recall
|
|
78
|
+
|
|
79
|
+
- tier: 4
|
|
80
|
+
label: architectural / novel / hard
|
|
81
|
+
use_for: >-
|
|
82
|
+
cycle detection in user-mutable trees, transactional batch protocols
|
|
83
|
+
with deferred constraints, partitioned-to-partitioned FK design, perf
|
|
84
|
+
tuning slow query against partitioned hot table, debugging heisenbug.
|
|
85
|
+
models:
|
|
86
|
+
anthropic:
|
|
87
|
+
- id: claude-opus-4-7
|
|
88
|
+
note: "[1m] 1M-context variant for large codebases"
|
|
89
|
+
openai:
|
|
90
|
+
- id: gpt-5
|
|
91
|
+
note: with extended thinking
|
|
92
|
+
- id: o3
|
|
93
|
+
note: pure reasoning
|
|
94
|
+
- id: o4
|
|
95
|
+
note: pure reasoning
|
|
96
|
+
google:
|
|
97
|
+
- id: gemini-3-pro
|
|
98
|
+
note: with thinking
|
|
@@ -199,6 +199,43 @@ Quick walk-through of common gotchas. For each, follow the same fork/share patte
|
|
|
199
199
|
|
|
200
200
|
If the **Plan inspection** step inferred the plan doesn't touch any of these: symlink / share. If unsure: fork. Cheap.
|
|
201
201
|
|
|
202
|
+
## Step 5.5 — Filesystem sandbox (OS-level write guard)
|
|
203
|
+
|
|
204
|
+
Symlinking + threading the worktree path through prompts keeps *cooperative* agents in the worktree, but it's not a guarantee: a buggy agent (often a smaller model spawned for a phase) can resolve an absolute path back to the **main checkout** and silently write there. Those writes never reach the worktree's commit — they sit as uncommitted thrash in the main checkout, and the "missing" edits read as a silent agent failure. Prompt instructions don't stop it; they rely on the agent's goodwill.
|
|
205
|
+
|
|
206
|
+
The deterministic, **harness-agnostic** fix is to confine the *process* (not the tool) at the OS filesystem layer: make the main checkout read-only for any command run inside the sandbox, regardless of which agent CLI issues the writes. The bundled [scripts/sandbox-run.sh](scripts/sandbox-run.sh) does this — `sandbox-exec` on macOS, `bwrap` (bubblewrap) on Linux.
|
|
207
|
+
|
|
208
|
+
**Model: deny-main, allow-rest.** The whole filesystem stays writable EXCEPT the main checkout, with the worktree (and the shared `.vinta-ai-workflows` dir) punched back to writable — even though the worktree usually lives *under* the main checkout (`.claude/worktrees/<name>/`). This deliberately does **not** lock `HOME` / caches / `/tmp`, so package managers, build tools, and test runners behave exactly as unsandboxed. The only thing the cage blocks is a write to the main checkout's own tracked tree — which is exactly the bug.
|
|
209
|
+
|
|
210
|
+
Wrapper contract:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
sandbox-run.sh \
|
|
214
|
+
--deny <main-checkout-root> \
|
|
215
|
+
--allow <worktree-path> \
|
|
216
|
+
--allow <summary_dir-and-prs-context-parent> # the .vinta-ai-workflows dir
|
|
217
|
+
-- <command> [args...]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The command runs with main-checkout writes blocked. A stray write fails with `Operation not permitted` (macOS) / `EROFS` (Linux) — the agent sees an ordinary error and retries against the correct path; the worktree stays the only writable copy of the repo.
|
|
221
|
+
|
|
222
|
+
**Capability probe (run here, record the result):**
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
command -v sandbox-exec >/dev/null && tier=enforced # macOS
|
|
226
|
+
command -v bwrap >/dev/null && tier=enforced # Linux
|
|
227
|
+
# neither → tier=none (sandbox unavailable)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- `tier=enforced` → the OS guarantee holds. The caller ([implement-plan](../implement-plan/SKILL.md)) can downgrade its post-run stray-write check to a backstop.
|
|
231
|
+
- `tier=none` → no sandbox tool on this machine (locked-down CI without `bwrap`'s user-namespace support, Windows, etc.). `sandbox-run.sh` still runs the command (best-effort, unsandboxed) and prints a loud warning; prevention falls back to the caller's reactive stray-write detection. **Don't fail provisioning over this** — surface the degraded tier and continue.
|
|
232
|
+
|
|
233
|
+
Escape hatch: `VINTA_SANDBOX=off` makes the wrapper a transparent pass-through (for the rare tool that needs access the sandbox blocks). Record the achieved tier in the summary (next step) so the caller knows whether it's running guaranteed or best-effort.
|
|
234
|
+
|
|
235
|
+
**Sandbox-mode shared-state rule.** Because main is read-only inside the cage, any path the *run* writes must live in the worktree or in an `--allow`'d dir — never a symlink that resolves back into the main checkout. Two follow-ons:
|
|
236
|
+
- The existing `deps_change=true` → copy/reinstall logic already gives the worktree its own writable `node_modules/` when a phase installs deps; non-churn phases keep the read-only symlink (agents shouldn't write deps anyway — correct).
|
|
237
|
+
- The `.vinta-ai-workflows/` dir (summary YAMLs, prs-context) is shared/symlinked but **written** during a run, so it must be passed as an `--allow` path (or made a real writable dir in the worktree). Record which.
|
|
238
|
+
|
|
202
239
|
## Step 6 — Write the summary file
|
|
203
240
|
|
|
204
241
|
`.vinta-ai-workflows/worktrees/<name>.yaml` (committed to `.gitignore` via the existing `.vinta-ai-workflows/` umbrella). Schema:
|
|
@@ -240,6 +277,11 @@ state:
|
|
|
240
277
|
redis_db_index: <int>
|
|
241
278
|
s3_prefix: wt-<name>/
|
|
242
279
|
cron_disabled: <bool>
|
|
280
|
+
sandbox:
|
|
281
|
+
tier: enforced | none # from the Filesystem sandbox step's capability probe
|
|
282
|
+
launcher: sandbox-exec | bwrap | null # null when tier=none
|
|
283
|
+
deny: [<main-checkout-root>] # subtree(s) made read-only inside the cage
|
|
284
|
+
allow: [<worktree-path>, <.vinta-ai-workflows-dir>] # writable exceptions
|
|
243
285
|
notes: |
|
|
244
286
|
<freeform — anything the user / agent should know>
|
|
245
287
|
```
|
|
@@ -254,6 +296,12 @@ Branch: `<branch>` (based on `<base-ref>`).
|
|
|
254
296
|
## What's forked vs shared
|
|
255
297
|
<one-line per row in `state` above>
|
|
256
298
|
|
|
299
|
+
## Write protection
|
|
300
|
+
Sandbox tier: `<enforced | none>` (launcher: `<sandbox-exec | bwrap | none>`).
|
|
301
|
+
When `enforced`, commands run via `sandbox-run.sh` cannot write to the main
|
|
302
|
+
checkout — only this worktree (+ `.vinta-ai-workflows`). When `none`, no OS
|
|
303
|
+
guard is active; the orchestrator's post-run stray-write check is the backstop.
|
|
304
|
+
|
|
257
305
|
## How to run things
|
|
258
306
|
- Lint: `<project lint command>` (runs inside this worktree)
|
|
259
307
|
- Tests: `<project test command>` (against `<test_db.forked_name>`)
|
|
@@ -276,6 +324,7 @@ One paragraph to the caller:
|
|
|
276
324
|
|
|
277
325
|
- Worktree path + branch.
|
|
278
326
|
- One line per fork decision (deps / env / dev DB / test DB / compose / other).
|
|
327
|
+
- **Sandbox tier** (`enforced` via `sandbox-exec` / `bwrap`, or `none`) — so the caller knows whether main-checkout writes are OS-blocked or only caught after the fact.
|
|
279
328
|
- Anything the user must do manually before running the app (`source .envrc`, `direnv allow`, login to a cloud CLI in the worktree, etc.).
|
|
280
329
|
- Teardown command.
|
|
281
330
|
|
|
@@ -300,6 +349,8 @@ Every step gated on user confirmation when the worktree has un-pushed branches.
|
|
|
300
349
|
- **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.
|
|
301
350
|
- **Worktree root governed by runtime conventions.** claude-code uses `.claude/worktrees/`; other harnesses use sibling dirs. Don't fight the harness — match it.
|
|
302
351
|
- **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.
|
|
352
|
+
- **Sandbox is the deterministic guard; the prompt is not.** When a sandbox tool exists, the [Filesystem sandbox](#step-55--filesystem-sandbox-os-level-write-guard) step's `sandbox-run.sh` makes the main checkout read-only for any spawned command — this is what actually *prevents* stray main-checkout writes across harnesses. Telling the agent "stay in the worktree" is best-effort only. Always probe + record the tier; never claim prevention when `tier=none`.
|
|
353
|
+
- **Deny-main, allow-rest — never allow-worktree-only.** Locking everything except the worktree breaks package managers / caches / `$HOME`. Lock only the main checkout subtree; re-allow the worktree (nested under it) and the written `.vinta-ai-workflows` dir. Don't tighten further without testing the project's real lint / test / build / migrate commands inside the cage.
|
|
303
354
|
- **Worktree base = `origin/<default-branch>` by default.** `HEAD` only when the user explicitly confirms; record the choice in the summary.
|
|
304
355
|
- **Refuse to provision a second worktree for the same branch.** Git enforces this — don't try to work around it.
|
|
305
356
|
- **Don't auto-install heavy deps** (e.g. `pnpm install` from scratch) without confirmation when the project's main `node_modules/` is already populated — symlink first, ask if reinstall is needed.
|
|
@@ -311,6 +362,9 @@ Every step gated on user confirmation when the worktree has un-pushed branches.
|
|
|
311
362
|
- **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.
|
|
312
363
|
- **Sharing a Redis DB without per-worktree prefix.** Tests writing `user:123` collide across worktrees. Pick an index OR a key prefix.
|
|
313
364
|
- **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.
|
|
365
|
+
- **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.
|
|
366
|
+
- **Assuming the sandbox is always there.** `bwrap` needs user namespaces enabled — some hardened kernels and CI images disable them. Probe (`command -v bwrap`); on `tier=none` degrade to the post-run check, don't silently believe writes are blocked.
|
|
367
|
+
- **Over-tightening into an allow-worktree-only cage.** Tempting, but it breaks every tool that writes outside the repo (npm cache, `~/.config`, `$TMPDIR`). The deny-main model is the one that needs no per-stack allowlist tuning.
|
|
314
368
|
- **Leaving cron / background workers on in the worktree.** They poll the shared DB and double-process jobs. Default to off.
|
|
315
369
|
- **Provisioning a worktree, running migrations, then realizing the user wanted to share the DB.** The **Database fork** step asks BEFORE migrating; rollback of a forked-DB migration is mechanical (drop the DB), but rollback of a shared-DB migration is a half-day.
|
|
316
370
|
- **Symlinking `.env` and then editing it.** The edit leaks into main. Copy (not symlink) the moment `env_change = true`.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# sandbox-run.sh — run a command with writes to a protected subtree (the main
|
|
3
|
+
# checkout) blocked at the OS level, regardless of which agent harness issues
|
|
4
|
+
# the writes. Harness-agnostic: it confines the *process*, not the tool.
|
|
5
|
+
#
|
|
6
|
+
# Model: deny-main, allow-rest. The whole filesystem stays writable EXCEPT the
|
|
7
|
+
# --deny subtree(s), with --allow subtree(s) punched back to writable (so a
|
|
8
|
+
# worktree nested under the main checkout, and the shared .vinta-ai-workflows
|
|
9
|
+
# dir, keep working). This deliberately does NOT lock HOME / caches / tmp, so
|
|
10
|
+
# package managers, build tools, and test runners behave exactly as unsandboxed.
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# sandbox-run.sh --deny <path> [--deny <path>...] \
|
|
14
|
+
# [--allow <path>...] -- <command> [args...]
|
|
15
|
+
#
|
|
16
|
+
# Required:
|
|
17
|
+
# --deny <path> subtree to make read-only (the main checkout root). Repeatable.
|
|
18
|
+
# Optional:
|
|
19
|
+
# --allow <path> subtree to keep writable even if nested under a --deny path
|
|
20
|
+
# (the worktree; the .vinta-ai-workflows summary/prs-context dir).
|
|
21
|
+
# Repeatable.
|
|
22
|
+
#
|
|
23
|
+
# Environment:
|
|
24
|
+
# VINTA_SANDBOX=off bypass entirely — exec the command unsandboxed and
|
|
25
|
+
# print a warning (escape hatch when a tool needs
|
|
26
|
+
# access the sandbox blocks, or on an unsupported FS).
|
|
27
|
+
# VINTA_SANDBOX_TIER_FILE if set, write the achieved tier to this file:
|
|
28
|
+
# "enforced" (sandbox active) or "none" (no sandbox
|
|
29
|
+
# tool available / bypassed). Callers read it to know
|
|
30
|
+
# whether the OS guarantee held or a backstop is needed.
|
|
31
|
+
#
|
|
32
|
+
# Exit: the command's own exit code. Setup failures exit 2.
|
|
33
|
+
|
|
34
|
+
set -u
|
|
35
|
+
|
|
36
|
+
die() { echo "sandbox-run: $*" >&2; exit 2; }
|
|
37
|
+
|
|
38
|
+
DENY=()
|
|
39
|
+
ALLOW=()
|
|
40
|
+
CMD=()
|
|
41
|
+
parsing_cmd=0
|
|
42
|
+
while [ $# -gt 0 ]; do
|
|
43
|
+
if [ "$parsing_cmd" -eq 1 ]; then CMD+=("$1"); shift; continue; fi
|
|
44
|
+
case "$1" in
|
|
45
|
+
--deny) shift; [ $# -gt 0 ] || die "--deny needs a path"; DENY+=("$1"); shift ;;
|
|
46
|
+
--allow) shift; [ $# -gt 0 ] || die "--allow needs a path"; ALLOW+=("$1"); shift ;;
|
|
47
|
+
--) parsing_cmd=1; shift ;;
|
|
48
|
+
*) die "unknown arg: $1 (did you forget '--' before the command?)" ;;
|
|
49
|
+
esac
|
|
50
|
+
done
|
|
51
|
+
|
|
52
|
+
[ "${#CMD[@]}" -gt 0 ] || die "no command given after '--'"
|
|
53
|
+
[ "${#DENY[@]}" -gt 0 ] || die "at least one --deny <path> required"
|
|
54
|
+
|
|
55
|
+
# Canonicalize a path without requiring the leaf to exist.
|
|
56
|
+
canon() {
|
|
57
|
+
local p="$1" d b
|
|
58
|
+
if [ -d "$p" ]; then (cd "$p" && pwd -P); return; fi
|
|
59
|
+
d=$(dirname "$p"); b=$(basename "$p")
|
|
60
|
+
if [ -d "$d" ]; then echo "$(cd "$d" && pwd -P)/$b"; else echo "$p"; fi
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
DENY_C=(); for p in "${DENY[@]}"; do DENY_C+=("$(canon "$p")"); done
|
|
64
|
+
ALLOW_C=(); for p in "${ALLOW[@]}"; do ALLOW_C+=("$(canon "$p")"); done
|
|
65
|
+
|
|
66
|
+
record_tier() {
|
|
67
|
+
[ -n "${VINTA_SANDBOX_TIER_FILE:-}" ] && printf '%s' "$1" > "$VINTA_SANDBOX_TIER_FILE" 2>/dev/null || true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if [ "${VINTA_SANDBOX:-}" = "off" ]; then
|
|
71
|
+
echo "sandbox-run: VINTA_SANDBOX=off — running UNSANDBOXED (no main-checkout protection)" >&2
|
|
72
|
+
record_tier none
|
|
73
|
+
exec "${CMD[@]}"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# ---- macOS: sandbox-exec -----------------------------------------------------
|
|
77
|
+
if command -v sandbox-exec >/dev/null 2>&1; then
|
|
78
|
+
profile=$(mktemp -t vinta-sandbox) || die "mktemp failed"
|
|
79
|
+
trap 'rm -f "$profile"' EXIT
|
|
80
|
+
{
|
|
81
|
+
echo '(version 1)'
|
|
82
|
+
echo '(allow default)'
|
|
83
|
+
for p in "${DENY_C[@]}"; do echo "(deny file-write* (subpath \"$p\"))"; done
|
|
84
|
+
for p in "${ALLOW_C[@]}"; do echo "(allow file-write* (subpath \"$p\"))"; done
|
|
85
|
+
} > "$profile"
|
|
86
|
+
record_tier enforced
|
|
87
|
+
exec sandbox-exec -f "$profile" "${CMD[@]}"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# ---- Linux: bubblewrap -------------------------------------------------------
|
|
91
|
+
if command -v bwrap >/dev/null 2>&1; then
|
|
92
|
+
args=(--bind / /)
|
|
93
|
+
for p in "${DENY_C[@]}"; do args+=(--ro-bind "$p" "$p"); done # main → read-only
|
|
94
|
+
for p in "${ALLOW_C[@]}"; do args+=(--bind "$p" "$p"); done # worktree → writable (overrides ro)
|
|
95
|
+
args+=(--dev /dev --proc /proc)
|
|
96
|
+
record_tier enforced
|
|
97
|
+
exec bwrap "${args[@]}" -- "${CMD[@]}"
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# ---- No sandbox tool: best-effort fallback -----------------------------------
|
|
101
|
+
echo "sandbox-run: no sandbox tool (sandbox-exec / bwrap) found — running UNSANDBOXED." >&2
|
|
102
|
+
echo "sandbox-run: main-checkout writes are NOT blocked; rely on the post-run stray-write check." >&2
|
|
103
|
+
record_tier none
|
|
104
|
+
exec "${CMD[@]}"
|
|
@@ -64,7 +64,8 @@ When true, invoke [prepare-worktree](../prepare-worktree/SKILL.md) **once**, bef
|
|
|
64
64
|
- `worktree_path` — absolute path the phase subagents will `cd` into.
|
|
65
65
|
- `worktree_branch` — the base branch prepare-worktree created; phase branches stack off this.
|
|
66
66
|
- `worktree_summary` — `.vinta-ai-workflows/worktrees/<name>.yaml` (read by teardown).
|
|
67
|
-
|
|
67
|
+
- `sandbox_tier` — `enforced` (the [Filesystem sandbox](../prepare-worktree/SKILL.md#step-55--filesystem-sandbox-os-level-write-guard) step found `sandbox-exec` / `bwrap` and will OS-block main-checkout writes) or `none` (no sandbox tool — prevention degrades to the Layer 1 backstop). Drives the [Spawn subagent](#1c-spawn-subagent) wrapping + Layer 1 below.
|
|
68
|
+
4. **Persist to tracking.** Write `run_options.worktree_path`, `run_options.worktree_branch`, `run_options.worktree_summary`, `run_options.sandbox_tier` into `{{PLAN_DIR}}/TRACKING_{plan-id}.md` (Step 1g schema gains these fields). All later phases read them — never re-provision mid-plan.
|
|
68
69
|
5. **Report to user.** Quote the prepare-worktree summary back: which dirs symlinked vs copied vs forked; which DB(s) forked + their names; compose project name; teardown command. Hold here until the user confirms (`AskUserQuestion`: `Looks good — start phase 1`, `Stop — let me adjust`).
|
|
69
70
|
|
|
70
71
|
**Worktree topology rule.** When `use_worktree = true`, every phase branches off the previous phase **inside the worktree**, not off `{{DEFAULT_BRANCH}}` in the main checkout. The first phase branches off `<worktree_branch>` (which prepare-worktree based on `origin/{{DEFAULT_BRANCH}}`); subsequent phases stack as usual. All `git` calls in the **{{BRANCH_PUSH_HEADING}}** step use `git -C <worktree_path>` (or `cd` into the worktree first). All inner / outer test commands in the [Prepare agent prompt](#1a-prepare-agent-prompt-token-efficient) step's working-instructions block run inside the worktree so they hit the forked DB / env / compose stack.
|
|
@@ -94,6 +95,9 @@ You are implementing {phase.id}: {phase.title} of plan {plan.id}.
|
|
|
94
95
|
call runs there. Do NOT touch the main checkout — its DB, env, and compose stack
|
|
95
96
|
are intentionally separated. See `<run_options.worktree_path>/WORKTREE.md` for
|
|
96
97
|
what's forked vs shared (deps, dev DB, test DB, compose project name, env file).
|
|
98
|
+
{If run_options.sandbox_tier = enforced:} Writes to the main checkout are
|
|
99
|
+
OS-blocked — if you see `Operation not permitted` / `EROFS` on a write, you
|
|
100
|
+
used a main-checkout path by mistake; redo it against this worktree path.
|
|
97
101
|
Branch base for this phase: `<phase-specific base>` — orchestrator already
|
|
98
102
|
created your phase branch there; commit straight to it.
|
|
99
103
|
|
|
@@ -174,6 +178,21 @@ Use whatever agent-spawning primitive the runtime exposes. Pass:
|
|
|
174
178
|
- Phase prompt from the [Prepare agent prompt](#1a-prepare-agent-prompt-token-efficient) step.
|
|
175
179
|
- The right **agent type**.
|
|
176
180
|
|
|
181
|
+
**Sandbox the spawn (only when `run_options.use_worktree = true`).** The prompt tells the subagent to stay in the worktree, but that's cooperative — a smaller model can resolve a path back to the main checkout and silently write there (the failure Layer 1 below catches reactively). When the runtime spawns subagents as **subprocesses** (it shells out to an agent CLI — e.g. `codex exec …`, a `claude -p …` child, a custom runner), wrap that launch command in the worktree's bundled guard so the OS blocks main-checkout writes regardless of harness:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
ai-tools/skills/prepare-worktree/scripts/sandbox-run.sh \
|
|
185
|
+
--deny <main-checkout-root> \
|
|
186
|
+
--allow <run_options.worktree_path> \
|
|
187
|
+
--allow <main-checkout-root>/.vinta-ai-workflows \
|
|
188
|
+
-- <the agent spawn command>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`<main-checkout-root>` is the repo root the skill was invoked from (never `worktree_path`). A stray write then fails with `Operation not permitted` / `EROFS`; the subagent retries against the worktree.
|
|
192
|
+
|
|
193
|
+
- **In-process subagent runtimes** (the orchestrator and subagent share one OS process — e.g. claude-code's Task tool) can't wrap a single spawn. Two options: (a) install a runtime pre-write guard hook scoped to the worktree, or (b) run the **entire** implement-plan invocation under `sandbox-run.sh` with the same `--deny` / `--allow` set (the deny-main model leaves `.vinta-ai-workflows` writable for the orchestrator's own tracking / prs-context writes). Pick whichever the runtime supports.
|
|
194
|
+
- **`run_options.sandbox_tier = none`** (no sandbox tool on the machine) → skip wrapping; prevention falls back entirely to the Layer 1 stray-write check below. Surface this once to the user so the weaker guarantee is explicit.
|
|
195
|
+
|
|
177
196
|
**Agent type per phase.** Project agents in [`ai-tools/agents/`](ai-tools/agents/) (exposed to claude-code via `.claude/agents` symlink):
|
|
178
197
|
|
|
179
198
|
{{AGENT_DISPATCH_TABLE}}
|
|
@@ -193,7 +212,7 @@ Three layers, all required, in order. The orchestrator never edits — every iss
|
|
|
193
212
|
3. **Verify the outer gate** ran + green. Look in the report for explicit confirmation that `{{BUILD_CMD}}` AND `{{TEST_CMD}}` were executed + passed{{E2E_LAYER1_NOTE}}. Vague confirmation → **re-run yourself**.
|
|
194
213
|
4. **Scope creep**: file touched outside expected surface area? Unrelated formatting churn? Surface it.
|
|
195
214
|
5. **No-secrets scan**: `git diff` for `password|secret|token|api_key|AKIA|BEGIN [A-Z]+ KEY`.
|
|
196
|
-
6. **Stray main-checkout writes (only when `run_options.use_worktree = true`)**: a subagent is told to work inside the worktree, but a buggy agent (often a smaller model) can resolve an absolute path back to the **main checkout** and silently edit files there (worktrees have independent working trees, so those edits never reach the phase commit — they sit as uncommitted thrash in the main checkout, and the "missing" edits read as a silent fixer/implementer failure).
|
|
215
|
+
6. **Stray main-checkout writes (only when `run_options.use_worktree = true`)**: a subagent is told to work inside the worktree, but a buggy agent (often a smaller model) can resolve an absolute path back to the **main checkout** and silently edit files there (worktrees have independent working trees, so those edits never reach the phase commit — they sit as uncommitted thrash in the main checkout, and the "missing" edits read as a silent fixer/implementer failure). **When `run_options.sandbox_tier = enforced` the OS sandbox from the [Spawn subagent](#1c-spawn-subagent) step already blocks these writes — this check becomes a cheap backstop (a clean `git status` is the expected result; non-empty output means the sandbox was bypassed or a path slipped through, still a BLOCKER).** When `sandbox_tier = none` it's the *only* line of defense — run it religiously. Either way, after **every** implementer **and** fixer subagent returns, run `git -C <main-checkout-path> status --short | grep -vE '^\?\?'` (tracked modifications only). Any output is a BLOCKER for this phase:
|
|
197
216
|
- Diff the stray files (`git -C <main-checkout-path> diff -- <path>`) to recover intent.
|
|
198
217
|
- If the edit belongs in the worktree, re-dispatch the fixer/implementer with an explicit instruction to write to the worktree path (the change is missing from the phase commit until it does).
|
|
199
218
|
- Once recovered (or confirmed superseded by the correctly-committed worktree version), discard the stray edits with `git -C <main-checkout-path> restore -- <path>` so the main checkout returns clean. Never leave the main checkout dirty between phases — a later phase's Layer 1 can't tell new thrash from old.
|
|
@@ -304,7 +323,7 @@ Two project-level signals decide the actual behavior:
|
|
|
304
323
|
|
|
305
324
|
Tracking lives at `{{PLAN_DIR}}/TRACKING_{plan-id}.md`. Commit on the **current** phase's branch — deletion in Step 3.
|
|
306
325
|
|
|
307
|
-
Schema: feature-name, plan path, started/last-updated dates, optional feature-flag info, **run options** (`pause_between_phases`, `generate_inline_comments`, `use_worktree`, `worktree_path`, `worktree_branch`, `worktree_summary` — last
|
|
326
|
+
Schema: feature-name, plan path, started/last-updated dates, optional feature-flag info, **run options** (`pause_between_phases`, `generate_inline_comments`, `use_worktree`, `worktree_path`, `worktree_branch`, `worktree_summary`, `sandbox_tier` — last four only when `use_worktree = true`), {{TRACKING_BRANCH_FIELD}}, completed-phases (with status, model{{TRACKING_PHASE_BRANCH_FIELD}}, e2e+screenshots if any, 5–15 line summary), current phase, remaining phases, deferred phases.
|
|
308
327
|
|
|
309
328
|
The orchestrator writes this from the git diff + the agent's summary — not from the agent's narration.
|
|
310
329
|
|
|
@@ -349,6 +368,7 @@ User invokes the skill against a partially-done plan:
|
|
|
349
368
|
2. **Worktree resume.** When `run_options.use_worktree = true`:
|
|
350
369
|
- Confirm the worktree still exists (`git worktree list | grep <worktree_path>`). Missing → ask user: `Reprovision (run prepare-worktree again with the same name)`, `Switch to main checkout (flip use_worktree to false for the rest of the run)`, `Stop`.
|
|
351
370
|
- Confirm the worktree summary file still parses; if not, regenerate from the existing worktree state.
|
|
371
|
+
- **Re-probe `sandbox_tier`** (`command -v sandbox-exec || command -v bwrap`) — a resume may run on a different machine than the original provisioning. Update `run_options.sandbox_tier` in tracking before spawning; the [Spawn subagent](#1c-spawn-subagent) wrapping follows the re-probed value.
|
|
352
372
|
- All resumed phases use the existing worktree — do not provision a second one.
|
|
353
373
|
3. `git -C <path> branch -a | grep plan/{plan-id-kebab}` to detect already-pushed phase branches (`<path>` = main checkout or worktree per `run_options.use_worktree`).
|
|
354
374
|
4. Cross-reference with the plan's phase list.
|
|
@@ -382,7 +402,7 @@ After all executable phases complete:
|
|
|
382
402
|
- **Honor opt-in flags.** `run_options.pause_between_phases` controls the [Per-phase pause gate](#1i-per-phase-pause-gate-opt-in); `run_options.generate_inline_comments` controls whether the [Open PR via context file](#1f-open-pr-via-context-file) step drafts inline comments (always writes the file when that step runs at all — empty comments when off); `run_options.use_worktree` controls whether the [Provision worktree](#step-05--provision-worktree-when-run_optionsuse_worktree--true) step runs and whether every later `git` / lint / test / build / migrate call uses the worktree path.
|
|
383
403
|
- **One worktree per plan run.** When `use_worktree = true`, provision once in the [Provision worktree](#step-05--provision-worktree-when-run_optionsuse_worktree--true) step and reuse for every phase. Never spawn a second worktree mid-plan; never silently fall back to the main checkout on prepare-worktree failure (ask the user).
|
|
384
404
|
- **Don't auto-tear-down the worktree.** Step 2 surfaces the teardown command; the user runs it when they're ready (after reviewer feedback is addressed, after the PR merges, etc.).
|
|
385
|
-
- **
|
|
405
|
+
- **Prevent stray main-checkout writes at the OS layer when you can; catch them always.** When `use_worktree = true` and a sandbox tool exists (`sandbox_tier = enforced`), wrap every subprocess subagent spawn in `prepare-worktree`'s `sandbox-run.sh` (deny main checkout, allow worktree + `.vinta-ai-workflows`) so main-checkout writes are blocked by the kernel regardless of harness — see [Spawn subagent](#1c-spawn-subagent). This is *prevention*; the prompt instruction alone is not. Regardless of tier, Layer 1 runs `git -C <main-checkout-path> status --short` after every implementer and fixer as the backstop; any tracked modification is a BLOCKER — recover the intent, re-dispatch to the worktree, then restore the main checkout clean. `<main-checkout-path>` is the repo root the skill was invoked from, never `run_options.worktree_path`.
|
|
386
406
|
- **PR-context file + `open-pr.sh` is the only PR-creation path.** No raw `gh pr create` / `glab mr create` calls outside the bundled script. The file is durable; the script is the publisher.
|
|
387
407
|
{{DEPENDENCY_LICENSE_RULE_LINE}}
|
|
388
408
|
- **[Open PR via context file](#1f-open-pr-via-context-file) gating** = combination of project PR policy ({{PR_POLICY_BLOCK}}) and `generate_inline_comments`. See the matrix in that step. Skip it entirely only when policy = branches only AND comments = off.
|
|
@@ -392,10 +412,10 @@ After all executable phases complete:
|
|
|
392
412
|
|
|
393
413
|
- [ ] Plan parsed; structured fields cached.
|
|
394
414
|
- [ ] Cross-repo + flag-removal phases identified + deferred.
|
|
395
|
-
- [ ] `run_options.use_worktree` resolved; [Provision worktree](#step-05--provision-worktree-when-run_optionsuse_worktree--true) ran when `true` (worktree provisioned + summary captured + user confirmed); skipped when `false`.
|
|
415
|
+
- [ ] `run_options.use_worktree` resolved; [Provision worktree](#step-05--provision-worktree-when-run_optionsuse_worktree--true) ran when `true` (worktree provisioned + summary captured + `sandbox_tier` probed + user confirmed); skipped when `false`.
|
|
396
416
|
- [ ] Current phase: prompt composed with **Goals + Non-goals** + **Guiding Decisions** + relevant **Data Model Changes** subsection + tracking summaries + this phase's body (plus the worktree block when `use_worktree = true`).
|
|
397
417
|
- [ ] Model picked from `**Suggested AI model**:` line (cheapest available); plan tier recorded.
|
|
398
|
-
- [ ] Subagent spawned, report received.
|
|
418
|
+
- [ ] Subagent spawned, report received. When `use_worktree = true` AND `sandbox_tier = enforced`: subprocess spawn wrapped in `sandbox-run.sh` (deny main, allow worktree + `.vinta-ai-workflows`), or the whole run sandboxed for in-process runtimes.
|
|
399
419
|
- [ ] Inner loop green: scoped lint + new tests individually + scoped suite.
|
|
400
420
|
- [ ] **Outer gate green:** `{{BUILD_CMD}}` AND `{{TEST_CMD}}`{{E2E_OUTER_GATE_CHECKLIST}} both passed.
|
|
401
421
|
- [ ] Layer 1 review: full diff read; no scope creep; no secrets; outer gate confirmed{{COAUTHOR_CHECKLIST_NOTE}}; when `use_worktree = true`, `git -C <main-checkout-path> status --short` clean (no stray main-checkout writes) after the implementer and after every fixer.
|