@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
@@ -306,7 +306,7 @@ function issueListByLabelArgs(owner, repo, label) {
306
306
  "--state",
307
307
  "all",
308
308
  "--json",
309
- "number,title,body,state,labels,milestone",
309
+ "number,title,body,state,labels,milestone,stateReason",
310
310
  "--limit",
311
311
  "200"
312
312
  ];
@@ -652,14 +652,19 @@ function resolveTaskIssueRef(title, labels) {
652
652
  var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
653
653
  var NEXT_HEADING = /^#{1,6}\s+\S/m;
654
654
  var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
655
- function intentGoalForSlug(description, slug) {
656
- const text = stripCode(description, { inlineSpans: "keep" });
655
+ function intentsSection(text) {
657
656
  const start = text.match(INTENTS_HEADING);
658
657
  if (!start || start.index === undefined)
659
- return "";
658
+ return null;
660
659
  const rest = text.slice(start.index + start[0].length);
661
660
  const next = rest.match(NEXT_HEADING);
662
- const section = rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
661
+ return rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
662
+ }
663
+ function intentGoalForSlug(description, slug) {
664
+ const text = stripCode(description, { inlineSpans: "keep" });
665
+ const section = intentsSection(text);
666
+ if (section === null)
667
+ return "";
663
668
  for (const line of section.split(`
664
669
  `)) {
665
670
  const trimmed = line.trim();
@@ -1951,6 +1956,7 @@ function parseInlineFieldList(section) {
1951
1956
  // ../../packages/aeg-core/src/verdict-extraction.ts
1952
1957
  var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
1953
1958
  var OBJECTIVES_VERSION_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Objectives version:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1959
+ var RULING_ORDINAL_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Ruling ordinal:\s*(\d+)(?!\d)/im;
1954
1960
  function firstFiveLines(comment) {
1955
1961
  return comment.split(`
1956
1962
  `).slice(0, 5).join(`
@@ -1964,6 +1970,15 @@ function extractObjectivesVersion(comment) {
1964
1970
  const m = firstFiveLines(comment).match(OBJECTIVES_VERSION_PATTERN);
1965
1971
  return m ? m[1].toLowerCase() : null;
1966
1972
  }
1973
+ function firstSevenLines(comment) {
1974
+ return comment.split(`
1975
+ `).slice(0, 7).join(`
1976
+ `);
1977
+ }
1978
+ function extractRulingOrdinal(comment) {
1979
+ const m = firstSevenLines(comment).match(RULING_ORDINAL_PATTERN);
1980
+ return m ? Number.parseInt(m[1], 10) : null;
1981
+ }
1967
1982
  function extractVerdict(comments, valuePattern, missingLabel) {
1968
1983
  const candidates = comments.filter((c) => valuePattern.test(c));
1969
1984
  if (candidates.length === 0) {
@@ -1971,6 +1986,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1971
1986
  value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
1972
1987
  headSha: null,
1973
1988
  objectivesVersion: null,
1989
+ rulingOrdinal: null,
1974
1990
  danglingNote: `no ${missingLabel} verdict comment found on this PR`
1975
1991
  };
1976
1992
  }
@@ -1981,6 +1997,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1981
1997
  value: `the most recent ${missingLabel} comment's VERDICT line is not within its first five lines — DANGLING, see below`,
1982
1998
  headSha: null,
1983
1999
  objectivesVersion: null,
2000
+ rulingOrdinal: null,
1984
2001
  danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
1985
2002
  };
1986
2003
  }
@@ -1988,6 +2005,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1988
2005
  value: m[1].toUpperCase().replace(/[_-]/g, " "),
1989
2006
  headSha: extractHeadSha(latest),
1990
2007
  objectivesVersion: extractObjectivesVersion(latest),
2008
+ rulingOrdinal: extractRulingOrdinal(latest),
1991
2009
  danglingNote: null
1992
2010
  };
1993
2011
  }
@@ -2349,6 +2367,9 @@ import { createHash as createHash2 } from "node:crypto";
2349
2367
  var HEADING_RE = /^##[ \t]*Objectives[ \t]*$/im;
2350
2368
  var NEXT_HEADING_RE = /^##[ \t]/m;
2351
2369
  var OBJECTIVE_LINE_RE = /^O(\d+)\.[ \t]*(.*)$/;
2370
+ function maskedForHeadingSearch(body) {
2371
+ return maskDetailsBlocks(maskCode(body));
2372
+ }
2352
2373
  function hasBacktickedPath(text) {
2353
2374
  let i = 0;
2354
2375
  while (i < text.length) {
@@ -2371,16 +2392,22 @@ function stripObjectiveBackticks(text) {
2371
2392
  function wordCount(text) {
2372
2393
  return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
2373
2394
  }
2374
- function objectivesSectionText(body) {
2375
- const heading = HEADING_RE.exec(body);
2395
+ function objectivesSectionBounds(body) {
2396
+ const masked = maskedForHeadingSearch(body);
2397
+ const heading = HEADING_RE.exec(masked);
2376
2398
  if (!heading)
2377
2399
  return null;
2378
- const afterHeading = body.slice(heading.index + heading[0].length);
2400
+ const start = heading.index + heading[0].length;
2401
+ const afterHeading = masked.slice(start);
2379
2402
  const next = NEXT_HEADING_RE.exec(afterHeading);
2380
- return next ? afterHeading.slice(0, next.index) : afterHeading;
2403
+ return { start, end: next ? start + next.index : body.length };
2404
+ }
2405
+ function objectivesSectionText(body) {
2406
+ const bounds = objectivesSectionBounds(body);
2407
+ return bounds === null ? null : body.slice(bounds.start, bounds.end);
2381
2408
  }
2382
2409
  function hasObjectivesHeading(body) {
2383
- return HEADING_RE.test(body);
2410
+ return HEADING_RE.test(maskedForHeadingSearch(body));
2384
2411
  }
2385
2412
  function objectivesOf(body) {
2386
2413
  const section = objectivesSectionText(body);
@@ -2440,6 +2467,15 @@ function isIssueNotFoundError(err) {
2440
2467
  `);
2441
2468
  return /could not resolve to an (?:issue|pull request)|\b404\b|not found/i.test(haystack);
2442
2469
  }
2470
+ function resolveObjectivesSource(prBody, issue, cutoverIssue) {
2471
+ if (issue !== null && issue < cutoverIssue)
2472
+ return { kind: "none" };
2473
+ if (issue !== null)
2474
+ return { kind: "issue", issue };
2475
+ if (hasObjectivesHeading(prBody))
2476
+ return { kind: "body" };
2477
+ return { kind: "none" };
2478
+ }
2443
2479
 
2444
2480
  // ../../packages/aeg-core/src/premise-check.ts
2445
2481
  import { createHash as createHash3 } from "node:crypto";
@@ -2941,17 +2977,40 @@ function checkBriefSections(prBody, readTier, options = {}) {
2941
2977
  ];
2942
2978
  return { errors: results.flatMap((r) => r.errors) };
2943
2979
  }
2944
- var AEG_BRIEF_V1_MARKER = "<!-- aeg:brief:v1 -->";
2945
- function contentAfterTwoLines(body) {
2946
- const first = body.indexOf(`
2947
- `);
2948
- if (first === -1)
2949
- return "";
2950
- const second = body.indexOf(`
2951
- `, first + 1);
2952
- if (second === -1)
2953
- return "";
2954
- return body.slice(second + 1);
2980
+ function contentAfterNLines(body, n) {
2981
+ let idx = -1;
2982
+ for (let i = 0;i < n; i++) {
2983
+ idx = body.indexOf(`
2984
+ `, idx + 1);
2985
+ if (idx === -1)
2986
+ return "";
2987
+ }
2988
+ return body.slice(idx + 1);
2989
+ }
2990
+ var BRIEF_MARKER_LINE_RE = /^<!-- aeg:brief:v(\d+) -->$/;
2991
+ function parseBriefMarkerVersion(firstLine) {
2992
+ const m = BRIEF_MARKER_LINE_RE.exec(firstLine.trim());
2993
+ if (!m)
2994
+ return null;
2995
+ const version = Number.parseInt(m[1], 10);
2996
+ return Number.isInteger(version) && version >= 1 ? version : null;
2997
+ }
2998
+ function frozenBriefContent(body, version) {
2999
+ return contentAfterNLines(body, version === 1 ? 2 : 3);
3000
+ }
3001
+ function resolveNewestFrozenBrief(comments, allowlist) {
3002
+ let best = null;
3003
+ for (const c of comments) {
3004
+ if (!isPrincipal(c.author, allowlist))
3005
+ continue;
3006
+ const version = parseBriefMarkerVersion(c.body.split(`
3007
+ `)[0] ?? "");
3008
+ if (version === null)
3009
+ continue;
3010
+ if (best === null || version > best.version)
3011
+ best = { ...c, version };
3012
+ }
3013
+ return best === null ? null : { ...best, content: frozenBriefContent(best.body, best.version) };
2955
3014
  }
2956
3015
  // ../../packages/aeg-core/src/doctrine-portability.ts
2957
3016
  var DEFAULT_SHIPS_PREFIX = "aeg-root/";
@@ -3678,6 +3737,11 @@ function isBoundToObjectives(extraction, currentVersion) {
3678
3737
  return true;
3679
3738
  return extraction.objectivesVersion === currentVersion;
3680
3739
  }
3740
+ function isBoundToRulings(extraction, currentOrdinal) {
3741
+ if (extraction.rulingOrdinal === null)
3742
+ return currentOrdinal === 0;
3743
+ return extraction.rulingOrdinal === currentOrdinal;
3744
+ }
3681
3745
  function checkReviewGate(input) {
3682
3746
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
3683
3747
  const waived = isWaiverLabelActorVerified({
@@ -3706,7 +3770,9 @@ function checkReviewGate(input) {
3706
3770
  const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
3707
3771
  const codeReviewObjectivesBound = isBoundToObjectives(codeReview, input.objectivesVersion);
3708
3772
  const securityObjectivesBound = isBoundToObjectives(security, input.objectivesVersion);
3709
- if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && securityClean && securityBound && securityObjectivesBound && mechanicalChecksClean) {
3773
+ const codeReviewRulingsBound = isBoundToRulings(codeReview, input.rulingOrdinal);
3774
+ const securityRulingsBound = isBoundToRulings(security, input.rulingOrdinal);
3775
+ if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && codeReviewRulingsBound && securityClean && securityBound && securityObjectivesBound && securityRulingsBound && mechanicalChecksClean) {
3710
3776
  return {
3711
3777
  verdict: "pass",
3712
3778
  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.`,
@@ -3720,6 +3786,8 @@ function checkReviewGate(input) {
3720
3786
  problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3721
3787
  } else if (!codeReviewObjectivesBound) {
3722
3788
  problems.push(`the newest code-review verdict was cast against objectives version ${codeReview.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3789
+ } else if (!codeReviewRulingsBound) {
3790
+ 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`);
3723
3791
  }
3724
3792
  if (!securityClean) {
3725
3793
  problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
@@ -3727,6 +3795,8 @@ function checkReviewGate(input) {
3727
3795
  problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3728
3796
  } else if (!securityObjectivesBound) {
3729
3797
  problems.push(`the newest security-review verdict was cast against objectives version ${security.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3798
+ } else if (!securityRulingsBound) {
3799
+ 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`);
3730
3800
  }
3731
3801
  if (!mechanicalChecksClean) {
3732
3802
  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(", ")}`);
@@ -3743,6 +3813,24 @@ var DEFAULT_RELEASE_ACTOR = "github-actions[bot]";
3743
3813
  function isChangesetsReleasePr(branch, author, expectedAuthor) {
3744
3814
  return branch === CHANGESET_RELEASE_BRANCH && author === expectedAuthor;
3745
3815
  }
3816
+ // ../../packages/aeg-core/src/ruling-ordinal.ts
3817
+ var RULING_MARKER_ORDINAL = /^<!-- aeg:principal:ruling:\d+-(\d+) -->$/;
3818
+ function newestPrincipalRulingOrdinal(comments, allowlist) {
3819
+ let best = 0;
3820
+ for (const c of comments) {
3821
+ if (!isPrincipal(c.author, allowlist))
3822
+ continue;
3823
+ const firstLine = (c.body.split(`
3824
+ `)[0] ?? "").trim();
3825
+ const m = RULING_MARKER_ORDINAL.exec(firstLine);
3826
+ if (!m)
3827
+ continue;
3828
+ const k = Number.parseInt(m[1], 10);
3829
+ if (k > best)
3830
+ best = k;
3831
+ }
3832
+ return best;
3833
+ }
3746
3834
  // ../../packages/aeg-core/src/markdown-table.ts
3747
3835
  function splitRow(line) {
3748
3836
  const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
@@ -5097,14 +5185,41 @@ var COMMANDS = [
5097
5185
  },
5098
5186
  {
5099
5187
  name: "task dispatch",
5100
- description: "Render, pin, and post the brief on the Issue as the frozen original; start the developer",
5188
+ description: "Deprecated — render, pin, and post the brief on the Issue as the frozen original; start the developer",
5101
5189
  flags: [
5102
5190
  { flag: "--agent <claude|codex|gemini>", description: "Start the developer through dispatchRole once posted" }
5103
5191
  ],
5104
5192
  details: [
5105
5193
  "Renders the brief from the Issue and the tree (the same assembly `brief render` uses) and posts it once as an Issue comment whose first line is `<!-- aeg:brief:v1 -->` and whose second line is `Brief hash: <sha256>` — the hash covers only the body below those two lines, so any reader recomputes it. Refuses outright, naming the existing comment's URL, when a `v1` comment already exists on the Issue — the brief is frozen by design, never overwritten or silently reissued.",
5106
5194
  "With `--agent`, starts the Developer through `dispatchRole` when `apps/cli/src/lib/dispatch.ts` exports it; otherwise prints the rendered brief and the manual dispatch instruction and exits `0` — a soft dependency, never a hard block.",
5107
- "Principal-only, with or without `--agent`: refuses before any render, forge read, or post when the authenticated `gh` identity is not on the Principal allowlist (or cannot be resolved at all) — dispatching is the `todo → in-flight` transition, not a general-purpose comment poster."
5195
+ "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.",
5196
+ "Deprecated in favor of `task brief` (preparation only) and `task run` (the full unattended loop) — kept for a documented compatibility window while callers migrate."
5197
+ ],
5198
+ status: "shipped"
5199
+ },
5200
+ {
5201
+ name: "task brief",
5202
+ description: "Render and freeze the brief as the Issue's original comment — preparation only, starts nobody",
5203
+ details: [
5204
+ "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.",
5205
+ "Successor to `task dispatch` for the preparation step; the full unattended run (preparation, then the developer, then the review loop) is `task run`."
5206
+ ],
5207
+ status: "shipped"
5208
+ },
5209
+ {
5210
+ name: "task run",
5211
+ description: "One command from a planned Issue to a reviewed pull request — exactly one developer started",
5212
+ flags: [
5213
+ {
5214
+ flag: "--agent <claude|codex|gemini>",
5215
+ description: "Vendor for the developer and both reviewers this run dispatches"
5216
+ }
5217
+ ],
5218
+ details: [
5219
+ "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.",
5220
+ "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.",
5221
+ "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.",
5222
+ "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."
5108
5223
  ],
5109
5224
  status: "shipped"
5110
5225
  },
@@ -5312,6 +5427,15 @@ var COMMANDS = [
5312
5427
  ],
5313
5428
  status: "shipped"
5314
5429
  },
5430
+ {
5431
+ name: "milestone status",
5432
+ description: "Print each of a Milestone's declared tranche intents with its derived lifecycle and issue counts",
5433
+ flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
5434
+ details: [
5435
+ "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."
5436
+ ],
5437
+ status: "shipped"
5438
+ },
5315
5439
  {
5316
5440
  name: "review status",
5317
5441
  description: "Print the review loop's own state for a PR, and its branch's distance from the base",
@@ -304,7 +304,7 @@ function issueListByLabelArgs(owner, repo, label) {
304
304
  "--state",
305
305
  "all",
306
306
  "--json",
307
- "number,title,body,state,labels,milestone",
307
+ "number,title,body,state,labels,milestone,stateReason",
308
308
  "--limit",
309
309
  "200"
310
310
  ];
@@ -650,14 +650,19 @@ function resolveTaskIssueRef(title, labels) {
650
650
  var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
651
651
  var NEXT_HEADING = /^#{1,6}\s+\S/m;
652
652
  var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
653
- function intentGoalForSlug(description, slug) {
654
- const text = stripCode(description, { inlineSpans: "keep" });
653
+ function intentsSection(text) {
655
654
  const start = text.match(INTENTS_HEADING);
656
655
  if (!start || start.index === undefined)
657
- return "";
656
+ return null;
658
657
  const rest = text.slice(start.index + start[0].length);
659
658
  const next = rest.match(NEXT_HEADING);
660
- const section = rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
659
+ return rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
660
+ }
661
+ function intentGoalForSlug(description, slug) {
662
+ const text = stripCode(description, { inlineSpans: "keep" });
663
+ const section = intentsSection(text);
664
+ if (section === null)
665
+ return "";
661
666
  for (const line of section.split(`
662
667
  `)) {
663
668
  const trimmed = line.trim();
@@ -1949,6 +1954,7 @@ function parseInlineFieldList(section) {
1949
1954
  // ../../packages/aeg-core/src/verdict-extraction.ts
1950
1955
  var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
1951
1956
  var OBJECTIVES_VERSION_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Objectives version:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
1957
+ var RULING_ORDINAL_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Ruling ordinal:\s*(\d+)(?!\d)/im;
1952
1958
  function firstFiveLines(comment) {
1953
1959
  return comment.split(`
1954
1960
  `).slice(0, 5).join(`
@@ -1962,6 +1968,15 @@ function extractObjectivesVersion(comment) {
1962
1968
  const m = firstFiveLines(comment).match(OBJECTIVES_VERSION_PATTERN);
1963
1969
  return m ? m[1].toLowerCase() : null;
1964
1970
  }
1971
+ function firstSevenLines(comment) {
1972
+ return comment.split(`
1973
+ `).slice(0, 7).join(`
1974
+ `);
1975
+ }
1976
+ function extractRulingOrdinal(comment) {
1977
+ const m = firstSevenLines(comment).match(RULING_ORDINAL_PATTERN);
1978
+ return m ? Number.parseInt(m[1], 10) : null;
1979
+ }
1965
1980
  function extractVerdict(comments, valuePattern, missingLabel) {
1966
1981
  const candidates = comments.filter((c) => valuePattern.test(c));
1967
1982
  if (candidates.length === 0) {
@@ -1969,6 +1984,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1969
1984
  value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
1970
1985
  headSha: null,
1971
1986
  objectivesVersion: null,
1987
+ rulingOrdinal: null,
1972
1988
  danglingNote: `no ${missingLabel} verdict comment found on this PR`
1973
1989
  };
1974
1990
  }
@@ -1979,6 +1995,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1979
1995
  value: `the most recent ${missingLabel} comment's VERDICT line is not within its first five lines — DANGLING, see below`,
1980
1996
  headSha: null,
1981
1997
  objectivesVersion: null,
1998
+ rulingOrdinal: null,
1982
1999
  danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
1983
2000
  };
1984
2001
  }
@@ -1986,6 +2003,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
1986
2003
  value: m[1].toUpperCase().replace(/[_-]/g, " "),
1987
2004
  headSha: extractHeadSha(latest),
1988
2005
  objectivesVersion: extractObjectivesVersion(latest),
2006
+ rulingOrdinal: extractRulingOrdinal(latest),
1989
2007
  danglingNote: null
1990
2008
  };
1991
2009
  }
@@ -2347,6 +2365,9 @@ import { createHash as createHash2 } from "node:crypto";
2347
2365
  var HEADING_RE = /^##[ \t]*Objectives[ \t]*$/im;
2348
2366
  var NEXT_HEADING_RE = /^##[ \t]/m;
2349
2367
  var OBJECTIVE_LINE_RE = /^O(\d+)\.[ \t]*(.*)$/;
2368
+ function maskedForHeadingSearch(body) {
2369
+ return maskDetailsBlocks(maskCode(body));
2370
+ }
2350
2371
  function hasBacktickedPath(text) {
2351
2372
  let i = 0;
2352
2373
  while (i < text.length) {
@@ -2369,16 +2390,22 @@ function stripObjectiveBackticks(text) {
2369
2390
  function wordCount(text) {
2370
2391
  return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
2371
2392
  }
2372
- function objectivesSectionText(body) {
2373
- const heading = HEADING_RE.exec(body);
2393
+ function objectivesSectionBounds(body) {
2394
+ const masked = maskedForHeadingSearch(body);
2395
+ const heading = HEADING_RE.exec(masked);
2374
2396
  if (!heading)
2375
2397
  return null;
2376
- const afterHeading = body.slice(heading.index + heading[0].length);
2398
+ const start = heading.index + heading[0].length;
2399
+ const afterHeading = masked.slice(start);
2377
2400
  const next = NEXT_HEADING_RE.exec(afterHeading);
2378
- return next ? afterHeading.slice(0, next.index) : afterHeading;
2401
+ return { start, end: next ? start + next.index : body.length };
2402
+ }
2403
+ function objectivesSectionText(body) {
2404
+ const bounds = objectivesSectionBounds(body);
2405
+ return bounds === null ? null : body.slice(bounds.start, bounds.end);
2379
2406
  }
2380
2407
  function hasObjectivesHeading(body) {
2381
- return HEADING_RE.test(body);
2408
+ return HEADING_RE.test(maskedForHeadingSearch(body));
2382
2409
  }
2383
2410
  function objectivesOf(body) {
2384
2411
  const section = objectivesSectionText(body);
@@ -2438,6 +2465,15 @@ function isIssueNotFoundError(err) {
2438
2465
  `);
2439
2466
  return /could not resolve to an (?:issue|pull request)|\b404\b|not found/i.test(haystack);
2440
2467
  }
2468
+ function resolveObjectivesSource(prBody, issue, cutoverIssue) {
2469
+ if (issue !== null && issue < cutoverIssue)
2470
+ return { kind: "none" };
2471
+ if (issue !== null)
2472
+ return { kind: "issue", issue };
2473
+ if (hasObjectivesHeading(prBody))
2474
+ return { kind: "body" };
2475
+ return { kind: "none" };
2476
+ }
2441
2477
 
2442
2478
  // ../../packages/aeg-core/src/premise-check.ts
2443
2479
  import { createHash as createHash3 } from "node:crypto";
@@ -2939,17 +2975,40 @@ function checkBriefSections(prBody, readTier, options = {}) {
2939
2975
  ];
2940
2976
  return { errors: results.flatMap((r) => r.errors) };
2941
2977
  }
2942
- var AEG_BRIEF_V1_MARKER = "<!-- aeg:brief:v1 -->";
2943
- function contentAfterTwoLines(body) {
2944
- const first = body.indexOf(`
2945
- `);
2946
- if (first === -1)
2947
- return "";
2948
- const second = body.indexOf(`
2949
- `, first + 1);
2950
- if (second === -1)
2951
- return "";
2952
- return body.slice(second + 1);
2978
+ function contentAfterNLines(body, n) {
2979
+ let idx = -1;
2980
+ for (let i = 0;i < n; i++) {
2981
+ idx = body.indexOf(`
2982
+ `, idx + 1);
2983
+ if (idx === -1)
2984
+ return "";
2985
+ }
2986
+ return body.slice(idx + 1);
2987
+ }
2988
+ var BRIEF_MARKER_LINE_RE = /^<!-- aeg:brief:v(\d+) -->$/;
2989
+ function parseBriefMarkerVersion(firstLine) {
2990
+ const m = BRIEF_MARKER_LINE_RE.exec(firstLine.trim());
2991
+ if (!m)
2992
+ return null;
2993
+ const version = Number.parseInt(m[1], 10);
2994
+ return Number.isInteger(version) && version >= 1 ? version : null;
2995
+ }
2996
+ function frozenBriefContent(body, version) {
2997
+ return contentAfterNLines(body, version === 1 ? 2 : 3);
2998
+ }
2999
+ function resolveNewestFrozenBrief(comments, allowlist) {
3000
+ let best = null;
3001
+ for (const c of comments) {
3002
+ if (!isPrincipal(c.author, allowlist))
3003
+ continue;
3004
+ const version = parseBriefMarkerVersion(c.body.split(`
3005
+ `)[0] ?? "");
3006
+ if (version === null)
3007
+ continue;
3008
+ if (best === null || version > best.version)
3009
+ best = { ...c, version };
3010
+ }
3011
+ return best === null ? null : { ...best, content: frozenBriefContent(best.body, best.version) };
2953
3012
  }
2954
3013
  // ../../packages/aeg-core/src/doctrine-portability.ts
2955
3014
  var DEFAULT_SHIPS_PREFIX = "aeg-root/";
@@ -3676,6 +3735,11 @@ function isBoundToObjectives(extraction, currentVersion) {
3676
3735
  return true;
3677
3736
  return extraction.objectivesVersion === currentVersion;
3678
3737
  }
3738
+ function isBoundToRulings(extraction, currentOrdinal) {
3739
+ if (extraction.rulingOrdinal === null)
3740
+ return currentOrdinal === 0;
3741
+ return extraction.rulingOrdinal === currentOrdinal;
3742
+ }
3679
3743
  function checkReviewGate(input) {
3680
3744
  const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
3681
3745
  const waived = isWaiverLabelActorVerified({
@@ -3704,7 +3768,9 @@ function checkReviewGate(input) {
3704
3768
  const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
3705
3769
  const codeReviewObjectivesBound = isBoundToObjectives(codeReview, input.objectivesVersion);
3706
3770
  const securityObjectivesBound = isBoundToObjectives(security, input.objectivesVersion);
3707
- if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && securityClean && securityBound && securityObjectivesBound && mechanicalChecksClean) {
3771
+ const codeReviewRulingsBound = isBoundToRulings(codeReview, input.rulingOrdinal);
3772
+ const securityRulingsBound = isBoundToRulings(security, input.rulingOrdinal);
3773
+ if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && codeReviewRulingsBound && securityClean && securityBound && securityObjectivesBound && securityRulingsBound && mechanicalChecksClean) {
3708
3774
  return {
3709
3775
  verdict: "pass",
3710
3776
  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.`,
@@ -3718,6 +3784,8 @@ function checkReviewGate(input) {
3718
3784
  problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3719
3785
  } else if (!codeReviewObjectivesBound) {
3720
3786
  problems.push(`the newest code-review verdict was cast against objectives version ${codeReview.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3787
+ } else if (!codeReviewRulingsBound) {
3788
+ 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`);
3721
3789
  }
3722
3790
  if (!securityClean) {
3723
3791
  problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
@@ -3725,6 +3793,8 @@ function checkReviewGate(input) {
3725
3793
  problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
3726
3794
  } else if (!securityObjectivesBound) {
3727
3795
  problems.push(`the newest security-review verdict was cast against objectives version ${security.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
3796
+ } else if (!securityRulingsBound) {
3797
+ 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`);
3728
3798
  }
3729
3799
  if (!mechanicalChecksClean) {
3730
3800
  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(", ")}`);
@@ -3741,6 +3811,24 @@ var DEFAULT_RELEASE_ACTOR = "github-actions[bot]";
3741
3811
  function isChangesetsReleasePr(branch, author, expectedAuthor) {
3742
3812
  return branch === CHANGESET_RELEASE_BRANCH && author === expectedAuthor;
3743
3813
  }
3814
+ // ../../packages/aeg-core/src/ruling-ordinal.ts
3815
+ var RULING_MARKER_ORDINAL = /^<!-- aeg:principal:ruling:\d+-(\d+) -->$/;
3816
+ function newestPrincipalRulingOrdinal(comments, allowlist) {
3817
+ let best = 0;
3818
+ for (const c of comments) {
3819
+ if (!isPrincipal(c.author, allowlist))
3820
+ continue;
3821
+ const firstLine = (c.body.split(`
3822
+ `)[0] ?? "").trim();
3823
+ const m = RULING_MARKER_ORDINAL.exec(firstLine);
3824
+ if (!m)
3825
+ continue;
3826
+ const k = Number.parseInt(m[1], 10);
3827
+ if (k > best)
3828
+ best = k;
3829
+ }
3830
+ return best;
3831
+ }
3744
3832
  // ../../packages/aeg-core/src/markdown-table.ts
3745
3833
  function splitRow(line) {
3746
3834
  const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
@@ -5408,14 +5496,41 @@ var COMMANDS = [
5408
5496
  },
5409
5497
  {
5410
5498
  name: "task dispatch",
5411
- description: "Render, pin, and post the brief on the Issue as the frozen original; start the developer",
5499
+ description: "Deprecated — render, pin, and post the brief on the Issue as the frozen original; start the developer",
5412
5500
  flags: [
5413
5501
  { flag: "--agent <claude|codex|gemini>", description: "Start the developer through dispatchRole once posted" }
5414
5502
  ],
5415
5503
  details: [
5416
5504
  "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.",
5417
5505
  "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.",
5418
- "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."
5506
+ "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.",
5507
+ "Deprecated in favor of `task brief` (preparation only) and `task run` (the full unattended loop) — kept for a documented compatibility window while callers migrate."
5508
+ ],
5509
+ status: "shipped"
5510
+ },
5511
+ {
5512
+ name: "task brief",
5513
+ description: "Render and freeze the brief as the Issue's original comment — preparation only, starts nobody",
5514
+ details: [
5515
+ "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.",
5516
+ "Successor to `task dispatch` for the preparation step; the full unattended run (preparation, then the developer, then the review loop) is `task run`."
5517
+ ],
5518
+ status: "shipped"
5519
+ },
5520
+ {
5521
+ name: "task run",
5522
+ description: "One command from a planned Issue to a reviewed pull request — exactly one developer started",
5523
+ flags: [
5524
+ {
5525
+ flag: "--agent <claude|codex|gemini>",
5526
+ description: "Vendor for the developer and both reviewers this run dispatches"
5527
+ }
5528
+ ],
5529
+ details: [
5530
+ "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.",
5531
+ "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.",
5532
+ "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.",
5533
+ "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."
5419
5534
  ],
5420
5535
  status: "shipped"
5421
5536
  },
@@ -5623,6 +5738,15 @@ var COMMANDS = [
5623
5738
  ],
5624
5739
  status: "shipped"
5625
5740
  },
5741
+ {
5742
+ name: "milestone status",
5743
+ description: "Print each of a Milestone's declared tranche intents with its derived lifecycle and issue counts",
5744
+ flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
5745
+ details: [
5746
+ "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."
5747
+ ],
5748
+ status: "shipped"
5749
+ },
5626
5750
  {
5627
5751
  name: "review status",
5628
5752
  description: "Print the review loop's own state for a PR, and its branch's distance from the base",