vinta-ai-workflows 0.7.0-alpha16 → 0.7.0-alpha17

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
@@ -7,6 +7,7 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [0.7.0] — YYYY-MM-DD
9
9
 
10
+ <!-- pre-release: 0.7.0-alpha17 on 2026-09-17 -->
10
11
  <!-- pre-release: 0.7.0-alpha16 on 2026-09-16 -->
11
12
  <!-- pre-release: 0.7.0-alpha15 on 2026-09-16 -->
12
13
  <!-- pre-release: 0.7.0-alpha14 on 2026-09-16 -->
@@ -26,6 +27,54 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
26
27
 
27
28
  ### Added
28
29
 
30
+ - **The daemon now has a log of its own, and a Logs view to read it in.**
31
+ Transcripts said what the agents did; nothing said what the *daemon* did, so
32
+ "the run stopped and I don't know why" had no evidence behind it at all.
33
+ `.vinta-ai-maestro/logs/daemon.ndjson` now records what the process bound,
34
+ every HTTP request and socket upgrade it refused and why, every start request
35
+ that produced no run, every scheduler dispatch and node transition, and every
36
+ deadlock — on one clock, so a node failure can be read beside the refusal
37
+ thirty seconds before it. `GET /api/logs` serves it as a tail or a follow,
38
+ with filters for level, run, node and a substring of any event or field; the
39
+ UI's new **Logs** section follows the tail until you scroll up. The file
40
+ rotates at 8 MiB and keeps five rotations, so it is bounded whatever the
41
+ daemon's uptime, and `purge` leaves it alone for the same reason it leaves
42
+ `flow.db` alone — it holds no repository contents, and it is the record of the
43
+ failure somebody is about to ask about.
44
+
45
+ - **A crash is no longer silent.** An exception escaping a node's own handling
46
+ used to become an unhandled rejection on a promise the scheduler does not
47
+ hold, and an unhandled rejection ends the process: hours of agent turns and a
48
+ real amount of money, gone with no journal row, no transcript entry and
49
+ nothing on disk saying why — because the thing that would have said it was the
50
+ thing that died. Three changes. A node that throws is now **contained** by the
51
+ rule §6 already states for failures: it fails, its transitive dependents
52
+ block, and everything independent of it keeps going. A crash that is still
53
+ fatal is **recorded first** — `daemon.uncaught_exception` /
54
+ `daemon.unhandled_rejection`, with the error's kind, its stack frames and the
55
+ ids of every run in flight. And the in-flight runs are **journalled as ended**
56
+ before the process goes, so `run --resume <run-id>` picks them up instead of
57
+ finding a row that says `running` for ever. `serve` and `run` both take
58
+ `--log-level`, `--log-stderr` and `--log-detail`.
59
+
60
+ - **The log is identifiers only, enforced rather than asked for — plus the one
61
+ field that is deliberately prose.** A log is called from anywhere, and "please
62
+ do not log repository contents" is a hope rather than a control. So a field
63
+ may only hold a string, a number, a boolean or null — an object is dropped,
64
+ never stringified, which is the way a diff or a file read becomes a log line —
65
+ identifier values are capped at 200 characters, field names that are secrets
66
+ by their name are redacted, and the daemon's token is registered at boot so
67
+ any value containing it is written `<redacted>`.
68
+
69
+ The exception is an error's **`message`**, which every failure record carries
70
+ beside its kind, capped at 2000 characters and redacted like anything else. It
71
+ is the default because `.vinta-ai-maestro/runs/` already holds every agent
72
+ transcript and gate log *verbatim*, in the same gitignored store and under the
73
+ same `purge` — an error message is a rounding error against a directory that
74
+ is already a copy of the repository, and excluding it cost the one string that
75
+ most often explains a failure. `--log-detail kind` narrows to the kind and the
76
+ stack frames for a checkout under a stricter obligation than this store's own.
77
+
29
78
  - **A run no longer dies with the terminal that started it.** `vinta-ai-maestro
30
79
  serve` accepts `POST /api/runs`, so a run can be submitted to a daemon that
31
80
  was already listening and belongs to that daemon rather than to the shell that
@@ -369,6 +418,28 @@ the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
369
418
 
370
419
  ### Changed
371
420
 
421
+ - **Crew members are named after their tier, not after a seniority.** A roster
422
+ reads `tier1`, `tier2-1`, `tier2-2`, `tier4` and `reviewer` where it used to
423
+ read `junior`, `mid-1`, `mid-2` and `senior`. Nothing about the data model
424
+ moved — `tier` was already the 1-4 number the orchestrator staffs on, and it
425
+ is still the only thing compared when one member covers for another. What
426
+ changed is that the label beside it now says the same thing, instead of
427
+ offering a second vocabulary that could drift from it and that reads as a
428
+ ranking of people rather than of difficulty. Workflows using the old ids keep
429
+ running: the ids are free-form and nothing resolves them by name.
430
+
431
+ - **`plan-feature` emits its workflow with the plan's date prefix.** The
432
+ executable sibling is now
433
+ `ai-plans/YYYY-MM-DD-<feature-kebab>.workflow.json` — previously it carried
434
+ no date, which sorted it away from the `YYYY-MM-DD-FEATURE_NAME_PLAN.md` and
435
+ `_SPEC.md` it belongs to, so the one file you want with the plan open was the
436
+ one you had to go looking for. A feature's three files now land adjacent. The
437
+ case difference between them is not an oversight: the markdown convention is
438
+ `UPPERCASE_WITH_UNDERSCORES` and a workflow's filename stem *is* its `id`,
439
+ which the schema requires to be lowercase kebab-case, so the date prefix is
440
+ the only part the three can share. Existing dateless workflows are unaffected
441
+ — this is the emitter's convention, not a schema rule.
442
+
372
443
  - **A session cannot inherit a machine's decision to stop compacting.** All
373
444
  three harnesses compact automatically when their context fills — there was
374
445
  never a flag to turn on — but each honours an environment variable that turns
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vinta-ai-workflows",
3
- "version": "0.7.0-alpha16",
3
+ "version": "0.7.0-alpha17",
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": {
package/schemas/README.md CHANGED
@@ -12,7 +12,7 @@ JSON Schema (Draft 2020-12) contracts for every YAML or JSON payload the vinta-a
12
12
  | PR-context inline comments | YAML inside the ` ```yaml ... ``` ` fence under `# Comments` of the same file | [`prs-context-comments.v1.schema.json`](prs-context-comments.v1.schema.json) | `implement-plan` / `amend-plan` | `open-pr.sh` |
13
13
  | MCP preflight cache | `.vinta-ai-workflows/cache.yaml` (gitignored — per-developer-machine state) | [`mcp-preflight-cache.v1.schema.json`](mcp-preflight-cache.v1.schema.json) | rendered `systematic-debugging` SKILL.md (writes during Phase 0) | rendered `systematic-debugging` SKILL.md (reads at Phase 0 start) |
14
14
  | AI model tier table | `ai-tools/skills/plan-feature/resources/ai-models.yaml` (shipped verbatim with the `plan-feature` foundation skill; source: `skills/vinta-derive-skills/resources/foundation-skills/plan-feature/resources/ai-models.yaml`) | [`ai-models.v1.schema.json`](ai-models.v1.schema.json) | nightly `check-ai-models` job ([`scripts/check-ai-models.mjs`](../scripts/check-ai-models.mjs)) → auto-PR | `plan-feature` (per-phase model suggestion) |
15
- | Workflow (executable plan) | `ai-plans/<feature-kebab>.workflow.json` — **JSON, not YAML** | [`workflow.v1.schema.json`](workflow.v1.schema.json) | `plan-feature` (emitted alongside the human-readable plan) | `vinta-ai-maestro` (the orchestrator daemon) |
15
+ | Workflow (executable plan) | `ai-plans/YYYY-MM-DD-<feature-kebab>.workflow.json` — **JSON, not YAML** | [`workflow.v1.schema.json`](workflow.v1.schema.json) | `plan-feature` (emitted alongside the human-readable plan) | `vinta-ai-maestro` (the orchestrator daemon) |
16
16
  | Plan post-mortem | `.vinta-ai-maestro/runs/<run-id>/postmortem.json` — **JSON, not YAML**; per-run state beside the frozen workflow, copied to `ai-plans/<feature-kebab>.postmortem.json` when a team wants it committed | [`postmortem.v1.schema.json`](postmortem.v1.schema.json) | `vinta-ai-maestro` (once a run has ended) | `plan-feature` (when planning the next feature in the same repo) |
17
17
 
18
18
  > `workflow.v1.schema.json` and `postmortem.v1.schema.json` are the two schemas here that are **generated, not hand-written**. The workflow's source of truth is `packages/vinta-ai-maestro/src/types.ts`; regenerate with `pnpm --filter vinta-ai-maestro schema:gen` and verify with `schema:check`. The post-mortem's is `packages/vinta-ai-maestro/src/postmortem/postmortem.ts`; regenerate with `node --experimental-strip-types src/postmortem/generate.ts` from `packages/vinta-ai-maestro`, verify with the same command plus `--check` — `tests/postmortem.test.ts` runs that byte comparison too. Edit the zod schemas, never the JSON.
@@ -7,7 +7,7 @@ description: Author a phased implementation plan for a new feature following the
7
7
 
8
8
  Plans live in `ai-plans/` as `YYYY-MM-DD-FEATURE_NAME_IMPLEMENTATION_PLAN.md` (uppercase + underscores). `..._SPEC.md` sibling exists → **read first**. Plan translates spec into phased delivery, doesn't re-derive requirements. No spec? Point at [create-spec](../create-spec/SKILL.md) first; plan without spec = plausible-sounding but unverified. Spec/plan pair share `YYYY-MM-DD-FEATURE_NAME` prefix.
9
9
 
10
- Every plan ships **two** files: the markdown above, and its executable sibling `ai-plans/<feature-kebab>.workflow.json` — the same phase graph in the form an orchestrator runs. See "Emit the executable workflow". Written every time; never gated on a question.
10
+ Every plan ships **two** files: the markdown above, and its executable sibling `ai-plans/{TODAY}-<feature-kebab>.workflow.json` — same date prefix, the same phase graph in the form an orchestrator runs. See "Emit the executable workflow". Written every time; never gated on a question.
11
11
 
12
12
  ## Step 0 — Interrogate before drafting (NON-NEGOTIABLE)
13
13
 
@@ -226,15 +226,15 @@ dependency on each other and are implemented concurrently.
226
226
 
227
227
  | Wave | Phases | Agent | Depends on |
228
228
  |---|---|---|---|
229
- | 1 | Phase 0, Phase 1b | `junior`, `mid-2` | — |
230
- | 2 | Phase 1, Phase 2 | `mid-1`, `mid-2` | Phase 0 |
231
- | 3 | Phase 3 | `senior` | Phase 1, Phase 2 |
232
- | 4 | Phase 4 — remove the `bookmarks-v2` flag | `junior` | Phase 3 (deferred — soak-gated) |
229
+ | 1 | Phase 0, Phase 1b | `tier1`, `tier2-2` | — |
230
+ | 2 | Phase 1, Phase 2 | `tier2-1`, `tier2-2` | Phase 0 |
231
+ | 3 | Phase 3 | `tier4` | Phase 1, Phase 2 |
232
+ | 4 | Phase 4 — remove the `bookmarks-v2` flag | `tier1` | Phase 3 (deferred — soak-gated) |
233
233
 
234
234
  **File overlap:** phases in the same wave touch disjoint files, with one exception —
235
235
  Phase 1 and Phase 2 both export from `@app/bookmarks/__init__.py`. Trivial merge.
236
236
 
237
- **Idle:** `senior` has nothing until wave 3 and `mid-1` nothing until wave 2 — the
237
+ **Idle:** `tier4` has nothing until wave 3 and `tier2-1` nothing until wave 2 — the
238
238
  foundation phase is the whole of wave 1 and only one agent can write it.
239
239
  ```
240
240
 
@@ -262,7 +262,7 @@ Don't contort the plan for concurrency, though. A genuinely sequential feature i
262
262
 
263
263
  ### Read the previous runs' post-mortems before drawing the graph
264
264
 
265
- Every plan you write is a guess about coupling. Every plan the orchestrator *ran* turned that guess into evidence, and it wrote the evidence down: one `postmortem.json` per finished run under `.vinta-ai-maestro/runs/<run-id>/`, plus any copy the team committed beside its plan as `ai-plans/<feature-kebab>.postmortem.json`. **Read them before the `**Depends on**:` lines, not after.** Newest first, and all of them — one run is an anecdote, three runs saying the same thing about the same layer is a rule about this codebase.
265
+ Every plan you write is a guess about coupling. Every plan the orchestrator *ran* turned that guess into evidence, and it wrote the evidence down: one `postmortem.json` per finished run under `.vinta-ai-maestro/runs/<run-id>/`, plus any copy the team committed beside its plan as `ai-plans/{DATE}-<feature-kebab>.postmortem.json`. **Read them before the `**Depends on**:` lines, not after.** Newest first, and all of them — one run is an anecdote, three runs saying the same thing about the same layer is a rule about this codebase.
266
266
 
267
267
  ```bash
268
268
  ls -t .vinta-ai-maestro/runs/*/postmortem.json ai-plans/*.postmortem.json 2>/dev/null | head -5
@@ -430,7 +430,7 @@ The mandatory final flag-removal phase depends on **every** gated phase — it d
430
430
 
431
431
  A plan does not pick a model per phase. It **staffs a team**, then assigns each phase to somebody on it.
432
432
 
433
- The difference is not cosmetic. Choosing a tier phase by phase answers "what should run this one" ten times and never adds it up, so the two questions that actually decide what a feature costs go unasked: **how many agents does this need at once, and is any of them too junior for what it was handed.** A roster asks both before a line is written, and it is falsifiable — you can look at it and say "nobody needs three seniors here".
433
+ The difference is not cosmetic. Choosing a tier phase by phase answers "what should run this one" ten times and never adds it up, so the two questions that actually decide what a feature costs go unasked: **how many agents does this need at once, and is any of them below the tier of what it was handed.** A roster asks both before a line is written, and it is falsifiable — you can look at it and say "nobody needs three Tier 4 members here".
434
434
 
435
435
  ### The Crew table
436
436
 
@@ -441,13 +441,17 @@ First thing under **Phased Rollout**, before the **Execution graph**:
441
441
 
442
442
  | Agent | Role | Tier | Takes | Why this tier |
443
443
  |---|---|---|---|---|
444
- | `junior` | implementer | 1 | Phase 0, Phase 4 | An empty module and a flag deletion — exact precedent, both of them. |
445
- | `mid-1` | implementer | 2 | Phase 1 | A DRF viewset mirroring `@app/tags/api/views.py` almost line for line. |
446
- | `mid-2` | implementer | 2 | Phase 1b, Phase 2 | Serializer with cross-field validation; the producer stub is the same shape. |
447
- | `senior` | implementer | 4 | Phase 3 | Cycle detection over a user-mutable tree — no precedent in this repo. |
444
+ | `tier1` | implementer | 1 | Phase 0, Phase 4 | An empty module and a flag deletion — exact precedent, both of them. |
445
+ | `tier2-1` | implementer | 2 | Phase 1 | A DRF viewset mirroring `@app/tags/api/views.py` almost line for line. |
446
+ | `tier2-2` | implementer | 2 | Phase 1b, Phase 2 | Serializer with cross-field validation; the producer stub is the same shape. |
447
+ | `tier4` | implementer | 4 | Phase 3 | Cycle detection over a user-mutable tree — no precedent in this repo. |
448
448
  | `reviewer` | reviewer | 4 | — reviews every phase | Reads the tree phase too; a reviewer below the hardest phase can never take it. |
449
449
  ```
450
450
 
451
+ **Name members after their tier, never after a seniority.** An implementer is `tier<N>`, or `tier<N>-<k>` when the roster carries more than one at that tier; a reviewer is `reviewer`, or `reviewer-<k>` when there are several. Ids are lowercase kebab-case — the schema rejects anything else.
452
+
453
+ The rule exists because the tier *is* the whole of what the orchestrator knows about a member: it is the floor on what they may be handed and the only thing compared when one covers for another. A roster that names members after seniority grades names people instead, and invites two mistakes — reading a capability ranking as a career one, and letting the label drift from the `tier` beside it, which is the number that actually decides anything.
454
+
451
455
  **Implementers write; reviewers read; nobody does both.** That is not a convention, it is the shape of the document — an implementer cannot be assigned a review and a reviewer cannot be assigned a phase, so an agent grading its own work is not something a plan can express. It replaces the older "review one tier above the author" rule, which was a proxy for independence and broke in both directions: a phase covered by the top-tier implementer had nobody above it, and a tier says nothing about *who* now that a member is a durable agent rather than a model id.
452
456
 
453
457
  Staff **at least one reviewer** on every plan. A roster of implementers only still runs — reviews fall back to the project's `agent_models.reviewer`, cold, one session per phase — but the reviewer then never accumulates anything about the codebase, which is most of what makes review cheap by phase three.
@@ -456,25 +460,25 @@ Staff **at least one reviewer** on every plan. A roster of implementers only sti
456
460
 
457
461
  **Every implementer has to earn their place, and there are exactly two ways to do it:**
458
462
 
459
- - **Concurrency.** A wave of three phases needs three pairs of hands, and they must be hands *at or above* each of those phases' tiers — two mids and a junior cannot run three Tier 2 phases two-wide, because the junior is not allowed to take one. So go wave by wave: sort the wave's phase tiers, sort the roster's tiers, and check the roster covers them one for one.
460
- - **Cheapness.** A member below the roster's other tiers earns their place by taking work the dearer members would otherwise do. A junior who runs the migration and the flag deletion is worth having even in a graph that is never two phases wide, because those two phases run at Tier 1 instead of Tier 2.
463
+ - **Concurrency.** A wave of three phases needs three pairs of hands, and they must be hands *at or above* each of those phases' tiers — two Tier 2 members and a Tier 1 cannot run three Tier 2 phases two-wide, because the Tier 1 member is not allowed to take one. So go wave by wave: sort the wave's phase tiers, sort the roster's tiers, and check the roster covers them one for one.
464
+ - **Cheapness.** A member below the roster's other tiers earns their place by taking work the dearer members would otherwise do. A Tier 1 member who runs the migration and the flag deletion is worth having even in a graph that is never two phases wide, because those two phases run at Tier 1 instead of Tier 2.
461
465
 
462
- **A member who does neither should not be on the roster** — a second mid in a graph that is never two wide adds no concurrency and saves nothing, and the executor refuses a member assigned no phase at all.
466
+ **A member who does neither should not be on the roster** — a second Tier 2 member in a graph that is never two wide adds no concurrency and saves nothing, and the executor refuses a member assigned no phase at all.
463
467
 
464
468
  Note what this does *not* say: the roster is not capped at the widest wave. Concurrency is capped there, but a cheaper member is bought with money, not with parallelism, and adding one to a narrow graph is a perfectly good trade — it now costs a worktree, which is the honest price (see "Every member gets a desk").
465
469
 
466
470
  Two more rules:
467
471
 
468
- - **Prefer fewer tiers over more.** Two mids beat one mid and one "upper-mid": a tier is a floor on what a member may be handed, so a roster that draws fine distinctions only makes it harder to cover for a busy peer.
472
+ - **Prefer fewer tiers over more.** Two members at Tier 2 beat one at Tier 2 and one at Tier 3 when both phases are Tier 2 work: a tier is a floor on what a member may be handed, so a roster that draws fine distinctions only makes it harder to cover for a busy peer.
469
473
  - **The `Takes` column is the assignment**, and it must agree with every phase's `**Assigned to**:` line. They are two renderings of one decision, so write the phases and let the table fall out — never the reverse.
470
474
 
471
475
  ### Assigning a phase
472
476
 
473
477
  Two constraints, and they pull against each other. That is what makes this a judgement rather than a lookup.
474
478
 
475
- **The rubric tier is a floor.** Never hand a Tier 3 phase to a junior to save money. A cheap model on work above its tier does not fail cleanly — it produces plausible code that fails review two rounds later, and by then nothing points back at the staffing decision. Default to the cheapest tier that plausibly works; that is not the same as the cheapest tier available.
479
+ **The rubric tier is a floor.** Never hand a Tier 3 phase to a Tier 1 member to save money. A cheap model on work above its tier does not fail cleanly — it produces plausible code that fails review two rounds later, and by then nothing points back at the staffing decision. Default to the cheapest tier that plausibly works; that is not the same as the cheapest tier available.
476
480
 
477
- **Nobody should be idle while work they could do is queued.** If the senior is busy from wave 1 to wave 4 and the two juniors are idle after wave 1, the plan is a senior-shaped chain with decoration. Rebalance by splitting a phase or moving a dependency, not by handing the senior's work to a junior.
481
+ **Nobody should be idle while work they could do is queued.** If the Tier 4 member is busy from wave 1 to wave 4 and the two Tier 1 members are idle after wave 1, the plan is a Tier 4-shaped chain with decoration. Rebalance by splitting a phase or moving a dependency, not by handing Tier 4 work down to Tier 1.
478
482
 
479
483
  When those two genuinely cannot both hold, **the floor wins and the plan says so** in the **Idle** note. A wave that serializes is a schedule; a phase run below its tier is a defect.
480
484
 
@@ -562,7 +566,17 @@ Don't mix styles within one sentence. In **Touch List**, use `@path` for new fil
562
566
 
563
567
  ## Emit the executable workflow
564
568
 
565
- Alongside the markdown plan, write `ai-plans/<feature-kebab>.workflow.json` — same directory, feature name lowercased with hyphens (`BOOKMARK_FOLDERS` → `ai-plans/bookmark-folders.workflow.json`), no date prefix. The markdown is what humans review; the JSON is the same phase graph in the form an orchestrator runs — one worktree lane per phase, branches cut from each phase's dependencies, gates queued behind capacity limits instead of stampeding.
569
+ Alongside the markdown plan, write `ai-plans/{TODAY}-<feature-kebab>.workflow.json` — same directory, same date prefix as the plan and the spec, feature name lowercased with hyphens (`2026-03-04` + `BOOKMARK_FOLDERS` → `ai-plans/2026-03-04-bookmark-folders.workflow.json`). The markdown is what humans review; the JSON is the same phase graph in the form an orchestrator runs — one worktree lane per phase, branches cut from each phase's dependencies, gates queued behind capacity limits instead of stampeding.
570
+
571
+ **The date prefix is what keeps the three files together.** `ai-plans/` accumulates every feature this repo has ever planned, and a dateless `bookmark-folders.workflow.json` sorts into a different part of the directory from the `2026-03-04-BOOKMARK_FOLDERS_PLAN.md` it belongs to — so the one file you need when the plan is in front of you is the one you have to search for. With the prefix a feature's three files land adjacent:
572
+
573
+ ```
574
+ ai-plans/2026-03-04-BOOKMARK_FOLDERS_PLAN.md
575
+ ai-plans/2026-03-04-BOOKMARK_FOLDERS_SPEC.md
576
+ ai-plans/2026-03-04-bookmark-folders.workflow.json
577
+ ```
578
+
579
+ The case difference is not an inconsistency to fix: the markdown convention is `UPPERCASE_WITH_UNDERSCORES`, and the workflow's stem **is** its `id`, which the schema requires to be lowercase kebab-case. They cannot be spelled the same way, so they share the one part that can be shared.
566
580
 
567
581
  **Unconditional.** Write it on every plan. Don't ask, don't gate it on a config field, don't skip it because the project has no orchestrator installed — a project without one carries a few KB it never reads, and a project that installs one later finds its plans already executable. The one thing that is *not* free is emitting it inconsistently: a half-populated `ai-plans/` teaches the team the file is optional.
568
582
 
@@ -576,7 +590,7 @@ First key in the file is `"$schema"`, pointing at `https://github.com/vintasoftw
576
590
  |---|---|
577
591
  | `$schema` | The URL above, literally. |
578
592
  | `schema_version` | `1`. |
579
- | `id` | The feature kebab — same slug as the filename. It lands in branch names (`plan/{id}/wave-2`), so kebab-case only, no dates, no underscores. |
593
+ | `id` | `{TODAY}-<feature-kebab>` — the filename's stem, exactly. Digits and hyphens only, no underscores and no uppercase: it is both the filename the daemon resolves and the slug in every branch name (`plan/2026-03-04-bookmark-folders/wave-2`), and the daemon refuses a workflow whose `id` and filename disagree. |
580
594
  | `plan_ref` | Repo-relative path of the markdown plan, e.g. `ai-plans/2026-03-04-BOOKMARK_FOLDERS_IMPLEMENTATION_PLAN.md`. |
581
595
  | `plan_context_refs` | The two plan sections that bound **every** phase, as anchors into the plan you just wrote: `<plan_ref>#1-goals` (which carries Non-goals with it) and `<plan_ref>#2-guiding-decisions`, in that order. Same file-and-anchor form as `prompt_ref`. See "Plan-level context". |
582
596
  | `base_branch` | What `**Depends on**: nothing — starts from the base branch` means concretely: the repo's default branch, unless **Guiding Decisions** names a long-lived feature branch. |
@@ -705,18 +719,18 @@ staffed by this **Crew** table:
705
719
  ```markdown
706
720
  | Agent | Role | Tier | Takes | Why this tier |
707
721
  |---|---|---|---|---|
708
- | `junior` | implementer | 1 | Phase 1, Phase 5 | A model plus its migration, and a flag deletion. Exact precedent, both. |
709
- | `mid-1` | implementer | 2 | Phase 2 | A DRF viewset mirroring the tags viewset almost line for line. |
710
- | `mid-2` | implementer | 2 | Phase 3, Phase 4 | Tree serializer and the list action that returns it — same shape twice. |
722
+ | `tier1` | implementer | 1 | Phase 1, Phase 5 | A model plus its migration, and a flag deletion. Exact precedent, both. |
723
+ | `tier2-1` | implementer | 2 | Phase 2 | A DRF viewset mirroring the tags viewset almost line for line. |
724
+ | `tier2-2` | implementer | 2 | Phase 3, Phase 4 | Tree serializer and the list action that returns it — same shape twice. |
711
725
  | `reviewer` | reviewer | 2 | — reviews every phase | Tier 2 is the hardest phase here, so one reviewer covers the plan. |
712
726
  ```
713
727
 
714
728
  Three implementers for a graph that is never more than **two** phases wide,
715
- which is the case worth reading closely. The two mids are there for concurrency:
716
- wave 2 is two Tier 2 phases, and two hands at Tier 2 is the only way to run it
717
- two-wide — a junior cannot take one of them. `junior` is there for cheapness:
718
- without them, Phase 1 and Phase 5 would run on a mid's model for work that has
719
- exact precedent. `junior` is idle in waves 2 and 3 and that is not a defect; a
729
+ which is the case worth reading closely. The two Tier 2 members are there for
730
+ concurrency: wave 2 is two Tier 2 phases, and two hands at Tier 2 is the only
731
+ way to run it two-wide — `tier1` cannot take one of them. `tier1` is there for
732
+ cheapness: without them, Phase 1 and Phase 5 would run on a Tier 2 model for
733
+ work that has exact precedent. `tier1` is idle in waves 2 and 3 and that is not a defect; a
720
734
  fourth implementer would be, because there would be nothing left for them to
721
735
  make cheaper and no third phase for them to run alongside.
722
736
 
@@ -731,22 +745,22 @@ and this **Execution graph** table:
731
745
  ```markdown
732
746
  | Wave | Phases | Agent | Depends on |
733
747
  |---|---|---|---|
734
- | 1 | Phase 1 | `junior` | — |
735
- | 2 | Phase 2, Phase 3 | `mid-1`, `mid-2` | Phase 1 |
736
- | 3 | Phase 4 | `mid-2` | Phase 2, Phase 3 |
737
- | 4 | Phase 5 — remove the `bookmark-folders` flag | `junior` | Phase 2, Phase 3, Phase 4 (deferred — soak-gated) |
748
+ | 1 | Phase 1 | `tier1` | — |
749
+ | 2 | Phase 2, Phase 3 | `tier2-1`, `tier2-2` | Phase 1 |
750
+ | 3 | Phase 4 | `tier2-2` | Phase 2, Phase 3 |
751
+ | 4 | Phase 5 — remove the `bookmark-folders` flag | `tier1` | Phase 2, Phase 3, Phase 4 (deferred — soak-gated) |
738
752
 
739
- **Idle:** both mids in wave 1 — the model has to exist before anything reads it.
740
- `mid-1` in wave 3, `junior` in waves 2 and 3.
753
+ **Idle:** both Tier 2 members in wave 1 — the model has to exist before anything
754
+ reads it. `tier2-1` in wave 3, `tier1` in waves 2 and 3.
741
755
  ```
742
756
 
743
- and this `ai-plans/bookmark-folders.workflow.json`:
757
+ and this `ai-plans/2026-03-04-bookmark-folders.workflow.json`:
744
758
 
745
759
  ```json
746
760
  {
747
761
  "$schema": "https://github.com/vintasoftware/vinta-ai-workflows/schemas/workflow.v1.schema.json",
748
762
  "schema_version": 1,
749
- "id": "bookmark-folders",
763
+ "id": "2026-03-04-bookmark-folders",
750
764
  "plan_ref": "ai-plans/2026-03-04-BOOKMARK_FOLDERS_IMPLEMENTATION_PLAN.md",
751
765
  "plan_context_refs": [
752
766
  "ai-plans/2026-03-04-BOOKMARK_FOLDERS_IMPLEMENTATION_PLAN.md#1-goals",
@@ -773,19 +787,19 @@ and this `ai-plans/bookmark-folders.workflow.json`:
773
787
  }
774
788
  },
775
789
  "crew": {
776
- "junior": {
790
+ "tier1": {
777
791
  "role": "implementer",
778
792
  "tier": 1,
779
793
  "model": "claude-haiku-4-5",
780
794
  "description": "A model plus its migration, and a flag deletion."
781
795
  },
782
- "mid-1": {
796
+ "tier2-1": {
783
797
  "role": "implementer",
784
798
  "tier": 2,
785
799
  "model": "claude-sonnet-5",
786
800
  "description": "A DRF viewset mirroring the tags viewset."
787
801
  },
788
- "mid-2": {
802
+ "tier2-2": {
789
803
  "role": "implementer",
790
804
  "tier": 2,
791
805
  "model": "claude-sonnet-5",
@@ -843,7 +857,7 @@ and this `ai-plans/bookmark-folders.workflow.json`:
843
857
  "types",
844
858
  "unit"
845
859
  ],
846
- "crew": "junior"
860
+ "crew": "tier1"
847
861
  },
848
862
  {
849
863
  "id": "p2",
@@ -864,7 +878,7 @@ and this `ai-plans/bookmark-folders.workflow.json`:
864
878
  "types",
865
879
  "unit"
866
880
  ],
867
- "crew": "mid-1"
881
+ "crew": "tier2-1"
868
882
  },
869
883
  {
870
884
  "id": "p3",
@@ -884,7 +898,7 @@ and this `ai-plans/bookmark-folders.workflow.json`:
884
898
  "types",
885
899
  "unit"
886
900
  ],
887
- "crew": "mid-2"
901
+ "crew": "tier2-2"
888
902
  },
889
903
  {
890
904
  "id": "p4",
@@ -909,7 +923,7 @@ and this `ai-plans/bookmark-folders.workflow.json`:
909
923
  "types",
910
924
  "unit"
911
925
  ],
912
- "crew": "mid-2"
926
+ "crew": "tier2-2"
913
927
  },
914
928
  {
915
929
  "id": "p5",
@@ -940,7 +954,7 @@ and this `ai-plans/bookmark-folders.workflow.json`:
940
954
  "types",
941
955
  "unit"
942
956
  ],
943
- "crew": "junior"
957
+ "crew": "tier1"
944
958
  }
945
959
  ]
946
960
  }
@@ -948,15 +962,15 @@ and this `ai-plans/bookmark-folders.workflow.json`:
948
962
 
949
963
  Read the three renderings against each other: `p2` and `p3` both name only `p1`, so they sit in wave 2 and run at once; `p4` names both, so it is wave 3; `p5` names every gated phase, so it is wave 4 and alone there. `p2` and `p4` both touch `apps/bookmarks/api/views.py` — allowed, because the edge between them puts them in different waves; had they been same-wave, that overlap is what "Same-wave phases must not fight over the same files" is about.
950
964
 
951
- No node carries a `model`. `p1` and `p5` are the Tier 1 phases and run on `junior`'s model because that is who took them — the roster says it once instead of two nodes repeating an id that goes stale on the next model bump.
965
+ No node carries a `model`. `p1` and `p5` are the Tier 1 phases and run on `tier1`'s model because that is who took them — the roster says it once instead of two nodes repeating an id that goes stale on the next model bump.
952
966
 
953
- `mid-2` takes `p3` and then `p4`, which is the pairing worth seeing: the same agent writes the tree serializer and the action that returns it, in the same worktree, **continuing the same session**. Its second phase does not pay to work out where the serializers live or how the suite is run — it did that in `p3`. What it *is* told, because the tree moved underneath it, is that it is now on `p4`'s branch, that `p3`'s work is in this tree (`p4` depends on it), and exactly which files differ from what it last saw.
967
+ `tier2-2` takes `p3` and then `p4`, which is the pairing worth seeing: the same agent writes the tree serializer and the action that returns it, in the same worktree, **continuing the same session**. Its second phase does not pay to work out where the serializers live or how the suite is run — it did that in `p3`. What it *is* told, because the tree moved underneath it, is that it is now on `p4`'s branch, that `p3`'s work is in this tree (`p4` depends on it), and exactly which files differ from what it last saw.
954
968
 
955
969
  No node names a reviewer. Every phase is read by `reviewer`, the only member on the roster whose role is to read them — so no agent ever reads its own diff. Its session carries only between reviews that land in the same lane, because it goes where the work is rather than keeping a tree of its own.
956
970
 
957
971
  `plan_context_refs` points at the same plan file the `prompt_ref`s do, at its **Goals** and **Guiding Decisions** headings. Each of the five phases is handed those two sections whole, so the implementer of `p3` knows that the tree serializer is deliberately not paginated if the plan's Non-goals said so, and the reviewer of `p3` can call a paginated one scope creep instead of a bonus.
958
972
 
959
- The lane pool is **three**: one desk per implementer, kept for the whole run so each one's session has a directory to come back to. Only two are ever busy at once — the graph is never wider than that — and the third idle desk is the price of `junior`'s session. `reviewer` has no desk at all; it reads whichever lane it is reviewing, changes still uncommitted, which is what lets a finding be fixed before the commit rather than after it. The `project` block is what lets those worktrees exist at once. `bookmarks_test` is forked per lane from a template that `uv run python manage.py migrate` builds once, so `p2` and `p3` run `uv run pytest` against separate rows instead of the same ones; `test-suite` stays at capacity 1 because three suites at once melt the machine, not because they would corrupt each other. `dev` and `test` name two different databases, which is what keeps their forks from being the same database under two roles.
973
+ The lane pool is **three**: one desk per implementer, kept for the whole run so each one's session has a directory to come back to. Only two are ever busy at once — the graph is never wider than that — and the third idle desk is the price of `tier1`'s session. `reviewer` has no desk at all; it reads whichever lane it is reviewing, changes still uncommitted, which is what lets a finding be fixed before the commit rather than after it. The `project` block is what lets those worktrees exist at once. `bookmarks_test` is forked per lane from a template that `uv run python manage.py migrate` builds once, so `p2` and `p3` run `uv run pytest` against separate rows instead of the same ones; `test-suite` stays at capacity 1 because three suites at once melt the machine, not because they would corrupt each other. `dev` and `test` name two different databases, which is what keeps their forks from being the same database under two roles.
960
974
 
961
975
  ## What to avoid
962
976
 
@@ -1019,7 +1033,7 @@ When in doubt, model the plan after a recent example in `ai-plans/` — look for
1019
1033
  - [ ] Open Questions lists what couldn't resolve, with recommended default.
1020
1034
  - [ ] Touch List groups files by phase.
1021
1035
  - [ ] All file references use `@path/to/file.py` or `[name](relative-path#Lline)`.
1022
- - [ ] **`ai-plans/<feature-kebab>.workflow.json` written** — every plan, no exceptions — with `$schema` set to the canonical URL and `schema_version: 1`.
1036
+ - [ ] **`ai-plans/{TODAY}-<feature-kebab>.workflow.json` written** — every plan, no exceptions — with `$schema` set to the canonical URL and `schema_version: 1`.
1023
1037
  - [ ] Workflow graph matches the **Execution graph** table: one node per phase, one `depends_on` entry per `**Depends on**:` clause carrying both the node id and the artifact, same waves.
1024
1038
  - [ ] Every node has `prompt_ref` (`<plan_ref>#phase-<number>`), `touches` from its **Touch List** block, and the `gates` it must pass.
1025
1039
  - [ ] **`plan_context_refs` names the Goals and Guiding Decisions anchors** (`<plan_ref>#1-goals`, `<plan_ref>#2-guiding-decisions`), matching the headings as written — references, never a summary of them. Every phase's implementer and reviewer read them; a phase that doesn't know the non-goals is a phase that scope-creeps.
@@ -20,7 +20,7 @@ The plan's **Phased Rollout** section opens with an **Execution graph** table an
20
20
  - **A phase assigned to a reviewer, or a reviewer that also takes phases** → stop and ask. The two roles are disjoint precisely so that an agent reviewing its own work is unrepresentable.
21
21
  - **A reviewer below every phase on the plan** → stop and ask. A reviewer's tier is a floor too, so one below the cheapest phase would never be picked.
22
22
  - **No reviewer at all** → not an error. Reviews fall back to `agent_models.reviewer`, cold, one session per phase. Say so once in the pool report so nobody reads the roster and assumes otherwise.
23
- - **A wave the roster cannot staff** → warn. Sort the wave's assigned tiers and the roster's tiers, compare one for one: a wave of two Tier 3 phases needs two members at Tier 3 or above, and a junior on the roster does not help because the floor forbids handing them one. Such a wave still runs — it serializes — so say so now rather than letting it look like a slow machine later.
23
+ - **A wave the roster cannot staff** → warn. Sort the wave's assigned tiers and the roster's tiers, compare one for one: a wave of two Tier 3 phases needs two members at Tier 3 or above, and a Tier 1 member on the roster does not help because the floor forbids handing them one. Such a wave still runs — it serializes — so say so now rather than letting it look like a slow machine later.
24
24
  - **A plan with no Crew table at all** is a legacy plan. Read each phase's `**Suggested AI model**:` tier instead and skip every check above.
25
25
  6. **Record the resolved graph and the roster** in `run.md` (see [Update tracking](#1d-update-tracking)) so a resume rebuilds the identical schedule and the identical staffing without re-asking anything.
26
26
 
@@ -169,7 +169,7 @@ while PENDING or RUNNING:
169
169
  `claim_agent(p)` is the staffing half of a dispatch, and it runs **before** the lane is taken. A lane is disk; who is holding it is what the phase costs and whether the result is any good.
170
170
 
171
171
  1. **The member the plan assigned, if they are free.** The common case, and the one the plan's cost estimate is written against.
172
- 2. **Otherwise the cheapest free member at or above that member's tier.** A wave should not serialize behind one agent when a qualified peer is idle. Reach for the *cheapest* qualified one, not the best available — covering for a peer must not quietly promote the phase to the top tier, or a busy wave silently runs every mid-tier phase on the senior's model.
172
+ 2. **Otherwise the cheapest free member at or above that member's tier.** A wave should not serialize behind one agent when a qualified peer is idle. Reach for the *cheapest* qualified one, not the best available — covering for a peer must not quietly promote the phase to the top tier, or a busy wave silently runs every Tier 2 phase on the Tier 4 member's model.
173
173
  3. **Otherwise nobody, and the phase waits** — even with a lane free. This is the one place staffing costs throughput, and it is deliberate: a phase run below its tier does not fail cleanly. It produces plausible code that fails review two rounds later, by which point nothing points back at the staffing decision.
174
174
 
175
175
  An implementer is held for the **whole phase**, not one turn of it: the fixer answering a review finding is the implementer continuing its own session, so handing the phase to someone else mid-flight would hand it to an agent with no session to continue. Release it when the phase settles.