vinta-ai-workflows 0.5.0 → 0.6.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 CHANGED
@@ -5,6 +5,88 @@ 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.6.0] — 2026-08-17
9
+
10
+ ### Changed
11
+
12
+ - **`write-unit-test` now covers the bug-fix case.** The shipped skill body
13
+ gains a **Regression tests** section. A bug fix ships a test that fails
14
+ without the fix; when the test came second, revert the fix to check. Name the
15
+ test for what must not happen. Assert the cause too, when the visible symptom
16
+ can look right while the bug is still there. Record a state that settles
17
+ instead of only its final value. This adds one narrow exception to universal
18
+ rule 6, which otherwise keeps tests decoupled from internals. When no test can
19
+ cover the regression, the PR says so and a comment gives the reason.
20
+ - **`stacks/react.md`** shows how to record a value on every render, so a bug in
21
+ a state that settles cannot hide behind the final UI. It also notes that
22
+ `waitFor` hides the same bug.
23
+ - **`systematic-debugging`** Phase 3 accepts the case where the fix came before
24
+ the test: revert the fix to prove the test fails. Phase 4 covers the
25
+ regression no test can reach, and the verification checklist asks for the
26
+ revert check.
27
+ - **`deslop-comments`** rule 2 no longer accepts a bug fix as a reason to keep
28
+ "not X, it's Y" framing. The regression test is the record.
29
+ - **`AGENTS.md` now lands at the repo root as a regular file, not at
30
+ `ai-tools/AGENTS.md` behind a root symlink.** The doc was always *read* from
31
+ the root through that symlink, so its links were written as repo-root paths —
32
+ which broke the moment anyone opened the real file at `ai-tools/AGENTS.md`, on
33
+ GitHub or in an editor that follows the real path. One location means one set
34
+ of links that resolves for every reader. `ai-tools/` still owns skills,
35
+ sub-agent definitions, and the setup script; only the conventions doc moved
36
+ out. Mirrors the change [vintasoftware/building-blocks#403](https://github.com/vintasoftware/building-blocks/pull/403)
37
+ made by hand, so the next sync into that repo no longer reverts it.
38
+
39
+ Wiring that changed in
40
+ [`setup-ai-tools.mjs`](skills/vinta-install-ai-tools-setup/resources/setup-ai-tools.mjs):
41
+
42
+ - The script no longer creates a root `AGENTS.md` symlink. It creates the
43
+ per-vendor aliases instead — `CLAUDE.md → AGENTS.md` when `claude` is a
44
+ selected vendor, `.github/copilot-instructions.md → ../AGENTS.md` when
45
+ `copilot` is. Both are gated on `--only`, matching the script's own rule
46
+ that unlisted vendors are left untouched.
47
+ - `ensureSymlink` now resolves a link's target relative to the link's own
48
+ directory and throws when it does not exist. Before, a link to a missing
49
+ file was created anyway: the script printed "Symlinks ensured" and exited 0
50
+ while leaving a dangling link that reads as an empty instruction file to an
51
+ agent.
52
+
53
+ Skills link the doc as `../../../AGENTS.md` — the depth from
54
+ `ai-tools/skills/<name>/SKILL.md`, and the same depth through every vendor
55
+ symlink (`.claude/skills/<name>/SKILL.md`, `.agents/…`, `.cursor/…`,
56
+ `.github/…`). The `implement-plan` / `amend-plan` templates and the
57
+ dependency-license block previously emitted a bare `[AGENTS.md](AGENTS.md)`,
58
+ which resolved to a non-existent file inside the skill's own folder; the
59
+ `systematic-debugging` template was one level short at `../../AGENTS.md`. All
60
+ four are fixed.
61
+
62
+ - **`vinta-write-agents-md` writes to the repo root.** Its output section now
63
+ states the doc's links must be repo-root-relative, and its verification adds a
64
+ link-resolution check run from the root.
65
+
66
+ - **`vinta-install-ai-tools-setup` requires a root `AGENTS.md` up front.** The
67
+ prerequisite check asks for a regular file at the root and routes projects on
68
+ the old layout to the sync migration below.
69
+
70
+ ### Added
71
+
72
+ - **`vinta-sync-ai-tools` migrates projects off the old layout.** New
73
+ `layout-migration` classification bucket — never batched with `tooling`, one
74
+ question of its own — plus a step-by-step migration for
75
+ `ai-tools/AGENTS.md` → root `AGENTS.md`: `git mv` so history follows the file,
76
+ link checks in both directions, a prose sweep over `ai-tools/**` for the old
77
+ path, and the setup-script re-run ordered *after* the move. Projects
78
+ bootstrapped on 0.6.0 or later skip it.
79
+
80
+ A skipped layout migration is **not** sticky the way a declined foundation
81
+ skill is — the next sync re-offers it. Nothing errors in the meantime: the new
82
+ script runs fine against the old layout, because `.github/copilot-instructions.md`
83
+ → `../AGENTS.md` still resolves through the surviving root symlink. That is
84
+ what makes the half-migrated state easy to miss, so the final report names it.
85
+
86
+ - **`vinta-analyze-codebase` recognizes the legacy layout.** A root `AGENTS.md`
87
+ that is a symlink into `ai-tools/AGENTS.md` is recorded as a finding for the
88
+ sync skill to act on.
89
+
8
90
  ## [0.5.0] — 2026-08-05
9
91
 
10
92
  ### Changed
package/README.md CHANGED
@@ -20,7 +20,7 @@ npx vinta-ai-workflows install --tool claude-code
20
20
  # Claude Code → /vinta-bootstrap-ai-tools
21
21
 
22
22
  # 4. Review and commit the generated layout.
23
- git add ai-tools/ AGENTS.md .claude/ .codex/ .cursor/ .github/ .agents/
23
+ git add ai-tools/ AGENTS.md CLAUDE.md .claude/ .codex/ .cursor/ .github/ .agents/
24
24
  git commit -m "Bootstrap ai-tools layout"
25
25
 
26
26
  # 5. Remove the builder skills — one-shot done.
@@ -53,7 +53,7 @@ After bootstrap, the project ships with **three foundation skills** that take a
53
53
  └─────────────────┘ pushed to GitHub / GitLab / etc.
54
54
  ```
55
55
 
56
- You also get: an `AGENTS.md` tuned to the codebase, project-specific sub-agents and skills derived from the actual stack, and per-vendor wiring so Claude Code / Codex / Cursor / Copilot all see the same artifacts.
56
+ You also get: an `AGENTS.md` tuned to the codebase — a regular file at the repo root, with per-vendor aliases (`CLAUDE.md`, `.github/copilot-instructions.md`) symlinked to it — project-specific sub-agents and skills derived from the actual stack, and per-vendor wiring so Claude Code / Codex / Cursor / Copilot all see the same artifacts.
57
57
 
58
58
  **The project keeps getting better without re-bootstrapping.** `vinta-ai-workflows` ships new foundation skills, sharper templates, schema additions, and best-practice updates lifted from real projects. A bootstrapped repo pulls those in via [`vinta-sync-ai-tools`](#staying-in-sync-with-upstream) — one command from the AI tool, per-change `Apply` / `Skip` / `Show diff` gating, opt-outs sticky across runs, schema migrations automatic. No manual re-scaffolding, no clobbered hand-tuning, no drift between projects on the same package version. This is treated as a first-class capability of the package, not an afterthought.
59
59
 
@@ -206,7 +206,7 @@ Standalone re-invocation works at every step — kick `create-spec` again when t
206
206
 
207
207
  ## Cheat sheet — what lands in your project
208
208
 
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:
209
+ Quick inventory of what `vinta-bootstrap-ai-tools` writes into your repo's `ai-tools/` layout — plus `AGENTS.md` at the repo root, the one artifact that lives outside it — and exposes via per-vendor wiring at `.claude/skills/`, `.agents/skills/`, etc. Two disclaimers up front:
210
210
 
211
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
  >
@@ -265,6 +265,8 @@ Agents write tests that pass but assert too little, mock everything, leave data
265
265
 
266
266
  Plus a "run it green, then break the code to watch it go red" step and a name-for-the-behavior / one-reason-to-fail convention.
267
267
 
268
+ The skill also covers the bug-fix case, in a **Regression tests** section. The test has to fail without the fix, so revert the fix to check when the test came second. Name it for what must not happen. Assert the cause too, when the visible symptom can look right while the bug is still there. Record a state that settles instead of only its final value. When no test can cover the regression, the PR says so.
269
+
268
270
  **Framework guidance ships as best-practice packs on two independent axes**, so a project only ever receives advice for frameworks it actually uses (a FastAPI repo never gets Django guidance; a pure library gets no web-framework pack):
269
271
 
270
272
  - **Runner pack** (`resources/packs/runners/`) — how to structure, assert, and mock. One, picked by the detected test runner: `pytest`, `vitest`, or `jest`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vinta-ai-workflows",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
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": {
@@ -120,7 +120,7 @@
120
120
  "type": "string"
121
121
  },
122
122
  "dependency_licenses": {
123
- "description": "Third-party dependency license policy. Captured at bootstrap; rendered into `ai-tools/AGENTS.md` (`## Dependency licenses` section) and the `implement-plan` / `amend-plan` skill bodies so the implementer / reviewer agents check a new dep's SPDX license against `forbidden_spdx` before running any install command (`npm add`, `pnpm add`, `pip install`, `poetry add`, `uv add`, `cargo add`, `go get`, etc.). When `enforcement: block`, the agent must refuse the install and surface the violation to the user; `warn` records the violation in the phase report but proceeds; `off` skips the check entirely.",
123
+ "description": "Third-party dependency license policy. Captured at bootstrap; rendered into the repo-root `AGENTS.md` (`## Dependency licenses` section) and the `implement-plan` / `amend-plan` skill bodies so the implementer / reviewer agents check a new dep's SPDX license against `forbidden_spdx` before running any install command (`npm add`, `pnpm add`, `pip install`, `poetry add`, `uv add`, `cargo add`, `go get`, etc.). When `enforcement: block`, the agent must refuse the install and surface the violation to the user; `warn` records the violation in the phase report but proceeds; `off` skips the check entirely.",
124
124
  "type": "object",
125
125
  "required": ["enforcement", "forbidden_spdx"],
126
126
  "additionalProperties": false,
@@ -144,7 +144,7 @@ This is a load-bearing input for the rest of the bootstrap flow — surface ever
144
144
 
145
145
  **Instruction documents** — search for any of these and capture their content:
146
146
 
147
- - `AGENTS.md` (root + per-app in monorepos).
147
+ - `AGENTS.md` (root + per-app in monorepos; also the legacy `ai-tools/AGENTS.md` path).
148
148
  - `CLAUDE.md`, `.cursorrules`, `.cursor/rules/*.mdc`, `.github/copilot-instructions.md`.
149
149
  - Codex / Cursor / Copilot instructions inside vendor dirs (`.codex/AGENTS.md`, etc.).
150
150
  - Anything tagged "AI conventions" / "Coding agent guide" / "LLM rules" in CONTRIBUTING / docs.
@@ -186,7 +186,8 @@ For each agent, also classify:
186
186
 
187
187
  - `ai-tools/scripts/setup-ai-tools.mjs` (Vinta canonical multi-vendor setup).
188
188
  - `package.json#scripts.setup:ai-tools` or similar alias.
189
- - Symlinks: `AGENTS.md → ai-tools/AGENTS.md`, `.claude/skills → ../ai-tools/skills`, etc.
189
+ - Symlinks: `CLAUDE.md → AGENTS.md`, `.github/copilot-instructions.md → ../AGENTS.md`, `.claude/skills → ../ai-tools/skills`, etc.
190
+ - **Legacy layout**: a root `AGENTS.md` that is a *symlink* into `ai-tools/AGENTS.md` predates the move of the doc to the repo root. Record it — [vinta-sync-ai-tools](../vinta-sync-ai-tools/SKILL.md) migrates it.
190
191
 
191
192
  **Plan/spec docs** — flag presence (full inventory comes later from [vinta-migrate-plans-specs](../vinta-migrate-plans-specs/SKILL.md)):
192
193
 
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: vinta-bootstrap-ai-tools
3
- description: Bootstrap a project's AI-tooling layout (`ai-tools/AGENTS.md`, `ai-tools/skills/`, `ai-tools/agents/*.yaml`) plus the multi-vendor setup script that wires the canonical sources into Claude Code, Cursor, Codex, and VS Code Copilot. Project-agnostic — adapts to whatever stack it finds. Detects langs, frameworks, build tools, deploy paths, multi-tenancy patterns, and CI conventions, interviews the user for the gaps, then drafts AGENTS.md, the foundation sub-agents (implementer / reviewer / fixer), and a starter set of project-specific skills. Stack-specific skill + agent templates (Medplum, Django, …) live as resources and get copied when the detected stack matches. Use when invoked in a fresh repo that doesn't yet have an `ai-tools/` directory, or to refresh an existing one. Orchestrates several sub-skills — see "Sub-skill flow" below.
3
+ description: Bootstrap a project's AI-tooling layout (root `AGENTS.md`, `ai-tools/skills/`, `ai-tools/agents/*.yaml`) plus the multi-vendor setup script that wires the canonical sources into Claude Code, Cursor, Codex, and VS Code Copilot. Project-agnostic — adapts to whatever stack it finds. Detects langs, frameworks, build tools, deploy paths, multi-tenancy patterns, and CI conventions, interviews the user for the gaps, then drafts AGENTS.md, the foundation sub-agents (implementer / reviewer / fixer), and a starter set of project-specific skills. Stack-specific skill + agent templates (Medplum, Django, …) live as resources and get copied when the detected stack matches. Use when invoked in a fresh repo that doesn't yet have an `ai-tools/` directory, or to refresh an existing one. Orchestrates several sub-skills — see "Sub-skill flow" below.
4
4
  ---
5
5
 
6
6
  # Bootstrap AI tools
7
7
 
8
8
  This skill produces, in the target repo:
9
9
 
10
- - `ai-tools/AGENTS.md` — universal project conventions read by Claude Code, Cursor, Codex, Copilot.
10
+ - `AGENTS.md` at the **repo root** — universal project conventions read by Claude Code, Cursor, Codex, Copilot. The one AI-tooling doc that lives outside `ai-tools/`: the tools expect it at the root, and its relative links only resolve for every reader from there.
11
11
  - `ai-tools/skills/<name>/SKILL.md` — domain-specific skills (always: foundation set + stack-matched copies).
12
12
  - `ai-tools/agents/<name>.yaml` — vendor-agnostic sub-agent definitions; the setup script materializes per-vendor copies.
13
13
  - `ai-tools/scripts/setup-ai-tools.mjs` + a `pnpm setup:ai-tools` (or equivalent) script alias.
@@ -20,7 +20,7 @@ Project-agnostic. Adapts to whatever the analysis finds. Stack-specific skill +
20
20
  This orchestrator runs six sub-skills in order. Each is its own SKILL.md so it can be invoked standalone (e.g. to refresh just AGENTS.md without redoing analysis from scratch).
21
21
 
22
22
  1. [vinta-analyze-codebase](../vinta-analyze-codebase/SKILL.md) — walk the repo, build a structured inventory: languages, frameworks, build tools, test frameworks, deploy targets, monorepo shape, env model, multi-tenancy patterns, CI providers. Outputs an in-memory inventory the rest of the flow consumes.
23
- 2. [vinta-write-agents-md](../vinta-write-agents-md/SKILL.md) — synthesize `ai-tools/AGENTS.md` from the inventory + a focused interview for what the analysis can't see.
23
+ 2. [vinta-write-agents-md](../vinta-write-agents-md/SKILL.md) — synthesize the root `AGENTS.md` from the inventory + a focused interview for what the analysis can't see.
24
24
  3. [vinta-derive-subagents](../vinta-derive-subagents/SKILL.md) — author `ai-tools/agents/*.yaml`. Always emits the foundation trio (`implementer`, `reviewer`, `fixer`); adds stack-specific specialists when the user supplies a template for a matched stack.
25
25
  4. [vinta-derive-skills](../vinta-derive-skills/SKILL.md) — author `ai-tools/skills/*/SKILL.md`. Always copies the project-agnostic foundation set (`plan-feature`, `create-spec`, `create-qa-use-cases`) verbatim from its bundled resources. Generates `implement-plan` from a parameterized template using project specifics. Asks the user whether the optional `add-e2e-test` and `add-env-var` skills are needed. Asks for stack-specific templates per matched stack.
26
26
  5. [vinta-install-ai-tools-setup](../vinta-install-ai-tools-setup/SKILL.md) — copy the canonical `setup-ai-tools.mjs` into `ai-tools/scripts/`, wire the package script alias, run setup, verify all vendor paths resolve.
@@ -43,7 +43,7 @@ The bootstrap orchestrator doesn't need to invoke it directly. It just needs to
43
43
  - Repo has a partial setup and the user wants to refresh / extend it. (This orchestrator is idempotent in spirit — sub-skills read what's there before writing.)
44
44
  - Forking the Vinta conventions into a new project.
45
45
 
46
- If the repo already has `ai-tools/AGENTS.md`, **do not overwrite** without explicit confirmation. The orchestrator's first interview question covers this.
46
+ If the repo already has an `AGENTS.md` (at the root, or at the legacy `ai-tools/AGENTS.md` path), **do not overwrite** without explicit confirmation. The orchestrator's first interview question covers this.
47
47
 
48
48
  ## Interview (Step 0 — before any sub-skill runs)
49
49
 
@@ -147,8 +147,8 @@ If the user answers "No" to any of the seven, that skill won't ship. If the user
147
147
  For each artifact, read it (frontmatter + body), then ask the user via `AskUserQuestion`:
148
148
 
149
149
  - **Instruction docs** (`AGENTS.md`, `CLAUDE.md`, etc.):
150
- - `Merge into new ai-tools/AGENTS.md` — `vinta-write-agents-md` folds existing content into the canonical sections, preserving anything still accurate.
151
- - `Keep as-is, link from ai-tools/AGENTS.md` — leave the file in place, reference it.
150
+ - `Merge into new AGENTS.md` — `vinta-write-agents-md` folds existing content into the canonical sections at the repo root, preserving anything still accurate.
151
+ - `Keep as-is, link from AGENTS.md` — leave the file in place, reference it.
152
152
  - `Replace from scratch` — discard existing, draft fresh from inventory + interview.
153
153
 
154
154
  - **Skills** (each under any vendor `skills/` dir):
@@ -173,7 +173,7 @@ Scan the repo root for `DESIGN.md` (case-insensitive). Per [Design.md with Curso
173
173
 
174
174
  If present, ask via `AskUserQuestion`:
175
175
 
176
- - `Keep and wire into AI tooling` (recommended) — leave file untouched at repo root; reference it from `ai-tools/AGENTS.md`; when `cursor` is in **Scope → Vendor coverage**, write `.cursor/rules/design.mdc` (Cursor Project Rules, Option A from the linked blog post) so Cursor auto-loads it before generating UI files.
176
+ - `Keep and wire into AI tooling` (recommended) — leave file untouched at repo root; reference it from `AGENTS.md`; when `cursor` is in **Scope → Vendor coverage**, write `.cursor/rules/design.mdc` (Cursor Project Rules, Option A from the linked blog post) so Cursor auto-loads it before generating UI files.
177
177
  - `Keep as-is, don't reference` — leave the file alone; no AGENTS.md mention, no Cursor rule.
178
178
  - `Drop` — delete (rare; confirm twice before removing).
179
179
 
@@ -391,12 +391,14 @@ No skill / agent content lives in `resources/stacks/<stack>/`. That's by design
391
391
  After Step 0.5 + all six sub-skills run, the target repo has:
392
392
 
393
393
  ```
394
+ AGENTS.md ← project conventions, at the repo root as a regular
395
+ file (not under ai-tools/, not a symlink) so its
396
+ relative links resolve for every reader
394
397
  .vinta-ai-workflows.yaml ← single source of truth (Step 0.5)
395
398
  schema: schemas/vinta-ai-workflows-config.v1.schema.json
396
399
  .vinta-ai-workflows/ ← gitignored local state (only when systematic-debugging enabled)
397
400
  └── cache.yaml ← MCP preflight cache, schema: mcp-preflight-cache.v1.schema.json
398
401
  ai-tools/
399
- ├── AGENTS.md
400
402
  ├── skills/
401
403
  │ ├── plan-feature/SKILL.md ← always (copied verbatim from derive-skills resources)
402
404
  │ ├── create-spec/SKILL.md ← always (copied verbatim)
@@ -448,7 +450,7 @@ ai-plans/ ← created by migrate-plans-specs (st
448
450
  write to this layout)
449
451
  ```
450
452
 
451
- Plus the symlinks + per-vendor generated files, set up by the install step.
453
+ Plus the symlinks + per-vendor generated files, set up by the install step. The doc aliases among them (`CLAUDE.md`, `.github/copilot-instructions.md`) point at the root `AGENTS.md` and are created only for selected vendors.
452
454
 
453
455
  Foundation skills break into three buckets — see [vinta-derive-skills](../vinta-derive-skills/SKILL.md) for the full mechanics:
454
456
 
@@ -481,7 +483,7 @@ Stack-specific skills + agents land in the target only when the user provides te
481
483
 
482
484
  After all sub-skills finish:
483
485
 
484
- 1. `ls -la ai-tools/` — confirm AGENTS.md, skills/, agents/, scripts/ all exist.
486
+ 1. `ls -la AGENTS.md ai-tools/` — confirm root `AGENTS.md` is a regular file and `ai-tools/` has skills/, agents/, scripts/.
485
487
  2. `node ai-tools/scripts/setup-ai-tools.mjs` — runs cleanly, no errors.
486
488
  3. Each selected vendor's directory has the expected files: `.claude/agents/*.md`, `.cursor/agents/*.md`, `.github/agents/*.agent.md`, `.codex/agents/*.toml`.
487
489
  4. Spot-check one skill, one agent: open SKILL.md / `<agent>.yaml` and confirm content describes THIS project (not a copy-pasted template with `<placeholder>` strings).
@@ -189,7 +189,7 @@ The remaining per-shell placeholders:
189
189
  | `{{FRAMEWORK_PACK_LOADER_BLOCK}}` | derived from the packs shipped (write-unit-test only) | bullets pointing at each shipped pack, runner first then stacks: `- Runner: **pytest** → read [resources/packs/runners/pytest.md](resources/packs/runners/pytest.md).` then `- Stack: **Django** → read [resources/packs/stacks/django.md](resources/packs/stacks/django.md).` (one bullet per matched stack). When only the runner shipped, emit just its bullet plus `- No stack pack matched — apply the universal rules and mirror the nearest existing tests.` When no pack shipped at all, render that single fallback line alone. |
190
190
  | `{{PROJECT_TEST_PREFERENCES_BLOCK}}` | derived from `skills.write-unit-test.{test_style,data_setup,assertion_style,db_isolation}` (write-unit-test only) | one bullet per field whose value is **not** `framework-default`, phrased as an instruction: `test_style: classes` → `- Group tests in test classes (not standalone functions).`; `data_setup: factories` → `- Build test data with factory classes / builders, not inline literals or fixture files.`; `assertion_style: plain-assert` → `- Use plain \`assert\` (not \`self.assertEqual\`-family methods).`; `db_isolation: transaction-rollback` → `- Isolate DB state with transaction rollback (each test in a rolled-back transaction).`. Emit `- No project-specific overrides — follow the framework pack's defaults.` when all four are `framework-default`. |
191
191
  | `{{ADDITIONAL_CONVENTIONS_BLOCK}}` | derived from `skills.write-unit-test.additional_conventions[]` (write-unit-test only) | when non-empty: a `### Additional project conventions` sub-block listing each entry as a bullet, verbatim. Empty string (whole block omitted) when the array is empty. Starts with a leading blank line so it slots cleanly under the preferences block. |
192
- | `{{DEPENDENCY_LICENSE_BLOCK}}` | derived from `policies.dependency_licenses` | Top-level `## Adding new third-party dependencies` section. Empty string when `enforcement: off` or the block is absent. When `block` / `warn`: short paragraph naming the enforcement mode + the forbidden SPDX list + the pre-install check (`npm view <pkg> license`, PyPI metadata, etc.) + a one-line pointer to the **Dependency licenses** section in [AGENTS.md](AGENTS.md) for the full table including `allowed_overrides` and `notes`. See [resources/dependency-license-block.md](resources/dependency-license-block.md) for the canonical body. |
192
+ | `{{DEPENDENCY_LICENSE_BLOCK}}` | derived from `policies.dependency_licenses` | Top-level `## Adding new third-party dependencies` section. Empty string when `enforcement: off` or the block is absent. When `block` / `warn`: short paragraph naming the enforcement mode + the forbidden SPDX list + the pre-install check (`npm view <pkg> license`, PyPI metadata, etc.) + a one-line pointer to the **Dependency licenses** section in the root `AGENTS.md` — linked as `../../../AGENTS.md`, the depth from `ai-tools/skills/<name>/SKILL.md` — for the full table including `allowed_overrides` and `notes`. See [resources/dependency-license-block.md](resources/dependency-license-block.md) for the canonical body. |
193
193
  | `{{DEPENDENCY_LICENSE_LAYER1_CHECK}}` | derived from `policies.dependency_licenses` | Layer 1 review checklist item. Empty string when `enforcement: off`. When `block` / `warn`: `6. **Dependency license scan**: \`git diff package.json pyproject.toml ...\` (project-relevant manifests) — for every added dep look up its SPDX license (\`npm view <pkg> license\`, PyPI metadata, repo \`LICENSE\`). A license in \`policies.dependency_licenses.forbidden_spdx\` and not in \`allowed_overrides\` is a BLOCKER (when \`block\`) or a SHOULD-FIX (when \`warn\`). A missing / \`UNKNOWN\` / undeclared license is **always a BLOCKER** regardless of enforcement mode — there is no override to silently bless undisclosed terms.` |
194
194
  | `{{DEPENDENCY_LICENSE_RULE_LINE}}` | derived from `policies.dependency_licenses` | Important-rules bullet. Empty string when `enforcement: off`. When `block`: `- **License check before any new dep.** Refuse \`npm add\` / \`pnpm add\` / \`pip install\` / \`poetry add\` / \`uv add\` / \`cargo add\` / \`go get\` when the package's SPDX license is in the forbidden list — see AGENTS.md **Dependency licenses**. User can grant a one-off override after acknowledging the violation; record the override in \`policies.dependency_licenses.allowed_overrides\` before re-running.` When `warn`: same line but `Proceed but record the violation in the phase report` instead of `Refuse`. |
195
195
  | `{{COMMIT_STRATEGY_*}}` family (`{{COMMIT_STRATEGY_STEP0_QUESTION}}`, `{{COMMIT_STRATEGY_STEP0_TRAILER}}`, `{{COMMIT_STRATEGY_CONFIRM_NOTE}}`, `{{BRANCH_NAMING_PATTERN_SUMMARY}}`, `{{BRANCH_NAMING_BLOCK}}`, `{{PER_PHASE_COMMIT_BLOCK}}`, `{{PR_OPEN_TIMING_BLOCK}}`, `{{PRS_CONTEXT_FILE_PATH}}`, `{{TRACKING_BRANCH_FIELD}}`, `{{TRACKING_PHASE_BRANCH_FIELD}}`, `{{FINAL_REPORT_BRANCH_SUMMARY}}`, `{{BRANCH_CHECKLIST_LINE}}`, `{{COMMIT_STRATEGY_CHECKLIST_BLOCK}}`, `{{BRANCH_PUSH_HEADING}}`) | Step 0 (`policies.commit_strategy`) | Render based on the project's commit strategy (`stacked-branches` / `modular-commits` / `ask`). Multi-line blocks now come from the commit-strategy partials; see the substitution table immediately after this row. |
@@ -11,7 +11,7 @@ The rendered section sits **directly before** `## Working instructions` so subag
11
11
  ```markdown
12
12
  ## Adding new third-party dependencies
13
13
 
14
- Before running any install command (`npm add`, `pnpm add`, `yarn add`, `pip install`, `poetry add`, `uv add`, `cargo add`, `go get`, `gem install`, equivalents), check the package's SPDX license against the project's forbidden list — see the **Dependency licenses** section in [AGENTS.md](AGENTS.md) for the full list, the per-package overrides, and any project-specific notes.
14
+ Before running any install command (`npm add`, `pnpm add`, `yarn add`, `pip install`, `poetry add`, `uv add`, `cargo add`, `go get`, `gem install`, equivalents), check the package's SPDX license against the project's forbidden list — see the **Dependency licenses** section in [AGENTS.md](../../../AGENTS.md) for the full list, the per-package overrides, and any project-specific notes.
15
15
 
16
16
  Quick lookup:
17
17
 
@@ -22,7 +22,7 @@ Do not expand scope to a file just because the agent `Read` it or reviewed it in
22
22
  Rules 1-5 are usually reworded. Rules 6-12 are usually deleted.
23
23
 
24
24
  1. **Dense, multi-clause sentences** that try to explain everything at once instead of one idea per sentence. Split a sentence that adds a second thought.
25
- 2. **Negative / before-state framing** - describing removed code, or leading with "not X" when X was never the point. Keep "not X, it's Y" only for a genuine edge case or a non-obvious gotcha.
25
+ 2. **Negative / before-state framing** - describing removed code, or leading with "not X" when X was never the point. Keep "not X, it's Y" only for a genuine edge case or a non-obvious gotcha. A regression test records a bug fix; a comment does not. Keep a comment about a bug fix only when no test can cover the regression, and then say why.
26
26
  3. **AI buzzwords** in place of plain software engineering vocabulary. Common offenders and their replacements:
27
27
  - `gate` / `gates` / `gating` → "check", "decides whether to show...", "authentication check", "feature check"
28
28
  - `guard` → "check" / "prevent"
@@ -11,7 +11,7 @@ The flow is destructive (force-push). Every modification is gated on user confir
11
11
 
12
12
  ## Working assumptions
13
13
 
14
- - Repo: {{PROJECT_NAME}} ({{STACK_SUMMARY}}). Conventions: [AGENTS.md](AGENTS.md).
14
+ - Repo: {{PROJECT_NAME}} ({{STACK_SUMMARY}}). Conventions: [AGENTS.md](../../../AGENTS.md).
15
15
  - Plan files: [`{{PLAN_DIR}}/YYYY-MM-DD-FEATURE_NAME_PLAN.md`]({{PLAN_DIR}}/).
16
16
  - Lint: `{{LINT_CMD}}`. Format: `{{FORMAT_CMD}}`.
17
17
  - Type / build gate: `{{BUILD_CMD}}`{{TYPECHECK_NOTE}}.
@@ -17,7 +17,7 @@ Execution counterpart to [plan-feature](../plan-feature/SKILL.md). Plan = contra
17
17
 
18
18
  ## Working assumptions
19
19
 
20
- - Repo: {{PROJECT_NAME}} ({{STACK_SUMMARY}}). Conventions: [AGENTS.md](AGENTS.md).
20
+ - Repo: {{PROJECT_NAME}} ({{STACK_SUMMARY}}). Conventions: [AGENTS.md](../../../AGENTS.md).
21
21
  - Plan files: [`{{PLAN_DIR}}/YYYY-MM-DD-FEATURE_NAME_IMPLEMENTATION_PLAN.md`]({{PLAN_DIR}}/).
22
22
  - Lint: `{{LINT_CMD}}`. Format: `{{FORMAT_CMD}}`.
23
23
  - Type / build gate: `{{BUILD_CMD}}`{{TYPECHECK_NOTE}}.
@@ -108,6 +108,7 @@ Goal: a single hypothesis, a single failing test, no speculative changes.
108
108
  - New test file: `{{NEW_TEST_CMD_PATTERN}}`
109
109
  - Scoped suite: `{{SCOPED_TEST_PATTERN}}`
110
110
  - The test must fail today for the same reason production fails. A test that fails for a different reason is a different bug.
111
+ - If the fix came before the test, because you found the cause while patching, prove it the other way round: revert the fix, run the test, confirm it fails, then restore the fix. Write the test first when you can — a test that has never failed proves nothing.
111
112
  3. Make the smallest possible change. One variable. No drive-by refactors. No "while I'm here".
112
113
  4. Re-run the failing test. Did it go green? Re-run the scoped suite — did anything else go red?
113
114
  5. If the test stays red, do **not** stack a second change on top. Return to Phase 1 with the new evidence (the test plus what it shows) and re-state the cause.
@@ -128,6 +129,7 @@ Goal: ship the fix at the right level of abstraction with the right safety net.
128
129
  4. **Route the fix diff through the shared review gate.** Invoke [review-phase](../review-phase/SKILL.md) — the same three-layer review (mechanical checks, plan/intent-compliance walkthrough, independent reviewer subagent) + fix loop that [implement-plan](../implement-plan/SKILL.md) and [amend-plan](../amend-plan/SKILL.md) use — passing the fix diff, the one-sentence root cause + the new failing-then-passing test as the "body" to walk against, and `WORKROOT` = the current checkout. A bug fix is not done until review-phase returns clean. (When this skill runs *inside* implement-plan's inner/outer loop, the enclosing phase's review-phase already covers this — don't double-review; the standalone invocation is for bugs debugged outside a plan.)
129
130
  5. **Verify on the observability side after deploy.** The error fingerprint from Phase 0 should stop firing. If the platform supports it, mark the issue resolved in the source MCP tool so a regression re-opens it instead of creating a duplicate.
130
131
  6. **Document if the fix is non-obvious.** A comment is justified only when the *why* would surprise the next reader — a hidden invariant, a workaround for a known upstream bug, a constraint not visible from the call site. Don't narrate the change.
132
+ 7. **When no test can cover the regression**, say so in the PR and leave a comment with the reason. This is the exception to item 6.
131
133
 
132
134
  ## Stop conditions — count your attempts
133
135
 
@@ -159,13 +161,13 @@ When this skill runs inside [implement-plan](../implement-plan/SKILL.md) (the in
159
161
 
160
162
  For the review gate in Phase 4, this skill shares [review-phase](../review-phase/SKILL.md) with implement-plan and amend-plan — one review implementation across all three, so a bug fix meets the same three-layer bar as any planned phase.
161
163
 
162
- For new test scaffolding, defer to the project's test conventions captured in [AGENTS.md](../../AGENTS.md). For env / config issues uncovered in Phase 0, route to the project's `add-env-var` skill if shipped (`{{PROJECT_SKILLS_LIST}}`).
164
+ For new test scaffolding, defer to the project's test conventions captured in [AGENTS.md](../../../AGENTS.md). For env / config issues uncovered in Phase 0, route to the project's `add-env-var` skill if shipped (`{{PROJECT_SKILLS_LIST}}`).
163
165
 
164
166
  ## Verification checklist (apply before claiming a bug fixed)
165
167
 
166
168
  1. Phase 0 evidence stored or linked in the PR description (trace id / issue link / dashboard URL).
167
169
  2. Root cause stated in one sentence in the PR description.
168
- 3. New failing-then-passing test cited by file:line.
170
+ 3. New failing-then-passing test cited by file:line. It fails before the fix, and reverting the fix makes it fail again.
169
171
  4. Full local gate green: `{{LINT_CMD}}` + `{{BUILD_CMD}}` + `{{TEST_CMD}}`{{E2E_OUTER_GATE_CHECKLIST}}.
170
172
  5. [review-phase](../review-phase/SKILL.md) run on the fix diff and returned clean (unless the fix landed inside an implement-plan phase already covered by its review-phase).
171
173
  6. Observability source updated post-deploy (issue resolved / alert acknowledged) so a recurrence pages instead of silently re-opening.
@@ -18,6 +18,11 @@ Component-testing idioms with React Testing Library. Loaded **only when the proj
18
18
  - Mock HTTP at the network layer with **MSW** (see the runner pack), so components run their real fetch path. For a component that loads data, `await screen.findBy...` the settled UI rather than mocking the fetching hook.
19
19
  - Pure logic (reducers, formatters, custom hooks without UI) — test directly, no render. Hooks: `renderHook` + `act`.
20
20
 
21
+ ## Regression tests on state that settles
22
+
23
+ - When the bug is a wrong intermediate state that ends up correct, asserting the final UI passes even with the bug present. Record the value on every render instead: push it to an array inside the `renderHook` callback, or from a test-only child in a rendered tree. Then assert the whole sequence — `expect(states).toEqual(["loading", "ready"])` catches the extra skeleton that `findByText` never sees.
24
+ - `await waitFor` hides the same bug. It retries until the assertion passes, so it cannot see a state that was wrong and then corrected.
25
+
21
26
  ## Isolation
22
27
 
23
28
  - Clean up is automatic per test with the Testing Library auto-cleanup; don't share rendered state between tests. Reset mocks in `afterEach` (see runner pack).
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: write-unit-test
3
- description: Write a unit test for a function, method, class, or module in {{PROJECT_NAME}} ({{STACK_SUMMARY}}) — mock only genuine externals, assert the full output (not counts/truthiness), clean up created data or roll back the transaction, never hit a service unavailable in dev/test, keep test logic literal, stay decoupled from internals. Applies this project's test preferences and the runner + stack packs for {{FRAMEWORK_PACKS_LIST}}. Use on "add a test", "write a unit test", "cover this with tests", or when new behavior lands uncovered.
3
+ description: Write a unit test for a function, method, class, or module in {{PROJECT_NAME}} ({{STACK_SUMMARY}}) — mock only genuine externals, assert the full output (not counts/truthiness), clean up created data or roll back the transaction, never hit a service unavailable in dev/test, keep test logic literal, stay decoupled from internals. Applies this project's test preferences and the runner + stack packs for {{FRAMEWORK_PACKS_LIST}}. A bug fix gets a regression test that fails without the fix. Use on "add a test", "write a unit test", "cover this with tests", "add a regression test", or when new behavior lands uncovered.
4
4
  ---
5
5
 
6
6
  # Write a unit test
@@ -29,6 +29,16 @@ A test should fail when the behavior breaks and pass when it's correct — nothi
29
29
  8. **Kill nondeterminism** — control every entropy source (clock, randomness, timezone/locale, iteration/DB order, concurrency). Freeze/inject them; a test that passes only sometimes is worse than none.
30
30
  9. **Cover the branches, not just the happy path** — aim high (95%+ branch coverage): each error path, guard, and edge (empty, boundary, null) gets a case. Coverage is the floor, correct assertions are the point.
31
31
 
32
+ ## Regression tests
33
+
34
+ A test that exists because of a bug has a second job: fail again if the bug comes back. A comment does not fail when the code changes; a test does.
35
+
36
+ - **Check that it fails without the fix.** Write the test first and watch it fail. If the fix came first, revert the fix, run the test, confirm it fails, then restore the fix. A test that passes with and without the fix protects nothing. This goes wrong most often with timing and cache-key bugs, where the assertion can run too early to see the broken state.
37
+ - **Name it for what must not happen**, in behavior terms, so the failure line reads as a description of the bug: `keeps the rows on screen across a sort flip instead of falling back to the skeleton`, not `sorts the ids`.
38
+ - **Assert the cause too, when the visible symptom can look right while the bug is still there.** For a cache-key bug, assert that the query ran once, not only that the screen showed the right rows. Keep the assertion on what the user sees and add the one about the cause next to it. This is the only exception to rule 6.
39
+ - **Watch states that settle.** When the bug is a wrong intermediate state that ends up correct, asserting the final state passes even with the bug present. Record the value on every update and assert the whole sequence instead. The stack pack shows how to do this for this project's framework.
40
+ - **When no test can cover the regression**, or the test would cost far more than it protects, say so in the PR and leave a comment with the reason. This is the only case where a comment stands in for a test.
41
+
32
42
  ## Project preferences
33
43
 
34
44
  Apply these; `framework-default` defers to the pack. If the existing suite contradicts a preference, follow the preference for new tests and flag it — don't rewrite old tests.
@@ -5,7 +5,7 @@ description: Install the multi-vendor `setup-ai-tools.mjs` script in the target
5
5
 
6
6
  # Install ai-tools setup
7
7
 
8
- The setup script reads `ai-tools/AGENTS.md`, `ai-tools/skills/`, and `ai-tools/agents/*.yaml` (canonical sources) and emits per-vendor copies + symlinks. This skill installs the script + its package wiring + runs it for the first time.
8
+ The setup script reads root `AGENTS.md`, `ai-tools/skills/`, and `ai-tools/agents/*.yaml` (canonical sources) and emits per-vendor copies + symlinks. This skill installs the script + its package wiring + runs it for the first time.
9
9
 
10
10
  ## Output
11
11
 
@@ -16,7 +16,7 @@ The setup script reads `ai-tools/AGENTS.md`, `ai-tools/skills/`, and `ai-tools/a
16
16
 
17
17
  ## Inputs
18
18
 
19
- - `ai-tools/AGENTS.md` (from [vinta-write-agents-md](../vinta-write-agents-md/SKILL.md))
19
+ - `AGENTS.md` at the repo root (from [vinta-write-agents-md](../vinta-write-agents-md/SKILL.md))
20
20
  - `ai-tools/agents/*.yaml` (from [vinta-derive-subagents](../vinta-derive-subagents/SKILL.md))
21
21
  - `ai-tools/skills/<name>/SKILL.md` (from [vinta-derive-skills](../vinta-derive-skills/SKILL.md))
22
22
  - Step 0 vendor selection from the [vinta-bootstrap-ai-tools](../vinta-bootstrap-ai-tools/SKILL.md#a-scope) **Scope → Vendor coverage** answer.
@@ -25,7 +25,9 @@ The setup script reads `ai-tools/AGENTS.md`, `ai-tools/skills/`, and `ai-tools/a
25
25
 
26
26
  ### 1. Prerequisite check
27
27
 
28
- `ai-tools/AGENTS.md` exists. `ai-tools/agents/*.yaml` has at least the foundation trio (`implementer`, `reviewer`, `fixer`). `ai-tools/skills/` is non-empty.
28
+ `AGENTS.md` exists at the repo root — as a **regular file**, not a symlink. `ai-tools/agents/*.yaml` has at least the foundation trio (`implementer`, `reviewer`, `fixer`). `ai-tools/skills/` is non-empty.
29
+
30
+ If the project predates the move to the root (a root `AGENTS.md` symlink pointing at `ai-tools/AGENTS.md`), migrate before running the script — `git mv ai-tools/AGENTS.md AGENTS.md` after deleting the symlink, then fix the relative links inside the doc and the links pointing at it from `ai-tools/**`. [vinta-sync-ai-tools](../vinta-sync-ai-tools/SKILL.md) does this as a guided migration.
29
31
 
30
32
  If any are missing → stop. Route the user back to the relevant sub-skill ([vinta-write-agents-md](../vinta-write-agents-md/SKILL.md), [vinta-derive-subagents](../vinta-derive-subagents/SKILL.md), [vinta-derive-skills](../vinta-derive-skills/SKILL.md)).
31
33
 
@@ -92,7 +94,7 @@ Expected output: list of symlinks ensured, list of generated per-vendor files, l
92
94
 
93
95
  For each vendor in the user's selection:
94
96
 
95
- - **Claude Code**: `ls .claude/agents/` lists every agent's `.md`. `ls .claude/skills/` (or `ls -L`) lists every skill dir. `cat AGENTS.md` resolves to the canonical content.
97
+ - **Claude Code**: `ls .claude/agents/` lists every agent's `.md`. `ls .claude/skills/` (or `ls -L`) lists every skill dir. `cat CLAUDE.md` resolves to the root `AGENTS.md` content.
96
98
  - **Cursor**: `ls .cursor/agents/` + `ls .cursor/skills/`. Open Cursor and check that the agent + skill panels surface them.
97
99
  - **VS Code Copilot**: `ls .github/agents/` lists `*.agent.md`. `ls .github/skills/` lists skill dirs. `cat .github/copilot-instructions.md` resolves to the canonical AGENTS.md.
98
100
  - **Codex**: `ls .codex/agents/` lists `*.toml`. Each TOML opens cleanly + has `name` / `description` / `sandbox_mode` / `developer_instructions` fields.
@@ -105,8 +107,8 @@ Stage:
105
107
  - `ai-tools/` (canonical sources + script)
106
108
  - `package.json` (script alias + yaml dep)
107
109
  - `pnpm-lock.yaml` / `package-lock.json` / `yarn.lock` (after install)
108
- - `AGENTS.md` (root symlink)
109
- - `.claude/`, `.cursor/`, `.github/agents/`, `.github/skills/`, `.github/copilot-instructions.md`, `.codex/agents/`, `.agents/skills/` — whichever vendors selected
110
+ - `AGENTS.md` (regular file at the repo root)
111
+ - `CLAUDE.md`, `.claude/`, `.cursor/`, `.github/agents/`, `.github/skills/`, `.github/copilot-instructions.md`, `.codex/agents/`, `.agents/skills/` — whichever vendors selected
110
112
 
111
113
  Commit message style matches the project's convention captured during the bootstrap interview's **Project conventions** group. Default: `Bootstrap ai-tools layout (AGENTS.md, skills, sub-agents, vendor wiring)`.
112
114
 
@@ -117,7 +119,7 @@ The setup script is **selective by vendor** but **destructive within a vendor**:
117
119
  - For vendors in `--only` (or all four by default): the script `rm -rf` the target dir and regenerates. Hand-edits to per-vendor files are lost.
118
120
  - For vendors NOT in `--only`: the script leaves their files untouched.
119
121
 
120
- **Always edit the canonical sources** (`ai-tools/agents/*.yaml`, `ai-tools/skills/`, `ai-tools/AGENTS.md`). Generated vendor files are derived; they get overwritten on the next run.
122
+ **Always edit the canonical sources** (`ai-tools/agents/*.yaml`, `ai-tools/skills/`, root `AGENTS.md`). Generated vendor files are derived; they get overwritten on the next run.
121
123
 
122
124
  ## Pitfalls
123
125
 
@@ -134,5 +136,5 @@ Final smoke test:
134
136
  1. `pnpm setup:ai-tools` runs cleanly, no errors.
135
137
  2. Spot-check one agent file in each selected vendor — content matches the canonical YAML.
136
138
  3. Spot-check one skill — the skill description shows up in the vendor's UI (Claude `/help`, Cursor agent panel, Copilot chat slash menu, Codex slash menu).
137
- 4. Edit `ai-tools/AGENTS.md` (add a comment line). Confirm the change appears in `cat AGENTS.md` (root symlink) + each selected vendor's instruction-file path.
139
+ 4. Edit `AGENTS.md` (add a comment line). Confirm the change appears at each selected vendor's instruction-file path (`CLAUDE.md`, `.github/copilot-instructions.md`).
138
140
  5. Edit `ai-tools/agents/implementer.yaml` (e.g. tweak description). Re-run setup. Confirm changes propagate to all selected vendor agent files.
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- // Setup script: wire ai-tools/{AGENTS.md,skills,agents/*.yaml} into every supported vendor.
2
+ // Setup script: wire AGENTS.md + ai-tools/{skills,agents/*.yaml} into every supported vendor.
3
3
  //
4
4
  // Source of truth:
5
- // - ai-tools/AGENTS.md — universal project conventions (markdown).
5
+ // - AGENTS.md — universal project conventions (markdown). Lives at the
6
+ // repo root so its relative links resolve for every reader.
6
7
  // - ai-tools/skills/<name>/ — universal skills (SKILL.md format converges across
7
8
  // Claude Code, Cursor, Codex, VS Code Copilot — symlinked).
8
9
  // - ai-tools/agents/<name>.yaml — vendor-agnostic sub-agent definitions. This script
@@ -10,7 +11,7 @@
10
11
  // format each tool expects (markdown, .agent.md, .toml).
11
12
  //
12
13
  // What this script does:
13
- // 1. Symlinks ai-tools/AGENTS.md + ai-tools/skills/ into each vendor's expected paths.
14
+ // 1. Symlinks AGENTS.md + ai-tools/skills/ into each vendor's expected paths.
14
15
  // 2. Reads each ai-tools/agents/<name>.yaml.
15
16
  // 3. For every supported vendor, generates a target file with vendor-specific frontmatter
16
17
  // / format. Vendor-specific overrides come from the YAML's `overrides:<vendor>:` block;
@@ -122,6 +123,12 @@ const DEFAULTS = {
122
123
  // ── Helpers ──────────────────────────────────────────────────────────────
123
124
 
124
125
  function ensureSymlink(target, linkPath) {
126
+ // `target` is relative to the link's own directory, so resolve it from there.
127
+ // A missing target would produce a broken link that reads as empty to an agent.
128
+ const resolved = join(dirname(linkPath), target);
129
+ if (!existsSync(resolved)) {
130
+ throw new Error(`[setup-ai-tools] cannot link ${linkPath} → ${target}: ${resolved} does not exist`);
131
+ }
125
132
  mkdirSync(dirname(linkPath), { recursive: true });
126
133
  let existing;
127
134
  try {
@@ -285,21 +292,27 @@ function emitCodex(name, doc, body) {
285
292
 
286
293
  const gitignoreAdded = ensureGitignoreEntries(['.vinta-ai-workflows/']);
287
294
 
288
- // ── Stage 1: stable symlinks (skills + AGENTS.md) ────────────────────────
295
+ // ── Stage 1: stable symlinks (skills + vendor doc aliases) ───────────────
289
296
 
290
- // Universal anchors — always set up regardless of --only selection.
291
- ensureSymlink('ai-tools/AGENTS.md', 'AGENTS.md');
297
+ // AGENTS.md is a real file at the repo root. Each vendor that reads it under a
298
+ // different filename gets a symlink below.
299
+ //
300
+ // Skills are symlinked into .agents/, .claude/, .cursor/, and .github/, so a
301
+ // SKILL.md always sits at <top-level-dir>/skills/<name>/SKILL.md — three levels
302
+ // below the root through every path. That is why SKILL.md files link the doc as
303
+ // `../../../AGENTS.md`. Keep new skills dirs at <top-level-dir>/skills.
292
304
  ensureSymlink('../ai-tools/skills', '.agents/skills');
293
305
 
294
306
  // Vendor-specific symlinks — gated on SELECTED.
295
307
  if (SELECTED.has('claude')) {
308
+ ensureSymlink('AGENTS.md', 'CLAUDE.md');
296
309
  ensureSymlink('../ai-tools/skills', '.claude/skills');
297
310
  }
298
311
  if (SELECTED.has('cursor')) {
299
312
  ensureSymlink('../ai-tools/skills', '.cursor/skills');
300
313
  }
301
314
  if (SELECTED.has('copilot')) {
302
- ensureSymlink('../ai-tools/AGENTS.md', '.github/copilot-instructions.md');
315
+ ensureSymlink('../AGENTS.md', '.github/copilot-instructions.md');
303
316
  ensureSymlink('../ai-tools/skills', '.github/skills');
304
317
  }
305
318
  // Codex shares `.agents/skills` (already linked above) — no extra symlink needed.
@@ -367,9 +380,9 @@ if (gitignoreAdded.length) {
367
380
  }
368
381
 
369
382
  console.log('Symlinks ensured:');
370
- console.log(' AGENTS.md → ai-tools/AGENTS.md');
371
383
  console.log(' .agents/skills → ../ai-tools/skills');
372
384
  if (SELECTED.has('claude')) {
385
+ console.log(' CLAUDE.md → AGENTS.md');
373
386
  console.log(' .claude/skills → ../ai-tools/skills');
374
387
  }
375
388
  if (SELECTED.has('cursor')) {
@@ -377,7 +390,7 @@ if (SELECTED.has('cursor')) {
377
390
  }
378
391
  if (SELECTED.has('copilot')) {
379
392
  console.log(' .github/skills → ../ai-tools/skills');
380
- console.log(' .github/copilot-instructions.md → ../ai-tools/AGENTS.md');
393
+ console.log(' .github/copilot-instructions.md → ../AGENTS.md');
381
394
  }
382
395
  console.log('');
383
396
 
@@ -58,6 +58,7 @@ For each change record, decide its bucket:
58
58
  | `config-schema-change` | Adds a field to the config schema or bumps its major. Migrate the config; ask user to fill new required fields. |
59
59
  | `not-applicable` | Project opted out (`disabled` in config) or never applicable (e.g. stack-specific change for a stack the project doesn't use). |
60
60
  | `tooling` | Setup-script / gitignore / packaging changes that propagate transparently — no per-change question; apply if approved as a batch. |
61
+ | `layout-migration` | The project's on-disk layout has to move before the new surface works. Never batched — one question, with the file moves listed. See "One-time migrations" below. |
61
62
 
62
63
  Cross-check via file diff: every `affects-project` change should correspond to a real file delta between `OLD_VERSION`'s source (reachable via git tag in the clone) and `NEW_VERSION`'s source. Diffs without changelog entries → flag as orphan (see step 2).
63
64
 
@@ -126,9 +127,11 @@ In order:
126
127
 
127
128
  3. **`affects-project` accepts** — re-render the relevant template (for `implement-plan` / `amend-plan`) or re-copy the foundation skill body (for verbatim ones — delegated to [vinta-update-project-skills](../vinta-update-project-skills/SKILL.md)). Use the values in `.vinta-ai-workflows.yaml` as the substitution source. Validate every `{{PLACEHOLDER}}` is resolved.
128
129
 
129
- 4. **`tooling` accepts** — run setup-ai-tools.mjs from the new package version in idempotent mode. The script handles gitignore append, vendor symlinks, vendor agent file regeneration.
130
+ 4. **`layout-migration` accepts** — run the migration from "One-time migrations" below **before** the setup script, so the script wires the vendors against the final layout.
130
131
 
131
- 5. Re-validate every YAML file under the project against its schema (`.vinta-ai-workflows.yaml`, `ai-tools/agents/*.yaml`, any `.vinta-ai-workflows/prs-context/**/*.md` frontmatter blocks). Surface validation failures; don't silently proceed.
132
+ 5. **`tooling` accepts** — run setup-ai-tools.mjs from the new package version in idempotent mode. The script handles gitignore append, vendor symlinks, vendor agent file regeneration.
133
+
134
+ 6. Re-validate every YAML file under the project against its schema (`.vinta-ai-workflows.yaml`, `ai-tools/agents/*.yaml`, any `.vinta-ai-workflows/prs-context/**/*.md` frontmatter blocks). Surface validation failures; don't silently proceed.
132
135
 
133
136
  ### 7. Bump version + final report
134
137
 
@@ -165,6 +168,40 @@ Run on first sync against a project without `.vinta-ai-workflows.yaml`:
165
168
 
166
169
  Then re-enter step 1 of the main flow.
167
170
 
171
+ ## One-time migrations
172
+
173
+ ### `ai-tools/AGENTS.md` → root `AGENTS.md` (0.6.0)
174
+
175
+ **Detect**: root `AGENTS.md` is a symlink (`test -L AGENTS.md`) and `ai-tools/AGENTS.md` exists. A project bootstrapped on 0.6.0 or later already has the doc at the root as a regular file — skip.
176
+
177
+ **Why**: the doc was read from the root through the symlink, so its links were written as repo-root paths. That broke them whenever the real file was opened at `ai-tools/AGENTS.md` — on GitHub, in a file tree, in any editor that follows the real path. Moving the file to the root makes one set of links correct everywhere.
178
+
179
+ **Steps** — ask once via `AskUserQuestion` (`Apply` / `Skip` / `Show the file list`), then:
180
+
181
+ 1. `rm AGENTS.md` (the symlink), `git mv ai-tools/AGENTS.md AGENTS.md`. Use `git mv` so the file's history follows it.
182
+ 2. Fix links **inside** the doc. They were already repo-root-relative, so most need no change — but verify every one from the root:
183
+
184
+ ```bash
185
+ grep -noE '\]\(([^)]+)\)' AGENTS.md | sed -E 's/\]\(//; s/\)$//' | while IFS=: read -r line target; do
186
+ path="${target%%#*}"
187
+ case "$path" in http*|"") continue ;; esac
188
+ [ -e "$path" ] || echo "BROKEN line $line: $target"
189
+ done
190
+ ```
191
+
192
+ 3. Fix links **pointing at** the doc from inside `ai-tools/`. Every `ai-tools/skills/<name>/SKILL.md` sits three levels below the root, so the link is `../../../AGENTS.md`. Files at other depths (e.g. `ai-tools/rules/*.md`, two levels down) need `../../AGENTS.md`. Bare `[AGENTS.md](AGENTS.md)` links inside a skill folder were always broken — they resolved to a non-existent sibling — and get the same fix.
193
+ 4. Rewrite prose references to the old path in the canonical sources: `ai-tools/agents/*.yaml`, `ai-tools/skills/**/SKILL.md`, any `ai-tools/rules/*.md`. `ai-tools/AGENTS.md` → `AGENTS.md`.
194
+ 5. Re-run the setup script so the vendor aliases repoint: `CLAUDE.md → AGENTS.md` (claude), `.github/copilot-instructions.md → ../AGENTS.md` (copilot). The generated vendor agent files under `.claude/agents/`, `.cursor/agents/`, `.github/agents/`, `.codex/agents/` are regenerated from the YAMLs in step 4 — don't hand-edit them.
195
+ 6. Confirm nothing live still points at the old path:
196
+
197
+ ```bash
198
+ grep -rn "ai-tools/AGENTS.md" --exclude-dir=node_modules --exclude-dir=.git .
199
+ ```
200
+
201
+ Historical records (`ai-plans/*.md`, changelogs, past plan docs) are dated snapshots — leave them. Surface the list to the user and let them decide.
202
+
203
+ **Order matters**: run this migration *before* `setup-ai-tools.mjs`. Running the new script against the old layout leaves `.github/copilot-instructions.md` pointing at `../AGENTS.md`, which resolves through the surviving root symlink — it works, but hides the fact that the migration hasn't happened.
204
+
168
205
  ## Pitfalls
169
206
 
170
207
  - **Hand-edited rendered skill bodies.** `vinta-sync-ai-tools` re-renders templates from the config — any inline edits the user made to the rendered `ai-tools/skills/<name>/SKILL.md` will be overwritten on `Apply`. Surface a warning when re-rendering would clobber a body the user touched (heuristic: file modified after the bootstrap commit). Default action: ask before overwriting.
@@ -173,6 +210,7 @@ Then re-enter step 1 of the main flow.
173
210
  - **Orphan diffs.** Surfaced, never auto-applied. Ask the maintainer to add a changelog entry; don't try to interpret an undocumented change.
174
211
  - **Breaking schema bump (`schema_version: N → N+1`).** Migration code must come from the package's release notes. If migration data is missing, stop — don't guess.
175
212
  - **Sticky opt-outs.** A user who accepted `add-env-var: disabled` once stays disabled across sync runs. To re-offer, manually flip to `enabled` in `.vinta-ai-workflows.yaml` and re-run sync.
213
+ - **A `layout-migration` skipped but the tooling batch applied.** The setup script runs fine against the pre-migration layout, so nothing errors — the project just sits half-migrated, with skill links still written for the old depth. Record the skip in the final report and re-offer the migration on the next sync (unlike a skipped foundation skill, a layout migration is not sticky).
176
214
 
177
215
  ## Verification
178
216
 
@@ -182,3 +220,4 @@ Then re-enter step 1 of the main flow.
182
220
  4. **Schema bump**: new field added with default; `schema_version` bumped if major changed; payload still validates.
183
221
  5. **Bootstrap from missing config**: `.vinta-ai-workflows.yaml` written with reverse-extracted values; user confirms before main flow runs.
184
222
  6. **Orphan diff**: changelog has no entry for a real file change; sync surfaces it at the end without applying.
223
+ 7. **Layout migration applied**: `AGENTS.md` is a regular file at the root, `ai-tools/AGENTS.md` is gone, `grep -rn "ai-tools/AGENTS.md"` returns only historical records, and both link checks in "One-time migrations" print nothing.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vinta-write-agents-md
3
- description: Synthesize `ai-tools/AGENTS.md` from the inventory produced by [vinta-analyze-codebase](../vinta-analyze-codebase/SKILL.md) plus a focused interview for what the analysis can't see. Used by [vinta-bootstrap-ai-tools](../vinta-bootstrap-ai-tools/SKILL.md) as step 2; also runnable standalone to refresh AGENTS.md after a major refactor. Project-agnostic — sections are the same shape across stacks, content adapts. Symlinked into root `AGENTS.md` so Claude Code, Cursor, Codex, VS Code Copilot all read it natively.
3
+ description: Synthesize the repo-root `AGENTS.md` from the inventory produced by [vinta-analyze-codebase](../vinta-analyze-codebase/SKILL.md) plus a focused interview for what the analysis can't see. Used by [vinta-bootstrap-ai-tools](../vinta-bootstrap-ai-tools/SKILL.md) as step 2; also runnable standalone to refresh AGENTS.md after a major refactor. Project-agnostic — sections are the same shape across stacks, content adapts. Lives at the repo root so Claude Code, Cursor, Codex, VS Code Copilot all read it natively and its relative links resolve for every reader.
4
4
  ---
5
5
 
6
6
  # Write AGENTS.md
@@ -9,7 +9,11 @@ The canonical project-conventions document. Read by every AI tool. Single source
9
9
 
10
10
  ## Output
11
11
 
12
- `ai-tools/AGENTS.md`. The setup script symlinks `AGENTS.md` at root and `.github/copilot-instructions.md` to it.
12
+ `AGENTS.md` at the **repo root**, as a regular file — not inside `ai-tools/`, not a symlink. Every other AI-tooling artifact lives under `ai-tools/`; this one doc is the exception, because the tools that read it expect it at the root and because its relative links must resolve the same way whether an agent reads it from the root or a human opens it on GitHub.
13
+
14
+ The setup script symlinks the per-vendor aliases to it: `CLAUDE.md` (when `claude` is a selected vendor) and `.github/copilot-instructions.md` (when `copilot` is).
15
+
16
+ Write the doc's internal links as **repo-root-relative paths** (`ai-tools/rules/testing.md`, `docs/ARCHITECTURE.md`) — that is what resolves from the root. Skills that link *back* to it from `ai-tools/skills/<name>/SKILL.md` use `../../../AGENTS.md`.
13
17
 
14
18
  ## Inputs
15
19
 
@@ -197,7 +201,7 @@ VAR_TWO
197
201
 
198
202
  ## Examples to study before writing
199
203
 
200
- If the source repo (the one defining `vinta-bootstrap-ai-tools`) has a real `AGENTS.md`, read it first as a worked example. Vinta's lives at `ai-tools/AGENTS.md` in the bootstrap-defining repo. Note its sectioning + density before writing the target's.
204
+ If the source repo (the one defining `vinta-bootstrap-ai-tools`) has a real `AGENTS.md`, read it first as a worked example. Vinta's lives at the root of the bootstrap-defining repo. Note its sectioning + density before writing the target's.
201
205
 
202
206
  ## Verification
203
207
 
@@ -207,7 +211,17 @@ After writing:
207
211
  2. Every claim cites a file or convention source.
208
212
  3. Section headings match the skeleton above (or skipped sections are explicit).
209
213
  4. Length: 200–400 lines for a typical project. Shorter = probably under-specified. Longer = probably duplicating docs/ content.
210
- 5. `pnpm setup:ai-tools` (or whichever vendors selected) re-runs cleanly after the file is in place — confirms the symlinks point at it.
214
+ 5. Every relative link in the doc resolves **from the repo root** — the script that checks it, run from the root, prints nothing:
215
+
216
+ ```bash
217
+ grep -noE '\]\(([^)]+)\)' AGENTS.md | sed -E 's/\]\(//; s/\)$//' | while IFS=: read -r line target; do
218
+ path="${target%%#*}"
219
+ case "$path" in http*|"") continue ;; esac
220
+ [ -e "$path" ] || echo "BROKEN line $line: $target"
221
+ done
222
+ ```
223
+
224
+ 6. `pnpm setup:ai-tools` (or whichever vendors selected) re-runs cleanly after the file is in place — confirms the vendor aliases point at it. The script throws if `AGENTS.md` is missing from the root.
211
225
 
212
226
  ## Pitfalls
213
227