@attalabs/vinaya 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +4 -4
  2. package/aeg-root/contracts/architect-planner.md +5 -5
  3. package/aeg-root/contracts/developer-reviewer.md +7 -7
  4. package/aeg-root/contracts/planner-developer.md +142 -0
  5. package/aeg-root/contracts/reviewer-archivist.md +2 -2
  6. package/aeg-root/contracts/tranche-archivist-planner.md +12 -11
  7. package/aeg-root/enforcement.md +15 -5
  8. package/aeg-root/process.md +53 -75
  9. package/aeg-root/roles/archivist.md +3 -3
  10. package/aeg-root/roles/developer.md +10 -10
  11. package/aeg-root/roles/planner.md +88 -23
  12. package/aeg-root/roles/principal.md +11 -11
  13. package/aeg-root/roles/reviewer.md +10 -10
  14. package/aeg-root/roles/security.md +7 -7
  15. package/aeg-root/roles/tranche-archivist.md +2 -2
  16. package/aeg-root/skills/aeg/SKILL.md +5 -5
  17. package/aeg-root/skills/aeg-roles/SKILL.md +7 -7
  18. package/aeg-root/state-machine.md +35 -34
  19. package/aeg-root/task-model.md +3 -3
  20. package/aeg-root/templates/brief-template.md +2 -2
  21. package/aeg-root/templates/issue-rationale-template.md +3 -3
  22. package/aeg-root/tranche-model.md +21 -21
  23. package/dist/checks/bin/check-body-bare-digits.js +116 -19
  24. package/dist/checks/bin/check-branch-topology.js +116 -19
  25. package/dist/checks/bin/check-brief-shape.js +121 -1101
  26. package/dist/checks/bin/check-changeset-coverage.js +877 -26
  27. package/dist/checks/bin/check-closes-n.js +116 -19
  28. package/dist/checks/bin/check-coherence.js +116 -19
  29. package/dist/checks/bin/check-dead-branch-push.js +116 -19
  30. package/dist/checks/bin/check-dispatch-readiness.js +119 -22
  31. package/dist/checks/bin/check-doc-coverage-push.js +877 -26
  32. package/dist/checks/bin/check-doc-coverage.js +879 -28
  33. package/dist/checks/bin/check-doctrine-no-procedures.js +116 -19
  34. package/dist/checks/bin/check-doctrine-portability.js +877 -26
  35. package/dist/checks/bin/check-evidence-fresh.js +116 -19
  36. package/dist/checks/bin/check-exec-bits.js +877 -26
  37. package/dist/checks/bin/check-first-push-dispatch.js +116 -19
  38. package/dist/checks/bin/check-issue-assignment.js +116 -19
  39. package/dist/checks/bin/check-main-branch-refusal.js +117 -20
  40. package/dist/checks/bin/check-no-disk-state.js +116 -19
  41. package/dist/checks/bin/check-pr-premise-reassert.js +5401 -0
  42. package/dist/checks/bin/check-pr-report-density.js +116 -19
  43. package/dist/checks/bin/check-quoted-command.js +875 -24
  44. package/dist/checks/bin/check-reader-resolvable-prose.js +875 -24
  45. package/dist/checks/bin/check-registry-gates.js +147 -21
  46. package/dist/checks/bin/check-retired-vocabulary.js +875 -24
  47. package/dist/checks/bin/check-review-gate.js +116 -19
  48. package/dist/checks/bin/check-single-plan-pr.js +116 -19
  49. package/dist/checks/bin/check-surface-scope.js +5722 -0
  50. package/dist/checks/bin/check-test-plan.js +116 -19
  51. package/dist/checks/bin/check-token-collection-wired.js +116 -19
  52. package/dist/checks/bin/check-token-report.js +128 -26
  53. package/dist/checks/bin/check-workspace-escape.js +875 -24
  54. package/dist/index.js +1002 -214
  55. package/package.json +1 -1
  56. package/aeg-root/contracts/brief-developer.md +0 -141
  57. package/aeg-root/contracts/planner-brief.md +0 -143
  58. package/aeg-root/roles/brief-author.md +0 -116
  59. package/aeg-root/skills/brief-authoring/SKILL.md +0 -509
@@ -24,6 +24,15 @@ var GATE_AUDIENCE = {
24
24
  internal: "A composite that shells this repo's own `typecheck`/`lint`/`test`/`build` scripts by name. Those names are this monorepo's toolchain (bun + turbo + biome), not an adopter's; the shipped equivalent of its intent is `vinaya check --all`, which is portable by construction."
25
25
  }
26
26
  };
27
+ var NON_GATE_BINS = [
28
+ "archive-task",
29
+ "assign-task-issue",
30
+ "dead-branch-audit",
31
+ "eval-agent-compliance",
32
+ "open-issue",
33
+ "open-pr",
34
+ "report-tokens"
35
+ ];
27
36
  function isShipped(a) {
28
37
  return "shippedAs" in a;
29
38
  }
@@ -384,7 +393,7 @@ var LABELS = [
384
393
  id: "vinaya/needs:brief-correction",
385
394
  category: "needs",
386
395
  form: "literal",
387
- carries: "Waiting on the Brief Author — the brief contradicts the surface it describes."
396
+ carries: "Waiting on the Planner — the brief contradicts the surface it describes."
388
397
  },
389
398
  {
390
399
  key: "waiver-docs",
@@ -2391,7 +2400,7 @@ function objectivesOf(body) {
2391
2400
  continue;
2392
2401
  }
2393
2402
  if (hasBacktickedPath(text) && wordCount(stripObjectiveBackticks(text)) < MIN_WORDS_OUTSIDE_BACKTICKS) {
2394
- errors.push(`O${n} is little more than a file path — an objective states an observable outcome, never a bare path (the Brief Author maps it to files).`);
2403
+ errors.push(`O${n} is little more than a file path — an objective states an observable outcome, never a bare path (the Planner maps it to files).`);
2395
2404
  continue;
2396
2405
  }
2397
2406
  objectives.push({ id: `O${n}`, text });
@@ -2603,18 +2612,14 @@ function checkTestPlanExclusivity(prBody) {
2603
2612
  function checkPrincipalPlaceholder(prBody) {
2604
2613
  const region = testPlanRegion(prBody);
2605
2614
  const lineRe = /^-\s*\[[ xX]\]\s*\*{2}\[principal\]\*{2}(.*)$/gim;
2615
+ const errors = [];
2606
2616
  for (const m of region.matchAll(lineRe)) {
2607
2617
  const content = m[1] ?? "";
2608
2618
  if (/^\s*None\b/i.test(content)) {
2609
- return {
2610
- status: "fail",
2611
- errors: [
2612
- 'brief-validation Test Plan shape: a `**[principal]**` checkbox item is a "None" placeholder — if there is no principal-runnable surface, omit the item entirely; an untickable placeholder box blocks the merge gate forever.'
2613
- ]
2614
- };
2619
+ errors.push(`brief-validation Test Plan shape: a \`**[principal]**\` checkbox item is a "None" placeholder ("${content.trim()}") — if there is no principal-runnable surface, omit the item entirely; an untickable placeholder box blocks the merge gate forever.`);
2615
2620
  }
2616
2621
  }
2617
- return { status: "pass", errors: [] };
2622
+ return errors.length > 0 ? { status: "fail", errors } : { status: "pass", errors: [] };
2618
2623
  }
2619
2624
  function checkSurfaceMap(prBody) {
2620
2625
  return headingCheck(prBody, "(?:technical\\s+)?surface map", "Technical surface map");
@@ -2930,6 +2935,18 @@ function checkBriefSections(prBody, readTier, options = {}) {
2930
2935
  ];
2931
2936
  return { errors: results.flatMap((r) => r.errors) };
2932
2937
  }
2938
+ var AEG_BRIEF_V1_MARKER = "<!-- aeg:brief:v1 -->";
2939
+ function contentAfterTwoLines(body) {
2940
+ const first = body.indexOf(`
2941
+ `);
2942
+ if (first === -1)
2943
+ return "";
2944
+ const second = body.indexOf(`
2945
+ `, first + 1);
2946
+ if (second === -1)
2947
+ return "";
2948
+ return body.slice(second + 1);
2949
+ }
2933
2950
  // ../../packages/aeg-core/src/doctrine-portability.ts
2934
2951
  var DEFAULT_SHIPS_PREFIX = "aeg-root/";
2935
2952
  var STATIC_PORTABLE_PREFIXES = [
@@ -3172,6 +3189,66 @@ function checkIssueObjectives(body, issueNumber) {
3172
3189
  return { status: "pass", errors: [] };
3173
3190
  return { status: "fail", errors: result.errors.map((e) => `issue-validation objectives: ${e}`) };
3174
3191
  }
3192
+ function topLevelSectionText(body, headingName) {
3193
+ const headingRe = new RegExp(`^##[ \\t]*${headingName}[ \\t]*$`, "im");
3194
+ const heading = headingRe.exec(body);
3195
+ if (!heading)
3196
+ return null;
3197
+ const afterHeading = body.slice(heading.index + heading[0].length);
3198
+ const next = /^##[ \t]/m.exec(afterHeading);
3199
+ return next ? afterHeading.slice(0, next.index) : afterHeading;
3200
+ }
3201
+ function looksLikeFilePath(entry2) {
3202
+ const stripped = entry2.replace(/\/\*\*?$/, "");
3203
+ const lastSegment = stripped.split("/").pop() ?? stripped;
3204
+ const extMatch = /\.[A-Za-z0-9]{1,6}$/.exec(lastSegment);
3205
+ return extMatch !== null && extMatch.index > 0;
3206
+ }
3207
+ function splitGlobList(raw) {
3208
+ return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
3209
+ }
3210
+ function parseIssueSurface(body) {
3211
+ const section = topLevelSectionText(body, "Surface");
3212
+ if (section === null)
3213
+ return { ok: false, errors: ["no `## Surface` heading found in the body."] };
3214
+ const inLine = /^in:\s*(.+)$/im.exec(section);
3215
+ const outLine = /^out:\s*(.+)$/im.exec(section);
3216
+ const errors = [];
3217
+ if (!inLine)
3218
+ errors.push("`## Surface` has no `in:` line.");
3219
+ if (!outLine)
3220
+ errors.push("`## Surface` has no `out:` line.");
3221
+ if (errors.length > 0)
3222
+ return { ok: false, errors };
3223
+ const inGlobs = splitGlobList(inLine[1]);
3224
+ const outGlobs = splitGlobList(outLine[1]);
3225
+ for (const g of [...inGlobs, ...outGlobs]) {
3226
+ if (looksLikeFilePath(g)) {
3227
+ errors.push(`\`${g}\` in \`## Surface\` looks like a file path — Surface entries are directory-level globs, never file paths.`);
3228
+ }
3229
+ }
3230
+ if (inGlobs.length === 0)
3231
+ errors.push("`## Surface`'s `in:` line resolved to zero globs.");
3232
+ if (errors.length > 0)
3233
+ return { ok: false, errors };
3234
+ return { ok: true, value: { in: inGlobs, out: outGlobs } };
3235
+ }
3236
+ function globCoversPath(glob, path) {
3237
+ const g = glob.replace(/\/\*\*?$/, "").replace(/\/+$/, "");
3238
+ const p = path.replace(/\/+$/, "");
3239
+ return g === p || g.startsWith(`${p}/`) || p.startsWith(`${g}/`);
3240
+ }
3241
+ function checkSurfaceScope(changedFiles, outGlobs) {
3242
+ if (outGlobs.length === 0)
3243
+ return { ok: true };
3244
+ const violations = [];
3245
+ for (const file of changedFiles) {
3246
+ const glob = outGlobs.find((g) => globCoversPath(g, file));
3247
+ if (glob)
3248
+ violations.push({ file, glob });
3249
+ }
3250
+ return violations.length > 0 ? { ok: false, violations } : { ok: true };
3251
+ }
3175
3252
  function isTaskIssueLabelSet(labels) {
3176
3253
  return hasLabel("tranche", labels);
3177
3254
  }
@@ -3236,6 +3313,8 @@ function checkProjectsRegistered(body, _labels, registeredNames) {
3236
3313
  }
3237
3314
  return errors.length > 0 ? { status: "fail", errors } : { status: "pass", errors: [] };
3238
3315
  }
3316
+ var DOC_PATH_RE = /(?:(?:aeg-root|apps|packages|specs|docs|tools|\.claude|\.github)\/[\w./@-]*\.(?:md|mdx)|\.claude\/(?:skills|rules)\/[\w./-]+|\b(?:docs-index|decisions|projects|state-machine|enforcement|process|README|CLAUDE)\.md\b|\b[\w-]+-(?:spec|decisions|backlog)\.md\b)/i;
3317
+ var DOC_PATH_RE_GLOBAL = new RegExp(DOC_PATH_RE.source, "gi");
3239
3318
 
3240
3319
  // ../../packages/aeg-core/src/coherence-checks.ts
3241
3320
  var COHERENCE_ENFORCED_FROM = "2026-07-01";
@@ -3765,16 +3844,33 @@ function checkG1(rows, existsFn) {
3765
3844
  return { check: "G1", status: findings.length > 0 ? "fail" : "pass", findings };
3766
3845
  }
3767
3846
  var SCAFFOLD_PLACEHOLDER = "[undocumented — fill in why]";
3847
+ var NON_GATE_BIN_NAMES = NON_GATE_BINS;
3848
+ var TWIN_CANDIDATE_EXCEPTIONS = {
3849
+ "apps/cli/src/checks/bin/check-dead-branch-push.ts": 'Ring 0 "Pushing to a branch whose pull request already resolved" (implementation: check-push-target.ts)',
3850
+ "apps/cli/src/checks/bin/check-issue-assignment.ts": `Ring 0 "A task branch's first push (Issue self-assignment)" (implementation: assign-task-issue.ts)`,
3851
+ "apps/cli/src/checks/bin/check-closes-n.ts": 'Ring 1 "Closes linkage" (implementation: verify-coherence.ts)'
3852
+ };
3853
+ var NON_GATE_HOOK_SCRIPTS = [".claude/hooks/track-transcript.sh"];
3768
3854
  function checkG2(rows, candidateFiles) {
3769
3855
  const implementations = new Set(rows.map((r) => r.implementation).filter((p) => p !== ""));
3856
+ const claimedNames = new Set(rows.flatMap((r) => claimedCheckNames(r.implementation)));
3770
3857
  const findings = [];
3771
3858
  for (const path of candidateFiles) {
3772
- if (!implementations.has(path)) {
3773
- findings.push({
3774
- path,
3775
- reason: `"${path}" is not named as the implementation of any row in enforcement.md's ring tables`
3776
- });
3777
- }
3859
+ if (implementations.has(path))
3860
+ continue;
3861
+ if (path.startsWith(AEG_CORE_BIN_PATH_PREFIX) && NON_GATE_BIN_NAMES.includes(basenameNoExt(path)))
3862
+ continue;
3863
+ if (NON_GATE_HOOK_SCRIPTS.includes(path))
3864
+ continue;
3865
+ if (path in TWIN_CANDIDATE_EXCEPTIONS)
3866
+ continue;
3867
+ const candidateNames = claimedCheckNames(path);
3868
+ if (candidateNames.length > 0 && candidateNames.some((n) => claimedNames.has(n)))
3869
+ continue;
3870
+ findings.push({
3871
+ path,
3872
+ reason: `"${path}" is not named as the implementation of any row in enforcement.md's ring tables`
3873
+ });
3778
3874
  }
3779
3875
  for (const row of rows) {
3780
3876
  const carriesPlaceholder = [row.summary, row.description, row.spec].some((cell) => cell === SCAFFOLD_PLACEHOLDER);
@@ -3786,7 +3882,7 @@ function checkG2(rows, candidateFiles) {
3786
3882
  });
3787
3883
  }
3788
3884
  }
3789
- return { check: "G2", status: findings.length > 0 ? "info" : "pass", findings };
3885
+ return { check: "G2", status: findings.length > 0 ? "fail" : "pass", findings };
3790
3886
  }
3791
3887
  function checkG3(ring0Rows, crossingFiles) {
3792
3888
  const ring0Implementations = new Set(ring0Rows.map((r) => r.implementation));
@@ -4662,7 +4758,6 @@ function findWorkspaceEscapes(files, knownPaths, workspaceDirs = DEFAULT_WORKSPA
4662
4758
  import { z } from "zod";
4663
4759
  var ROLE_VALUES = [
4664
4760
  "planner",
4665
- "brief-author",
4666
4761
  "developer",
4667
4762
  "code-reviewer",
4668
4763
  "security",
@@ -4736,18 +4831,20 @@ var dispatchShared = {
4736
4831
  round: z.number().int().optional(),
4737
4832
  effect_id: z.string()
4738
4833
  };
4834
+ var dispatchUsageField = z.object({ input: z.number().nonnegative(), output: z.number().nonnegative() }).strict().nullable();
4739
4835
  var DispatchEventSchema = z.discriminatedUnion("event", [
4740
4836
  z.object({ ...dispatchShared, event: z.literal("dispatched"), prompt_hash: z.string() }).strict(),
4741
4837
  z.object({
4742
4838
  ...dispatchShared,
4743
4839
  event: z.literal("outcome_received"),
4744
4840
  outcome: DispatchOutcomeSchema,
4745
- usage: z.object({ input: z.number().nonnegative(), output: z.number().nonnegative() }).strict().nullable()
4841
+ usage: dispatchUsageField
4746
4842
  }).strict(),
4747
4843
  z.object({
4748
4844
  ...dispatchShared,
4749
4845
  event: z.literal("dispatch_failed"),
4750
- reason: z.enum(["timeout", "crash", "refused", "unattributed_write"])
4846
+ reason: z.enum(["timeout", "crash", "refused", "unattributed_write"]),
4847
+ usage: dispatchUsageField
4751
4848
  }).strict()
4752
4849
  ]);
4753
4850
  var loopShared = {
@@ -28,6 +28,15 @@ var GATE_AUDIENCE = {
28
28
  internal: "A composite that shells this repo's own `typecheck`/`lint`/`test`/`build` scripts by name. Those names are this monorepo's toolchain (bun + turbo + biome), not an adopter's; the shipped equivalent of its intent is `vinaya check --all`, which is portable by construction."
29
29
  }
30
30
  };
31
+ var NON_GATE_BINS = [
32
+ "archive-task",
33
+ "assign-task-issue",
34
+ "dead-branch-audit",
35
+ "eval-agent-compliance",
36
+ "open-issue",
37
+ "open-pr",
38
+ "report-tokens"
39
+ ];
31
40
  function isShipped(a) {
32
41
  return "shippedAs" in a;
33
42
  }
@@ -388,7 +397,7 @@ var LABELS = [
388
397
  id: "vinaya/needs:brief-correction",
389
398
  category: "needs",
390
399
  form: "literal",
391
- carries: "Waiting on the Brief Author — the brief contradicts the surface it describes."
400
+ carries: "Waiting on the Planner — the brief contradicts the surface it describes."
392
401
  },
393
402
  {
394
403
  key: "waiver-docs",
@@ -2395,7 +2404,7 @@ function objectivesOf(body) {
2395
2404
  continue;
2396
2405
  }
2397
2406
  if (hasBacktickedPath(text) && wordCount(stripObjectiveBackticks(text)) < MIN_WORDS_OUTSIDE_BACKTICKS) {
2398
- errors.push(`O${n} is little more than a file path — an objective states an observable outcome, never a bare path (the Brief Author maps it to files).`);
2407
+ errors.push(`O${n} is little more than a file path — an objective states an observable outcome, never a bare path (the Planner maps it to files).`);
2399
2408
  continue;
2400
2409
  }
2401
2410
  objectives.push({ id: `O${n}`, text });
@@ -2607,18 +2616,14 @@ function checkTestPlanExclusivity(prBody) {
2607
2616
  function checkPrincipalPlaceholder(prBody) {
2608
2617
  const region = testPlanRegion(prBody);
2609
2618
  const lineRe = /^-\s*\[[ xX]\]\s*\*{2}\[principal\]\*{2}(.*)$/gim;
2619
+ const errors = [];
2610
2620
  for (const m of region.matchAll(lineRe)) {
2611
2621
  const content = m[1] ?? "";
2612
2622
  if (/^\s*None\b/i.test(content)) {
2613
- return {
2614
- status: "fail",
2615
- errors: [
2616
- 'brief-validation Test Plan shape: a `**[principal]**` checkbox item is a "None" placeholder — if there is no principal-runnable surface, omit the item entirely; an untickable placeholder box blocks the merge gate forever.'
2617
- ]
2618
- };
2623
+ errors.push(`brief-validation Test Plan shape: a \`**[principal]**\` checkbox item is a "None" placeholder ("${content.trim()}") — if there is no principal-runnable surface, omit the item entirely; an untickable placeholder box blocks the merge gate forever.`);
2619
2624
  }
2620
2625
  }
2621
- return { status: "pass", errors: [] };
2626
+ return errors.length > 0 ? { status: "fail", errors } : { status: "pass", errors: [] };
2622
2627
  }
2623
2628
  function checkSurfaceMap(prBody) {
2624
2629
  return headingCheck(prBody, "(?:technical\\s+)?surface map", "Technical surface map");
@@ -2934,6 +2939,18 @@ function checkBriefSections(prBody, readTier, options = {}) {
2934
2939
  ];
2935
2940
  return { errors: results.flatMap((r) => r.errors) };
2936
2941
  }
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);
2953
+ }
2937
2954
  // ../../packages/aeg-core/src/doctrine-portability.ts
2938
2955
  var DEFAULT_SHIPS_PREFIX = "aeg-root/";
2939
2956
  var STATIC_PORTABLE_PREFIXES = [
@@ -3176,6 +3193,66 @@ function checkIssueObjectives(body, issueNumber) {
3176
3193
  return { status: "pass", errors: [] };
3177
3194
  return { status: "fail", errors: result.errors.map((e) => `issue-validation objectives: ${e}`) };
3178
3195
  }
3196
+ function topLevelSectionText(body, headingName) {
3197
+ const headingRe = new RegExp(`^##[ \\t]*${headingName}[ \\t]*$`, "im");
3198
+ const heading = headingRe.exec(body);
3199
+ if (!heading)
3200
+ return null;
3201
+ const afterHeading = body.slice(heading.index + heading[0].length);
3202
+ const next = /^##[ \t]/m.exec(afterHeading);
3203
+ return next ? afterHeading.slice(0, next.index) : afterHeading;
3204
+ }
3205
+ function looksLikeFilePath(entry2) {
3206
+ const stripped = entry2.replace(/\/\*\*?$/, "");
3207
+ const lastSegment = stripped.split("/").pop() ?? stripped;
3208
+ const extMatch = /\.[A-Za-z0-9]{1,6}$/.exec(lastSegment);
3209
+ return extMatch !== null && extMatch.index > 0;
3210
+ }
3211
+ function splitGlobList(raw) {
3212
+ return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
3213
+ }
3214
+ function parseIssueSurface(body) {
3215
+ const section = topLevelSectionText(body, "Surface");
3216
+ if (section === null)
3217
+ return { ok: false, errors: ["no `## Surface` heading found in the body."] };
3218
+ const inLine = /^in:\s*(.+)$/im.exec(section);
3219
+ const outLine = /^out:\s*(.+)$/im.exec(section);
3220
+ const errors = [];
3221
+ if (!inLine)
3222
+ errors.push("`## Surface` has no `in:` line.");
3223
+ if (!outLine)
3224
+ errors.push("`## Surface` has no `out:` line.");
3225
+ if (errors.length > 0)
3226
+ return { ok: false, errors };
3227
+ const inGlobs = splitGlobList(inLine[1]);
3228
+ const outGlobs = splitGlobList(outLine[1]);
3229
+ for (const g of [...inGlobs, ...outGlobs]) {
3230
+ if (looksLikeFilePath(g)) {
3231
+ errors.push(`\`${g}\` in \`## Surface\` looks like a file path — Surface entries are directory-level globs, never file paths.`);
3232
+ }
3233
+ }
3234
+ if (inGlobs.length === 0)
3235
+ errors.push("`## Surface`'s `in:` line resolved to zero globs.");
3236
+ if (errors.length > 0)
3237
+ return { ok: false, errors };
3238
+ return { ok: true, value: { in: inGlobs, out: outGlobs } };
3239
+ }
3240
+ function globCoversPath(glob, path) {
3241
+ const g = glob.replace(/\/\*\*?$/, "").replace(/\/+$/, "");
3242
+ const p = path.replace(/\/+$/, "");
3243
+ return g === p || g.startsWith(`${p}/`) || p.startsWith(`${g}/`);
3244
+ }
3245
+ function checkSurfaceScope(changedFiles, outGlobs) {
3246
+ if (outGlobs.length === 0)
3247
+ return { ok: true };
3248
+ const violations = [];
3249
+ for (const file of changedFiles) {
3250
+ const glob = outGlobs.find((g) => globCoversPath(g, file));
3251
+ if (glob)
3252
+ violations.push({ file, glob });
3253
+ }
3254
+ return violations.length > 0 ? { ok: false, violations } : { ok: true };
3255
+ }
3179
3256
  function isTaskIssueLabelSet(labels) {
3180
3257
  return hasLabel("tranche", labels);
3181
3258
  }
@@ -3240,6 +3317,8 @@ function checkProjectsRegistered(body, _labels, registeredNames) {
3240
3317
  }
3241
3318
  return errors.length > 0 ? { status: "fail", errors } : { status: "pass", errors: [] };
3242
3319
  }
3320
+ var DOC_PATH_RE = /(?:(?:aeg-root|apps|packages|specs|docs|tools|\.claude|\.github)\/[\w./@-]*\.(?:md|mdx)|\.claude\/(?:skills|rules)\/[\w./-]+|\b(?:docs-index|decisions|projects|state-machine|enforcement|process|README|CLAUDE)\.md\b|\b[\w-]+-(?:spec|decisions|backlog)\.md\b)/i;
3321
+ var DOC_PATH_RE_GLOBAL = new RegExp(DOC_PATH_RE.source, "gi");
3243
3322
 
3244
3323
  // ../../packages/aeg-core/src/coherence-checks.ts
3245
3324
  var COHERENCE_ENFORCED_FROM = "2026-07-01";
@@ -3769,16 +3848,33 @@ function checkG1(rows, existsFn) {
3769
3848
  return { check: "G1", status: findings.length > 0 ? "fail" : "pass", findings };
3770
3849
  }
3771
3850
  var SCAFFOLD_PLACEHOLDER = "[undocumented — fill in why]";
3851
+ var NON_GATE_BIN_NAMES = NON_GATE_BINS;
3852
+ var TWIN_CANDIDATE_EXCEPTIONS = {
3853
+ "apps/cli/src/checks/bin/check-dead-branch-push.ts": 'Ring 0 "Pushing to a branch whose pull request already resolved" (implementation: check-push-target.ts)',
3854
+ "apps/cli/src/checks/bin/check-issue-assignment.ts": `Ring 0 "A task branch's first push (Issue self-assignment)" (implementation: assign-task-issue.ts)`,
3855
+ "apps/cli/src/checks/bin/check-closes-n.ts": 'Ring 1 "Closes linkage" (implementation: verify-coherence.ts)'
3856
+ };
3857
+ var NON_GATE_HOOK_SCRIPTS = [".claude/hooks/track-transcript.sh"];
3772
3858
  function checkG2(rows, candidateFiles) {
3773
3859
  const implementations = new Set(rows.map((r) => r.implementation).filter((p) => p !== ""));
3860
+ const claimedNames = new Set(rows.flatMap((r) => claimedCheckNames(r.implementation)));
3774
3861
  const findings = [];
3775
3862
  for (const path of candidateFiles) {
3776
- if (!implementations.has(path)) {
3777
- findings.push({
3778
- path,
3779
- reason: `"${path}" is not named as the implementation of any row in enforcement.md's ring tables`
3780
- });
3781
- }
3863
+ if (implementations.has(path))
3864
+ continue;
3865
+ if (path.startsWith(AEG_CORE_BIN_PATH_PREFIX) && NON_GATE_BIN_NAMES.includes(basenameNoExt(path)))
3866
+ continue;
3867
+ if (NON_GATE_HOOK_SCRIPTS.includes(path))
3868
+ continue;
3869
+ if (path in TWIN_CANDIDATE_EXCEPTIONS)
3870
+ continue;
3871
+ const candidateNames = claimedCheckNames(path);
3872
+ if (candidateNames.length > 0 && candidateNames.some((n) => claimedNames.has(n)))
3873
+ continue;
3874
+ findings.push({
3875
+ path,
3876
+ reason: `"${path}" is not named as the implementation of any row in enforcement.md's ring tables`
3877
+ });
3782
3878
  }
3783
3879
  for (const row of rows) {
3784
3880
  const carriesPlaceholder = [row.summary, row.description, row.spec].some((cell) => cell === SCAFFOLD_PLACEHOLDER);
@@ -3790,7 +3886,7 @@ function checkG2(rows, candidateFiles) {
3790
3886
  });
3791
3887
  }
3792
3888
  }
3793
- return { check: "G2", status: findings.length > 0 ? "info" : "pass", findings };
3889
+ return { check: "G2", status: findings.length > 0 ? "fail" : "pass", findings };
3794
3890
  }
3795
3891
  function checkG3(ring0Rows, crossingFiles) {
3796
3892
  const ring0Implementations = new Set(ring0Rows.map((r) => r.implementation));
@@ -4666,7 +4762,6 @@ function findWorkspaceEscapes(files, knownPaths, workspaceDirs = DEFAULT_WORKSPA
4666
4762
  import { z } from "zod";
4667
4763
  var ROLE_VALUES = [
4668
4764
  "planner",
4669
- "brief-author",
4670
4765
  "developer",
4671
4766
  "code-reviewer",
4672
4767
  "security",
@@ -4740,18 +4835,20 @@ var dispatchShared = {
4740
4835
  round: z.number().int().optional(),
4741
4836
  effect_id: z.string()
4742
4837
  };
4838
+ var dispatchUsageField = z.object({ input: z.number().nonnegative(), output: z.number().nonnegative() }).strict().nullable();
4743
4839
  var DispatchEventSchema = z.discriminatedUnion("event", [
4744
4840
  z.object({ ...dispatchShared, event: z.literal("dispatched"), prompt_hash: z.string() }).strict(),
4745
4841
  z.object({
4746
4842
  ...dispatchShared,
4747
4843
  event: z.literal("outcome_received"),
4748
4844
  outcome: DispatchOutcomeSchema,
4749
- usage: z.object({ input: z.number().nonnegative(), output: z.number().nonnegative() }).strict().nullable()
4845
+ usage: dispatchUsageField
4750
4846
  }).strict(),
4751
4847
  z.object({
4752
4848
  ...dispatchShared,
4753
4849
  event: z.literal("dispatch_failed"),
4754
- reason: z.enum(["timeout", "crash", "refused", "unattributed_write"])
4850
+ reason: z.enum(["timeout", "crash", "refused", "unattributed_write"]),
4851
+ usage: dispatchUsageField
4755
4852
  }).strict()
4756
4853
  ]);
4757
4854
  var loopShared = {