@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
@@ -315,7 +315,7 @@ function issueListByLabelArgs(owner, repo, label) {
315
315
  "--state",
316
316
  "all",
317
317
  "--json",
318
- "number,title,body,state,labels,milestone",
318
+ "number,title,body,state,labels,milestone,stateReason",
319
319
  "--limit",
320
320
  "200"
321
321
  ];
@@ -661,14 +661,19 @@ function resolveTaskIssueRef(title, labels) {
661
661
  var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
662
662
  var NEXT_HEADING = /^#{1,6}\s+\S/m;
663
663
  var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
664
- function intentGoalForSlug(description, slug) {
665
- const text = stripCode(description, { inlineSpans: "keep" });
664
+ function intentsSection(text) {
666
665
  const start = text.match(INTENTS_HEADING);
667
666
  if (!start || start.index === undefined)
668
- return "";
667
+ return null;
669
668
  const rest = text.slice(start.index + start[0].length);
670
669
  const next = rest.match(NEXT_HEADING);
671
- const section = rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
670
+ return rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
671
+ }
672
+ function intentGoalForSlug(description, slug) {
673
+ const text = stripCode(description, { inlineSpans: "keep" });
674
+ const section = intentsSection(text);
675
+ if (section === null)
676
+ return "";
672
677
  for (const line of section.split(`
673
678
  `)) {
674
679
  const trimmed = line.trim();
@@ -1960,6 +1965,7 @@ function parseInlineFieldList(section) {
1960
1965
  // ../../packages/aeg-core/src/verdict-extraction.ts
1961
1966
  var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
1962
1967
  var OBJECTIVES_VERSION_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Objectives version:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1968
+ var RULING_ORDINAL_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Ruling ordinal:\s*(\d+)(?!\d)/im;
1963
1969
  function firstFiveLines(comment) {
1964
1970
  return comment.split(`
1965
1971
  `).slice(0, 5).join(`
@@ -1973,6 +1979,15 @@ function extractObjectivesVersion(comment) {
1973
1979
  const m = firstFiveLines(comment).match(OBJECTIVES_VERSION_PATTERN);
1974
1980
  return m ? m[1].toLowerCase() : null;
1975
1981
  }
1982
+ function firstSevenLines(comment) {
1983
+ return comment.split(`
1984
+ `).slice(0, 7).join(`
1985
+ `);
1986
+ }
1987
+ function extractRulingOrdinal(comment) {
1988
+ const m = firstSevenLines(comment).match(RULING_ORDINAL_PATTERN);
1989
+ return m ? Number.parseInt(m[1], 10) : null;
1990
+ }
1976
1991
  function extractVerdict(comments, valuePattern, missingLabel) {
1977
1992
  const candidates = comments.filter((c) => valuePattern.test(c));
1978
1993
  if (candidates.length === 0) {
@@ -1980,6 +1995,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1980
1995
  value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
1981
1996
  headSha: null,
1982
1997
  objectivesVersion: null,
1998
+ rulingOrdinal: null,
1983
1999
  danglingNote: `no ${missingLabel} verdict comment found on this PR`
1984
2000
  };
1985
2001
  }
@@ -1990,6 +2006,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1990
2006
  value: `the most recent ${missingLabel} comment's VERDICT line is not within its first five lines — DANGLING, see below`,
1991
2007
  headSha: null,
1992
2008
  objectivesVersion: null,
2009
+ rulingOrdinal: null,
1993
2010
  danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
1994
2011
  };
1995
2012
  }
@@ -1997,6 +2014,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1997
2014
  value: m[1].toUpperCase().replace(/[_-]/g, " "),
1998
2015
  headSha: extractHeadSha(latest),
1999
2016
  objectivesVersion: extractObjectivesVersion(latest),
2017
+ rulingOrdinal: extractRulingOrdinal(latest),
2000
2018
  danglingNote: null
2001
2019
  };
2002
2020
  }
@@ -2358,6 +2376,9 @@ import { createHash as createHash2 } from "node:crypto";
2358
2376
  var HEADING_RE = /^##[ \t]*Objectives[ \t]*$/im;
2359
2377
  var NEXT_HEADING_RE = /^##[ \t]/m;
2360
2378
  var OBJECTIVE_LINE_RE = /^O(\d+)\.[ \t]*(.*)$/;
2379
+ function maskedForHeadingSearch(body) {
2380
+ return maskDetailsBlocks(maskCode(body));
2381
+ }
2361
2382
  function hasBacktickedPath(text) {
2362
2383
  let i = 0;
2363
2384
  while (i < text.length) {
@@ -2380,16 +2401,22 @@ function stripObjectiveBackticks(text) {
2380
2401
  function wordCount(text) {
2381
2402
  return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
2382
2403
  }
2383
- function objectivesSectionText(body) {
2384
- const heading = HEADING_RE.exec(body);
2404
+ function objectivesSectionBounds(body) {
2405
+ const masked = maskedForHeadingSearch(body);
2406
+ const heading = HEADING_RE.exec(masked);
2385
2407
  if (!heading)
2386
2408
  return null;
2387
- const afterHeading = body.slice(heading.index + heading[0].length);
2409
+ const start = heading.index + heading[0].length;
2410
+ const afterHeading = masked.slice(start);
2388
2411
  const next = NEXT_HEADING_RE.exec(afterHeading);
2389
- return next ? afterHeading.slice(0, next.index) : afterHeading;
2412
+ return { start, end: next ? start + next.index : body.length };
2413
+ }
2414
+ function objectivesSectionText(body) {
2415
+ const bounds = objectivesSectionBounds(body);
2416
+ return bounds === null ? null : body.slice(bounds.start, bounds.end);
2390
2417
  }
2391
2418
  function hasObjectivesHeading(body) {
2392
- return HEADING_RE.test(body);
2419
+ return HEADING_RE.test(maskedForHeadingSearch(body));
2393
2420
  }
2394
2421
  function objectivesOf(body) {
2395
2422
  const section = objectivesSectionText(body);
@@ -2449,6 +2476,15 @@ function isIssueNotFoundError(err) {
2449
2476
  `);
2450
2477
  return /could not resolve to an (?:issue|pull request)|\b404\b|not found/i.test(haystack);
2451
2478
  }
2479
+ function resolveObjectivesSource(prBody, issue, cutoverIssue) {
2480
+ if (issue !== null && issue < cutoverIssue)
2481
+ return { kind: "none" };
2482
+ if (issue !== null)
2483
+ return { kind: "issue", issue };
2484
+ if (hasObjectivesHeading(prBody))
2485
+ return { kind: "body" };
2486
+ return { kind: "none" };
2487
+ }
2452
2488
 
2453
2489
  // ../../packages/aeg-core/src/premise-check.ts
2454
2490
  import { createHash as createHash3 } from "node:crypto";
@@ -2950,17 +2986,40 @@ function checkBriefSections(prBody, readTier, options = {}) {
2950
2986
  ];
2951
2987
  return { errors: results.flatMap((r) => r.errors) };
2952
2988
  }
2953
- var AEG_BRIEF_V1_MARKER = "<!-- aeg:brief:v1 -->";
2954
- function contentAfterTwoLines(body) {
2955
- const first = body.indexOf(`
2956
- `);
2957
- if (first === -1)
2958
- return "";
2959
- const second = body.indexOf(`
2960
- `, first + 1);
2961
- if (second === -1)
2962
- return "";
2963
- return body.slice(second + 1);
2989
+ function contentAfterNLines(body, n) {
2990
+ let idx = -1;
2991
+ for (let i = 0;i < n; i++) {
2992
+ idx = body.indexOf(`
2993
+ `, idx + 1);
2994
+ if (idx === -1)
2995
+ return "";
2996
+ }
2997
+ return body.slice(idx + 1);
2998
+ }
2999
+ var BRIEF_MARKER_LINE_RE = /^<!-- aeg:brief:v(\d+) -->$/;
3000
+ function parseBriefMarkerVersion(firstLine) {
3001
+ const m = BRIEF_MARKER_LINE_RE.exec(firstLine.trim());
3002
+ if (!m)
3003
+ return null;
3004
+ const version = Number.parseInt(m[1], 10);
3005
+ return Number.isInteger(version) && version >= 1 ? version : null;
3006
+ }
3007
+ function frozenBriefContent(body, version) {
3008
+ return contentAfterNLines(body, version === 1 ? 2 : 3);
3009
+ }
3010
+ function resolveNewestFrozenBrief(comments, allowlist) {
3011
+ let best = null;
3012
+ for (const c of comments) {
3013
+ if (!isPrincipal(c.author, allowlist))
3014
+ continue;
3015
+ const version = parseBriefMarkerVersion(c.body.split(`
3016
+ `)[0] ?? "");
3017
+ if (version === null)
3018
+ continue;
3019
+ if (best === null || version > best.version)
3020
+ best = { ...c, version };
3021
+ }
3022
+ return best === null ? null : { ...best, content: frozenBriefContent(best.body, best.version) };
2964
3023
  }
2965
3024
  // ../../packages/aeg-core/src/doctrine-portability.ts
2966
3025
  var DEFAULT_SHIPS_PREFIX = "aeg-root/";
@@ -3687,6 +3746,11 @@ function isBoundToObjectives(extraction, currentVersion) {
3687
3746
  return true;
3688
3747
  return extraction.objectivesVersion === currentVersion;
3689
3748
  }
3749
+ function isBoundToRulings(extraction, currentOrdinal) {
3750
+ if (extraction.rulingOrdinal === null)
3751
+ return currentOrdinal === 0;
3752
+ return extraction.rulingOrdinal === currentOrdinal;
3753
+ }
3690
3754
  function checkReviewGate(input) {
3691
3755
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
3692
3756
  const waived = isWaiverLabelActorVerified({
@@ -3715,7 +3779,9 @@ function checkReviewGate(input) {
3715
3779
  const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
3716
3780
  const codeReviewObjectivesBound = isBoundToObjectives(codeReview, input.objectivesVersion);
3717
3781
  const securityObjectivesBound = isBoundToObjectives(security, input.objectivesVersion);
3718
- if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && securityClean && securityBound && securityObjectivesBound && mechanicalChecksClean) {
3782
+ const codeReviewRulingsBound = isBoundToRulings(codeReview, input.rulingOrdinal);
3783
+ const securityRulingsBound = isBoundToRulings(security, input.rulingOrdinal);
3784
+ if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && codeReviewRulingsBound && securityClean && securityBound && securityObjectivesBound && securityRulingsBound && mechanicalChecksClean) {
3719
3785
  return {
3720
3786
  verdict: "pass",
3721
3787
  reason: `code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS, both covering head ${input.headSha}, and every reported mechanical check is green.`,
@@ -3729,6 +3795,8 @@ function checkReviewGate(input) {
3729
3795
  problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3730
3796
  } else if (!codeReviewObjectivesBound) {
3731
3797
  problems.push(`the newest code-review verdict was cast against objectives version ${codeReview.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3798
+ } else if (!codeReviewRulingsBound) {
3799
+ problems.push(`the newest code-review verdict was cast against ruling ordinal ${codeReview.rulingOrdinal ?? "none"}, a newer ruling (ruling ${input.rulingOrdinal}) is now posted on this PR`);
3732
3800
  }
3733
3801
  if (!securityClean) {
3734
3802
  problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
@@ -3736,6 +3804,8 @@ function checkReviewGate(input) {
3736
3804
  problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3737
3805
  } else if (!securityObjectivesBound) {
3738
3806
  problems.push(`the newest security-review verdict was cast against objectives version ${security.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3807
+ } else if (!securityRulingsBound) {
3808
+ problems.push(`the newest security-review verdict was cast against ruling ordinal ${security.rulingOrdinal ?? "none"}, a newer ruling (ruling ${input.rulingOrdinal}) is now posted on this PR`);
3739
3809
  }
3740
3810
  if (!mechanicalChecksClean) {
3741
3811
  problems.push(reportedMechanicalChecks.length === 0 ? "no mechanical checks have reported for this head yet" : `mechanical check(s) not green: ${reportedMechanicalChecks.filter((c) => c.bucket !== "pass").map((c) => `${c.name} (${c.bucket})`).join(", ")}`);
@@ -3752,6 +3822,24 @@ var DEFAULT_RELEASE_ACTOR = "github-actions[bot]";
3752
3822
  function isChangesetsReleasePr(branch, author, expectedAuthor) {
3753
3823
  return branch === CHANGESET_RELEASE_BRANCH && author === expectedAuthor;
3754
3824
  }
3825
+ // ../../packages/aeg-core/src/ruling-ordinal.ts
3826
+ var RULING_MARKER_ORDINAL = /^<!-- aeg:principal:ruling:\d+-(\d+) -->$/;
3827
+ function newestPrincipalRulingOrdinal(comments, allowlist) {
3828
+ let best = 0;
3829
+ for (const c of comments) {
3830
+ if (!isPrincipal(c.author, allowlist))
3831
+ continue;
3832
+ const firstLine = (c.body.split(`
3833
+ `)[0] ?? "").trim();
3834
+ const m = RULING_MARKER_ORDINAL.exec(firstLine);
3835
+ if (!m)
3836
+ continue;
3837
+ const k = Number.parseInt(m[1], 10);
3838
+ if (k > best)
3839
+ best = k;
3840
+ }
3841
+ return best;
3842
+ }
3755
3843
  // ../../packages/aeg-core/src/markdown-table.ts
3756
3844
  function splitRow(line) {
3757
3845
  const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
@@ -5415,14 +5503,41 @@ var COMMANDS = [
5415
5503
  },
5416
5504
  {
5417
5505
  name: "task dispatch",
5418
- description: "Render, pin, and post the brief on the Issue as the frozen original; start the developer",
5506
+ description: "Deprecated — render, pin, and post the brief on the Issue as the frozen original; start the developer",
5419
5507
  flags: [
5420
5508
  { flag: "--agent <claude|codex|gemini>", description: "Start the developer through dispatchRole once posted" }
5421
5509
  ],
5422
5510
  details: [
5423
5511
  "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.",
5424
5512
  "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.",
5425
- "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."
5513
+ "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.",
5514
+ "Deprecated in favor of `task brief` (preparation only) and `task run` (the full unattended loop) — kept for a documented compatibility window while callers migrate."
5515
+ ],
5516
+ status: "shipped"
5517
+ },
5518
+ {
5519
+ name: "task brief",
5520
+ description: "Render and freeze the brief as the Issue's original comment — preparation only, starts nobody",
5521
+ details: [
5522
+ "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.",
5523
+ "Successor to `task dispatch` for the preparation step; the full unattended run (preparation, then the developer, then the review loop) is `task run`."
5524
+ ],
5525
+ status: "shipped"
5526
+ },
5527
+ {
5528
+ name: "task run",
5529
+ description: "One command from a planned Issue to a reviewed pull request — exactly one developer started",
5530
+ flags: [
5531
+ {
5532
+ flag: "--agent <claude|codex|gemini>",
5533
+ description: "Vendor for the developer and both reviewers this run dispatches"
5534
+ }
5535
+ ],
5536
+ details: [
5537
+ "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.",
5538
+ "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.",
5539
+ "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.",
5540
+ "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."
5426
5541
  ],
5427
5542
  status: "shipped"
5428
5543
  },
@@ -5630,6 +5745,15 @@ var COMMANDS = [
5630
5745
  ],
5631
5746
  status: "shipped"
5632
5747
  },
5748
+ {
5749
+ name: "milestone status",
5750
+ description: "Print each of a Milestone's declared tranche intents with its derived lifecycle and issue counts",
5751
+ flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
5752
+ details: [
5753
+ "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."
5754
+ ],
5755
+ status: "shipped"
5756
+ },
5633
5757
  {
5634
5758
  name: "review status",
5635
5759
  description: "Print the review loop's own state for a PR, and its branch's distance from the base",
@@ -303,7 +303,7 @@ function issueListByLabelArgs(owner, repo, label) {
303
303
  "--state",
304
304
  "all",
305
305
  "--json",
306
- "number,title,body,state,labels,milestone",
306
+ "number,title,body,state,labels,milestone,stateReason",
307
307
  "--limit",
308
308
  "200"
309
309
  ];
@@ -649,14 +649,19 @@ function resolveTaskIssueRef(title, labels) {
649
649
  var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
650
650
  var NEXT_HEADING = /^#{1,6}\s+\S/m;
651
651
  var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
652
- function intentGoalForSlug(description, slug) {
653
- const text = stripCode(description, { inlineSpans: "keep" });
652
+ function intentsSection(text) {
654
653
  const start = text.match(INTENTS_HEADING);
655
654
  if (!start || start.index === undefined)
656
- return "";
655
+ return null;
657
656
  const rest = text.slice(start.index + start[0].length);
658
657
  const next = rest.match(NEXT_HEADING);
659
- const section = rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
658
+ return rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
659
+ }
660
+ function intentGoalForSlug(description, slug) {
661
+ const text = stripCode(description, { inlineSpans: "keep" });
662
+ const section = intentsSection(text);
663
+ if (section === null)
664
+ return "";
660
665
  for (const line of section.split(`
661
666
  `)) {
662
667
  const trimmed = line.trim();
@@ -1948,6 +1953,7 @@ function parseInlineFieldList(section) {
1948
1953
  // ../../packages/aeg-core/src/verdict-extraction.ts
1949
1954
  var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
1950
1955
  var OBJECTIVES_VERSION_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Objectives version:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1956
+ var RULING_ORDINAL_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Ruling ordinal:\s*(\d+)(?!\d)/im;
1951
1957
  function firstFiveLines(comment) {
1952
1958
  return comment.split(`
1953
1959
  `).slice(0, 5).join(`
@@ -1961,6 +1967,15 @@ function extractObjectivesVersion(comment) {
1961
1967
  const m = firstFiveLines(comment).match(OBJECTIVES_VERSION_PATTERN);
1962
1968
  return m ? m[1].toLowerCase() : null;
1963
1969
  }
1970
+ function firstSevenLines(comment) {
1971
+ return comment.split(`
1972
+ `).slice(0, 7).join(`
1973
+ `);
1974
+ }
1975
+ function extractRulingOrdinal(comment) {
1976
+ const m = firstSevenLines(comment).match(RULING_ORDINAL_PATTERN);
1977
+ return m ? Number.parseInt(m[1], 10) : null;
1978
+ }
1964
1979
  function extractVerdict(comments, valuePattern, missingLabel) {
1965
1980
  const candidates = comments.filter((c) => valuePattern.test(c));
1966
1981
  if (candidates.length === 0) {
@@ -1968,6 +1983,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1968
1983
  value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
1969
1984
  headSha: null,
1970
1985
  objectivesVersion: null,
1986
+ rulingOrdinal: null,
1971
1987
  danglingNote: `no ${missingLabel} verdict comment found on this PR`
1972
1988
  };
1973
1989
  }
@@ -1978,6 +1994,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1978
1994
  value: `the most recent ${missingLabel} comment's VERDICT line is not within its first five lines — DANGLING, see below`,
1979
1995
  headSha: null,
1980
1996
  objectivesVersion: null,
1997
+ rulingOrdinal: null,
1981
1998
  danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
1982
1999
  };
1983
2000
  }
@@ -1985,6 +2002,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1985
2002
  value: m[1].toUpperCase().replace(/[_-]/g, " "),
1986
2003
  headSha: extractHeadSha(latest),
1987
2004
  objectivesVersion: extractObjectivesVersion(latest),
2005
+ rulingOrdinal: extractRulingOrdinal(latest),
1988
2006
  danglingNote: null
1989
2007
  };
1990
2008
  }
@@ -2346,6 +2364,9 @@ import { createHash as createHash2 } from "node:crypto";
2346
2364
  var HEADING_RE = /^##[ \t]*Objectives[ \t]*$/im;
2347
2365
  var NEXT_HEADING_RE = /^##[ \t]/m;
2348
2366
  var OBJECTIVE_LINE_RE = /^O(\d+)\.[ \t]*(.*)$/;
2367
+ function maskedForHeadingSearch(body) {
2368
+ return maskDetailsBlocks(maskCode(body));
2369
+ }
2349
2370
  function hasBacktickedPath(text) {
2350
2371
  let i = 0;
2351
2372
  while (i < text.length) {
@@ -2368,16 +2389,22 @@ function stripObjectiveBackticks(text) {
2368
2389
  function wordCount(text) {
2369
2390
  return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
2370
2391
  }
2371
- function objectivesSectionText(body) {
2372
- const heading = HEADING_RE.exec(body);
2392
+ function objectivesSectionBounds(body) {
2393
+ const masked = maskedForHeadingSearch(body);
2394
+ const heading = HEADING_RE.exec(masked);
2373
2395
  if (!heading)
2374
2396
  return null;
2375
- const afterHeading = body.slice(heading.index + heading[0].length);
2397
+ const start = heading.index + heading[0].length;
2398
+ const afterHeading = masked.slice(start);
2376
2399
  const next = NEXT_HEADING_RE.exec(afterHeading);
2377
- return next ? afterHeading.slice(0, next.index) : afterHeading;
2400
+ return { start, end: next ? start + next.index : body.length };
2401
+ }
2402
+ function objectivesSectionText(body) {
2403
+ const bounds = objectivesSectionBounds(body);
2404
+ return bounds === null ? null : body.slice(bounds.start, bounds.end);
2378
2405
  }
2379
2406
  function hasObjectivesHeading(body) {
2380
- return HEADING_RE.test(body);
2407
+ return HEADING_RE.test(maskedForHeadingSearch(body));
2381
2408
  }
2382
2409
  function objectivesOf(body) {
2383
2410
  const section = objectivesSectionText(body);
@@ -2437,6 +2464,15 @@ function isIssueNotFoundError(err) {
2437
2464
  `);
2438
2465
  return /could not resolve to an (?:issue|pull request)|\b404\b|not found/i.test(haystack);
2439
2466
  }
2467
+ function resolveObjectivesSource(prBody, issue, cutoverIssue) {
2468
+ if (issue !== null && issue < cutoverIssue)
2469
+ return { kind: "none" };
2470
+ if (issue !== null)
2471
+ return { kind: "issue", issue };
2472
+ if (hasObjectivesHeading(prBody))
2473
+ return { kind: "body" };
2474
+ return { kind: "none" };
2475
+ }
2440
2476
 
2441
2477
  // ../../packages/aeg-core/src/premise-check.ts
2442
2478
  import { createHash as createHash3 } from "node:crypto";
@@ -2938,17 +2974,40 @@ function checkBriefSections(prBody, readTier, options = {}) {
2938
2974
  ];
2939
2975
  return { errors: results.flatMap((r) => r.errors) };
2940
2976
  }
2941
- var AEG_BRIEF_V1_MARKER = "<!-- aeg:brief:v1 -->";
2942
- function contentAfterTwoLines(body) {
2943
- const first = body.indexOf(`
2944
- `);
2945
- if (first === -1)
2946
- return "";
2947
- const second = body.indexOf(`
2948
- `, first + 1);
2949
- if (second === -1)
2950
- return "";
2951
- return body.slice(second + 1);
2977
+ function contentAfterNLines(body, n) {
2978
+ let idx = -1;
2979
+ for (let i = 0;i < n; i++) {
2980
+ idx = body.indexOf(`
2981
+ `, idx + 1);
2982
+ if (idx === -1)
2983
+ return "";
2984
+ }
2985
+ return body.slice(idx + 1);
2986
+ }
2987
+ var BRIEF_MARKER_LINE_RE = /^<!-- aeg:brief:v(\d+) -->$/;
2988
+ function parseBriefMarkerVersion(firstLine) {
2989
+ const m = BRIEF_MARKER_LINE_RE.exec(firstLine.trim());
2990
+ if (!m)
2991
+ return null;
2992
+ const version = Number.parseInt(m[1], 10);
2993
+ return Number.isInteger(version) && version >= 1 ? version : null;
2994
+ }
2995
+ function frozenBriefContent(body, version) {
2996
+ return contentAfterNLines(body, version === 1 ? 2 : 3);
2997
+ }
2998
+ function resolveNewestFrozenBrief(comments, allowlist) {
2999
+ let best = null;
3000
+ for (const c of comments) {
3001
+ if (!isPrincipal(c.author, allowlist))
3002
+ continue;
3003
+ const version = parseBriefMarkerVersion(c.body.split(`
3004
+ `)[0] ?? "");
3005
+ if (version === null)
3006
+ continue;
3007
+ if (best === null || version > best.version)
3008
+ best = { ...c, version };
3009
+ }
3010
+ return best === null ? null : { ...best, content: frozenBriefContent(best.body, best.version) };
2952
3011
  }
2953
3012
  // ../../packages/aeg-core/src/doctrine-portability.ts
2954
3013
  var DEFAULT_SHIPS_PREFIX = "aeg-root/";
@@ -3675,6 +3734,11 @@ function isBoundToObjectives(extraction, currentVersion) {
3675
3734
  return true;
3676
3735
  return extraction.objectivesVersion === currentVersion;
3677
3736
  }
3737
+ function isBoundToRulings(extraction, currentOrdinal) {
3738
+ if (extraction.rulingOrdinal === null)
3739
+ return currentOrdinal === 0;
3740
+ return extraction.rulingOrdinal === currentOrdinal;
3741
+ }
3678
3742
  function checkReviewGate(input) {
3679
3743
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
3680
3744
  const waived = isWaiverLabelActorVerified({
@@ -3703,7 +3767,9 @@ function checkReviewGate(input) {
3703
3767
  const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
3704
3768
  const codeReviewObjectivesBound = isBoundToObjectives(codeReview, input.objectivesVersion);
3705
3769
  const securityObjectivesBound = isBoundToObjectives(security, input.objectivesVersion);
3706
- if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && securityClean && securityBound && securityObjectivesBound && mechanicalChecksClean) {
3770
+ const codeReviewRulingsBound = isBoundToRulings(codeReview, input.rulingOrdinal);
3771
+ const securityRulingsBound = isBoundToRulings(security, input.rulingOrdinal);
3772
+ if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && codeReviewRulingsBound && securityClean && securityBound && securityObjectivesBound && securityRulingsBound && mechanicalChecksClean) {
3707
3773
  return {
3708
3774
  verdict: "pass",
3709
3775
  reason: `code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS, both covering head ${input.headSha}, and every reported mechanical check is green.`,
@@ -3717,6 +3783,8 @@ function checkReviewGate(input) {
3717
3783
  problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3718
3784
  } else if (!codeReviewObjectivesBound) {
3719
3785
  problems.push(`the newest code-review verdict was cast against objectives version ${codeReview.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3786
+ } else if (!codeReviewRulingsBound) {
3787
+ problems.push(`the newest code-review verdict was cast against ruling ordinal ${codeReview.rulingOrdinal ?? "none"}, a newer ruling (ruling ${input.rulingOrdinal}) is now posted on this PR`);
3720
3788
  }
3721
3789
  if (!securityClean) {
3722
3790
  problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
@@ -3724,6 +3792,8 @@ function checkReviewGate(input) {
3724
3792
  problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3725
3793
  } else if (!securityObjectivesBound) {
3726
3794
  problems.push(`the newest security-review verdict was cast against objectives version ${security.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3795
+ } else if (!securityRulingsBound) {
3796
+ problems.push(`the newest security-review verdict was cast against ruling ordinal ${security.rulingOrdinal ?? "none"}, a newer ruling (ruling ${input.rulingOrdinal}) is now posted on this PR`);
3727
3797
  }
3728
3798
  if (!mechanicalChecksClean) {
3729
3799
  problems.push(reportedMechanicalChecks.length === 0 ? "no mechanical checks have reported for this head yet" : `mechanical check(s) not green: ${reportedMechanicalChecks.filter((c) => c.bucket !== "pass").map((c) => `${c.name} (${c.bucket})`).join(", ")}`);
@@ -3740,6 +3810,24 @@ var DEFAULT_RELEASE_ACTOR = "github-actions[bot]";
3740
3810
  function isChangesetsReleasePr(branch, author, expectedAuthor) {
3741
3811
  return branch === CHANGESET_RELEASE_BRANCH && author === expectedAuthor;
3742
3812
  }
3813
+ // ../../packages/aeg-core/src/ruling-ordinal.ts
3814
+ var RULING_MARKER_ORDINAL = /^<!-- aeg:principal:ruling:\d+-(\d+) -->$/;
3815
+ function newestPrincipalRulingOrdinal(comments, allowlist) {
3816
+ let best = 0;
3817
+ for (const c of comments) {
3818
+ if (!isPrincipal(c.author, allowlist))
3819
+ continue;
3820
+ const firstLine = (c.body.split(`
3821
+ `)[0] ?? "").trim();
3822
+ const m = RULING_MARKER_ORDINAL.exec(firstLine);
3823
+ if (!m)
3824
+ continue;
3825
+ const k = Number.parseInt(m[1], 10);
3826
+ if (k > best)
3827
+ best = k;
3828
+ }
3829
+ return best;
3830
+ }
3743
3831
  // ../../packages/aeg-core/src/markdown-table.ts
3744
3832
  function splitRow(line) {
3745
3833
  const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
@@ -5094,14 +5182,41 @@ var COMMANDS = [
5094
5182
  },
5095
5183
  {
5096
5184
  name: "task dispatch",
5097
- description: "Render, pin, and post the brief on the Issue as the frozen original; start the developer",
5185
+ description: "Deprecated — render, pin, and post the brief on the Issue as the frozen original; start the developer",
5098
5186
  flags: [
5099
5187
  { flag: "--agent <claude|codex|gemini>", description: "Start the developer through dispatchRole once posted" }
5100
5188
  ],
5101
5189
  details: [
5102
5190
  "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.",
5103
5191
  "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.",
5104
- "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."
5192
+ "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.",
5193
+ "Deprecated in favor of `task brief` (preparation only) and `task run` (the full unattended loop) — kept for a documented compatibility window while callers migrate."
5194
+ ],
5195
+ status: "shipped"
5196
+ },
5197
+ {
5198
+ name: "task brief",
5199
+ description: "Render and freeze the brief as the Issue's original comment — preparation only, starts nobody",
5200
+ details: [
5201
+ "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.",
5202
+ "Successor to `task dispatch` for the preparation step; the full unattended run (preparation, then the developer, then the review loop) is `task run`."
5203
+ ],
5204
+ status: "shipped"
5205
+ },
5206
+ {
5207
+ name: "task run",
5208
+ description: "One command from a planned Issue to a reviewed pull request — exactly one developer started",
5209
+ flags: [
5210
+ {
5211
+ flag: "--agent <claude|codex|gemini>",
5212
+ description: "Vendor for the developer and both reviewers this run dispatches"
5213
+ }
5214
+ ],
5215
+ details: [
5216
+ "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.",
5217
+ "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.",
5218
+ "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.",
5219
+ "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."
5105
5220
  ],
5106
5221
  status: "shipped"
5107
5222
  },
@@ -5309,6 +5424,15 @@ var COMMANDS = [
5309
5424
  ],
5310
5425
  status: "shipped"
5311
5426
  },
5427
+ {
5428
+ name: "milestone status",
5429
+ description: "Print each of a Milestone's declared tranche intents with its derived lifecycle and issue counts",
5430
+ flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
5431
+ details: [
5432
+ "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."
5433
+ ],
5434
+ status: "shipped"
5435
+ },
5312
5436
  {
5313
5437
  name: "review status",
5314
5438
  description: "Print the review loop's own state for a PR, and its branch's distance from the base",