@attalabs/vinaya 0.17.1 → 0.19.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 (87) hide show
  1. package/README.md +37 -1
  2. package/aeg-root/contracts/architect-planner.md +72 -0
  3. package/aeg-root/roles/architect.md +91 -0
  4. package/aeg-root/roles/planner.md +14 -15
  5. package/aeg-root/tranche-model.md +9 -1
  6. package/dist/checks/bin/check-body-bare-digits.js +62 -14
  7. package/dist/checks/bin/check-branch-topology.js +57 -12
  8. package/dist/checks/bin/check-brief-shape.js +57 -12
  9. package/dist/checks/bin/check-closes-n.js +57 -12
  10. package/dist/checks/bin/check-coherence.js +62 -14
  11. package/dist/checks/bin/check-dead-branch-push.js +57 -12
  12. package/dist/checks/bin/check-dispatch-readiness.js +129 -41
  13. package/dist/checks/bin/check-doc-coverage-push.js +62 -14
  14. package/dist/checks/bin/check-doc-coverage.js +62 -14
  15. package/dist/checks/bin/check-evidence-fresh.js +57 -12
  16. package/dist/checks/bin/check-first-push-dispatch.js +130 -42
  17. package/dist/checks/bin/check-issue-assignment.js +57 -12
  18. package/dist/checks/bin/check-no-disk-state.js +57 -12
  19. package/dist/checks/bin/check-reader-resolvable-prose.js +57 -12
  20. package/dist/checks/bin/check-registry-gates.js +57 -12
  21. package/dist/checks/bin/check-review-gate.js +62 -14
  22. package/dist/checks/bin/check-single-plan-pr.js +57 -12
  23. package/dist/checks/bin/check-test-plan.js +57 -12
  24. package/dist/index.js +889 -109
  25. package/package.json +1 -1
  26. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  27. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  28. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  29. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  30. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  31. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  32. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  33. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  34. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  35. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  36. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  37. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  38. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  39. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  40. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  41. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  42. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  43. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  44. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  45. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  46. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  47. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  48. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  49. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  50. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  51. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  52. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  53. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  54. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  55. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  56. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  57. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  58. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  59. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  60. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  61. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  62. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/[root-of-the-server]__0e95dv5._.js +1 -1
  63. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/1q96_modules_@clerk_nextjs_dist_esm_app-router_client_keyless-creator-reader_0lom2js.js +1 -1
  64. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__029-1ch._.js +1 -1
  65. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0q56ohl._.js +1 -1
  66. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1ccgndq._.js → [root-of-the-server]__0ulzsom._.js} +1 -1
  67. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1_ojzp1._.js +1 -1
  68. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1d_8nm4._.js +1 -1
  69. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1zkwtx8._.js +1 -1
  70. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__210ehg9._.js +1 -1
  71. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_00yjcv1._.js +1 -1
  72. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0rix02i._.js +1 -1
  73. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_18-d2be._.js +1 -1
  74. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +5 -5
  75. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1gw6vc9._.js +1 -1
  76. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  77. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  78. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  79. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  80. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  81. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{40x7f5hm7a5tk.js → 2r36w820aqwb9.js} +1 -1
  82. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0ojfbn3yvy-8_.js → 2xoclb4g7d9n7.js} +1 -1
  83. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0l_vubs1ephe5.js → 3ocvtlhgckh81.js} +1 -1
  84. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  85. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{p5sWGp4ANwORiaX4Kk6U2 → RISz5nirLr1mUfMqzSaFS}/_buildManifest.js +0 -0
  86. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{p5sWGp4ANwORiaX4Kk6U2 → RISz5nirLr1mUfMqzSaFS}/_clientMiddlewareManifest.js +0 -0
  87. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{p5sWGp4ANwORiaX4Kk6U2 → RISz5nirLr1mUfMqzSaFS}/_ssgManifest.js +0 -0
package/README.md CHANGED
@@ -20,7 +20,9 @@ npx @attalabs/vinaya init # or: pnpm dlx / yarn dlx / bunx
20
20
  | `vinaya check <name> \| --all` | Run one check, or every registered check (core + `vinaya.config.json`-registered). `--json` for the enveloped `{ checks: CheckOutcome[] }` form; `--diff-only` scopes `scope: 'diff'` checks to changed files; `--parallel[=n]` caps concurrency (default: cpu-derived). Findings always print as the check contract's JSON lines on stderr, regardless of `--json`. Exit 0 iff every check passed. |
21
21
  | `vinaya new check <yourname>/<id>` | Scaffold a self-contained custom check into `./scripts/vinaya-checks/<id>.ts`, ready to register in `vinaya.config.json` under that namespaced key |
22
22
  | `vinaya review post --role code-reviewer \| security --pr <n> ...` | Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR from structured flags (verdict, findings, per-field text) instead of a hand-typed comment. Resolves the PR's real head itself (`gh pr view --json headRefOid`); renders every structural `VERDICT:`/`Judged head:` line from validated inputs, never from caller-supplied text; refuses a contradictory verdict (a BLOCKER/CRITICAL-or-HIGH finding with a clean verdict) before posting anything; and after posting, re-fetches the comment and refuses to exit 0 unless it re-parses through the exact `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` functions the merge gate calls. `--json` for the enveloped machine form. |
23
- | `vinaya studio` | Launch Vinaya Studio. Inside a checkout that carries Studio's source (`apps/vinaya-studio/web` — it lives in the attalabs monorepo, not this repository) it runs the dev app; a published install runs its bundled standalone server instead, fetched from attalabs' published release artifact at publish time. |
23
+ | `vinaya studio` | Launch Vinaya Studio. Inside a checkout that carries Studio's source (`apps/vinaya-studio/web` — it lives in the attalabs monorepo, not this repository) it runs the dev app; a published install runs its bundled standalone server instead, fetched from attalabs' published release artifact at publish time. `--port <n>` binds an exact port — see below. |
24
+ | `vinaya milestone create --title <title> --body-file <path>` | Create a GitHub Milestone from a validated body. The title is free text, never parsed for a version — the description's optional `Release:` field is the sole authority for one. Refuses before any `gh` write when the goal is absent, `Release:` is present but malformed, or an `### Tranche intents` section (`- <slug>: <intent text>`) doesn't parse. `--validate-only` runs every check without writing; `--json` for the enveloped machine form. |
25
+ | `vinaya milestone adopt --target <title> --slug <slug> [--slug <slug> ...]` | Move one or more existing tranches into a target Milestone: reattaches every Issue carrying each `vinaya/tranche:<slug>` label to `--target`, then closes (never deletes) each slug's old tranche-Milestone. Every fact is gathered and refused-or-passed as ONE batch before any write — an unknown slug, a slug whose label carries no Issues, a target that does not exist or is closed, or a slug already adopted into a different Milestone refuses the whole invocation, not just its own slug. `--validate-only` runs every check without writing; `--json` for the enveloped machine form. |
24
26
 
25
27
  ## Config
26
28
 
@@ -74,6 +76,8 @@ One thing git cannot version is the config itself: **each fresh clone runs `git
74
76
 
75
77
  Two shapes deviate: a repo already using **husky** keeps its `.husky/` directory (husky's `prepare` script owns per-clone wiring), and a repo with its own active raw hooks in `.git/hooks` stays on the legacy append-a-managed-block layout there — re-routing `core.hooksPath` would silently disable the adopter's own hooks. On that legacy layout `vinaya doctor` warns that clones have no hooks, and `vinaya upgrade` migrates to the tracked layout as soon as nothing foreign would be disabled.
76
78
 
79
+ On that legacy `.git/hooks` layout, note where the hook actually lives: hooks are never per-worktree, so its real home is the main checkout's shared hooks directory (`git rev-parse --git-common-dir`), which from a linked worktree is outside that worktree's own root. `vinaya eject` follows the hook there and bounds the removal to that directory's `hooks/` subtree, rather than to the worktree root — so ejecting from a linked worktree strips the hook instead of leaving it armed. A canonically spelled block path resolving anywhere else is refused, and the refusal is whole-run: `eject` removes nothing at all rather than making a partial destructive pass. (The `.git/` test is byte-exact, so a non-canonical spelling takes the working-tree branch instead — nothing vinaya generates produces one.)
80
+
77
81
  ## Agent skills
78
82
 
79
83
  The emitter that will generate skill pointers under `.agents/skills/vinaya-<role>/SKILL.md`, for tools natively scanning `.agents/skills/` (Codex, Antigravity, Grok Build), is built — `vinaya init` does not call it yet. Each file, once wired, is a 3-line pointer delegating to `vinaya doctrine --role <role>` at read time. User-facing flag, `init`/`upgrade`/`eject`/`doctor` wiring, and the `--agents` selection flag land in #152.
@@ -136,6 +140,38 @@ The generated review-authority workflows run only default-branch code: the requi
136
140
 
137
141
  `.vinaya/doc-owners` binds code globs to the docs that must change with them; `vinaya check`'s C5 gate enforces that binding against each pull request's own diff — it fires only when a changed file matches a bound glob. That leaves a gap C5 cannot close on its own: a binding whose code was deleted or renamed wholesale matches nothing on any later diff, ever again, and reads as healthy forever. `vinaya doctor` closes it separately — it never fails `vinaya check` — by walking every binding against the repo's full tracked-file list and reporting any whose code glob matches zero tracked files anywhere in the repo, or whose in-repo doc pointer doesn't exist on disk. Report-only, like every other `vinaya doctor` diagnostic: it repoints or removes nothing itself.
138
142
 
143
+ ## Brief-schema divergence
144
+
145
+ `briefSchema` in `vinaya.config.json` is yours: `vinaya upgrade` preserves it wholesale and never rewrites it. On its own that ownership has a silent cost — nothing else reads it either, so a builtin deleted to work around a defect stays deleted, with no later upgrade to repair it and nothing to surface it.
146
+
147
+ `vinaya doctor` reports the divergence. It compares your `briefSchema.pr` and `briefSchema.issue` against the set `vinaya init` ships and names any builtin that is absent — nothing more. Extra sections, whether a second builtin or your own `heading`/`field`/`phrase` matcher, are additions rather than weakenings and are never reported. The finding is `info` severity and can never fail your CI: running without a builtin is legitimate configuration, and the goal is to make the choice visible, not to argue you back to the default.
148
+
149
+ Once an omission is deliberate, name it in `briefSchema.ack` and it goes quiet:
150
+
151
+ ```json
152
+ { "briefSchema": { "ack": ["closesN"] } }
153
+ ```
154
+
155
+ `ack` grants nothing and gates nothing — it only silences this report, and acking a builtin you still declare changes no behaviour anywhere. A deletion you did not intend, having no ack, keeps surfacing.
156
+
157
+ Two things worth knowing about its shape. `ack` is one flat list rather than per-kind, so acking a name that the shipped default declares for **both** kinds — `project` is the only such name today — silences it for both. And a config carrying no `briefSchema` key at all diverges on every builtin, so it reports on every run until you either declare the sections or ack them; that is the one shape where this becomes standing output rather than a one-time notice.
158
+
159
+ Report-only, like every other `vinaya doctor` diagnostic: it restores nothing itself.
160
+
161
+ ## Pinning Studio's port
162
+
163
+ `vinaya studio` binds `3008`, or `3108` when that is taken. `--port <n>` overrides both, and the override does **not** fall back: if the port you named is busy, the command refuses instead of quietly binding a different one.
164
+
165
+ That asymmetry is deliberate. The default pair exists so a casual `vinaya studio` still comes up when something else holds `3008`. But naming a port is how you buy certainty about which server answered you — and silently moving to another port spends exactly that. Run two Studio servers on one machine without it and a `200` from `/studio` tells you nothing about which process replied.
166
+
167
+ ```bash
168
+ vinaya studio --port 3208 # or --port=3208
169
+ ```
170
+
171
+ A malformed value exits `2` before anything starts: no value, a non-number, one outside `1`–`65535`, a leading zero, or the flag given twice with different values.
172
+
173
+ The flag applies to a published install, where this CLI launches the bundled server and owns the port. In a checkout carrying Studio's source it is refused, because that path runs Studio's own dev script, which picks its own port and ignores what it is passed — accepting the flag there would report a port nothing ever binds.
174
+
139
175
  ## Known limits
140
176
 
141
177
  The five core AEG checks (`coherence`, `dispatch-readiness`, and siblings) are bound to the Vinaya development repository — they read governance documents relative to it. Outside a Vinaya workspace, `vinaya check --all` reports those checks as `status: 'error'` rather than crashing.
@@ -0,0 +1,72 @@
1
+ ---
2
+ sidebar_title: Architect → Planner
3
+ title: Architect → Planner
4
+ order: 8
5
+ contract_id: architect-planner
6
+ description: Carries a product goal's declared tranche intents down to the Planner, so a tranche's goal is derived rather than invented.
7
+ status: active
8
+ producer: architect
9
+ consumer: planner
10
+ carrier: milestone-description
11
+ summary: Ever had a tranche's "why" live only in someone's head, one level above the plan?
12
+ ---
13
+ # Contract: Architect → Planner
14
+
15
+ ## The short version
16
+
17
+ This seam sits between declaring a product goal and planning the tranches that serve it. It exists because most tranches carry no Milestone at all — this contract governs the minority that do, and it is deliberately thin: one field crosses, matched by one key.
18
+
19
+ **What crosses** — the intent line for one tranche slug: `- <slug>: <intent text>`, written inside the Milestone's `### Tranche intents` section. Nothing else. The Architect does not name dependencies, sizing, traps, or an agent class — those remain entirely the Planner's, derived from reading the code, exactly as they are for an unmilestoned tranche.
20
+
21
+ **The hand-off is malformed when** — an intent line names a slug with no clear referent (the Architect should not invent a slug the Planner has not planned and will not plan), or when a tranche's Planner-authored rationale on its own Issues repeats or contradicts the intent line instead of leaving it alone. The intent is the *why-this-serves-the-larger-goal*; the rationale is the *why-this-task-is-shaped-this-way*. They answer different questions and neither substitutes for the other.
22
+
23
+ **What it does not carry** — sizing, dependency/conflict edges, traps, agent class, stop conditions, or documentation lists, all of which stay entirely the Planner's own to derive; any statement of status, which is derived from the forge and never written into a Milestone description by either role; and scheduling or estimates, which belong to whatever tool plans the roadmap.
24
+
25
+ **How it physically runs** — the carrier is the Milestone's own description. The Architect writes the intent line there, in the fixed `### Tranche intents` grammar, before or after the tranche it names is planned — order does not matter, because the tranche's goal is derived by matching the slug at read time, never by either role writing a pointer to the other. The Planner never edits the Milestone to consume this: reading the derived goal is automatic, the same way an unmilestoned tranche's empty goal is automatic.
26
+
27
+ ---
28
+
29
+ ## Reference
30
+
31
+ **Status:** active
32
+ **Seam:** the hand-off from the Architect (producer) to the Planner (consumer) — one direction, and thin by design, since most tranches never cross it at all.
33
+
34
+ ---
35
+
36
+ ## Why this file exists
37
+
38
+ A Milestone that names tranche intents is a claim about *why several tranches matter together* — a fact one altitude above any single tranche's own plan. Without a contract, that claim has no defined shape, and a Planner reading a Milestone's description free-form would either re-derive the product goal from scratch (duplicating the Architect's work) or invent detail the Architect never wrote (the same false-precision failure `contracts/planner-brief.md` guards against one seam down). This contract keeps the crossing to exactly one field, matched by exactly one key — a slug — so the two roles can never disagree about what crossed.
39
+
40
+ ---
41
+
42
+ ## The contract — field-by-field mapping
43
+
44
+ | Architect emits (Milestone field) | Planner consumes it as | What the consumption means |
45
+ |---|---|---|
46
+ | **`### Tranche intents` bullet** (`- <slug>: <intent text>`) | The tranche's derived **goal** (read via `intentGoalForSlug`, never stored) | The Planner does nothing to consume this — it is automatic, the same read path that gives an unmilestoned tranche its empty goal. The Planner never copies the intent text onto the Issue or into the rationale; the goal is derived at read time from the Milestone, always. |
47
+
48
+ That is the entire table. Compare `contracts/planner-brief.md`'s eight-field table — this seam is one field because the Architect's whole output is one field. Adding a second field to what the Architect emits is a Type 1 decision (`roles/architect.md`'s "one role, one job" constraint) and changes this contract, not a workaround inside it.
49
+
50
+ ---
51
+
52
+ ## Producer obligations (the Architect)
53
+
54
+ - Write an intent line for a slug only when the goal genuinely names that tranche — not speculatively, and not to reserve a name. An intent naming a slug the Planner never plans just sits there, `planned` forever, misleading a reader of the Milestone.
55
+ - Never write sizing, dependency edges, traps, agent class, or stop conditions into the Milestone. Those are entirely the Planner's, per `contracts/planner-brief.md`, whether or not this contract's seam is in play.
56
+ - Never write status. A Milestone's own lifecycle, like a tranche's, is derived — never a field either role sets by hand (`tranche-model.md` §4).
57
+
58
+ ## Consumer obligations (the Planner)
59
+
60
+ - Read a tranche's derived goal (if any) at planning time as context, the same way an unmilestoned tranche's empty goal is context — never as a directive that changes sizing, boundary, or dependency edges. Those come from reading the code, always, per `roles/planner.md`'s mandatory deep-dig.
61
+ - Never edit a Milestone's description to add, correct, or remove an intent line. If an intent line is wrong or stale, that is escalated to whoever the Architect's output goes to next — not silently fixed mid-plan.
62
+ - Never treat an intent line as a substitute for the Planner's own rationale. The Issue still carries all eight `contracts/planner-brief.md` fields regardless of whether this Milestone-level seam is in play.
63
+
64
+ ---
65
+
66
+ ## Changing this contract
67
+
68
+ A contract changes **as a unit**, same discipline as `contracts/planner-brief.md`. A change to this file is a **Tier 1** change (the seam is thin enough that widening it does not by itself alter a cross-role authority boundary the way `planner-brief.md`'s eight fields do) — but it still changes what `roles/architect.md` may emit and what `roles/planner.md` may consume, so both role docs must still point here rather than restate the mapping.
69
+
70
+ ---
71
+
72
+ *This contract is the seam. It is one row because the Architect's whole output, past the goal prose itself, is one row.*
@@ -0,0 +1,91 @@
1
+ ---
2
+ sidebar_title: Architect
3
+ title: Architect
4
+ order: 1.5
5
+ role_id: architect
6
+ description: Declares a product goal as a Milestone, and names which tranches serve it — nothing else.
7
+ actor: agent
8
+ performs:
9
+ - create-the-milestone
10
+ refuses_when: >
11
+ The goal is absent; a declared Release: field is present but not a
12
+ version; the Tranche intents section doesn't parse (every non-blank
13
+ line under it must read `- <slug>: <intent text>`); asked to cut a
14
+ task Issue or size a task (that's the Planner's); or asked to parse
15
+ a version out of the Milestone title (Release: is the only
16
+ authority, never the title).
17
+ summary: Ever had a Milestone's title quietly become the only place a version lived?
18
+ ---
19
+ # Architect — Role Reference
20
+
21
+ ## The short version
22
+
23
+ You declare a product goal as a GitHub Milestone, and — optionally — name which tranche slugs serve it. That is the whole job. One role, one job: you never cut a task Issue, never size a task, never plan a tranche.
24
+
25
+ **You own** — the Milestone: its title (free text, for humans, never parsed), its goal (prose), an optional `Release:` field (the sole authority for the milestone's version), and an optional `### Tranche intents` section naming which tranche slugs this goal covers and what each contributes.
26
+
27
+ **You refuse** — to write a Milestone with no goal; to write one whose `Release:` field is present but not a version; to write one whose `### Tranche intents` section doesn't parse; and to do anything past that one write — cutting a task Issue, sizing a task, planning a tranche, or moving one — those are the Planner's.
28
+
29
+ **You never** parse a version out of the title, cut a task Issue, size a task, or hand a Milestone off already carrying execution state. Most tranches never get a Milestone at all — you exist for the minority that serve a larger, deliberately-named product goal.
30
+
31
+ **How it physically runs** — `vinaya milestone create --title <title> --body-file <path>` is the whole surface. `checkMilestoneShape` refuses a malformed body before any `gh` call; there is nothing to worktree, branch, or commit — creating a Milestone is a forge action like cutting a labeled Issue is.
32
+
33
+ ---
34
+
35
+ ## Reference
36
+
37
+ **Audience:** whatever agent (or human) runs `vinaya milestone create` — invoked manually, on demand, never by automation. There is no dispatch queue for this role and no brief format for it: you are handed an intent for a product goal and a slice of tranche slugs it plausibly covers, and you write one Milestone.
38
+
39
+ You are the Architect when you are about to create a Milestone. You are NOT the Planner (you never cut a task Issue, size a task, or plan a tranche's dependency graph) and you are NOT the Tranche Archivist (you never close a Milestone or write a retrospective). Read `tranche-model.md` §4 first — it holds the model this role is one piece of: most tranches carry no Milestone; a Milestone exists only when a human has decided a set of tranches serves one larger, named goal.
40
+
41
+ ---
42
+
43
+ ## When you are the Architect
44
+
45
+ - Someone has a product goal worth naming durably, distinct from any single tranche
46
+ - You are about to run `vinaya milestone create`, not plan a tranche or cut an Issue
47
+ - You are not executing a brief — there is no brief format for this role
48
+
49
+ ---
50
+
51
+ ## Entry gate (self-locating)
52
+
53
+ Before writing anything, confirm:
54
+
55
+ 1. **You have a goal, in prose.** Not a title — the Milestone title is free text a human reads; it is never parsed for anything, and the version comes from `Release:` alone. If you have a nice title and no goal prose, you do not have enough to write yet.
56
+ 2. **`Release:`, if you know the version, is a real version.** Omit the field entirely if this goal declares no version yet — that is a normal, complete Milestone, not a defective one.
57
+ 3. **`### Tranche intents`, if you're naming tranches this goal covers, is one bullet per slug: `- <slug>: <intent text>`.** A slug named here that carries no Issues yet is fine — it resolves to a real, `planned` tranche the moment the Planner cuts it. Naming zero tranches is also fine: the Milestone still derives `planned` (`tranche-model.md` §4's zero-tranche guard, one altitude above the tranche-level guard `roles/planner.md` already applies).
58
+
59
+ If any of these isn't true, refuse rather than write a Milestone `checkMilestoneShape` will reject anyway — the check is the same gate either way; running it in your head first just means you refuse before drafting instead of after.
60
+
61
+ ---
62
+
63
+ ## What you own
64
+
65
+ **The Milestone, once.** `vinaya milestone create --title <title> --body-file <path>` writes exactly one Milestone from a validated body. There is no `edit` — a Milestone's title and goal, once written, are corrected by whoever owns Milestone editing next (out of this task's surface; see `tranche-model.md` §4's forward pointers). You do not maintain a Milestone across its life; you declare it once.
66
+
67
+ **The `Release:` grammar.** Line-anchored, `**`-optional on both sides, code fences stripped first, first match wins — the same shape `Project:` and `Depends-on:` already use elsewhere in this doctrine (`packages/aeg-core/src/milestone-validation.ts`). A malformed value refuses; an absent field is a normal, versionless Milestone.
68
+
69
+ **The `### Tranche intents` grammar.** One bullet per tranche, `- <slug>: <intent text>`, under a `### Tranche intents` heading. This is deliberately the simplest grammar that could parse: you write it by hand, in one sitting, with no template tool between you and the Milestone body.
70
+
71
+ ---
72
+
73
+ ## What you do NOT do
74
+
75
+ - **Cut a task Issue.** That's the Planner's canonical plan act (`roles/planner.md`) — an Architect that sizes tasks is planning against code it has not read.
76
+ - **Size a task, or decide a tranche's dependency/conflict edges.** Not your altitude.
77
+ - **Parse a version out of the title.** The title is free text for humans. The first person to write a nice title must not be able to break a downstream reader of `Release:` — that is the entire reason the field exists separately from the title.
78
+ - **Move, close, or edit an existing Milestone.** Adoption and movement are a later tranche's job (`tranche-model.md` §4's forward pointers) — you create, once, and stop.
79
+ - **Write status anywhere.** A Milestone's lifecycle (`planned`/`active`/`complete`) is derived from its declared tranches' own Issues, never written by you.
80
+
81
+ ---
82
+
83
+ ## Hand-off — governed by the Architect→Planner contract
84
+
85
+ What you write — a Milestone whose `### Tranche intents` section may name a tranche slug the Planner has not planned yet, or hasn't cut Issues for yet — is the **producer side** of `aeg-root/contracts/architect-planner.md`. Read it before writing a Milestone that names any tranche slugs: it states exactly what crosses this seam (an intent line, matched by slug, nothing else) and what does not (no brief, no rationale, no status).
86
+
87
+ ---
88
+
89
+ ## Turn-end: report your tokens, don't append them
90
+
91
+ You do not append your own row to any ledger file — self-append was retired for every role (`tranche-model.md` §12). Report your tokens instead: `Tokens: milestone — Architect — <model> — in/out/cost`, wherever you report back (there is no PR for a Milestone write to carry it in). If your host cannot read its own usage, report `—` for the numeric cells rather than estimating.
@@ -3,10 +3,9 @@ sidebar_title: Planner
3
3
  title: Planner
4
4
  order: 1
5
5
  role_id: planner
6
- description: Turns an intent and a slice of tickets into a whole tranche — the milestone, its tasks, and the dependencies between them.
6
+ description: Turns an intent and a slice of tickets into a whole tranche — its tasks, and the dependencies between them.
7
7
  actor: agent
8
8
  performs:
9
- - create-the-milestone
10
9
  - cut-labeled-issues
11
10
  - size-tasks-via-deep-dig
12
11
  - write-planners-rationale
@@ -26,22 +25,22 @@ summary: Ever had a project start with no real plan, just vibes?
26
25
 
27
26
  You turn an intent and a slice of work into a whole tranche — not one task, and not a roadmap. This is the team leader at planning altitude, not a separate person: the same intelligence, sizing work instead of shaping it.
28
27
 
29
- **You own** — the tranche as it lives on the forge: a milestone whose open state is its life, and one issue per task carrying your rationale. That rationale is what this role produces — what the task is and deliberately is not; why it is one task rather than three; every project and shared-package consumer in its blast radius; why each dependency and conflict edge exists; the traps your dig found; the class of agent it needs; when it must stop rather than improvise; and the documents it will make incoherent. Moving a task between tranches is yours too, and only while it has no branch and no pull request.
28
+ **You own** — the tranche as it lives on the forge: its tranche label, and one issue per task carrying your rationale. That rationale is what this role produces — what the task is and deliberately is not; why it is one task rather than three; every project and shared-package consumer in its blast radius; why each dependency and conflict edge exists; the traps your dig found; the class of agent it needs; when it must stop rather than improvise; and the documents it will make incoherent. Moving a task between tranches is yours too, and only while it has no branch and no pull request.
30
29
 
31
30
  **You refuse** — to plan until every input is present and reachable: a bounded intent, the specs and docs for each surface in scope actually read, the code readable, each shared package's consumers enumerable, every project registered, and the previous tranche on each product in scope closed out. You refuse too to size a task without reading its code, to emit a task with no rationale, to declare a shared-package change against only the consumer that drove it, to put execution state or a brief inside a plan, to treat a task with no issue as dispatchable, to hand over a task whose dependency has not merged or whose conflicting sibling is open, and to make a new committed file the home for a report.
32
31
 
33
32
  **You never** write the brief, write status anywhere, execute a task, settle a contested architectural question alone, invent a project the registry does not carry, or close a tranche down — that last is the archivist's.
34
33
 
35
- **How it physically runs** — creating the milestone and cutting the issues are forge actions, so most planning commits nothing: no branch, no worktree, no plan pull request. When a plan also writes a file — a spec change, most often — it reaches main as every change does: worktree, branch, pull request, green checks. Only one plan pull request per tranche may be open at once. You plan out loud, stage by stage, and say plainly when dispatch is the Principal's to trigger.
34
+ **How it physically runs** — cutting the issues is a forge action, so most planning commits nothing: no branch, no worktree, no plan pull request. When a plan also writes a file — a spec change, most often — it reaches main as every change does: worktree, branch, pull request, green checks. Only one plan pull request per tranche may be open at once. You plan out loud, stage by stage, and say plainly when dispatch is the Principal's to trigger.
36
35
 
37
36
 
38
37
  ---
39
38
 
40
39
  ## Reference
41
40
 
42
- **One altitude above the Brief Author.** The Brief Author turns one planned task into one brief; the Planner turns an intent plus a slice of tickets into a whole **tranche** — a GitHub Milestone plus a set of labeled forge Issues.
41
+ **One altitude above the Brief Author.** The Brief Author turns one planned task into one brief; the Planner turns an intent plus a slice of tickets into a whole **tranche** — a set of `vinaya/tranche:<slug>`-labeled forge Issues. No Milestone is required: a tranche's identity is its label alone (`tranche-model.md` §4).
43
42
 
44
- **Forge-native by default — no topology file, no plan PR, no commit.** Create a Milestone titled `<slug>` (its description is the tranche goal), then cut task Issues labeled `vinaya/tranche:<slug>` with the full Planner's rationale (see "The Planner's rationale" below) in each body. `@attalabs/aeg-forge-state` derives topology, dependencies, and lifecycle purely from those forge objects — nothing to write to `main`, nothing for `verify-coherence` to fall back to a file for. This cutover is now complete for every active tranche. Do not create a new topology file for a new tranche; if you find yourself about to write one, stop — the forge-native path below is the whole job.
43
+ **Forge-native by default — no topology file, no plan PR, no commit, and usually no Milestone.** Cut task Issues labeled `vinaya/tranche:<slug>` with the full Planner's rationale (see "The Planner's rationale" below) in each body. `@attalabs/aeg-forge-state` derives topology, dependencies, and lifecycle purely from those forge objects — nothing to write to `main`, nothing for `verify-coherence` to fall back to a file for. This cutover is now complete for every active tranche. Do not create a new topology file for a new tranche; if you find yourself about to write one, stop — the forge-native path below is the whole job. Creating a Milestone is not your job either — that's the Architect's (`roles/architect.md`), and most tranches never get one. If a Milestone already exists naming this slug in its `### Tranche intents` section, this tranche's goal is picked up from that intent line automatically; you neither create nor edit the Milestone to make that happen.
45
44
 
46
45
  Read this with `tranche-model.md` (the model) and `coordination.md` (session start). The Planner exists because the relationships *between* tasks — dependencies, conflicts, split-vs-combine — are invisible to a brief written in isolation. Seeing them is the whole job.
47
46
 
@@ -99,11 +98,11 @@ The principle: **the planner does not start work it cannot finish well.** Garbag
99
98
 
100
99
  ## What you produce
101
100
 
102
- Exactly two artifacts, both on the forge, nothing committed to the repo:
103
- 1. **A Milestone** — titled `<slug>`, description = the tranche goal. Its open/closed state is the tranche's lifecycle (open = active, closed = complete) — nothing else sets it.
104
- 2. **Forge Issues** — one per task, labeled `vinaya/tranche:<slug>`. Each holds task identity + metadata + the **Planner's rationale** (§"The Planner's rationale" below): title, project label(s), `depends-on`/`conflicts-with` references, external ticket link, and the rationale block. **No brief** (that's just-in-time, in the PR body later). **No status** (derived from the forge). **No priority/estimates/points** (those live in the company's planning tool).
101
+ Exactly one artifact, on the forge, nothing committed to the repo:
105
102
 
106
- **Cutting forge Issues IS the canonical plan act.** The tranche is not fully planned until every task has a real Issue, labeled and attached to the Milestone. `#TBD` is not a valid state in a dispatched or active tranche it means the plan is incomplete. The Planner writes the rationale INTO the Issue body. Brief Authors read the rationale from the Issue; they must not need to load a separate tranche file to understand what they are implementing there isn't one.
103
+ **Forge Issues** one per task, labeled `vinaya/tranche:<slug>`. Each holds task identity + metadata + the **Planner's rationale** (§"The Planner's rationale" below): title, project label(s), `depends-on`/`conflicts-with` references, external ticket link, and the rationale block. **No brief** (that's just-in-time, in the PR body later). **No status** (derived from the forge). **No priority/estimates/points** (those live in the company's planning tool). **No Milestone** you never create or attach one; the label is the tranche's whole identity, and a Milestone is a separate, optional, Architect-created artifact naming a larger product goal some tranches serve (`tranche-model.md` §4).
104
+
105
+ **Cutting forge Issues IS the canonical plan act.** The tranche is not fully planned until every task has a real Issue, correctly labeled. `#TBD` is not a valid state in a dispatched or active tranche — it means the plan is incomplete. The Planner writes the rationale INTO the Issue body. Brief Authors read the rationale from the Issue; they must not need to load a separate tranche file to understand what they are implementing — there isn't one.
107
106
 
108
107
  You write no briefs and no status. **Cutting the Issue and writing its number into the topology table is the backlog → todo promotion.** Leaving the Issue column as `#TBD` keeps the task backlog — it is neither briefable nor executable. Not every task in the tranche need have an Issue at plan time; backlog tasks may remain `#TBD` until promoted. But before any task is dispatched, the Planner must cut its Issue, record the real number in the topology table, and only then hand it to the Brief Author. A task with `#TBD` in its Issue column is not dispatchable — the Brief Author and Developer both hard-STOP on it.
109
108
 
@@ -239,7 +238,7 @@ When you raise a warning, state the specific signal, give your recommendation (u
239
238
 
240
239
  ## Naming the tranche
241
240
 
242
- Name the Milestone (its title is the `<slug>`) after its **center of gravity — the durable, highest-leverage work — not its narrowest downstream feature.** When a tranche onboards a project onto shared infrastructure (or grows that infra), name the onboarding/infra, not the feature riding on it. A name must not imply narrower scope than the tasks' `Project(s)` fields reveal.
241
+ Name the tranche (its `vinaya/tranche:<slug>` label) after its **center of gravity — the durable, highest-leverage work — not its narrowest downstream feature.** When a tranche onboards a project onto shared infrastructure (or grows that infra), name the onboarding/infra, not the feature riding on it. A name must not imply narrower scope than the tasks' `Project(s)` fields reveal.
243
242
 
244
243
  ---
245
244
 
@@ -250,10 +249,10 @@ Moving a task from one tranche to another is a **Planner power** — it is a top
250
249
  **Only `todo`/backlog tasks are movable.** A task with an open branch or open PR (in-flight / in-review) must be finished or dropped first — never relocated mid-flight. Verify with the forge before moving: no `task/<src>/<n>` branch, no open PR.
251
250
 
252
251
  **The refactor, step by step:**
253
- 1. **Plan the destination** — confirm the destination Milestone and each moved task's refreshed Planner's rationale (sizing may change once it lands on the new tranche's substrate; re-derive it, do not copy the stale one).
254
- 2. **Relabel each moved Issue** `vinaya/tranche:<src>` → `vinaya/tranche:<dest>`, re-attach it to the destination Milestone, and post a one-line provenance comment on it (from where, to where, why) — the relabel + comment *is* the move; there is no separate topology row to edit.
252
+ 1. **Plan the destination** — confirm the destination tranche (and, if either the source or destination tranche happens to carry a Milestone under the legacy 1:1 regime, that Milestone) and each moved task's refreshed Planner's rationale (sizing may change once it lands on the new tranche's substrate; re-derive it, do not copy the stale one).
253
+ 2. **Relabel each moved Issue** `vinaya/tranche:<src>` → `vinaya/tranche:<dest>`, re-attach it to the destination Milestone if one exists, and post a one-line provenance comment on it (from where, to where, why) — the relabel + comment *is* the move; there is no separate topology row to edit.
255
254
  3. **If the source tranche still has a legacy topology file** (rare — see the forge-native default above), annotate the moved task's row with `Moved out → <dest>` before archival. Forge-native source tranches need no file annotation; the Issue's relabel + comment is the whole record.
256
- 4. **Leave the close to the Archivist.** After your plan lands, the source tranche has no open task work (every task merged, dropped, or now moved) and the Tranche Archivist can close it — closing the source Milestone. You do not archive it yourself — deciding-what's-next and refactoring is yours; the close-out mechanics are the Archivist's.
255
+ 4. **Leave the close to the Archivist.** After your plan lands, the source tranche has no open task work (every task merged, dropped, or now moved) and the Tranche Archivist can close it — closing the source Milestone too, if one exists. You do not archive it yourself — deciding-what's-next and refactoring is yours; the close-out mechanics are the Archivist's.
257
256
 
258
257
  **Movement provenance is recorded on the forge** (auditable): the Issue (relabel + Milestone re-attach + comment) and the Archivist retrospective ("Tasks moved out") posted to the pinned lessons Issue. This is the honest, forge-derivable record that a task changed address rather than vanishing.
259
258
 
@@ -271,7 +270,7 @@ Once an Issue is assigned (`todo`), a Developer picks it up: reads the rationale
271
270
 
272
271
  ## Step 0 — creating the tranche itself needs no worktree, no PR, no commit
273
272
 
274
- **Creating a Milestone and cutting labeled Issues are forge actions, not repo-file changes — there is nothing to commit.** The old requirement to open a `plan/<tranche>` worktree + PR existed because the topology file was a repo file, and every repo-file change reaches `main` through a worktree branch + PR + green merge, same as a Developer's. That still applies **only if this planning act also writes an actual repo file** — most commonly a spec change. If your plan produces no repo-file change at all (the common case — Milestone + Issues only), skip this section entirely: no worktree, no plan PR, nothing for the git hooks or a merge gate to gate.
273
+ **Cutting labeled Issues is a forge action, not a repo-file change — there is nothing to commit.** The old requirement to open a `plan/<tranche>` worktree + PR existed because the topology file was a repo file, and every repo-file change reaches `main` through a worktree branch + PR + green merge, same as a Developer's. That still applies **only if this planning act also writes an actual repo file** — most commonly a spec change. If your plan produces no repo-file change at all (the common case — Issues only), skip this section entirely: no worktree, no plan PR, nothing for the git hooks or a merge gate to gate.
275
274
 
276
275
  When a plan **does** write a repo file — a spec change, most often — open it the same way any doc change does: a worktree off the main branch, commit, pull request:
277
276
 
@@ -81,6 +81,14 @@ So: there is **no status column anywhere.** The Developer does not "flip to in-r
81
81
 
82
82
  **A plan is a Milestone plus labeled Issues. There is no topology file, and `aeg-root/tranches/` holds nothing but the archive.** This section is kept because the *rules* the file encoded still bind — they simply bind the forge objects now. Read it as the reasoning behind the shape, not as a file to create.
83
83
 
84
+ > **A tranche's identity is its `vinaya/tranche:<slug>` label, not a Milestone.** The one-Milestone-one-tranche reading above is the LEGACY case, still true forever for a Milestone titled exactly a tranche slug, but no longer the only case. A Milestone is no longer required for a tranche to exist, and one Milestone may legitimately hold several tranches' labels at once.
85
+ >
86
+ > **A Milestone now means a product goal, not a tranche.** Most tranches still have none — they are fully represented by their labeled Issues, derive `planned`/`active`/`complete` from those Issues alone (§3, one altitude down from what follows), and carry an empty goal, exactly as an unmilestoned tranche always has. A Milestone exists only when someone deliberately declares a higher-level goal that one or more tranches serve. The **Architect** (`roles/architect.md`) is the one role that creates a Milestone, via `vinaya milestone create` — the Planner no longer does (§6). The Milestone's **title is free text**, read by no parser; its description carries the goal in prose plus two optional, machine-parsed fields: an optional `Release:` field (the sole authority for the milestone's version — never the title, so the first person to write a nice title cannot break a downstream reader), and an optional `### Tranche intents` section, one `- <slug>: <intent text>` bullet per tranche this milestone's goal covers.
87
+ >
88
+ > **A tranche's goal is derived, never stored:** it is the intent line matching that tranche's slug, found by searching every Milestone's description — not a field anyone writes onto the tranche itself. A slug with no matching intent line anywhere resolves to an empty goal, same as a tranche with no Milestone at all; an intent line naming a slug that carries no Issues yet is a real, `planned` tranche, not a missing one. A Milestone's own lifecycle is the same `planned`/`active`/`complete` derivation one altitude up: a Milestone declaring zero tranches (or only tranches that are themselves still `planned`) derives `planned`, never `complete` — the same at-least-one guard §3 applies to a tranche's Issues, applied here to a Milestone's tranches.
89
+ >
90
+ > **An existing tranche moves into a Milestone with `vinaya milestone adopt --target <title> --slug <slug> [--slug <slug> ...]`, never by hand.** It reattaches every Issue carrying each named `vinaya/tranche:<slug>` label to the target Milestone, then closes (never deletes) each slug's old tranche-Milestone — closed, so its history and Issue associations survive, exactly as the Archivist's own tranche-close does (§11). This is GitHub-view hygiene, not a change to how the slug derives: no reader in this model consults an Issue's native milestone field, only its label, so `adopt` moving that field is cosmetic grouping for a human looking at GitHub, the same reason `open-issue.ts` auto-attaches a new task Issue to its tranche's Milestone at creation time. `checkAdoptable` (`@attalabs/aeg-core`) gathers every fact for every named slug and refuses the WHOLE invocation before any write — an unknown slug, a slug whose label carries no Issues, a target that does not exist or is closed, or a slug already adopted into a different Milestone — so one bad slug in a multi-slug call blocks every slug in that call, never a partial move. Creating the Milestones a roadmap declares is a separate, prior step: N calls to `vinaya milestone create`, one per goal — `adopt` only ever moves a tranche that already exists into a Milestone that already exists.
91
+
84
92
  The file held **only** what the forge models poorly: the task→Issue mapping and the dependency/conflict graph. It contains **no status, no PR numbers, no merge dates, no timestamps — nothing the forge already knows. It contains no task prose, no boundary descriptions, no rationale — nothing that belongs on the Issue.** Its task topology was edited only by the Planner, at plan time, so it could not race and could not drift on status (it stored none). The same rule now binds the Milestone and its Issues: the Planner cuts them, and nothing downstream writes status back. The one exception is the tranche's own **lifecycle marker** (active/complete — §12), a single header line the Archivist sets at close-out; this is the tranche's lifecycle, not per-task execution status, and it is set once when the whole tranche ends.
85
93
 
86
94
  **`#TBD` is still forbidden, wherever a task is recorded.** Every task must carry a real forge Issue number. A tranche that contains `#TBD` is an incomplete plan — the Planner has not cut the Issues, which is the canonical plan act. **The Planner's rationale (Boundary, Sizing, Project(s)+blast radius, Dependency rationale, Traps to avoid, Suggested agent-class, Stop-and-escalate) lives on the Issue body.** Nothing outside the Issue repeats the rationale. Brief Authors read it from the Issue, which is now its only home.
@@ -145,7 +153,7 @@ The Planner's job — the reason the tranche exists — is the relationships a b
145
153
  - **Independently verifiable → split** into single-project tasks with a `depends-on` edge.
146
154
  - **Verification-coupled → combine** into one task, one branch, one PR, multiple projects (e.g. generalize a shared `core` package *and* migrate the first consumer onto it — the only proof the refactor is correct is the consumer working). Cross-project PRs touching two, three, four projects are normal, not exceptions.
147
155
 
148
- The Planner writes no briefs (those are just-in-time, §7) and writes no status (that's the forge). It owns the thin file and the `backlog`/`todo` distinction (assigning an Issue is the `todo` promotion). Its upstream input — a ticket slice, a backlog, or just the Principal's stated intent — is optional and lives outside AEG (§2); the Planner is where the company's plan and AEG's execution meet. It also enforces the **plan-integrity gates** in `roles/planner.md` — the recognized failure modes turned into live refusals and calibrated warnings (see §10), and the **readiness gate** (verify all inputs are present and reachable before planning a single task). The full role spec, including refusal language, is in `roles/planner.md`.
156
+ The Planner writes no briefs (those are just-in-time, §7), writes no status (that's the forge), and creates no Milestone (that's the Architect's, §4) — a tranche it plans need carry no Milestone at all, and most don't. It owns the `backlog`/`todo` distinction (assigning an Issue is the `todo` promotion). Its upstream input — a ticket slice, a backlog, or just the Principal's stated intent — is optional and lives outside AEG (§2); the Planner is where the company's plan and AEG's execution meet. It also enforces the **plan-integrity gates** in `roles/planner.md` — the recognized failure modes turned into live refusals and calibrated warnings (see §10), and the **readiness gate** (verify all inputs are present and reachable before planning a single task). The full role spec, including refusal language, is in `roles/planner.md`.
149
157
 
150
158
  ---
151
159
 
@@ -274,6 +274,9 @@ function issueListByLabelArgs(owner, repo, label) {
274
274
  "200"
275
275
  ];
276
276
  }
277
+ function ghIssueListByLabel(owner, repo, label) {
278
+ return JSON.parse(run(issueListByLabelArgs(owner, repo, label)));
279
+ }
277
280
  async function ghIssueListByLabelAsync(owner, repo, label) {
278
281
  return JSON.parse(await runAsync(issueListByLabelArgs(owner, repo, label)));
279
282
  }
@@ -289,18 +292,6 @@ async function ghIssueListByAnyLabelAsync(owner, repo, labels) {
289
292
  return dedupeByNumber(perLabel.flat());
290
293
  }
291
294
 
292
- // ../../packages/aeg-forge-state/src/fetch-milestone.ts
293
- function findMilestoneForSlug(owner, repo, slug) {
294
- const milestones = ghApiGet(`repos/${owner}/${repo}/milestones?state=all&per_page=100`);
295
- const match = milestones.find((m) => m.title === slug);
296
- if (!match)
297
- return null;
298
- return {
299
- goal: match.description ?? "",
300
- lifecycle: match.state === "closed" ? "complete" : "active"
301
- };
302
- }
303
-
304
295
  // ../../packages/aeg-forge-state/src/labels.ts
305
296
  var LABELS = [
306
297
  {
@@ -543,6 +534,60 @@ function resolveTaskIssueRef(title, labels) {
543
534
  return { trancheSlug, taskId };
544
535
  }
545
536
 
537
+ // ../../packages/aeg-forge-state/src/fetch-milestone.ts
538
+ var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
539
+ var NEXT_HEADING = /^#{1,6}\s+\S/m;
540
+ var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
541
+ function intentGoalForSlug(description, slug) {
542
+ const text = stripCode(description, { inlineSpans: "keep" });
543
+ const start = text.match(INTENTS_HEADING);
544
+ if (!start || start.index === undefined)
545
+ return "";
546
+ const rest = text.slice(start.index + start[0].length);
547
+ const next = rest.match(NEXT_HEADING);
548
+ const section = rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
549
+ for (const line of section.split(`
550
+ `)) {
551
+ const trimmed = line.trim();
552
+ if (trimmed.length === 0)
553
+ continue;
554
+ const m = trimmed.match(INTENT_BULLET);
555
+ if (m && (m[1] ?? "").toLowerCase() === slug.toLowerCase())
556
+ return (m[2] ?? "").trim();
557
+ }
558
+ return "";
559
+ }
560
+ function matchesLegacyMilestone(milestones, slug) {
561
+ return milestones.find((m) => m.title === slug) ?? null;
562
+ }
563
+ function factsFromLegacyMilestone(milestone) {
564
+ return {
565
+ goal: milestone.description ?? "",
566
+ lifecycle: milestone.state === "closed" ? "complete" : "active"
567
+ };
568
+ }
569
+ function lifecycleFromIssues(issues) {
570
+ if (issues.length === 0)
571
+ return "planned";
572
+ return issues.some((i) => i.state === "OPEN") ? "active" : "complete";
573
+ }
574
+ function goalFromMilestones(milestones, slug) {
575
+ for (const m of milestones) {
576
+ const goal = intentGoalForSlug(m.description ?? "", slug);
577
+ if (goal)
578
+ return goal;
579
+ }
580
+ return "";
581
+ }
582
+ function findMilestoneForSlug(owner, repo, slug) {
583
+ const milestones = ghApiGet(`repos/${owner}/${repo}/milestones?state=all&per_page=100`);
584
+ const legacy = matchesLegacyMilestone(milestones, slug);
585
+ if (legacy)
586
+ return factsFromLegacyMilestone(legacy);
587
+ const issues = ghIssueListByLabel(owner, repo, trancheLabel(slug));
588
+ return { goal: goalFromMilestones(milestones, slug), lifecycle: lifecycleFromIssues(issues) };
589
+ }
590
+
546
591
  // ../../packages/aeg-forge-state/src/derive-from-forge.ts
547
592
  function trancheFromIssues(slug, issues, known) {
548
593
  return {
@@ -3174,7 +3219,8 @@ var BRIEF_BUILTINS = [
3174
3219
  "for",
3175
3220
  "closesN",
3176
3221
  "premiseCoverage",
3177
- "issueRationale"
3222
+ "issueRationale",
3223
+ "milestoneShape"
3178
3224
  ];
3179
3225
  var BriefSectionSchema = z.union([
3180
3226
  z.object({ builtin: z.enum(BRIEF_BUILTINS) }),
@@ -3184,7 +3230,9 @@ var BriefSectionSchema = z.union([
3184
3230
  ]);
3185
3231
  var BriefSchemaSchema = z.object({
3186
3232
  pr: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
3187
- issue: z.object({ sections: z.array(BriefSectionSchema) }).optional()
3233
+ issue: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
3234
+ milestone: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
3235
+ ack: z.array(z.enum(BRIEF_BUILTINS)).optional()
3188
3236
  });
3189
3237
  function isSafeRepoRelPath(p) {
3190
3238
  if (p.length === 0)
@@ -273,6 +273,9 @@ function issueListByLabelArgs(owner, repo, label) {
273
273
  "200"
274
274
  ];
275
275
  }
276
+ function ghIssueListByLabel(owner, repo, label) {
277
+ return JSON.parse(run(issueListByLabelArgs(owner, repo, label)));
278
+ }
276
279
  async function ghIssueListByLabelAsync(owner, repo, label) {
277
280
  return JSON.parse(await runAsync(issueListByLabelArgs(owner, repo, label)));
278
281
  }
@@ -288,18 +291,6 @@ async function ghIssueListByAnyLabelAsync(owner, repo, labels) {
288
291
  return dedupeByNumber(perLabel.flat());
289
292
  }
290
293
 
291
- // ../../packages/aeg-forge-state/src/fetch-milestone.ts
292
- function findMilestoneForSlug(owner, repo, slug) {
293
- const milestones = ghApiGet(`repos/${owner}/${repo}/milestones?state=all&per_page=100`);
294
- const match = milestones.find((m) => m.title === slug);
295
- if (!match)
296
- return null;
297
- return {
298
- goal: match.description ?? "",
299
- lifecycle: match.state === "closed" ? "complete" : "active"
300
- };
301
- }
302
-
303
294
  // ../../packages/aeg-forge-state/src/labels.ts
304
295
  var LABELS = [
305
296
  {
@@ -542,6 +533,60 @@ function resolveTaskIssueRef(title, labels) {
542
533
  return { trancheSlug, taskId };
543
534
  }
544
535
 
536
+ // ../../packages/aeg-forge-state/src/fetch-milestone.ts
537
+ var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
538
+ var NEXT_HEADING = /^#{1,6}\s+\S/m;
539
+ var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
540
+ function intentGoalForSlug(description, slug) {
541
+ const text = stripCode(description, { inlineSpans: "keep" });
542
+ const start = text.match(INTENTS_HEADING);
543
+ if (!start || start.index === undefined)
544
+ return "";
545
+ const rest = text.slice(start.index + start[0].length);
546
+ const next = rest.match(NEXT_HEADING);
547
+ const section = rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
548
+ for (const line of section.split(`
549
+ `)) {
550
+ const trimmed = line.trim();
551
+ if (trimmed.length === 0)
552
+ continue;
553
+ const m = trimmed.match(INTENT_BULLET);
554
+ if (m && (m[1] ?? "").toLowerCase() === slug.toLowerCase())
555
+ return (m[2] ?? "").trim();
556
+ }
557
+ return "";
558
+ }
559
+ function matchesLegacyMilestone(milestones, slug) {
560
+ return milestones.find((m) => m.title === slug) ?? null;
561
+ }
562
+ function factsFromLegacyMilestone(milestone) {
563
+ return {
564
+ goal: milestone.description ?? "",
565
+ lifecycle: milestone.state === "closed" ? "complete" : "active"
566
+ };
567
+ }
568
+ function lifecycleFromIssues(issues) {
569
+ if (issues.length === 0)
570
+ return "planned";
571
+ return issues.some((i) => i.state === "OPEN") ? "active" : "complete";
572
+ }
573
+ function goalFromMilestones(milestones, slug) {
574
+ for (const m of milestones) {
575
+ const goal = intentGoalForSlug(m.description ?? "", slug);
576
+ if (goal)
577
+ return goal;
578
+ }
579
+ return "";
580
+ }
581
+ function findMilestoneForSlug(owner, repo, slug) {
582
+ const milestones = ghApiGet(`repos/${owner}/${repo}/milestones?state=all&per_page=100`);
583
+ const legacy = matchesLegacyMilestone(milestones, slug);
584
+ if (legacy)
585
+ return factsFromLegacyMilestone(legacy);
586
+ const issues = ghIssueListByLabel(owner, repo, trancheLabel(slug));
587
+ return { goal: goalFromMilestones(milestones, slug), lifecycle: lifecycleFromIssues(issues) };
588
+ }
589
+
545
590
  // ../../packages/aeg-forge-state/src/derive-from-forge.ts
546
591
  function trancheFromIssues(slug, issues, known) {
547
592
  return {