vinta-ai-workflows 0.3.0 → 0.5.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.
Files changed (29) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/README.md +24 -1
  3. package/package.json +1 -1
  4. package/schemas/vinta-ai-workflows-config.v1.schema.json +43 -0
  5. package/skills/vinta-analyze-codebase/SKILL.md +15 -1
  6. package/skills/vinta-bootstrap-ai-tools/SKILL.md +40 -11
  7. package/skills/vinta-derive-skills/SKILL.md +46 -5
  8. package/skills/vinta-derive-skills/resources/foundation-skills/deslop-comments/SKILL.md +147 -21
  9. package/skills/vinta-derive-skills/resources/foundation-skills/plan-feature/resources/ai-models.yaml +0 -2
  10. package/skills/vinta-derive-skills/resources/plan-execution/partials/pr-context.md +5 -3
  11. package/skills/vinta-derive-skills/resources/plan-execution/shell/amend-plan-template.md +2 -0
  12. package/skills/vinta-derive-skills/resources/prs-context-template.md +25 -0
  13. package/skills/vinta-derive-skills/resources/write-unit-test-packs/runners/jest.md +49 -0
  14. package/skills/vinta-derive-skills/resources/write-unit-test-packs/runners/pytest.md +36 -0
  15. package/skills/vinta-derive-skills/resources/write-unit-test-packs/runners/vitest.md +43 -0
  16. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/django.md +36 -0
  17. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/fastapi.md +34 -0
  18. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/flask.md +31 -0
  19. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/medplum-test.globalSetup.ts +54 -0
  20. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/medplum-test.setup.ts +39 -0
  21. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/medplum.md +47 -0
  22. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/nextjs.md +27 -0
  23. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/prisma.md +31 -0
  24. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/python-package.md +28 -0
  25. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/react-router.md +25 -0
  26. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/react.md +29 -0
  27. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/tanstack-start.md +32 -0
  28. package/skills/vinta-derive-skills/resources/write-unit-test-packs/stacks/typescript-package.md +32 -0
  29. package/skills/vinta-derive-skills/resources/write-unit-test-template.md +44 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,135 @@ 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.5.0] — 2026-08-05
9
+
10
+ ### Changed
11
+
12
+ - **PR context is now written in Simple English.** `integrate-phase` runs
13
+ `deslop-comments` over the prs-context file it just wrote, before `open-pr.sh`
14
+ publishes it, so the PR title, the description body, and every inline comment
15
+ read as Simple English — one idea per sentence, current behavior stated
16
+ directly, no AI-slop vocabulary. The project's PR-template structure survives
17
+ the pass untouched. `amend-plan` runs the same pass whenever it rewrites a
18
+ prs-context file after a force-push. New step 5 in
19
+ [`partials/pr-context.md`](skills/vinta-derive-skills/resources/plan-execution/partials/pr-context.md)
20
+ (the old steps 5–7 shift to 6–8); the writing rules are also stated up front in
21
+ [`prs-context-template.md`](skills/vinta-derive-skills/resources/prs-context-template.md)
22
+ so the first draft comes out clean. No config change — `deslop-comments`
23
+ already always ships.
24
+ - **`deslop-comments` now deletes comments, not just rewrites them.** The pass
25
+ used to reword whatever it found; deleting a comment is now a normal outcome.
26
+ The "What counts as slop" list splits accordingly — rules 1–5 are rewordings,
27
+ rules 6–12 are usually deletions — and seven new rules cover the comments that
28
+ should not exist at any length: **where-used lists** (call-site inventories go
29
+ stale the first time someone adds a caller), **another module's internals
30
+ explained from here**, **one decision explained in several places** (write the
31
+ reason once at the site that makes the decision), **restating the code**, **a
32
+ comment standing in for a name** (report the rename, don't edit the comment),
33
+ **plans and deferred work** ("temporary until Y"), and **paths not taken**
34
+ ("tried X, settled on Y"). A new **essay-voice** rule bans sayings, metaphor,
35
+ and "it looks like X but is really Y" framing, and the buzzword table grew by
36
+ roughly a dozen entries (`gloss`, `roll-call`, `signal`, `the tell`, `unpick`,
37
+ `papering over`, `strictly worse`, `earns its place`, …).
38
+
39
+ Process gained four steps: check comments next to changed code for claims that
40
+ are no longer true (nothing else catches a stale comment — no test, no type
41
+ error, no lint rule), decide delete-or-reword before editing, collect
42
+ duplicates across the whole file set before editing any of it, and grep the
43
+ result for the skill's own buzzword list, since those words get written and
44
+ then skimmed past. The summary now reports comment counts before and after,
45
+ plus any comment found to be factually wrong.
46
+
47
+ A new **"When the fix is not a comment edit"** section covers the case where
48
+ the real fix is a test, a rename, or a function split: leave the comment,
49
+ report it with the file and what must exist first. "What to leave alone" gains
50
+ a matching guard — keep the reason a non-obvious line exists, and shorten the
51
+ wording instead. This pass removes noise, not knowledge.
52
+
53
+ ## [0.4.0] — 2026-07-31
54
+
55
+ ### Added
56
+
57
+ - **`write-unit-test` foundation skill (template-rendered, default-on when a
58
+ unit-test framework is detected).** A new foundation skill that ships into
59
+ target projects' `ai-tools/skills/write-unit-test/` to write durable unit
60
+ tests. The body enforces six universal rules regardless of framework — mock
61
+ only genuine externals (never the unit under test / DB / local collaborators),
62
+ assert the **full** expected output (not counts or truthiness), clean up
63
+ created data or run inside a rolled-back transaction, never hit an external
64
+ service that can't run in dev/test, keep test logic literal (no loops /
65
+ conditionals recomputing the expectation), and decouple from implementation
66
+ internals — plus a self-review checklist and a "run it green, then break it to
67
+ see it go red" step.
68
+ - Rendered from
69
+ [`skills/vinta-derive-skills/resources/write-unit-test-template.md`](skills/vinta-derive-skills/resources/write-unit-test-template.md)
70
+ (bucket B). New placeholders: `{{FRAMEWORK_PACKS_LIST}}`,
71
+ `{{FRAMEWORK_PACK_LOADER_BLOCK}}`, `{{PROJECT_TEST_PREFERENCES_BLOCK}}`,
72
+ `{{ADDITIONAL_CONVENTIONS_BLOCK}}` (reuses the existing `{{TEST_CMD}}` /
73
+ `{{NEW_TEST_CMD_PATTERN}}` / `{{SCOPED_TEST_NOTE}}` / `{{PROJECT_NAME}}` /
74
+ `{{STACK_SUMMARY}}` set). All four new placeholders are documented in the
75
+ `vinta-derive-skills` placeholder table.
76
+ - **Best-practice packs on two independent axes** (the framework-specific
77
+ "sub-skills"), verbatim references under
78
+ [`skills/vinta-derive-skills/resources/write-unit-test-packs/`](skills/vinta-derive-skills/resources/write-unit-test-packs/).
79
+ `derive-skills` copies only what the project matches into
80
+ `ai-tools/skills/write-unit-test/resources/packs/` so framework-specific
81
+ advice never ships to a project that doesn't use that framework (a FastAPI
82
+ repo never receives Django guidance; a pure library gets no web-framework
83
+ pack):
84
+ - **Runner packs** (`packs/runners/`, stack-agnostic — structure /
85
+ assertions / mocking): `pytest.md`, `vitest.md`, `jest.md`. One selected
86
+ by `inventory.tests.unit_framework`.
87
+ - **Stack packs** (`packs/stacks/`, DB isolation / framework client / domain
88
+ mocks): `django.md`, `fastapi.md`, `flask.md`, `medplum.md`, `react.md`,
89
+ `nextjs.md`, `tanstack-start.md`, `react-router.md`, `prisma.md`,
90
+ `python-package.md`, `typescript-package.md`. Every pack whose stack the inventory matched
91
+ ships (a project can match several — e.g. Next.js + React, or TanStack
92
+ Start + React); a repo with no web framework (a standalone TS/Python package like
93
+ an open-source lib) gets its `*-package.md` and no more; when nothing
94
+ matches, the runner pack ships alone. Missing packs for a present
95
+ framework are recorded as a gap (candidate for a new pack via
96
+ `add-foundation-skill`).
97
+ - **Stack packs may carry a one-time project-setup step**, applied at derive
98
+ time (not per test). Medplum ships the two Vitest setup files
99
+ `medplum-test.globalSetup.ts` + `medplum-test.setup.ts` (FHIR search-parameter
100
+ indexing shared across workers via `provide`/`inject`, so `MockClient`
101
+ search filtering works) — the derive flow installs them and registers both
102
+ `test.globalSetup` + `test.setupFiles` in the Vitest config; the `medplum.md`
103
+ pack only references it. Bundled verbatim (dated headers) from the team's
104
+ `medplum-snippet-catalog` (`TestSetup/`) so bootstrap stays offline-safe.
105
+ - **Schema:** new `foundation_skills.write-unit-test` enum
106
+ (`enabled` / `disabled`) and a new `skills.write-unit-test` config block —
107
+ `test_style` (functions / classes / framework-default), `data_setup`
108
+ (factories / fixtures / inline / framework-default), `assertion_style`
109
+ (plain-assert / framework-methods / framework-default), `db_isolation`
110
+ (transaction-rollback / truncate / recreate / framework-default), and a
111
+ free-form `additional_conventions[]`. All optional; every field defaults to
112
+ `framework-default`.
113
+ - **Bootstrap:** `write-unit-test` is **default-on, not an opt-in** — the
114
+ interview sets `foundation_skills.write-unit-test: enabled` whenever a
115
+ unit-test framework was detected, `disabled` otherwise. New Project-conventions
116
+ interview item **C.9 (Test conventions for `write-unit-test`)** captures the
117
+ four preferences + extras, pre-filled from inference on existing projects.
118
+ (The former C.9 "Agent model tiers" is now **C.10**; its Step 0.5 references
119
+ were updated.) Step 0.5 emits the `skills.write-unit-test` block; the outputs
120
+ tree, the three-buckets paragraph, and the `Replace with Vinta foundation
121
+ version` foundation-shape list all now list `write-unit-test`.
122
+ - **Inference:** `vinta-analyze-codebase` §6 (Tests) now reads a sample of the
123
+ existing unit tests and reports a `tests.conventions` block
124
+ (`test_style` / `data_setup` / `assertion_style` / `db_isolation` / `extras`,
125
+ each `framework-default` when mixed / greenfield) that seeds the C.9 defaults.
126
+ - **Review-driven refinements** (PR #21): two universal rules added — **kill
127
+ nondeterminism** (clock / randomness / timezone / ordering / concurrency → no
128
+ flaky tests) and **cover the branches** (aim 95%+ branch coverage); the
129
+ full-value assertion rule now carves out **brittle human-facing text** (assert
130
+ a stable anchor — error code, role, i18n key — for error/UI strings, not exact
131
+ prose). Runner packs gained a **record/replay cassette** note for complex APIs
132
+ like LLM endpoints (PollyJS / `nock.back` for JS, `vcrpy` / `betamax` for
133
+ Python). The Medplum pack gained a **`MockClient` limitations** section
134
+ (pre-seeded example resources skew unfiltered searches, partial search-param
135
+ support, server-only behavior not enforced — defer those to integration/e2e).
136
+
8
137
  ## [0.3.0] — 2026-07-17
9
138
 
10
139
  ### Fixed
package/README.md CHANGED
@@ -240,7 +240,8 @@ Land at `ai-tools/skills/<name>/SKILL.md`. Always-on unless flagged optional.
240
240
  | `integrate-phase` | always (unit) | Plan-execution sub-skill: push the reviewed phase along the project's commit strategy + open the PR via context file. Under `commit_strategy = ask`, ships as `integrate-phase-stacked` + `integrate-phase-modular`. |
241
241
  | `amend-plan` | always (generated) | History-rewriting companion to `implement-plan` — revises in-flight plans, amends prior-phase commits, force-pushes, rebases stacked downstream branches. Reuses `review-phase` + `implement-phase`'s shared prompt loop. |
242
242
  | [`handoff`](skills/vinta-derive-skills/resources/foundation-skills/handoff/SKILL.md) | always | Session-continuation handoff doc → `.vinta-ai-workflows/handoffs/`. **Write** captures goal, verified-vs-unverified state, decisions + rejected alternatives, landmines, and the single next step — gathered from the repo, never memory. **Resume** verifies a handoff's claims against the repo before continuing from its next step. |
243
- | [`deslop-comments`](skills/vinta-derive-skills/resources/foundation-skills/deslop-comments/SKILL.md) | always | Rewrite comments + doc blocks touched during a task into Simple English (strips AI-slop vocabulary + negative framing; comment-only — no renames, no behavior change). `review-phase` Layer 2 depends on it; also invokable standalone. |
243
+ | [`deslop-comments`](skills/vinta-derive-skills/resources/foundation-skills/deslop-comments/SKILL.md) | always | Rewrite comments + doc blocks touched during a task into Simple English, and delete the ones that shouldn't exist at all — where-used lists, another module's internals, restatements of the code, deferred-work notes, paths not taken (comment-only; no renames, no behavior change). `review-phase` Layer 2 depends on it, `integrate-phase` / `amend-plan` run it over the PR-context file, and it's invokable standalone. |
244
+ | `write-unit-test` | default-on (generated) | Write durable unit tests for a unit of behavior, enforcing six framework-agnostic rules + this project's captured test conventions, with runner- and stack-specific best-practice packs. Ships automatically whenever a unit-test framework is detected (set `disabled` to opt out). Generated from a template; config under `skills.write-unit-test.*`. See below. |
244
245
  | `systematic-debugging` | **opt-in** | Root-cause-first debugging with project-specific repro commands + MCP evidence-gathering (error tracking, traces, logs, metrics, alerts). Renders from a catalogue of observability MCP servers the user declares. |
245
246
  | `add-e2e-test` | **opt-in** | Add an e2e test. Body covers e2e framework, page-object pattern, auth/storage-state, seed helpers, tenant scoping, screenshot conventions. |
246
247
  | `add-env-var` | **opt-in** | Propagate a new env var through every layer (`.env.example`, build tool envPrefix, build cache hash, app config, AGENTS.md, CI, deploy injection). |
@@ -249,6 +250,28 @@ Land at `ai-tools/skills/<name>/SKILL.md`. Always-on unless flagged optional.
249
250
  | [`thermo-nuclear-code-quality-review`](skills/vinta-derive-skills/resources/foundation-skills/thermo-nuclear-code-quality-review/SKILL.md) | **opt-in** | Deliberately harsh structural-maintainability audit of a diff — abstraction quality, giant files, spaghetti-condition growth — hunting "code-judo" reframes that collapse whole branches / helpers / modes / layers. Read-only; hands each fix to `fixer`. `review-phase` Layer 3 runs a condensed lens every phase and escalates to this full audit on core-architecture / ~1000-line / structurally-smelly phases. |
250
251
  | `handoff-to-client` | **opt-in** | (API-only repos) Generate a self-contained API-change handoff doc for the client teams consuming the repo's API — every endpoint / operation added / changed / deprecated / removed vs the default branch, with request/response shapes, auth + error changes, breaking-change flags, one example per operation, and per-platform migration notes. Template-rendered from `skills.handoff-to-client.*` config (client platforms, API style, spec path). |
251
252
 
253
+ #### `write-unit-test` in depth
254
+
255
+ Agents write tests that pass but assert too little, mock everything, leave data behind, or couple to internals — this skill exists so you don't have to babysit that. It bakes in **six framework-agnostic rules** that hold for every test in every stack:
256
+
257
+ 1. **Mock only genuine externals** — never the unit under test, its local collaborators, or the DB.
258
+ 2. **Assert the full expected value**, not counts or truthiness (`assert result`, `len(x) == 3` are too weak).
259
+ 3. **Leave no trace** — transaction-rollback where available, else clean up in teardown (runs on failure too).
260
+ 4. **No external service** that can't run in dev/test — stub at the boundary.
261
+ 5. **Test logic stays literal** — no loops/conditionals recomputing the expected value.
262
+ 6. **Decouple from internals** — assert observable state/output; survive a behavior-preserving rewrite.
263
+ 7. **Kill nondeterminism** — control every entropy source (clock, randomness, timezone/locale, iteration/DB order, concurrency); a flaky test is worse than none. Don't over-fit brittle text either — assert a stable anchor (error code, role, i18n key) for error messages and UI copy, not the exact churning prose.
264
+ 8. **Cover the branches** — aim high (95%+ branch coverage): every error path, guard, and edge gets a case. Coverage is the floor; correct assertions are the point.
265
+
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
+
268
+ **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
+
270
+ - **Runner pack** (`resources/packs/runners/`) — how to structure, assert, and mock. One, picked by the detected test runner: `pytest`, `vitest`, or `jest`.
271
+ - **Stack pack(s)** (`resources/packs/stacks/`) — DB isolation, framework client, domain mocks. Every matched stack's pack ships (a project can match several — e.g. Next.js + React, or TanStack Start + React): `django`, `fastapi`, `flask`, `medplum`, `react`, `nextjs`, `tanstack-start`, `react-router`, `prisma`, plus pure-library packs `python-package` / `typescript-package` for framework-less packages.
272
+
273
+ **Project preferences are captured at bootstrap** (and inferred from an existing suite, then confirmed) under `skills.write-unit-test.*`: `test_style` (classes vs functions), `data_setup` (factories / fixtures / inline), `assertion_style`, `db_isolation`, plus free-form `additional_conventions`. Each defaults to `framework-default`, deferring to the pack. New frameworks slot in by adding a pack under [`write-unit-test-packs/`](skills/vinta-derive-skills/resources/write-unit-test-packs/) and a detection signal — no schema change.
274
+
252
275
  ### Foundation sub-agents (project-agnostic)
253
276
 
254
277
  Land at `ai-tools/agents/<name>.yaml` (canonical YAML; `setup-ai-tools.mjs` emits per-vendor copies into `.claude/agents/`, `.cursor/`, `.codex/`, `.github/`). Always-on trio.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vinta-ai-workflows",
3
- "version": "0.3.0",
3
+ "version": "0.5.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": {
@@ -232,6 +232,11 @@
232
232
  "description": "Generate a markdown API-change handoff document for client teams consuming an API-only repo — endpoints added/changed/deprecated/removed on the current branch vs the default branch, with request/response shapes, breaking-change flags, and per-platform migration notes. Template-rendered (bucket B) from `vinta-derive-skills/resources/handoff-to-client-template.md` and opt-in (bucket C) — only ships when the bootstrap interview confirms an API-only repo with separate client codebases. When `enabled`, project-level configuration lives under `skills.handoff-to-client` (client platforms, API style, spec path, output dir).",
233
233
  "type": "string",
234
234
  "enum": ["enabled", "disabled"]
235
+ },
236
+ "write-unit-test": {
237
+ "description": "Write durable unit tests for a unit of behavior — mock only genuine externals, assert the full expected output (not counts/truthiness), clean up created data or run inside a rolled-back transaction, never hit an external service that can't run in dev/test, keep test logic literal, and decouple from implementation internals. Template-rendered (bucket B) from `vinta-derive-skills/resources/write-unit-test-template.md`, with best-practice packs copied verbatim into the skill's `resources/packs/` on two independent axes — one runner pack (`runners/`: pytest / vitest / jest) selected by the detected test framework, plus a stack pack per matched stack (`stacks/`: django / fastapi / flask / medplum / react / nextjs / tanstack-start / react-router / prisma / python-package / typescript-package) so framework-specific advice ships only when that framework is present. Default-on: bootstrap sets `enabled` whenever `vinta-analyze-codebase` detects a unit-test framework, `disabled` when none is found (there's nothing to render against). When `enabled`, project-level test conventions live under `skills.write-unit-test` (test style, data setup, assertion style, DB isolation, inferred extra conventions).",
238
+ "type": "string",
239
+ "enum": ["enabled", "disabled"]
235
240
  }
236
241
  }
237
242
  },
@@ -479,6 +484,44 @@
479
484
  "default": "one-off-runs/"
480
485
  }
481
486
  }
487
+ },
488
+ "write-unit-test": {
489
+ "description": "Configuration for the `write-unit-test` foundation skill. Optional when `foundation_skills.write-unit-test` is `enabled` — every field defaults to `framework-default`, deferring to the shipped framework pack. At bootstrap, values are inferred from the existing test suite (see `vinta-analyze-codebase` inventory `tests.conventions`) and confirmed by the user; on a greenfield project they're asked directly. Rendered into the skill body at derive time.",
490
+ "type": "object",
491
+ "additionalProperties": false,
492
+ "properties": {
493
+ "test_style": {
494
+ "description": "Whether new tests are written as standalone functions or grouped in test classes. `functions` (pytest-style `def test_x()`), `classes` (TestCase / describe-block grouping), or `framework-default` (defer to the framework pack — functions for pytest/vitest/jest, classes for Django TestCase). Inferred from the dominant shape in the existing suite.",
495
+ "type": "string",
496
+ "enum": ["functions", "classes", "framework-default"],
497
+ "default": "framework-default"
498
+ },
499
+ "data_setup": {
500
+ "description": "How test data is constructed. `factories` (factory classes like factory_boy / FactoryBot / builder helpers — preferred when the project has them), `fixtures` (framework fixtures / fixture files), `inline` (built in the test body), or `framework-default`. Inferred from `tests.test_data_helpers` and the dominant pattern in the suite.",
501
+ "type": "string",
502
+ "enum": ["factories", "fixtures", "inline", "framework-default"],
503
+ "default": "framework-default"
504
+ },
505
+ "assertion_style": {
506
+ "description": "Which assertion form new tests use. `plain-assert` (Python plain `assert` / pytest introspection, or JS `expect`), `framework-methods` (`unittest` `self.assertEqual` family), or `framework-default`. Inferred from the existing suite — a pytest suite that already uses plain `assert` sets `plain-assert`; a `TestCase`-heavy suite sets `framework-methods`.",
507
+ "type": "string",
508
+ "enum": ["plain-assert", "framework-methods", "framework-default"],
509
+ "default": "framework-default"
510
+ },
511
+ "db_isolation": {
512
+ "description": "How data created by a test is undone between tests. `transaction-rollback` (each test runs in a transaction rolled back at teardown — preferred where supported), `truncate` (tables truncated between tests, e.g. Django TransactionTestCase), `recreate` (schema/DB rebuilt), or `framework-default`. Inferred from the test base classes / fixtures in use.",
513
+ "type": "string",
514
+ "enum": ["transaction-rollback", "truncate", "recreate", "framework-default"],
515
+ "default": "framework-default"
516
+ },
517
+ "additional_conventions": {
518
+ "description": "Free-form project-specific test conventions the four structured fields can't capture — inferred from the existing suite at bootstrap (e.g. `co-locate tests next to source as *.test.ts`, `one assertion per test`, `name tests should_<behavior>_when_<condition>`, `mock the network with our recorded-cassette helper`) or supplied by the user. Rendered verbatim into the skill body's project-conventions section. Empty array is fine — the universal rules + framework pack already cover the essentials.",
519
+ "type": "array",
520
+ "items": { "type": "string", "minLength": 1 },
521
+ "uniqueItems": true,
522
+ "default": []
523
+ }
524
+ }
482
525
  }
483
526
  }
484
527
  }
@@ -62,6 +62,8 @@ Match top-level deps against this table:
62
62
  | `flask` | Flask |
63
63
  | `pytest` | pytest |
64
64
  | `mypy`, `ruff`, `black` | Python lint/type tools |
65
+ | `@tanstack/react-start`, `@tanstack/start` | TanStack Start (check `@tanstack/react-router`) |
66
+ | `react-router`, `react-router-dom`, `@react-router/*` | React Router |
65
67
  | `tRPC`, `@trpc/*` | tRPC |
66
68
  | `prisma`, `@prisma/client` | Prisma |
67
69
  | `drizzle-orm` | Drizzle |
@@ -94,6 +96,12 @@ For each app/package: name + path + framework (cross-ref the [Frameworks (from d
94
96
  - Unit / integration: where are they? `apps/*/src/**/*.test.ts`, `tests/`, `__tests__/`, `<pkg>/tests/unit`, `<pkg>/tests/integration`. Glob.
95
97
  - E2E: `e2e/`, `playwright/`, `cypress/`. Read fixtures + setup files for: auth strategy, seed helpers, tenant scoping in seeds.
96
98
  - Test data: factories, fixtures, mocks. Note conventions (`@medplum/mock`, `factory_boy`, MSW, etc).
99
+ - **Unit-test conventions (feeds `write-unit-test`).** Read a representative sample of existing unit tests (not e2e) and infer the **dominant** pattern for each axis below. These pre-fill `skills.write-unit-test.*` in the bootstrap interview (group C.9), so the shipped skill matches the suite instead of imposing a house style. Report `framework-default` for any axis where the suite is mixed or there's no signal (greenfield):
100
+ - **test_style** — `functions` (standalone `def test_x` / bare `it(...)`) vs `classes` (`TestCase` subclasses, `describe` grouping). Which dominates?
101
+ - **data_setup** — `factories` (factory_boy / FactoryBot / builder helpers — cross-ref `test_data_helpers`), `fixtures` (pytest fixtures / fixture files / `setUpTestData`), or `inline` (objects built in the test body).
102
+ - **assertion_style** — `plain-assert` (Python `assert` / JS `expect`) vs `framework-methods` (`self.assertEqual`-family).
103
+ - **db_isolation** — `transaction-rollback` (Django `TestCase`, pytest-django `db`, SQLAlchemy savepoint fixtures), `truncate` (`TransactionTestCase`), or `recreate`. Read the test base classes / DB fixtures to tell.
104
+ - **extras** — any other recurring convention worth carrying (test file co-location + naming pattern, one-assertion-per-test, a house HTTP-mock helper / recorded-cassette pattern, custom assertion helpers). List them; they seed `additional_conventions`.
97
105
 
98
106
  ### 7. Deploy / release model
99
107
 
@@ -155,7 +163,7 @@ This is a load-bearing input for the rest of the bootstrap flow — surface ever
155
163
  For each skill, also classify:
156
164
 
157
165
  - `vinta-managed` — directory name starts with `vinta-` (installed by `vinta-ai-workflows`; can be left alone or refreshed via that CLI).
158
- - `foundation-shape` — name matches the Vinta foundation set (`plan-feature`, `create-spec`, `create-qa-use-cases`, `implement-plan`, `implement-phase`, `review-phase`, `integrate-phase`, `amend-plan`, `add-e2e-test`, `add-env-var`). `implement-phase` / `review-phase` / `integrate-phase` are the plan-execution sub-skills co-shipped with `implement-plan`.
166
+ - `foundation-shape` — name matches the Vinta foundation set (`plan-feature`, `create-spec`, `create-qa-use-cases`, `implement-plan`, `implement-phase`, `review-phase`, `integrate-phase`, `amend-plan`, `add-e2e-test`, `add-env-var`, `write-unit-test`). `implement-phase` / `review-phase` / `integrate-phase` are the plan-execution sub-skills co-shipped with `implement-plan`.
159
167
  - `project-custom` — anything else; written by the team.
160
168
 
161
169
  **Sub-agents** — list every agent file with its name + description:
@@ -273,6 +281,12 @@ tests:
273
281
  e2e_framework: playwright | cypress | none
274
282
  e2e_paths: [<path>]
275
283
  test_data_helpers: [<file>] # factories, mock clients, seed scripts
284
+ conventions: # inferred dominant patterns → skills.write-unit-test.* (framework-default when mixed / no signal)
285
+ test_style: functions | classes | framework-default
286
+ data_setup: factories | fixtures | inline | framework-default
287
+ assertion_style: plain-assert | framework-methods | framework-default
288
+ db_isolation: transaction-rollback | truncate | recreate | framework-default
289
+ extras: [<free-form recurring convention>] # seeds additional_conventions; [] when none
276
290
 
277
291
  deploy:
278
292
  targets: [vercel, aws-ecs, heroku, k8s, medplum-bots, npm-publish, ...]
@@ -81,7 +81,19 @@ These bleed across sub-skills, so capture once now:
81
81
 
82
82
  Lands in `.vinta-ai-workflows.yaml` `policies.commit_strategy`. Drives the `implement-plan` template's branch / commit / PR-opening flow + the `amend-plan` skill's supported-strategies check.
83
83
 
84
- 9. **Agent model tiers (optional).** The plan-execution family picks the **implementer** model per phase from the plan itself, but the **reviewer**, **fixer**, and the mechanical steps (**worktree prep**, **opening the PR / integrate**) have no model input unless the project sets one. Offer to pin each to a **tier** (1–4) into the same table `plan-feature` uses — `ai-tools/skills/plan-feature/resources/ai-models.yaml` — so review runs on a capable model while cheap mechanical work runs on a cheap one. `AskUserQuestion`: `Use recommended defaults (reviewer 3, fixer 2, worktree prep 1, integrate 1)`, `Customize each`, `Leave unset — every spawn uses the runtime default`. On `Customize each`, ask a per-role tier (1–4) for `reviewer` / `fixer` / `worktree_prep` / `integrate`. Land the chosen tiers in `.vinta-ai-workflows.yaml` `agent_models`; omit any role the user leaves unset (unset → runtime default). Recommend the defaults for most teams; recommend `Leave unset` only when the runtime exposes a single model anyway. Note `worktree_prep` is a no-op unless `prepare-worktree` is enabled, and setting `worktree_prep` / `integrate` makes `implement-plan` delegate those steps to a cheap subagent instead of running them inline.
84
+ 9. **Test conventions for `write-unit-test`.** The `write-unit-test` foundation skill ships **by default whenever a unit-test framework is detected** (item 2 above) — it's not an opt-in in group D. Capture the project's test conventions so the rendered skill matches the suite instead of imposing a house style. Land them under `skills.write-unit-test.*` in `.vinta-ai-workflows.yaml`.
85
+
86
+ **On an existing project, infer first, then confirm.** [vinta-analyze-codebase](../vinta-analyze-codebase/SKILL.md#6-tests)'s `tests.conventions` block reports the dominant patterns it read from the suite (test style, data setup, assertion style, DB isolation, plus any recurring extras). Present those inferred values as the defaults and ask only "these match? anything to change?". On a greenfield project with no tests yet, ask directly (recommend `framework-default` for all four — the shipped framework pack already encodes the idiomatic choice). Skip this item entirely when no test framework was detected (`foundation_skills.write-unit-test: disabled`).
87
+
88
+ Four structured `AskUserQuestion` fields (each offers a `Framework default` option — pick it to defer to the pack):
89
+ - **Test style** — `Standalone functions`, `Test classes`, `Framework default`. → `test_style`.
90
+ - **Test-data setup** — `Factory classes / builders`, `Fixtures`, `Inline in the test`, `Framework default`. → `data_setup`.
91
+ - **Assertion style** — `Plain assert / expect`, `Framework assertion methods (self.assertEqual …)`, `Framework default`. → `assertion_style`.
92
+ - **DB isolation** — `Transaction rollback`, `Truncate between tests`, `Recreate schema/DB`, `Framework default`. → `db_isolation`.
93
+
94
+ **Additional conventions (optional).** Open prose: *"Any recurring test conventions the four fields above don't capture? (e.g. 'co-locate tests as `*.test.ts` next to source', 'name tests `should_<behavior>_when_<condition>`', 'mock HTTP with our recorded-cassette helper'.)"* On an existing project, seed this from the extras `vinta-analyze-codebase` surfaced and let the user trim. Each entry lands in `skills.write-unit-test.additional_conventions[]`. Leave empty when there's nothing project-specific — the universal rules + framework pack already cover the essentials.
95
+
96
+ 10. **Agent model tiers (optional).** The plan-execution family picks the **implementer** model per phase from the plan itself, but the **reviewer**, **fixer**, and the mechanical steps (**worktree prep**, **opening the PR / integrate**) have no model input unless the project sets one. Offer to pin each to a **tier** (1–4) into the same table `plan-feature` uses — `ai-tools/skills/plan-feature/resources/ai-models.yaml` — so review runs on a capable model while cheap mechanical work runs on a cheap one. `AskUserQuestion`: `Use recommended defaults (reviewer 3, fixer 2, worktree prep 1, integrate 1)`, `Customize each`, `Leave unset — every spawn uses the runtime default`. On `Customize each`, ask a per-role tier (1–4) for `reviewer` / `fixer` / `worktree_prep` / `integrate`. Land the chosen tiers in `.vinta-ai-workflows.yaml` `agent_models`; omit any role the user leaves unset (unset → runtime default). Recommend the defaults for most teams; recommend `Leave unset` only when the runtime exposes a single model anyway. Note `worktree_prep` is a no-op unless `prepare-worktree` is enabled, and setting `worktree_prep` / `integrate` makes `implement-plan` delegate those steps to a cheap subagent instead of running them inline.
85
97
 
86
98
  ### D. Optional foundation skills
87
99
 
@@ -144,7 +156,7 @@ For each artifact, read it (frontmatter + body), then ask the user via `AskUserQ
144
156
  - `Keep in current vendor path, don't touch` — leaves it where it is. AGENTS.md may reference it; downstream skill setup won't manage it.
145
157
  - `Drop` — delete (rare; usually the user wants to migrate).
146
158
 
147
- Foundation-shape skills (name matches `plan-feature`, `create-spec`, `create-qa-use-cases`, `implement-plan`, `implement-phase`, `review-phase`, `integrate-phase`, `amend-plan`, `add-e2e-test`, `add-env-var`, `add-one-off-script`, `prepare-worktree`, `thermo-nuclear-code-quality-review`, `deslop-comments`, `handoff`, `handoff-to-client`) get an extra option: `Replace with Vinta foundation version` — overwrites with the canonical foundation content, preserving the user's name. Useful when the existing version is stale. (`implement-phase` / `review-phase` / `integrate-phase` are the plan-execution sub-skills co-shipped with `implement-plan`; replace them as a unit.)
159
+ Foundation-shape skills (name matches `plan-feature`, `create-spec`, `create-qa-use-cases`, `implement-plan`, `implement-phase`, `review-phase`, `integrate-phase`, `amend-plan`, `add-e2e-test`, `add-env-var`, `add-one-off-script`, `prepare-worktree`, `thermo-nuclear-code-quality-review`, `deslop-comments`, `handoff`, `handoff-to-client`, `write-unit-test`) get an extra option: `Replace with Vinta foundation version` — overwrites with the canonical foundation content, preserving the user's name. Useful when the existing version is stale. (`implement-phase` / `review-phase` / `integrate-phase` are the plan-execution sub-skills co-shipped with `implement-plan`; replace them as a unit.)
148
160
 
149
161
  - **Sub-agents** (each under any vendor `agents/` dir):
150
162
  - `Migrate to ai-tools/agents/<name>.yaml` — converts vendor-specific format → canonical YAML; `setup-ai-tools.mjs` re-emits per-vendor copies.
@@ -263,23 +275,24 @@ foundation_skills:
263
275
  add-one-off-script: <Optional foundation skills → add-one-off-script answer → enabled | disabled>
264
276
  prepare-worktree: <Optional foundation skills → prepare-worktree answer → enabled | disabled>
265
277
  thermo-nuclear-code-quality-review: <Optional foundation skills → thermo-nuclear-code-quality-review answer → enabled | disabled>
266
- deslop-comments: enabled # always ships — review-phase Layer 2 comment-hygiene + fix loop depend on it
278
+ deslop-comments: enabled # always ships — review-phase Layer 2 comment-hygiene + fix loop and the integrate-phase PR-context prose pass depend on it
267
279
  handoff: enabled # always ships — project-agnostic session-continuation handoff docs
268
280
  handoff-to-client: <Optional foundation skills → handoff-to-client answer → enabled | disabled; only asked for API-only repos>
281
+ write-unit-test: <enabled when a unit-test framework was detected (Project conventions → Test framework(s)); disabled when none found — default-on, not an opt-in>
269
282
 
270
283
  foundation_agents:
271
284
  implementer: enabled
272
285
  reviewer: enabled
273
286
  fixer: enabled
274
287
 
275
- # Only emit the roles the user chose in the "Agent model tiers" question (C.9).
288
+ # Only emit the roles the user chose in the "Agent model tiers" question (C.10).
276
289
  # Omit the whole block on "Leave unset"; omit any individual role left unset.
277
290
  # Each value is a tier (1–4) into ai-tools/skills/plan-feature/resources/ai-models.yaml.
278
291
  agent_models:
279
- reviewer: <C.9 → reviewer tier, default 3>
280
- fixer: <C.9 → fixer tier, default 2>
281
- worktree_prep: <C.9 → worktree_prep tier, default 1; omit when prepare-worktree disabled>
282
- integrate: <C.9 → integrate tier, default 1>
292
+ reviewer: <C.10 → reviewer tier, default 3>
293
+ fixer: <C.10 → fixer tier, default 2>
294
+ worktree_prep: <C.10 → worktree_prep tier, default 1; omit when prepare-worktree disabled>
295
+ integrate: <C.10 → integrate tier, default 1>
283
296
 
284
297
  stacks: <Stack detection → matched stacks>
285
298
  stack_specialist_agents: <empty unless user supplied templates>
@@ -305,6 +318,17 @@ skills:
305
318
  api_spec_path: <handoff-to-client follow-up — repo-relative spec path + regen command; omit when none>
306
319
  output_dir: .vinta-ai-workflows/client-handoffs
307
320
 
321
+ # Only emit this block when foundation_skills.write-unit-test = enabled.
322
+ # Values come from Project conventions → C.9 (Test conventions), inferred from the
323
+ # existing suite (analyze-codebase tests.conventions) and confirmed. Omit any field
324
+ # left at framework-default and the whole block when there's nothing but defaults.
325
+ write-unit-test:
326
+ test_style: <C.9 → functions | classes | framework-default>
327
+ data_setup: <C.9 → factories | fixtures | inline | framework-default>
328
+ assertion_style: <C.9 → plain-assert | framework-methods | framework-default>
329
+ db_isolation: <C.9 → transaction-rollback | truncate | recreate | framework-default>
330
+ additional_conventions: <C.9 → free-form array of extra conventions inferred/supplied; [] when none>
331
+
308
332
  # Only emit this block when foundation_skills.prepare-worktree = enabled.
309
333
  prepare-worktree:
310
334
  worktree_root: <prepare-worktree follow-up — `.claude/worktrees` | `../<repo>-wt-`>
@@ -377,7 +401,7 @@ ai-tools/
377
401
  │ ├── plan-feature/SKILL.md ← always (copied verbatim from derive-skills resources)
378
402
  │ ├── create-spec/SKILL.md ← always (copied verbatim)
379
403
  │ ├── create-qa-use-cases/SKILL.md ← always (copied verbatim)
380
- │ ├── deslop-comments/SKILL.md ← always (copied verbatim; review-phase Layer 2 comment-hygiene + fix loop depend on it)
404
+ │ ├── deslop-comments/SKILL.md ← always (copied verbatim; review-phase Layer 2 comment-hygiene + fix loop and the integrate-phase PR-context prose pass depend on it)
381
405
  │ ├── handoff/SKILL.md ← always (copied verbatim; session-continuation handoff docs under .vinta-ai-workflows/handoffs/)
382
406
  │ ├── implement-plan/SKILL.md ← always (conductor; generated from template, project-specific)
383
407
  │ ├── implement-phase/SKILL.md ← always (plan-execution unit; co-shipped with implement-plan)
@@ -388,6 +412,11 @@ ai-tools/
388
412
  │ ├── add-env-var/SKILL.md ← optional — only if user opts in
389
413
  │ ├── systematic-debugging/SKILL.md ← optional — only if user opts in (template-rendered)
390
414
  │ ├── handoff-to-client/SKILL.md ← optional — only for API-only repos, if user opts in (template-rendered; config under skills.handoff-to-client.*)
415
+ │ ├── write-unit-test/ ← default-on when a unit-test framework was detected (template-rendered; config under skills.write-unit-test.*)
416
+ │ │ ├── SKILL.md
417
+ │ │ └── resources/packs/ ← verbatim packs for the detected runner + matched stacks only
418
+ │ │ ├── runners/<runner>.md ← one: pytest.md / vitest.md / jest.md
419
+ │ │ └── stacks/<stack>.md ← only matched: django / fastapi / flask / medplum / react / nextjs / tanstack-start / react-router / prisma / python-package / typescript-package
391
420
  │ ├── add-one-off-script/ ← optional — only if user opts in (verbatim copy + bundled BaseOneOffScript + LocalRuntime templates)
392
421
  │ │ ├── SKILL.md
393
422
  │ │ └── resources/
@@ -423,8 +452,8 @@ Plus the symlinks + per-vendor generated files, set up by the install step.
423
452
 
424
453
  Foundation skills break into three buckets — see [vinta-derive-skills](../vinta-derive-skills/SKILL.md) for the full mechanics:
425
454
 
426
- - **Always copy verbatim**: `plan-feature`, `create-spec`, `create-qa-use-cases`, `deslop-comments`, `handoff`. Bundled with the bootstrap skill set; project-agnostic enough to ship as-is (with light path scrubs). `deslop-comments` always ships because `review-phase`'s Layer 2 comment-hygiene check + fix loop dispatch it. `handoff` always ships because its session-continuation body is fully project-agnostic.
427
- - **Always generate**: the plan-execution unit — `implement-plan` (conductor) + its co-shipped sub-skills `implement-phase` / `review-phase` / `integrate-phase`, plus `amend-plan` (conductor). Bodies have too much project-specific content (test commands, branch convention, PR + co-author policy, agent dispatch) — generated from parameterized templates + shared partials using interview answers + inventory. The sub-skills are not independently opt-in; they always ship with the conductors.
455
+ - **Always copy verbatim**: `plan-feature`, `create-spec`, `create-qa-use-cases`, `deslop-comments`, `handoff`. Bundled with the bootstrap skill set; project-agnostic enough to ship as-is (with light path scrubs). `deslop-comments` always ships because `review-phase`'s Layer 2 comment-hygiene check + fix loop dispatch it, and `integrate-phase` / `amend-plan` run it over the PR-context file. `handoff` always ships because its session-continuation body is fully project-agnostic.
456
+ - **Always generate**: the plan-execution unit — `implement-plan` (conductor) + its co-shipped sub-skills `implement-phase` / `review-phase` / `integrate-phase`, plus `amend-plan` (conductor). Bodies have too much project-specific content (test commands, branch convention, PR + co-author policy, agent dispatch) — generated from parameterized templates + shared partials using interview answers + inventory. The sub-skills are not independently opt-in; they always ship with the conductors. `write-unit-test` is also template-rendered but **conditionally generated** — it ships whenever a unit-test framework was detected (not an opt-in question, not ask-first): the rendered body bakes in the project's test command + captured `skills.write-unit-test.*` conventions, and the verbatim packs land in its `resources/packs/` — one runner pack for the detected test framework plus a stack pack per matched stack (Django / FastAPI / Flask / Medplum / React / Next.js / TanStack Start / React Router / Prisma / pure-package), so framework-specific advice only ships when that framework is present. A few stack packs also apply a one-time project-setup step at derive time (e.g. Medplum wires the two FHIR-indexing Vitest setup files — `test.globalSetup.ts` + `test.setup.ts` — into the Vitest config so `MockClient` search filtering works) — a project mutation done once, not per test.
428
457
  - **Optional, ask first**: `add-e2e-test`, `add-env-var`, `systematic-debugging`, `add-one-off-script`, `prepare-worktree`, `thermo-nuclear-code-quality-review`, `handoff-to-client`. Skipped by default; orchestrator asks via `AskUserQuestion` whether the project has the relevant flow at all. `add-e2e-test` / `add-env-var`: if yes + user has a template → copy + adapt; if yes + no template → draft from scratch via interview; if no → don't ship. `systematic-debugging`: if yes → render the bundled template plus the per-tool MCP catalogue blocks for the observability tools selected in its follow-up; if no → don't ship. `add-one-off-script`: if yes → copy the bundled SKILL.md verbatim plus the language-specific `BaseOneOffScript` template (`one_off_script_base.py` / `one_off_script_base.ts`) chosen via its follow-up; if no → don't ship. `prepare-worktree`: if yes → copy the bundled SKILL.md verbatim, populate `skills.prepare-worktree.*` defaults from its follow-ups, and (when the user opted in via the worktree-default follow-up) flip `run_options.implement-plan.use_worktree` to `true` so `implement-plan`'s Step 0 question (c) defaults to yes; if no → don't ship. `thermo-nuclear-code-quality-review`: if yes → copy the bundled SKILL.md verbatim (no follow-up config); if no → don't ship. `handoff-to-client`: asked only for API-only repos; if yes → render the bundled template using `skills.handoff-to-client.*` (client platforms, API style, spec path, output dir) from its follow-ups; if no → don't ship.
429
458
 
430
459
  Stack-specific skills + agents land in the target only when the user provides templates for them. If they don't have templates yet, the orchestrator records the detected stacks + skill categories as a TODO list the user can address later via [vinta-derive-skills](../vinta-derive-skills/SKILL.md) / [vinta-derive-subagents](../vinta-derive-subagents/SKILL.md) standalone runs.
@@ -26,7 +26,7 @@ Before drafting any new SKILL.md, walk through every entry in `existing_ai_artif
26
26
  - **Migrate to `ai-tools/skills/<name>/`** — `git mv` the existing skill folder (with all its resources) into the canonical layout. After move, scan the body for hard-coded vendor paths that no longer apply (`.cursor/skills/...` self-references, etc.) and rewrite to the new path. `setup-ai-tools.mjs` will re-link to the chosen vendors.
27
27
  - **Keep in current vendor path, don't touch** — leave it where it is; AGENTS.md may reference it; downstream skill setup won't manage it. **Don't ship a foundation duplicate that would shadow it.**
28
28
  - **Drop** — log removal; don't emit anything.
29
- - **Replace with Vinta foundation version** (foundation-shape only — `plan-feature`, `create-spec`, `create-qa-use-cases`, `implement-plan`, `add-e2e-test`, `add-env-var`, `add-one-off-script`, `prepare-worktree`, `thermo-nuclear-code-quality-review`, `deslop-comments`, `handoff`, `handoff-to-client`) — proceed with the bucket A / B / C flow below for that name. The user has explicitly opted into overwriting their version.
29
+ - **Replace with Vinta foundation version** (foundation-shape only — `plan-feature`, `create-spec`, `create-qa-use-cases`, `implement-plan`, `add-e2e-test`, `add-env-var`, `add-one-off-script`, `prepare-worktree`, `thermo-nuclear-code-quality-review`, `deslop-comments`, `handoff`, `handoff-to-client`, `write-unit-test`) — proceed with the bucket A / B / C flow below for that name. The user has explicitly opted into overwriting their version.
30
30
  - **`vinta-managed`** (any skill whose dir starts with `vinta-`) — leave alone. These come from the `vinta-ai-workflows` CLI; `vinta-derive-skills` doesn't manage them.
31
31
 
32
32
  For each name in the foundation set: only emit it (verbatim copy / generate from template / interview-draft) when no existing skill of that name is being migrated or kept. If the user said `Migrate` or `Keep` for an existing `plan-feature/SKILL.md`, **do not overwrite**.
@@ -47,7 +47,7 @@ These are project-agnostic enough to ship as-is. Copy from this skill's [resourc
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
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
- | `deslop-comments` | [resources/foundation-skills/deslop-comments/SKILL.md](resources/foundation-skills/deslop-comments/SKILL.md) | Rewrite comments + doc blocks touched during a task into Simple English — strip AI-slop vocabulary and negative framing; comment-only, no renames or behavior change. **Always ships** — the `review-phase` Layer 2 comment-hygiene check + fix loop dispatch it, so it must exist in the target. Also user-invokable standalone ("deslop these comments"). |
50
+ | `deslop-comments` | [resources/foundation-skills/deslop-comments/SKILL.md](resources/foundation-skills/deslop-comments/SKILL.md) | Rewrite comments + doc blocks touched during a task into Simple English — strip AI-slop vocabulary and negative framing; comment-only, no renames or behavior change. **Always ships** — the `review-phase` Layer 2 comment-hygiene check + fix loop dispatch it, and `integrate-phase` / `amend-plan` run it over the PR-context file, so it must exist in the target. Also user-invokable standalone ("deslop these comments"). |
51
51
  | `handoff` | [resources/foundation-skills/handoff/SKILL.md](resources/foundation-skills/handoff/SKILL.md) | Write or consume a session-continuation handoff doc under `.vinta-ai-workflows/handoffs/` — goal, current state, decisions + rationale, landmines, next step — so a fresh agent session (or a teammate) resumes in-flight work without re-deriving context. **Always ships** — fully project-agnostic; standalone (no cross-links into the always-on trio). |
52
52
  | `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). |
53
53
  | `thermo-nuclear-code-quality-review` | [resources/foundation-skills/thermo-nuclear-code-quality-review/SKILL.md](resources/foundation-skills/thermo-nuclear-code-quality-review/SKILL.md) | Opt-in deep structural-maintainability audit of a diff — abstraction quality, giant files, spaghetti-condition growth — harsher than the standard per-phase review; hunts for "code-judo" reframes that collapse branches / helpers / layers rather than polishing them. Read-only; hands each fix to the `fixer` agent. Invoked deliberately by the user, or escalated from the `review-phase` Layer 3 structural-simplification lens. Opt-in (bucket A + ask-first via the bootstrap's **Optional foundation skills** interview). |
@@ -57,7 +57,7 @@ These reference each other:
57
57
  - `plan-feature` reads spec; dispatches `create-qa-use-cases` when the doc is missing (e2e-enabled projects only — see below).
58
58
  - `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.
59
59
 
60
- The **always-on unit** is `plan-feature` + `create-spec` + `open-pr-from-context` — copy all three or none. `deslop-comments` also always ships (independently — it doesn't cross-reference the trio), because the `review-phase` Layer 2 comment-hygiene check + fix loop dispatch it; a target missing it would leave that review step dangling. `handoff` also always ships independently — its body is fully project-agnostic and nothing else depends on it. `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.
60
+ The **always-on unit** is `plan-feature` + `create-spec` + `open-pr-from-context` — copy all three or none. `deslop-comments` also always ships (independently — it doesn't cross-reference the trio), because the `review-phase` Layer 2 comment-hygiene check + fix loop dispatch it and `integrate-phase` / `amend-plan` run it over the PR-context file; a target missing it would leave those steps dangling. `handoff` also always ships independently — its body is fully project-agnostic and nothing else depends on it. `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.
61
61
 
62
62
  **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:
63
63
 
@@ -67,7 +67,7 @@ The **always-on unit** is `plan-feature` + `create-spec` + `open-pr-from-context
67
67
 
68
68
  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.
69
69
 
70
- ### B. Generate — the **plan-execution unit** + `systematic-debugging` from templates
70
+ ### B. Generate — the **plan-execution unit** + `systematic-debugging` + `write-unit-test` from templates
71
71
 
72
72
  Project-specific skills, generated from templates because their bodies cite real test commands, branch conventions, agent dispatch table, PR / co-author policy, or selected MCP tools.
73
73
 
@@ -85,6 +85,41 @@ The five are a **co-shipped unit**: `implement-plan` is always generated, so `im
85
85
 
86
86
  - **`handoff-to-client`** — generate a markdown API-change handoff document for the client teams consuming an API-only repo: endpoints/operations added / changed / deprecated / removed on the current branch vs the default branch, with request/response shapes, breaking-change flags, and per-platform migration notes. From [resources/handoff-to-client-template.md](resources/handoff-to-client-template.md). **Opt-in** — only generated when `foundation_skills.handoff-to-client` is `enabled` in `.vinta-ai-workflows.yaml` (the bootstrap interview asks only when the repo looks API-only with separate client codebases). The rendered body bakes in `skills.handoff-to-client.*` config: client platforms, API style, optional API spec path, output dir. Standalone — no cross-links into the plan-execution unit.
87
87
 
88
+ - **`write-unit-test`** — write durable unit tests for a unit of behavior, enforcing the universal rules (mock only genuine externals, assert the full expected output not counts/truthiness, clean up created data or roll back the transaction, never hit an external service unavailable in dev/test, keep test logic literal, decouple from internals) plus this project's captured test conventions. From [resources/write-unit-test-template.md](resources/write-unit-test-template.md). **Default-on, not ask-first** — generated whenever `foundation_skills.write-unit-test` is `enabled`, which bootstrap sets automatically when `vinta-analyze-codebase` detected a unit-test framework (`disabled` → skip; nothing to render against). The rendered body bakes in `skills.write-unit-test.*` config: test style, data setup, assertion style, DB isolation, plus any inferred `additional_conventions`. Standalone — no cross-links into the plan-execution unit.
89
+
90
+ **Ship the packs too — two axes, matched independently.** Alongside the rendered `SKILL.md`, copy the **verbatim** packs the project needs from [resources/write-unit-test-packs/](resources/write-unit-test-packs/) into `ai-tools/skills/write-unit-test/resources/packs/`. There are two kinds, selected on **different** signals so framework-specific advice only ships when its framework is actually present:
91
+
92
+ - **Runner pack** (`resources/packs/runners/<runner>.md` → shipped to `.../packs/runners/`) — the test-runner idioms (structure, assertions, mocking). Select **one (or, in a polyglot repo, one per language)** by `inventory.tests.unit_framework`: `pytest` → `pytest.md`; `vitest` → `vitest.md`; `jest` → `jest.md`. Runner packs are **stack-agnostic** — they contain no Django/FastAPI/Medplum specifics.
93
+ - **Stack pack(s)** (`resources/packs/stacks/<stack>.md` → shipped to `.../packs/stacks/`) — DB isolation, framework client/test-client, domain mocks. Ship **every** pack whose stack the inventory matched (a project can match several — e.g. `nextjs` + `react`, or `tanstack-start` + `react`). Match on `inventory.frameworks` / `inventory.stacks`:
94
+
95
+ | Detected signal | Stack pack |
96
+ |---|---|
97
+ | Django | `django.md` |
98
+ | FastAPI | `fastapi.md` |
99
+ | Flask | `flask.md` |
100
+ | Medplum (`@medplum/*`) | `medplum.md` |
101
+ | React (renders UI) | `react.md` |
102
+ | Next.js | `nextjs.md` |
103
+ | TanStack Start (`@tanstack/react-start`) | `tanstack-start.md` |
104
+ | React Router (`react-router` / `@react-router/*`) | `react-router.md` |
105
+ | Prisma (`@prisma/client`) | `prisma.md` |
106
+ | Standalone Python package (no web framework) | `python-package.md` |
107
+ | Standalone TS package / lib (no framework) | `typescript-package.md` |
108
+
109
+ **Only ship what matched — never dump the whole directory.** A FastAPI project must not receive `django.md`; a pure-library project gets its `*-package.md` and **no** web-framework pack. When no stack pack matches (e.g. a plain script repo), ship the runner pack alone — the rendered body already falls back to the universal rules + the nearest existing tests. When the runner or a clearly-present stack has no pack here, ship nothing for it and record the gap in the orchestrator's final summary (a candidate for a new pack via the source repo's `add-foundation-skill` flow). The `{{FRAMEWORK_PACK_LOADER_BLOCK}}` + `{{FRAMEWORK_PACKS_LIST}}` placeholders are rendered from exactly the set of packs you shipped.
110
+
111
+ **Some stack packs need a one-time project-setup step, done here — not per test.** A pack may require a project-level test-setup fixture that must exist before any test runs; wire it once at derive time so no individual test has to. Currently:
112
+
113
+ - **Medplum** (Vitest) — `MockClient` search filtering (`searchResources` / `searchOne` with filters) silently returns nothing unless the FHIR structure definitions + search parameters are indexed in the test process. When you ship `medplum.md`, also install the **two** bundled Vitest setup files and wire both hooks:
114
+ - Copy [resources/write-unit-test-packs/stacks/medplum-test.globalSetup.ts](resources/write-unit-test-packs/stacks/medplum-test.globalSetup.ts) → the project's setup dir (commonly `src/test.globalSetup.ts`) and register it in `vitest.config.*` under `test.globalSetup`. It indexes the bundles once in the main process and shares `globalSchema.types` to every worker via `project.provide`.
115
+ - Copy [resources/write-unit-test-packs/stacks/medplum-test.setup.ts](resources/write-unit-test-packs/stacks/medplum-test.setup.ts) → `src/test.setup.ts` and register it under `test.setupFiles`. It `inject`s the shared index and assigns it at **module scope** (not `beforeAll` — timing matters).
116
+ - **Deps:** `@medplum/definitions` as a devDependency; Vitest **3+** (the `provide`/`inject` API); Node **≥ 22.4** or add `ws` for the WebSocket polyfill (@medplum/core v5). Flag any of these that are missing.
117
+ - **Version alignment:** `@medplum/core`, `@medplum/definitions`, `@medplum/mock`, `@medplum/fhirtypes` must be on matching versions.
118
+ - **Idempotent + merge, don't clobber:** if the project already has `test.setup.ts` / `test.globalSetup.ts`, merge the indexing + inject/provide logic in rather than overwriting; if it already does this, do nothing. Create the files + point the runner config at them only when absent.
119
+ - **Jest-based Medplum project** (rare — the catalog is Vitest-first): there's no bundled Jest variant; index the FHIR bundles in a single `setupFilesAfterEnv` file instead and record it as a follow-up to add a Jest snippet to the catalogue.
120
+
121
+ This is a project mutation — do it once at bootstrap. Canonical source is the team's snippet catalogue (`vintasoftware/medplum-snippet-catalog`, `TestSetup/`); the bundled copies (dated in their headers) keep bootstrap offline-safe — prefer the live catalogue if it has advanced. The rendered `medplum.md` only *mentions* the override so tests don't re-index.
122
+
88
123
  Every plan-execution shell + `systematic-debugging` consumes the placeholder set below — render each by substituting from the inventory + Step 0 interview answers. `systematic-debugging` reuses the same set plus two extra placeholders (`{{OBSERVABILITY_MCP_BLOCK}}`, `{{OBSERVABILITY_MCP_LIST}}`) rendered from the catalogue.
89
124
 
90
125
  #### Rendering the plan-execution unit
@@ -150,6 +185,10 @@ The remaining per-shell placeholders:
150
185
  | `{{CLIENT_PLATFORMS_LIST}}` | `skills.handoff-to-client.client_platforms` (handoff-to-client only) | comma-separated, e.g. `iOS (Swift), Android (Kotlin), web SPA (React)` |
151
186
  | `{{CLIENT_HANDOFF_DIR}}` | `skills.handoff-to-client.output_dir` (handoff-to-client only) | `.vinta-ai-workflows/client-handoffs` (default) |
152
187
  | `{{API_SPEC_BLOCK}}` | derived from `skills.handoff-to-client.api_spec_path` (handoff-to-client only) | when set: a paragraph instructing the agent to regenerate/read the spec at that path on both base and branch and diff the two as the primary change-enumeration source (cross-checking against the code diff). When absent: a one-line note that no machine-readable spec exists, so shapes come from route/serializer code alone. |
188
+ | `{{FRAMEWORK_PACKS_LIST}}` | derived from the packs shipped (write-unit-test only) | comma-separated names of the runner + stack packs that landed under `resources/packs/`, e.g. `pytest + Django` or `vitest, React, TanStack Start`. Renders `the project's test framework` when only the runner shipped with no stack pack. |
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
+ | `{{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
+ | `{{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. |
153
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. |
154
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.` |
155
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`. |
@@ -221,6 +260,7 @@ Render each template (expand includes → substitute placeholders → write), pr
221
260
  - `ai-tools/skills/amend-plan/SKILL.md` (from `plan-execution/shell/amend-plan-template.md`)
222
261
  - `ai-tools/skills/systematic-debugging/SKILL.md` (from `systematic-debugging-template.md`) — only when `foundation_skills.systematic-debugging: enabled`
223
262
  - `ai-tools/skills/handoff-to-client/SKILL.md` (from `handoff-to-client-template.md`) — only when `foundation_skills.handoff-to-client: enabled`
263
+ - `ai-tools/skills/write-unit-test/SKILL.md` (from `write-unit-test-template.md`) **plus** `ai-tools/skills/write-unit-test/resources/packs/runners/<runner>.md` + `.../packs/stacks/<stack>.md` (verbatim from `write-unit-test-packs/`, only the detected runner + matched stacks) — only when `foundation_skills.write-unit-test: enabled` (default-on when a test framework was detected)
224
264
 
225
265
  The first five are the **plan-execution unit** — render them together (they share partials + placeholders + cross-links); a change to one partial ripples to every shell that includes it.
226
266
 
@@ -321,4 +361,5 @@ After all skills written:
321
361
  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`.
322
362
  5. **Plan-execution unit intact.** All five files exist; no surviving `<!-- include -->` markers; the conductor's `[implement-phase]` / `[review-phase]` / integrate-phase links resolve, and `amend-plan` + `systematic-debugging` (when shipped) → `[review-phase]` resolve. Under `ask`: both `integrate-phase-stacked` + `integrate-phase-modular` exist and the conductor's dispatch line names both.
323
363
  6. `implement-plan` (+ the plan-execution sub-skills) cite real commands + the project's actual code host + branch / PR / co-author policy.
324
- 7. Setup script ([vinta-install-ai-tools-setup](../vinta-install-ai-tools-setup/SKILL.md)) runs cleanly with the new skills in place.
364
+ 7. **`write-unit-test` (when shipped)** renders with no surviving `{{PLACEHOLDER}}`, cites the project's real test command, and every pack path in its `{{FRAMEWORK_PACK_LOADER_BLOCK}}` resolves to a file that actually landed under `ai-tools/skills/write-unit-test/resources/packs/`. One runner pack; stack packs only for frameworks the project actually uses (no `django.md` in a FastAPI repo, no web-framework pack in a pure-library repo). **Medplum projects**: confirm both FHIR-indexing setup files exist and are registered in `vitest.config.*` (`test.globalSetup` + `test.setupFiles`), `@medplum/definitions` is a devDependency, and no test re-indexes.
365
+ 8. Setup script ([vinta-install-ai-tools-setup](../vinta-install-ai-tools-setup/SKILL.md)) runs cleanly with the new skills in place.