@attalabs/vinaya 0.24.1 → 0.26.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 (130) hide show
  1. package/README.md +26 -3
  2. package/aeg-root/contracts/architect-planner.md +5 -5
  3. package/aeg-root/contracts/developer-reviewer.md +10 -7
  4. package/aeg-root/contracts/planner-developer.md +142 -0
  5. package/aeg-root/contracts/reviewer-archivist.md +2 -2
  6. package/aeg-root/contracts/tranche-archivist-planner.md +12 -11
  7. package/aeg-root/enforcement.md +25 -11
  8. package/aeg-root/process.md +53 -75
  9. package/aeg-root/roles/archivist.md +3 -3
  10. package/aeg-root/roles/developer.md +30 -24
  11. package/aeg-root/roles/planner.md +100 -21
  12. package/aeg-root/roles/principal.md +14 -11
  13. package/aeg-root/roles/reviewer.md +33 -17
  14. package/aeg-root/roles/security.md +28 -13
  15. package/aeg-root/roles/tranche-archivist.md +2 -2
  16. package/aeg-root/skills/aeg/SKILL.md +5 -5
  17. package/aeg-root/skills/aeg-roles/SKILL.md +7 -7
  18. package/aeg-root/state-machine.md +36 -35
  19. package/aeg-root/task-model.md +3 -3
  20. package/aeg-root/templates/brief-template.md +22 -6
  21. package/aeg-root/templates/issue-rationale-template.md +35 -2
  22. package/aeg-root/templates/pr-report-template.md +5 -13
  23. package/aeg-root/tranche-model.md +21 -21
  24. package/dist/checks/bin/check-body-bare-digits.js +684 -142
  25. package/dist/checks/bin/check-branch-topology.js +741 -88
  26. package/dist/checks/bin/check-brief-shape.js +708 -83
  27. package/dist/checks/bin/check-changeset-coverage.js +1445 -149
  28. package/dist/checks/bin/check-closes-n.js +741 -88
  29. package/dist/checks/bin/check-coherence.js +808 -149
  30. package/dist/checks/bin/check-dead-branch-push.js +617 -81
  31. package/dist/checks/bin/check-dispatch-readiness.js +811 -152
  32. package/dist/checks/bin/check-doc-coverage-push.js +1445 -149
  33. package/dist/checks/bin/check-doc-coverage.js +1447 -151
  34. package/dist/checks/bin/check-doctrine-no-procedures.js +684 -142
  35. package/dist/checks/bin/check-doctrine-portability.js +1445 -149
  36. package/dist/checks/bin/check-evidence-fresh.js +684 -142
  37. package/dist/checks/bin/check-exec-bits.js +1445 -149
  38. package/dist/checks/bin/check-first-push-dispatch.js +808 -149
  39. package/dist/checks/bin/check-issue-assignment.js +741 -88
  40. package/dist/checks/bin/check-main-branch-refusal.js +618 -82
  41. package/dist/checks/bin/check-no-disk-state.js +617 -81
  42. package/dist/checks/bin/check-pr-premise-reassert.js +5401 -0
  43. package/dist/checks/bin/check-pr-report-density.js +617 -81
  44. package/dist/checks/bin/check-quoted-command.js +1443 -147
  45. package/dist/checks/bin/check-reader-resolvable-prose.js +1481 -153
  46. package/dist/checks/bin/check-registry-gates.js +659 -85
  47. package/dist/checks/bin/check-retired-vocabulary.js +1443 -147
  48. package/dist/checks/bin/check-review-gate.js +758 -145
  49. package/dist/checks/bin/check-single-plan-pr.js +617 -81
  50. package/dist/checks/bin/check-surface-scope.js +5722 -0
  51. package/dist/checks/bin/check-test-plan.js +617 -81
  52. package/dist/checks/bin/check-token-collection-wired.js +617 -81
  53. package/dist/checks/bin/check-token-report.js +629 -88
  54. package/dist/checks/bin/check-workspace-escape.js +1443 -147
  55. package/dist/index.js +8548 -4335
  56. package/package.json +1 -1
  57. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-render.ts +2 -2
  58. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +26 -1
  59. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  60. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  61. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  62. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  63. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  64. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  65. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  66. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  67. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  68. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  69. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  70. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  71. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  72. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  73. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  74. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  75. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  76. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  77. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  78. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  79. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  80. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  81. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  82. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  83. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  84. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  85. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  86. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  87. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  88. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  89. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  90. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  91. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  92. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  93. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  94. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  95. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  96. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  97. 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
  98. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
  99. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
  100. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
  101. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1pndh3_._.js → [root-of-the-server]__0p8q38b._.js} +1 -1
  102. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
  103. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
  104. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
  105. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
  106. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
  107. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
  108. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0wxycau._.js +1 -1
  109. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
  110. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  111. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_topbar_index_tsx_1h1gs1y._.js +1 -1
  112. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  113. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  114. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  115. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  116. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3m1kgax7j2vgs.js → 0bn8c8v5q429o.js} +1 -1
  117. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0doqwpd81sjzn.js → 0keji7wvbe1d1.js} +1 -1
  118. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1maoxbrl3pv5d.css +1 -0
  119. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1uogpj5w2n5ju.js → 3-6a3tinbdb-7.js} +1 -1
  120. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0zebsmmk1bvnb.js → 3_9xytdmiv1xi.js} +4 -4
  121. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  122. package/aeg-root/contracts/brief-developer.md +0 -141
  123. package/aeg-root/contracts/planner-brief.md +0 -130
  124. package/aeg-root/roles/brief-author.md +0 -114
  125. package/aeg-root/skills/brief-authoring/SKILL.md +0 -509
  126. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3qc67qhcvbg0o.css +0 -1
  127. /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
  128. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{QY2GhiI47-765D9DHGy1I → DMofV2-9FrHE7100GfrpI}/_buildManifest.js +0 -0
  129. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{QY2GhiI47-765D9DHGy1I → DMofV2-9FrHE7100GfrpI}/_clientMiddlewareManifest.js +0 -0
  130. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{QY2GhiI47-765D9DHGy1I → DMofV2-9FrHE7100GfrpI}/_ssgManifest.js +0 -0
@@ -30,6 +30,15 @@ var GATE_AUDIENCE = {
30
30
  internal: "A composite that shells this repo's own `typecheck`/`lint`/`test`/`build` scripts by name. Those names are this monorepo's toolchain (bun + turbo + biome), not an adopter's; the shipped equivalent of its intent is `vinaya check --all`, which is portable by construction."
31
31
  }
32
32
  };
33
+ var NON_GATE_BINS = [
34
+ "archive-task",
35
+ "assign-task-issue",
36
+ "dead-branch-audit",
37
+ "eval-agent-compliance",
38
+ "open-issue",
39
+ "open-pr",
40
+ "report-tokens"
41
+ ];
33
42
  function isShipped(a) {
34
43
  return "shippedAs" in a;
35
44
  }
@@ -390,7 +399,7 @@ var LABELS = [
390
399
  id: "vinaya/needs:brief-correction",
391
400
  category: "needs",
392
401
  form: "literal",
393
- carries: "Waiting on the Brief Author — the brief contradicts the surface it describes."
402
+ carries: "Waiting on the Planner — the brief contradicts the surface it describes."
394
403
  },
395
404
  {
396
405
  key: "waiver-docs",
@@ -1941,13 +1950,18 @@ function parseInlineFieldList(section) {
1941
1950
  }
1942
1951
  // ../../packages/aeg-core/src/verdict-extraction.ts
1943
1952
  var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
1944
- function firstThreeLines(comment) {
1953
+ var OBJECTIVES_VERSION_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Objectives version:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1954
+ function firstFiveLines(comment) {
1945
1955
  return comment.split(`
1946
- `).slice(0, 3).join(`
1956
+ `).slice(0, 5).join(`
1947
1957
  `);
1948
1958
  }
1949
1959
  function extractHeadSha(comment) {
1950
- const m = firstThreeLines(comment).match(HEAD_SHA_PATTERN);
1960
+ const m = firstFiveLines(comment).match(HEAD_SHA_PATTERN);
1961
+ return m ? m[1].toLowerCase() : null;
1962
+ }
1963
+ function extractObjectivesVersion(comment) {
1964
+ const m = firstFiveLines(comment).match(OBJECTIVES_VERSION_PATTERN);
1951
1965
  return m ? m[1].toLowerCase() : null;
1952
1966
  }
1953
1967
  function extractVerdict(comments, valuePattern, missingLabel) {
@@ -1956,21 +1970,24 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1956
1970
  return {
1957
1971
  value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
1958
1972
  headSha: null,
1973
+ objectivesVersion: null,
1959
1974
  danglingNote: `no ${missingLabel} verdict comment found on this PR`
1960
1975
  };
1961
1976
  }
1962
1977
  const latest = candidates[candidates.length - 1];
1963
- const m = firstThreeLines(latest).match(valuePattern);
1978
+ const m = firstFiveLines(latest).match(valuePattern);
1964
1979
  if (!m) {
1965
1980
  return {
1966
- value: `the most recent ${missingLabel} comment's VERDICT line is not within its first three lines — DANGLING, see below`,
1981
+ value: `the most recent ${missingLabel} comment's VERDICT line is not within its first five lines — DANGLING, see below`,
1967
1982
  headSha: null,
1968
- danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-three-line read window`
1983
+ objectivesVersion: null,
1984
+ danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
1969
1985
  };
1970
1986
  }
1971
1987
  return {
1972
1988
  value: m[1].toUpperCase().replace(/[_-]/g, " "),
1973
1989
  headSha: extractHeadSha(latest),
1990
+ objectivesVersion: extractObjectivesVersion(latest),
1974
1991
  danglingNote: null
1975
1992
  };
1976
1993
  }
@@ -2327,8 +2344,105 @@ function overrideActive(opts) {
2327
2344
  return true;
2328
2345
  return false;
2329
2346
  }
2330
- // ../../packages/aeg-core/src/premise-check.ts
2347
+ // ../../packages/aeg-core/src/objectives.ts
2331
2348
  import { createHash as createHash2 } from "node:crypto";
2349
+ var HEADING_RE = /^##[ \t]*Objectives[ \t]*$/im;
2350
+ var NEXT_HEADING_RE = /^##[ \t]/m;
2351
+ var OBJECTIVE_LINE_RE = /^O(\d+)\.[ \t]*(.*)$/;
2352
+ function hasBacktickedPath(text) {
2353
+ let i = 0;
2354
+ while (i < text.length) {
2355
+ const start = text.indexOf("`", i);
2356
+ if (start === -1)
2357
+ return false;
2358
+ const end = text.indexOf("`", start + 1);
2359
+ if (end === -1)
2360
+ return false;
2361
+ if (text.slice(start + 1, end).includes("/"))
2362
+ return true;
2363
+ i = end + 1;
2364
+ }
2365
+ return false;
2366
+ }
2367
+ var MIN_WORDS_OUTSIDE_BACKTICKS = 3;
2368
+ function stripObjectiveBackticks(text) {
2369
+ return text.replace(/`[^`\n]*`/g, " ");
2370
+ }
2371
+ function wordCount(text) {
2372
+ return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
2373
+ }
2374
+ function objectivesSectionText(body) {
2375
+ const heading = HEADING_RE.exec(body);
2376
+ if (!heading)
2377
+ return null;
2378
+ const afterHeading = body.slice(heading.index + heading[0].length);
2379
+ const next = NEXT_HEADING_RE.exec(afterHeading);
2380
+ return next ? afterHeading.slice(0, next.index) : afterHeading;
2381
+ }
2382
+ function hasObjectivesHeading(body) {
2383
+ return HEADING_RE.test(body);
2384
+ }
2385
+ function objectivesOf(body) {
2386
+ const section = objectivesSectionText(body);
2387
+ if (section === null) {
2388
+ return { ok: false, errors: ["no `## Objectives` heading found in the body."] };
2389
+ }
2390
+ const lines = section.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
2391
+ if (lines.length === 0) {
2392
+ return { ok: false, errors: ["the `## Objectives` section has no `O<n>.` lines."] };
2393
+ }
2394
+ const objectives = [];
2395
+ const errors = [];
2396
+ for (const line of lines) {
2397
+ const m = OBJECTIVE_LINE_RE.exec(line);
2398
+ if (!m) {
2399
+ errors.push(`"${line}" is not a well-formed objective line — expected \`O<n>. <sentence>\`, e.g. \`O1. …\`.`);
2400
+ continue;
2401
+ }
2402
+ const n = m[1];
2403
+ const text = m[2].trim();
2404
+ if (text.length === 0) {
2405
+ errors.push(`O${n} has no sentence — every objective is one observable sentence.`);
2406
+ continue;
2407
+ }
2408
+ if (hasBacktickedPath(text) && wordCount(stripObjectiveBackticks(text)) < MIN_WORDS_OUTSIDE_BACKTICKS) {
2409
+ errors.push(`O${n} is little more than a file path — an objective states an observable outcome, never a bare path (the Planner maps it to files).`);
2410
+ continue;
2411
+ }
2412
+ objectives.push({ id: `O${n}`, text });
2413
+ }
2414
+ for (let i = 0;i < objectives.length; i++) {
2415
+ const expected = i + 1;
2416
+ const actual = Number.parseInt(objectives[i].id.slice(1), 10);
2417
+ if (actual !== expected) {
2418
+ errors.push(`numbering is not contiguous from O1 — expected O${expected}, found ${objectives[i].id}.`);
2419
+ break;
2420
+ }
2421
+ }
2422
+ if (errors.length > 0)
2423
+ return { ok: false, errors };
2424
+ return { ok: true, objectives };
2425
+ }
2426
+ function normalizeText(text) {
2427
+ return text.trim().replace(/\s+/g, " ");
2428
+ }
2429
+ function objectivesVersion(objectives) {
2430
+ const normalized = objectives.map((o) => `${o.id} ${normalizeText(o.text)}`).join(`
2431
+ `);
2432
+ return createHash2("sha256").update(normalized).digest("hex");
2433
+ }
2434
+ function isIssueNotFoundError(err) {
2435
+ const stderr = err?.stderr;
2436
+ const haystack = [
2437
+ err?.message ?? "",
2438
+ typeof stderr === "string" ? stderr : stderr?.toString() ?? ""
2439
+ ].join(`
2440
+ `);
2441
+ return /could not resolve to an (?:issue|pull request)|\b404\b|not found/i.test(haystack);
2442
+ }
2443
+
2444
+ // ../../packages/aeg-core/src/premise-check.ts
2445
+ import { createHash as createHash3 } from "node:crypto";
2332
2446
  var ASSERTION_KINDS = new Set(["contains", "absent", "sha256"]);
2333
2447
  function isPremiseHeader(line) {
2334
2448
  const stripped = line.replace(/[*#]/g, "").trim();
@@ -2399,7 +2513,7 @@ function checkPremises(assertions, fileReader) {
2399
2513
  } else if (a.kind === "absent" && content.includes(a.value)) {
2400
2514
  failures.push(`premise-check: ${a.path} now contains "${a.value}", but the brief pinned it absent — the premise has moved; re-dig before proceeding.`);
2401
2515
  } else if (a.kind === "sha256") {
2402
- const actual = createHash2("sha256").update(content).digest("hex");
2516
+ const actual = createHash3("sha256").update(content).digest("hex");
2403
2517
  if (actual !== a.value.toLowerCase()) {
2404
2518
  failures.push(`premise-check: ${a.path} sha256 mismatch (brief pinned ${a.value}, file is now ${actual}) — the file changed since this brief was authored; re-dig before proceeding.`);
2405
2519
  }
@@ -2504,18 +2618,14 @@ function checkTestPlanExclusivity(prBody) {
2504
2618
  function checkPrincipalPlaceholder(prBody) {
2505
2619
  const region = testPlanRegion(prBody);
2506
2620
  const lineRe = /^-\s*\[[ xX]\]\s*\*{2}\[principal\]\*{2}(.*)$/gim;
2621
+ const errors = [];
2507
2622
  for (const m of region.matchAll(lineRe)) {
2508
2623
  const content = m[1] ?? "";
2509
2624
  if (/^\s*None\b/i.test(content)) {
2510
- return {
2511
- status: "fail",
2512
- errors: [
2513
- 'brief-validation Test Plan shape: a `**[principal]**` checkbox item is a "None" placeholder — if there is no principal-runnable surface, omit the item entirely; an untickable placeholder box blocks the merge gate forever.'
2514
- ]
2515
- };
2625
+ errors.push(`brief-validation Test Plan shape: a \`**[principal]**\` checkbox item is a "None" placeholder ("${content.trim()}") — if there is no principal-runnable surface, omit the item entirely; an untickable placeholder box blocks the merge gate forever.`);
2516
2626
  }
2517
2627
  }
2518
- return { status: "pass", errors: [] };
2628
+ return errors.length > 0 ? { status: "fail", errors } : { status: "pass", errors: [] };
2519
2629
  }
2520
2630
  function checkSurfaceMap(prBody) {
2521
2631
  return headingCheck(prBody, "(?:technical\\s+)?surface map", "Technical surface map");
@@ -2760,8 +2870,55 @@ function checkDefeatCases(prBody) {
2760
2870
  ]
2761
2871
  };
2762
2872
  }
2873
+ var PART_CITATION_RE = /Part\s+\d+\s*\(([^)]*)\)/gi;
2874
+ var OBJECTIVE_REF_RE = /O(\d+)/g;
2875
+ function citedObjectiveIds(section6) {
2876
+ const ids = new Set;
2877
+ for (const m of section6.matchAll(PART_CITATION_RE)) {
2878
+ for (const r of m[1].matchAll(OBJECTIVE_REF_RE))
2879
+ ids.add(Number.parseInt(r[1], 10));
2880
+ }
2881
+ return ids;
2882
+ }
2883
+ function checkObjectivesCopy(prBody, issueObjectives) {
2884
+ const parsed = objectivesOf(prBody);
2885
+ if (!parsed.ok) {
2886
+ return { status: "fail", errors: parsed.errors.map((e) => `brief-validation objectives copy: ${e}`) };
2887
+ }
2888
+ if (objectivesVersion(parsed.objectives) !== objectivesVersion(issueObjectives)) {
2889
+ return {
2890
+ status: "fail",
2891
+ errors: [
2892
+ "brief-validation objectives copy: the brief's `## Objectives` section does not match the Issue's — copy the Issue's `## Objectives` section (aeg-root/roles/developer.md)."
2893
+ ]
2894
+ };
2895
+ }
2896
+ return { status: "pass", errors: [] };
2897
+ }
2898
+ function checkObjectivesCoverage(prBody) {
2899
+ const parsed = objectivesOf(prBody);
2900
+ if (!parsed.ok) {
2901
+ return { status: "fail", errors: parsed.errors.map((e) => `brief-validation objectives coverage: ${e}`) };
2902
+ }
2903
+ const declaredIds = new Set(parsed.objectives.map((o) => Number.parseInt(o.id.slice(1), 10)));
2904
+ const section6 = extractNumberedSection(prBody, 6) ?? "";
2905
+ const cited = citedObjectiveIds(section6);
2906
+ const maxDeclared = Math.max(...declaredIds, 0);
2907
+ const errors = [];
2908
+ for (const id of declaredIds) {
2909
+ if (!cited.has(id)) {
2910
+ errors.push(`brief-validation objectives coverage: O${id} is not cited by any Part in §6 — every objective must be cited by at least one Part.`);
2911
+ }
2912
+ }
2913
+ for (const id of cited) {
2914
+ if (!declaredIds.has(id)) {
2915
+ errors.push(`brief-validation objectives coverage: a Part in §6 cites O${id}, but the Objectives section ends at O${maxDeclared}.`);
2916
+ }
2917
+ }
2918
+ return errors.length === 0 ? { status: "pass", errors: [] } : { status: "fail", errors };
2919
+ }
2763
2920
  function checkBriefSections(prBody, readTier, options = {}) {
2764
- const { requireClosesN = true, consumersOf = () => [] } = options;
2921
+ const { requireClosesN = true, consumersOf = () => [], issueObjectives } = options;
2765
2922
  const results = [
2766
2923
  checkTierField(prBody, readTier),
2767
2924
  checkTestPlan(prBody),
@@ -2779,10 +2936,23 @@ function checkBriefSections(prBody, readTier, options = {}) {
2779
2936
  checkCommandsCarryOutput(prBody),
2780
2937
  checkConsumerTests(prBody, consumersOf),
2781
2938
  checkDefeatCases(prBody),
2939
+ ...issueObjectives !== undefined ? [checkObjectivesCopy(prBody, issueObjectives), checkObjectivesCoverage(prBody)] : [],
2782
2940
  ...requireClosesN ? [checkClosesN(prBody)] : []
2783
2941
  ];
2784
2942
  return { errors: results.flatMap((r) => r.errors) };
2785
2943
  }
2944
+ var AEG_BRIEF_V1_MARKER = "<!-- aeg:brief:v1 -->";
2945
+ function contentAfterTwoLines(body) {
2946
+ const first = body.indexOf(`
2947
+ `);
2948
+ if (first === -1)
2949
+ return "";
2950
+ const second = body.indexOf(`
2951
+ `, first + 1);
2952
+ if (second === -1)
2953
+ return "";
2954
+ return body.slice(second + 1);
2955
+ }
2786
2956
  // ../../packages/aeg-core/src/doctrine-portability.ts
2787
2957
  var DEFAULT_SHIPS_PREFIX = "aeg-root/";
2788
2958
  var STATIC_PORTABLE_PREFIXES = [
@@ -2977,14 +3147,14 @@ function checkSectionDensity(prBody, heading) {
2977
3147
  ]
2978
3148
  };
2979
3149
  }
2980
- function checkSummaryDensity(prBody) {
2981
- return checkSectionDensity(prBody, "Summary");
3150
+ function checkDecisionsDensity(prBody) {
3151
+ return checkSectionDensity(prBody, "Decisions");
2982
3152
  }
2983
3153
  function checkScopeDensity(prBody) {
2984
3154
  return checkSectionDensity(prBody, "Scope");
2985
3155
  }
2986
3156
  function checkPrReportDensity(prBody) {
2987
- const results = [checkSummaryDensity(prBody), checkScopeDensity(prBody)];
3157
+ const results = [checkDecisionsDensity(prBody), checkScopeDensity(prBody)];
2988
3158
  return { errors: results.flatMap((r) => r.errors) };
2989
3159
  }
2990
3160
  // ../../packages/aeg-core/src/issue-validation.ts
@@ -3016,6 +3186,75 @@ function checkIssueRationale(body) {
3016
3186
  }
3017
3187
  return { status: errors.length > 0 ? "fail" : "pass", errors };
3018
3188
  }
3189
+ var OBJECTIVES_SINCE_ISSUE = 404;
3190
+ function checkIssueObjectives(body, issueNumber) {
3191
+ if (issueNumber !== null && issueNumber < OBJECTIVES_SINCE_ISSUE)
3192
+ return { status: "pass", errors: [] };
3193
+ const result = objectivesOf(body);
3194
+ if (result.ok)
3195
+ return { status: "pass", errors: [] };
3196
+ return { status: "fail", errors: result.errors.map((e) => `issue-validation objectives: ${e}`) };
3197
+ }
3198
+ function topLevelSectionText(body, headingName) {
3199
+ const headingRe = new RegExp(`^##[ \\t]*${headingName}[ \\t]*$`, "im");
3200
+ const heading = headingRe.exec(body);
3201
+ if (!heading)
3202
+ return null;
3203
+ const afterHeading = body.slice(heading.index + heading[0].length);
3204
+ const next = /^##[ \t]/m.exec(afterHeading);
3205
+ return next ? afterHeading.slice(0, next.index) : afterHeading;
3206
+ }
3207
+ function looksLikeFilePath(entry2) {
3208
+ const stripped = entry2.replace(/\/\*\*?$/, "");
3209
+ const lastSegment = stripped.split("/").pop() ?? stripped;
3210
+ const extMatch = /\.[A-Za-z0-9]{1,6}$/.exec(lastSegment);
3211
+ return extMatch !== null && extMatch.index > 0;
3212
+ }
3213
+ function splitGlobList(raw) {
3214
+ return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
3215
+ }
3216
+ function parseIssueSurface(body) {
3217
+ const section = topLevelSectionText(body, "Surface");
3218
+ if (section === null)
3219
+ return { ok: false, errors: ["no `## Surface` heading found in the body."] };
3220
+ const inLine = /^in:\s*(.+)$/im.exec(section);
3221
+ const outLine = /^out:\s*(.+)$/im.exec(section);
3222
+ const errors = [];
3223
+ if (!inLine)
3224
+ errors.push("`## Surface` has no `in:` line.");
3225
+ if (!outLine)
3226
+ errors.push("`## Surface` has no `out:` line.");
3227
+ if (errors.length > 0)
3228
+ return { ok: false, errors };
3229
+ const inGlobs = splitGlobList(inLine[1]);
3230
+ const outGlobs = splitGlobList(outLine[1]);
3231
+ for (const g of [...inGlobs, ...outGlobs]) {
3232
+ if (looksLikeFilePath(g)) {
3233
+ errors.push(`\`${g}\` in \`## Surface\` looks like a file path — Surface entries are directory-level globs, never file paths.`);
3234
+ }
3235
+ }
3236
+ if (inGlobs.length === 0)
3237
+ errors.push("`## Surface`'s `in:` line resolved to zero globs.");
3238
+ if (errors.length > 0)
3239
+ return { ok: false, errors };
3240
+ return { ok: true, value: { in: inGlobs, out: outGlobs } };
3241
+ }
3242
+ function globCoversPath(glob, path) {
3243
+ const g = glob.replace(/\/\*\*?$/, "").replace(/\/+$/, "");
3244
+ const p = path.replace(/\/+$/, "");
3245
+ return g === p || g.startsWith(`${p}/`) || p.startsWith(`${g}/`);
3246
+ }
3247
+ function checkSurfaceScope(changedFiles, outGlobs) {
3248
+ if (outGlobs.length === 0)
3249
+ return { ok: true };
3250
+ const violations = [];
3251
+ for (const file of changedFiles) {
3252
+ const glob = outGlobs.find((g) => globCoversPath(g, file));
3253
+ if (glob)
3254
+ violations.push({ file, glob });
3255
+ }
3256
+ return violations.length > 0 ? { ok: false, violations } : { ok: true };
3257
+ }
3019
3258
  function isTaskIssueLabelSet(labels) {
3020
3259
  return hasLabel("tranche", labels);
3021
3260
  }
@@ -3080,6 +3319,8 @@ function checkProjectsRegistered(body, _labels, registeredNames) {
3080
3319
  }
3081
3320
  return errors.length > 0 ? { status: "fail", errors } : { status: "pass", errors: [] };
3082
3321
  }
3322
+ var DOC_PATH_RE = /(?:(?:aeg-root|apps|packages|specs|docs|tools|\.claude|\.github)\/[\w./@-]*\.(?:md|mdx)|\.claude\/(?:skills|rules)\/[\w./-]+|\b(?:docs-index|decisions|projects|state-machine|enforcement|process|README|CLAUDE)\.md\b|\b[\w-]+-(?:spec|decisions|backlog)\.md\b)/i;
3323
+ var DOC_PATH_RE_GLOBAL = new RegExp(DOC_PATH_RE.source, "gi");
3083
3324
 
3084
3325
  // ../../packages/aeg-core/src/coherence-checks.ts
3085
3326
  var COHERENCE_ENFORCED_FROM = "2026-07-01";
@@ -3277,7 +3518,8 @@ function checkR1(issuesBySlug, grandfatheredIssues, registeredNames = []) {
3277
3518
  continue;
3278
3519
  const errors = [
3279
3520
  ...checkIssueRationale(issue.body).errors,
3280
- ...checkProjectsRegistered(issue.body, issue.labels, registeredNames).errors
3521
+ ...checkProjectsRegistered(issue.body, issue.labels, registeredNames).errors,
3522
+ ...checkIssueObjectives(issue.body, issue.number).errors
3281
3523
  ];
3282
3524
  if (errors.length === 0)
3283
3525
  continue;
@@ -3392,6 +3634,27 @@ function checkClosesN2(branch, prBody, trancheFiles, taskIssueRefs) {
3392
3634
  }
3393
3635
  return { ok: true, expectedIssue };
3394
3636
  }
3637
+ // ../../packages/aeg-core/src/archive-task.ts
3638
+ function closesRefs(text) {
3639
+ return [...new Set([...text.matchAll(/Closes #(\d+)/gi)].map((m) => Number(m[1])))];
3640
+ }
3641
+ function extractIssue(body) {
3642
+ const stripped = stripCode(body);
3643
+ const bodyNums = closesRefs(stripped);
3644
+ const anchored = anchoredRegion(stripped, "CLOSES");
3645
+ if (anchored !== null) {
3646
+ const anchorNums = closesRefs(anchored);
3647
+ if (anchorNums.length === 0)
3648
+ return { issue: null, extraIssues: bodyNums, outsideHeader: false };
3649
+ const issue2 = anchorNums[0];
3650
+ return { issue: issue2, extraIssues: bodyNums.filter((n) => n !== issue2), outsideHeader: false };
3651
+ }
3652
+ const headerNums = closesRefs(headerRegion(stripped));
3653
+ if (bodyNums.length === 0)
3654
+ return { issue: null, extraIssues: [], outsideHeader: false };
3655
+ const issue = headerNums.length > 0 ? headerNums[0] : bodyNums[0];
3656
+ return { issue, extraIssues: bodyNums.filter((n) => n !== issue), outsideHeader: headerNums.length === 0 };
3657
+ }
3395
3658
  // ../../packages/aeg-core/src/review-gate.ts
3396
3659
  function isBoundToHead(extraction, headSha) {
3397
3660
  if (!extraction.headSha)
@@ -3410,6 +3673,11 @@ function isBoundByPatchIdentity(extraction, headSha, patchIdOf) {
3410
3673
  function isBoundToPatch(extraction, headSha, patchIdOf) {
3411
3674
  return isBoundToHead(extraction, headSha) || isBoundByPatchIdentity(extraction, headSha, patchIdOf);
3412
3675
  }
3676
+ function isBoundToObjectives(extraction, currentVersion) {
3677
+ if (currentVersion === null)
3678
+ return true;
3679
+ return extraction.objectivesVersion === currentVersion;
3680
+ }
3413
3681
  function checkReviewGate(input) {
3414
3682
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
3415
3683
  const waived = isWaiverLabelActorVerified({
@@ -3436,7 +3704,9 @@ function checkReviewGate(input) {
3436
3704
  const securityClean = security.value === "PASS";
3437
3705
  const codeReviewBound = isBoundToPatch(codeReview, input.headSha, input.patchIdOf);
3438
3706
  const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
3439
- if (codeReviewClean && codeReviewBound && securityClean && securityBound && mechanicalChecksClean) {
3707
+ const codeReviewObjectivesBound = isBoundToObjectives(codeReview, input.objectivesVersion);
3708
+ const securityObjectivesBound = isBoundToObjectives(security, input.objectivesVersion);
3709
+ if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && securityClean && securityBound && securityObjectivesBound && mechanicalChecksClean) {
3440
3710
  return {
3441
3711
  verdict: "pass",
3442
3712
  reason: `code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS, both covering head ${input.headSha}, and every reported mechanical check is green.`,
@@ -3448,11 +3718,15 @@ function checkReviewGate(input) {
3448
3718
  problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
3449
3719
  } else if (!codeReviewBound) {
3450
3720
  problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3721
+ } else if (!codeReviewObjectivesBound) {
3722
+ problems.push(`the newest code-review verdict was cast against objectives version ${codeReview.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3451
3723
  }
3452
3724
  if (!securityClean) {
3453
3725
  problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
3454
3726
  } else if (!securityBound) {
3455
3727
  problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3728
+ } else if (!securityObjectivesBound) {
3729
+ problems.push(`the newest security-review verdict was cast against objectives version ${security.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3456
3730
  }
3457
3731
  if (!mechanicalChecksClean) {
3458
3732
  problems.push(reportedMechanicalChecks.length === 0 ? "no mechanical checks have reported for this head yet" : `mechanical check(s) not green: ${reportedMechanicalChecks.filter((c) => c.bucket !== "pass").map((c) => `${c.name} (${c.bucket})`).join(", ")}`);
@@ -3576,16 +3850,33 @@ function checkG1(rows, existsFn) {
3576
3850
  return { check: "G1", status: findings.length > 0 ? "fail" : "pass", findings };
3577
3851
  }
3578
3852
  var SCAFFOLD_PLACEHOLDER = "[undocumented — fill in why]";
3853
+ var NON_GATE_BIN_NAMES = NON_GATE_BINS;
3854
+ var TWIN_CANDIDATE_EXCEPTIONS = {
3855
+ "apps/cli/src/checks/bin/check-dead-branch-push.ts": 'Ring 0 "Pushing to a branch whose pull request already resolved" (implementation: check-push-target.ts)',
3856
+ "apps/cli/src/checks/bin/check-issue-assignment.ts": `Ring 0 "A task branch's first push (Issue self-assignment)" (implementation: assign-task-issue.ts)`,
3857
+ "apps/cli/src/checks/bin/check-closes-n.ts": 'Ring 1 "Closes linkage" (implementation: verify-coherence.ts)'
3858
+ };
3859
+ var NON_GATE_HOOK_SCRIPTS = [".claude/hooks/track-transcript.sh"];
3579
3860
  function checkG2(rows, candidateFiles) {
3580
3861
  const implementations = new Set(rows.map((r) => r.implementation).filter((p) => p !== ""));
3862
+ const claimedNames = new Set(rows.flatMap((r) => claimedCheckNames(r.implementation)));
3581
3863
  const findings = [];
3582
3864
  for (const path of candidateFiles) {
3583
- if (!implementations.has(path)) {
3584
- findings.push({
3585
- path,
3586
- reason: `"${path}" is not named as the implementation of any row in enforcement.md's ring tables`
3587
- });
3588
- }
3865
+ if (implementations.has(path))
3866
+ continue;
3867
+ if (path.startsWith(AEG_CORE_BIN_PATH_PREFIX) && NON_GATE_BIN_NAMES.includes(basenameNoExt(path)))
3868
+ continue;
3869
+ if (NON_GATE_HOOK_SCRIPTS.includes(path))
3870
+ continue;
3871
+ if (path in TWIN_CANDIDATE_EXCEPTIONS)
3872
+ continue;
3873
+ const candidateNames = claimedCheckNames(path);
3874
+ if (candidateNames.length > 0 && candidateNames.some((n) => claimedNames.has(n)))
3875
+ continue;
3876
+ findings.push({
3877
+ path,
3878
+ reason: `"${path}" is not named as the implementation of any row in enforcement.md's ring tables`
3879
+ });
3589
3880
  }
3590
3881
  for (const row of rows) {
3591
3882
  const carriesPlaceholder = [row.summary, row.description, row.spec].some((cell) => cell === SCAFFOLD_PLACEHOLDER);
@@ -3597,7 +3888,7 @@ function checkG2(rows, candidateFiles) {
3597
3888
  });
3598
3889
  }
3599
3890
  }
3600
- return { check: "G2", status: findings.length > 0 ? "info" : "pass", findings };
3891
+ return { check: "G2", status: findings.length > 0 ? "fail" : "pass", findings };
3601
3892
  }
3602
3893
  function checkG3(ring0Rows, crossingFiles) {
3603
3894
  const ring0Implementations = new Set(ring0Rows.map((r) => r.implementation));
@@ -3688,55 +3979,20 @@ function checkG6(rows, registeredCheckNames) {
3688
3979
  }
3689
3980
  return { check: "G6", status: findings.length > 0 ? "fail" : "pass", findings };
3690
3981
  }
3691
- // ../../packages/aeg-core/src/dead-branch-push-guard.ts
3692
- function checkDeadBranchPush(input) {
3693
- const { branch, prState, prNumber } = input;
3694
- if (prState === "MERGED") {
3695
- return {
3696
- verdict: "refuse",
3697
- reason: `Branch \`${branch}\` already has a MERGED PR (#${prNumber}) — this branch's work is done. Push a new task branch instead of pushing onto an already-resolved one.`
3698
- };
3699
- }
3700
- if (prState === "CLOSED") {
3701
- return {
3702
- verdict: "refuse",
3703
- reason: `Branch \`${branch}\` has a CLOSED PR (#${prNumber}) that was never merged — pushing here would resurrect abandoned/rejected work. Open a new task branch instead.`
3704
- };
3705
- }
3706
- if (prState === "OPEN") {
3707
- return {
3708
- verdict: "allow",
3709
- reason: `Branch \`${branch}\` has an OPEN PR (#${prNumber}) — pushing more commits to it is normal.`
3710
- };
3711
- }
3712
- if (prState === "NONE") {
3713
- return {
3714
- verdict: "allow",
3715
- reason: `Branch \`${branch}\` has no PR yet — a brand-new task branch legitimately has none.`
3716
- };
3717
- }
3718
- return {
3719
- verdict: "allow",
3720
- reason: `Could not determine \`${branch}\`'s PR state (forge unreachable) — failing open rather than blocking the push on a transient issue.`
3721
- };
3982
+ // ../../packages/aeg-core/src/reader-resolvable-prose.ts
3983
+ var PRODUCT_SLUG_SCOPE = [
3984
+ "apps/cli/src",
3985
+ ".github/workflows",
3986
+ ".vinaya",
3987
+ "apps/cli/README.md",
3988
+ "packages/sources/README.md"
3989
+ ];
3990
+ function isUnderOrEqual(path, prefix) {
3991
+ return path === prefix || path.startsWith(`${prefix}/`);
3722
3992
  }
3723
- // ../../packages/aeg-core/src/baseline-capture.ts
3724
- function captureBaseline(counts, capturedAt) {
3725
- return counts.map((c) => ({ tool: c.tool, findingCount: c.findingCount, capturedAt }));
3993
+ function isProductScopeFile(path) {
3994
+ return PRODUCT_SLUG_SCOPE.some((prefix) => isUnderOrEqual(path, prefix));
3726
3995
  }
3727
- function compareToBaseline(current, baseline) {
3728
- const perTool = current.map((c) => {
3729
- const baselineEntry = baseline.find((b) => b.tool === c.tool);
3730
- const baselineCount = baselineEntry ? baselineEntry.findingCount : c.findingCount;
3731
- return { tool: c.tool, baseline: baselineCount, current: c.findingCount, delta: c.findingCount - baselineCount };
3732
- });
3733
- return {
3734
- withinBudget: perTool.every((t) => t.delta <= 0),
3735
- delta: perTool.reduce((sum, t) => sum + t.delta, 0),
3736
- perTool
3737
- };
3738
- }
3739
- // ../../packages/aeg-core/src/reader-resolvable-prose.ts
3740
3996
  var SHIPS_PREFIX = "aeg-root/";
3741
3997
  function shipsArchivePrefix(shipsPrefix) {
3742
3998
  return `${shipsPrefix}tranches/completed/`;
@@ -3756,6 +4012,8 @@ function classifyProseFile(path, readerFacingPrefix, readerFacingSuffix, shipsPr
3756
4012
  }
3757
4013
  if (isSpecFile2(path) || isClaudeMdFile(path))
3758
4014
  return "internal";
4015
+ if (isProductScopeFile(path))
4016
+ return "product";
3759
4017
  return null;
3760
4018
  }
3761
4019
  var SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
@@ -3768,6 +4026,9 @@ function stripNonProse(path, content) {
3768
4026
  }
3769
4027
  return content;
3770
4028
  }
4029
+ function stripNonProseForProduct(path, content) {
4030
+ return path.endsWith(".md") ? stripNonProse(path, content) : content;
4031
+ }
3771
4032
  function lineAt(content, index) {
3772
4033
  let line = 1;
3773
4034
  for (let i = 0;i < index; i++) {
@@ -3792,12 +4053,19 @@ function checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuff
3792
4053
  { pattern: TRANCHE_SLUG_VN_PATTERN, what: "an internal tranche slug" },
3793
4054
  ...legacyPattern ? [{ pattern: legacyPattern, what: "an internal tranche slug", group: 2 }] : []
3794
4055
  ];
4056
+ const productPatterns = [
4057
+ { pattern: TRANCHE_SLUG_VN_PATTERN, what: "an internal tranche slug in product code" }
4058
+ ];
3795
4059
  for (const file of files) {
3796
4060
  const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix, shipsPrefix);
3797
- if (!cls || !SWEPT_CLASSES.has(cls))
4061
+ if (!cls || cls === "internal")
3798
4062
  continue;
3799
- const scrubbed = stripNonProse(file.path, file.content);
3800
- for (const { pattern, what, group } of patterns) {
4063
+ if (cls !== "product" && !SWEPT_CLASSES.has(cls))
4064
+ continue;
4065
+ const blocking = cls === "product";
4066
+ const scrubbed = blocking ? stripNonProseForProduct(file.path, file.content) : stripNonProse(file.path, file.content);
4067
+ const patternsToRun = blocking ? productPatterns : patterns;
4068
+ for (const { pattern, what, group } of patternsToRun) {
3801
4069
  pattern.lastIndex = 0;
3802
4070
  let match = pattern.exec(scrubbed);
3803
4071
  while (match !== null) {
@@ -3805,7 +4073,8 @@ function checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuff
3805
4073
  findings.push({
3806
4074
  file: file.path,
3807
4075
  line: lineAt(scrubbed, match.index),
3808
- message: `references ${what} ("${cited}") a reader outside this repo's tracker cannot resolve`
4076
+ message: `references ${what} ("${cited}") a reader outside this repo's tracker cannot resolve`,
4077
+ blocking
3809
4078
  });
3810
4079
  match = pattern.exec(scrubbed);
3811
4080
  }
@@ -3855,7 +4124,8 @@ function checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, read
3855
4124
  findings.push({
3856
4125
  file: file.path,
3857
4126
  line,
3858
- message: `uses coined term "${term}" without defining it inline or linking the glossary`
4127
+ message: `uses coined term "${term}" without defining it inline or linking the glossary`,
4128
+ blocking: false
3859
4129
  });
3860
4130
  }
3861
4131
  }
@@ -3878,6 +4148,7 @@ function parseGlossaryTerms(glossaryContent) {
3878
4148
  }
3879
4149
  return terms;
3880
4150
  }
4151
+
3881
4152
  // ../../packages/aeg-core/src/quoted-command.ts
3882
4153
  var QUOTED_COMMAND_SHIPS_PREFIX = "aeg-root/";
3883
4154
  var QUOTED_COMMAND_SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
@@ -3972,6 +4243,54 @@ function evaluateCitedQuotes(citedQuotes, citedFileContents) {
3972
4243
  }
3973
4244
  return findings;
3974
4245
  }
4246
+ // ../../packages/aeg-core/src/dead-branch-push-guard.ts
4247
+ function checkDeadBranchPush(input) {
4248
+ const { branch, prState, prNumber } = input;
4249
+ if (prState === "MERGED") {
4250
+ return {
4251
+ verdict: "refuse",
4252
+ reason: `Branch \`${branch}\` already has a MERGED PR (#${prNumber}) — this branch's work is done. Push a new task branch instead of pushing onto an already-resolved one.`
4253
+ };
4254
+ }
4255
+ if (prState === "CLOSED") {
4256
+ return {
4257
+ verdict: "refuse",
4258
+ reason: `Branch \`${branch}\` has a CLOSED PR (#${prNumber}) that was never merged — pushing here would resurrect abandoned/rejected work. Open a new task branch instead.`
4259
+ };
4260
+ }
4261
+ if (prState === "OPEN") {
4262
+ return {
4263
+ verdict: "allow",
4264
+ reason: `Branch \`${branch}\` has an OPEN PR (#${prNumber}) — pushing more commits to it is normal.`
4265
+ };
4266
+ }
4267
+ if (prState === "NONE") {
4268
+ return {
4269
+ verdict: "allow",
4270
+ reason: `Branch \`${branch}\` has no PR yet — a brand-new task branch legitimately has none.`
4271
+ };
4272
+ }
4273
+ return {
4274
+ verdict: "allow",
4275
+ reason: `Could not determine \`${branch}\`'s PR state (forge unreachable) — failing open rather than blocking the push on a transient issue.`
4276
+ };
4277
+ }
4278
+ // ../../packages/aeg-core/src/baseline-capture.ts
4279
+ function captureBaseline(counts, capturedAt) {
4280
+ return counts.map((c) => ({ tool: c.tool, findingCount: c.findingCount, capturedAt }));
4281
+ }
4282
+ function compareToBaseline(current, baseline) {
4283
+ const perTool = current.map((c) => {
4284
+ const baselineEntry = baseline.find((b) => b.tool === c.tool);
4285
+ const baselineCount = baselineEntry ? baselineEntry.findingCount : c.findingCount;
4286
+ return { tool: c.tool, baseline: baselineCount, current: c.findingCount, delta: c.findingCount - baselineCount };
4287
+ });
4288
+ return {
4289
+ withinBudget: perTool.every((t) => t.delta <= 0),
4290
+ delta: perTool.reduce((sum, t) => sum + t.delta, 0),
4291
+ perTool
4292
+ };
4293
+ }
3975
4294
  // ../../packages/aeg-core/src/retired-vocabulary.ts
3976
4295
  var RETIRED_PATTERNS = [
3977
4296
  String.raw`\bD-[0-9]{3}\b`,
@@ -4441,6 +4760,223 @@ function findWorkspaceEscapes(files, knownPaths, workspaceDirs = DEFAULT_WORKSPA
4441
4760
  }
4442
4761
  return findings;
4443
4762
  }
4763
+ // ../../packages/aeg-core/src/log/schema.ts
4764
+ import { z } from "zod";
4765
+ var ROLE_VALUES = [
4766
+ "planner",
4767
+ "developer",
4768
+ "code-reviewer",
4769
+ "security",
4770
+ "principal",
4771
+ "archivist",
4772
+ "architect"
4773
+ ];
4774
+ var RoleSchema = z.enum(ROLE_VALUES);
4775
+ var HOST_VALUES = ["hook", "ci", "cli", "loop"];
4776
+ var HostSchema = z.enum(HOST_VALUES);
4777
+ var RUN_ID_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
4778
+ var HeaderMetaSchema = z.object({
4779
+ schema: z.literal(1),
4780
+ ts: z.string(),
4781
+ run_id: z.string().regex(RUN_ID_PATTERN),
4782
+ seq: z.number().int().nonnegative(),
4783
+ repo: z.string().nullable(),
4784
+ vinaya: z.string(),
4785
+ doctrine: z.string(),
4786
+ host: HostSchema,
4787
+ machine: z.string()
4788
+ }).strict();
4789
+ var SubjectSchema = z.object({
4790
+ issue: z.number().int().nullable(),
4791
+ pr: z.number().int().optional(),
4792
+ sha: z.string().optional(),
4793
+ role: z.union([RoleSchema, z.literal("unattributed")]),
4794
+ round: z.number().int().optional(),
4795
+ objectives_version: z.string().optional()
4796
+ }).strict();
4797
+ var HeaderSchema = z.object({
4798
+ meta: HeaderMetaSchema,
4799
+ subject: SubjectSchema
4800
+ }).strict();
4801
+ var envelopeTail = {
4802
+ duration_ms: z.number().nonnegative().optional(),
4803
+ payload: z.object({}).strict()
4804
+ };
4805
+ var DispatchOutcomeSchema = z.discriminatedUnion("type", [
4806
+ z.object({ type: z.literal("pr_opened"), pr: z.number().int(), head: z.string() }).strict(),
4807
+ z.object({
4808
+ type: z.literal("round_pushed"),
4809
+ pr: z.number().int(),
4810
+ head: z.string(),
4811
+ comment_id: z.number().int()
4812
+ }).strict(),
4813
+ z.object({
4814
+ type: z.literal("verdict"),
4815
+ verdict: z.enum(["APPROVE", "REQUEST CHANGES", "PASS", "FAIL"]),
4816
+ head: z.string(),
4817
+ comment_id: z.number().int(),
4818
+ objectives: z.array(z.object({ id: z.string(), met: z.boolean() }).strict()),
4819
+ findings: z.array(z.object({ id: z.string(), severity: z.string(), state: z.string().optional() }).strict())
4820
+ }).strict(),
4821
+ z.object({
4822
+ type: z.literal("escalation"),
4823
+ class: z.enum(["authority", "strategy", "product"]),
4824
+ comment_id: z.number().int()
4825
+ }).strict(),
4826
+ z.object({ type: z.literal("brief"), comment_id: z.number().int(), hash: z.string() }).strict(),
4827
+ z.object({ type: z.literal("plan"), issues: z.array(z.number().int()) }).strict(),
4828
+ z.object({ type: z.literal("archive"), provenance_comment_id: z.number().int() }).strict()
4829
+ ]);
4830
+ var dispatchShared = {
4831
+ meta: HeaderMetaSchema,
4832
+ subject: SubjectSchema,
4833
+ kind: z.literal("dispatch"),
4834
+ ...envelopeTail,
4835
+ target_role: RoleSchema,
4836
+ model: z.string(),
4837
+ round: z.number().int().optional(),
4838
+ effect_id: z.string()
4839
+ };
4840
+ var dispatchUsageField = z.object({ input: z.number().nonnegative(), output: z.number().nonnegative() }).strict().nullable();
4841
+ var DispatchEventSchema = z.discriminatedUnion("event", [
4842
+ z.object({ ...dispatchShared, event: z.literal("dispatched"), prompt_hash: z.string() }).strict(),
4843
+ z.object({
4844
+ ...dispatchShared,
4845
+ event: z.literal("outcome_received"),
4846
+ outcome: DispatchOutcomeSchema,
4847
+ usage: dispatchUsageField
4848
+ }).strict(),
4849
+ z.object({
4850
+ ...dispatchShared,
4851
+ event: z.literal("dispatch_failed"),
4852
+ reason: z.enum(["timeout", "crash", "refused", "unattributed_write"]),
4853
+ usage: dispatchUsageField
4854
+ }).strict()
4855
+ ]);
4856
+ var loopShared = {
4857
+ meta: HeaderMetaSchema,
4858
+ subject: SubjectSchema,
4859
+ kind: z.literal("dev_review_loop"),
4860
+ ...envelopeTail,
4861
+ loop_id: z.string()
4862
+ };
4863
+ var DevReviewLoopEventSchema = z.discriminatedUnion("event", [
4864
+ z.object({
4865
+ ...loopShared,
4866
+ event: z.literal("loop_started"),
4867
+ task: z.number().int(),
4868
+ policy: z.object({
4869
+ max_rounds: z.number().int().nonnegative(),
4870
+ reviewers: z.array(RoleSchema),
4871
+ models: z.record(RoleSchema, z.string())
4872
+ }).strict()
4873
+ }).strict(),
4874
+ z.object({ ...loopShared, event: z.literal("round_started"), round: z.number().int(), base_head: z.string() }).strict(),
4875
+ z.object({
4876
+ ...loopShared,
4877
+ event: z.literal("gate_result_read"),
4878
+ round: z.number().int(),
4879
+ head: z.string(),
4880
+ green: z.boolean()
4881
+ }).strict(),
4882
+ z.object({
4883
+ ...loopShared,
4884
+ event: z.literal("verdicts_read"),
4885
+ round: z.number().int(),
4886
+ head: z.string(),
4887
+ all_approve: z.boolean(),
4888
+ blockers: z.number().int().nonnegative()
4889
+ }).strict(),
4890
+ z.object({
4891
+ ...loopShared,
4892
+ event: z.literal("findings_compared"),
4893
+ round: z.number().int(),
4894
+ open: z.array(z.string()),
4895
+ resolved: z.array(z.string()),
4896
+ new: z.array(z.string()),
4897
+ recurring: z.array(z.string())
4898
+ }).strict(),
4899
+ z.object({
4900
+ ...loopShared,
4901
+ event: z.literal("stop_condition_met"),
4902
+ round: z.number().int(),
4903
+ condition: z.enum([
4904
+ "green",
4905
+ "max_rounds",
4906
+ "no_progress",
4907
+ "escalated",
4908
+ "principal_stop",
4909
+ "confidence",
4910
+ "reappearance"
4911
+ ])
4912
+ }).strict(),
4913
+ z.object({
4914
+ ...loopShared,
4915
+ event: z.literal("paused"),
4916
+ round: z.number().int(),
4917
+ reason: z.enum(["escalation", "principal_item", "refreeze_needed"])
4918
+ }).strict(),
4919
+ z.object({
4920
+ ...loopShared,
4921
+ event: z.literal("resumed"),
4922
+ round: z.number().int(),
4923
+ by: z.literal("principal")
4924
+ }).strict(),
4925
+ z.object({
4926
+ ...loopShared,
4927
+ event: z.literal("round_ended"),
4928
+ round: z.number().int(),
4929
+ base_head: z.string(),
4930
+ head: z.string(),
4931
+ files_changed: z.number().int().nonnegative(),
4932
+ insertions: z.number().int().nonnegative(),
4933
+ deletions: z.number().int().nonnegative(),
4934
+ wall_ms: z.number().nonnegative(),
4935
+ outcome: z.enum(["green", "changes_requested", "escalated"])
4936
+ }).strict(),
4937
+ z.object({
4938
+ ...loopShared,
4939
+ event: z.literal("journal_finalized"),
4940
+ rounds: z.number().int().nonnegative(),
4941
+ total_wall_ms: z.number().nonnegative(),
4942
+ time_to_green_ms: z.number().nonnegative().nullable(),
4943
+ files_changed_total: z.number().int().nonnegative(),
4944
+ final_head: z.string(),
4945
+ result: z.enum(["merged_ready", "stopped"])
4946
+ }).strict()
4947
+ ]);
4948
+ var ForgeOpSchema = z.enum([
4949
+ "pr.create",
4950
+ "pr.comment",
4951
+ "pr.body.replace",
4952
+ "pr.refreeze",
4953
+ "issue.create",
4954
+ "issue.edit",
4955
+ "issue.comment",
4956
+ "milestone.create",
4957
+ "milestone.edit",
4958
+ "milestone.close",
4959
+ "label.add",
4960
+ "label.remove"
4961
+ ]);
4962
+ var ForgeWriteTargetSchema = z.object({
4963
+ issue: z.number().int().optional(),
4964
+ pr: z.number().int().optional()
4965
+ }).strict();
4966
+ var forgeWriteShared = {
4967
+ meta: HeaderMetaSchema,
4968
+ subject: SubjectSchema,
4969
+ kind: z.literal("forge_write"),
4970
+ ...envelopeTail,
4971
+ op: ForgeOpSchema,
4972
+ target: ForgeWriteTargetSchema
4973
+ };
4974
+ var ForgeWriteEventSchema = z.discriminatedUnion("event", [
4975
+ z.object({ ...forgeWriteShared, event: z.literal("validated") }).strict(),
4976
+ z.object({ ...forgeWriteShared, event: z.literal("refused"), reason: z.string() }).strict(),
4977
+ z.object({ ...forgeWriteShared, event: z.literal("written"), comment_ids: z.array(z.string()) }).strict()
4978
+ ]);
4979
+ var LogEventSchema = z.union([DispatchEventSchema, DevReviewLoopEventSchema, ForgeWriteEventSchema]);
4444
4980
  // ../../packages/sources/src/commands.ts
4445
4981
  var COMMANDS = [
4446
4982
  {
@@ -4554,11 +5090,24 @@ var COMMANDS = [
4554
5090
  { flag: "--out <path>", description: "Write the rendered brief to a file instead of stdout" }
4555
5091
  ],
4556
5092
  details: [
4557
- "Reads the task Issue (`vinaya/tranche:<tranche>`-labeled, id `<n>`) and the tree, and fills every section a program can derive: the header `Project:`/`Tier:`/`Closes #N`, the Step 0 worktree line, the dispatch-gate status as the pre-flight line, §4's file list (with consumer packages and a `sha256` premise pin per file), §7 from the `.vinaya/doc-owners` derivation, and every remaining section from the Issue's eight-field Planner rationale. Refuses — naming the missing fact — when a derived section cannot be derived: no Issue, the dispatch gate not clear, or a `--surfaces` glob matching no tracked file.",
5093
+ "Reads the task Issue (`vinaya/tranche:<tranche>`-labeled, id `<n>`) and the tree, and fills every section a program can derive: the header `Project:`/`Tier:`/`Closes #N`, the Step 0 worktree line, the dispatch-gate status as the pre-flight line, §4's file list (with consumer packages and a `sha256` premise pin per file, and Out of surface from the Issue's own `## Surface` `out:` list), §6 from the Issue's `## Parts`, §7 from the `.vinaya/doc-owners` derivation, §9 from the Issue's `## Test plan`, §10 from the Issue's `## Stop conditions` plus the rationale's Stop-and-escalate field, and every remaining section from the Issue's eight-field Planner rationale. Refuses — naming the missing section — when it cannot be derived: no Issue, the dispatch gate not clear, a `--surfaces` glob matching no tracked file, or the Issue missing/malformed `## Surface`/`## Parts`/`## Test plan`/`## Stop conditions` — never a bracketed placeholder.",
4558
5094
  "Never writes under `aeg-root/` or to the Issue — a brief is pasted to the Developer, never committed. Reads `aeg-root/templates/brief-template.md` at run time for its one fixed sentence and the standing autonomy clause; every other byte is generated from the forge/tree facts."
4559
5095
  ],
4560
5096
  status: "shipped"
4561
5097
  },
5098
+ {
5099
+ name: "task dispatch",
5100
+ description: "Render, pin, and post the brief on the Issue as the frozen original; start the developer",
5101
+ flags: [
5102
+ { flag: "--agent <claude|codex|gemini>", description: "Start the developer through dispatchRole once posted" }
5103
+ ],
5104
+ details: [
5105
+ "Renders the brief from the Issue and the tree (the same assembly `brief render` uses) and posts it once as an Issue comment whose first line is `<!-- aeg:brief:v1 -->` and whose second line is `Brief hash: <sha256>` — the hash covers only the body below those two lines, so any reader recomputes it. Refuses outright, naming the existing comment's URL, when a `v1` comment already exists on the Issue — the brief is frozen by design, never overwritten or silently reissued.",
5106
+ "With `--agent`, starts the Developer through `dispatchRole` when `apps/cli/src/lib/dispatch.ts` exports it; otherwise prints the rendered brief and the manual dispatch instruction and exits `0` — a soft dependency, never a hard block.",
5107
+ "Principal-only, with or without `--agent`: refuses before any render, forge read, or post when the authenticated `gh` identity is not on the Principal allowlist (or cannot be resolved at all) — dispatching is the `todo → in-flight` transition, not a general-purpose comment poster."
5108
+ ],
5109
+ status: "shipped"
5110
+ },
4562
5111
  {
4563
5112
  name: "pr create",
4564
5113
  description: "Open a pull request after full brief-schema validation",
@@ -4611,6 +5160,19 @@ var COMMANDS = [
4611
5160
  ],
4612
5161
  status: "shipped"
4613
5162
  },
5163
+ {
5164
+ name: "pr rule",
5165
+ description: "Post a Principal ruling on a PR, marked and versioned — never mistaken for a review verdict",
5166
+ flags: [
5167
+ { flag: "--file", description: "Path to the ruling file to post as a PR comment" },
5168
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
5169
+ ],
5170
+ details: [
5171
+ "Refuses before posting when the file's first line reads as an escalation (`ESCALATE:`), or when the file carries verdict grammar anywhere `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` would treat as a candidate — a `VERDICT:` line past the extractor's own first-five-line window still counts, since the whole-body candidate test is what disqualifies the file, not merely a clean read.",
5172
+ "Marker numbers (`<!-- aeg:principal:ruling:<pr>-<k> -->`) are counted on the forge at post time from `gh pr view --json comments`, never derived from a local file — two rulings racing to the same number are a known, undocumented-lock case."
5173
+ ],
5174
+ status: "shipped"
5175
+ },
4614
5176
  {
4615
5177
  name: "pr verify-evidence",
4616
5178
  description: "Prove a pull request's AEG:EVIDENCE region was machine-generated — regenerate it and compare",
@@ -4652,6 +5214,44 @@ var COMMANDS = [
4652
5214
  ],
4653
5215
  status: "shipped"
4654
5216
  },
5217
+ {
5218
+ name: "issue objectives edit",
5219
+ description: "Rewrite a task Issue's `## Objectives` section by command — versioned, findable on the forge",
5220
+ flags: [
5221
+ { flag: "--add", description: "Append a new objective as `O<max+1>` with the given sentence" },
5222
+ { flag: "--drop", description: "Remove an objective by id (`O<k>`) — never renumbers the survivors" },
5223
+ {
5224
+ flag: "--replace",
5225
+ description: 'Replace an objective\'s sentence in place, keeping its id (`O<k>` "<sentence>")'
5226
+ },
5227
+ { flag: "--reason", description: "Required, non-empty: why this change is being made" },
5228
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
5229
+ ],
5230
+ details: [
5231
+ "Exactly one of `--add`/`--drop`/`--replace` is required. Every edit goes through the same validated `issue edit` write path (`writeValidatedIssueEdit`) as `vinaya issue edit` itself — no second, unvalidated write path.",
5232
+ "A `--drop` that leaves the surviving objectives non-contiguous from `O1` is refused with `objectivesOf`'s own parser message: dropping never renumbers survivors, and task 1's contiguous-from-O1 grammar is the one parser everything else reads, so a live contradiction between the two stops here for a Principal ruling rather than silently renumbering.",
5233
+ "Splices the rendered section back in place (`## Objectives` heading through the next `##` heading, or end of body) — every other byte of the Issue body is untouched. Posts one comment marked `<!-- aeg:objectives:v<k> -->` carrying the previous list, the new list, the reason, and the new `objectivesVersion`; `k` is counted on the forge at post time, never derived from a local file."
5234
+ ],
5235
+ status: "shipped"
5236
+ },
5237
+ {
5238
+ name: "log flush",
5239
+ description: "Post a target Issue or PR's outbox as one or more marked comments, then truncate what the forge confirmed",
5240
+ flags: [
5241
+ { flag: "--issue", description: "Flush this Issue's outbox (`~/.vinaya/outbox/<owner>-<repo>/<n>.ndjson`)" },
5242
+ {
5243
+ flag: "--pr",
5244
+ description: "Flush the Issue named by this PR's `Closes #N` line, posting the comments on the PR"
5245
+ },
5246
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
5247
+ ],
5248
+ details: [
5249
+ "Exactly one of `--issue`/`--pr` is required. `--pr` resolves the Issue from the PR body's `Closes #N` line — the same anchor every gate reads — and refuses with a check error naming the missing line when the body carries none.",
5250
+ "Splits the outbox first at `run_id` boundaries (a maximal run of consecutive lines sharing one `run_id`, never a global group-by, so an interleaved outbox never produces a range spanning a gap), then at `FORGE_COMMENT_MAX_CHARS` (65536) within each run. Each comment opens with `<!-- aeg:log:<run_id>:<seq_from>-<seq_to> -->` on its own line, then one fenced `ndjson` block, one outbox line per line, verbatim. A single line too large to fit alone is refused by name, never split across two comments.",
5251
+ "Logs its own `forge_write` line — `validated` before posting, `written` (with every returned comment id) after the last post succeeds, or `refused` (with gh's error) on any failure — through the same `log()` every other family uses, into the same outbox, before truncating. The outbox is truncated only to the lines confirmed posted; a failed chunk's lines, and anything appended to the outbox during the flush (including the flush's own `validated`/`written`/`refused` line), always survive to ride the next flush."
5252
+ ],
5253
+ status: "shipped"
5254
+ },
4655
5255
  {
4656
5256
  name: "milestone create",
4657
5257
  description: "Create a GitHub Milestone from a validated body",
@@ -4744,6 +5344,10 @@ var COMMANDS = [
4744
5344
  flag: "--findings-file",
4745
5345
  description: "One finding per line: `SEVERITY|file:line|description` (`|`-delimited). A re-review names a prior id in the description as `F<n> <class> <state>:`. Omit for zero findings."
4746
5346
  },
5347
+ {
5348
+ flag: "--objectives-file",
5349
+ description: "One line per objective: `O<n>|MET|<evidence>` or `O<n>|NOT MET|<evidence>` (`|`-delimited; evidence is the rest of the line). Required whenever the closed Issue (or the PR body's own `## Objectives`) has an objectives list to judge; its ids must cover that list exactly, and a re-review must restate every prior objective. Never together with `--escalate`."
5350
+ },
4747
5351
  { flag: "--brief-conformance", description: "code-reviewer only: the BRIEF CONFORMANCE line" },
4748
5352
  { flag: "--spec-conformance", description: "code-reviewer only: the SPEC CONFORMANCE line" },
4749
5353
  { flag: "--scope", description: "code-reviewer only: the SCOPE line" },
@@ -4774,8 +5378,9 @@ var COMMANDS = [
4774
5378
  "`--print-only` runs the exact same render-then-self-check path as a real post, then returns before `gh pr comment` — closes atta-labs/vinaya#184, where the old command silently posted a verdict anyway after a reviewer guessed this flag existed.",
4775
5379
  "Every structural line (`VERDICT:`, `Judged head:`) is rendered from this command's own validated enum/sha inputs — never from a caller-supplied string — so a Reviewer's free-typed prose can no longer produce a shape the merge gate's line-anchored regex fails to see.",
4776
5380
  "The verdict is derived, not typed: a BLOCKER (or CRITICAL/HIGH) finding forces REQUEST_CHANGES/FAIL and its absence forces APPROVE/PASS, before posting anything — an explicit `--verdict` that disagrees is refused naming the derived value. When a same-role verdict comment already exists on the PR, a new findings file must carry every prior id with a state and no non-blocking finding outside the diff since that comment's judged head, or the post is refused.",
4777
- "Before the post ever reaches the forge, runs the exact `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` functions `checkReviewGate` calls over its own rendered text and refuses (exit 2) unless exactly the intended verdict extracts and the other role extracts none an escalation requires both to extract none. Both extractors read only a comment's first three lines, which are always this command's own structural lines, so no caller-supplied field can smuggle a line the gate would misread.",
4778
- "After posting, re-fetches the PR's comments and runs them through the same extractors `checkReviewGate` calls the same functions, not a second implementation and exits non-zero naming precisely what failed to re-parse if the post does not come back clean, bound to the resolved head, and free of the other role's verdict. There is no `--skip-verify` escape."
5381
+ "Renders an `OBJECTIVES:` block (one `O<n>: MET | NOT MET <evidence>` line per objective) after `SPEC CONFORMANCE:`/before `CONFIG SCAN:`, and an `Objectives version:` line at line 5 resolved from the closed Issue's `## Objectives` list, or the PR body's own section when it closes none. A clean verdict (`APPROVE`/`PASS`) is refused alongside any `NOT MET`; an Issue below the objectives cutover renders neither line at all, matching the merge gate's own skip.",
5382
+ "Before the post ever reaches the forge, runs the exact `extractCodeReviewVerdict`/`extractSecurityReviewVerdict` functions `checkReviewGate` calls over its own rendered text and refuses (exit 2) unless exactly the intended verdict extracts and the other role extracts none an escalation requires both to extract none. Both extractors read only a comment's first five lines; a code-review or security render's caller-supplied fields never open one of those lines, but an escalation's `--summary` can (pre-cutover, it becomes line 5 unprefixed) — this pre-post re-parse, not the render's construction, is what catches that case before it ever reaches the forge.",
5383
+ "After posting, re-fetches the PR's comments and runs them through the same extractors `checkReviewGate` calls — the same functions, not a second implementation — and exits non-zero naming precisely what failed to re-parse if the post does not come back clean, bound to the resolved head and objectives version, and free of the other role's verdict. There is no `--skip-verify` escape."
4779
5384
  ],
4780
5385
  status: "shipped"
4781
5386
  },
@@ -4944,6 +5549,54 @@ var COMMANDS = [
4944
5549
  'This is the one procedure named in `apps/cli/specs/self-hosting.md`, "How the published version is produced" — publishing itself stays manual and human-triggered; this command only removes the hand-typed four-step recipe.'
4945
5550
  ],
4946
5551
  status: "shipped"
5552
+ },
5553
+ {
5554
+ name: "dispatch",
5555
+ description: "Start a role's headless agent session (claude/codex/gemini) with attribution set on its environment, recording the outcome through the Vinaya Log",
5556
+ flags: [
5557
+ {
5558
+ flag: "--agent",
5559
+ description: "Vendor to start: `claude`, `codex`, or `gemini` (falls back to `dispatch.agent` in config)"
5560
+ },
5561
+ { flag: "--prompt-file", description: "Path to the prompt text sent to the agent (never on argv)" },
5562
+ { flag: "--task", description: "This dispatch's task Issue number — mutually exclusive with `--pr`" },
5563
+ {
5564
+ flag: "--pr",
5565
+ description: "Flush the outbox to this PR's Issue after the dispatch — mutually exclusive with `--task`"
5566
+ },
5567
+ { flag: "--round", description: "Round number, for a dispatch inside a review loop" },
5568
+ {
5569
+ flag: "--resume <id>",
5570
+ description: "Resume the vendor's own session/thread from a prior dispatch's returned `resumeId`, instead of starting a fresh one"
5571
+ },
5572
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
5573
+ ],
5574
+ details: [
5575
+ "Sets `VINAYA_RUN_ID`/`VINAYA_ROLE`/`VINAYA_TASK`/`VINAYA_ROUND` on the child only — never on this process's own environment — and refuses by name, before any spawn attempt, when the named vendor binary is absent from `PATH` or present but not executable.",
5576
+ "Records `dispatched` (with the prompt's sha256), `outcome_received` (duration, the vendor's own usage when its stdout prints a recognizable shape), or `dispatch_failed` (`timeout`, `crash`, or `refused`) through the Vinaya Log's one `dispatch` family writer, `dispatchRole`. A wall-time ceiling (`dispatch.timeoutMs` in config, default one hour) sends `SIGTERM` then, after a grace window, `SIGKILL`.",
5577
+ "When `--task` or `--pr` is given, flushes that outbox via `vinaya log flush` immediately after the child settles — `--task` and `--pr` are mutually exclusive here, matching `log flush`'s own single-target rule. Without either, the dispatch still runs and logs; nothing is flushed, and the lines ride to the next flush.",
5578
+ "A successful dispatch's `DispatchHandle` carries `resumeId` — the vendor's own session/thread identifier (claude/gemini: `session_id`; codex: `thread_id`), parsed from its stdout, `null` on any failure. Passing that value as `--resume <id>` on a later call swaps in that vendor's own resume invocation (`claude -p -r <id> ...`; `codex exec resume <id> ...`; `gemini ... --resume <id> ...`) in place of its first-dispatch args."
5579
+ ],
5580
+ status: "shipped"
5581
+ },
5582
+ {
5583
+ name: "dev-review-loop",
5584
+ description: "Dispatch the developer, run review rounds against the forge, resume the same developer session every round, and hold every verdict until the policy says publish",
5585
+ flags: [
5586
+ { flag: "--task <n>", description: "This task's Issue number — its frozen `aeg:brief:v1` comment is the brief" },
5587
+ {
5588
+ flag: "--agent <claude|codex|gemini>",
5589
+ description: "Vendor for every dispatch this loop makes (falls back to `dispatch.agent` in config)"
5590
+ },
5591
+ { flag: "--json", description: "Enveloped JSON output (schema: 1)" }
5592
+ ],
5593
+ details: [
5594
+ "Dispatches the developer through `dispatchRole` with the brief read from the Issue, waits for the PR it opens, then runs `assessRound` (`@attalabs/aeg-core`) — the entire policy — against observations this command reads from the forge: the head via `git ls-remote` only (never `gh pr view headRefOid`, which can lag a push), CI conclusion via the check-runs API (never run locally), and ruling comments matching `<!-- aeg:principal:ruling:<pr>-<k> -->`.",
5595
+ "Nothing is posted to the PR before the policy decides `publish`: each round's reviewer and security verdicts are dispatched fresh (never a resumed session) through `dispatchRole`, rendered through `review post`'s own render functions, and written to a local file under the outbox — never `gh pr comment`/`gh pr review`. Posting the held verdicts is a separate, later task.",
5596
+ "The developer's session is resumed every round via `dispatchRole`'s `resumeId` — never a fresh session — for every vendor; a round whose resume fails for a vendor that resumed successfully the round before stops the loop rather than silently falling back to a fresh developer session.",
5597
+ "Every dispatch and round transition is a log line through the Vinaya Log, flushed to the forge at each round boundary."
5598
+ ],
5599
+ status: "shipped"
4947
5600
  }
4948
5601
  ];
4949
5602
  // ../../packages/sources/src/forge-adapter.ts
@@ -4955,10 +5608,10 @@ function createForgeSource(config) {
4955
5608
  };
4956
5609
  }
4957
5610
  // ../../packages/sources/src/select-source.ts
4958
- import { z } from "zod";
4959
- var StateSourceConfigSchema = z.discriminatedUnion("kind", [
4960
- z.object({ kind: z.literal("forge"), owner: z.string(), repo: z.string() }),
4961
- z.object({ kind: z.literal("file"), root: z.string().optional() })
5611
+ import { z as z2 } from "zod";
5612
+ var StateSourceConfigSchema = z2.discriminatedUnion("kind", [
5613
+ z2.object({ kind: z2.literal("forge"), owner: z2.string(), repo: z2.string() }),
5614
+ z2.object({ kind: z2.literal("file"), root: z2.string().optional() })
4962
5615
  ]);
4963
5616
  // src/checks/contract.ts
4964
5617
  var CHECK_SCHEMA_VERSION = 1;
@@ -4972,27 +5625,27 @@ import { execFileSync as execFileSync2 } from "node:child_process";
4972
5625
  import { existsSync, mkdirSync, readFileSync as readFileSync2, realpathSync, writeFileSync } from "node:fs";
4973
5626
  import { homedir } from "node:os";
4974
5627
  import { dirname, join, resolve } from "node:path";
4975
- import { z as z2 } from "zod";
5628
+ import { z as z3 } from "zod";
4976
5629
 
4977
5630
  // src/lib/agent-vendors.ts
4978
5631
  var AGENT_VENDORS = ["skills", "claude", "gemini"];
4979
5632
 
4980
5633
  // src/lib/config.ts
4981
- var EnvEntrySchema = z2.union([
4982
- z2.literal(true),
4983
- z2.object({ optional: z2.literal(true) }),
4984
- z2.object({ anyOf: z2.array(z2.string()).min(2) }),
4985
- z2.string()
5634
+ var EnvEntrySchema = z3.union([
5635
+ z3.literal(true),
5636
+ z3.object({ optional: z3.literal(true) }),
5637
+ z3.object({ anyOf: z3.array(z3.string()).min(2) }),
5638
+ z3.string()
4986
5639
  ]);
4987
- var CheckEntrySchema = z2.object({
4988
- run: z2.string(),
4989
- scope: z2.enum(["diff", "full"]),
4990
- include: z2.array(z2.string()).optional(),
4991
- args: z2.array(z2.string()).optional(),
4992
- timeoutMs: z2.number().optional(),
4993
- env: z2.record(z2.string(), EnvEntrySchema).optional(),
4994
- requiresOpenPr: z2.boolean().optional(),
4995
- ownWorkflow: z2.boolean().optional()
5640
+ var CheckEntrySchema = z3.object({
5641
+ run: z3.string(),
5642
+ scope: z3.enum(["diff", "full"]),
5643
+ include: z3.array(z3.string()).optional(),
5644
+ args: z3.array(z3.string()).optional(),
5645
+ timeoutMs: z3.number().optional(),
5646
+ env: z3.record(z3.string(), EnvEntrySchema).optional(),
5647
+ requiresOpenPr: z3.boolean().optional(),
5648
+ ownWorkflow: z3.boolean().optional()
4996
5649
  }).superRefine((entry2, ctx) => {
4997
5650
  if (!entry2.env)
4998
5651
  return;
@@ -5002,22 +5655,22 @@ var CheckEntrySchema = z2.object({
5002
5655
  const members = value.anyOf;
5003
5656
  if (new Set(members).size !== members.length) {
5004
5657
  ctx.addIssue({
5005
- code: z2.ZodIssueCode.custom,
5658
+ code: z3.ZodIssueCode.custom,
5006
5659
  message: `env.${key}.anyOf has duplicate members`,
5007
5660
  path: ["env", key, "anyOf"]
5008
5661
  });
5009
5662
  }
5010
5663
  if (!members.includes(key)) {
5011
5664
  ctx.addIssue({
5012
- code: z2.ZodIssueCode.custom,
5665
+ code: z3.ZodIssueCode.custom,
5013
5666
  message: `env.${key}.anyOf must include "${key}" itself as a member`,
5014
5667
  path: ["env", key, "anyOf"]
5015
5668
  });
5016
5669
  }
5017
5670
  }
5018
5671
  });
5019
- var RoleEntrySchema = z2.object({
5020
- contract: z2.string().refine((p) => p.includes("/"), {
5672
+ var RoleEntrySchema = z3.object({
5673
+ contract: z3.string().refine((p) => p.includes("/"), {
5021
5674
  message: 'must be a path (contain at least one "/"), not a bare filename'
5022
5675
  })
5023
5676
  });
@@ -5036,19 +5689,21 @@ var BRIEF_BUILTINS = [
5036
5689
  "closesN",
5037
5690
  "premiseCoverage",
5038
5691
  "issueRationale",
5692
+ "objectives",
5693
+ "briefSections",
5039
5694
  "milestoneShape"
5040
5695
  ];
5041
- var BriefSectionSchema = z2.union([
5042
- z2.object({ builtin: z2.enum(BRIEF_BUILTINS) }),
5043
- z2.object({ heading: z2.string().min(1), name: z2.string().optional() }),
5044
- z2.object({ field: z2.string().min(1), name: z2.string().optional() }),
5045
- z2.object({ phrase: z2.string().min(1), name: z2.string().optional() })
5696
+ var BriefSectionSchema = z3.union([
5697
+ z3.object({ builtin: z3.enum(BRIEF_BUILTINS) }),
5698
+ z3.object({ heading: z3.string().min(1), name: z3.string().optional() }),
5699
+ z3.object({ field: z3.string().min(1), name: z3.string().optional() }),
5700
+ z3.object({ phrase: z3.string().min(1), name: z3.string().optional() })
5046
5701
  ]);
5047
- var BriefSchemaSchema = z2.object({
5048
- pr: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
5049
- issue: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
5050
- milestone: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
5051
- ack: z2.array(z2.enum(BRIEF_BUILTINS)).optional()
5702
+ var BriefSchemaSchema = z3.object({
5703
+ pr: z3.object({ sections: z3.array(BriefSectionSchema) }).optional(),
5704
+ issue: z3.object({ sections: z3.array(BriefSectionSchema) }).optional(),
5705
+ milestone: z3.object({ sections: z3.array(BriefSectionSchema) }).optional(),
5706
+ ack: z3.array(z3.enum(BRIEF_BUILTINS)).optional()
5052
5707
  });
5053
5708
  function isSafeRepoRelPath(p) {
5054
5709
  if (p.length === 0)
@@ -5057,7 +5712,7 @@ function isSafeRepoRelPath(p) {
5057
5712
  return false;
5058
5713
  return p.split(/[\\/]/).every((seg) => seg !== ".." && seg !== "");
5059
5714
  }
5060
- var SafeRepoRelPath = z2.string().refine(isSafeRepoRelPath, {
5715
+ var SafeRepoRelPath = z3.string().refine(isSafeRepoRelPath, {
5061
5716
  message: "must be a repo-root-relative path with no `..` segment or absolute root"
5062
5717
  });
5063
5718
  var CANONICAL_HOOK_BLOCK_PREFIXES = [".git/", ".husky/", ".vinaya/hooks/", ".claude/hooks/"];
@@ -5067,22 +5722,22 @@ function isCanonicalHookBlockPath(p) {
5067
5722
  var ManagedHookBlockPath = SafeRepoRelPath.refine(isCanonicalHookBlockPath, {
5068
5723
  message: `must start with one of ${CANONICAL_HOOK_BLOCK_PREFIXES.join(", ")} (byte-exact, case-sensitive)`
5069
5724
  });
5070
- var ManagedBlockRecordSchema = z2.object({
5725
+ var ManagedBlockRecordSchema = z3.object({
5071
5726
  path: ManagedHookBlockPath,
5072
- marker: z2.string(),
5073
- comment: z2.enum(["hash", "html"])
5727
+ marker: z3.string(),
5728
+ comment: z3.enum(["hash", "html"])
5074
5729
  });
5075
- var ManagedManifestSchema = z2.object({
5076
- version: z2.number().int().positive(),
5077
- files: z2.array(SafeRepoRelPath),
5078
- blocks: z2.array(ManagedBlockRecordSchema),
5079
- labels: z2.array(z2.string()),
5080
- agents: z2.array(z2.enum(AGENT_VENDORS)).optional()
5730
+ var ManagedManifestSchema = z3.object({
5731
+ version: z3.number().int().positive(),
5732
+ files: z3.array(SafeRepoRelPath),
5733
+ blocks: z3.array(ManagedBlockRecordSchema),
5734
+ labels: z3.array(z3.string()),
5735
+ agents: z3.array(z3.enum(AGENT_VENDORS)).optional()
5081
5736
  });
5082
- var ProjectEntrySchema = z2.object({
5083
- name: z2.string().min(1),
5084
- description: z2.string().min(1).optional(),
5085
- path: z2.string().min(1).optional()
5737
+ var ProjectEntrySchema = z3.object({
5738
+ name: z3.string().min(1),
5739
+ description: z3.string().min(1).optional(),
5740
+ path: z3.string().min(1).optional()
5086
5741
  });
5087
5742
  function parseTokensCollectDeclaration(value) {
5088
5743
  const m = value.trim().match(/^(\S+)\s+(\S+)$/);
@@ -5093,31 +5748,35 @@ function parseTokensCollectDeclaration(value) {
5093
5748
  return null;
5094
5749
  return { interpreter, script };
5095
5750
  }
5096
- var VinayaConfigSchema = z2.object({
5097
- rings: z2.object({
5098
- ring1_forgeWriteInterception: z2.boolean(),
5099
- ring2_asyncAudits: z2.boolean()
5751
+ var VinayaConfigSchema = z3.object({
5752
+ rings: z3.object({
5753
+ ring1_forgeWriteInterception: z3.boolean(),
5754
+ ring2_asyncAudits: z3.boolean()
5100
5755
  }).optional(),
5101
- checks: z2.record(z2.string(), CheckEntrySchema).optional(),
5102
- roles: z2.record(z2.string(), RoleEntrySchema).optional(),
5756
+ checks: z3.record(z3.string(), CheckEntrySchema).optional(),
5757
+ roles: z3.record(z3.string(), RoleEntrySchema).optional(),
5103
5758
  briefSchema: BriefSchemaSchema.optional(),
5104
5759
  managed: ManagedManifestSchema.optional(),
5105
- principals: z2.array(z2.string()).min(1).optional(),
5106
- releaseActor: z2.string().min(1).optional(),
5107
- ci: z2.object({ setup: z2.string().min(1) }).optional(),
5108
- tokens: z2.object({
5109
- collect: z2.string().min(1).refine((v) => parseTokensCollectDeclaration(v) !== null, {
5760
+ principals: z3.array(z3.string()).min(1).optional(),
5761
+ releaseActor: z3.string().min(1).optional(),
5762
+ ci: z3.object({ setup: z3.string().min(1) }).optional(),
5763
+ tokens: z3.object({
5764
+ collect: z3.string().min(1).refine((v) => parseTokensCollectDeclaration(v) !== null, {
5110
5765
  message: 'tokens.collect must be exactly "<interpreter> <repo-relative-script-path>" — two whitespace-separated tokens, no flags, no shell syntax, no quoting'
5111
5766
  })
5112
5767
  }).optional(),
5113
- blastRadius: z2.object({ extraDomains: z2.array(z2.string()).optional() }).optional(),
5114
- proseGates: z2.object({
5115
- doctrineRoot: z2.string().min(1).optional(),
5116
- readerFacingPrefix: z2.string().min(1).optional(),
5117
- readerFacingSuffix: z2.string().min(1).optional(),
5118
- legacySlugDir: z2.string().min(1).optional()
5768
+ blastRadius: z3.object({ extraDomains: z3.array(z3.string()).optional() }).optional(),
5769
+ proseGates: z3.object({
5770
+ doctrineRoot: z3.string().min(1).optional(),
5771
+ readerFacingPrefix: z3.string().min(1).optional(),
5772
+ readerFacingSuffix: z3.string().min(1).optional(),
5773
+ legacySlugDir: z3.string().min(1).optional()
5119
5774
  }).optional(),
5120
- projects: z2.array(ProjectEntrySchema).optional()
5775
+ projects: z3.array(ProjectEntrySchema).optional(),
5776
+ dispatch: z3.object({
5777
+ timeoutMs: z3.number().int().positive().optional(),
5778
+ agent: z3.enum(["claude", "codex", "gemini"]).optional()
5779
+ }).optional()
5121
5780
  });
5122
5781
  var GLOBAL_VINAYA_HOME = join(homedir(), ".vinaya");
5123
5782
  var GLOBAL_CONFIG_PATH = join(GLOBAL_VINAYA_HOME, "config.json");