@attalabs/vinaya 0.26.0 → 0.27.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 (41) hide show
  1. package/README.md +4 -2
  2. package/aeg-root/enforcement.md +2 -2
  3. package/aeg-root/milestone-model.md +2 -0
  4. package/aeg-root/process.md +1 -1
  5. package/aeg-root/roles/planner.md +2 -0
  6. package/aeg-root/roles/principal.md +4 -2
  7. package/aeg-root/roles/reviewer.md +6 -2
  8. package/aeg-root/roles/security.md +6 -2
  9. package/dist/checks/bin/check-body-bare-digits.js +128 -22
  10. package/dist/checks/bin/check-branch-topology.js +148 -24
  11. package/dist/checks/bin/check-brief-shape.js +430 -32
  12. package/dist/checks/bin/check-changeset-coverage.js +148 -24
  13. package/dist/checks/bin/check-closes-n.js +148 -24
  14. package/dist/checks/bin/check-coherence.js +148 -24
  15. package/dist/checks/bin/check-dead-branch-push.js +110 -22
  16. package/dist/checks/bin/check-dispatch-readiness.js +148 -24
  17. package/dist/checks/bin/check-doc-coverage-push.js +148 -24
  18. package/dist/checks/bin/check-doc-coverage.js +148 -24
  19. package/dist/checks/bin/check-doctrine-no-procedures.js +110 -22
  20. package/dist/checks/bin/check-doctrine-portability.js +148 -24
  21. package/dist/checks/bin/check-evidence-fresh.js +650 -63
  22. package/dist/checks/bin/check-exec-bits.js +148 -24
  23. package/dist/checks/bin/check-first-push-dispatch.js +148 -24
  24. package/dist/checks/bin/check-issue-assignment.js +148 -24
  25. package/dist/checks/bin/check-main-branch-refusal.js +110 -22
  26. package/dist/checks/bin/check-no-disk-state.js +110 -22
  27. package/dist/checks/bin/check-pr-premise-reassert.js +110 -22
  28. package/dist/checks/bin/check-pr-report-density.js +110 -22
  29. package/dist/checks/bin/check-quoted-command.js +148 -24
  30. package/dist/checks/bin/check-reader-resolvable-prose.js +148 -24
  31. package/dist/checks/bin/check-registry-gates.js +110 -22
  32. package/dist/checks/bin/check-retired-vocabulary.js +148 -24
  33. package/dist/checks/bin/check-review-gate.js +169 -77
  34. package/dist/checks/bin/check-single-plan-pr.js +110 -22
  35. package/dist/checks/bin/check-surface-scope.js +148 -24
  36. package/dist/checks/bin/check-test-plan.js +110 -22
  37. package/dist/checks/bin/check-token-collection-wired.js +110 -22
  38. package/dist/checks/bin/check-token-report.js +110 -22
  39. package/dist/checks/bin/check-workspace-escape.js +148 -24
  40. package/dist/index.js +1584 -572
  41. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import { spawnSync as spawnSync3 } from "node:child_process";
4
5
  import { readFileSync as readFileSync35 } from "node:fs";
5
- import { dirname as dirname12, join as join34 } from "node:path";
6
+ import { dirname as dirname13, join as join34 } from "node:path";
6
7
  import { fileURLToPath as fileURLToPath2 } from "node:url";
7
8
 
8
9
  // src/commands/archive.ts
@@ -281,7 +282,7 @@ function issueListByLabelArgs(owner, repo, label) {
281
282
  "--state",
282
283
  "all",
283
284
  "--json",
284
- "number,title,body,state,labels,milestone",
285
+ "number,title,body,state,labels,milestone,stateReason",
285
286
  "--limit",
286
287
  "200"
287
288
  ];
@@ -615,14 +616,19 @@ async function fetchTrancheIssuesAsync(owner, repo, slug) {
615
616
  var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
616
617
  var NEXT_HEADING = /^#{1,6}\s+\S/m;
617
618
  var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
618
- function intentGoalForSlug(description, slug) {
619
- const text = stripCode(description, { inlineSpans: "keep" });
619
+ function intentsSection(text) {
620
620
  const start = text.match(INTENTS_HEADING);
621
621
  if (!start || start.index === undefined)
622
- return "";
622
+ return null;
623
623
  const rest = text.slice(start.index + start[0].length);
624
624
  const next = rest.match(NEXT_HEADING);
625
- const section = rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
625
+ return rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
626
+ }
627
+ function intentGoalForSlug(description, slug) {
628
+ const text = stripCode(description, { inlineSpans: "keep" });
629
+ const section = intentsSection(text);
630
+ if (section === null)
631
+ return "";
626
632
  for (const line of section.split(`
627
633
  `)) {
628
634
  const trimmed = line.trim();
@@ -634,6 +640,23 @@ function intentGoalForSlug(description, slug) {
634
640
  }
635
641
  return "";
636
642
  }
643
+ function intentLines(description) {
644
+ const text = stripCode(description, { inlineSpans: "keep" });
645
+ const section = intentsSection(text);
646
+ if (section === null)
647
+ return [];
648
+ const lines = [];
649
+ for (const line of section.split(`
650
+ `)) {
651
+ const trimmed = line.trim();
652
+ if (trimmed.length === 0)
653
+ continue;
654
+ const m = trimmed.match(INTENT_BULLET);
655
+ if (m)
656
+ lines.push({ slug: (m[1] ?? "").toLowerCase(), goal: (m[2] ?? "").trim() });
657
+ }
658
+ return lines;
659
+ }
637
660
  function matchesLegacyMilestone(milestones, slug) {
638
661
  return milestones.find((m) => m.title === slug) ?? null;
639
662
  }
@@ -1904,6 +1927,7 @@ function isPrincipal(login, principalAllowlist) {
1904
1927
  // ../../packages/aeg-core/src/verdict-extraction.ts
1905
1928
  var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
1906
1929
  var OBJECTIVES_VERSION_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Objectives version:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1930
+ var RULING_ORDINAL_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Ruling ordinal:\s*(\d+)(?!\d)/im;
1907
1931
  function firstFiveLines(comment) {
1908
1932
  return comment.split(`
1909
1933
  `).slice(0, 5).join(`
@@ -1917,6 +1941,15 @@ function extractObjectivesVersion(comment) {
1917
1941
  const m = firstFiveLines(comment).match(OBJECTIVES_VERSION_PATTERN);
1918
1942
  return m ? m[1].toLowerCase() : null;
1919
1943
  }
1944
+ function firstSevenLines(comment) {
1945
+ return comment.split(`
1946
+ `).slice(0, 7).join(`
1947
+ `);
1948
+ }
1949
+ function extractRulingOrdinal(comment) {
1950
+ const m = firstSevenLines(comment).match(RULING_ORDINAL_PATTERN);
1951
+ return m ? Number.parseInt(m[1], 10) : null;
1952
+ }
1920
1953
  function extractVerdict(comments, valuePattern, missingLabel) {
1921
1954
  const candidates = comments.filter((c) => valuePattern.test(c));
1922
1955
  if (candidates.length === 0) {
@@ -1924,6 +1957,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1924
1957
  value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
1925
1958
  headSha: null,
1926
1959
  objectivesVersion: null,
1960
+ rulingOrdinal: null,
1927
1961
  danglingNote: `no ${missingLabel} verdict comment found on this PR`
1928
1962
  };
1929
1963
  }
@@ -1934,6 +1968,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1934
1968
  value: `the most recent ${missingLabel} comment's VERDICT line is not within its first five lines — DANGLING, see below`,
1935
1969
  headSha: null,
1936
1970
  objectivesVersion: null,
1971
+ rulingOrdinal: null,
1937
1972
  danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
1938
1973
  };
1939
1974
  }
@@ -1941,6 +1976,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1941
1976
  value: m[1].toUpperCase().replace(/[_-]/g, " "),
1942
1977
  headSha: extractHeadSha(latest),
1943
1978
  objectivesVersion: extractObjectivesVersion(latest),
1979
+ rulingOrdinal: extractRulingOrdinal(latest),
1944
1980
  danglingNote: null
1945
1981
  };
1946
1982
  }
@@ -2471,6 +2507,9 @@ import { createHash as createHash2 } from "node:crypto";
2471
2507
  var HEADING_RE = /^##[ \t]*Objectives[ \t]*$/im;
2472
2508
  var NEXT_HEADING_RE = /^##[ \t]/m;
2473
2509
  var OBJECTIVE_LINE_RE = /^O(\d+)\.[ \t]*(.*)$/;
2510
+ function maskedForHeadingSearch(body) {
2511
+ return maskDetailsBlocks(maskCode(body));
2512
+ }
2474
2513
  function hasBacktickedPath(text) {
2475
2514
  let i = 0;
2476
2515
  while (i < text.length) {
@@ -2493,16 +2532,22 @@ function stripObjectiveBackticks(text) {
2493
2532
  function wordCount(text) {
2494
2533
  return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
2495
2534
  }
2496
- function objectivesSectionText(body) {
2497
- const heading = HEADING_RE.exec(body);
2535
+ function objectivesSectionBounds(body) {
2536
+ const masked = maskedForHeadingSearch(body);
2537
+ const heading = HEADING_RE.exec(masked);
2498
2538
  if (!heading)
2499
2539
  return null;
2500
- const afterHeading = body.slice(heading.index + heading[0].length);
2540
+ const start = heading.index + heading[0].length;
2541
+ const afterHeading = masked.slice(start);
2501
2542
  const next = NEXT_HEADING_RE.exec(afterHeading);
2502
- return next ? afterHeading.slice(0, next.index) : afterHeading;
2543
+ return { start, end: next ? start + next.index : body.length };
2544
+ }
2545
+ function objectivesSectionText(body) {
2546
+ const bounds = objectivesSectionBounds(body);
2547
+ return bounds === null ? null : body.slice(bounds.start, bounds.end);
2503
2548
  }
2504
2549
  function hasObjectivesHeading(body) {
2505
- return HEADING_RE.test(body);
2550
+ return HEADING_RE.test(maskedForHeadingSearch(body));
2506
2551
  }
2507
2552
  function objectivesOf(body) {
2508
2553
  const section = objectivesSectionText(body);
@@ -2566,6 +2611,15 @@ function isIssueNotFoundError(err) {
2566
2611
  `);
2567
2612
  return /could not resolve to an (?:issue|pull request)|\b404\b|not found/i.test(haystack);
2568
2613
  }
2614
+ function resolveObjectivesSource(prBody, issue, cutoverIssue) {
2615
+ if (issue !== null && issue < cutoverIssue)
2616
+ return { kind: "none" };
2617
+ if (issue !== null)
2618
+ return { kind: "issue", issue };
2619
+ if (hasObjectivesHeading(prBody))
2620
+ return { kind: "body" };
2621
+ return { kind: "none" };
2622
+ }
2569
2623
 
2570
2624
  // ../../packages/aeg-core/src/premise-check.ts
2571
2625
  var ASSERTION_KINDS = new Set(["contains", "absent", "sha256"]);
@@ -2830,16 +2884,43 @@ function extractFencedBlocks(text) {
2830
2884
  return blocks;
2831
2885
  }
2832
2886
  var AEG_BRIEF_V1_MARKER = "<!-- aeg:brief:v1 -->";
2833
- function contentAfterTwoLines(body) {
2834
- const first = body.indexOf(`
2835
- `);
2836
- if (first === -1)
2837
- return "";
2838
- const second = body.indexOf(`
2839
- `, first + 1);
2840
- if (second === -1)
2841
- return "";
2842
- return body.slice(second + 1);
2887
+ function contentAfterNLines(body, n) {
2888
+ let idx = -1;
2889
+ for (let i = 0;i < n; i++) {
2890
+ idx = body.indexOf(`
2891
+ `, idx + 1);
2892
+ if (idx === -1)
2893
+ return "";
2894
+ }
2895
+ return body.slice(idx + 1);
2896
+ }
2897
+ function briefMarkerFor(version) {
2898
+ return `<!-- aeg:brief:v${version} -->`;
2899
+ }
2900
+ var BRIEF_MARKER_LINE_RE = /^<!-- aeg:brief:v(\d+) -->$/;
2901
+ function parseBriefMarkerVersion(firstLine) {
2902
+ const m = BRIEF_MARKER_LINE_RE.exec(firstLine.trim());
2903
+ if (!m)
2904
+ return null;
2905
+ const version = Number.parseInt(m[1], 10);
2906
+ return Number.isInteger(version) && version >= 1 ? version : null;
2907
+ }
2908
+ function frozenBriefContent(body, version) {
2909
+ return contentAfterNLines(body, version === 1 ? 2 : 3);
2910
+ }
2911
+ function resolveNewestFrozenBrief(comments, allowlist) {
2912
+ let best = null;
2913
+ for (const c of comments) {
2914
+ if (!isPrincipal(c.author, allowlist))
2915
+ continue;
2916
+ const version = parseBriefMarkerVersion(c.body.split(`
2917
+ `)[0] ?? "");
2918
+ if (version === null)
2919
+ continue;
2920
+ if (best === null || version > best.version)
2921
+ best = { ...c, version };
2922
+ }
2923
+ return best === null ? null : { ...best, content: frozenBriefContent(best.body, best.version) };
2843
2924
  }
2844
2925
  // ../../packages/aeg-core/src/doctrine-portability.ts
2845
2926
  var EXEMPT_LITERALS = new Set([
@@ -3196,6 +3277,30 @@ function checkDocsWithinSurface(body, issueNumber) {
3196
3277
  }
3197
3278
  return { status: errors.length > 0 ? "fail" : "pass", errors };
3198
3279
  }
3280
+ function checkSurfaceExcludesBoundDoc(body, docOwnersContent) {
3281
+ if (docOwnersContent === null)
3282
+ return { status: "pass", errors: [] };
3283
+ const surface = parseIssueSurface(body);
3284
+ if (!surface.ok)
3285
+ return { status: "pass", errors: [] };
3286
+ const { bindings } = parseDocOwners(docOwnersContent);
3287
+ if (bindings.length === 0)
3288
+ return { status: "pass", errors: [] };
3289
+ const errors = [];
3290
+ for (const binding of bindings) {
3291
+ if (isUrlPointer(binding.pointer))
3292
+ continue;
3293
+ const inGlob = surface.value.in.find((g) => globsOverlap(binding.glob, g));
3294
+ if (!inGlob)
3295
+ continue;
3296
+ const pointerPath = pointerToPath(binding.pointer);
3297
+ const outGlob = surface.value.out.find((g) => globCoversPath(g, pointerPath));
3298
+ if (!outGlob)
3299
+ continue;
3300
+ errors.push(`issue-validation Surface: \`${inGlob}\` in \`## Surface\`'s \`in:\` list matches ${DOC_OWNERS_PATH}:${binding.lineNum} (glob \`${binding.glob}\` → ${binding.pointer}), but \`${outGlob}\` in \`## Surface\`'s \`out:\` list excludes ${binding.pointer} — this task's own surface cannot satisfy doc-coverage and surface-scope at once.`);
3301
+ }
3302
+ return { status: errors.length > 0 ? "fail" : "pass", errors };
3303
+ }
3199
3304
 
3200
3305
  // ../../packages/aeg-core/src/coherence-checks.ts
3201
3306
  var R1_GRANDFATHERED_ISSUES = new Set([279, 280, 281, 282]);
@@ -3297,6 +3402,24 @@ function buildProvenanceBlock(facts) {
3297
3402
  return { block: lines.join(`
3298
3403
  `), issue, dangling };
3299
3404
  }
3405
+ // ../../packages/aeg-core/src/ruling-ordinal.ts
3406
+ var RULING_MARKER_ORDINAL = /^<!-- aeg:principal:ruling:\d+-(\d+) -->$/;
3407
+ function newestPrincipalRulingOrdinal(comments, allowlist) {
3408
+ let best = 0;
3409
+ for (const c of comments) {
3410
+ if (!isPrincipal(c.author, allowlist))
3411
+ continue;
3412
+ const firstLine = (c.body.split(`
3413
+ `)[0] ?? "").trim();
3414
+ const m = RULING_MARKER_ORDINAL.exec(firstLine);
3415
+ if (!m)
3416
+ continue;
3417
+ const k = Number.parseInt(m[1], 10);
3418
+ if (k > best)
3419
+ best = k;
3420
+ }
3421
+ return best;
3422
+ }
3300
3423
  // ../../packages/aeg-core/src/diagram-model.ts
3301
3424
  import matter from "gray-matter";
3302
3425
  // ../../packages/aeg-core/src/reader-resolvable-prose.ts
@@ -3616,12 +3739,26 @@ function renderSection2(facts) {
3616
3739
  "",
3617
3740
  `- **Tranche:** \`${facts.trancheSlug}\`, task ${facts.taskId}, Issue #${facts.issue}. Branch \`task/${facts.trancheSlug}/${facts.taskId}\`. \`Depends-on: ${depends}\`, \`Conflicts-with: ${conflicts}\`. Confirm \`READY TO DISPATCH\` at your own Step 0.`,
3618
3741
  `- **Read Issue #${facts.issue} in full** for the complete rationale — do not re-derive it.`,
3742
+ `- **Revision:** rendered at \`${facts.sourceRevision}\` — the checkout's HEAD equaled the remote default branch, and no pinned file below carried an uncommitted change, when these facts were read.`,
3619
3743
  `- ${facts.rationale.boundary}`,
3620
3744
  `- ${facts.rationale.trapsToAvoid}`
3621
3745
  ];
3622
3746
  return lines.join(`
3623
3747
  `);
3624
3748
  }
3749
+ function extractSourceRevision(briefText) {
3750
+ const lines = briefText.split(`
3751
+ `);
3752
+ const start = lines.findIndex((l) => /^##\s*2\.\s*Context\b/.test(l.trim()));
3753
+ if (start === -1)
3754
+ return null;
3755
+ const rest = lines.slice(start + 1);
3756
+ const end = rest.findIndex((l) => /^##\s/.test(l));
3757
+ const section2 = (end === -1 ? rest : rest.slice(0, end)).join(`
3758
+ `);
3759
+ const m = /^- \*\*Revision:\*\* rendered at `([0-9a-fA-F]{7,40})`/m.exec(section2);
3760
+ return m ? m[1] : null;
3761
+ }
3625
3762
  function renderSection3(facts) {
3626
3763
  return ["## 3. Technical dependencies", "", `${facts.rationale.dependencyRationale}`].join(`
3627
3764
  `);
@@ -3825,6 +3962,9 @@ function renderBrief(facts, template) {
3825
3962
  const missing = [];
3826
3963
  if (facts.projects.length === 0)
3827
3964
  missing.push("Project (task has no Project(s) declared)");
3965
+ if (!facts.sourceRevision) {
3966
+ missing.push("Revision (no source revision resolved — the caller must refuse before render, never render with an empty one)");
3967
+ }
3828
3968
  if (facts.objectives.length === 0 && facts.issue >= OBJECTIVES_SINCE_ISSUE) {
3829
3969
  missing.push("Objectives (Issue has no `## Objectives` section)");
3830
3970
  }
@@ -7959,14 +8099,41 @@ var COMMANDS = [
7959
8099
  },
7960
8100
  {
7961
8101
  name: "task dispatch",
7962
- description: "Render, pin, and post the brief on the Issue as the frozen original; start the developer",
8102
+ description: "Deprecated — render, pin, and post the brief on the Issue as the frozen original; start the developer",
7963
8103
  flags: [
7964
8104
  { flag: "--agent <claude|codex|gemini>", description: "Start the developer through dispatchRole once posted" }
7965
8105
  ],
7966
8106
  details: [
7967
8107
  "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.",
7968
8108
  "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.",
7969
- "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."
8109
+ "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.",
8110
+ "Deprecated in favor of `task brief` (preparation only) and `task run` (the full unattended loop) — kept for a documented compatibility window while callers migrate."
8111
+ ],
8112
+ status: "shipped"
8113
+ },
8114
+ {
8115
+ name: "task brief",
8116
+ description: "Render and freeze the brief as the Issue's original comment — preparation only, starts nobody",
8117
+ details: [
8118
+ "The preparation half of `task dispatch`, extracted so it is callable on its own: resolves the task's Issue, renders the brief, refuses on any gap, refuses when a frozen brief already exists, and posts it once as the same `aeg:brief:v1` Issue comment `task dispatch` posts. Starts no agent under any circumstances — there is no `--agent` flag here at all.",
8119
+ "Successor to `task dispatch` for the preparation step; the full unattended run (preparation, then the developer, then the review loop) is `task run`."
8120
+ ],
8121
+ status: "shipped"
8122
+ },
8123
+ {
8124
+ name: "task run",
8125
+ description: "One command from a planned Issue to a reviewed pull request — exactly one developer started",
8126
+ flags: [
8127
+ {
8128
+ flag: "--agent <claude|codex|gemini>",
8129
+ description: "Vendor for the developer and both reviewers this run dispatches"
8130
+ }
8131
+ ],
8132
+ details: [
8133
+ "Composes `task brief`'s own preparation (`prepareTask`) with `dev-review-loop` (`devReviewLoop`) — nothing else. Preparation starts no agent; the loop's own round 1 reads the frozen brief off the Issue and is the only place a developer is ever dispatched from a fresh task, so exactly one developer is started by construction.",
8134
+ "A brief already frozen on the Issue is reused, never re-posted — the second `task dispatch`/`task brief` call this composes around does not fail the whole run, it just skips straight to running the loop. A task whose Issue refuses preparation (a missing brief section, an unmet dispatch gate) is refused before any agent starts, with nothing posted.",
8135
+ "Refuses when the frozen brief's developer branch already has an open pull request — the old `task dispatch` followed by `task run` cannot start two developers this way.",
8136
+ "Exit and printed summary distinguish a published, reviewed pull request (exit `0`, the PR URL) from a pause (exit `1`, with the exact `vinaya dev-review-loop --resume <pr>` command to continue), a usage/argv error (exit `2`), and any other failure (exit `3`) — never sharing `1` with a pause, so an unattended host tells them apart from the exit code alone. A run that pauses is resumed with the loop's own existing `--resume <pr>` flag, never a flag on this command."
7970
8137
  ],
7971
8138
  status: "shipped"
7972
8139
  },
@@ -8174,6 +8341,15 @@ var COMMANDS = [
8174
8341
  ],
8175
8342
  status: "shipped"
8176
8343
  },
8344
+ {
8345
+ name: "milestone status",
8346
+ description: "Print each of a Milestone's declared tranche intents with its derived lifecycle and issue counts",
8347
+ flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
8348
+ details: [
8349
+ "Read-only — writes nothing. For each `- <slug>: …` line in the Milestone's `### Tranche intents` section, prints the tranche's lifecycle (`planned`/`active`/`complete`) and its labeled Issues' counts (merged, open, not planned), all derived from the forge. Refuses if `<n>` isn't a real Milestone in this repo, or if the forge is unreachable."
8350
+ ],
8351
+ status: "shipped"
8352
+ },
8177
8353
  {
8178
8354
  name: "review status",
8179
8355
  description: "Print the review loop's own state for a PR, and its branch's distance from the base",
@@ -8664,6 +8840,62 @@ function expandGlob(glob) {
8664
8840
  function sha256OfFile(path) {
8665
8841
  return createHash5("sha256").update(readFileSync9(path)).digest("hex");
8666
8842
  }
8843
+ function resolveRemoteDefaultBranch(cwd) {
8844
+ let out;
8845
+ try {
8846
+ out = execFileSync9("git", ["ls-remote", "--symref", "origin", "HEAD"], {
8847
+ cwd,
8848
+ encoding: "utf8",
8849
+ stdio: ["ignore", "pipe", "pipe"]
8850
+ }).trim();
8851
+ } catch {
8852
+ return null;
8853
+ }
8854
+ const lines = out.split(`
8855
+ `);
8856
+ const symrefLine = lines.find((l) => l.startsWith("ref:"));
8857
+ const shaLine = lines.find((l) => !l.startsWith("ref:") && /\tHEAD$/.test(l));
8858
+ const branch = symrefLine ? /^ref:\s*refs\/heads\/(\S+)/.exec(symrefLine)?.[1] : undefined;
8859
+ const sha = shaLine?.split("\t")[0];
8860
+ return branch && sha ? { branch, sha } : null;
8861
+ }
8862
+ function checkStaleAgainstRemote(headSha, resolveRemote = resolveRemoteDefaultBranch) {
8863
+ const remote = resolveRemote();
8864
+ if (!remote) {
8865
+ return [
8866
+ "the remote default branch could not be resolved (`git ls-remote origin HEAD` failed — offline?) — refusing rather than rendering from a checkout of unknown freshness."
8867
+ ];
8868
+ }
8869
+ if (headSha !== remote.sha) {
8870
+ return [
8871
+ `checkout HEAD \`${headSha}\` is behind the remote default branch \`${remote.branch}\` at \`${remote.sha}\` — fetch and update before preparing a brief.`
8872
+ ];
8873
+ }
8874
+ return [];
8875
+ }
8876
+ function checkDirtyPinnedFiles(pinnedPaths, cwd) {
8877
+ if (pinnedPaths.length === 0)
8878
+ return [];
8879
+ let out;
8880
+ try {
8881
+ out = execFileSync9("git", ["status", "--porcelain", "--", ...pinnedPaths], {
8882
+ cwd,
8883
+ encoding: "utf8",
8884
+ stdio: ["ignore", "pipe", "pipe"]
8885
+ });
8886
+ } catch (err) {
8887
+ return [
8888
+ `could not check working-tree status for the brief's pinned files: ${err instanceof Error ? err.message : String(err)}`
8889
+ ];
8890
+ }
8891
+ if (!out.trim())
8892
+ return [];
8893
+ const dirty = out.split(`
8894
+ `).filter((l) => l.length > 0).map((l) => l.slice(3).trim()).filter(Boolean);
8895
+ return [
8896
+ `checkout carries uncommitted changes to pinned file(s): ${dirty.join(", ")} — commit or discard them before preparing a brief.`
8897
+ ];
8898
+ }
8667
8899
  function resolveBoundaryPaths(tokens, allTrackedFiles) {
8668
8900
  const trackedSet = new Set(allTrackedFiles);
8669
8901
  const resolved = new Set;
@@ -8716,6 +8948,10 @@ async function assembleAndRenderBrief(trancheSlug, taskId, surfaceGlobsOverride)
8716
8948
  missing: ["could not resolve owner/repo (set AEG_REPO=owner/repo, or confirm `git remote get-url origin`)."]
8717
8949
  };
8718
8950
  }
8951
+ const headSha = git(["rev-parse", "HEAD"]);
8952
+ const staleness = checkStaleAgainstRemote(headSha);
8953
+ if (staleness.length > 0)
8954
+ return { ok: false, missing: staleness };
8719
8955
  const source = createForgeSource({ owner: repo.owner, repo: repo.repo });
8720
8956
  let tranche;
8721
8957
  try {
@@ -8797,6 +9033,9 @@ async function assembleAndRenderBrief(trancheSlug, taskId, surfaceGlobsOverride)
8797
9033
  `).map((s) => s.trim()).filter(Boolean);
8798
9034
  const boundaryTokens = extractBoundaryFilePaths(rationale.boundary ?? "");
8799
9035
  const surfaceFiles = resolveBoundaryPaths(boundaryTokens, allTrackedFiles).sort().map((path) => ({ path, sha256: sha256OfFile(path), packageName: packageNameForPath(path) }));
9036
+ const dirtiness = checkDirtyPinnedFiles(surfaceFiles.map((f) => f.path));
9037
+ if (dirtiness.length > 0)
9038
+ return { ok: false, missing: dirtiness };
8800
9039
  const workspaces = workspaceGlobs();
8801
9040
  const consumersOf = buildConsumersOf(workspaces, listDirs, readManifest);
8802
9041
  const partsResult = parseIssueParts(issueBody);
@@ -8826,7 +9065,8 @@ async function assembleAndRenderBrief(trancheSlug, taskId, surfaceGlobsOverride)
8826
9065
  dispatchBlockers: gate.blockers,
8827
9066
  surfaceFiles,
8828
9067
  consumersOf,
8829
- docOwnersContent: existsSync7(DOC_OWNERS_PATH2) ? readFileSync9(DOC_OWNERS_PATH2, "utf8") : null
9068
+ docOwnersContent: existsSync7(DOC_OWNERS_PATH2) ? readFileSync9(DOC_OWNERS_PATH2, "utf8") : null,
9069
+ sourceRevision: headSha
8830
9070
  };
8831
9071
  const template = readFileSync9(TEMPLATE_PATH, "utf8");
8832
9072
  const result = renderBrief(facts, template);
@@ -10563,6 +10803,27 @@ function log(e) {
10563
10803
 
10564
10804
  // src/lib/dispatch.ts
10565
10805
  import { dirname as dirname7, join as join14 } from "node:path";
10806
+ var ANSI_RESET = "\x1B[0m";
10807
+ var ROLE_ANSI = {
10808
+ planner: "\x1B[34m",
10809
+ developer: "\x1B[36m",
10810
+ "code-reviewer": "\x1B[35m",
10811
+ security: "\x1B[31m",
10812
+ principal: "\x1B[33m",
10813
+ archivist: "\x1B[32m",
10814
+ architect: "\x1B[93m"
10815
+ };
10816
+ var LOOP_ANSI = "\x1B[90m";
10817
+ function colourEnabled(stream) {
10818
+ return Boolean(stream.isTTY) && process.env.NO_COLOR === undefined;
10819
+ }
10820
+ function colourAgentLine(role, line, stream) {
10821
+ const prefixed = `[${role}] ${line}`;
10822
+ return colourEnabled(stream) ? `${ROLE_ANSI[role]}${prefixed}${ANSI_RESET}` : prefixed;
10823
+ }
10824
+ function colourLoopLine(line, stream) {
10825
+ return colourEnabled(stream) ? `${LOOP_ANSI}${line}${ANSI_RESET}` : line;
10826
+ }
10566
10827
  var AGENT_VENDOR_NAMES = ["claude", "codex", "gemini"];
10567
10828
  function isAgentVendor2(value) {
10568
10829
  return AGENT_VENDOR_NAMES.includes(value);
@@ -10757,6 +11018,14 @@ function recordResumeState(record) {
10757
11018
  return null;
10758
11019
  }
10759
11020
  }
11021
+ function readResumeRecord(role, agent, repo, task, pr) {
11022
+ try {
11023
+ const path = resumeRecordPathFor(role, agent, repo, task, pr);
11024
+ return JSON.parse(readFileSync14(path, "utf8"));
11025
+ } catch {
11026
+ return null;
11027
+ }
11028
+ }
10760
11029
  function renderClaudeEvent(obj) {
10761
11030
  const type = obj.type;
10762
11031
  if (type === "assistant" || type === "user") {
@@ -10929,6 +11198,10 @@ async function dispatchRole(role, agent, prompt2, opts) {
10929
11198
  const effectId = randomUUID2();
10930
11199
  const vendor = VENDOR_TABLE[agent];
10931
11200
  const start = Date.now();
11201
+ const writeLifecycle = (msg) => {
11202
+ process.stderr.write(`${colourLoopLine(msg, process.stderr)}
11203
+ `);
11204
+ };
10932
11205
  const roundField = opts.round !== undefined ? { round: opts.round } : {};
10933
11206
  const resolvedModel = opts.model !== undefined ? `requested:${opts.model}` : "default";
10934
11207
  if (opts.model !== undefined) {
@@ -10948,8 +11221,7 @@ async function dispatchRole(role, agent, prompt2, opts) {
10948
11221
  usage: null,
10949
11222
  duration_ms: durationMs
10950
11223
  });
10951
- process.stderr.write(`[vinaya dispatch ${effectId}] ${role} via ${agent}: refused — model '${opts.model}' is a ${foreignVendor} model; ` + `${agent} does not accept it. ${agent} accepts its own model names (never a ${foreignVendor} alias or a ` + `'${foreignVendor}-'/'gemma-' full name).
10952
- `);
11224
+ writeLifecycle(`[vinaya dispatch ${effectId}] ${role} via ${agent}: refused — model '${opts.model}' is a ${foreignVendor} model; ` + `${agent} does not accept it. ${agent} accepts its own model names (never a ${foreignVendor} alias or a ` + `'${foreignVendor}-'/'gemma-' full name).`);
10953
11225
  await waitForDispatchLine(outboxPath, priorSize, runId, effectId, "dispatch_failed");
10954
11226
  return { exitCode: null, durationMs, usage: null, resumeId: null, timedOut: false, failureReason: "refused" };
10955
11227
  }
@@ -11006,8 +11278,7 @@ async function dispatchRole(role, agent, prompt2, opts) {
11006
11278
  const MAX_STDOUT_BYTES = 1e6;
11007
11279
  const outputTee = openOutputTee(effectId);
11008
11280
  if (outputTee.path !== null) {
11009
- process.stderr.write(`[vinaya dispatch ${effectId}] ${role} via ${agent}: output teed to ${outputTee.path}
11010
- `);
11281
+ writeLifecycle(`[vinaya dispatch ${effectId}] ${role} via ${agent}: output teed to ${outputTee.path}`);
11011
11282
  }
11012
11283
  let renderCarry = "";
11013
11284
  child.stdout.on("data", (chunk) => {
@@ -11023,9 +11294,13 @@ async function dispatchRole(role, agent, prompt2, opts) {
11023
11294
  continue;
11024
11295
  try {
11025
11296
  const rendered = vendor.renderEvent(JSON.parse(line));
11026
- if (rendered)
11027
- process.stderr.write(`${rendered}
11297
+ if (rendered) {
11298
+ const out = rendered.split(`
11299
+ `).map((l) => colourAgentLine(role, l, process.stderr)).join(`
11300
+ `);
11301
+ process.stderr.write(`${out}
11028
11302
  `);
11303
+ }
11029
11304
  } catch {}
11030
11305
  }
11031
11306
  });
@@ -11034,22 +11309,18 @@ async function dispatchRole(role, agent, prompt2, opts) {
11034
11309
  });
11035
11310
  const heartbeatTimer = setInterval(() => {
11036
11311
  const elapsedS = Math.round((Date.now() - start) / 1000);
11037
- process.stderr.write(`[vinaya dispatch ${effectId}] ${role} via ${agent}: still running — ${elapsedS}s elapsed (ceiling ${Math.round(timeoutMs / 1000)}s)
11038
- `);
11312
+ writeLifecycle(`[vinaya dispatch ${effectId}] ${role} via ${agent}: still running — ${elapsedS}s elapsed (ceiling ${Math.round(timeoutMs / 1000)}s)`);
11039
11313
  }, HEARTBEAT_INTERVAL_MS);
11040
11314
  const warnLeadMs = timeoutWarningLeadMs(timeoutMs);
11041
11315
  const warnTimer = setTimeout(() => {
11042
- process.stderr.write(`[vinaya dispatch ${effectId}] ${role} via ${agent}: approaching timeout — SIGTERM in ~${Math.round(warnLeadMs / 1000)}s unless it finishes first
11043
- `);
11316
+ writeLifecycle(`[vinaya dispatch ${effectId}] ${role} via ${agent}: approaching timeout — SIGTERM in ~${Math.round(warnLeadMs / 1000)}s unless it finishes first`);
11044
11317
  }, Math.max(timeoutMs - warnLeadMs, 0));
11045
11318
  const timeoutTimer = setTimeout(() => {
11046
11319
  timedOut = true;
11047
- process.stderr.write(`[vinaya dispatch ${effectId}] ${role} via ${agent}: ceiling reached — sending SIGTERM
11048
- `);
11320
+ writeLifecycle(`[vinaya dispatch ${effectId}] ${role} via ${agent}: ceiling reached — sending SIGTERM`);
11049
11321
  child.kill("SIGTERM");
11050
11322
  killTimer = setTimeout(() => {
11051
- process.stderr.write(`[vinaya dispatch ${effectId}] ${role} via ${agent}: still alive after SIGTERM — sending SIGKILL
11052
- `);
11323
+ writeLifecycle(`[vinaya dispatch ${effectId}] ${role} via ${agent}: still alive after SIGTERM — sending SIGKILL`);
11053
11324
  child.kill("SIGKILL");
11054
11325
  }, SIGKILL_GRACE_MS);
11055
11326
  }, timeoutMs);
@@ -11134,8 +11405,7 @@ async function dispatchRole(role, agent, prompt2, opts) {
11134
11405
  capturedAt: new Date().toISOString()
11135
11406
  });
11136
11407
  if (resumeRecordPath !== null) {
11137
- process.stderr.write(`[vinaya dispatch ${effectId}] ${role} via ${agent}: resumable — session recorded at ${resumeRecordPath}
11138
- `);
11408
+ writeLifecycle(`[vinaya dispatch ${effectId}] ${role} via ${agent}: resumable — session recorded at ${resumeRecordPath}`);
11139
11409
  }
11140
11410
  }
11141
11411
  const reportedModel = vendor.parseModel(stdoutBuf);
@@ -11161,10 +11431,10 @@ async function dispatchRole(role, agent, prompt2, opts) {
11161
11431
 
11162
11432
  // src/lib/dev-review-loop.ts
11163
11433
  import { randomUUID as randomUUID3 } from "node:crypto";
11164
- import { execFileSync as execFileSync17 } from "node:child_process";
11165
- import { mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync3, readFileSync as readFileSync17, rmSync as rmSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync9 } from "node:fs";
11166
- import { tmpdir as tmpdir4 } from "node:os";
11167
- import { dirname as dirname8, join as join18 } from "node:path";
11434
+ import { execFileSync as execFileSync16 } from "node:child_process";
11435
+ import { existsSync as existsSync13, mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync2, readFileSync as readFileSync17, rmSync as rmSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync8 } from "node:fs";
11436
+ import { tmpdir as tmpdir3 } from "node:os";
11437
+ import { dirname as dirname8, join as join17 } from "node:path";
11168
11438
 
11169
11439
  // src/commands/review-post.ts
11170
11440
  import { execFileSync as execFileSync15 } from "node:child_process";
@@ -11494,6 +11764,15 @@ function readProjectPaths(root = repoRoot()) {
11494
11764
  return [];
11495
11765
  }
11496
11766
  }
11767
+ function readDocOwnersContent(root = repoRoot()) {
11768
+ if (!root)
11769
+ return null;
11770
+ try {
11771
+ return readFileSync15(join15(root, DOC_OWNERS_PATH), "utf8");
11772
+ } catch {
11773
+ return null;
11774
+ }
11775
+ }
11497
11776
  var CHECK_ISSUE_CONTENT = "issue-content";
11498
11777
  var ISSUE_CONTENT_RECOVERY = {
11499
11778
  blastRadius: "Add a second registered `Project(s)` this task also touches, or a `blast-radius-ack: <why one lens is enough>` line, then re-run `{cmd}`.",
@@ -11501,7 +11780,8 @@ var ISSUE_CONTENT_RECOVERY = {
11501
11780
  rationaleNamesDocs: 'Name a concrete doc/skill path (e.g. `aeg-root/…`, `.claude/skills/…/SKILL.md`) in "Docs to keep coherent" or "Traps", or write the `no-doc-surface` sentinel if the surface genuinely has none, then re-run `{cmd}`.',
11502
11781
  surfaceGlobsResolve: "Fix the named `## Surface` `in:` glob so it matches at least one real tracked file (a typo, or a directory that does not exist yet), then re-run `{cmd}`.",
11503
11782
  partsCiteObjectives: "Fix the named Part to cite an objective id the `## Objectives` section actually defines, or add the missing objective, then re-run `{cmd}`.",
11504
- docsWithinSurface: 'Move the named doc pointer to a path `## Surface`\'s `in:` globs actually cover (never widen the surface just to fit the pointer — that renders an unusable brief), or drop it from "Docs to keep coherent" if this task does not really keep it coherent, then re-run `{cmd}`.'
11783
+ docsWithinSurface: 'Move the named doc pointer to a path `## Surface`\'s `in:` globs actually cover (never widen the surface just to fit the pointer — that renders an unusable brief), or drop it from "Docs to keep coherent" if this task does not really keep it coherent, then re-run `{cmd}`.',
11784
+ surfaceExcludesBoundDoc: "Either move the named `out:` glob so it no longer covers the bound document, or narrow the `in:` glob so it no longer reaches the doc-owners binding — the Issue cannot declare both at once. Then re-run `{cmd}`."
11505
11785
  };
11506
11786
  function validateIssueContent(input) {
11507
11787
  const findings = [
@@ -11513,7 +11793,8 @@ function validateIssueContent(input) {
11513
11793
  [checkRationaleNamesDocs(input.body).errors, "rationaleNamesDocs"],
11514
11794
  [checkSurfaceGlobsResolve(input.body, input.resolvesToFile).errors, "surfaceGlobsResolve"],
11515
11795
  [checkPartsCiteDefinedObjectives(input.body).errors, "partsCiteObjectives"],
11516
- [checkDocsWithinSurface(input.body, input.issueNumber).errors, "docsWithinSurface"]
11796
+ [checkDocsWithinSurface(input.body, input.issueNumber).errors, "docsWithinSurface"],
11797
+ [checkSurfaceExcludesBoundDoc(input.body, input.docOwnersContent).errors, "surfaceExcludesBoundDoc"]
11517
11798
  ];
11518
11799
  const errors = [];
11519
11800
  for (const [messages, kind] of findings) {
@@ -11587,7 +11868,8 @@ function validateTaskIssue(body, title, labels, retryCommand, issueNumber) {
11587
11868
  projectPaths: readProjectPaths(),
11588
11869
  retryCommand,
11589
11870
  issueNumber,
11590
- resolvesToFile: (glob) => expandGlob(glob).length > 0
11871
+ resolvesToFile: (glob) => expandGlob(glob).length > 0,
11872
+ docOwnersContent: readDocOwnersContent()
11591
11873
  });
11592
11874
  if (contentErrors.length > 0)
11593
11875
  refuse2(contentErrors);
@@ -11707,14 +11989,14 @@ function renderFindingsSection(findings) {
11707
11989
  class ObjectivesParseError extends Error {
11708
11990
  }
11709
11991
  var OBJECTIVE_ID_ONLY = /^O\d+$/;
11710
- var STRUCTURAL_MARKER_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?(?:VERDICT|Judged head|Objectives version):/i;
11992
+ var STRUCTURAL_MARKER_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?(?:VERDICT|Judged head|Objectives version|Ruling ordinal):/i;
11711
11993
  function invalidObjectiveEvidenceReason(evidence) {
11712
11994
  if (evidence.includes(`
11713
11995
  `)) {
11714
11996
  return "evidence contains a newline — each objective renders as exactly one line";
11715
11997
  }
11716
11998
  if (STRUCTURAL_MARKER_PATTERN.test(evidence)) {
11717
- return "evidence looks like a VERDICT:/Judged head:/Objectives version: line, which would corrupt the rendered comment's structural markers";
11999
+ return "evidence looks like a VERDICT:/Judged head:/Objectives version:/Ruling ordinal: line, which would corrupt the rendered comment's structural markers";
11718
12000
  }
11719
12001
  return null;
11720
12002
  }
@@ -11797,6 +12079,7 @@ function renderCodeReviewComment(input) {
11797
12079
  if (input.objectivesVersion !== null) {
11798
12080
  lines.push(`Objectives version: ${input.objectivesVersion}`, "");
11799
12081
  }
12082
+ lines.push(`Ruling ordinal: ${input.rulingOrdinal}`, "");
11800
12083
  if (input.scopeEvidence !== null) {
11801
12084
  lines.push("```", input.scopeEvidence, "```", "");
11802
12085
  }
@@ -11820,6 +12103,7 @@ function renderSecurityComment(input) {
11820
12103
  if (input.objectivesVersion !== null) {
11821
12104
  lines.push(`Objectives version: ${input.objectivesVersion}`, "");
11822
12105
  }
12106
+ lines.push(`Ruling ordinal: ${input.rulingOrdinal}`, "");
11823
12107
  lines.push("FINDINGS (ordered by severity):", renderFindingsSection(sorted), "");
11824
12108
  if (input.objectiveResults !== null) {
11825
12109
  lines.push(renderObjectivesBlock(input.objectiveResults), "");
@@ -11900,6 +12184,7 @@ function renderEscalationComment(input) {
11900
12184
  if (input.objectivesVersion !== null) {
11901
12185
  lines.push(`Objectives version: ${input.objectivesVersion}`, "");
11902
12186
  }
12187
+ lines.push(`Ruling ordinal: ${input.rulingOrdinal}`, "");
11903
12188
  lines.push(input.summary, "", renderTokensLine(input.role, input.roleLabel, input), renderCastByLine(input.roleLabel, input.sessionId));
11904
12189
  return lines.join(`
11905
12190
  `);
@@ -11909,7 +12194,7 @@ function isBoundToHead(extraction, headSha) {
11909
12194
  return false;
11910
12195
  return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase());
11911
12196
  }
11912
- function checkExtraction(extraction, expectedValue, headSha, expectedObjectivesVersion) {
12197
+ function checkExtraction(extraction, expectedValue, headSha, expectedObjectivesVersion, expectedRulingOrdinal) {
11913
12198
  if (extraction.danglingNote) {
11914
12199
  return {
11915
12200
  ok: false,
@@ -11937,6 +12222,12 @@ function checkExtraction(extraction, expectedValue, headSha, expectedObjectivesV
11937
12222
  reason: `re-extraction found objectives version ${extraction.objectivesVersion ?? "none"}, expected ${expectedObjectivesVersion ?? "none"} — the posted comment's Objectives version: line does not match what this command rendered.`
11938
12223
  };
11939
12224
  }
12225
+ if (extraction.rulingOrdinal !== expectedRulingOrdinal) {
12226
+ return {
12227
+ ok: false,
12228
+ reason: `re-extraction found ruling ordinal ${extraction.rulingOrdinal ?? "none"}, expected ${expectedRulingOrdinal} — the posted comment's Ruling ordinal: line does not match what this command rendered.`
12229
+ };
12230
+ }
11940
12231
  return { ok: true, reason: "clean" };
11941
12232
  }
11942
12233
  function principalBodies(comments, principalAllowlist) {
@@ -11952,14 +12243,14 @@ function checkNoCrossRoleVerdict(postedBody, crossExtract, crossRoleLabel) {
11952
12243
  }
11953
12244
  return null;
11954
12245
  }
11955
- function verifyPostedCodeReview(comments, verdict, headSha, principalAllowlist, postedBody, objectivesVersion2) {
11956
- const own = checkExtraction(extractCodeReviewVerdict(principalBodies(comments, principalAllowlist)), CODE_REVIEW_VERDICT_TEXT[verdict], headSha, objectivesVersion2);
12246
+ function verifyPostedCodeReview(comments, verdict, headSha, principalAllowlist, postedBody, objectivesVersion2, rulingOrdinal) {
12247
+ const own = checkExtraction(extractCodeReviewVerdict(principalBodies(comments, principalAllowlist)), CODE_REVIEW_VERDICT_TEXT[verdict], headSha, objectivesVersion2, rulingOrdinal);
11957
12248
  if (!own.ok)
11958
12249
  return own;
11959
12250
  return checkNoCrossRoleVerdict(postedBody, extractSecurityReviewVerdict, "security") ?? own;
11960
12251
  }
11961
- function verifyPostedSecurity(comments, verdict, headSha, principalAllowlist, postedBody, objectivesVersion2) {
11962
- const own = checkExtraction(extractSecurityReviewVerdict(principalBodies(comments, principalAllowlist)), verdict, headSha, objectivesVersion2);
12252
+ function verifyPostedSecurity(comments, verdict, headSha, principalAllowlist, postedBody, objectivesVersion2, rulingOrdinal) {
12253
+ const own = checkExtraction(extractSecurityReviewVerdict(principalBodies(comments, principalAllowlist)), verdict, headSha, objectivesVersion2, rulingOrdinal);
11963
12254
  if (!own.ok)
11964
12255
  return own;
11965
12256
  return checkNoCrossRoleVerdict(postedBody, extractCodeReviewVerdict, "code-review") ?? own;
@@ -12199,32 +12490,39 @@ function fetchIssueBodyForObjectives(issue) {
12199
12490
  function resolveObjectivesForPr(pr) {
12200
12491
  const prBody = fetchPrBody(pr);
12201
12492
  const { issue } = extractIssue(prBody);
12202
- if (issue !== null && issue < OBJECTIVES_SINCE_ISSUE)
12493
+ const source = resolveObjectivesSource(prBody, issue, OBJECTIVES_SINCE_ISSUE);
12494
+ if (source.kind === "none")
12203
12495
  return { kind: "skip" };
12204
- if (issue !== null) {
12496
+ if (source.kind === "issue") {
12205
12497
  let issueBody;
12206
12498
  try {
12207
- issueBody = fetchIssueBodyForObjectives(issue);
12499
+ issueBody = fetchIssueBodyForObjectives(source.issue);
12208
12500
  } catch (err) {
12209
12501
  if (isIssueNotFoundError(err)) {
12210
- refuseCmd(`Issue #${issue} does not resolve via \`gh issue view\` — no objectives to judge against.`, "Confirm the Issue exists, or fix `Closes #N` in the PR body, then re-run.");
12502
+ refuseCmd(`Issue #${source.issue} does not resolve via \`gh issue view\` — no objectives to judge against.`, "Confirm the Issue exists, or fix `Closes #N` in the PR body, then re-run.");
12211
12503
  }
12212
- refuseCmd(`Could not fetch Issue #${issue}'s body via \`gh issue view\` to resolve its objectives — no objectives to judge against: ${err instanceof Error ? err.message : String(err)}`, "Confirm `gh auth status` passes, then re-run.");
12504
+ refuseCmd(`Could not fetch Issue #${source.issue}'s body via \`gh issue view\` to resolve its objectives — no objectives to judge against: ${err instanceof Error ? err.message : String(err)}`, "Confirm `gh auth status` passes, then re-run.");
12213
12505
  }
12214
- const parsed = objectivesOf(issueBody);
12215
- if (!parsed.ok) {
12216
- refuseCmd(`Issue #${issue}'s \`## Objectives\` section does not parse (${parsed.errors.join("; ")}) — no objectives to judge against.`, "Fix the Issue body, then re-run.");
12506
+ const parsed2 = objectivesOf(issueBody);
12507
+ if (!parsed2.ok) {
12508
+ refuseCmd(`Issue #${source.issue}'s \`## Objectives\` section does not parse (${parsed2.errors.join("; ")}) — no objectives to judge against.`, "Fix the Issue body, then re-run.");
12217
12509
  }
12218
- return { kind: "list", objectives: parsed.objectives, version: objectivesVersion(parsed.objectives) };
12510
+ return { kind: "list", objectives: parsed2.objectives, version: objectivesVersion(parsed2.objectives) };
12219
12511
  }
12220
- if (hasObjectivesHeading(prBody)) {
12221
- const parsed = objectivesOf(prBody);
12222
- if (!parsed.ok) {
12223
- refuseCmd(`This PR body's own \`## Objectives\` section does not parse (${parsed.errors.join("; ")}) — no objectives to judge against.`, "Fix the PR body's Objectives section, then re-run.");
12224
- }
12225
- return { kind: "list", objectives: parsed.objectives, version: objectivesVersion(parsed.objectives) };
12512
+ const parsed = objectivesOf(prBody);
12513
+ if (!parsed.ok) {
12514
+ refuseCmd(`This PR body's own \`## Objectives\` section does not parse (${parsed.errors.join("; ")}) — no objectives to judge against.`, "Fix the PR body's Objectives section, then re-run.");
12515
+ }
12516
+ return { kind: "list", objectives: parsed.objectives, version: objectivesVersion(parsed.objectives) };
12517
+ }
12518
+ function resolveRulingOrdinalForPr(pr) {
12519
+ let comments;
12520
+ try {
12521
+ comments = fetchComments(pr);
12522
+ } catch (err) {
12523
+ refuseCmd(`Could not fetch PR ${pr}'s comments via \`gh pr view --json comments\` to resolve the newest ruling ordinal: ${err instanceof Error ? err.message : String(err)}`, "Confirm `gh auth status` passes, then re-run.");
12226
12524
  }
12227
- refuseCmd("no objectives to judge against — this PR closes no Issue and its body carries no `## Objectives` section.", "Add `Closes #N` pointing at an Issue with an `## Objectives` list, or add a `## Objectives` section to the PR body, then re-run.");
12525
+ return newestPrincipalRulingOrdinal(comments, resolvePrincipalAllowlist(loadTrustAnchorConfig()));
12228
12526
  }
12229
12527
  function readObjectivesFile(path) {
12230
12528
  if (path.trim() === "") {
@@ -12248,7 +12546,7 @@ function readObjectivesFile(path) {
12248
12546
  function resolveObjectiveResultsForCommand(resolution, objectivesFileRaw) {
12249
12547
  if (resolution.kind === "skip") {
12250
12548
  if (objectivesFileRaw !== undefined) {
12251
- refuseCmd("`--objectives-file` was given, but no objectives to judge against exist for this PR (its Issue predates the objectives cutover).", "Drop `--objectives-file` for this PR, or judge against a post-cutover Issue.");
12549
+ refuseCmd("`--objectives-file` was given, but no objectives to judge against exist for this PR (either its Issue predates the objectives cutover, or it closes no Issue and its body carries no `## Objectives` section).", "Drop `--objectives-file` for this PR, judge against a post-cutover Issue, or add a `## Objectives` section to the PR body.");
12252
12550
  }
12253
12551
  return { objectivesVersion: null, objectiveResults: null };
12254
12552
  }
@@ -12437,6 +12735,7 @@ async function reviewPostCommand(args) {
12437
12735
  const headSha2 = resolveHeadSha(pr);
12438
12736
  const escalationObjectivesResolution = resolveObjectivesForPr(pr);
12439
12737
  const escalationObjectivesVersion = escalationObjectivesResolution.kind === "list" ? escalationObjectivesResolution.version : null;
12738
+ const escalationRulingOrdinal = resolveRulingOrdinalForPr(pr);
12440
12739
  const body2 = renderEscalationComment({
12441
12740
  ...tokens,
12442
12741
  headSha: headSha2,
@@ -12444,7 +12743,8 @@ async function reviewPostCommand(args) {
12444
12743
  summary,
12445
12744
  role: tokensRole,
12446
12745
  roleLabel,
12447
- objectivesVersion: escalationObjectivesVersion
12746
+ objectivesVersion: escalationObjectivesVersion,
12747
+ rulingOrdinal: escalationRulingOrdinal
12448
12748
  });
12449
12749
  checkRenderedCommentOrRefuse(body2, { kind: "escalation" });
12450
12750
  if (printOnly) {
@@ -12502,6 +12802,7 @@ Self-verification: clean — no verdict extracted.
12502
12802
  const objectivesResolution2 = resolveObjectivesForPr(pr);
12503
12803
  const { objectivesVersion: resolvedObjectivesVersion2, objectiveResults: objectiveResults2 } = resolveObjectiveResultsForCommand(objectivesResolution2, flags.get("--objectives-file"));
12504
12804
  refuseIfCleanVerdictHasNotMetObjective(isCleanVerdict(verdict2), "APPROVE", objectiveResults2);
12805
+ const resolvedRulingOrdinal2 = resolveRulingOrdinalForPr(pr);
12505
12806
  let comments2;
12506
12807
  try {
12507
12808
  comments2 = fetchComments(pr);
@@ -12521,7 +12822,8 @@ Self-verification: clean — no verdict extracted.
12521
12822
  tests,
12522
12823
  docs,
12523
12824
  objectivesVersion: resolvedObjectivesVersion2,
12524
- objectiveResults: objectiveResults2
12825
+ objectiveResults: objectiveResults2,
12826
+ rulingOrdinal: resolvedRulingOrdinal2
12525
12827
  };
12526
12828
  const body2 = renderCodeReviewComment(input2);
12527
12829
  checkRenderedCommentOrRefuse(body2, { kind: "code-review", verdict: verdict2 });
@@ -12536,7 +12838,7 @@ Self-verification: clean — no verdict extracted.
12536
12838
  } catch (err) {
12537
12839
  refuseCmd(`Posted the comment (${url2}) but could not re-fetch PR ${pr}'s comments to self-verify: ${err instanceof Error ? err.message : String(err)}`, "Check `gh auth status`/network and manually confirm the posted comment parses cleanly — this command could not verify it.");
12538
12840
  }
12539
- const result2 = verifyPostedCodeReview(postComments2, verdict2, headSha2, principalAllowlist2, body2, resolvedObjectivesVersion2);
12841
+ const result2 = verifyPostedCodeReview(postComments2, verdict2, headSha2, principalAllowlist2, body2, resolvedObjectivesVersion2, resolvedRulingOrdinal2);
12540
12842
  if (!result2.ok) {
12541
12843
  refuseCmd(`Posted comment ${url2}, but self-verification FAILED on re-parse: ${result2.reason}`, "The posted comment does not re-parse clean through the same extractCodeReviewVerdict/extractSecurityReviewVerdict functions the merge gate calls. Do not treat the post as valid — inspect the comment and this command for drift, fix, and re-run.");
12542
12844
  }
@@ -12579,6 +12881,7 @@ Self-verification: clean — re-parsed VERDICT is bound to head ${headSha2}.
12579
12881
  const objectivesResolution = resolveObjectivesForPr(pr);
12580
12882
  const { objectivesVersion: resolvedObjectivesVersion, objectiveResults } = resolveObjectiveResultsForCommand(objectivesResolution, flags.get("--objectives-file"));
12581
12883
  refuseIfCleanVerdictHasNotMetObjective(isCleanVerdict(verdict), "PASS", objectiveResults);
12884
+ const resolvedRulingOrdinal = resolveRulingOrdinalForPr(pr);
12582
12885
  let comments;
12583
12886
  try {
12584
12887
  comments = fetchComments(pr);
@@ -12595,7 +12898,8 @@ Self-verification: clean — re-parsed VERDICT is bound to head ${headSha2}.
12595
12898
  secrets,
12596
12899
  secretsEvidence,
12597
12900
  objectivesVersion: resolvedObjectivesVersion,
12598
- objectiveResults
12901
+ objectiveResults,
12902
+ rulingOrdinal: resolvedRulingOrdinal
12599
12903
  };
12600
12904
  const body = renderSecurityComment(input);
12601
12905
  checkRenderedCommentOrRefuse(body, { kind: "security", verdict });
@@ -12610,7 +12914,7 @@ Self-verification: clean — re-parsed VERDICT is bound to head ${headSha2}.
12610
12914
  } catch (err) {
12611
12915
  refuseCmd(`Posted the comment (${url}) but could not re-fetch PR ${pr}'s comments to self-verify: ${err instanceof Error ? err.message : String(err)}`, "Check `gh auth status`/network and manually confirm the posted comment parses cleanly — this command could not verify it.");
12612
12916
  }
12613
- const result = verifyPostedSecurity(postComments, verdict, headSha, principalAllowlist, body, resolvedObjectivesVersion);
12917
+ const result = verifyPostedSecurity(postComments, verdict, headSha, principalAllowlist, body, resolvedObjectivesVersion, resolvedRulingOrdinal);
12614
12918
  if (!result.ok) {
12615
12919
  refuseCmd(`Posted comment ${url}, but self-verification FAILED on re-parse: ${result.reason}`, "The posted comment does not re-parse clean through the same extractCodeReviewVerdict/extractSecurityReviewVerdict functions the merge gate calls. Do not treat the post as valid — inspect the comment and this command for drift, fix, and re-run.");
12616
12920
  }
@@ -12625,244 +12929,125 @@ Self-verification: clean — re-parsed VERDICT is bound to head ${headSha}.
12625
12929
  }
12626
12930
  }
12627
12931
 
12628
- // src/lib/dispatch-task.ts
12629
- import { execFileSync as execFileSync16 } from "node:child_process";
12630
- import { createHash as createHash7 } from "node:crypto";
12631
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "node:fs";
12632
- import { tmpdir as tmpdir3 } from "node:os";
12633
- import { join as join17 } from "node:path";
12634
- var DISPATCH_AGENTS = ["claude", "codex", "gemini"];
12932
+ // src/lib/review-gate-check-name.ts
12933
+ var REVIEW_GATE_CHECK_RUN_NAME = "vinaya review gate";
12635
12934
 
12636
- class DispatchTaskError extends Error {
12637
- }
12935
+ // src/lib/dev-review-loop.ts
12638
12936
  function sh3(cmd, args) {
12639
12937
  return execFileSync16(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
12640
12938
  }
12641
- function briefHash(brief) {
12642
- return createHash7("sha256").update(`${brief}
12643
- `).digest("hex");
12644
- }
12645
- function fetchIssueComments(n) {
12939
+ function resolveHead(branch) {
12646
12940
  let out;
12647
12941
  try {
12648
- out = sh3("gh", ["issue", "view", String(n), "--json", "comments"]);
12942
+ out = sh3("git", ["ls-remote", "origin", `refs/heads/${branch}`]);
12649
12943
  } catch (err) {
12650
- throw new DispatchTaskError(`could not fetch Issue #${n}'s comments (\`gh issue view\`) to check for an existing brief — refusing rather than risking a duplicate: ${err instanceof Error ? err.message : String(err)}`);
12944
+ throw new Error(`resolveHead: \`git ls-remote origin refs/heads/${branch}\` failed: ${err instanceof Error ? err.message : String(err)}`);
12651
12945
  }
12946
+ const sha = out.split(/\s+/)[0] ?? "";
12947
+ if (!sha)
12948
+ throw new Error(`resolveHead: branch \`${branch}\` has no head on \`origin\` (empty ls-remote output).`);
12949
+ return sha;
12950
+ }
12951
+ function fetchMechanicalCheckRuns(headSha) {
12952
+ let out;
12652
12953
  try {
12653
- return JSON.parse(out).comments;
12954
+ out = sh3("gh", [
12955
+ "api",
12956
+ `repos/{owner}/{repo}/commits/${headSha}/check-runs`,
12957
+ "--paginate",
12958
+ "--jq",
12959
+ ".check_runs[] | {id, name, status, conclusion}"
12960
+ ]);
12654
12961
  } catch {
12655
- throw new DispatchTaskError(`could not parse \`gh issue view ${n} --json comments\` output.`);
12962
+ return null;
12656
12963
  }
12657
- }
12658
- function fetchIssueBody(n) {
12659
- try {
12660
- return JSON.parse(sh3("gh", ["issue", "view", String(n), "--json", "body"])).body;
12661
- } catch (err) {
12662
- throw new DispatchTaskError(`could not fetch Issue #${n}'s body (\`gh issue view\`) to resolve its suggested agent-class: ${err instanceof Error ? err.message : String(err)}`);
12964
+ const runs = out.split(`
12965
+ `).filter((l) => l.trim().length > 0).map((l) => JSON.parse(l));
12966
+ const latestByName = new Map;
12967
+ for (const run2 of runs) {
12968
+ const seen = latestByName.get(run2.name);
12969
+ if (!seen || run2.id > seen.id)
12970
+ latestByName.set(run2.name, run2);
12663
12971
  }
12972
+ return Array.from(latestByName.values()).filter((r) => r.name !== REVIEW_GATE_CHECK_RUN_NAME);
12664
12973
  }
12665
- function extractAgentClass(rawRationaleField) {
12666
- const m = /agent-class\**\s*[—–-]\s*(\w+)/i.exec(rawRationaleField);
12667
- const word = m?.[1]?.toLowerCase();
12668
- return word !== undefined && isAgentClass(word) ? word : null;
12974
+ function fetchCiConclusion(headSha) {
12975
+ const latest = fetchMechanicalCheckRuns(headSha);
12976
+ if (latest === null || latest.length === 0)
12977
+ return "pending";
12978
+ if (latest.some((r) => r.status !== "completed"))
12979
+ return "pending";
12980
+ if (latest.every((r) => r.conclusion === "success" || r.conclusion === "neutral" || r.conclusion === "skipped")) {
12981
+ return "green";
12982
+ }
12983
+ return "red";
12669
12984
  }
12670
- function resolveModelFromRationale(agent, rawRationaleField, explicitModel) {
12671
- if (explicitModel !== undefined)
12672
- return explicitModel;
12673
- const agentClass = rawRationaleField !== undefined ? extractAgentClass(rawRationaleField) : null;
12674
- if (agentClass === null)
12675
- return;
12676
- return resolveClassModel(agent, agentClass) ?? undefined;
12985
+ function fetchFailingCheckNames(headSha) {
12986
+ const latest = fetchMechanicalCheckRuns(headSha);
12987
+ if (latest === null)
12988
+ return [];
12989
+ return latest.filter((r) => r.status === "completed").filter((r) => r.conclusion !== "success" && r.conclusion !== "neutral" && r.conclusion !== "skipped").map((r) => r.name);
12677
12990
  }
12678
- function resolveModelForDispatch(agent, issue, explicitModel) {
12679
- if (explicitModel !== undefined)
12680
- return explicitModel;
12681
- const raw = parseRationaleFields(fetchIssueBody(issue)).suggestedAgentClass;
12682
- return resolveModelFromRationale(agent, raw, undefined);
12991
+ var RULING_MARKER = /^<!-- aeg:principal:ruling:\d+-\d+ -->$/;
12992
+ function contentAfterOneLine(body) {
12993
+ const idx = body.indexOf(`
12994
+ `);
12995
+ return idx === -1 ? "" : body.slice(idx + 1);
12683
12996
  }
12684
- function findExistingV1Comment(n) {
12685
- return fetchIssueComments(n).find((c) => c.body.split(`
12686
- `)[0] === AEG_BRIEF_V1_MARKER) ?? null;
12997
+ function markerComments(raw) {
12998
+ const parsed = JSON.parse(raw);
12999
+ return parsed.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null }));
12687
13000
  }
12688
- async function resolveDispatchRole() {
12689
- const dispatchModulePath = "./dispatch.js";
12690
- try {
12691
- const mod = await import(dispatchModulePath);
12692
- return typeof mod.dispatchRole === "function" ? mod.dispatchRole : null;
12693
- } catch {
12694
- return null;
12695
- }
12696
- }
12697
- function printManualDispatchInstruction(tranche, n, agent) {
12698
- process.stdout.write(`
12699
- vinaya task dispatch: \`dispatchRole\` is not available yet (apps/cli/src/lib/dispatch.ts has no such export) — the brief above is posted; start the developer yourself:
12700
-
12701
- ` + ` vinaya dispatch developer --agent ${agent} --tranche ${tranche} --task ${n}
12702
-
12703
- Once \`dispatchRole\` ships, the same \`--agent\` flag on \`task dispatch\` will start it automatically.
12704
- `);
12705
- }
12706
- async function withPromptFile(prompt2, fn) {
12707
- const dir = mkdtempSync2(join17(tmpdir3(), "vinaya-dispatch-prompt-"));
12708
- const promptFile = join17(dir, "prompt.md");
12709
- writeFileSync8(promptFile, prompt2, "utf8");
12710
- try {
12711
- return await fn(promptFile);
12712
- } finally {
12713
- rmSync5(dir, { recursive: true, force: true });
12714
- }
12715
- }
12716
- function resolveDispatchAuthorization() {
12717
- const login = currentGhLogin();
12718
- const allowlist = resolvePrincipalAllowlist(loadTrustAnchorConfig());
12719
- return { authorized: login !== null && isPrincipal(login, allowlist), login };
12720
- }
12721
- var defaultDeps2 = {
12722
- assembleAndRenderBrief,
12723
- findExistingV1Comment,
12724
- postMarkedComment,
12725
- resolveDispatchRole,
12726
- resolveDispatchAuthorization,
12727
- resolveModelForDispatch
12728
- };
12729
- async function dispatchTask(input, deps = defaultDeps2) {
12730
- const { tranche, n, agent, model } = input;
12731
- {
12732
- const { authorized, login } = deps.resolveDispatchAuthorization();
12733
- if (!authorized) {
12734
- throw new DispatchTaskError(login === null ? "could not resolve the identity `gh` is authenticated as — `task dispatch` is Principal-only and refuses rather than proceeding with an unverified actor." : `\`${login}\` is not on the Principal allowlist — \`task dispatch\` is Principal-only.`);
12735
- }
12736
- }
12737
- const result = await deps.assembleAndRenderBrief(tranche, String(n));
12738
- if (!result.ok) {
12739
- throw new DispatchTaskError(`cannot dispatch — brief render refused:
12740
- ${result.missing.map((m) => ` - ${m}`).join(`
12741
- `)}`);
12742
- }
12743
- const issue = result.issue;
12744
- const existing = deps.findExistingV1Comment(issue);
12745
- if (existing) {
12746
- throw new DispatchTaskError(`Task ${n} in tranche \`${tranche}\` is already dispatched — see ${existing.url}`);
12747
- }
12748
- let dispatchRole2 = null;
12749
- let resolvedModel;
12750
- if (agent) {
12751
- dispatchRole2 = await deps.resolveDispatchRole();
12752
- if (dispatchRole2) {
12753
- resolvedModel = deps.resolveModelForDispatch(agent, issue, model);
12754
- }
12755
- }
12756
- const hash = briefHash(result.brief);
12757
- const commentBody = `Brief hash: ${hash}
12758
- ${result.brief}`;
12759
- const url = deps.postMarkedComment("issue", String(issue), AEG_BRIEF_V1_MARKER, commentBody);
12760
- if (agent) {
12761
- if (dispatchRole2) {
12762
- await withPromptFile(result.brief, (promptFile) => dispatchRole2("developer", agent, result.brief, {
12763
- task: issue,
12764
- promptFile,
12765
- model: resolvedModel
12766
- }));
12767
- } else {
12768
- printManualDispatchInstruction(tranche, n, agent);
12769
- }
12770
- }
12771
- return { posted: true, commentUrl: url, brief: result.brief };
12772
- }
12773
-
12774
- // src/lib/dev-review-loop.ts
12775
- function sh4(cmd, args) {
12776
- return execFileSync17(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
12777
- }
12778
- function resolveHead(branch) {
12779
- let out;
12780
- try {
12781
- out = sh4("git", ["ls-remote", "origin", `refs/heads/${branch}`]);
12782
- } catch (err) {
12783
- throw new Error(`resolveHead: \`git ls-remote origin refs/heads/${branch}\` failed: ${err instanceof Error ? err.message : String(err)}`);
12784
- }
12785
- const sha = out.split(/\s+/)[0] ?? "";
12786
- if (!sha)
12787
- throw new Error(`resolveHead: branch \`${branch}\` has no head on \`origin\` (empty ls-remote output).`);
12788
- return sha;
12789
- }
12790
- function fetchCiConclusion(headSha) {
12791
- let out;
12792
- try {
12793
- out = sh4("gh", [
12794
- "api",
12795
- `repos/{owner}/{repo}/commits/${headSha}/check-runs`,
12796
- "--paginate",
12797
- "--jq",
12798
- ".check_runs[] | {id, name, status, conclusion}"
12799
- ]);
12800
- } catch {
12801
- return "pending";
12802
- }
12803
- const runs = out.split(`
12804
- `).filter((l) => l.trim().length > 0).map((l) => JSON.parse(l));
12805
- if (runs.length === 0)
12806
- return "pending";
12807
- const latestByName = new Map;
12808
- for (const run2 of runs) {
12809
- const seen = latestByName.get(run2.name);
12810
- if (!seen || run2.id > seen.id)
12811
- latestByName.set(run2.name, run2);
12812
- }
12813
- const latest = Array.from(latestByName.values());
12814
- if (latest.some((r) => r.status !== "completed"))
12815
- return "pending";
12816
- if (latest.every((r) => r.conclusion === "success" || r.conclusion === "neutral" || r.conclusion === "skipped")) {
12817
- return "green";
12818
- }
12819
- return "red";
12820
- }
12821
- var RULING_MARKER = /^<!-- aeg:principal:ruling:\d+-\d+ -->$/;
12822
- function contentAfterOneLine(body) {
12823
- const idx = body.indexOf(`
12824
- `);
12825
- return idx === -1 ? "" : body.slice(idx + 1);
12826
- }
12827
- function markerComments(raw) {
12828
- const parsed = JSON.parse(raw);
12829
- return parsed.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null }));
12830
- }
12831
- function principalAllowlist() {
12832
- return resolvePrincipalAllowlist(loadTrustAnchorConfig());
13001
+ function principalAllowlist() {
13002
+ return resolvePrincipalAllowlist(loadTrustAnchorConfig());
12833
13003
  }
12834
13004
  function filterPrincipalRulings(comments, allowlist) {
12835
13005
  return comments.filter((c) => isPrincipal(c.author, allowlist)).filter((c) => RULING_MARKER.test(c.body.split(`
12836
13006
  `)[0] ?? "")).map((c) => contentAfterOneLine(c.body).trim());
12837
13007
  }
12838
13008
  function findPrincipalFrozenBrief(comments, allowlist) {
12839
- return comments.find((c) => c.body.split(`
12840
- `)[0] === AEG_BRIEF_V1_MARKER && isPrincipal(c.author, allowlist)) ?? null;
13009
+ return resolveNewestFrozenBrief(comments, allowlist);
12841
13010
  }
12842
13011
  function fetchRulings(prNumber) {
12843
13012
  let out;
12844
13013
  try {
12845
- out = sh4("gh", ["pr", "view", String(prNumber), "--json", "comments"]);
13014
+ out = sh3("gh", ["pr", "view", String(prNumber), "--json", "comments"]);
12846
13015
  } catch (err) {
12847
13016
  throw new Error(`fetchRulings: could not fetch PR #${prNumber}'s comments: ${err instanceof Error ? err.message : String(err)}`);
12848
13017
  }
12849
13018
  return filterPrincipalRulings(markerComments(out), principalAllowlist());
12850
13019
  }
12851
- function fetchFrozenBrief(issueNumber) {
13020
+ function fetchNewestRulingOrdinal(prNumber) {
13021
+ let out;
13022
+ try {
13023
+ out = sh3("gh", ["pr", "view", String(prNumber), "--json", "comments"]);
13024
+ } catch (err) {
13025
+ throw new Error(`fetchNewestRulingOrdinal: could not fetch PR #${prNumber}'s comments: ${err instanceof Error ? err.message : String(err)}`);
13026
+ }
13027
+ return newestPrincipalRulingOrdinal(markerComments(out), principalAllowlist());
13028
+ }
13029
+ function fetchIssueComments(issueNumber, caller) {
12852
13030
  let out;
12853
13031
  try {
12854
- out = sh4("gh", ["issue", "view", String(issueNumber), "--json", "comments"]);
13032
+ out = sh3("gh", ["issue", "view", String(issueNumber), "--json", "comments"]);
12855
13033
  } catch (err) {
12856
- throw new Error(`fetchFrozenBrief: could not fetch Issue #${issueNumber}'s comments: ${err instanceof Error ? err.message : String(err)}`);
13034
+ throw new Error(`${caller}: could not fetch Issue #${issueNumber}'s comments: ${err instanceof Error ? err.message : String(err)}`);
12857
13035
  }
12858
- const found = findPrincipalFrozenBrief(markerComments(out), principalAllowlist());
13036
+ return markerComments(out);
13037
+ }
13038
+ function fetchFrozenBrief(issueNumber) {
13039
+ const found = resolveNewestFrozenBrief(fetchIssueComments(issueNumber, "fetchFrozenBrief"), principalAllowlist());
12859
13040
  if (!found) {
12860
- throw new Error(`fetchFrozenBrief: Issue #${issueNumber} carries no principal-authored, frozen \`aeg:brief:v1\` comment — \`vinaya task dispatch\` must post the brief before this loop can start.`);
13041
+ throw new Error(`fetchFrozenBrief: Issue #${issueNumber} carries no principal-authored, frozen \`aeg:brief:v<k>\` comment — \`vinaya task brief\` must post the brief before this loop can start.`);
12861
13042
  }
12862
- return contentAfterTwoLines(found.body);
13043
+ return found.content;
13044
+ }
13045
+ var NO_SOURCE_REVISION = "(none — pre-task-4 frozen brief)";
13046
+ function fetchSourceRevision(issueNumber) {
13047
+ return extractSourceRevision(fetchFrozenBrief(issueNumber)) ?? NO_SOURCE_REVISION;
12863
13048
  }
12864
13049
  function fetchIssueTitle(issueNumber) {
12865
- const out = sh4("gh", ["issue", "view", String(issueNumber), "--json", "title"]);
13050
+ const out = sh3("gh", ["issue", "view", String(issueNumber), "--json", "title"]);
12866
13051
  return JSON.parse(out).title;
12867
13052
  }
12868
13053
  function extractObjectivesSection(body) {
@@ -12876,8 +13061,106 @@ function extractObjectivesSection(body) {
12876
13061
  return (end === -1 ? rest : rest.slice(0, end)).join(`
12877
13062
  `).trim();
12878
13063
  }
12879
- function fetchIssueObjectives(issueNumber) {
12880
- return extractObjectivesSection(fetchFrozenBrief(issueNumber));
13064
+ var OBJECTIVES_EDIT_MARKER_RE = /^<!--\s*aeg:objectives:v(\d+)\s*-->$/;
13065
+ function findLatestPrincipalObjectivesEdit(comments, allowlist) {
13066
+ let best = null;
13067
+ for (const c of comments) {
13068
+ if (!isPrincipal(c.author, allowlist))
13069
+ continue;
13070
+ const m = OBJECTIVES_EDIT_MARKER_RE.exec((c.body.split(`
13071
+ `)[0] ?? "").trim());
13072
+ if (!m)
13073
+ continue;
13074
+ const k = Number.parseInt(m[1], 10);
13075
+ if (best === null || k > best.k)
13076
+ best = { k, comment: c };
13077
+ }
13078
+ return best?.comment ?? null;
13079
+ }
13080
+ function parseObjectiveLines(raw) {
13081
+ const parsed = objectivesOf(["## Objectives", "", raw].join(`
13082
+ `));
13083
+ return parsed.ok ? parsed.objectives : null;
13084
+ }
13085
+ function parseObjectivesEditComment(body) {
13086
+ const lines = body.split(`
13087
+ `);
13088
+ const previousIdx = lines.findIndex((l) => l.trim() === "Previous:");
13089
+ const nowIdx = lines.findIndex((l) => l.trim() === "Now:");
13090
+ const reasonLine = lines.find((l) => l.startsWith("Reason:"));
13091
+ const versionLine = lines.find((l) => l.startsWith("Version:"));
13092
+ if (previousIdx === -1 || nowIdx === -1 || nowIdx < previousIdx || !reasonLine || !versionLine)
13093
+ return null;
13094
+ const previousBlock = lines.slice(previousIdx + 1, nowIdx).join(`
13095
+ `).trim();
13096
+ const nowRest = lines.slice(nowIdx + 1);
13097
+ const nowEnd = nowRest.findIndex((l) => l.trim() === "");
13098
+ const nowBlock = (nowEnd === -1 ? nowRest : nowRest.slice(0, nowEnd)).join(`
13099
+ `).trim();
13100
+ const previous = parseObjectiveLines(previousBlock);
13101
+ const now = parseObjectiveLines(nowBlock);
13102
+ if (!previous || !now)
13103
+ return null;
13104
+ return {
13105
+ previous,
13106
+ now,
13107
+ reason: reasonLine.slice("Reason:".length).trim(),
13108
+ version: versionLine.slice("Version:".length).trim()
13109
+ };
13110
+ }
13111
+ function resolveIssueObjectives(issueNumber) {
13112
+ let out;
13113
+ try {
13114
+ out = sh3("gh", ["issue", "view", String(issueNumber), "--json", "comments"]);
13115
+ } catch (err) {
13116
+ throw new Error(`resolveIssueObjectives: could not fetch Issue #${issueNumber}'s comments: ${err instanceof Error ? err.message : String(err)}`);
13117
+ }
13118
+ const comments = markerComments(out);
13119
+ const allowlist = principalAllowlist();
13120
+ const latestEdit = findLatestPrincipalObjectivesEdit(comments, allowlist);
13121
+ if (latestEdit) {
13122
+ const parsed = parseObjectivesEditComment(latestEdit.body);
13123
+ if (parsed) {
13124
+ return {
13125
+ text: parsed.now.map((o) => `${o.id}. ${o.text}`).join(`
13126
+ `),
13127
+ version: parsed.version,
13128
+ edit: { previous: parsed.previous, now: parsed.now, reason: parsed.reason }
13129
+ };
13130
+ }
13131
+ }
13132
+ const brief = findPrincipalFrozenBrief(comments, allowlist);
13133
+ if (!brief) {
13134
+ throw new Error(`resolveIssueObjectives: Issue #${issueNumber} carries no principal-authored, frozen \`aeg:brief:v1\` comment — \`vinaya task dispatch\` must post the brief before this loop can start.`);
13135
+ }
13136
+ const text = extractObjectivesSection(brief.content);
13137
+ if (text.length === 0)
13138
+ return { text: "", version: null, edit: null };
13139
+ const parsedObjectives = objectivesOf(["## Objectives", "", text].join(`
13140
+ `));
13141
+ return { text, version: parsedObjectives.ok ? objectivesVersion(parsedObjectives.objectives) : null, edit: null };
13142
+ }
13143
+ function describeObjectivesEdit(issueNumber, edit) {
13144
+ const { previous, now, reason } = edit;
13145
+ const base = `vinaya issue objectives edit ${issueNumber}`;
13146
+ const sameThrough = (n) => previous.slice(0, n).every((o, i) => o.id === now[i]?.id && o.text === now[i]?.text);
13147
+ if (now.length === previous.length + 1 && sameThrough(previous.length)) {
13148
+ const added = now[now.length - 1];
13149
+ return `${base} --add "${added.text}" --reason "${reason}"`;
13150
+ }
13151
+ if (now.length === previous.length - 1) {
13152
+ const missing = previous.find((p) => !now.some((n) => n.id === p.id));
13153
+ if (missing && now.every((n, i) => n.id === previous.filter((p) => p.id !== missing.id)[i]?.id)) {
13154
+ return `${base} --drop ${missing.id} --reason "${reason}"`;
13155
+ }
13156
+ }
13157
+ if (now.length === previous.length) {
13158
+ const changed = now.find((n, i) => previous[i]?.id === n.id && previous[i]?.text !== n.text);
13159
+ if (changed && now.every((n, i) => n.id === previous[i]?.id)) {
13160
+ return `${base} --replace ${changed.id} "${changed.text}" --reason "${reason}"`;
13161
+ }
13162
+ }
13163
+ return `${base} — could not reconstruct the exact flags from the edit comment's Previous:/Now: diff; Reason: ${reason}`;
12881
13164
  }
12882
13165
  var ISSUE_TITLE_SHAPE = /^\[([^\]]+)\]\s+(\d+)\s+[—-]/;
12883
13166
  function developerBranchFor(issueNumber, fetchTitle = fetchIssueTitle) {
@@ -12890,7 +13173,7 @@ function developerBranchFor(issueNumber, fetchTitle = fetchIssueTitle) {
12890
13173
  }
12891
13174
  function findOpenPrForBranch(branch) {
12892
13175
  try {
12893
- const out = sh4("gh", ["pr", "list", "--head", branch, "--state", "open", "--json", "number,headRefName"]);
13176
+ const out = sh3("gh", ["pr", "list", "--head", branch, "--state", "open", "--json", "number,headRefName"]);
12894
13177
  const list = JSON.parse(out);
12895
13178
  const found = list.find((p) => p.headRefName === branch);
12896
13179
  return found ? { number: found.number, branch } : null;
@@ -12919,26 +13202,50 @@ function renderReviewerPrompt(facts) {
12919
13202
  `) : "(none)",
12920
13203
  "",
12921
13204
  `HEAD: ${facts.head}`,
12922
- `CI: ${facts.ciConclusion}`
13205
+ `CI: ${facts.ciConclusion}`,
13206
+ `BRIEF REVISION: ${facts.revision}`
12923
13207
  ].join(`
12924
13208
  `);
12925
13209
  }
12926
13210
  function outboxRoot() {
12927
- return join18(GLOBAL_VINAYA_HOME, "outbox");
13211
+ return join17(GLOBAL_VINAYA_HOME, "outbox");
12928
13212
  }
12929
13213
  function heldVerdictPath(root, task, round, role) {
12930
- return join18(root, "dev-review-loop", String(task), `round-${round}-${role}.md`);
13214
+ return join17(root, "dev-review-loop", String(task), `round-${round}-${role}.md`);
12931
13215
  }
12932
13216
  function writeHeldVerdict(root, task, round, role, renderedComment) {
12933
- const dir = join18(root, "dev-review-loop", String(task));
13217
+ const dir = join17(root, "dev-review-loop", String(task));
12934
13218
  mkdirSync5(dir, { recursive: true });
12935
- writeFileSync9(heldVerdictPath(root, task, round, role), renderedComment, "utf8");
13219
+ writeFileSync8(heldVerdictPath(root, task, round, role), renderedComment, "utf8");
13220
+ }
13221
+ function reviewerWorkDir(root, task, round, role, attempt = 1) {
13222
+ const suffix = attempt > 1 ? `-retry${attempt - 1}` : "";
13223
+ return join17(root, "dev-review-loop", String(task), `round-${round}-${role}-work${suffix}`);
12936
13224
  }
12937
- function reviewerWorkDir(root, task, round, role) {
12938
- return join18(root, "dev-review-loop", String(task), `round-${round}-${role}-work`);
13225
+ function missingReviewerArtifacts(workDir, hasObjectives) {
13226
+ const missing = [];
13227
+ const reportRaw = readIfExists(join17(workDir, "report.txt"));
13228
+ if (reportRaw === null)
13229
+ missing.push("report.txt");
13230
+ if (!existsSync13(join17(workDir, "findings.txt")))
13231
+ missing.push("findings.txt");
13232
+ const isEscalation = reportRaw !== null && parseReport(reportRaw).ESCALATE !== undefined;
13233
+ if (hasObjectives && !isEscalation && !existsSync13(join17(workDir, "objectives.txt")))
13234
+ missing.push("objectives.txt");
13235
+ return missing;
13236
+ }
13237
+
13238
+ class ReviewerInfrastructureFailure extends Error {
13239
+ role;
13240
+ missing;
13241
+ constructor(role, missing) {
13242
+ super(`${role}'s work directory carried no ${missing.join(" and no ")} after a fresh dispatch and one fresh retry.`);
13243
+ this.role = role;
13244
+ this.missing = missing;
13245
+ }
12939
13246
  }
12940
13247
  function forgeEffectPath(root, task, key) {
12941
- return join18(root, "dev-review-loop", String(task), `effect-${key}.json`);
13248
+ return join17(root, "dev-review-loop", String(task), `effect-${key}.json`);
12942
13249
  }
12943
13250
  function readForgeEffect(path) {
12944
13251
  const raw = readIfExists(path);
@@ -12952,7 +13259,7 @@ function readForgeEffect(path) {
12952
13259
  }
12953
13260
  function writeForgeEffect(path, record) {
12954
13261
  mkdirSync5(dirname8(path), { recursive: true });
12955
- writeFileSync9(path, JSON.stringify(record), "utf8");
13262
+ writeFileSync8(path, JSON.stringify(record), "utf8");
12956
13263
  }
12957
13264
  function postForgeEffectOnce(root, task, key, poster) {
12958
13265
  const path = forgeEffectPath(root, task, key);
@@ -12966,20 +13273,20 @@ function postForgeEffectOnce(root, task, key, poster) {
12966
13273
  return url;
12967
13274
  }
12968
13275
  function postPrComment(pr, body) {
12969
- const dir = mkdtempSync3(join18(tmpdir4(), "vinaya-dev-review-loop-comment-"));
12970
- const tmp = join18(dir, "comment.md");
12971
- writeFileSync9(tmp, body, "utf8");
13276
+ const dir = mkdtempSync2(join17(tmpdir3(), "vinaya-dev-review-loop-comment-"));
13277
+ const tmp = join17(dir, "comment.md");
13278
+ writeFileSync8(tmp, body, "utf8");
12972
13279
  try {
12973
- return execFileSync17("gh", ["pr", "comment", String(pr), "--body-file", tmp], {
13280
+ return execFileSync16("gh", ["pr", "comment", String(pr), "--body-file", tmp], {
12974
13281
  encoding: "utf8",
12975
13282
  stdio: ["ignore", "pipe", "pipe"]
12976
13283
  }).trim();
12977
13284
  } finally {
12978
- rmSync6(dir, { recursive: true, force: true });
13285
+ rmSync5(dir, { recursive: true, force: true });
12979
13286
  }
12980
13287
  }
12981
13288
  function fetchAllPrCommentBodies(pr) {
12982
- const out = sh4("gh", ["pr", "view", String(pr), "--json", "comments"]);
13289
+ const out = sh3("gh", ["pr", "view", String(pr), "--json", "comments"]);
12983
13290
  return principalBodies(markerComments(out), principalAllowlist());
12984
13291
  }
12985
13292
  function publishRound(root, input) {
@@ -13010,9 +13317,9 @@ function publishRound(root, input) {
13010
13317
  function pauseMarker(reason) {
13011
13318
  return `<!-- aeg:loop:paused:${reason} -->`;
13012
13319
  }
13013
- function renderPauseComment(prNumber, reason) {
13320
+ function renderPauseComment(prNumber, reason, detail) {
13014
13321
  return [
13015
- `The dev-review-loop paused: ${reason}.`,
13322
+ `The dev-review-loop paused: ${reason}${detail ? ` — ${detail}` : ""}.`,
13016
13323
  "",
13017
13324
  "A Principal ruling is needed before this can continue. Once one is posted on this PR, resume with:",
13018
13325
  "",
@@ -13022,16 +13329,16 @@ function renderPauseComment(prNumber, reason) {
13022
13329
  ].join(`
13023
13330
  `);
13024
13331
  }
13025
- function postPauseComment(root, task, round, head, prNumber, reason) {
13026
- postForgeEffectOnce(root, task, `pause-${round}-${head}`, () => postMarkedComment("pr", String(prNumber), pauseMarker(reason), renderPauseComment(prNumber, reason)));
13332
+ function postPauseComment(root, task, round, head, prNumber, reason, detail) {
13333
+ postForgeEffectOnce(root, task, `pause-${round}-${head}`, () => postMarkedComment("pr", String(prNumber), pauseMarker(reason), renderPauseComment(prNumber, reason, detail)));
13027
13334
  }
13028
13335
  function pauseStatePath(root, task) {
13029
- return join18(root, "dev-review-loop", String(task), "pause-state.json");
13336
+ return join17(root, "dev-review-loop", String(task), "pause-state.json");
13030
13337
  }
13031
13338
  function writePauseState(root, state) {
13032
13339
  const path = pauseStatePath(root, state.task);
13033
13340
  mkdirSync5(dirname8(path), { recursive: true });
13034
- writeFileSync9(path, JSON.stringify(state), "utf8");
13341
+ writeFileSync8(path, JSON.stringify(state), "utf8");
13035
13342
  }
13036
13343
  function readPauseState(root, task) {
13037
13344
  const raw = readIfExists(pauseStatePath(root, task));
@@ -13077,41 +13384,41 @@ function readIfExists(path) {
13077
13384
  return null;
13078
13385
  }
13079
13386
  }
13080
- function withPromptFile2(prompt2, fn) {
13081
- const dir = mkdtempSync3(join18(tmpdir4(), "vinaya-dev-review-loop-prompt-"));
13082
- const promptFile = join18(dir, "prompt.md");
13083
- writeFileSync9(promptFile, prompt2, "utf8");
13387
+ function withPromptFile(prompt2, fn) {
13388
+ const dir = mkdtempSync2(join17(tmpdir3(), "vinaya-dev-review-loop-prompt-"));
13389
+ const promptFile = join17(dir, "prompt.md");
13390
+ writeFileSync8(promptFile, prompt2, "utf8");
13084
13391
  try {
13085
13392
  return fn(promptFile);
13086
13393
  } finally {
13087
- rmSync6(dir, { recursive: true, force: true });
13394
+ rmSync5(dir, { recursive: true, force: true });
13088
13395
  }
13089
13396
  }
13090
13397
 
13091
13398
  class DevReviewLoopResumeError extends Error {
13092
13399
  }
13093
13400
  function defaultRepoRoot() {
13094
- return sh4("git", ["rev-parse", "--show-toplevel"]);
13401
+ return sh3("git", ["rev-parse", "--show-toplevel"]);
13095
13402
  }
13096
13403
  function defaultGitRevParseOriginMain() {
13097
- return sh4("git", ["rev-parse", "origin/main"]);
13404
+ return sh3("git", ["rev-parse", "origin/main"]);
13098
13405
  }
13099
13406
  function defaultGitFetch(sha) {
13100
13407
  try {
13101
- execFileSync17("git", ["fetch", "--quiet", "origin", sha], { stdio: ["ignore", "ignore", "ignore"] });
13408
+ execFileSync16("git", ["fetch", "--quiet", "origin", sha], { stdio: ["ignore", "ignore", "ignore"] });
13102
13409
  } catch {}
13103
13410
  }
13104
13411
  function defaultGitDiffShortstat(base, head) {
13105
13412
  try {
13106
- return sh4("git", ["diff", `${base}...${head}`, "--shortstat"]);
13413
+ return sh3("git", ["diff", `${base}...${head}`, "--shortstat"]);
13107
13414
  } catch {
13108
13415
  return "";
13109
13416
  }
13110
13417
  }
13111
13418
  function defaultFlushOutbox(task) {
13112
13419
  try {
13113
- const cliEntry = join18(packageRoot(import.meta.url), "src", "index.ts");
13114
- execFileSync17("bun", [cliEntry, "log", "flush", "--issue", String(task)], {
13420
+ const cliEntry = join17(packageRoot(import.meta.url), "src", "index.ts");
13421
+ execFileSync16("bun", [cliEntry, "log", "flush", "--issue", String(task)], {
13115
13422
  stdio: ["ignore", "pipe", "pipe"]
13116
13423
  });
13117
13424
  } catch (err) {
@@ -13122,16 +13429,27 @@ function defaultFlushOutbox(task) {
13122
13429
  function defaultSleep2(ms) {
13123
13430
  return new Promise((resolve3) => setTimeout(resolve3, ms));
13124
13431
  }
13125
- function defaultDeps3() {
13432
+ function gatePollEnvOverride(name, fallback) {
13433
+ const raw = process.env[name];
13434
+ if (!raw)
13435
+ return fallback;
13436
+ const n = Number(raw);
13437
+ return Number.isFinite(n) && n > 0 ? n : fallback;
13438
+ }
13439
+ function defaultDeps2() {
13126
13440
  return {
13127
13441
  dispatchRole,
13128
13442
  resolveHead,
13129
13443
  fetchCiConclusion,
13444
+ fetchFailingCheckNames,
13130
13445
  fetchRulings,
13446
+ fetchNewestRulingOrdinal,
13131
13447
  fetchFrozenBrief,
13132
- fetchIssueObjectives,
13448
+ resolveIssueObjectives,
13449
+ fetchSourceRevision,
13133
13450
  developerBranchFor: (n) => developerBranchFor(n),
13134
13451
  findOpenPrForBranch,
13452
+ readResumeRecord: (task, agent, repo) => readResumeRecord("developer", agent, repo, task),
13135
13453
  outboxRoot,
13136
13454
  repoRoot: defaultRepoRoot,
13137
13455
  gitRevParseOriginMain: defaultGitRevParseOriginMain,
@@ -13142,8 +13460,8 @@ function defaultDeps3() {
13142
13460
  now: () => Date.now(),
13143
13461
  prPollMaxAttempts: 120,
13144
13462
  prPollIntervalMs: 15000,
13145
- gatePollMaxAttempts: 120,
13146
- gatePollIntervalMs: 15000
13463
+ gatePollMaxAttempts: gatePollEnvOverride("VINAYA_DEV_REVIEW_LOOP_GATE_POLL_MAX_ATTEMPTS", 120),
13464
+ gatePollIntervalMs: gatePollEnvOverride("VINAYA_DEV_REVIEW_LOOP_GATE_POLL_INTERVAL_MS", 15000)
13147
13465
  };
13148
13466
  }
13149
13467
  function parseShortstat(stat) {
@@ -13219,8 +13537,8 @@ async function waitForOwnLoopLine(path, priorSize, runId, event, sleep, timeoutM
13219
13537
  await sleep(5);
13220
13538
  }
13221
13539
  }
13222
- function buildVerdictFromReport(role, workDir, headSha, agent, taskId, handle) {
13223
- const reportRaw = readIfExists(join18(workDir, "report.txt")) ?? "";
13540
+ function buildVerdictFromReport(role, workDir, headSha, agent, taskId, handle, objectivesVersionAtDispatch, rulingOrdinalAtDispatch) {
13541
+ const reportRaw = readIfExists(join17(workDir, "report.txt")) ?? "";
13224
13542
  const report = parseReport(reportRaw);
13225
13543
  const sessionId = handle.resumeId ?? "(unknown)";
13226
13544
  const tokensIn = handle.usage ? String(handle.usage.input) : "—";
@@ -13237,7 +13555,8 @@ function buildVerdictFromReport(role, workDir, headSha, agent, taskId, handle) {
13237
13555
  summary: report.SUMMARY ?? "(no summary given)",
13238
13556
  role: role === "reviewer" ? "review" : "security",
13239
13557
  roleLabel,
13240
- objectivesVersion: null,
13558
+ objectivesVersion: objectivesVersionAtDispatch,
13559
+ rulingOrdinal: rulingOrdinalAtDispatch,
13241
13560
  taskId: String(taskId),
13242
13561
  model: agent,
13243
13562
  tokensIn,
@@ -13247,12 +13566,13 @@ function buildVerdictFromReport(role, workDir, headSha, agent, taskId, handle) {
13247
13566
  });
13248
13567
  return { observation: { role, verdict: "ESCALATE", objectives: [], findings: [] }, rendered: rendered2 };
13249
13568
  }
13250
- const findingsRaw = readIfExists(join18(workDir, "findings.txt")) ?? "";
13569
+ const findingsRaw = readIfExists(join17(workDir, "findings.txt")) ?? "";
13251
13570
  const allowedSeverities = role === "reviewer" ? ["BLOCKER", "MAJOR", "MINOR"] : ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
13252
13571
  const findings = findingsRaw.trim() ? parseFindingsFile(findingsRaw, allowedSeverities) : [];
13253
- const objectivesRaw = readIfExists(join18(workDir, "objectives.txt"));
13572
+ const objectivesRaw = readIfExists(join17(workDir, "objectives.txt"));
13254
13573
  const objectiveResults = objectivesRaw?.trim() ? parseObjectivesFile(objectivesRaw) : [];
13255
13574
  const objectives = objectiveResults.map((o) => ({ id: o.id, met: o.status === "MET" }));
13575
+ const renderedObjectiveResults = objectivesVersionAtDispatch !== null ? objectiveResults : null;
13256
13576
  const findingObservations = findings.map((f, i) => ({ id: `F${i + 1}`, severity: f.severity, state: null }));
13257
13577
  if (role === "reviewer") {
13258
13578
  const verdict2 = deriveCodeReviewVerdict(findings);
@@ -13266,8 +13586,9 @@ function buildVerdictFromReport(role, workDir, headSha, agent, taskId, handle) {
13266
13586
  scopeEvidence: null,
13267
13587
  tests: report.TESTS ?? "(not reported)",
13268
13588
  docs: report.DOCS ?? "(not reported)",
13269
- objectivesVersion: null,
13270
- objectiveResults: null,
13589
+ objectivesVersion: objectivesVersionAtDispatch,
13590
+ objectiveResults: renderedObjectiveResults,
13591
+ rulingOrdinal: rulingOrdinalAtDispatch,
13271
13592
  taskId: String(taskId),
13272
13593
  model: agent,
13273
13594
  tokensIn,
@@ -13293,8 +13614,9 @@ function buildVerdictFromReport(role, workDir, headSha, agent, taskId, handle) {
13293
13614
  configScan: report.CONFIG_SCAN ?? "(not reported)",
13294
13615
  secrets: report.SECRETS ?? "none found",
13295
13616
  secretsEvidence: null,
13296
- objectivesVersion: null,
13297
- objectiveResults: null,
13617
+ objectivesVersion: objectivesVersionAtDispatch,
13618
+ objectiveResults: renderedObjectiveResults,
13619
+ rulingOrdinal: rulingOrdinalAtDispatch,
13298
13620
  taskId: String(taskId),
13299
13621
  model: agent,
13300
13622
  tokensIn,
@@ -13304,6 +13626,9 @@ function buildVerdictFromReport(role, workDir, headSha, agent, taskId, handle) {
13304
13626
  });
13305
13627
  return { observation: { role, verdict, objectives, findings: findingObservations }, rendered };
13306
13628
  }
13629
+ function hasObjectivesFacts(facts) {
13630
+ return facts.objectives.trim().length > 0;
13631
+ }
13307
13632
  function renderReviewerDispatchPrompt(role, facts, workDir) {
13308
13633
  const base = renderReviewerPrompt(facts);
13309
13634
  const lint = lintReviewerPrompt(base);
@@ -13314,9 +13639,12 @@ function renderReviewerDispatchPrompt(role, facts, workDir) {
13314
13639
  const instructions = [
13315
13640
  roleLine,
13316
13641
  "Review the PR at the HEAD above against the OBJECTIVES and RULINGS above.",
13317
- `Write your findings to ${join18(workDir, "findings.txt")}, one per line: SEVERITY|file:line|description`,
13642
+ `Write your findings to ${join17(workDir, "findings.txt")}, one per line: SEVERITY|file:line|description`,
13318
13643
  role === "reviewer" ? "(severities: BLOCKER, MAJOR, MINOR — leave the file empty if there are none)." : "(severities: CRITICAL, HIGH, MEDIUM, LOW — leave the file empty if there are none).",
13319
- `Write a short report to ${join18(workDir, "report.txt")} as one \`KEY: value\` line per field:`,
13644
+ ...hasObjectivesFacts(facts) ? [
13645
+ `Write one line per objective listed above to ${join17(workDir, "objectives.txt")}: O<n>|MET|<evidence> or O<n>|NOT MET|<evidence>.`
13646
+ ] : [],
13647
+ `Write a short report to ${join17(workDir, "report.txt")} as one \`KEY: value\` line per field:`,
13320
13648
  role === "reviewer" ? " BRIEF_CONFORMANCE, SPEC_CONFORMANCE, SCOPE, TESTS, DOCS" : " CONFIG_SCAN, SECRETS",
13321
13649
  "To escalate instead of casting a verdict, write only `ESCALATE: authority|strategy|product` and `SUMMARY: <text>` to report.txt."
13322
13650
  ].join(`
@@ -13330,7 +13658,7 @@ function taskFromPrBody(body) {
13330
13658
  return m ? Number(m[1]) : null;
13331
13659
  }
13332
13660
  function fetchPrBody2(pr) {
13333
- const out = sh4("gh", ["pr", "view", String(pr), "--json", "body"]);
13661
+ const out = sh3("gh", ["pr", "view", String(pr), "--json", "body"]);
13334
13662
  return JSON.parse(out).body;
13335
13663
  }
13336
13664
  function routeCompletionEvents(events, decisionType) {
@@ -13341,8 +13669,40 @@ function routeCompletionEvents(events, decisionType) {
13341
13669
  toDeferUntilPublish: events.filter((e) => e.event === "journal_finalized")
13342
13670
  };
13343
13671
  }
13672
+ var MAX_GATE_STALLED_TURNS = 2;
13673
+ function driverDecidedPauseEvents(loopId, state, round, stats) {
13674
+ const envelope = { kind: "dev_review_loop", payload: {} };
13675
+ return [
13676
+ { ...envelope, loop_id: loopId, event: "stop_condition_met", round, condition: "principal_stop" },
13677
+ { ...envelope, loop_id: loopId, event: "paused", round, reason: "principal_item" },
13678
+ {
13679
+ ...envelope,
13680
+ loop_id: loopId,
13681
+ event: "round_ended",
13682
+ round,
13683
+ base_head: stats.baseHead,
13684
+ head: stats.head,
13685
+ files_changed: stats.filesChanged,
13686
+ insertions: stats.insertions,
13687
+ deletions: stats.deletions,
13688
+ wall_ms: stats.wallMs,
13689
+ outcome: "changes_requested"
13690
+ },
13691
+ {
13692
+ ...envelope,
13693
+ loop_id: loopId,
13694
+ event: "journal_finalized",
13695
+ rounds: state.rounds.length + 1,
13696
+ total_wall_ms: state.totalWallMs + stats.wallMs,
13697
+ time_to_green_ms: null,
13698
+ files_changed_total: state.totalFilesChanged + stats.filesChanged,
13699
+ final_head: stats.head,
13700
+ result: "stopped"
13701
+ }
13702
+ ];
13703
+ }
13344
13704
  async function devReviewLoop(input, deps = {}) {
13345
- const d = { ...defaultDeps3(), ...deps };
13705
+ const d = { ...defaultDeps2(), ...deps };
13346
13706
  const root = d.outboxRoot();
13347
13707
  let task;
13348
13708
  let branch;
@@ -13383,7 +13743,7 @@ async function devReviewLoop(input, deps = {}) {
13383
13743
  process.env.VINAYA_TASK = String(task);
13384
13744
  const repo = await resolveRepo().catch(() => null);
13385
13745
  const repoRoot2 = d.repoRoot();
13386
- const confidenceFilePath = join18(repoRoot2, ".worktrees", branch, CONFIDENCE_FILE_NAME);
13746
+ const confidenceFilePath = join17(repoRoot2, ".worktrees", branch, CONFIDENCE_FILE_NAME);
13387
13747
  const loopOutboxPath = outboxPathFor({ outboxRoot: () => root }, repo, task);
13388
13748
  async function logEvents(events) {
13389
13749
  for (const e of events) {
@@ -13404,9 +13764,12 @@ async function devReviewLoop(input, deps = {}) {
13404
13764
  let devDispatchSucceededBefore = false;
13405
13765
  let lastReviewContext = null;
13406
13766
  let resumedDispatch = resumeFrom !== null;
13767
+ let lastFailingChecks = [];
13768
+ let pendingGateRedRetry = false;
13769
+ let gateStalledStreak = 0;
13407
13770
  async function dispatchDeveloper(prompt2, roundNum) {
13408
13771
  const isResume = devResumeId !== null;
13409
- const handle = await withPromptFile2(prompt2, (promptFile) => d.dispatchRole("developer", input.agent, prompt2, {
13772
+ const handle = await withPromptFile(prompt2, (promptFile) => d.dispatchRole("developer", input.agent, prompt2, {
13410
13773
  task,
13411
13774
  round: roundNum,
13412
13775
  resumeId: devResumeId ?? undefined,
@@ -13421,15 +13784,23 @@ async function devReviewLoop(input, deps = {}) {
13421
13784
  return handle;
13422
13785
  }
13423
13786
  async function dispatchReviewer(role, roundNum, facts) {
13424
- const workDir = reviewerWorkDir(root, task, roundNum, role);
13425
- mkdirSync5(workDir, { recursive: true });
13787
+ const hasObjectives = hasObjectivesFacts(facts);
13426
13788
  const dispatchRoleName = role === "reviewer" ? "code-reviewer" : "security";
13427
- const prompt2 = renderReviewerDispatchPrompt(role, facts, workDir);
13428
- const handle = await withPromptFile2(prompt2, (promptFile) => d.dispatchRole(dispatchRoleName, input.agent, prompt2, { task, round: roundNum, promptFile }));
13429
- await assertDispatchOrEscalate(handle, input.agent, false, false);
13430
- const parsed = buildVerdictFromReport(role, workDir, facts.head, input.agent, task, handle);
13431
- writeHeldVerdict(root, task, roundNum, role, parsed.rendered);
13432
- return parsed;
13789
+ let lastMissing = [];
13790
+ for (let attempt = 1;attempt <= 2; attempt++) {
13791
+ const workDir = reviewerWorkDir(root, task, roundNum, role, attempt);
13792
+ mkdirSync5(workDir, { recursive: true });
13793
+ const prompt2 = renderReviewerDispatchPrompt(role, facts, workDir);
13794
+ const handle = await withPromptFile(prompt2, (promptFile) => d.dispatchRole(dispatchRoleName, input.agent, prompt2, { task, round: roundNum, promptFile }));
13795
+ await assertDispatchOrEscalate(handle, input.agent, false, false);
13796
+ const missing = missingReviewerArtifacts(workDir, hasObjectives);
13797
+ if (missing.length > 0) {
13798
+ lastMissing = missing;
13799
+ continue;
13800
+ }
13801
+ return buildVerdictFromReport(role, workDir, facts.head, input.agent, task, handle, facts.objectivesVersion, facts.rulingOrdinal);
13802
+ }
13803
+ throw new ReviewerInfrastructureFailure(role, lastMissing);
13433
13804
  }
13434
13805
  function computeStats(head, roundStartMs2) {
13435
13806
  const baseHead = d.gitRevParseOriginMain();
@@ -13443,7 +13814,13 @@ async function devReviewLoop(input, deps = {}) {
13443
13814
  const c = d.fetchCiConclusion(head);
13444
13815
  return c === "pending" ? null : c;
13445
13816
  }, d.gatePollMaxAttempts, d.gatePollIntervalMs, d.sleep, `devReviewLoop: CI never resolved off 'pending' for head ${head} within the poll budget.`).catch(() => "red");
13446
- return { green: conclusion === "green", stats: computeStats(head, roundStartMs2), ciConclusion: conclusion };
13817
+ const failingChecks = conclusion === "red" ? d.fetchFailingCheckNames(head) : [];
13818
+ return {
13819
+ green: conclusion === "green",
13820
+ stats: computeStats(head, roundStartMs2),
13821
+ ciConclusion: conclusion,
13822
+ failingChecks
13823
+ };
13447
13824
  }
13448
13825
  function readAndClearConfidence() {
13449
13826
  const content = readIfExists(confidenceFilePath);
@@ -13458,35 +13835,88 @@ async function devReviewLoop(input, deps = {}) {
13458
13835
  lastReviewContext = rulings.map((r, i) => `${i + 1}. ${r}`).join(`
13459
13836
  `);
13460
13837
  } else {
13461
- const brief = d.fetchFrozenBrief(task);
13462
- await dispatchDeveloper(brief, round);
13463
- prNumber = await pollUntil(() => d.findOpenPrForBranch(branch), d.prPollMaxAttempts, d.prPollIntervalMs, d.sleep, `devReviewLoop: no open PR appeared for branch \`${branch}\` within the poll budget.`).then((pr) => pr.number);
13838
+ const existingPr = d.findOpenPrForBranch(branch);
13839
+ if (existingPr) {
13840
+ prNumber = existingPr.number;
13841
+ const rec = d.readResumeRecord(task, input.agent, repo);
13842
+ if (rec)
13843
+ devResumeId = rec.resumeId;
13844
+ } else {
13845
+ let branchExists2 = true;
13846
+ try {
13847
+ d.resolveHead(branch);
13848
+ } catch {
13849
+ branchExists2 = false;
13850
+ }
13851
+ if (branchExists2) {
13852
+ const rec = d.readResumeRecord(task, input.agent, repo);
13853
+ if (rec)
13854
+ devResumeId = rec.resumeId;
13855
+ const openPrPrompt = [
13856
+ "This branch already exists with no open pull request for it.",
13857
+ "Open the pull request through the validated path per aeg-root/roles/developer.md:",
13858
+ '`bun apps/cli/src/index.ts pr create --body-file <path> --title "<title>"`.'
13859
+ ].join(`
13860
+
13861
+ `);
13862
+ await dispatchDeveloper(openPrPrompt, round);
13863
+ prNumber = await pollUntil(() => d.findOpenPrForBranch(branch), d.prPollMaxAttempts, d.prPollIntervalMs, d.sleep, `devReviewLoop: no open PR appeared for branch \`${branch}\` within the poll budget after resuming to open one.`).then((pr) => pr.number);
13864
+ } else {
13865
+ const brief = d.fetchFrozenBrief(task);
13866
+ await dispatchDeveloper(brief, round);
13867
+ prNumber = await pollUntil(() => d.findOpenPrForBranch(branch), d.prPollMaxAttempts, d.prPollIntervalMs, d.sleep, `devReviewLoop: no open PR appeared for branch \`${branch}\` within the poll budget.`).then((pr) => pr.number);
13868
+ }
13869
+ }
13464
13870
  }
13465
13871
  let decision = { type: "dispatch_developer" };
13466
13872
  let firstPass = !resumeFrom;
13467
13873
  let pendingCompletionEvents = [];
13468
13874
  while (true) {
13469
13875
  if (decision.type === "dispatch_developer") {
13876
+ const isGateRedRetry = pendingGateRedRetry;
13470
13877
  if (!firstPass) {
13471
13878
  const prompt2 = [
13472
13879
  resumedDispatch ? `Principal ruling on this pause:
13473
13880
 
13474
13881
  ${lastReviewContext}
13475
- ` : lastReviewContext ? `Round ${round} review findings:
13882
+ ` : isGateRedRetry ? `CI is red on the last head. Failing check-run(s): ${lastFailingChecks.length > 0 ? lastFailingChecks.join(", ") : "(unknown)"}. Fix and push.` : `Round ${round} review findings:
13476
13883
 
13477
13884
  ${lastReviewContext}
13478
- ` : "CI was red on the last head — fix and push.",
13885
+ `,
13479
13886
  "Address the findings above per aeg-root/roles/developer.md. Push fixes as new commits on the SAME branch; do not open a new PR.",
13480
13887
  round >= 2 ? CONFIDENCE_PROMPT_LINE : ""
13481
13888
  ].filter(Boolean).join(`
13482
13889
 
13483
13890
  `);
13891
+ const headBeforeDispatch = isGateRedRetry ? d.resolveHead(branch) : null;
13484
13892
  roundStartMs = d.now();
13485
13893
  await dispatchDeveloper(prompt2, round);
13486
13894
  resumedDispatch = false;
13895
+ if (headBeforeDispatch !== null) {
13896
+ const changedHead = await pollUntil(() => {
13897
+ const h = d.resolveHead(branch);
13898
+ return h !== headBeforeDispatch ? h : null;
13899
+ }, d.gatePollMaxAttempts, d.gatePollIntervalMs, d.sleep, "devReviewLoop: head-change wait timed out").catch(() => null);
13900
+ if (changedHead === null) {
13901
+ gateStalledStreak += 1;
13902
+ const stats = computeStats(headBeforeDispatch, roundStartMs);
13903
+ const detail = `head ${headBeforeDispatch} unchanged after dispatch; failing check-run(s): ${lastFailingChecks.length > 0 ? lastFailingChecks.join(", ") : "(unknown)"}`;
13904
+ if (gateStalledStreak < MAX_GATE_STALLED_TURNS) {
13905
+ d.flushOutbox(task);
13906
+ continue;
13907
+ }
13908
+ await logEvents(driverDecidedPauseEvents(config.loopId, state, round, stats));
13909
+ decision = { type: "pause", reason: "infrastructure", detail };
13910
+ d.flushOutbox(task);
13911
+ continue;
13912
+ }
13913
+ }
13487
13914
  }
13488
13915
  firstPass = false;
13489
13916
  const gate = await waitForGreenGate(roundStartMs);
13917
+ lastFailingChecks = gate.failingChecks;
13918
+ pendingGateRedRetry = !gate.green;
13919
+ gateStalledStreak = 0;
13490
13920
  const confidence = round >= 2 && gate.green ? readAndClearConfidence() : undefined;
13491
13921
  const obs = { kind: "gate", round, green: gate.green, confidence, stats: gate.stats };
13492
13922
  const result = assessRound(state, obs);
@@ -13506,33 +13936,78 @@ ${CONFIDENCE_PROMPT_LINE}`;
13506
13936
  const result = assessRound(state, obs);
13507
13937
  state = result.state;
13508
13938
  decision = result.decision;
13939
+ pendingGateRedRetry = false;
13940
+ gateStalledStreak = 0;
13509
13941
  await logEvents(result.events);
13510
13942
  d.flushOutbox(task);
13511
13943
  } else if (decision.type === "dispatch_reviewers") {
13512
13944
  const head = d.resolveHead(branch);
13513
13945
  const ciConclusion = d.fetchCiConclusion(head);
13514
- const objectives = d.fetchIssueObjectives(task);
13946
+ const resolvedObjectives = d.resolveIssueObjectives(task);
13515
13947
  const rulings = d.fetchRulings(prNumber);
13516
- const facts = { objectives, rulings, head, ciConclusion };
13517
- const [reviewer, security] = await Promise.all([
13518
- dispatchReviewer("reviewer", round, facts),
13519
- dispatchReviewer("security", round, facts)
13520
- ]);
13521
- lastReviewContext = `${reviewer.rendered}
13948
+ const rulingOrdinal = d.fetchNewestRulingOrdinal(prNumber);
13949
+ const revision = d.fetchSourceRevision(task);
13950
+ const facts = {
13951
+ objectives: resolvedObjectives.text,
13952
+ objectivesVersion: resolvedObjectives.version,
13953
+ rulings,
13954
+ rulingOrdinal,
13955
+ head,
13956
+ ciConclusion,
13957
+ revision
13958
+ };
13959
+ let verdicts = null;
13960
+ try {
13961
+ verdicts = await Promise.all([
13962
+ dispatchReviewer("reviewer", round, facts),
13963
+ dispatchReviewer("security", round, facts)
13964
+ ]);
13965
+ } catch (err) {
13966
+ if (!(err instanceof ReviewerInfrastructureFailure))
13967
+ throw err;
13968
+ const stats = computeStats(head, roundStartMs);
13969
+ await logEvents(driverDecidedPauseEvents(config.loopId, state, round, stats));
13970
+ decision = { type: "pause", reason: "infrastructure", detail: err.message };
13971
+ }
13972
+ if (verdicts) {
13973
+ const reassessedObjectives = d.resolveIssueObjectives(task);
13974
+ const reassessedRulingOrdinal = d.fetchNewestRulingOrdinal(prNumber);
13975
+ if (reassessedObjectives.version !== facts.objectivesVersion) {
13976
+ const command = reassessedObjectives.edit ? describeObjectivesEdit(task, reassessedObjectives.edit) : `vinaya issue objectives edit ${task} ... (edit comment not found on re-read)`;
13977
+ const detail = `objectives moved from ${facts.objectivesVersion ?? "none"} to ${reassessedObjectives.version ?? "none"} between reviewer dispatch and assessment — superseded by \`${command}\``;
13978
+ const stats = computeStats(head, roundStartMs);
13979
+ await logEvents(driverDecidedPauseEvents(config.loopId, state, round, stats));
13980
+ decision = { type: "pause", reason: "objectives_changed", detail };
13981
+ d.flushOutbox(task);
13982
+ } else if (reassessedRulingOrdinal !== facts.rulingOrdinal) {
13983
+ const detail = `a new ruling landed between reviewer dispatch and assessment — ruling ordinal moved from ${facts.rulingOrdinal} to ${reassessedRulingOrdinal} — superseded by ruling ${prNumber}-${reassessedRulingOrdinal}`;
13984
+ const stats = computeStats(head, roundStartMs);
13985
+ await logEvents(driverDecidedPauseEvents(config.loopId, state, round, stats));
13986
+ decision = { type: "pause", reason: "ruling_posted", detail };
13987
+ d.flushOutbox(task);
13988
+ } else {
13989
+ const [reviewer, security] = verdicts;
13990
+ writeHeldVerdict(root, task, round, "reviewer", reviewer.rendered);
13991
+ writeHeldVerdict(root, task, round, "security", security.rendered);
13992
+ lastReviewContext = `${reviewer.rendered}
13522
13993
 
13523
13994
  ---
13524
13995
 
13525
13996
  ${security.rendered}`;
13526
- const obs = { kind: "verdicts", round, verdicts: [reviewer.observation, security.observation] };
13527
- const result = assessRound(state, obs);
13528
- state = result.state;
13529
- decision = result.decision;
13530
- const routed = routeCompletionEvents(result.events, decision.type);
13531
- pendingCompletionEvents = routed.toDeferUntilPublish;
13532
- await logEvents(routed.toLogNow);
13533
- d.flushOutbox(task);
13534
- if (decision.type === "dispatch_developer")
13535
- round += 1;
13997
+ const obs = { kind: "verdicts", round, verdicts: [reviewer.observation, security.observation] };
13998
+ const result = assessRound(state, obs);
13999
+ state = result.state;
14000
+ decision = result.decision;
14001
+ const routed = routeCompletionEvents(result.events, decision.type);
14002
+ pendingCompletionEvents = routed.toDeferUntilPublish;
14003
+ await logEvents(routed.toLogNow);
14004
+ d.flushOutbox(task);
14005
+ if (decision.type === "dispatch_developer")
14006
+ round += 1;
14007
+ }
14008
+ } else {
14009
+ d.flushOutbox(task);
14010
+ }
13536
14011
  }
13537
14012
  if (decision.type === "publish") {
13538
14013
  publishRound(root, {
@@ -13555,9 +14030,10 @@ ${security.rendered}`;
13555
14030
  branch,
13556
14031
  prNumber,
13557
14032
  reason: decision.reason,
14033
+ detail: decision.detail,
13558
14034
  pausedAt: new Date().toISOString()
13559
14035
  });
13560
- postPauseComment(root, task, round, pauseHead, prNumber, decision.reason);
14036
+ postPauseComment(root, task, round, pauseHead, prNumber, decision.reason, decision.detail);
13561
14037
  d.flushOutbox(task);
13562
14038
  return { finalDecision: decision, prNumber, task };
13563
14039
  }
@@ -13617,10 +14093,10 @@ async function devReviewLoopCommand(args) {
13617
14093
  if (parsed.json) {
13618
14094
  printJson({ finalDecision: result.finalDecision, prNumber: result.prNumber, task: result.task });
13619
14095
  } else if (result.finalDecision.type === "publish") {
13620
- process.stdout.write(`vinaya dev-review-loop: task ${result.task}, PR #${result.prNumber} — publish
14096
+ process.stdout.write(`${colourLoopLine(`vinaya dev-review-loop: task ${result.task}, PR #${result.prNumber} — publish`, process.stdout)}
13621
14097
  `);
13622
14098
  } else if (result.finalDecision.type === "pause") {
13623
- process.stdout.write(`vinaya dev-review-loop: task ${result.task}, PR #${result.prNumber} — paused (${result.finalDecision.reason})
14099
+ process.stdout.write(`${colourLoopLine(`vinaya dev-review-loop: task ${result.task}, PR #${result.prNumber} — paused (${result.finalDecision.reason})`, process.stdout)}
13624
14100
  `);
13625
14101
  }
13626
14102
  if (result.finalDecision.type === "pause")
@@ -13631,10 +14107,10 @@ async function devReviewLoopCommand(args) {
13631
14107
  import { readFileSync as readFileSync19 } from "node:fs";
13632
14108
 
13633
14109
  // src/commands/log.ts
13634
- import { execFileSync as execFileSync18 } from "node:child_process";
13635
- import { lstatSync, readFileSync as readFileSync18, statSync as statSync2, writeFileSync as writeFileSync10, rmSync as rmSync7 } from "node:fs";
13636
- import { homedir as homedir4, tmpdir as tmpdir5 } from "node:os";
13637
- import { join as join19 } from "node:path";
14110
+ import { execFileSync as execFileSync17 } from "node:child_process";
14111
+ import { lstatSync, readFileSync as readFileSync18, statSync as statSync2, writeFileSync as writeFileSync9, rmSync as rmSync6 } from "node:fs";
14112
+ import { homedir as homedir4, tmpdir as tmpdir4 } from "node:os";
14113
+ import { join as join18 } from "node:path";
13638
14114
  var FORGE_COMMENT_MAX_CHARS = 65536;
13639
14115
  function isEnoent2(err) {
13640
14116
  return typeof err === "object" && err !== null && err.code === "ENOENT";
@@ -13794,7 +14270,7 @@ function planFlush(lines, maxChars) {
13794
14270
  }
13795
14271
  function gh2(args) {
13796
14272
  try {
13797
- return execFileSync18("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
14273
+ return execFileSync17("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
13798
14274
  } catch (err) {
13799
14275
  const stderr = err.stderr;
13800
14276
  throw new Error(String(stderr ?? err.message).trim() || "gh command failed");
@@ -13809,8 +14285,8 @@ function issueFromPr(prNumber) {
13809
14285
  return issue;
13810
14286
  }
13811
14287
  function postChunk(op, targetId, body, index) {
13812
- const tmp = join19(tmpdir5(), `vinaya-log-flush-${process.pid}-${Date.now()}-${index}.md`);
13813
- writeFileSync10(tmp, body, { flag: "wx" });
14288
+ const tmp = join18(tmpdir4(), `vinaya-log-flush-${process.pid}-${Date.now()}-${index}.md`);
14289
+ writeFileSync9(tmp, body, { flag: "wx" });
13814
14290
  try {
13815
14291
  const out = op === "pr.comment" ? gh2(["pr", "comment", targetId, "--body-file", tmp]) : gh2(["issue", "comment", targetId, "--body-file", tmp]);
13816
14292
  const match = /#issuecomment-(\d+)/.exec(out);
@@ -13820,7 +14296,7 @@ function postChunk(op, targetId, body, index) {
13820
14296
  `);
13821
14297
  return `unparsed:${out.slice(0, 200)}`;
13822
14298
  } finally {
13823
- rmSync7(tmp, { force: true });
14299
+ rmSync6(tmp, { force: true });
13824
14300
  }
13825
14301
  }
13826
14302
  function parseArgs3(args) {
@@ -13853,7 +14329,7 @@ async function logFlushCommand(args) {
13853
14329
  const forgeTargetId = parsed.pr !== undefined ? String(parsed.pr) : String(issueNumber);
13854
14330
  const resolved = await resolveRepo();
13855
14331
  const repo = resolved && isSafeRepoSegment3(resolved.owner) && isSafeRepoSegment3(resolved.repo) ? resolved : null;
13856
- const outboxRoot2 = () => join19(GLOBAL_VINAYA_HOME, "outbox");
14332
+ const outboxRoot2 = () => join18(GLOBAL_VINAYA_HOME, "outbox");
13857
14333
  const path = outboxPathFor2({ outboxRoot: outboxRoot2, repo }, { issue: issueNumber });
13858
14334
  let lstat;
13859
14335
  try {
@@ -13933,7 +14409,7 @@ async function logFlushCommand(args) {
13933
14409
  `).join("");
13934
14410
  const liveNow = readFileSync18(path);
13935
14411
  const tail = liveNow.subarray(Math.min(startOffset, liveNow.byteLength));
13936
- writeFileSync10(path, Buffer.concat([Buffer.from(unposted, "utf8"), tail]));
14412
+ writeFileSync9(path, Buffer.concat([Buffer.from(unposted, "utf8"), tail]));
13937
14413
  if (!finalLanded) {
13938
14414
  emitCheckError({
13939
14415
  schema: CHECK_SCHEMA_VERSION,
@@ -13955,6 +14431,7 @@ async function logFlushCommand(args) {
13955
14431
  }
13956
14432
 
13957
14433
  // src/commands/dispatch.ts
14434
+ var KNOWN_FLAGS = ["--agent", "--model", "--prompt-file", "--task", "--pr", "--round", "--resume", "--json"];
13958
14435
  function parseArgs4(args) {
13959
14436
  const role = args[0];
13960
14437
  let agent;
@@ -13965,6 +14442,7 @@ function parseArgs4(args) {
13965
14442
  let round;
13966
14443
  let resume;
13967
14444
  let json = false;
14445
+ const unknown = [];
13968
14446
  for (let i = 1;i < args.length; i++) {
13969
14447
  const a = args[i];
13970
14448
  if (a === "--agent")
@@ -13983,11 +14461,18 @@ function parseArgs4(args) {
13983
14461
  resume = args[++i];
13984
14462
  else if (a === "--json")
13985
14463
  json = true;
14464
+ else if (a !== undefined)
14465
+ unknown.push(a);
13986
14466
  }
13987
- return { role, agent, model, promptFile, task, pr, round, resume, json };
14467
+ return { role, agent, model, promptFile, task, pr, round, resume, json, unknown };
13988
14468
  }
13989
14469
  async function dispatchCommand(args) {
13990
14470
  const parsed = parseArgs4(args);
14471
+ if (parsed.unknown.length > 0) {
14472
+ process.stderr.write(`vinaya dispatch: unrecognized flag${parsed.unknown.length > 1 ? "s" : ""} ${parsed.unknown.map((f) => `'${f}'`).join(", ")} — expected one of ${KNOWN_FLAGS.join(", ")}
14473
+ `);
14474
+ process.exit(1);
14475
+ }
13991
14476
  if (!parsed.role || !ROLE_VALUES.includes(parsed.role)) {
13992
14477
  process.stderr.write(`vinaya dispatch: invalid role '${parsed.role ?? "(none)"}' — expected one of ${ROLE_VALUES.join(", ")}
13993
14478
  `);
@@ -14060,13 +14545,13 @@ async function dispatchCommand(args) {
14060
14545
  }
14061
14546
 
14062
14547
  // src/commands/doctor.ts
14063
- import { execFileSync as execFileSync19 } from "node:child_process";
14064
- import { existsSync as existsSync15, readdirSync as readdirSync8, readFileSync as readFileSync23, statSync as statSync3 } from "node:fs";
14065
- import { delimiter, join as join22 } from "node:path";
14548
+ import { execFileSync as execFileSync18 } from "node:child_process";
14549
+ import { existsSync as existsSync17, readdirSync as readdirSync8, readFileSync as readFileSync23, statSync as statSync3 } from "node:fs";
14550
+ import { delimiter, join as join21 } from "node:path";
14066
14551
 
14067
14552
  // src/lib/self-host.ts
14068
- import { readdirSync as readdirSync7, readFileSync as readFileSync20, realpathSync as realpathSync2 } from "node:fs";
14069
- import { join as join20, sep as sep3 } from "node:path";
14553
+ import { existsSync as existsSync14, readdirSync as readdirSync7, readFileSync as readFileSync20, realpathSync as realpathSync2 } from "node:fs";
14554
+ import { dirname as dirname9, join as join19, sep as sep3 } from "node:path";
14070
14555
  var VINAYA_PACKAGE_NAME = "@attalabs/vinaya";
14071
14556
  var DEFAULT_BIN = "dist/index.js";
14072
14557
  var MAX_CANDIDATE_DIRS = 2000;
@@ -14124,7 +14609,7 @@ function expandPattern(repoRoot2, pattern) {
14124
14609
  for (const dir of dirs) {
14125
14610
  if (segment.includes("*")) {
14126
14611
  const matches = segmentMatcher(segment);
14127
- for (const name of childDirs(join20(repoRoot2, dir))) {
14612
+ for (const name of childDirs(join19(repoRoot2, dir))) {
14128
14613
  if (matches(name))
14129
14614
  next.push(dir ? `${dir}/${name}` : name);
14130
14615
  }
@@ -14139,7 +14624,7 @@ function expandPattern(repoRoot2, pattern) {
14139
14624
  function resolvesInsideRepo(repoRoot2, rel) {
14140
14625
  try {
14141
14626
  const root = realpathSync2(repoRoot2);
14142
- const target = realpathSync2(join20(repoRoot2, rel));
14627
+ const target = realpathSync2(join19(repoRoot2, rel));
14143
14628
  return target === root || target.startsWith(`${root}${sep3}`);
14144
14629
  } catch {
14145
14630
  return false;
@@ -14157,14 +14642,14 @@ function binPath(pkg) {
14157
14642
  return DEFAULT_BIN;
14158
14643
  }
14159
14644
  function detectVendoredVinaya(repoRoot2) {
14160
- const patterns = workspacePatterns(readPackageJson(join20(repoRoot2, "package.json")));
14645
+ const patterns = workspacePatterns(readPackageJson(join19(repoRoot2, "package.json")));
14161
14646
  const seen = new Set;
14162
14647
  for (const pattern of patterns) {
14163
14648
  for (const dir of expandPattern(repoRoot2, pattern)) {
14164
14649
  if (seen.has(dir))
14165
14650
  continue;
14166
14651
  seen.add(dir);
14167
- const member = readPackageJson(join20(repoRoot2, dir, "package.json"));
14652
+ const member = readPackageJson(join19(repoRoot2, dir, "package.json"));
14168
14653
  if (member?.name === VINAYA_PACKAGE_NAME) {
14169
14654
  const bin2 = `${dir}/${binPath(member)}`;
14170
14655
  if (!isSafeRelPath(dir) || !isSafeRelPath(bin2))
@@ -14177,16 +14662,29 @@ function detectVendoredVinaya(repoRoot2) {
14177
14662
  }
14178
14663
  return null;
14179
14664
  }
14665
+ function resolveAuthorRepoSourceEntry(ownPackageRoot, cwd = process.cwd()) {
14666
+ if (!ownPackageRoot.split(sep3).includes("node_modules"))
14667
+ return null;
14668
+ const info = resolveDoctrineRootInfo(undefined, cwd);
14669
+ if (info?.source !== "tree")
14670
+ return null;
14671
+ const toplevel = dirname9(info.root);
14672
+ const cliPkg = readPackageJson(join19(toplevel, "apps", "cli", "package.json"));
14673
+ if (cliPkg?.name !== VINAYA_PACKAGE_NAME)
14674
+ return null;
14675
+ const sourceEntry = join19(toplevel, "apps", "cli", "src", "index.ts");
14676
+ return existsSync14(sourceEntry) ? sourceEntry : null;
14677
+ }
14180
14678
 
14181
14679
  // src/lib/env-lint.ts
14182
- import { existsSync as existsSync13, readFileSync as readFileSync21 } from "node:fs";
14680
+ import { existsSync as existsSync15, readFileSync as readFileSync21 } from "node:fs";
14183
14681
  var ENV_READ_PATTERN = /\b(?:process\.env|Bun\.env|Deno\.env)\b/;
14184
14682
  function checksMissingEnvDeclaration(specs) {
14185
14683
  const names = [];
14186
14684
  for (const spec of specs) {
14187
14685
  if (spec.env)
14188
14686
  continue;
14189
- if (!existsSync13(spec.run))
14687
+ if (!existsSync15(spec.run))
14190
14688
  continue;
14191
14689
  let source;
14192
14690
  try {
@@ -14204,8 +14702,8 @@ function envDeclarationWarning(name) {
14204
14702
  }
14205
14703
 
14206
14704
  // src/lib/registry-write.ts
14207
- import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync22, writeFileSync as writeFileSync11 } from "node:fs";
14208
- import { dirname as dirname9, join as join21 } from "node:path";
14705
+ import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
14706
+ import { dirname as dirname10, join as join20 } from "node:path";
14209
14707
  var PROJECTS_REGISTRY_PATH = ".vinaya/projects.md";
14210
14708
  var CONFIG_FILE_PATH = "vinaya.config.json";
14211
14709
  function rowLine(name, path, specsPath) {
@@ -14271,9 +14769,9 @@ ${newRow}
14271
14769
  `);
14272
14770
  }
14273
14771
  function planRegistryRow(repoRoot2, name, path, specsPath) {
14274
- const abs2 = join21(repoRoot2, PROJECTS_REGISTRY_PATH);
14772
+ const abs2 = join20(repoRoot2, PROJECTS_REGISTRY_PATH);
14275
14773
  const line = rowLine(name, path, specsPath);
14276
- if (!existsSync14(abs2)) {
14774
+ if (!existsSync16(abs2)) {
14277
14775
  return { action: "create-host", rowLine: line, path: PROJECTS_REGISTRY_PATH };
14278
14776
  }
14279
14777
  const existing = readFileSync22(abs2, "utf-8");
@@ -14281,13 +14779,13 @@ function planRegistryRow(repoRoot2, name, path, specsPath) {
14281
14779
  return { action: already ? "skip-present" : "append-row", rowLine: line, path: PROJECTS_REGISTRY_PATH };
14282
14780
  }
14283
14781
  function applyRegistryRow(repoRoot2, plan, name, path, specsPath) {
14284
- const abs2 = join21(repoRoot2, PROJECTS_REGISTRY_PATH);
14782
+ const abs2 = join20(repoRoot2, PROJECTS_REGISTRY_PATH);
14285
14783
  if (plan.action === "create-host") {
14286
- mkdirSync6(dirname9(abs2), { recursive: true });
14287
- writeFileSync11(abs2, freshProjectsRegistry(name, path, specsPath), "utf-8");
14784
+ mkdirSync6(dirname10(abs2), { recursive: true });
14785
+ writeFileSync10(abs2, freshProjectsRegistry(name, path, specsPath), "utf-8");
14288
14786
  } else if (plan.action === "append-row") {
14289
14787
  const existing = readFileSync22(abs2, "utf-8");
14290
- writeFileSync11(abs2, appendRegistryRow(existing, name, path, specsPath), "utf-8");
14788
+ writeFileSync10(abs2, appendRegistryRow(existing, name, path, specsPath), "utf-8");
14291
14789
  }
14292
14790
  }
14293
14791
  function renderRegistryRowDiffLine(plan) {
@@ -14300,8 +14798,8 @@ function renderRegistryRowDiffLine(plan) {
14300
14798
  ${plan.rowLine}`;
14301
14799
  }
14302
14800
  function readProjectEntries(repoRoot2) {
14303
- const abs2 = join21(repoRoot2, CONFIG_FILE_PATH);
14304
- if (!existsSync14(abs2))
14801
+ const abs2 = join20(repoRoot2, CONFIG_FILE_PATH);
14802
+ if (!existsSync16(abs2))
14305
14803
  return [];
14306
14804
  try {
14307
14805
  return VinayaConfigSchema.parse(JSON.parse(readFileSync22(abs2, "utf-8"))).projects ?? [];
@@ -14316,10 +14814,10 @@ function planConfigProjectEntry(repoRoot2, entry2) {
14316
14814
  function applyConfigProjectEntry(repoRoot2, plan) {
14317
14815
  if (plan.action === "skip-present")
14318
14816
  return;
14319
- const abs2 = join21(repoRoot2, CONFIG_FILE_PATH);
14817
+ const abs2 = join20(repoRoot2, CONFIG_FILE_PATH);
14320
14818
  const seed = JSON.parse(readFileSync22(abs2, "utf-8"));
14321
14819
  const projects = Array.isArray(seed.projects) ? seed.projects : [];
14322
- writeFileSync11(abs2, `${JSON.stringify({ ...seed, projects: [...projects, plan.entry] }, null, 2)}
14820
+ writeFileSync10(abs2, `${JSON.stringify({ ...seed, projects: [...projects, plan.entry] }, null, 2)}
14323
14821
  `, "utf-8");
14324
14822
  }
14325
14823
  function renderConfigProjectEntryDiffLine(plan) {
@@ -14336,7 +14834,7 @@ function renderConfigProjectEntryDiffLine(plan) {
14336
14834
 
14337
14835
  // src/commands/doctor.ts
14338
14836
  function readVersion() {
14339
- const pkg = JSON.parse(readFileSync23(join22(packageRoot(import.meta.url), "package.json"), "utf-8"));
14837
+ const pkg = JSON.parse(readFileSync23(join21(packageRoot(import.meta.url), "package.json"), "utf-8"));
14340
14838
  return pkg.version;
14341
14839
  }
14342
14840
  function realDeps4() {
@@ -14357,8 +14855,8 @@ var info = (check, message) => ({ check, severity: "info", message });
14357
14855
  var warn = (check, message) => ({ check, severity: "warn", message });
14358
14856
  var error = (check, message) => ({ check, severity: "error", message });
14359
14857
  function readConfig(repoRoot2) {
14360
- const p = join22(repoRoot2, CONFIG_PATH);
14361
- if (!existsSync15(p))
14858
+ const p = join21(repoRoot2, CONFIG_PATH);
14859
+ if (!existsSync17(p))
14362
14860
  return { kind: "missing" };
14363
14861
  let raw;
14364
14862
  try {
@@ -14401,8 +14899,8 @@ function diagnoseInstall(repoRoot2, ctx, manifest) {
14401
14899
  for (const op of buildInitOps(ctx)) {
14402
14900
  if (op.kind === "create-file") {
14403
14901
  const check = labelForPath(op.path);
14404
- const abs2 = join22(repoRoot2, op.path);
14405
- const exists = existsSync15(abs2);
14902
+ const abs2 = join21(repoRoot2, op.path);
14903
+ const exists = existsSync17(abs2);
14406
14904
  const owned = ownedFiles.has(op.path) || isDefaultedAgentVendorPath(op.path, manifest);
14407
14905
  if (!exists) {
14408
14906
  findings.push(owned ? error(check, `${op.path} is recorded as vinaya-managed but missing on disk — run \`vinaya upgrade\`.`) : error(check, `${op.path} is not installed — run \`vinaya init\`.`));
@@ -14427,7 +14925,7 @@ function diagnoseInstall(repoRoot2, ctx, manifest) {
14427
14925
  const check = "hooks";
14428
14926
  const abs2 = resolveManagedBlockPath(repoRoot2, op.path);
14429
14927
  const owned = ownedBlocks.has(blockKey(op.path, op.marker));
14430
- if (!existsSync15(abs2)) {
14928
+ if (!existsSync17(abs2)) {
14431
14929
  findings.push(owned ? error(check, `${op.path} is missing — likely a fresh clone (raw git hooks aren't tracked by git). Run \`vinaya upgrade\` to restore it.`) : info(check, `${op.path} is not installed.`));
14432
14930
  continue;
14433
14931
  }
@@ -14479,22 +14977,22 @@ async function diagnoseHookRouting(repoRoot2, hookDir, readHooksPath) {
14479
14977
  function diagnoseCustomChecks(repoRoot2, config) {
14480
14978
  const findings = [];
14481
14979
  for (const [name, entry2] of Object.entries(config.checks ?? {})) {
14482
- const scriptAbs = join22(repoRoot2, entry2.run);
14483
- findings.push(existsSync15(scriptAbs) ? ok("checks", `custom check '${name}' → ${entry2.run}`) : error("checks", `custom check '${name}' points at a missing script: ${entry2.run}`));
14980
+ const scriptAbs = join21(repoRoot2, entry2.run);
14981
+ findings.push(existsSync17(scriptAbs) ? ok("checks", `custom check '${name}' → ${entry2.run}`) : error("checks", `custom check '${name}' points at a missing script: ${entry2.run}`));
14484
14982
  }
14485
14983
  return findings;
14486
14984
  }
14487
14985
  function listTrackedFiles(repoRoot2) {
14488
14986
  try {
14489
- return execFileSync19("git", ["ls-files"], { cwd: repoRoot2, encoding: "utf-8" }).split(`
14987
+ return execFileSync18("git", ["ls-files"], { cwd: repoRoot2, encoding: "utf-8" }).split(`
14490
14988
  `).map((s) => s.trim()).filter(Boolean);
14491
14989
  } catch {
14492
14990
  return [];
14493
14991
  }
14494
14992
  }
14495
14993
  function diagnoseDocOwnersHealth(repoRoot2) {
14496
- const path = join22(repoRoot2, DOC_OWNERS_PATH);
14497
- const content = existsSync15(path) ? readFileSync23(path, "utf-8") : null;
14994
+ const path = join21(repoRoot2, DOC_OWNERS_PATH);
14995
+ const content = existsSync17(path) ? readFileSync23(path, "utf-8") : null;
14498
14996
  const state = classifyDocOwnersManifest(content);
14499
14997
  if (state === "absent" || state === "empty")
14500
14998
  return [];
@@ -14511,8 +15009,8 @@ function diagnoseDocOwnersHealth(repoRoot2) {
14511
15009
  findings.push(warn("doc-owners", `${DOC_OWNERS_PATH}:${b.lineNum} binds glob '${b.glob}', which matches none of the ${codeFiles.length} ` + "tracked code file(s) in this repo — the code it names may have been deleted, renamed, or never " + "existed. Repoint the binding to the code's new location, or remove it."));
14512
15010
  }
14513
15011
  if (!isUrlPointer(b.pointer)) {
14514
- const pointerPath = join22(repoRoot2, pointerToPath(b.pointer));
14515
- if (!existsSync15(pointerPath)) {
15012
+ const pointerPath = join21(repoRoot2, pointerToPath(b.pointer));
15013
+ if (!existsSync17(pointerPath)) {
14516
15014
  flagged = true;
14517
15015
  findings.push(warn("doc-owners", `${DOC_OWNERS_PATH}:${b.lineNum} points to ${b.pointer}, which does not exist on disk. Repoint the ` + "binding, or add the missing doc."));
14518
15016
  }
@@ -14527,7 +15025,7 @@ var LEGACY_AEG_PACKAGES_PATH = ".aeg/packages";
14527
15025
  function readWorkspacesForDoctor(repoRoot2) {
14528
15026
  const fromPackageJson = () => {
14529
15027
  try {
14530
- const pkg = JSON.parse(readFileSync23(join22(repoRoot2, "package.json"), "utf-8"));
15028
+ const pkg = JSON.parse(readFileSync23(join21(repoRoot2, "package.json"), "utf-8"));
14531
15029
  return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
14532
15030
  } catch {
14533
15031
  return [];
@@ -14535,7 +15033,7 @@ function readWorkspacesForDoctor(repoRoot2) {
14535
15033
  };
14536
15034
  const fromPnpmWorkspaceYaml = () => {
14537
15035
  try {
14538
- return parsePnpmWorkspaceYaml(readFileSync23(join22(repoRoot2, "pnpm-workspace.yaml"), "utf-8"));
15036
+ return parsePnpmWorkspaceYaml(readFileSync23(join21(repoRoot2, "pnpm-workspace.yaml"), "utf-8"));
14539
15037
  } catch {
14540
15038
  return [];
14541
15039
  }
@@ -14544,18 +15042,18 @@ function readWorkspacesForDoctor(repoRoot2) {
14544
15042
  }
14545
15043
  function listChildDirsForDoctor(dir, repoRoot2) {
14546
15044
  try {
14547
- return readdirSync8(join22(repoRoot2, dir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
15045
+ return readdirSync8(join21(repoRoot2, dir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
14548
15046
  } catch {
14549
15047
  return [];
14550
15048
  }
14551
15049
  }
14552
15050
  function diagnoseBlastRadiusDeprecation(repoRoot2, config) {
14553
15051
  const derived = deriveWorkspacePackageDomains(readWorkspacesForDoctor(repoRoot2), (dir) => listChildDirsForDoctor(dir, repoRoot2));
14554
- const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync15(join22(repoRoot2, p)));
15052
+ const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync17(join21(repoRoot2, p)));
14555
15053
  const covered = new Set([...derived, ...defaults]);
14556
15054
  const configExtra = new Set(config?.blastRadius?.extraDomains ?? []);
14557
- const legacyPath = join22(repoRoot2, LEGACY_AEG_PACKAGES_PATH);
14558
- if (!existsSync15(legacyPath)) {
15055
+ const legacyPath = join21(repoRoot2, LEGACY_AEG_PACKAGES_PATH);
15056
+ if (!existsSync17(legacyPath)) {
14559
15057
  return [
14560
15058
  info("blast-radius", `checkBlastRadiusScope is active via live derivation (${derived.length} packages/* domain(s)) + built-in defaults (${defaults.length} present) — no ${LEGACY_AEG_PACKAGES_PATH}, and the check is not dormant.`)
14561
15059
  ];
@@ -14595,7 +15093,7 @@ function diagnoseBriefSchemaDrift(config) {
14595
15093
  }
14596
15094
  function diagnoseEnvDeclarations(repoRoot2, config) {
14597
15095
  const resolved = resolveChecks(coreCheckRegistry(), config?.checks).resolved;
14598
- const specs = resolved.map((entry2) => entry2.source === "config" ? { ...entry2.spec, run: join22(repoRoot2, entry2.spec.run) } : entry2.spec);
15096
+ const specs = resolved.map((entry2) => entry2.source === "config" ? { ...entry2.spec, run: join21(repoRoot2, entry2.spec.run) } : entry2.spec);
14599
15097
  const missing = checksMissingEnvDeclaration(specs);
14600
15098
  const findings = missing.map((name) => info("env", envDeclarationWarning(name)));
14601
15099
  for (const message of lintEnvDeclarations(config?.checks)) {
@@ -14616,7 +15114,7 @@ function diagnoseCheckClassification(config) {
14616
15114
  return findings;
14617
15115
  }
14618
15116
  function diagnoseGlobalConfigChecks() {
14619
- if (!existsSync15(GLOBAL_CONFIG_PATH))
15117
+ if (!existsSync17(GLOBAL_CONFIG_PATH))
14620
15118
  return [];
14621
15119
  let raw;
14622
15120
  try {
@@ -14669,8 +15167,8 @@ function coversWorkflowsDir(pattern) {
14669
15167
  return bare === ".github/workflows" || bare === "/.github/workflows";
14670
15168
  }
14671
15169
  function diagnoseCodeowners(repoRoot2) {
14672
- const path = join22(repoRoot2, ".github", "CODEOWNERS");
14673
- if (!existsSync15(path)) {
15170
+ const path = join21(repoRoot2, ".github", "CODEOWNERS");
15171
+ if (!existsSync17(path)) {
14674
15172
  return info("codeowners", "no .github/CODEOWNERS — vinaya's workflow files have no required-review protection; see `vinaya init`'s printed recommendation.");
14675
15173
  }
14676
15174
  let body;
@@ -14688,7 +15186,7 @@ function diagnoseVinayaOnPath(agents) {
14688
15186
  return [];
14689
15187
  const names = process.platform === "win32" ? ["vinaya.cmd", "vinaya.exe", "vinaya.bat"] : ["vinaya"];
14690
15188
  const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
14691
- const onPath = dirs.some((dir) => names.some((name) => existsSync15(join22(dir, name))));
15189
+ const onPath = dirs.some((dir) => names.some((name) => existsSync17(join21(dir, name))));
14692
15190
  return [
14693
15191
  onPath ? ok("vinaya-on-path", "`vinaya` resolves on PATH — agent-native commands will work.") : info("vinaya-on-path", "`vinaya` is not resolvable on PATH — `/vinaya <role>` and the .agents/skills/vinaya-*/SKILL.md files " + 'installed here will fail "command not found" on first use. Run `npm install -g @attalabs/vinaya`.')
14694
15192
  ];
@@ -14700,8 +15198,8 @@ function diagnosePrincipals(config) {
14700
15198
  return info("principals", 'vinaya.config.json has no "principals" — review-gate and the waiver-label actor check both fall back to a ' + "hardcoded placeholder allowlist that will not include anyone on this repo. Every reviewer's verdict is " + 'silently ignored (DANGLING) until you add `"principals": ["<your-github-login>", ...]` to vinaya.config.json.');
14701
15199
  }
14702
15200
  function diagnoseProjectsCoherence(repoRoot2, config) {
14703
- const registryAbs = join22(repoRoot2, PROJECTS_REGISTRY_PATH);
14704
- const registryNames = existsSync15(registryAbs) ? new Set(parseRegistry(readFileSync23(registryAbs, "utf-8")).map((p) => p.name)) : new Set;
15201
+ const registryAbs = join21(repoRoot2, PROJECTS_REGISTRY_PATH);
15202
+ const registryNames = existsSync17(registryAbs) ? new Set(parseRegistry(readFileSync23(registryAbs, "utf-8")).map((p) => p.name)) : new Set;
14705
15203
  const configNames = new Set((config?.projects ?? []).map((p) => p.name));
14706
15204
  if (registryNames.size === 0 && configNames.size === 0)
14707
15205
  return [];
@@ -14720,17 +15218,17 @@ var TEST_INVOCATION_SUBSTRINGS = ["npm test", "npm run test", "bun test", "bunx
14720
15218
  function diagnoseTestCi(repoRoot2) {
14721
15219
  let pkg;
14722
15220
  try {
14723
- pkg = JSON.parse(readFileSync23(join22(repoRoot2, "package.json"), "utf-8"));
15221
+ pkg = JSON.parse(readFileSync23(join21(repoRoot2, "package.json"), "utf-8"));
14724
15222
  } catch {
14725
15223
  return [];
14726
15224
  }
14727
15225
  const testScript = pkg?.scripts?.test;
14728
15226
  if (typeof testScript !== "string" || testScript.trim() === "")
14729
15227
  return [];
14730
- const workflowsDir = join22(repoRoot2, ".github/workflows");
14731
- const files = existsSync15(workflowsDir) ? readdirSync8(workflowsDir).filter((name) => statSync3(join22(workflowsDir, name)).isFile()) : [];
15228
+ const workflowsDir = join21(repoRoot2, ".github/workflows");
15229
+ const files = existsSync17(workflowsDir) ? readdirSync8(workflowsDir).filter((name) => statSync3(join21(workflowsDir, name)).isFile()) : [];
14732
15230
  const invoked = files.some((name) => {
14733
- const content = readFileSync23(join22(workflowsDir, name), "utf-8");
15231
+ const content = readFileSync23(join21(workflowsDir, name), "utf-8");
14734
15232
  return TEST_INVOCATION_SUBSTRINGS.some((s) => content.includes(s));
14735
15233
  });
14736
15234
  if (invoked)
@@ -14827,8 +15325,8 @@ async function doctorCommand(args) {
14827
15325
  }
14828
15326
 
14829
15327
  // src/commands/eject.ts
14830
- import { existsSync as existsSync16, readFileSync as readFileSync24 } from "node:fs";
14831
- import { join as join23 } from "node:path";
15328
+ import { existsSync as existsSync18, readFileSync as readFileSync24 } from "node:fs";
15329
+ import { join as join22 } from "node:path";
14832
15330
  function realDeps5() {
14833
15331
  return {
14834
15332
  detectRepo: detectGitRepo,
@@ -14845,8 +15343,8 @@ function parse2(args) {
14845
15343
  return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
14846
15344
  }
14847
15345
  function readManifest2(repoRoot2) {
14848
- const p = join23(repoRoot2, CONFIG_PATH);
14849
- if (!existsSync16(p))
15346
+ const p = join22(repoRoot2, CONFIG_PATH);
15347
+ if (!existsSync18(p))
14850
15348
  return { kind: "none" };
14851
15349
  let raw;
14852
15350
  try {
@@ -14940,8 +15438,8 @@ async function ejectCommand(args) {
14940
15438
  }
14941
15439
 
14942
15440
  // src/commands/init.ts
14943
- import { existsSync as existsSync17, readFileSync as readFileSync25, writeFileSync as writeFileSync12 } from "node:fs";
14944
- import { join as join24 } from "node:path";
15441
+ import { existsSync as existsSync19, readFileSync as readFileSync25, writeFileSync as writeFileSync11 } from "node:fs";
15442
+ import { join as join23 } from "node:path";
14945
15443
  function realDeps6() {
14946
15444
  return {
14947
15445
  detectRepo: detectGitRepo,
@@ -14981,8 +15479,8 @@ function parseAgentsFlag(args) {
14981
15479
  }
14982
15480
  var PRODUCT_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
14983
15481
  function readManifest3(repoRoot2) {
14984
- const p = join24(repoRoot2, CONFIG_PATH);
14985
- if (!existsSync17(p))
15482
+ const p = join23(repoRoot2, CONFIG_PATH);
15483
+ if (!existsSync19(p))
14986
15484
  return null;
14987
15485
  try {
14988
15486
  return VinayaConfigSchema.parse(JSON.parse(readFileSync25(p, "utf-8"))).managed ?? null;
@@ -14991,9 +15489,9 @@ function readManifest3(repoRoot2) {
14991
15489
  }
14992
15490
  }
14993
15491
  function writeManifest(repoRoot2, manifest) {
14994
- const configAbs = join24(repoRoot2, CONFIG_PATH);
15492
+ const configAbs = join23(repoRoot2, CONFIG_PATH);
14995
15493
  const seed = JSON.parse(readFileSync25(configAbs, "utf-8"));
14996
- writeFileSync12(configAbs, `${JSON.stringify({ ...seed, managed: manifest }, null, 2)}
15494
+ writeFileSync11(configAbs, `${JSON.stringify({ ...seed, managed: manifest }, null, 2)}
14997
15495
  `, "utf-8");
14998
15496
  }
14999
15497
  async function runInit(args, deps) {
@@ -15208,10 +15706,10 @@ function issueEditCommand(args) {
15208
15706
  }
15209
15707
 
15210
15708
  // src/commands/issue-objectives.ts
15211
- import { execFileSync as execFileSync20 } from "node:child_process";
15212
- import { mkdtempSync as mkdtempSync4, rmSync as rmSync8, writeFileSync as writeFileSync13 } from "node:fs";
15213
- import { tmpdir as tmpdir6 } from "node:os";
15214
- import { join as join25 } from "node:path";
15709
+ import { execFileSync as execFileSync19 } from "node:child_process";
15710
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "node:fs";
15711
+ import { tmpdir as tmpdir5 } from "node:os";
15712
+ import { join as join24 } from "node:path";
15215
15713
  var RETRY = 'vinaya issue objectives edit <n> --add "<sentence>" | --drop O<k> | --replace O<k> "<sentence>" --reason "<text>"';
15216
15714
  var MARKER_PREFIX = "<!-- aeg:objectives:v";
15217
15715
  var HEADING_RE2 = /^##[ \t]*Objectives[ \t]*$/im;
@@ -15278,7 +15776,7 @@ function parseArgs5(args) {
15278
15776
  function fetchIssueBodyAndComments(issueRef) {
15279
15777
  let out;
15280
15778
  try {
15281
- out = execFileSync20("gh", ["issue", "view", issueRef, "--json", "body,comments"], {
15779
+ out = execFileSync19("gh", ["issue", "view", issueRef, "--json", "body,comments"], {
15282
15780
  encoding: "utf8",
15283
15781
  stdio: ["ignore", "pipe", "pipe"]
15284
15782
  });
@@ -15342,9 +15840,9 @@ function issueObjectivesEditCommand(args) {
15342
15840
  const previous = parsed.objectives;
15343
15841
  const updated = applyOp(previous, op);
15344
15842
  const newBody = spliceObjectivesSection(body, renderObjectives(updated));
15345
- const dir = mkdtempSync4(join25(tmpdir6(), "vinaya-objectives-edit-"));
15346
- const tmp = join25(dir, "body.md");
15347
- writeFileSync13(tmp, newBody, "utf8");
15843
+ const dir = mkdtempSync3(join24(tmpdir5(), "vinaya-objectives-edit-"));
15844
+ const tmp = join24(dir, "body.md");
15845
+ writeFileSync12(tmp, newBody, "utf8");
15348
15846
  try {
15349
15847
  const ghArgs = ["--body-file", tmp];
15350
15848
  writeValidatedIssueEdit({
@@ -15356,7 +15854,7 @@ function issueObjectivesEditCommand(args) {
15356
15854
  quiet: true
15357
15855
  });
15358
15856
  } finally {
15359
- rmSync8(dir, { recursive: true, force: true });
15857
+ rmSync7(dir, { recursive: true, force: true });
15360
15858
  }
15361
15859
  const newVersion = objectivesVersion(updated);
15362
15860
  const k = countMarkerComments(comments, MARKER_PREFIX) + 1;
@@ -15381,13 +15879,14 @@ function issueObjectivesEditCommand(args) {
15381
15879
  }
15382
15880
 
15383
15881
  // src/commands/milestone.ts
15384
- import { execFileSync as execFileSync21 } from "node:child_process";
15882
+ import { execFileSync as execFileSync20 } from "node:child_process";
15385
15883
  var RETRY_CREATE2 = "vinaya milestone create --title <title> --body-file <path>";
15386
15884
  var RETRY_ADOPT = "vinaya milestone adopt --target <title> --slug <slug> [--slug <slug> ...]";
15387
15885
  var RETRY_EDIT2 = "vinaya milestone edit <n> --body-file <path>";
15388
15886
  var RETRY_CLOSE = "vinaya milestone close --slug <slug>";
15389
- function sh5(args, input) {
15390
- return execFileSync21(args[0], args.slice(1), {
15887
+ var RETRY_STATUS = "vinaya milestone status <n>";
15888
+ function sh4(args, input) {
15889
+ return execFileSync20(args[0], args.slice(1), {
15391
15890
  encoding: "utf8",
15392
15891
  input,
15393
15892
  env: process.env,
@@ -15395,13 +15894,19 @@ function sh5(args, input) {
15395
15894
  }).trim();
15396
15895
  }
15397
15896
  function shJson2(args, input) {
15398
- return JSON.parse(sh5(args, input));
15897
+ return JSON.parse(sh4(args, input));
15399
15898
  }
15400
15899
  function ghErrorDetail(e) {
15401
15900
  const stderr = e?.stderr;
15402
15901
  const text = typeof stderr === "string" ? stderr : stderr?.toString();
15403
15902
  return (text && text.trim().length > 0 ? text : e?.message ?? "unknown error").trim();
15404
15903
  }
15904
+ function is404(e) {
15905
+ const stderr = e?.stderr;
15906
+ const haystack = [e?.message ?? "", typeof stderr === "string" ? stderr : stderr?.toString() ?? ""].join(`
15907
+ `);
15908
+ return /\b404\b|not found/i.test(haystack);
15909
+ }
15405
15910
  function locateBodyOrRefuse2(args, commandName, retryCommand) {
15406
15911
  let result;
15407
15912
  try {
@@ -15469,7 +15974,7 @@ async function milestoneCreateCommand(args) {
15469
15974
  const repoFlag = await resolveRepoFlagOrRefuse(RETRY_CREATE2);
15470
15975
  let out;
15471
15976
  try {
15472
- out = sh5(["gh", "api", `repos/${repoFlag}/milestones`, "--input", "-"], JSON.stringify({ title, description: body }));
15977
+ out = sh4(["gh", "api", `repos/${repoFlag}/milestones`, "--input", "-"], JSON.stringify({ title, description: body }));
15473
15978
  } catch (e) {
15474
15979
  refuse2([
15475
15980
  makeCheckError("forge-fetch", `\`gh api repos/${repoFlag}/milestones\` failed: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_CREATE2}\`.`)
@@ -15508,7 +16013,7 @@ async function milestoneEditCommand(args) {
15508
16013
  const repoFlag = await resolveRepoFlagOrRefuse(RETRY_EDIT2);
15509
16014
  let out;
15510
16015
  try {
15511
- out = sh5(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${number}`, "--input", "-"], JSON.stringify({ description: body }));
16016
+ out = sh4(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${number}`, "--input", "-"], JSON.stringify({ description: body }));
15512
16017
  } catch (e) {
15513
16018
  refuse2([
15514
16019
  makeCheckError("forge-fetch", `\`gh api repos/${repoFlag}/milestones/${number}\` failed: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_EDIT2}\`.`)
@@ -15635,7 +16140,7 @@ async function milestoneAdoptCommand(args) {
15635
16140
  for (const slugFact of facts.slugs) {
15636
16141
  for (const issueNumber of slugFact.issueNumbers) {
15637
16142
  try {
15638
- sh5(["gh", "issue", "edit", String(issueNumber), "-R", repoFlag, "--milestone", target]);
16143
+ sh4(["gh", "issue", "edit", String(issueNumber), "-R", repoFlag, "--milestone", target]);
15639
16144
  } catch (e) {
15640
16145
  failMidWrite(`failed to attach Issue #${issueNumber} (tranche \`${slugFact.slug}\`) to Milestone "${target}": ` + `${ghErrorDetail(e)}. Slugs already adopted this run: ${adopted.map((a) => a.slug).join(", ") || "(none)"}.`);
15641
16146
  }
@@ -15646,7 +16151,7 @@ async function milestoneAdoptCommand(args) {
15646
16151
  closedMilestone = legacy.number;
15647
16152
  if (legacy.state !== "closed") {
15648
16153
  try {
15649
- sh5(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${legacy.number}`, "-f", "state=closed"]);
16154
+ sh4(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${legacy.number}`, "-f", "state=closed"]);
15650
16155
  } catch (e) {
15651
16156
  failMidWrite(`attached tranche \`${slugFact.slug}\`'s Issue(s) to Milestone "${target}", but failed to close its old ` + `Milestone #${legacy.number}: ${ghErrorDetail(e)}. Slugs already adopted this run: ` + `${adopted.map((a) => a.slug).join(", ") || "(none)"}.`);
15652
16157
  }
@@ -15744,7 +16249,7 @@ async function milestoneCloseCommand(args) {
15744
16249
  }
15745
16250
  let out;
15746
16251
  try {
15747
- out = sh5(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${target.number}`, "-f", "state=closed"]);
16252
+ out = sh4(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${target.number}`, "-f", "state=closed"]);
15748
16253
  } catch (e) {
15749
16254
  refuse2([
15750
16255
  makeCheckError("forge-fetch", `\`gh api repos/${repoFlag}/milestones/${target.number}\` failed: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_CLOSE}\`.`)
@@ -15757,12 +16262,116 @@ async function milestoneCloseCommand(args) {
15757
16262
  process.stdout.write(`${closed.html_url}
15758
16263
  `);
15759
16264
  }
16265
+ function tallyIssueCounts(issues) {
16266
+ const counts = { merged: 0, open: 0, notPlanned: 0 };
16267
+ for (const issue of issues) {
16268
+ if (issue.state === "OPEN")
16269
+ counts.open++;
16270
+ else if (issue.stateReason === "NOT_PLANNED")
16271
+ counts.notPlanned++;
16272
+ else
16273
+ counts.merged++;
16274
+ }
16275
+ return counts;
16276
+ }
16277
+ function formatIssueCounts(total, counts) {
16278
+ if (total === 0)
16279
+ return "0 issues";
16280
+ const parts = [];
16281
+ if (counts.merged > 0)
16282
+ parts.push(`${counts.merged} merged`);
16283
+ if (counts.open > 0)
16284
+ parts.push(`${counts.open} open`);
16285
+ if (counts.notPlanned > 0)
16286
+ parts.push(`${counts.notPlanned} not planned`);
16287
+ return parts.length > 0 ? `${total} issues · ${parts.join(" · ")}` : `${total} issues`;
16288
+ }
16289
+ function extractStatusNumber(rest) {
16290
+ const numberArg = rest[0];
16291
+ if (!numberArg || numberArg.startsWith("-") || !/^\d+$/.test(numberArg))
16292
+ return null;
16293
+ return numberArg;
16294
+ }
16295
+ async function milestoneStatusCommand(args) {
16296
+ const json = args.includes("--json");
16297
+ const rest = args.filter((a) => a !== "--json");
16298
+ const numberArg = extractStatusNumber(rest);
16299
+ if (!numberArg) {
16300
+ refuse2([
16301
+ makeCheckError("forge-args", "`vinaya milestone status` requires the target Milestone number (digits only) as the first argument.", `Pass the Milestone number, e.g. \`${RETRY_STATUS}\`.`)
16302
+ ]);
16303
+ }
16304
+ const repoFlag = await resolveRepoFlagOrRefuse(RETRY_STATUS);
16305
+ const [owner, repo] = repoFlag.split("/");
16306
+ let milestone;
16307
+ try {
16308
+ milestone = shJson2(["gh", "api", `repos/${repoFlag}/milestones/${numberArg}`]);
16309
+ } catch (e) {
16310
+ if (is404(e)) {
16311
+ refuse2([
16312
+ makeCheckError("milestone-status", `Milestone #${numberArg} is not an open-or-closed Milestone in ${repoFlag}.`, `Confirm the number with \`gh api repos/${repoFlag}/milestones\`, then re-run \`${RETRY_STATUS}\`.`)
16313
+ ]);
16314
+ }
16315
+ refuse2([
16316
+ makeCheckError("forge-fetch", `could not fetch Milestone #${numberArg} from the forge: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_STATUS}\`.`)
16317
+ ]);
16318
+ }
16319
+ const header = { number: milestone.number, title: milestone.title, state: milestone.state };
16320
+ const intents = intentLines(milestone.description ?? "");
16321
+ if (intents.length === 0) {
16322
+ if (json)
16323
+ printJson({ milestone: header, tranches: [] });
16324
+ else {
16325
+ process.stdout.write(`${header.title} (#${header.number}, ${header.state})
16326
+ `);
16327
+ process.stdout.write(`no tranche intents declared
16328
+ `);
16329
+ }
16330
+ return;
16331
+ }
16332
+ const rows = [];
16333
+ for (const intent of intents) {
16334
+ let issues;
16335
+ try {
16336
+ issues = await fetchTrancheIssuesAsync(owner, repo, intent.slug);
16337
+ } catch (e) {
16338
+ refuse2([
16339
+ makeCheckError("forge-fetch", `could not fetch tranche \`${intent.slug}\`'s Issues from the forge: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_STATUS}\`.`)
16340
+ ]);
16341
+ }
16342
+ let milestoneFacts;
16343
+ try {
16344
+ milestoneFacts = findMilestoneForSlug(owner, repo, intent.slug);
16345
+ } catch (e) {
16346
+ refuse2([
16347
+ makeCheckError("forge-fetch", `could not derive tranche \`${intent.slug}\`'s Milestone facts from the forge: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_STATUS}\`.`)
16348
+ ]);
16349
+ }
16350
+ const tranche = trancheFromIssues(intent.slug, issues, milestoneFacts);
16351
+ rows.push({
16352
+ slug: intent.slug,
16353
+ lifecycle: tranche.lifecycle,
16354
+ issues: issues.length,
16355
+ counts: tallyIssueCounts(issues)
16356
+ });
16357
+ }
16358
+ if (json) {
16359
+ printJson({ milestone: header, tranches: rows });
16360
+ } else {
16361
+ process.stdout.write(`${header.title} (#${header.number}, ${header.state})
16362
+ `);
16363
+ for (const row2 of rows) {
16364
+ process.stdout.write(`${row2.slug} ${row2.lifecycle} ${formatIssueCounts(row2.issues, row2.counts)}
16365
+ `);
16366
+ }
16367
+ }
16368
+ }
15760
16369
 
15761
16370
  // src/commands/new-check.ts
15762
- import { chmodSync as chmodSync3, existsSync as existsSync18, mkdirSync as mkdirSync7, readFileSync as readFileSync26, writeFileSync as writeFileSync14 } from "node:fs";
15763
- import { join as join26 } from "node:path";
15764
- var TEMPLATE_PATH2 = join26(packageRoot(import.meta.url), "templates", "custom-check.template.ts");
15765
- var CHECKS_DIR = join26("scripts", "vinaya-checks");
16371
+ import { chmodSync as chmodSync3, existsSync as existsSync20, mkdirSync as mkdirSync7, readFileSync as readFileSync26, writeFileSync as writeFileSync13 } from "node:fs";
16372
+ import { join as join25 } from "node:path";
16373
+ var TEMPLATE_PATH2 = join25(packageRoot(import.meta.url), "templates", "custom-check.template.ts");
16374
+ var CHECKS_DIR = join25("scripts", "vinaya-checks");
15766
16375
  var USAGE2 = "Usage: vinaya new check <yourname>/<id> (both segments: lowercase letters, digits, hyphens; e.g. myteam/vocab-check)";
15767
16376
  function newCheckCommand(args) {
15768
16377
  const name = args[0];
@@ -15779,20 +16388,20 @@ function newCheckCommand(args) {
15779
16388
  \`vinaya check\` refuses the entire run over a key it cannot resolve, so a bare, un-namespaced name would brick every check invocation in this repo.`);
15780
16389
  process.exit(2);
15781
16390
  }
15782
- const scriptsDir = join26(process.cwd(), CHECKS_DIR);
15783
- if (!existsSync18(scriptsDir))
16391
+ const scriptsDir = join25(process.cwd(), CHECKS_DIR);
16392
+ if (!existsSync20(scriptsDir))
15784
16393
  mkdirSync7(scriptsDir, { recursive: true });
15785
16394
  const fileStem = name.slice(name.indexOf("/") + 1);
15786
- const targetPath = join26(scriptsDir, `${fileStem}.ts`);
15787
- if (existsSync18(targetPath)) {
16395
+ const targetPath = join25(scriptsDir, `${fileStem}.ts`);
16396
+ if (existsSync20(targetPath)) {
15788
16397
  console.error(`Error: ${targetPath} already exists.`);
15789
16398
  process.exit(1);
15790
16399
  }
15791
16400
  const template = readFileSync26(TEMPLATE_PATH2, "utf-8");
15792
16401
  const contents = template.split("{{CHECK_NAME}}").join(name);
15793
- writeFileSync14(targetPath, contents, "utf-8");
16402
+ writeFileSync13(targetPath, contents, "utf-8");
15794
16403
  chmodSync3(targetPath, 493);
15795
- const relPath = join26(CHECKS_DIR, `${fileStem}.ts`);
16404
+ const relPath = join25(CHECKS_DIR, `${fileStem}.ts`);
15796
16405
  const registration = JSON.stringify({ checks: { [name]: { run: `./${relPath}`, scope: "diff" } } }, null, 2);
15797
16406
  process.stdout.write(`Created ${relPath}
15798
16407
 
@@ -15802,10 +16411,10 @@ ${registration}
15802
16411
  }
15803
16412
 
15804
16413
  // src/commands/new-noop-check.ts
15805
- import { chmodSync as chmodSync4, existsSync as existsSync19, mkdirSync as mkdirSync8, readFileSync as readFileSync27, writeFileSync as writeFileSync15 } from "node:fs";
15806
- import { join as join27 } from "node:path";
15807
- var TEMPLATE_PATH3 = join27(packageRoot(import.meta.url), "templates", "noop-check.template.ts");
15808
- var CHECKS_DIR2 = join27("vinaya", "checks");
16414
+ import { chmodSync as chmodSync4, existsSync as existsSync21, mkdirSync as mkdirSync8, readFileSync as readFileSync27, writeFileSync as writeFileSync14 } from "node:fs";
16415
+ import { join as join26 } from "node:path";
16416
+ var TEMPLATE_PATH3 = join26(packageRoot(import.meta.url), "templates", "noop-check.template.ts");
16417
+ var CHECKS_DIR2 = join26("vinaya", "checks");
15809
16418
  var USAGE3 = "Usage: vinaya new noop-check <core-check-id> (the exact id of a core check — run `vinaya check --plan` to list them)";
15810
16419
  function newNoopCheckCommand(args) {
15811
16420
  const name = args[0];
@@ -15819,19 +16428,19 @@ function newNoopCheckCommand(args) {
15819
16428
  \`new noop-check\` only silences a CORE check — a \`checks\` key that exactly matches a core check id REPLACES it. A namespaced or unknown name has nothing to replace; use \`vinaya new check\` to scaffold a new, additive check instead.`);
15820
16429
  process.exit(2);
15821
16430
  }
15822
- const checksDir = join27(process.cwd(), CHECKS_DIR2);
15823
- if (!existsSync19(checksDir))
16431
+ const checksDir = join26(process.cwd(), CHECKS_DIR2);
16432
+ if (!existsSync21(checksDir))
15824
16433
  mkdirSync8(checksDir, { recursive: true });
15825
- const targetPath = join27(checksDir, `${name}.ts`);
15826
- if (existsSync19(targetPath)) {
16434
+ const targetPath = join26(checksDir, `${name}.ts`);
16435
+ if (existsSync21(targetPath)) {
15827
16436
  console.error(`Error: ${targetPath} already exists.`);
15828
16437
  process.exit(1);
15829
16438
  }
15830
16439
  const template = readFileSync27(TEMPLATE_PATH3, "utf-8");
15831
16440
  const contents = template.split("{{CHECK_NAME}}").join(name);
15832
- writeFileSync15(targetPath, contents, "utf-8");
16441
+ writeFileSync14(targetPath, contents, "utf-8");
15833
16442
  chmodSync4(targetPath, 493);
15834
- const relPath = join27(CHECKS_DIR2, `${name}.ts`);
16443
+ const relPath = join26(CHECKS_DIR2, `${name}.ts`);
15835
16444
  const entry2 = {
15836
16445
  run: `./${relPath}`,
15837
16446
  scope: coreSpec.scope
@@ -15849,10 +16458,10 @@ ${registration}
15849
16458
  }
15850
16459
 
15851
16460
  // src/commands/new-role.ts
15852
- import { existsSync as existsSync20, mkdirSync as mkdirSync9, readFileSync as readFileSync28, writeFileSync as writeFileSync16 } from "node:fs";
15853
- import { join as join28 } from "node:path";
15854
- var TEMPLATE_PATH4 = join28(packageRoot(import.meta.url), "templates", "role.template.md");
15855
- var ROLES_DIR = join28("vinaya", "roles");
16461
+ import { existsSync as existsSync22, mkdirSync as mkdirSync9, readFileSync as readFileSync28, writeFileSync as writeFileSync15 } from "node:fs";
16462
+ import { join as join27 } from "node:path";
16463
+ var TEMPLATE_PATH4 = join27(packageRoot(import.meta.url), "templates", "role.template.md");
16464
+ var ROLES_DIR = join27("vinaya", "roles");
15856
16465
  var USAGE4 = "Usage: vinaya new role <yourname>/<id> (both segments: lowercase letters, digits, hyphens; e.g. acme/qa-lead)";
15857
16466
  function newRoleCommand(args) {
15858
16467
  const name = args[0];
@@ -15865,19 +16474,19 @@ function newRoleCommand(args) {
15865
16474
  A bare, un-namespaced key resolves as an OVERRIDE of a core role — a complete replacement of that role's contract, and a real governance decision this scaffolder does not make for you. Pick a namespaced name, or write the override entry by hand if you really mean to replace a core role's contract.`);
15866
16475
  process.exit(2);
15867
16476
  }
15868
- const rolesDir = join28(process.cwd(), ROLES_DIR);
15869
- if (!existsSync20(rolesDir))
16477
+ const rolesDir = join27(process.cwd(), ROLES_DIR);
16478
+ if (!existsSync22(rolesDir))
15870
16479
  mkdirSync9(rolesDir, { recursive: true });
15871
16480
  const roleId = name.slice(name.indexOf("/") + 1);
15872
- const targetPath = join28(rolesDir, `${roleId}.md`);
15873
- if (existsSync20(targetPath)) {
16481
+ const targetPath = join27(rolesDir, `${roleId}.md`);
16482
+ if (existsSync22(targetPath)) {
15874
16483
  console.error(`Error: ${targetPath} already exists.`);
15875
16484
  process.exit(1);
15876
16485
  }
15877
16486
  const template = readFileSync28(TEMPLATE_PATH4, "utf-8");
15878
16487
  const contents = template.split("{{ROLE_ID}}").join(roleId);
15879
- writeFileSync16(targetPath, contents, "utf-8");
15880
- const relPath = join28(ROLES_DIR, `${roleId}.md`);
16488
+ writeFileSync15(targetPath, contents, "utf-8");
16489
+ const relPath = join27(ROLES_DIR, `${roleId}.md`);
15881
16490
  const registration = JSON.stringify({ roles: { [name]: { contract: `./${relPath}` } } }, null, 2);
15882
16491
  process.stdout.write(`Created ${relPath}
15883
16492
 
@@ -15887,7 +16496,7 @@ ${registration}
15887
16496
  }
15888
16497
 
15889
16498
  // src/commands/pr.ts
15890
- import { execFileSync as execFileSync22 } from "node:child_process";
16499
+ import { execFileSync as execFileSync21 } from "node:child_process";
15891
16500
 
15892
16501
  // src/lib/numstat.ts
15893
16502
  function summariseNumstat(numstat) {
@@ -16113,6 +16722,23 @@ function blankUnanchoredStructuralFields(body) {
16113
16722
  `).map((l) => blankProjectField(blankTierField(l))).join(`
16114
16723
  `);
16115
16724
  }
16725
+ var OBJECTIVE_MARKER_LINE = /^ {0,3}O\d{1,9}\./;
16726
+ function blankObjectiveMarkerLine(line) {
16727
+ const m = OBJECTIVE_MARKER_LINE.exec(line);
16728
+ if (!m)
16729
+ return line;
16730
+ return " ".repeat(m[0].length) + line.slice(m[0].length);
16731
+ }
16732
+ function blankObjectiveMarkers(body) {
16733
+ const bounds = objectivesSectionBounds(body);
16734
+ if (!bounds)
16735
+ return body;
16736
+ const section = body.slice(bounds.start, bounds.end);
16737
+ const blanked = section.split(`
16738
+ `).map(blankObjectiveMarkerLine).join(`
16739
+ `);
16740
+ return body.slice(0, bounds.start) + blanked + body.slice(bounds.end);
16741
+ }
16116
16742
  var CLOSES_REF = /Closes\s*#\d+/i;
16117
16743
  var EVIDENCE_HEADING = /^#{1,6}\s/;
16118
16744
  var EVIDENCE_HEAD_LINE = /^Head:\s*\S+$/i;
@@ -16140,6 +16766,7 @@ function buildScanMask(ctx) {
16140
16766
  masked = blankPremiseValues(masked);
16141
16767
  masked = blankTokenReportSection(masked);
16142
16768
  masked = blankUnanchoredStructuralFields(masked);
16769
+ masked = blankObjectiveMarkers(masked);
16143
16770
  return masked;
16144
16771
  }
16145
16772
  var TOKEN_WITH_DIGIT = /\S*\p{Nd}\S*/gu;
@@ -16179,7 +16806,7 @@ var RETRY_CREATE3 = "vinaya pr create --validate-only …";
16179
16806
  var RETRY_EDIT3 = "vinaya pr edit <n> --validate-only …";
16180
16807
  function git4(args) {
16181
16808
  try {
16182
- return execFileSync22("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
16809
+ return execFileSync21("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
16183
16810
  } catch {
16184
16811
  return "";
16185
16812
  }
@@ -16213,7 +16840,7 @@ function reportPass2(json, command) {
16213
16840
  function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
16214
16841
  const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
16215
16842
  try {
16216
- const out = execFileSync22("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
16843
+ const out = execFileSync21("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
16217
16844
  const url = out.trim();
16218
16845
  if (json)
16219
16846
  printJson({ validated: true, written: true, url });
@@ -16228,7 +16855,7 @@ function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
16228
16855
  function fetchPrForgeContext(prRef) {
16229
16856
  let viewOut;
16230
16857
  try {
16231
- viewOut = execFileSync22("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
16858
+ viewOut = execFileSync21("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
16232
16859
  encoding: "utf8",
16233
16860
  stdio: ["ignore", "pipe", "pipe"]
16234
16861
  });
@@ -16375,7 +17002,7 @@ function prEditCommand(args) {
16375
17002
  }
16376
17003
 
16377
17004
  // src/commands/pr-rule.ts
16378
- import { execFileSync as execFileSync23 } from "node:child_process";
17005
+ import { execFileSync as execFileSync22 } from "node:child_process";
16379
17006
  import { readFileSync as readFileSync29 } from "node:fs";
16380
17007
  var RETRY2 = "vinaya pr rule <pr> --file <ruling.md>";
16381
17008
  var NO_CODE_REVIEW_CANDIDATE = "no code-reviewer verdict comment found on this PR";
@@ -16400,7 +17027,7 @@ function refuseIfCastsAVerdict(body, filePath) {
16400
17027
  function fetchPrCommentBodies(prRef) {
16401
17028
  let out;
16402
17029
  try {
16403
- out = execFileSync23("gh", ["pr", "view", prRef, "--json", "comments"], {
17030
+ out = execFileSync22("gh", ["pr", "view", prRef, "--json", "comments"], {
16404
17031
  encoding: "utf8",
16405
17032
  stdio: ["ignore", "pipe", "pipe"]
16406
17033
  });
@@ -16460,22 +17087,22 @@ function prRuleCommand(args) {
16460
17087
  }
16461
17088
 
16462
17089
  // src/commands/pr-report.ts
16463
- import { execFileSync as execFileSync24, spawnSync as spawnSync2 } from "node:child_process";
16464
- import { existsSync as existsSync21, mkdtempSync as mkdtempSync5, readFileSync as readFileSync30, rmSync as rmSync9, writeFileSync as writeFileSync17 } from "node:fs";
16465
- import { tmpdir as tmpdir7 } from "node:os";
16466
- import { join as join29 } from "node:path";
17090
+ import { execFileSync as execFileSync23, spawnSync as spawnSync2 } from "node:child_process";
17091
+ import { existsSync as existsSync23, mkdtempSync as mkdtempSync4, readFileSync as readFileSync30, rmSync as rmSync8, writeFileSync as writeFileSync16 } from "node:fs";
17092
+ import { tmpdir as tmpdir6 } from "node:os";
17093
+ import { join as join28 } from "node:path";
16467
17094
  var EVIDENCE_START = "<!-- AEG:EVIDENCE:START -->";
16468
17095
  var EVIDENCE_END = "<!-- AEG:EVIDENCE:END -->";
16469
17096
  function git5(args) {
16470
17097
  try {
16471
- return execFileSync24("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
17098
+ return execFileSync23("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
16472
17099
  } catch {
16473
17100
  return "";
16474
17101
  }
16475
17102
  }
16476
17103
  function gh3(args) {
16477
17104
  try {
16478
- return execFileSync24("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
17105
+ return execFileSync23("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
16479
17106
  } catch (err) {
16480
17107
  const stderr = err.stderr;
16481
17108
  throw new Error(String(stderr ?? err.message).trim() || "gh command failed");
@@ -16494,7 +17121,7 @@ class GitCommandError extends Error {
16494
17121
  }
16495
17122
  function gitStrict(args) {
16496
17123
  try {
16497
- return execFileSync24("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
17124
+ return execFileSync23("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
16498
17125
  } catch (err) {
16499
17126
  const stderr = err.stderr;
16500
17127
  throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
@@ -16527,10 +17154,27 @@ function computeGroupA() {
16527
17154
  const numstat = gitStrict(["diff", `${base}...${head}`, "--numstat"]);
16528
17155
  return { head, base, numstat };
16529
17156
  }
17157
+ function describeGradedBodySource(source) {
17158
+ switch (source) {
17159
+ case "write":
17160
+ return "the drafted body file (`--write`)";
17161
+ case "push":
17162
+ return "the live pull-request body (`--push`)";
17163
+ case "ambient":
17164
+ return "the ambient `PR_BODY` environment (no `--write`/`--push`)";
17165
+ }
17166
+ }
17167
+ var BODY_READING_CHECK_NAMES = new Set(coreCheckRegistry().filter((spec) => {
17168
+ const decl = spec.env?.PR_BODY;
17169
+ return decl === true || typeof decl === "object" && decl !== null && "optional" in decl;
17170
+ }).map((spec) => spec.name));
17171
+ function shouldRenderAsSkipped(outcome, gradedBody) {
17172
+ return gradedBody === "" && outcome.status === "pass" && outcome.errors.length === 0 && BODY_READING_CHECK_NAMES.has(outcome.name);
17173
+ }
16530
17174
  function resolveSelfEntry() {
16531
17175
  const root = packageRoot(import.meta.url);
16532
- const dist = join29(root, "dist", "index.js");
16533
- return existsSync21(dist) ? dist : join29(root, "src", "index.ts");
17176
+ const dist = join28(root, "dist", "index.js");
17177
+ return existsSync23(dist) ? dist : join28(root, "src", "index.ts");
16534
17178
  }
16535
17179
  var FAILING_STATUSES = new Set(["fail", "error", "timeout"]);
16536
17180
  function anyGateFailed(outcomes) {
@@ -16577,7 +17221,7 @@ function renderGroupA(groupA) {
16577
17221
  ].join(`
16578
17222
  `);
16579
17223
  }
16580
- function renderGroupB(outcomes) {
17224
+ function renderGroupB(outcomes, gradedBodySource) {
16581
17225
  const sorted = [...outcomes].sort((a, b) => a.name.localeCompare(b.name));
16582
17226
  const lines = sorted.flatMap((o) => {
16583
17227
  const rows = [`${o.name}: ${o.status}`];
@@ -16585,8 +17229,18 @@ function renderGroupB(outcomes) {
16585
17229
  rows.push(` ${e.severity}: ${e.message}`);
16586
17230
  return rows;
16587
17231
  });
16588
- return ["### Group B — attested", "", "`vinaya check --all --diff-only`", "", "```", lines.join(`
16589
- `), "```"].join(`
17232
+ return [
17233
+ "### Group B — attested",
17234
+ "",
17235
+ "`vinaya check --all --diff-only`",
17236
+ "",
17237
+ `Graded body: ${describeGradedBodySource(gradedBodySource)}`,
17238
+ "",
17239
+ "```",
17240
+ lines.join(`
17241
+ `),
17242
+ "```"
17243
+ ].join(`
16590
17244
  `);
16591
17245
  }
16592
17246
  var AGENT_COMMAND_TIMEOUT_MS = 30000;
@@ -16652,14 +17306,14 @@ function renderGroupC(groupC) {
16652
17306
  `)].join(`
16653
17307
  `);
16654
17308
  }
16655
- function buildBlockInner(groupA, gateOutcomes, groupC) {
17309
+ function buildBlockInner(groupA, gateOutcomes, groupC, gradedBodySource) {
16656
17310
  return [
16657
17311
  `Head: ${groupA.head}`,
16658
17312
  `${EVIDENCE_SUMMARY_PREFIX}\`${summariseNumstat(groupA.numstat)}\``,
16659
17313
  "",
16660
17314
  renderGroupA(groupA),
16661
17315
  "",
16662
- renderGroupB(gateOutcomes),
17316
+ renderGroupB(gateOutcomes, gradedBodySource),
16663
17317
  "",
16664
17318
  renderGroupC(groupC)
16665
17319
  ].join(`
@@ -16846,9 +17500,11 @@ async function buildReport(opts = {}) {
16846
17500
  const groupA = opts.groupA ?? computeGroupA();
16847
17501
  const gateRunner = opts.gateRunner ?? runRealGates;
16848
17502
  const gateResult = await gateRunner();
16849
- const outcomes = gateResult.outcomes.filter((o) => o.name !== "evidence-fresh");
16850
- const groupC = opts.groupC ?? computeGroupC(opts.body ?? process.env.PR_BODY ?? "");
16851
- const blockInner = buildBlockInner(groupA, outcomes, groupC);
17503
+ const gradedBody = opts.body ?? process.env.PR_BODY ?? "";
17504
+ const gradedBodySource = opts.gradedBodySource ?? "ambient";
17505
+ const outcomes = gateResult.outcomes.filter((o) => o.name !== "evidence-fresh").map((o) => shouldRenderAsSkipped(o, gradedBody) ? { ...o, status: "skipped" } : o);
17506
+ const groupC = opts.groupC ?? computeGroupC(gradedBody);
17507
+ const blockInner = buildBlockInner(groupA, outcomes, groupC, gradedBodySource);
16852
17508
  const block = `${EVIDENCE_START}
16853
17509
  ${blockInner}
16854
17510
  ${EVIDENCE_END}`;
@@ -16862,16 +17518,16 @@ ${EVIDENCE_END}`;
16862
17518
  }
16863
17519
  var USAGE5 = "Usage: vinaya pr report [--write <body-file> | --push <pr>] [--phase <phase>] [--role <role>] " + "[--model <id>] [--transcript <path>]";
16864
17520
  function ghEditBody(pr, body) {
16865
- const dir = mkdtempSync5(join29(tmpdir7(), "vinaya-pr-report-push-"));
16866
- const tmp = join29(dir, "body.md");
16867
- writeFileSync17(tmp, body);
17521
+ const dir = mkdtempSync4(join28(tmpdir6(), "vinaya-pr-report-push-"));
17522
+ const tmp = join28(dir, "body.md");
17523
+ writeFileSync16(tmp, body);
16868
17524
  try {
16869
17525
  gh3(["pr", "edit", pr, "--body-file", tmp]);
16870
17526
  } finally {
16871
- rmSync9(dir, { recursive: true, force: true });
17527
+ rmSync8(dir, { recursive: true, force: true });
16872
17528
  }
16873
17529
  }
16874
- async function prReportCommand(args) {
17530
+ async function prReportCommand(args, testOverrides) {
16875
17531
  const writeIdx = args.indexOf("--write");
16876
17532
  const writePath = writeIdx !== -1 ? args[writeIdx + 1] : undefined;
16877
17533
  const pushIdx = args.indexOf("--push");
@@ -16914,10 +17570,19 @@ ${USAGE5}`);
16914
17570
  process.env.PR_NUMBER = pushPr;
16915
17571
  process.env.BRANCH = git5(["rev-parse", "--abbrev-ref", "HEAD"]);
16916
17572
  }
16917
- const existingForWrite = writePath === undefined ? undefined : existsSync21(writePath) ? readFileSync30(writePath, "utf8") : "";
17573
+ const existingForWrite = writePath === undefined ? undefined : existsSync23(writePath) ? readFileSync30(writePath, "utf8") : "";
17574
+ if (writePath !== undefined) {
17575
+ process.env.PR_BODY = existingForWrite;
17576
+ process.env.BRANCH = git5(["rev-parse", "--abbrev-ref", "HEAD"]);
17577
+ }
17578
+ const gradedBodySource = pushPr ? "push" : writePath !== undefined ? "write" : "ambient";
16918
17579
  let result;
16919
17580
  try {
16920
- result = await buildReport({ body: preEditBody ?? existingForWrite });
17581
+ result = await buildReport({
17582
+ body: preEditBody ?? existingForWrite,
17583
+ gradedBodySource,
17584
+ gateRunner: testOverrides?.gateRunner
17585
+ });
16921
17586
  } catch (err) {
16922
17587
  if (err instanceof UnresolvableMergeBaseError || err instanceof GitCommandError) {
16923
17588
  console.error(`vinaya pr report: refused — ${err.message}`);
@@ -16958,9 +17623,9 @@ ${USAGE5}`);
16958
17623
  try {
16959
17624
  ghEditBody(pushPr, preEditBody);
16960
17625
  } catch (err) {
16961
- const dir = mkdtempSync5(join29(tmpdir7(), "vinaya-pr-report-push-restore-failed-"));
16962
- const savePath = join29(dir, "pre-edit-body.md");
16963
- writeFileSync17(savePath, preEditBody);
17626
+ const dir = mkdtempSync4(join28(tmpdir6(), "vinaya-pr-report-push-restore-failed-"));
17627
+ const savePath = join28(dir, "pre-edit-body.md");
17628
+ writeFileSync16(savePath, preEditBody);
16964
17629
  console.error(`vinaya pr report: self-verification FAILED on PR ${pushPr} AND the restore of its pre-edit body also failed: ${err instanceof Error ? err.message : String(err)}. PR ${pushPr}'s body may now be corrupted — the pre-edit body was saved to ${savePath}; restore it by hand with \`gh pr edit ${pushPr} --body-file ${savePath}\`.`);
16965
17630
  process.exit(1);
16966
17631
  }
@@ -16993,7 +17658,7 @@ The AEG:EVIDENCE block was still pushed to PR ${pushPr}.`);
16993
17658
  transcriptPath,
16994
17659
  modelOverride
16995
17660
  });
16996
- writeFileSync17(writePath, composeWrittenBody(existing, result.blockInner, tokens));
17661
+ writeFileSync16(writePath, composeWrittenBody(existing, result.blockInner, tokens));
16997
17662
  if (tokens.collected) {
16998
17663
  process.stdout.write(`Wrote AEG:EVIDENCE and AEG:TOKENS blocks to ${writePath}
16999
17664
  `);
@@ -17013,7 +17678,7 @@ The AEG:EVIDENCE block was still written to ${writePath}.`);
17013
17678
  }
17014
17679
 
17015
17680
  // src/commands/pr-verify-evidence.ts
17016
- import { execFileSync as execFileSync25 } from "node:child_process";
17681
+ import { execFileSync as execFileSync24 } from "node:child_process";
17017
17682
 
17018
17683
  // src/commands/pr-verify-evidence-logic.ts
17019
17684
  function publishedMergeBase(region) {
@@ -17100,10 +17765,10 @@ function renderVerdict(verdict) {
17100
17765
 
17101
17766
  // src/commands/pr-verify-evidence.ts
17102
17767
  function gh4(args) {
17103
- return execFileSync25("gh", args, { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
17768
+ return execFileSync24("gh", args, { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
17104
17769
  }
17105
17770
  function assertCleanWorktree() {
17106
- const dirty = execFileSync25("git", ["status", "--porcelain"], {
17771
+ const dirty = execFileSync24("git", ["status", "--porcelain"], {
17107
17772
  encoding: "utf8",
17108
17773
  cwd: repoRoot2()
17109
17774
  }).trim();
@@ -17140,7 +17805,7 @@ function assertRepoRootCwd() {
17140
17805
  process.exit(2);
17141
17806
  }
17142
17807
  function repoRoot2() {
17143
- return execFileSync25("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
17808
+ return execFileSync24("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
17144
17809
  }
17145
17810
  async function prVerifyEvidenceCommand(args) {
17146
17811
  const prRef = args[0] && !args[0].startsWith("-") ? args[0] : undefined;
@@ -17164,7 +17829,7 @@ async function prVerifyEvidenceCommand(args) {
17164
17829
  process.exit(1);
17165
17830
  return;
17166
17831
  }
17167
- const localHead = execFileSync25("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim();
17832
+ const localHead = execFileSync24("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim();
17168
17833
  if (!forgeHead) {
17169
17834
  process.stderr.write(`pr verify-evidence: REFUSED — could not read #${prRef}'s head from the forge, so the verdict cannot be bound to a commit.
17170
17835
  `);
@@ -17187,13 +17852,13 @@ async function prVerifyEvidenceCommand(args) {
17187
17852
  }
17188
17853
 
17189
17854
  // src/commands/quickstart.ts
17190
- import { execFileSync as execFileSync26 } from "node:child_process";
17191
- import { existsSync as existsSync23, readFileSync as readFileSync32 } from "node:fs";
17192
- import { join as join31 } from "node:path";
17855
+ import { execFileSync as execFileSync25 } from "node:child_process";
17856
+ import { existsSync as existsSync25, readFileSync as readFileSync32 } from "node:fs";
17857
+ import { join as join30 } from "node:path";
17193
17858
 
17194
17859
  // src/lib/doc-owners-write.ts
17195
- import { existsSync as existsSync22, mkdirSync as mkdirSync10, readFileSync as readFileSync31, writeFileSync as writeFileSync18 } from "node:fs";
17196
- import { dirname as dirname10, join as join30 } from "node:path";
17860
+ import { existsSync as existsSync24, mkdirSync as mkdirSync10, readFileSync as readFileSync31, writeFileSync as writeFileSync17 } from "node:fs";
17861
+ import { dirname as dirname11, join as join29 } from "node:path";
17197
17862
  function bindingLine(glob, pointer) {
17198
17863
  return `${glob} ${pointer}`;
17199
17864
  }
@@ -17209,9 +17874,9 @@ function appendDocOwnersBinding(content, glob, pointer) {
17209
17874
  `;
17210
17875
  }
17211
17876
  function planDocOwnersBinding(repoRoot3, glob, pointer) {
17212
- const abs2 = join30(repoRoot3, DOC_OWNERS_PATH);
17877
+ const abs2 = join29(repoRoot3, DOC_OWNERS_PATH);
17213
17878
  const line = bindingLine(glob, pointer);
17214
- if (!existsSync22(abs2)) {
17879
+ if (!existsSync24(abs2)) {
17215
17880
  return { action: "create-host", line, path: DOC_OWNERS_PATH };
17216
17881
  }
17217
17882
  const existing = readFileSync31(abs2, "utf-8");
@@ -17219,13 +17884,13 @@ function planDocOwnersBinding(repoRoot3, glob, pointer) {
17219
17884
  return { action: already ? "skip-present" : "append-row", line, path: DOC_OWNERS_PATH };
17220
17885
  }
17221
17886
  function applyDocOwnersBinding(repoRoot3, plan, glob, pointer) {
17222
- const abs2 = join30(repoRoot3, DOC_OWNERS_PATH);
17887
+ const abs2 = join29(repoRoot3, DOC_OWNERS_PATH);
17223
17888
  if (plan.action === "create-host") {
17224
- mkdirSync10(dirname10(abs2), { recursive: true });
17225
- writeFileSync18(abs2, freshDocOwners(glob, pointer), "utf-8");
17889
+ mkdirSync10(dirname11(abs2), { recursive: true });
17890
+ writeFileSync17(abs2, freshDocOwners(glob, pointer), "utf-8");
17226
17891
  } else if (plan.action === "append-row") {
17227
17892
  const existing = readFileSync31(abs2, "utf-8");
17228
- writeFileSync18(abs2, appendDocOwnersBinding(existing, glob, pointer), "utf-8");
17893
+ writeFileSync17(abs2, appendDocOwnersBinding(existing, glob, pointer), "utf-8");
17229
17894
  }
17230
17895
  }
17231
17896
  function renderDocOwnersBindingDiffLine(plan) {
@@ -17288,7 +17953,7 @@ function pointerIsMissingLocalFile(repoRoot3, pointer) {
17288
17953
  if (/^https?:\/\//i.test(pointer))
17289
17954
  return false;
17290
17955
  const path = pointer.split("#")[0] ?? pointer;
17291
- return !existsSync23(join31(repoRoot3, path));
17956
+ return !existsSync25(join30(repoRoot3, path));
17292
17957
  }
17293
17958
  async function bindDocOwnerLoop(deps, repoRoot3) {
17294
17959
  let any = false;
@@ -17355,7 +18020,7 @@ async function registerProjectLoop(deps) {
17355
18020
  }
17356
18021
  }
17357
18022
  function readPackageVersion() {
17358
- const pkg = JSON.parse(readFileSync32(join31(packageRoot(import.meta.url), "package.json"), "utf-8"));
18023
+ const pkg = JSON.parse(readFileSync32(join30(packageRoot(import.meta.url), "package.json"), "utf-8"));
17359
18024
  return pkg.version;
17360
18025
  }
17361
18026
  function realDeps7() {
@@ -17387,7 +18052,7 @@ function realDeps7() {
17387
18052
  };
17388
18053
  }
17389
18054
  function execGit(repoRoot3, args) {
17390
- return execFileSync26("git", args, { cwd: repoRoot3, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
18055
+ return execFileSync25("git", args, { cwd: repoRoot3, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
17391
18056
  }
17392
18057
  function errorDetail(err) {
17393
18058
  const stderr = err.stderr;
@@ -17521,11 +18186,11 @@ async function quickstartCommand(args) {
17521
18186
  }
17522
18187
 
17523
18188
  // src/commands/release.ts
17524
- import { execFileSync as execFileSync27 } from "node:child_process";
18189
+ import { execFileSync as execFileSync26 } from "node:child_process";
17525
18190
  function realDeps8() {
17526
18191
  const git6 = (args) => {
17527
18192
  try {
17528
- return execFileSync27("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
18193
+ return execFileSync26("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
17529
18194
  } catch {
17530
18195
  return null;
17531
18196
  }
@@ -17544,7 +18209,7 @@ function realDeps8() {
17544
18209
  },
17545
18210
  fetchOrigin() {
17546
18211
  try {
17547
- execFileSync27("git", ["fetch", "origin"], { stdio: "inherit" });
18212
+ execFileSync26("git", ["fetch", "origin"], { stdio: "inherit" });
17548
18213
  return true;
17549
18214
  } catch {
17550
18215
  return false;
@@ -17565,14 +18230,14 @@ function realDeps8() {
17565
18230
  },
17566
18231
  npmWhoami() {
17567
18232
  try {
17568
- execFileSync27("npm", ["whoami"], { stdio: "ignore" });
18233
+ execFileSync26("npm", ["whoami"], { stdio: "ignore" });
17569
18234
  return true;
17570
18235
  } catch {
17571
18236
  return false;
17572
18237
  }
17573
18238
  },
17574
18239
  runStreamed(cmd, args) {
17575
- execFileSync27(cmd, args, { stdio: "inherit" });
18240
+ execFileSync26(cmd, args, { stdio: "inherit" });
17576
18241
  },
17577
18242
  tagsAtHead() {
17578
18243
  const tags = git6(["tag", "--points-at", "HEAD"]);
@@ -17583,7 +18248,7 @@ function realDeps8() {
17583
18248
  },
17584
18249
  npmViewVersion(pkg) {
17585
18250
  try {
17586
- const out = execFileSync27("npm", ["view", pkg, "version"], {
18251
+ const out = execFileSync26("npm", ["view", pkg, "version"], {
17587
18252
  encoding: "utf8",
17588
18253
  stdio: ["ignore", "pipe", "pipe"]
17589
18254
  }).trim();
@@ -17725,11 +18390,11 @@ async function releaseCommand(args) {
17725
18390
  }
17726
18391
 
17727
18392
  // src/commands/review-status.ts
17728
- import { execFileSync as execFileSync28 } from "node:child_process";
18393
+ import { execFileSync as execFileSync27 } from "node:child_process";
17729
18394
  var MAX_ROUNDS2 = 3;
17730
18395
  function fetchPr(prNumber) {
17731
18396
  try {
17732
- const out = execFileSync28("gh", ["pr", "view", prNumber, "--json", "comments,headRefOid,baseRefName"], {
18397
+ const out = execFileSync27("gh", ["pr", "view", prNumber, "--json", "comments,headRefOid,baseRefName"], {
17733
18398
  encoding: "utf8",
17734
18399
  stdio: ["ignore", "pipe", "pipe"]
17735
18400
  });
@@ -17740,7 +18405,7 @@ function fetchPr(prNumber) {
17740
18405
  }
17741
18406
  function behindBy(base) {
17742
18407
  try {
17743
- const out = execFileSync28("git", ["rev-list", "--count", `HEAD..origin/${base}`], {
18408
+ const out = execFileSync27("git", ["rev-list", "--count", `HEAD..origin/${base}`], {
17744
18409
  encoding: "utf8",
17745
18410
  stdio: ["ignore", "pipe", "pipe"]
17746
18411
  }).trim();
@@ -17783,9 +18448,9 @@ async function reviewStatusCommand(args) {
17783
18448
 
17784
18449
  // src/commands/studio.ts
17785
18450
  import { execFile as execFile5, spawn as spawn3 } from "node:child_process";
17786
- import { existsSync as existsSync24, readFileSync as readFileSync33, renameSync as renameSync2 } from "node:fs";
18451
+ import { existsSync as existsSync26, readFileSync as readFileSync33, renameSync as renameSync2 } from "node:fs";
17787
18452
  import net from "node:net";
17788
- import { dirname as dirname11, join as join32 } from "node:path";
18453
+ import { dirname as dirname12, join as join31 } from "node:path";
17789
18454
  import { promisify as promisify5 } from "node:util";
17790
18455
 
17791
18456
  // src/lib/studio-bundle.ts
@@ -17838,9 +18503,9 @@ class PortFlagError extends Error {
17838
18503
  function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
17839
18504
  let dir = cwd;
17840
18505
  for (;; ) {
17841
- const webDir = join32(dir, "apps", "vinaya-studio", "web");
17842
- const pkgPath = join32(webDir, "package.json");
17843
- if (existsSync24(pkgPath)) {
18506
+ const webDir = join31(dir, "apps", "vinaya-studio", "web");
18507
+ const pkgPath = join31(webDir, "package.json");
18508
+ if (existsSync26(pkgPath)) {
17844
18509
  try {
17845
18510
  const pkg = JSON.parse(readFileSync33(pkgPath, "utf-8"));
17846
18511
  if (pkg.name === "@atta/vinaya-studio-web") {
@@ -17848,15 +18513,15 @@ function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
17848
18513
  }
17849
18514
  } catch {}
17850
18515
  }
17851
- if (existsSync24(join32(dir, ".git")))
18516
+ if (existsSync26(join31(dir, ".git")))
17852
18517
  break;
17853
- const parent = dirname11(dir);
18518
+ const parent = dirname12(dir);
17854
18519
  if (parent === dir)
17855
18520
  break;
17856
18521
  dir = parent;
17857
18522
  }
17858
- const standaloneWebDir = join32(packageRoot(moduleUrl), "studio-standalone", "apps", "vinaya-studio", "web");
17859
- if (existsSync24(join32(standaloneWebDir, "server.js"))) {
18523
+ const standaloneWebDir = join31(packageRoot(moduleUrl), "studio-standalone", "apps", "vinaya-studio", "web");
18524
+ if (existsSync26(join31(standaloneWebDir, "server.js"))) {
17860
18525
  return { kind: "package", packageDir: standaloneWebDir };
17861
18526
  }
17862
18527
  return { kind: "missing" };
@@ -17876,9 +18541,9 @@ function isPortFree(port) {
17876
18541
  });
17877
18542
  }
17878
18543
  function ensureStudioNodeModules(bundleRoot) {
17879
- const real = join32(bundleRoot, "node_modules");
17880
- const packed = join32(bundleRoot, STUDIO_NODE_MODULES_PACKED_DIRNAME);
17881
- if (!existsSync24(real) && existsSync24(packed)) {
18544
+ const real = join31(bundleRoot, "node_modules");
18545
+ const packed = join31(bundleRoot, STUDIO_NODE_MODULES_PACKED_DIRNAME);
18546
+ if (!existsSync26(real) && existsSync26(packed)) {
17882
18547
  renameSync2(packed, real);
17883
18548
  }
17884
18549
  }
@@ -17927,8 +18592,8 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
17927
18592
  }
17928
18593
  return spawnDev(target.webDir, args);
17929
18594
  case "package": {
17930
- const bundleRoot = join32(target.packageDir, "..", "..", "..");
17931
- return spawnStandalone(cwd, join32(target.packageDir, "server.js"), bundleRoot, explicitPort);
18595
+ const bundleRoot = join31(target.packageDir, "..", "..", "..");
18596
+ return spawnStandalone(cwd, join31(target.packageDir, "server.js"), bundleRoot, explicitPort);
17932
18597
  }
17933
18598
  case "missing":
17934
18599
  console.error("Vinaya Studio isn't available in this install — no published @attalabs/vinaya build bundles the Studio app yet. Inside a checkout that contains Studio's source (apps/vinaya-studio/web), `vinaya studio` runs it directly.");
@@ -17936,12 +18601,180 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
17936
18601
  }
17937
18602
  }
17938
18603
 
18604
+ // src/lib/dispatch-task.ts
18605
+ import { execFileSync as execFileSync28 } from "node:child_process";
18606
+ import { createHash as createHash7 } from "node:crypto";
18607
+ import { mkdtempSync as mkdtempSync5, rmSync as rmSync9, writeFileSync as writeFileSync18 } from "node:fs";
18608
+ import { tmpdir as tmpdir7 } from "node:os";
18609
+ import { join as join32 } from "node:path";
18610
+ var DISPATCH_AGENTS = ["claude", "codex", "gemini"];
18611
+
18612
+ class DispatchTaskError extends Error {
18613
+ }
18614
+ function sh5(cmd, args) {
18615
+ return execFileSync28(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
18616
+ }
18617
+ function briefHash(brief) {
18618
+ return createHash7("sha256").update(`${brief}
18619
+ `).digest("hex");
18620
+ }
18621
+ function fetchIssueComments2(n) {
18622
+ let out;
18623
+ try {
18624
+ out = sh5("gh", ["issue", "view", String(n), "--json", "comments"]);
18625
+ } catch (err) {
18626
+ throw new DispatchTaskError(`could not fetch Issue #${n}'s comments (\`gh issue view\`) to check for an existing brief — refusing rather than risking a duplicate: ${err instanceof Error ? err.message : String(err)}`);
18627
+ }
18628
+ try {
18629
+ return JSON.parse(out).comments.map((c) => ({
18630
+ body: c.body,
18631
+ url: c.url,
18632
+ author: c.author?.login ?? null
18633
+ }));
18634
+ } catch {
18635
+ throw new DispatchTaskError(`could not parse \`gh issue view ${n} --json comments\` output.`);
18636
+ }
18637
+ }
18638
+ function principalAllowlist2() {
18639
+ return resolvePrincipalAllowlist(loadTrustAnchorConfig());
18640
+ }
18641
+ function fetchIssueBody(n) {
18642
+ try {
18643
+ return JSON.parse(sh5("gh", ["issue", "view", String(n), "--json", "body"])).body;
18644
+ } catch (err) {
18645
+ throw new DispatchTaskError(`could not fetch Issue #${n}'s body (\`gh issue view\`) to resolve its suggested agent-class: ${err instanceof Error ? err.message : String(err)}`);
18646
+ }
18647
+ }
18648
+ function extractAgentClass(rawRationaleField) {
18649
+ const m = /agent-class\**\s*[—–-]\s*(\w+)/i.exec(rawRationaleField);
18650
+ const word = m?.[1]?.toLowerCase();
18651
+ return word !== undefined && isAgentClass(word) ? word : null;
18652
+ }
18653
+ function resolveModelFromRationale(agent, rawRationaleField, explicitModel) {
18654
+ if (explicitModel !== undefined)
18655
+ return explicitModel;
18656
+ const agentClass = rawRationaleField !== undefined ? extractAgentClass(rawRationaleField) : null;
18657
+ if (agentClass === null)
18658
+ return;
18659
+ return resolveClassModel(agent, agentClass) ?? undefined;
18660
+ }
18661
+ function resolveModelForDispatch(agent, issue, explicitModel) {
18662
+ if (explicitModel !== undefined)
18663
+ return explicitModel;
18664
+ const raw = parseRationaleFields(fetchIssueBody(issue)).suggestedAgentClass;
18665
+ return resolveModelFromRationale(agent, raw, undefined);
18666
+ }
18667
+ function findExistingFrozenBrief(n) {
18668
+ return resolveNewestFrozenBrief(fetchIssueComments2(n), principalAllowlist2());
18669
+ }
18670
+ async function withPromptFile2(prompt2, fn) {
18671
+ const dir = mkdtempSync5(join32(tmpdir7(), "vinaya-dispatch-prompt-"));
18672
+ const promptFile = join32(dir, "prompt.md");
18673
+ writeFileSync18(promptFile, prompt2, "utf8");
18674
+ try {
18675
+ return await fn(promptFile);
18676
+ } finally {
18677
+ rmSync9(dir, { recursive: true, force: true });
18678
+ }
18679
+ }
18680
+ function resolveDispatchAuthorization() {
18681
+ const login = currentGhLogin();
18682
+ const allowlist = resolvePrincipalAllowlist(loadTrustAnchorConfig());
18683
+ return { authorized: login !== null && isPrincipal(login, allowlist), login };
18684
+ }
18685
+ var defaultPrepareTaskDeps = {
18686
+ assembleAndRenderBrief,
18687
+ findExistingFrozenBrief,
18688
+ postMarkedComment,
18689
+ resolveDispatchAuthorization
18690
+ };
18691
+ async function prepareTask(input, deps = defaultPrepareTaskDeps) {
18692
+ const { tranche, n, supersede } = input;
18693
+ {
18694
+ const { authorized, login } = deps.resolveDispatchAuthorization();
18695
+ if (!authorized) {
18696
+ throw new DispatchTaskError(login === null ? "could not resolve the identity `gh` is authenticated as — preparing a task is Principal-only and refuses rather than proceeding with an unverified actor." : `\`${login}\` is not on the Principal allowlist — preparing a task is Principal-only.`);
18697
+ }
18698
+ }
18699
+ if (supersede && supersede.reason.trim().length === 0) {
18700
+ throw new DispatchTaskError("--supersede requires --reason <text> — a superseding brief must name why the prior one was wrong, same authorization as a first freeze, never a silent rewrite.");
18701
+ }
18702
+ if (supersede && /[\r\n]/.test(supersede.reason)) {
18703
+ throw new DispatchTaskError("--reason must be a single line — it becomes one line of the frozen comment header, and a newline in it would corrupt every reader's header-line count for this version.");
18704
+ }
18705
+ const result = await deps.assembleAndRenderBrief(tranche, String(n));
18706
+ if (!result.ok) {
18707
+ throw new DispatchTaskError(`cannot dispatch — brief render refused:
18708
+ ${result.missing.map((m) => ` - ${m}`).join(`
18709
+ `)}`);
18710
+ }
18711
+ const issue = result.issue;
18712
+ const existing = deps.findExistingFrozenBrief(issue);
18713
+ const hash = briefHash(result.brief);
18714
+ let marker;
18715
+ let commentBody;
18716
+ let version;
18717
+ if (supersede) {
18718
+ if (!existing) {
18719
+ throw new DispatchTaskError(`Task ${n} in tranche \`${tranche}\` has no frozen brief yet — nothing to supersede. Run \`vinaya task brief ${tranche} ${n}\` without --supersede first.`);
18720
+ }
18721
+ version = existing.version + 1;
18722
+ marker = briefMarkerFor(version);
18723
+ commentBody = `Brief hash: ${hash}
18724
+ Supersedes: ${existing.url} — ${supersede.reason}
18725
+ ${result.brief}`;
18726
+ } else {
18727
+ if (existing) {
18728
+ throw new DispatchTaskError(`Task ${n} in tranche \`${tranche}\` is already dispatched — see ${existing.url}`);
18729
+ }
18730
+ version = 1;
18731
+ marker = AEG_BRIEF_V1_MARKER;
18732
+ commentBody = `Brief hash: ${hash}
18733
+ ${result.brief}`;
18734
+ }
18735
+ if (deps.beforePost) {
18736
+ await deps.beforePost(issue);
18737
+ }
18738
+ const url = deps.postMarkedComment("issue", String(issue), marker, commentBody);
18739
+ return { issue, brief: result.brief, commentUrl: url, version };
18740
+ }
18741
+ var defaultDeps3 = {
18742
+ assembleAndRenderBrief,
18743
+ findExistingFrozenBrief,
18744
+ postMarkedComment,
18745
+ dispatchRole,
18746
+ resolveDispatchAuthorization,
18747
+ resolveModelForDispatch
18748
+ };
18749
+ async function dispatchTask(input, deps = defaultDeps3) {
18750
+ const { tranche, n, agent, model } = input;
18751
+ let resolvedModel;
18752
+ const prep = await prepareTask({ tranche, n }, {
18753
+ assembleAndRenderBrief: deps.assembleAndRenderBrief,
18754
+ findExistingFrozenBrief: deps.findExistingFrozenBrief,
18755
+ postMarkedComment: deps.postMarkedComment,
18756
+ resolveDispatchAuthorization: deps.resolveDispatchAuthorization,
18757
+ beforePost: agent ? async (issue) => {
18758
+ resolvedModel = deps.resolveModelForDispatch(agent, issue, model);
18759
+ } : undefined
18760
+ });
18761
+ if (agent) {
18762
+ await withPromptFile2(prep.brief, (promptFile) => deps.dispatchRole("developer", agent, prep.brief, {
18763
+ task: prep.issue,
18764
+ promptFile,
18765
+ model: resolvedModel
18766
+ }));
18767
+ }
18768
+ return { posted: true, commentUrl: prep.commentUrl, brief: prep.brief };
18769
+ }
18770
+
17939
18771
  // src/commands/task.ts
17940
18772
  async function taskDispatchCommand(args) {
17941
18773
  const trancheSlug = args[0];
17942
18774
  const taskIdArg = args[1];
17943
18775
  if (!trancheSlug || !taskIdArg || trancheSlug.startsWith("--")) {
17944
- console.error(`Usage: vinaya task dispatch <tranche> <n> [--agent ${DISPATCH_AGENTS.join(" | ")}] [--model <name>]`);
18776
+ console.error(`Usage: vinaya task dispatch <tranche> <n> [--agent ${DISPATCH_AGENTS.join(" | ")}] [--model <name>]
18777
+ ` + "Deprecated: prefer `vinaya task brief` (preparation only) or `vinaya task run` (the full unattended loop).");
17945
18778
  process.exit(2);
17946
18779
  }
17947
18780
  const n = Number.parseInt(taskIdArg, 10);
@@ -17978,9 +18811,155 @@ async function taskDispatchCommand(args) {
17978
18811
  Posted: ${result.commentUrl}
17979
18812
  `);
17980
18813
  }
18814
+ async function taskBriefCommand(args) {
18815
+ const trancheSlug = args[0];
18816
+ const taskIdArg = args[1];
18817
+ if (!trancheSlug || !taskIdArg || trancheSlug.startsWith("--")) {
18818
+ console.error("Usage: vinaya task brief <tranche> <n> [--supersede --reason <text>]");
18819
+ process.exit(2);
18820
+ }
18821
+ const n = Number.parseInt(taskIdArg, 10);
18822
+ if (!Number.isInteger(n) || String(n) !== taskIdArg) {
18823
+ console.error(`vinaya task brief: task id must be numeric — got "${taskIdArg}".`);
18824
+ process.exit(2);
18825
+ }
18826
+ const rest = args.slice(2);
18827
+ const hasSupersede = rest.includes("--supersede");
18828
+ const reasonIdx = rest.indexOf("--reason");
18829
+ const reason = reasonIdx !== -1 ? rest[reasonIdx + 1] : undefined;
18830
+ if (hasSupersede && !reason) {
18831
+ console.error("vinaya task brief: --supersede requires --reason <text>.");
18832
+ process.exit(2);
18833
+ }
18834
+ if (!hasSupersede && reasonIdx !== -1) {
18835
+ console.error("vinaya task brief: --reason is only meaningful with --supersede.");
18836
+ process.exit(2);
18837
+ }
18838
+ const result = await prepareTask({
18839
+ tranche: trancheSlug,
18840
+ n,
18841
+ supersede: hasSupersede ? { reason } : undefined
18842
+ });
18843
+ process.stdout.write(`${result.brief}
18844
+ `);
18845
+ process.stdout.write(`
18846
+ Posted (v${result.version}): ${result.commentUrl}
18847
+ `);
18848
+ }
18849
+
18850
+ // src/lib/task-run.ts
18851
+ class RunTaskError extends Error {
18852
+ }
18853
+ var ALREADY_DISPATCHED_PATTERN = /is already dispatched/;
18854
+ function isAlreadyDispatchedError(err) {
18855
+ return err instanceof DispatchTaskError && ALREADY_DISPATCHED_PATTERN.test(err.message);
18856
+ }
18857
+ var defaultRunTaskDeps = {
18858
+ prepareTask,
18859
+ assembleAndRenderBrief,
18860
+ developerBranchFor,
18861
+ findOpenPrForBranch,
18862
+ devReviewLoop,
18863
+ resolveRepo: () => resolveRepo()
18864
+ };
18865
+ async function resolvePrUrl(resolveRepo3, prNumber) {
18866
+ const repo = await resolveRepo3().catch(() => null);
18867
+ return repo ? `https://github.com/${repo.owner}/${repo.repo}/pull/${prNumber}` : null;
18868
+ }
18869
+ async function runTask(input, deps = defaultRunTaskDeps) {
18870
+ const { tranche, n, agent } = input;
18871
+ let issue;
18872
+ try {
18873
+ const prep = await deps.prepareTask({ tranche, n });
18874
+ issue = prep.issue;
18875
+ } catch (err) {
18876
+ if (!isAlreadyDispatchedError(err))
18877
+ throw err;
18878
+ const rendered = await deps.assembleAndRenderBrief(tranche, String(n));
18879
+ if (!rendered.ok) {
18880
+ throw new RunTaskError(`runTask: task ${n} in tranche \`${tranche}\` was already dispatched, but re-resolving its Issue number failed:
18881
+ ${rendered.missing.map((m) => ` - ${m}`).join(`
18882
+ `)}`);
18883
+ }
18884
+ issue = rendered.issue;
18885
+ }
18886
+ const branch = deps.developerBranchFor(issue);
18887
+ const existingPr = deps.findOpenPrForBranch(branch);
18888
+ if (existingPr) {
18889
+ throw new RunTaskError(`runTask: task ${n} in tranche \`${tranche}\`'s developer branch \`${branch}\` already has an open pull request (#${existingPr.number}) — refusing to start a second developer. Resume the review loop instead: \`vinaya dev-review-loop --resume ${existingPr.number}\`.`);
18890
+ }
18891
+ const loopResult = await deps.devReviewLoop({ task: issue, agent });
18892
+ const prUrl = await resolvePrUrl(deps.resolveRepo, loopResult.prNumber);
18893
+ return { ...loopResult, prUrl };
18894
+ }
18895
+
18896
+ // src/commands/task-run.ts
18897
+ var TASK_RUN_FAILURE_EXIT_CODE = 3;
18898
+ var KNOWN_FLAGS2 = ["--agent"];
18899
+ function parseFlags2(rest) {
18900
+ let agent;
18901
+ const unknown = [];
18902
+ for (let i = 0;i < rest.length; i++) {
18903
+ const a = rest[i];
18904
+ if (a === "--agent")
18905
+ agent = rest[++i];
18906
+ else if (a !== undefined)
18907
+ unknown.push(a);
18908
+ }
18909
+ return { agent, unknown };
18910
+ }
18911
+ async function taskRunCommand(args) {
18912
+ const trancheSlug = args[0];
18913
+ const taskIdArg = args[1];
18914
+ if (!trancheSlug || !taskIdArg || trancheSlug.startsWith("--")) {
18915
+ console.error(`Usage: vinaya task run <tranche> <n> --agent ${DISPATCH_AGENTS.join(" | ")}`);
18916
+ process.exit(2);
18917
+ }
18918
+ const n = Number.parseInt(taskIdArg, 10);
18919
+ if (!Number.isInteger(n) || String(n) !== taskIdArg) {
18920
+ console.error(`vinaya task run: task id must be numeric — got "${taskIdArg}".`);
18921
+ process.exit(2);
18922
+ }
18923
+ const parsed = parseFlags2(args.slice(2));
18924
+ if (parsed.unknown.length > 0) {
18925
+ console.error(`vinaya task run: unrecognized flag${parsed.unknown.length > 1 ? "s" : ""} ${parsed.unknown.map((f) => `'${f}'`).join(", ")} — expected one of ${KNOWN_FLAGS2.join(", ")}`);
18926
+ process.exit(2);
18927
+ }
18928
+ if (!parsed.agent || !DISPATCH_AGENTS.includes(parsed.agent)) {
18929
+ console.error(`vinaya task run: --agent <${DISPATCH_AGENTS.join("|")}> is required.`);
18930
+ process.exit(2);
18931
+ }
18932
+ const agent = parsed.agent;
18933
+ let result;
18934
+ try {
18935
+ result = await runTask({ tranche: trancheSlug, n, agent });
18936
+ } catch (err) {
18937
+ const message = err instanceof Error ? err.message : String(err);
18938
+ process.stderr.write(`Error: ${message}
18939
+ `);
18940
+ process.exit(TASK_RUN_FAILURE_EXIT_CODE);
18941
+ }
18942
+ const prRef = result.prUrl ?? `PR #${result.prNumber}`;
18943
+ const decision = result.finalDecision;
18944
+ if (decision.type === "publish") {
18945
+ process.stdout.write(`${colourLoopLine(`vinaya task run: task ${result.task}, ${prRef} — published`, process.stdout)}
18946
+ `);
18947
+ return;
18948
+ }
18949
+ if (decision.type !== "pause") {
18950
+ process.stderr.write(`Error: vinaya task run: devReviewLoop returned an unexpected final decision type \`${decision.type}\`.
18951
+ `);
18952
+ process.exit(TASK_RUN_FAILURE_EXIT_CODE);
18953
+ }
18954
+ process.stdout.write(`${colourLoopLine(`vinaya task run: task ${result.task}, ${prRef} — paused (${decision.reason})`, process.stdout)}
18955
+ `);
18956
+ process.stdout.write(`Resume with: vinaya dev-review-loop --resume ${result.prNumber}
18957
+ `);
18958
+ process.exit(1);
18959
+ }
17981
18960
 
17982
18961
  // src/commands/upgrade.ts
17983
- import { existsSync as existsSync25, readFileSync as readFileSync34, rmSync as rmSync10, writeFileSync as writeFileSync19 } from "node:fs";
18962
+ import { existsSync as existsSync27, readFileSync as readFileSync34, rmSync as rmSync10, writeFileSync as writeFileSync19 } from "node:fs";
17984
18963
  import { join as join33 } from "node:path";
17985
18964
  function realDeps9() {
17986
18965
  return {
@@ -18000,7 +18979,7 @@ function flags2(args) {
18000
18979
  }
18001
18980
  function readManifest4(repoRoot3) {
18002
18981
  const p = join33(repoRoot3, CONFIG_PATH);
18003
- if (!existsSync25(p))
18982
+ if (!existsSync27(p))
18004
18983
  return { kind: "missing" };
18005
18984
  let raw;
18006
18985
  try {
@@ -18028,7 +19007,7 @@ function writeManifestVersion(repoRoot3, manifest) {
18028
19007
  }
18029
19008
  function stripFor(repoRoot3, path, marker, comment) {
18030
19009
  const abs2 = resolveManagedBlockPath(repoRoot3, path);
18031
- if (!existsSync25(abs2))
19010
+ if (!existsSync27(abs2))
18032
19011
  return { path, marker, comment, present: false, removesHost: false };
18033
19012
  const stripped = stripBlockFromContent(readFileSync34(abs2, "utf-8"), marker, comment);
18034
19013
  if (stripped === null)
@@ -18057,7 +19036,7 @@ function planHookRouting(repoRoot3, manifest, recorded, hooksPathValue) {
18057
19036
  const blocked = [];
18058
19037
  for (const b of legacy) {
18059
19038
  const abs2 = resolveManagedBlockPath(repoRoot3, b.path);
18060
- if (!existsSync25(abs2))
19039
+ if (!existsSync27(abs2))
18061
19040
  continue;
18062
19041
  const stripped = stripBlockFromContent(readFileSync34(abs2, "utf-8"), b.marker, b.comment);
18063
19042
  if (stripped === null)
@@ -18094,7 +19073,7 @@ function planUpgrade(ops, repoRoot3, manifest, routing, staleSkillPaths = []) {
18094
19073
  const entries = [];
18095
19074
  const staleFiles = staleSkillPaths.map((path) => ({
18096
19075
  path,
18097
- present: existsSync25(join33(repoRoot3, path))
19076
+ present: existsSync27(join33(repoRoot3, path))
18098
19077
  }));
18099
19078
  let hasChanges = routing.arm || routing.migratesManifest || routing.strips.some((s) => s.present) || staleFiles.some((f) => f.present);
18100
19079
  const ownedFiles = new Set(manifest.files);
@@ -18103,7 +19082,7 @@ function planUpgrade(ops, repoRoot3, manifest, routing, staleSkillPaths = []) {
18103
19082
  for (const op of ops) {
18104
19083
  if (op.kind === "create-file") {
18105
19084
  const abs2 = join33(repoRoot3, op.path);
18106
- const exists = existsSync25(abs2);
19085
+ const exists = existsSync27(abs2);
18107
19086
  const owned = ownedFiles.has(op.path) || isDefaultedAgentVendorPath(op.path, manifest);
18108
19087
  let action;
18109
19088
  let triggerChange;
@@ -18140,7 +19119,7 @@ function planUpgrade(ops, repoRoot3, manifest, routing, staleSkillPaths = []) {
18140
19119
  let action;
18141
19120
  if (!owned) {
18142
19121
  action = "not-installed";
18143
- } else if (!existsSync25(abs2)) {
19122
+ } else if (!existsSync27(abs2)) {
18144
19123
  action = "recreate-host";
18145
19124
  hasChanges = true;
18146
19125
  } else {
@@ -18314,7 +19293,7 @@ function applyUpgrade(plan, repoRoot3) {
18314
19293
  if (!s.present)
18315
19294
  continue;
18316
19295
  const abs2 = resolveManagedBlockPath(repoRoot3, s.path);
18317
- if (!existsSync25(abs2))
19296
+ if (!existsSync27(abs2))
18318
19297
  continue;
18319
19298
  const stripped = stripBlockFromContent(readFileSync34(abs2, "utf-8"), s.marker, s.comment);
18320
19299
  if (stripped === null)
@@ -18542,7 +19521,34 @@ function printHelp() {
18542
19521
  }
18543
19522
 
18544
19523
  // src/index.ts
18545
- var PACKAGE_ROOT = join34(dirname12(fileURLToPath2(import.meta.url)), "..");
19524
+ var PACKAGE_ROOT = join34(dirname13(fileURLToPath2(import.meta.url)), "..");
19525
+ function maybeDeferToAuthorRepoSource() {
19526
+ if (process.env.VINAYA_NO_DEFER === "1")
19527
+ return;
19528
+ if (process.env.GITHUB_ACTIONS)
19529
+ return;
19530
+ const sourceEntry = resolveAuthorRepoSourceEntry(packageRoot(import.meta.url));
19531
+ if (!sourceEntry)
19532
+ return;
19533
+ const bunCheck = spawnSync3("bun", ["--version"], { stdio: "ignore" });
19534
+ if (bunCheck.error || bunCheck.status !== 0) {
19535
+ process.stderr.write(`vinaya: author repo detected, but 'bun' is not on PATH — running the installed build instead of ${sourceEntry}
19536
+ `);
19537
+ return;
19538
+ }
19539
+ process.stderr.write(`vinaya: deferring to source at ${sourceEntry}
19540
+ `);
19541
+ const result = spawnSync3("bun", [sourceEntry, ...process.argv.slice(2)], {
19542
+ stdio: "inherit",
19543
+ env: process.env
19544
+ });
19545
+ if (result.signal) {
19546
+ process.kill(process.pid, result.signal);
19547
+ return;
19548
+ }
19549
+ process.exit(result.status ?? 1);
19550
+ }
19551
+ maybeDeferToAuthorRepoSource();
18546
19552
  function readVersion2() {
18547
19553
  const pkg = JSON.parse(readFileSync35(join34(PACKAGE_ROOT, "package.json"), "utf-8"));
18548
19554
  return pkg.version;
@@ -18677,8 +19683,12 @@ try {
18677
19683
  const [subcommand, ...rest] = args;
18678
19684
  if (subcommand === "dispatch") {
18679
19685
  await taskDispatchCommand(rest);
19686
+ } else if (subcommand === "brief") {
19687
+ await taskBriefCommand(rest);
19688
+ } else if (subcommand === "run") {
19689
+ await taskRunCommand(rest);
18680
19690
  } else {
18681
- console.error(`Unknown 'task' subcommand: ${subcommand ?? "(none)"} (expected 'dispatch')`);
19691
+ console.error(`Unknown 'task' subcommand: ${subcommand ?? "(none)"} (expected 'dispatch', 'brief', or 'run')`);
18682
19692
  process.exit(2);
18683
19693
  }
18684
19694
  break;
@@ -18703,8 +19713,10 @@ try {
18703
19713
  await milestoneEditCommand(rest);
18704
19714
  } else if (subcommand === "close") {
18705
19715
  await milestoneCloseCommand(rest);
19716
+ } else if (subcommand === "status") {
19717
+ await milestoneStatusCommand(rest);
18706
19718
  } else {
18707
- console.error(`Unknown 'milestone' subcommand: ${subcommand ?? "(none)"} (expected create/adopt/edit/close)`);
19719
+ console.error(`Unknown 'milestone' subcommand: ${subcommand ?? "(none)"} (expected create/adopt/edit/close/status)`);
18708
19720
  process.exit(2);
18709
19721
  }
18710
19722
  break;