@attalabs/vinaya 0.24.1 → 0.25.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 (117) hide show
  1. package/README.md +25 -2
  2. package/aeg-root/contracts/developer-reviewer.md +3 -0
  3. package/aeg-root/contracts/planner-brief.md +13 -0
  4. package/aeg-root/enforcement.md +10 -6
  5. package/aeg-root/process.md +4 -4
  6. package/aeg-root/roles/brief-author.md +4 -2
  7. package/aeg-root/roles/developer.md +20 -14
  8. package/aeg-root/roles/planner.md +14 -0
  9. package/aeg-root/roles/principal.md +3 -0
  10. package/aeg-root/roles/reviewer.md +23 -7
  11. package/aeg-root/roles/security.md +21 -6
  12. package/aeg-root/skills/brief-authoring/SKILL.md +2 -2
  13. package/aeg-root/state-machine.md +2 -2
  14. package/aeg-root/templates/brief-template.md +20 -4
  15. package/aeg-root/templates/issue-rationale-template.md +34 -1
  16. package/aeg-root/templates/pr-report-template.md +5 -13
  17. package/dist/checks/bin/check-body-bare-digits.js +572 -127
  18. package/dist/checks/bin/check-branch-topology.js +629 -73
  19. package/dist/checks/bin/check-brief-shape.js +1675 -70
  20. package/dist/checks/bin/check-changeset-coverage.js +572 -127
  21. package/dist/checks/bin/check-closes-n.js +629 -73
  22. package/dist/checks/bin/check-coherence.js +696 -134
  23. package/dist/checks/bin/check-dead-branch-push.js +505 -66
  24. package/dist/checks/bin/check-dispatch-readiness.js +696 -134
  25. package/dist/checks/bin/check-doc-coverage-push.js +572 -127
  26. package/dist/checks/bin/check-doc-coverage.js +572 -127
  27. package/dist/checks/bin/check-doctrine-no-procedures.js +572 -127
  28. package/dist/checks/bin/check-doctrine-portability.js +572 -127
  29. package/dist/checks/bin/check-evidence-fresh.js +572 -127
  30. package/dist/checks/bin/check-exec-bits.js +572 -127
  31. package/dist/checks/bin/check-first-push-dispatch.js +696 -134
  32. package/dist/checks/bin/check-issue-assignment.js +629 -73
  33. package/dist/checks/bin/check-main-branch-refusal.js +505 -66
  34. package/dist/checks/bin/check-no-disk-state.js +505 -66
  35. package/dist/checks/bin/check-pr-report-density.js +505 -66
  36. package/dist/checks/bin/check-quoted-command.js +572 -127
  37. package/dist/checks/bin/check-reader-resolvable-prose.js +610 -133
  38. package/dist/checks/bin/check-registry-gates.js +516 -68
  39. package/dist/checks/bin/check-retired-vocabulary.js +572 -127
  40. package/dist/checks/bin/check-review-gate.js +646 -130
  41. package/dist/checks/bin/check-single-plan-pr.js +505 -66
  42. package/dist/checks/bin/check-test-plan.js +505 -66
  43. package/dist/checks/bin/check-token-collection-wired.js +505 -66
  44. package/dist/checks/bin/check-token-report.js +505 -66
  45. package/dist/checks/bin/check-workspace-escape.js +572 -127
  46. package/dist/index.js +7696 -4271
  47. package/package.json +1 -1
  48. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-render.ts +2 -2
  49. package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +26 -1
  50. package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
  51. package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
  52. package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
  53. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
  54. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
  55. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  56. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  57. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  58. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  59. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  60. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
  61. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  62. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  63. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
  64. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
  65. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
  66. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
  67. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
  68. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
  69. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
  70. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
  71. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
  72. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
  73. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
  74. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
  75. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
  76. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
  77. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
  78. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
  79. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
  80. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
  81. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
  82. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
  83. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
  84. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
  85. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
  86. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
  87. package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
  88. 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
  89. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
  90. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
  91. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
  92. 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
  93. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
  94. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
  95. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
  96. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
  97. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
  98. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
  99. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0wxycau._.js +1 -1
  100. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
  101. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
  102. package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_topbar_index_tsx_1h1gs1y._.js +1 -1
  103. package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
  104. package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
  105. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
  106. package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
  107. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3m1kgax7j2vgs.js → 0bn8c8v5q429o.js} +1 -1
  108. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0doqwpd81sjzn.js → 0keji7wvbe1d1.js} +1 -1
  109. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1maoxbrl3pv5d.css +1 -0
  110. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1uogpj5w2n5ju.js → 3-6a3tinbdb-7.js} +1 -1
  111. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0zebsmmk1bvnb.js → 3_9xytdmiv1xi.js} +4 -4
  112. package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
  113. package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3qc67qhcvbg0o.css +0 -1
  114. /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
  115. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{QY2GhiI47-765D9DHGy1I → DMofV2-9FrHE7100GfrpI}/_buildManifest.js +0 -0
  116. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{QY2GhiI47-765D9DHGy1I → DMofV2-9FrHE7100GfrpI}/_clientMiddlewareManifest.js +0 -0
  117. /package/studio-standalone/apps/vinaya-studio/web/.next/static/{QY2GhiI47-765D9DHGy1I → DMofV2-9FrHE7100GfrpI}/_ssgManifest.js +0 -0
@@ -1938,13 +1938,18 @@ function parseInlineFieldList(section) {
1938
1938
  }
1939
1939
  // ../../packages/aeg-core/src/verdict-extraction.ts
1940
1940
  var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
1941
- function firstThreeLines(comment) {
1941
+ var OBJECTIVES_VERSION_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Objectives version:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1942
+ function firstFiveLines(comment) {
1942
1943
  return comment.split(`
1943
- `).slice(0, 3).join(`
1944
+ `).slice(0, 5).join(`
1944
1945
  `);
1945
1946
  }
1946
1947
  function extractHeadSha(comment) {
1947
- const m = firstThreeLines(comment).match(HEAD_SHA_PATTERN);
1948
+ const m = firstFiveLines(comment).match(HEAD_SHA_PATTERN);
1949
+ return m ? m[1].toLowerCase() : null;
1950
+ }
1951
+ function extractObjectivesVersion(comment) {
1952
+ const m = firstFiveLines(comment).match(OBJECTIVES_VERSION_PATTERN);
1948
1953
  return m ? m[1].toLowerCase() : null;
1949
1954
  }
1950
1955
  function extractVerdict(comments, valuePattern, missingLabel) {
@@ -1953,21 +1958,24 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1953
1958
  return {
1954
1959
  value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
1955
1960
  headSha: null,
1961
+ objectivesVersion: null,
1956
1962
  danglingNote: `no ${missingLabel} verdict comment found on this PR`
1957
1963
  };
1958
1964
  }
1959
1965
  const latest = candidates[candidates.length - 1];
1960
- const m = firstThreeLines(latest).match(valuePattern);
1966
+ const m = firstFiveLines(latest).match(valuePattern);
1961
1967
  if (!m) {
1962
1968
  return {
1963
- value: `the most recent ${missingLabel} comment's VERDICT line is not within its first three lines — DANGLING, see below`,
1969
+ value: `the most recent ${missingLabel} comment's VERDICT line is not within its first five lines — DANGLING, see below`,
1964
1970
  headSha: null,
1965
- danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-three-line read window`
1971
+ objectivesVersion: null,
1972
+ danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
1966
1973
  };
1967
1974
  }
1968
1975
  return {
1969
1976
  value: m[1].toUpperCase().replace(/[_-]/g, " "),
1970
1977
  headSha: extractHeadSha(latest),
1978
+ objectivesVersion: extractObjectivesVersion(latest),
1971
1979
  danglingNote: null
1972
1980
  };
1973
1981
  }
@@ -2324,8 +2332,105 @@ function overrideActive(opts) {
2324
2332
  return true;
2325
2333
  return false;
2326
2334
  }
2327
- // ../../packages/aeg-core/src/premise-check.ts
2335
+ // ../../packages/aeg-core/src/objectives.ts
2328
2336
  import { createHash as createHash2 } from "node:crypto";
2337
+ var HEADING_RE = /^##[ \t]*Objectives[ \t]*$/im;
2338
+ var NEXT_HEADING_RE = /^##[ \t]/m;
2339
+ var OBJECTIVE_LINE_RE = /^O(\d+)\.[ \t]*(.*)$/;
2340
+ function hasBacktickedPath(text) {
2341
+ let i = 0;
2342
+ while (i < text.length) {
2343
+ const start = text.indexOf("`", i);
2344
+ if (start === -1)
2345
+ return false;
2346
+ const end = text.indexOf("`", start + 1);
2347
+ if (end === -1)
2348
+ return false;
2349
+ if (text.slice(start + 1, end).includes("/"))
2350
+ return true;
2351
+ i = end + 1;
2352
+ }
2353
+ return false;
2354
+ }
2355
+ var MIN_WORDS_OUTSIDE_BACKTICKS = 3;
2356
+ function stripObjectiveBackticks(text) {
2357
+ return text.replace(/`[^`\n]*`/g, " ");
2358
+ }
2359
+ function wordCount(text) {
2360
+ return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
2361
+ }
2362
+ function objectivesSectionText(body) {
2363
+ const heading = HEADING_RE.exec(body);
2364
+ if (!heading)
2365
+ return null;
2366
+ const afterHeading = body.slice(heading.index + heading[0].length);
2367
+ const next = NEXT_HEADING_RE.exec(afterHeading);
2368
+ return next ? afterHeading.slice(0, next.index) : afterHeading;
2369
+ }
2370
+ function hasObjectivesHeading(body) {
2371
+ return HEADING_RE.test(body);
2372
+ }
2373
+ function objectivesOf(body) {
2374
+ const section = objectivesSectionText(body);
2375
+ if (section === null) {
2376
+ return { ok: false, errors: ["no `## Objectives` heading found in the body."] };
2377
+ }
2378
+ const lines = section.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
2379
+ if (lines.length === 0) {
2380
+ return { ok: false, errors: ["the `## Objectives` section has no `O<n>.` lines."] };
2381
+ }
2382
+ const objectives = [];
2383
+ const errors = [];
2384
+ for (const line of lines) {
2385
+ const m = OBJECTIVE_LINE_RE.exec(line);
2386
+ if (!m) {
2387
+ errors.push(`"${line}" is not a well-formed objective line — expected \`O<n>. <sentence>\`, e.g. \`O1. …\`.`);
2388
+ continue;
2389
+ }
2390
+ const n = m[1];
2391
+ const text = m[2].trim();
2392
+ if (text.length === 0) {
2393
+ errors.push(`O${n} has no sentence — every objective is one observable sentence.`);
2394
+ continue;
2395
+ }
2396
+ if (hasBacktickedPath(text) && wordCount(stripObjectiveBackticks(text)) < MIN_WORDS_OUTSIDE_BACKTICKS) {
2397
+ errors.push(`O${n} is little more than a file path — an objective states an observable outcome, never a bare path (the Brief Author maps it to files).`);
2398
+ continue;
2399
+ }
2400
+ objectives.push({ id: `O${n}`, text });
2401
+ }
2402
+ for (let i = 0;i < objectives.length; i++) {
2403
+ const expected = i + 1;
2404
+ const actual = Number.parseInt(objectives[i].id.slice(1), 10);
2405
+ if (actual !== expected) {
2406
+ errors.push(`numbering is not contiguous from O1 — expected O${expected}, found ${objectives[i].id}.`);
2407
+ break;
2408
+ }
2409
+ }
2410
+ if (errors.length > 0)
2411
+ return { ok: false, errors };
2412
+ return { ok: true, objectives };
2413
+ }
2414
+ function normalizeText(text) {
2415
+ return text.trim().replace(/\s+/g, " ");
2416
+ }
2417
+ function objectivesVersion(objectives) {
2418
+ const normalized = objectives.map((o) => `${o.id} ${normalizeText(o.text)}`).join(`
2419
+ `);
2420
+ return createHash2("sha256").update(normalized).digest("hex");
2421
+ }
2422
+ function isIssueNotFoundError(err) {
2423
+ const stderr = err?.stderr;
2424
+ const haystack = [
2425
+ err?.message ?? "",
2426
+ typeof stderr === "string" ? stderr : stderr?.toString() ?? ""
2427
+ ].join(`
2428
+ `);
2429
+ return /could not resolve to an (?:issue|pull request)|\b404\b|not found/i.test(haystack);
2430
+ }
2431
+
2432
+ // ../../packages/aeg-core/src/premise-check.ts
2433
+ import { createHash as createHash3 } from "node:crypto";
2329
2434
  var ASSERTION_KINDS = new Set(["contains", "absent", "sha256"]);
2330
2435
  function isPremiseHeader(line) {
2331
2436
  const stripped = line.replace(/[*#]/g, "").trim();
@@ -2396,7 +2501,7 @@ function checkPremises(assertions, fileReader) {
2396
2501
  } else if (a.kind === "absent" && content.includes(a.value)) {
2397
2502
  failures.push(`premise-check: ${a.path} now contains "${a.value}", but the brief pinned it absent — the premise has moved; re-dig before proceeding.`);
2398
2503
  } else if (a.kind === "sha256") {
2399
- const actual = createHash2("sha256").update(content).digest("hex");
2504
+ const actual = createHash3("sha256").update(content).digest("hex");
2400
2505
  if (actual !== a.value.toLowerCase()) {
2401
2506
  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.`);
2402
2507
  }
@@ -2757,8 +2862,55 @@ function checkDefeatCases(prBody) {
2757
2862
  ]
2758
2863
  };
2759
2864
  }
2865
+ var PART_CITATION_RE = /Part\s+\d+\s*\(([^)]*)\)/gi;
2866
+ var OBJECTIVE_REF_RE = /O(\d+)/g;
2867
+ function citedObjectiveIds(section6) {
2868
+ const ids = new Set;
2869
+ for (const m of section6.matchAll(PART_CITATION_RE)) {
2870
+ for (const r of m[1].matchAll(OBJECTIVE_REF_RE))
2871
+ ids.add(Number.parseInt(r[1], 10));
2872
+ }
2873
+ return ids;
2874
+ }
2875
+ function checkObjectivesCopy(prBody, issueObjectives) {
2876
+ const parsed = objectivesOf(prBody);
2877
+ if (!parsed.ok) {
2878
+ return { status: "fail", errors: parsed.errors.map((e) => `brief-validation objectives copy: ${e}`) };
2879
+ }
2880
+ if (objectivesVersion(parsed.objectives) !== objectivesVersion(issueObjectives)) {
2881
+ return {
2882
+ status: "fail",
2883
+ errors: [
2884
+ "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)."
2885
+ ]
2886
+ };
2887
+ }
2888
+ return { status: "pass", errors: [] };
2889
+ }
2890
+ function checkObjectivesCoverage(prBody) {
2891
+ const parsed = objectivesOf(prBody);
2892
+ if (!parsed.ok) {
2893
+ return { status: "fail", errors: parsed.errors.map((e) => `brief-validation objectives coverage: ${e}`) };
2894
+ }
2895
+ const declaredIds = new Set(parsed.objectives.map((o) => Number.parseInt(o.id.slice(1), 10)));
2896
+ const section6 = extractNumberedSection(prBody, 6) ?? "";
2897
+ const cited = citedObjectiveIds(section6);
2898
+ const maxDeclared = Math.max(...declaredIds, 0);
2899
+ const errors = [];
2900
+ for (const id of declaredIds) {
2901
+ if (!cited.has(id)) {
2902
+ 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.`);
2903
+ }
2904
+ }
2905
+ for (const id of cited) {
2906
+ if (!declaredIds.has(id)) {
2907
+ errors.push(`brief-validation objectives coverage: a Part in §6 cites O${id}, but the Objectives section ends at O${maxDeclared}.`);
2908
+ }
2909
+ }
2910
+ return errors.length === 0 ? { status: "pass", errors: [] } : { status: "fail", errors };
2911
+ }
2760
2912
  function checkBriefSections(prBody, readTier, options = {}) {
2761
- const { requireClosesN = true, consumersOf = () => [] } = options;
2913
+ const { requireClosesN = true, consumersOf = () => [], issueObjectives } = options;
2762
2914
  const results = [
2763
2915
  checkTierField(prBody, readTier),
2764
2916
  checkTestPlan(prBody),
@@ -2776,6 +2928,7 @@ function checkBriefSections(prBody, readTier, options = {}) {
2776
2928
  checkCommandsCarryOutput(prBody),
2777
2929
  checkConsumerTests(prBody, consumersOf),
2778
2930
  checkDefeatCases(prBody),
2931
+ ...issueObjectives !== undefined ? [checkObjectivesCopy(prBody, issueObjectives), checkObjectivesCoverage(prBody)] : [],
2779
2932
  ...requireClosesN ? [checkClosesN(prBody)] : []
2780
2933
  ];
2781
2934
  return { errors: results.flatMap((r) => r.errors) };
@@ -2974,14 +3127,14 @@ function checkSectionDensity(prBody, heading) {
2974
3127
  ]
2975
3128
  };
2976
3129
  }
2977
- function checkSummaryDensity(prBody) {
2978
- return checkSectionDensity(prBody, "Summary");
3130
+ function checkDecisionsDensity(prBody) {
3131
+ return checkSectionDensity(prBody, "Decisions");
2979
3132
  }
2980
3133
  function checkScopeDensity(prBody) {
2981
3134
  return checkSectionDensity(prBody, "Scope");
2982
3135
  }
2983
3136
  function checkPrReportDensity(prBody) {
2984
- const results = [checkSummaryDensity(prBody), checkScopeDensity(prBody)];
3137
+ const results = [checkDecisionsDensity(prBody), checkScopeDensity(prBody)];
2985
3138
  return { errors: results.flatMap((r) => r.errors) };
2986
3139
  }
2987
3140
  // ../../packages/aeg-core/src/issue-validation.ts
@@ -3013,6 +3166,15 @@ function checkIssueRationale(body) {
3013
3166
  }
3014
3167
  return { status: errors.length > 0 ? "fail" : "pass", errors };
3015
3168
  }
3169
+ var OBJECTIVES_SINCE_ISSUE = 404;
3170
+ function checkIssueObjectives(body, issueNumber) {
3171
+ if (issueNumber !== null && issueNumber < OBJECTIVES_SINCE_ISSUE)
3172
+ return { status: "pass", errors: [] };
3173
+ const result = objectivesOf(body);
3174
+ if (result.ok)
3175
+ return { status: "pass", errors: [] };
3176
+ return { status: "fail", errors: result.errors.map((e) => `issue-validation objectives: ${e}`) };
3177
+ }
3016
3178
  function isTaskIssueLabelSet(labels) {
3017
3179
  return hasLabel("tranche", labels);
3018
3180
  }
@@ -3274,7 +3436,8 @@ function checkR1(issuesBySlug, grandfatheredIssues, registeredNames = []) {
3274
3436
  continue;
3275
3437
  const errors = [
3276
3438
  ...checkIssueRationale(issue.body).errors,
3277
- ...checkProjectsRegistered(issue.body, issue.labels, registeredNames).errors
3439
+ ...checkProjectsRegistered(issue.body, issue.labels, registeredNames).errors,
3440
+ ...checkIssueObjectives(issue.body, issue.number).errors
3278
3441
  ];
3279
3442
  if (errors.length === 0)
3280
3443
  continue;
@@ -3389,6 +3552,27 @@ function checkClosesN2(branch, prBody, trancheFiles, taskIssueRefs) {
3389
3552
  }
3390
3553
  return { ok: true, expectedIssue };
3391
3554
  }
3555
+ // ../../packages/aeg-core/src/archive-task.ts
3556
+ function closesRefs(text) {
3557
+ return [...new Set([...text.matchAll(/Closes #(\d+)/gi)].map((m) => Number(m[1])))];
3558
+ }
3559
+ function extractIssue(body) {
3560
+ const stripped = stripCode(body);
3561
+ const bodyNums = closesRefs(stripped);
3562
+ const anchored = anchoredRegion(stripped, "CLOSES");
3563
+ if (anchored !== null) {
3564
+ const anchorNums = closesRefs(anchored);
3565
+ if (anchorNums.length === 0)
3566
+ return { issue: null, extraIssues: bodyNums, outsideHeader: false };
3567
+ const issue2 = anchorNums[0];
3568
+ return { issue: issue2, extraIssues: bodyNums.filter((n) => n !== issue2), outsideHeader: false };
3569
+ }
3570
+ const headerNums = closesRefs(headerRegion(stripped));
3571
+ if (bodyNums.length === 0)
3572
+ return { issue: null, extraIssues: [], outsideHeader: false };
3573
+ const issue = headerNums.length > 0 ? headerNums[0] : bodyNums[0];
3574
+ return { issue, extraIssues: bodyNums.filter((n) => n !== issue), outsideHeader: headerNums.length === 0 };
3575
+ }
3392
3576
  // ../../packages/aeg-core/src/review-gate.ts
3393
3577
  function isBoundToHead(extraction, headSha) {
3394
3578
  if (!extraction.headSha)
@@ -3407,6 +3591,11 @@ function isBoundByPatchIdentity(extraction, headSha, patchIdOf) {
3407
3591
  function isBoundToPatch(extraction, headSha, patchIdOf) {
3408
3592
  return isBoundToHead(extraction, headSha) || isBoundByPatchIdentity(extraction, headSha, patchIdOf);
3409
3593
  }
3594
+ function isBoundToObjectives(extraction, currentVersion) {
3595
+ if (currentVersion === null)
3596
+ return true;
3597
+ return extraction.objectivesVersion === currentVersion;
3598
+ }
3410
3599
  function checkReviewGate(input) {
3411
3600
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
3412
3601
  const waived = isWaiverLabelActorVerified({
@@ -3433,7 +3622,9 @@ function checkReviewGate(input) {
3433
3622
  const securityClean = security.value === "PASS";
3434
3623
  const codeReviewBound = isBoundToPatch(codeReview, input.headSha, input.patchIdOf);
3435
3624
  const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
3436
- if (codeReviewClean && codeReviewBound && securityClean && securityBound && mechanicalChecksClean) {
3625
+ const codeReviewObjectivesBound = isBoundToObjectives(codeReview, input.objectivesVersion);
3626
+ const securityObjectivesBound = isBoundToObjectives(security, input.objectivesVersion);
3627
+ if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && securityClean && securityBound && securityObjectivesBound && mechanicalChecksClean) {
3437
3628
  return {
3438
3629
  verdict: "pass",
3439
3630
  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.`,
@@ -3445,11 +3636,15 @@ function checkReviewGate(input) {
3445
3636
  problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
3446
3637
  } else if (!codeReviewBound) {
3447
3638
  problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3639
+ } else if (!codeReviewObjectivesBound) {
3640
+ problems.push(`the newest code-review verdict was cast against objectives version ${codeReview.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3448
3641
  }
3449
3642
  if (!securityClean) {
3450
3643
  problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
3451
3644
  } else if (!securityBound) {
3452
3645
  problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3646
+ } else if (!securityObjectivesBound) {
3647
+ problems.push(`the newest security-review verdict was cast against objectives version ${security.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3453
3648
  }
3454
3649
  if (!mechanicalChecksClean) {
3455
3650
  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(", ")}`);
@@ -3685,55 +3880,20 @@ function checkG6(rows, registeredCheckNames) {
3685
3880
  }
3686
3881
  return { check: "G6", status: findings.length > 0 ? "fail" : "pass", findings };
3687
3882
  }
3688
- // ../../packages/aeg-core/src/dead-branch-push-guard.ts
3689
- function checkDeadBranchPush(input) {
3690
- const { branch, prState, prNumber } = input;
3691
- if (prState === "MERGED") {
3692
- return {
3693
- verdict: "refuse",
3694
- 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.`
3695
- };
3696
- }
3697
- if (prState === "CLOSED") {
3698
- return {
3699
- verdict: "refuse",
3700
- 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.`
3701
- };
3702
- }
3703
- if (prState === "OPEN") {
3704
- return {
3705
- verdict: "allow",
3706
- reason: `Branch \`${branch}\` has an OPEN PR (#${prNumber}) — pushing more commits to it is normal.`
3707
- };
3708
- }
3709
- if (prState === "NONE") {
3710
- return {
3711
- verdict: "allow",
3712
- reason: `Branch \`${branch}\` has no PR yet — a brand-new task branch legitimately has none.`
3713
- };
3714
- }
3715
- return {
3716
- verdict: "allow",
3717
- reason: `Could not determine \`${branch}\`'s PR state (forge unreachable) — failing open rather than blocking the push on a transient issue.`
3718
- };
3883
+ // ../../packages/aeg-core/src/reader-resolvable-prose.ts
3884
+ var PRODUCT_SLUG_SCOPE = [
3885
+ "apps/cli/src",
3886
+ ".github/workflows",
3887
+ ".vinaya",
3888
+ "apps/cli/README.md",
3889
+ "packages/sources/README.md"
3890
+ ];
3891
+ function isUnderOrEqual(path, prefix) {
3892
+ return path === prefix || path.startsWith(`${prefix}/`);
3719
3893
  }
3720
- // ../../packages/aeg-core/src/baseline-capture.ts
3721
- function captureBaseline(counts, capturedAt) {
3722
- return counts.map((c) => ({ tool: c.tool, findingCount: c.findingCount, capturedAt }));
3894
+ function isProductScopeFile(path) {
3895
+ return PRODUCT_SLUG_SCOPE.some((prefix) => isUnderOrEqual(path, prefix));
3723
3896
  }
3724
- function compareToBaseline(current, baseline) {
3725
- const perTool = current.map((c) => {
3726
- const baselineEntry = baseline.find((b) => b.tool === c.tool);
3727
- const baselineCount = baselineEntry ? baselineEntry.findingCount : c.findingCount;
3728
- return { tool: c.tool, baseline: baselineCount, current: c.findingCount, delta: c.findingCount - baselineCount };
3729
- });
3730
- return {
3731
- withinBudget: perTool.every((t) => t.delta <= 0),
3732
- delta: perTool.reduce((sum, t) => sum + t.delta, 0),
3733
- perTool
3734
- };
3735
- }
3736
- // ../../packages/aeg-core/src/reader-resolvable-prose.ts
3737
3897
  var SHIPS_PREFIX = "aeg-root/";
3738
3898
  function shipsArchivePrefix(shipsPrefix) {
3739
3899
  return `${shipsPrefix}tranches/completed/`;
@@ -3753,6 +3913,8 @@ function classifyProseFile(path, readerFacingPrefix, readerFacingSuffix, shipsPr
3753
3913
  }
3754
3914
  if (isSpecFile2(path) || isClaudeMdFile(path))
3755
3915
  return "internal";
3916
+ if (isProductScopeFile(path))
3917
+ return "product";
3756
3918
  return null;
3757
3919
  }
3758
3920
  var SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
@@ -3765,6 +3927,9 @@ function stripNonProse(path, content) {
3765
3927
  }
3766
3928
  return content;
3767
3929
  }
3930
+ function stripNonProseForProduct(path, content) {
3931
+ return path.endsWith(".md") ? stripNonProse(path, content) : content;
3932
+ }
3768
3933
  function lineAt(content, index) {
3769
3934
  let line = 1;
3770
3935
  for (let i = 0;i < index; i++) {
@@ -3789,12 +3954,19 @@ function checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuff
3789
3954
  { pattern: TRANCHE_SLUG_VN_PATTERN, what: "an internal tranche slug" },
3790
3955
  ...legacyPattern ? [{ pattern: legacyPattern, what: "an internal tranche slug", group: 2 }] : []
3791
3956
  ];
3957
+ const productPatterns = [
3958
+ { pattern: TRANCHE_SLUG_VN_PATTERN, what: "an internal tranche slug in product code" }
3959
+ ];
3792
3960
  for (const file of files) {
3793
3961
  const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix, shipsPrefix);
3794
- if (!cls || !SWEPT_CLASSES.has(cls))
3962
+ if (!cls || cls === "internal")
3795
3963
  continue;
3796
- const scrubbed = stripNonProse(file.path, file.content);
3797
- for (const { pattern, what, group } of patterns) {
3964
+ if (cls !== "product" && !SWEPT_CLASSES.has(cls))
3965
+ continue;
3966
+ const blocking = cls === "product";
3967
+ const scrubbed = blocking ? stripNonProseForProduct(file.path, file.content) : stripNonProse(file.path, file.content);
3968
+ const patternsToRun = blocking ? productPatterns : patterns;
3969
+ for (const { pattern, what, group } of patternsToRun) {
3798
3970
  pattern.lastIndex = 0;
3799
3971
  let match = pattern.exec(scrubbed);
3800
3972
  while (match !== null) {
@@ -3802,7 +3974,8 @@ function checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuff
3802
3974
  findings.push({
3803
3975
  file: file.path,
3804
3976
  line: lineAt(scrubbed, match.index),
3805
- message: `references ${what} ("${cited}") a reader outside this repo's tracker cannot resolve`
3977
+ message: `references ${what} ("${cited}") a reader outside this repo's tracker cannot resolve`,
3978
+ blocking
3806
3979
  });
3807
3980
  match = pattern.exec(scrubbed);
3808
3981
  }
@@ -3852,7 +4025,8 @@ function checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, read
3852
4025
  findings.push({
3853
4026
  file: file.path,
3854
4027
  line,
3855
- message: `uses coined term "${term}" without defining it inline or linking the glossary`
4028
+ message: `uses coined term "${term}" without defining it inline or linking the glossary`,
4029
+ blocking: false
3856
4030
  });
3857
4031
  }
3858
4032
  }
@@ -3875,6 +4049,7 @@ function parseGlossaryTerms(glossaryContent) {
3875
4049
  }
3876
4050
  return terms;
3877
4051
  }
4052
+
3878
4053
  // ../../packages/aeg-core/src/quoted-command.ts
3879
4054
  var QUOTED_COMMAND_SHIPS_PREFIX = "aeg-root/";
3880
4055
  var QUOTED_COMMAND_SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
@@ -3969,6 +4144,54 @@ function evaluateCitedQuotes(citedQuotes, citedFileContents) {
3969
4144
  }
3970
4145
  return findings;
3971
4146
  }
4147
+ // ../../packages/aeg-core/src/dead-branch-push-guard.ts
4148
+ function checkDeadBranchPush(input) {
4149
+ const { branch, prState, prNumber } = input;
4150
+ if (prState === "MERGED") {
4151
+ return {
4152
+ verdict: "refuse",
4153
+ 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.`
4154
+ };
4155
+ }
4156
+ if (prState === "CLOSED") {
4157
+ return {
4158
+ verdict: "refuse",
4159
+ 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.`
4160
+ };
4161
+ }
4162
+ if (prState === "OPEN") {
4163
+ return {
4164
+ verdict: "allow",
4165
+ reason: `Branch \`${branch}\` has an OPEN PR (#${prNumber}) — pushing more commits to it is normal.`
4166
+ };
4167
+ }
4168
+ if (prState === "NONE") {
4169
+ return {
4170
+ verdict: "allow",
4171
+ reason: `Branch \`${branch}\` has no PR yet — a brand-new task branch legitimately has none.`
4172
+ };
4173
+ }
4174
+ return {
4175
+ verdict: "allow",
4176
+ reason: `Could not determine \`${branch}\`'s PR state (forge unreachable) — failing open rather than blocking the push on a transient issue.`
4177
+ };
4178
+ }
4179
+ // ../../packages/aeg-core/src/baseline-capture.ts
4180
+ function captureBaseline(counts, capturedAt) {
4181
+ return counts.map((c) => ({ tool: c.tool, findingCount: c.findingCount, capturedAt }));
4182
+ }
4183
+ function compareToBaseline(current, baseline) {
4184
+ const perTool = current.map((c) => {
4185
+ const baselineEntry = baseline.find((b) => b.tool === c.tool);
4186
+ const baselineCount = baselineEntry ? baselineEntry.findingCount : c.findingCount;
4187
+ return { tool: c.tool, baseline: baselineCount, current: c.findingCount, delta: c.findingCount - baselineCount };
4188
+ });
4189
+ return {
4190
+ withinBudget: perTool.every((t) => t.delta <= 0),
4191
+ delta: perTool.reduce((sum, t) => sum + t.delta, 0),
4192
+ perTool
4193
+ };
4194
+ }
3972
4195
  // ../../packages/aeg-core/src/retired-vocabulary.ts
3973
4196
  var RETIRED_PATTERNS = [
3974
4197
  String.raw`\bD-[0-9]{3}\b`,
@@ -4438,6 +4661,222 @@ function findWorkspaceEscapes(files, knownPaths, workspaceDirs = DEFAULT_WORKSPA
4438
4661
  }
4439
4662
  return findings;
4440
4663
  }
4664
+ // ../../packages/aeg-core/src/log/schema.ts
4665
+ import { z } from "zod";
4666
+ var ROLE_VALUES = [
4667
+ "planner",
4668
+ "brief-author",
4669
+ "developer",
4670
+ "code-reviewer",
4671
+ "security",
4672
+ "principal",
4673
+ "archivist",
4674
+ "architect"
4675
+ ];
4676
+ var RoleSchema = z.enum(ROLE_VALUES);
4677
+ var HOST_VALUES = ["hook", "ci", "cli", "loop"];
4678
+ var HostSchema = z.enum(HOST_VALUES);
4679
+ var RUN_ID_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
4680
+ var HeaderMetaSchema = z.object({
4681
+ schema: z.literal(1),
4682
+ ts: z.string(),
4683
+ run_id: z.string().regex(RUN_ID_PATTERN),
4684
+ seq: z.number().int().nonnegative(),
4685
+ repo: z.string().nullable(),
4686
+ vinaya: z.string(),
4687
+ doctrine: z.string(),
4688
+ host: HostSchema,
4689
+ machine: z.string()
4690
+ }).strict();
4691
+ var SubjectSchema = z.object({
4692
+ issue: z.number().int().nullable(),
4693
+ pr: z.number().int().optional(),
4694
+ sha: z.string().optional(),
4695
+ role: z.union([RoleSchema, z.literal("unattributed")]),
4696
+ round: z.number().int().optional(),
4697
+ objectives_version: z.string().optional()
4698
+ }).strict();
4699
+ var HeaderSchema = z.object({
4700
+ meta: HeaderMetaSchema,
4701
+ subject: SubjectSchema
4702
+ }).strict();
4703
+ var envelopeTail = {
4704
+ duration_ms: z.number().nonnegative().optional(),
4705
+ payload: z.object({}).strict()
4706
+ };
4707
+ var DispatchOutcomeSchema = z.discriminatedUnion("type", [
4708
+ z.object({ type: z.literal("pr_opened"), pr: z.number().int(), head: z.string() }).strict(),
4709
+ z.object({
4710
+ type: z.literal("round_pushed"),
4711
+ pr: z.number().int(),
4712
+ head: z.string(),
4713
+ comment_id: z.number().int()
4714
+ }).strict(),
4715
+ z.object({
4716
+ type: z.literal("verdict"),
4717
+ verdict: z.enum(["APPROVE", "REQUEST CHANGES", "PASS", "FAIL"]),
4718
+ head: z.string(),
4719
+ comment_id: z.number().int(),
4720
+ objectives: z.array(z.object({ id: z.string(), met: z.boolean() }).strict()),
4721
+ findings: z.array(z.object({ id: z.string(), severity: z.string(), state: z.string().optional() }).strict())
4722
+ }).strict(),
4723
+ z.object({
4724
+ type: z.literal("escalation"),
4725
+ class: z.enum(["authority", "strategy", "product"]),
4726
+ comment_id: z.number().int()
4727
+ }).strict(),
4728
+ z.object({ type: z.literal("brief"), comment_id: z.number().int(), hash: z.string() }).strict(),
4729
+ z.object({ type: z.literal("plan"), issues: z.array(z.number().int()) }).strict(),
4730
+ z.object({ type: z.literal("archive"), provenance_comment_id: z.number().int() }).strict()
4731
+ ]);
4732
+ var dispatchShared = {
4733
+ meta: HeaderMetaSchema,
4734
+ subject: SubjectSchema,
4735
+ kind: z.literal("dispatch"),
4736
+ ...envelopeTail,
4737
+ target_role: RoleSchema,
4738
+ model: z.string(),
4739
+ round: z.number().int().optional(),
4740
+ effect_id: z.string()
4741
+ };
4742
+ var DispatchEventSchema = z.discriminatedUnion("event", [
4743
+ z.object({ ...dispatchShared, event: z.literal("dispatched"), prompt_hash: z.string() }).strict(),
4744
+ z.object({
4745
+ ...dispatchShared,
4746
+ event: z.literal("outcome_received"),
4747
+ outcome: DispatchOutcomeSchema,
4748
+ usage: z.object({ input: z.number().nonnegative(), output: z.number().nonnegative() }).strict().nullable()
4749
+ }).strict(),
4750
+ z.object({
4751
+ ...dispatchShared,
4752
+ event: z.literal("dispatch_failed"),
4753
+ reason: z.enum(["timeout", "crash", "refused", "unattributed_write"])
4754
+ }).strict()
4755
+ ]);
4756
+ var loopShared = {
4757
+ meta: HeaderMetaSchema,
4758
+ subject: SubjectSchema,
4759
+ kind: z.literal("dev_review_loop"),
4760
+ ...envelopeTail,
4761
+ loop_id: z.string()
4762
+ };
4763
+ var DevReviewLoopEventSchema = z.discriminatedUnion("event", [
4764
+ z.object({
4765
+ ...loopShared,
4766
+ event: z.literal("loop_started"),
4767
+ task: z.number().int(),
4768
+ policy: z.object({
4769
+ max_rounds: z.number().int().nonnegative(),
4770
+ reviewers: z.array(RoleSchema),
4771
+ models: z.record(RoleSchema, z.string())
4772
+ }).strict()
4773
+ }).strict(),
4774
+ z.object({ ...loopShared, event: z.literal("round_started"), round: z.number().int(), base_head: z.string() }).strict(),
4775
+ z.object({
4776
+ ...loopShared,
4777
+ event: z.literal("gate_result_read"),
4778
+ round: z.number().int(),
4779
+ head: z.string(),
4780
+ green: z.boolean()
4781
+ }).strict(),
4782
+ z.object({
4783
+ ...loopShared,
4784
+ event: z.literal("verdicts_read"),
4785
+ round: z.number().int(),
4786
+ head: z.string(),
4787
+ all_approve: z.boolean(),
4788
+ blockers: z.number().int().nonnegative()
4789
+ }).strict(),
4790
+ z.object({
4791
+ ...loopShared,
4792
+ event: z.literal("findings_compared"),
4793
+ round: z.number().int(),
4794
+ open: z.array(z.string()),
4795
+ resolved: z.array(z.string()),
4796
+ new: z.array(z.string()),
4797
+ recurring: z.array(z.string())
4798
+ }).strict(),
4799
+ z.object({
4800
+ ...loopShared,
4801
+ event: z.literal("stop_condition_met"),
4802
+ round: z.number().int(),
4803
+ condition: z.enum([
4804
+ "green",
4805
+ "max_rounds",
4806
+ "no_progress",
4807
+ "escalated",
4808
+ "principal_stop",
4809
+ "confidence",
4810
+ "reappearance"
4811
+ ])
4812
+ }).strict(),
4813
+ z.object({
4814
+ ...loopShared,
4815
+ event: z.literal("paused"),
4816
+ round: z.number().int(),
4817
+ reason: z.enum(["escalation", "principal_item", "refreeze_needed"])
4818
+ }).strict(),
4819
+ z.object({
4820
+ ...loopShared,
4821
+ event: z.literal("resumed"),
4822
+ round: z.number().int(),
4823
+ by: z.literal("principal")
4824
+ }).strict(),
4825
+ z.object({
4826
+ ...loopShared,
4827
+ event: z.literal("round_ended"),
4828
+ round: z.number().int(),
4829
+ base_head: z.string(),
4830
+ head: z.string(),
4831
+ files_changed: z.number().int().nonnegative(),
4832
+ insertions: z.number().int().nonnegative(),
4833
+ deletions: z.number().int().nonnegative(),
4834
+ wall_ms: z.number().nonnegative(),
4835
+ outcome: z.enum(["green", "changes_requested", "escalated"])
4836
+ }).strict(),
4837
+ z.object({
4838
+ ...loopShared,
4839
+ event: z.literal("journal_finalized"),
4840
+ rounds: z.number().int().nonnegative(),
4841
+ total_wall_ms: z.number().nonnegative(),
4842
+ time_to_green_ms: z.number().nonnegative().nullable(),
4843
+ files_changed_total: z.number().int().nonnegative(),
4844
+ final_head: z.string(),
4845
+ result: z.enum(["merged_ready", "stopped"])
4846
+ }).strict()
4847
+ ]);
4848
+ var ForgeOpSchema = z.enum([
4849
+ "pr.create",
4850
+ "pr.comment",
4851
+ "pr.body.replace",
4852
+ "pr.refreeze",
4853
+ "issue.create",
4854
+ "issue.edit",
4855
+ "issue.comment",
4856
+ "milestone.create",
4857
+ "milestone.edit",
4858
+ "milestone.close",
4859
+ "label.add",
4860
+ "label.remove"
4861
+ ]);
4862
+ var ForgeWriteTargetSchema = z.object({
4863
+ issue: z.number().int().optional(),
4864
+ pr: z.number().int().optional()
4865
+ }).strict();
4866
+ var forgeWriteShared = {
4867
+ meta: HeaderMetaSchema,
4868
+ subject: SubjectSchema,
4869
+ kind: z.literal("forge_write"),
4870
+ ...envelopeTail,
4871
+ op: ForgeOpSchema,
4872
+ target: ForgeWriteTargetSchema
4873
+ };
4874
+ var ForgeWriteEventSchema = z.discriminatedUnion("event", [
4875
+ z.object({ ...forgeWriteShared, event: z.literal("validated") }).strict(),
4876
+ z.object({ ...forgeWriteShared, event: z.literal("refused"), reason: z.string() }).strict(),
4877
+ z.object({ ...forgeWriteShared, event: z.literal("written"), comment_ids: z.array(z.string()) }).strict()
4878
+ ]);
4879
+ var LogEventSchema = z.union([DispatchEventSchema, DevReviewLoopEventSchema, ForgeWriteEventSchema]);
4441
4880
  // src/checks/contract.ts
4442
4881
  var CHECK_SCHEMA_VERSION = 1;
4443
4882
  function emitCheckError(error) {
@@ -4450,27 +4889,27 @@ import { execFileSync as execFileSync2 } from "node:child_process";
4450
4889
  import { existsSync, mkdirSync, readFileSync as readFileSync2, realpathSync, writeFileSync } from "node:fs";
4451
4890
  import { homedir } from "node:os";
4452
4891
  import { dirname, join, resolve } from "node:path";
4453
- import { z } from "zod";
4892
+ import { z as z2 } from "zod";
4454
4893
 
4455
4894
  // src/lib/agent-vendors.ts
4456
4895
  var AGENT_VENDORS = ["skills", "claude", "gemini"];
4457
4896
 
4458
4897
  // src/lib/config.ts
4459
- var EnvEntrySchema = z.union([
4460
- z.literal(true),
4461
- z.object({ optional: z.literal(true) }),
4462
- z.object({ anyOf: z.array(z.string()).min(2) }),
4463
- z.string()
4898
+ var EnvEntrySchema = z2.union([
4899
+ z2.literal(true),
4900
+ z2.object({ optional: z2.literal(true) }),
4901
+ z2.object({ anyOf: z2.array(z2.string()).min(2) }),
4902
+ z2.string()
4464
4903
  ]);
4465
- var CheckEntrySchema = z.object({
4466
- run: z.string(),
4467
- scope: z.enum(["diff", "full"]),
4468
- include: z.array(z.string()).optional(),
4469
- args: z.array(z.string()).optional(),
4470
- timeoutMs: z.number().optional(),
4471
- env: z.record(z.string(), EnvEntrySchema).optional(),
4472
- requiresOpenPr: z.boolean().optional(),
4473
- ownWorkflow: z.boolean().optional()
4904
+ var CheckEntrySchema = z2.object({
4905
+ run: z2.string(),
4906
+ scope: z2.enum(["diff", "full"]),
4907
+ include: z2.array(z2.string()).optional(),
4908
+ args: z2.array(z2.string()).optional(),
4909
+ timeoutMs: z2.number().optional(),
4910
+ env: z2.record(z2.string(), EnvEntrySchema).optional(),
4911
+ requiresOpenPr: z2.boolean().optional(),
4912
+ ownWorkflow: z2.boolean().optional()
4474
4913
  }).superRefine((entry2, ctx) => {
4475
4914
  if (!entry2.env)
4476
4915
  return;
@@ -4480,22 +4919,22 @@ var CheckEntrySchema = z.object({
4480
4919
  const members = value.anyOf;
4481
4920
  if (new Set(members).size !== members.length) {
4482
4921
  ctx.addIssue({
4483
- code: z.ZodIssueCode.custom,
4922
+ code: z2.ZodIssueCode.custom,
4484
4923
  message: `env.${key}.anyOf has duplicate members`,
4485
4924
  path: ["env", key, "anyOf"]
4486
4925
  });
4487
4926
  }
4488
4927
  if (!members.includes(key)) {
4489
4928
  ctx.addIssue({
4490
- code: z.ZodIssueCode.custom,
4929
+ code: z2.ZodIssueCode.custom,
4491
4930
  message: `env.${key}.anyOf must include "${key}" itself as a member`,
4492
4931
  path: ["env", key, "anyOf"]
4493
4932
  });
4494
4933
  }
4495
4934
  }
4496
4935
  });
4497
- var RoleEntrySchema = z.object({
4498
- contract: z.string().refine((p) => p.includes("/"), {
4936
+ var RoleEntrySchema = z2.object({
4937
+ contract: z2.string().refine((p) => p.includes("/"), {
4499
4938
  message: 'must be a path (contain at least one "/"), not a bare filename'
4500
4939
  })
4501
4940
  });
@@ -4514,19 +4953,21 @@ var BRIEF_BUILTINS = [
4514
4953
  "closesN",
4515
4954
  "premiseCoverage",
4516
4955
  "issueRationale",
4956
+ "objectives",
4957
+ "briefSections",
4517
4958
  "milestoneShape"
4518
4959
  ];
4519
- var BriefSectionSchema = z.union([
4520
- z.object({ builtin: z.enum(BRIEF_BUILTINS) }),
4521
- z.object({ heading: z.string().min(1), name: z.string().optional() }),
4522
- z.object({ field: z.string().min(1), name: z.string().optional() }),
4523
- z.object({ phrase: z.string().min(1), name: z.string().optional() })
4960
+ var BriefSectionSchema = z2.union([
4961
+ z2.object({ builtin: z2.enum(BRIEF_BUILTINS) }),
4962
+ z2.object({ heading: z2.string().min(1), name: z2.string().optional() }),
4963
+ z2.object({ field: z2.string().min(1), name: z2.string().optional() }),
4964
+ z2.object({ phrase: z2.string().min(1), name: z2.string().optional() })
4524
4965
  ]);
4525
- var BriefSchemaSchema = z.object({
4526
- pr: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
4527
- issue: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
4528
- milestone: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
4529
- ack: z.array(z.enum(BRIEF_BUILTINS)).optional()
4966
+ var BriefSchemaSchema = z2.object({
4967
+ pr: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
4968
+ issue: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
4969
+ milestone: z2.object({ sections: z2.array(BriefSectionSchema) }).optional(),
4970
+ ack: z2.array(z2.enum(BRIEF_BUILTINS)).optional()
4530
4971
  });
4531
4972
  function isSafeRepoRelPath(p) {
4532
4973
  if (p.length === 0)
@@ -4535,7 +4976,7 @@ function isSafeRepoRelPath(p) {
4535
4976
  return false;
4536
4977
  return p.split(/[\\/]/).every((seg) => seg !== ".." && seg !== "");
4537
4978
  }
4538
- var SafeRepoRelPath = z.string().refine(isSafeRepoRelPath, {
4979
+ var SafeRepoRelPath = z2.string().refine(isSafeRepoRelPath, {
4539
4980
  message: "must be a repo-root-relative path with no `..` segment or absolute root"
4540
4981
  });
4541
4982
  var CANONICAL_HOOK_BLOCK_PREFIXES = [".git/", ".husky/", ".vinaya/hooks/", ".claude/hooks/"];
@@ -4545,22 +4986,22 @@ function isCanonicalHookBlockPath(p) {
4545
4986
  var ManagedHookBlockPath = SafeRepoRelPath.refine(isCanonicalHookBlockPath, {
4546
4987
  message: `must start with one of ${CANONICAL_HOOK_BLOCK_PREFIXES.join(", ")} (byte-exact, case-sensitive)`
4547
4988
  });
4548
- var ManagedBlockRecordSchema = z.object({
4989
+ var ManagedBlockRecordSchema = z2.object({
4549
4990
  path: ManagedHookBlockPath,
4550
- marker: z.string(),
4551
- comment: z.enum(["hash", "html"])
4991
+ marker: z2.string(),
4992
+ comment: z2.enum(["hash", "html"])
4552
4993
  });
4553
- var ManagedManifestSchema = z.object({
4554
- version: z.number().int().positive(),
4555
- files: z.array(SafeRepoRelPath),
4556
- blocks: z.array(ManagedBlockRecordSchema),
4557
- labels: z.array(z.string()),
4558
- agents: z.array(z.enum(AGENT_VENDORS)).optional()
4994
+ var ManagedManifestSchema = z2.object({
4995
+ version: z2.number().int().positive(),
4996
+ files: z2.array(SafeRepoRelPath),
4997
+ blocks: z2.array(ManagedBlockRecordSchema),
4998
+ labels: z2.array(z2.string()),
4999
+ agents: z2.array(z2.enum(AGENT_VENDORS)).optional()
4559
5000
  });
4560
- var ProjectEntrySchema = z.object({
4561
- name: z.string().min(1),
4562
- description: z.string().min(1).optional(),
4563
- path: z.string().min(1).optional()
5001
+ var ProjectEntrySchema = z2.object({
5002
+ name: z2.string().min(1),
5003
+ description: z2.string().min(1).optional(),
5004
+ path: z2.string().min(1).optional()
4564
5005
  });
4565
5006
  function parseTokensCollectDeclaration(value) {
4566
5007
  const m = value.trim().match(/^(\S+)\s+(\S+)$/);
@@ -4571,31 +5012,35 @@ function parseTokensCollectDeclaration(value) {
4571
5012
  return null;
4572
5013
  return { interpreter, script };
4573
5014
  }
4574
- var VinayaConfigSchema = z.object({
4575
- rings: z.object({
4576
- ring1_forgeWriteInterception: z.boolean(),
4577
- ring2_asyncAudits: z.boolean()
5015
+ var VinayaConfigSchema = z2.object({
5016
+ rings: z2.object({
5017
+ ring1_forgeWriteInterception: z2.boolean(),
5018
+ ring2_asyncAudits: z2.boolean()
4578
5019
  }).optional(),
4579
- checks: z.record(z.string(), CheckEntrySchema).optional(),
4580
- roles: z.record(z.string(), RoleEntrySchema).optional(),
5020
+ checks: z2.record(z2.string(), CheckEntrySchema).optional(),
5021
+ roles: z2.record(z2.string(), RoleEntrySchema).optional(),
4581
5022
  briefSchema: BriefSchemaSchema.optional(),
4582
5023
  managed: ManagedManifestSchema.optional(),
4583
- principals: z.array(z.string()).min(1).optional(),
4584
- releaseActor: z.string().min(1).optional(),
4585
- ci: z.object({ setup: z.string().min(1) }).optional(),
4586
- tokens: z.object({
4587
- collect: z.string().min(1).refine((v) => parseTokensCollectDeclaration(v) !== null, {
5024
+ principals: z2.array(z2.string()).min(1).optional(),
5025
+ releaseActor: z2.string().min(1).optional(),
5026
+ ci: z2.object({ setup: z2.string().min(1) }).optional(),
5027
+ tokens: z2.object({
5028
+ collect: z2.string().min(1).refine((v) => parseTokensCollectDeclaration(v) !== null, {
4588
5029
  message: 'tokens.collect must be exactly "<interpreter> <repo-relative-script-path>" — two whitespace-separated tokens, no flags, no shell syntax, no quoting'
4589
5030
  })
4590
5031
  }).optional(),
4591
- blastRadius: z.object({ extraDomains: z.array(z.string()).optional() }).optional(),
4592
- proseGates: z.object({
4593
- doctrineRoot: z.string().min(1).optional(),
4594
- readerFacingPrefix: z.string().min(1).optional(),
4595
- readerFacingSuffix: z.string().min(1).optional(),
4596
- legacySlugDir: z.string().min(1).optional()
5032
+ blastRadius: z2.object({ extraDomains: z2.array(z2.string()).optional() }).optional(),
5033
+ proseGates: z2.object({
5034
+ doctrineRoot: z2.string().min(1).optional(),
5035
+ readerFacingPrefix: z2.string().min(1).optional(),
5036
+ readerFacingSuffix: z2.string().min(1).optional(),
5037
+ legacySlugDir: z2.string().min(1).optional()
4597
5038
  }).optional(),
4598
- projects: z.array(ProjectEntrySchema).optional()
5039
+ projects: z2.array(ProjectEntrySchema).optional(),
5040
+ dispatch: z2.object({
5041
+ timeoutMs: z2.number().int().positive().optional(),
5042
+ agent: z2.enum(["claude", "codex", "gemini"]).optional()
5043
+ }).optional()
4599
5044
  });
4600
5045
  var GLOBAL_VINAYA_HOME = join(homedir(), ".vinaya");
4601
5046
  var GLOBAL_CONFIG_PATH = join(GLOBAL_VINAYA_HOME, "config.json");
@@ -4753,7 +5198,7 @@ var CHECK_NAME = "review-gate";
4753
5198
  var OWN_CHECK_RUN_NAME = "vinaya review gate";
4754
5199
  function fetchPr(prNumber) {
4755
5200
  try {
4756
- const out = execFileSync3("gh", ["pr", "view", String(prNumber), "--json", "number,comments,labels,headRefName,headRefOid,baseRefName"], {
5201
+ const out = execFileSync3("gh", ["pr", "view", String(prNumber), "--json", "number,comments,labels,headRefName,headRefOid,baseRefName,body"], {
4757
5202
  encoding: "utf8",
4758
5203
  stdio: ["ignore", "pipe", "pipe"]
4759
5204
  });
@@ -4762,6 +5207,69 @@ function fetchPr(prNumber) {
4762
5207
  return null;
4763
5208
  }
4764
5209
  }
5210
+ function fetchIssueBodyForObjectives(issueNumber) {
5211
+ return execFileSync3("gh", ["issue", "view", String(issueNumber), "--json", "body", "--jq", ".body"], {
5212
+ encoding: "utf8",
5213
+ stdio: ["ignore", "pipe", "pipe"]
5214
+ });
5215
+ }
5216
+ function resolveObjectivesVersion(pr) {
5217
+ const { issue } = extractIssue(pr.body);
5218
+ if (issue !== null && issue < OBJECTIVES_SINCE_ISSUE)
5219
+ return null;
5220
+ if (issue !== null) {
5221
+ let issueBody;
5222
+ try {
5223
+ issueBody = fetchIssueBodyForObjectives(issue);
5224
+ } catch (err) {
5225
+ if (isIssueNotFoundError(err)) {
5226
+ emitCheckError({
5227
+ schema: CHECK_SCHEMA_VERSION,
5228
+ check: CHECK_NAME,
5229
+ severity: "error",
5230
+ message: `review-gate severity:infra — Issue #${issue} does not resolve via \`gh issue view\` — cannot verify the objectives-version binding for a PR whose linked Issue is at/above the objectives cutover.`,
5231
+ agent_recovery_prompt: `Restore Issue #${issue}, fix \`Closes #N\` to name a real Issue, or have a principal apply the \`vinaya/waiver:review\` label, then re-run \`vinaya check review-gate\`.`
5232
+ });
5233
+ process.exit(1);
5234
+ }
5235
+ emitCheckError({
5236
+ schema: CHECK_SCHEMA_VERSION,
5237
+ check: CHECK_NAME,
5238
+ severity: "error",
5239
+ message: `review-gate severity:infra — could not fetch Issue #${issue}'s body via \`gh issue view\` to resolve its objectives version: ${err.message}`,
5240
+ agent_recovery_prompt: "Confirm `gh auth status` passes and the Issue number is correct, then re-run `vinaya check review-gate`."
5241
+ });
5242
+ process.exit(1);
5243
+ }
5244
+ const parsed = objectivesOf(issueBody);
5245
+ if (!parsed.ok) {
5246
+ emitCheckError({
5247
+ schema: CHECK_SCHEMA_VERSION,
5248
+ check: CHECK_NAME,
5249
+ severity: "error",
5250
+ message: `review-gate severity:infra — Issue #${issue}'s \`## Objectives\` section does not parse (${parsed.errors.join("; ")}) — cannot verify the objectives-version binding.`,
5251
+ agent_recovery_prompt: `Fix Issue #${issue}'s \`## Objectives\` section, or have a principal apply the \`vinaya/waiver:review\` label, then re-run \`vinaya check review-gate\`.`
5252
+ });
5253
+ process.exit(1);
5254
+ }
5255
+ return objectivesVersion(parsed.objectives);
5256
+ }
5257
+ if (hasObjectivesHeading(pr.body)) {
5258
+ const own = objectivesOf(pr.body);
5259
+ if (!own.ok) {
5260
+ emitCheckError({
5261
+ schema: CHECK_SCHEMA_VERSION,
5262
+ check: CHECK_NAME,
5263
+ severity: "error",
5264
+ message: `review-gate severity:infra — this PR body's own \`## Objectives\` section does not parse (${own.errors.join("; ")}) — cannot verify the objectives-version binding.`,
5265
+ agent_recovery_prompt: "Fix the PR body's `## Objectives` section, or have a principal apply the `vinaya/waiver:review` label, then re-run `vinaya check review-gate`."
5266
+ });
5267
+ process.exit(1);
5268
+ }
5269
+ return objectivesVersion(own.objectives);
5270
+ }
5271
+ return null;
5272
+ }
4765
5273
  function shaFromLsRemote(branch) {
4766
5274
  try {
4767
5275
  const out = execFileSync3("git", ["ls-remote", "origin", `refs/heads/${branch}`], {
@@ -4908,14 +5416,22 @@ function main() {
4908
5416
  });
4909
5417
  process.exit(1);
4910
5418
  }
5419
+ const principalAllowlist = resolvePrincipalAllowlist(loadTrustAnchorConfig());
5420
+ const waived = isWaiverLabelActorVerified({
5421
+ label: WAIVER_LABEL_REVIEW,
5422
+ labels,
5423
+ labelActor: waiverLabelActor,
5424
+ principalAllowlist
5425
+ });
4911
5426
  const result = checkReviewGate({
4912
5427
  comments: pr.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null })),
4913
5428
  labels,
4914
5429
  waiverLabelActor,
4915
- principalAllowlist: resolvePrincipalAllowlist(loadTrustAnchorConfig()),
5430
+ principalAllowlist,
4916
5431
  mechanicalChecks,
4917
5432
  headSha,
4918
- patchIdOf: (sha) => patchIdAt(pr.baseRefName, sha)
5433
+ patchIdOf: (sha) => patchIdAt(pr.baseRefName, sha),
5434
+ objectivesVersion: waived ? null : resolveObjectivesVersion(pr)
4919
5435
  });
4920
5436
  if (result.verdict === "fail") {
4921
5437
  emitCheckError({