@attalabs/vinaya 0.1.0 → 0.1.2

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 (47) hide show
  1. package/LICENSE +201 -661
  2. package/README.md +1 -1
  3. package/aeg-root/contracts/archivist-tranche-archivist.md +92 -0
  4. package/aeg-root/contracts/brief-developer.md +140 -0
  5. package/aeg-root/contracts/developer-reviewer.md +106 -0
  6. package/aeg-root/contracts/planner-brief.md +130 -0
  7. package/aeg-root/contracts/reviewer-archivist.md +96 -0
  8. package/aeg-root/contracts/tranche-archivist-planner.md +116 -0
  9. package/aeg-root/coordination.md +293 -0
  10. package/aeg-root/enforcement.md +159 -0
  11. package/aeg-root/glossary.md +32 -0
  12. package/aeg-root/process.md +371 -0
  13. package/aeg-root/roles/archivist.md +167 -0
  14. package/aeg-root/roles/brief-author.md +108 -0
  15. package/aeg-root/roles/developer.md +429 -0
  16. package/aeg-root/roles/planner.md +285 -0
  17. package/aeg-root/roles/principal.md +107 -0
  18. package/aeg-root/roles/reviewer.md +130 -0
  19. package/aeg-root/roles/security.md +121 -0
  20. package/aeg-root/roles/tranche-archivist.md +257 -0
  21. package/aeg-root/skills/aeg/SKILL.md +90 -0
  22. package/aeg-root/skills/aeg-roles/SKILL.md +58 -0
  23. package/aeg-root/skills/brief-authoring/SKILL.md +497 -0
  24. package/aeg-root/state-machine.md +642 -0
  25. package/aeg-root/templates/brief-template.md +103 -0
  26. package/aeg-root/templates/issue-rationale-template.md +35 -0
  27. package/aeg-root/templates/pr-report-template.md +68 -0
  28. package/aeg-root/tranche-model.md +304 -0
  29. package/dist/checks/bin/check-branch-topology.js +2840 -0
  30. package/dist/checks/bin/check-brief-shape.js +2775 -0
  31. package/dist/checks/bin/check-closes-n.js +2824 -0
  32. package/dist/checks/bin/check-coherence.js +2922 -0
  33. package/dist/checks/bin/check-dead-branch-push.js +2799 -0
  34. package/dist/checks/bin/check-dispatch-readiness.js +2923 -0
  35. package/dist/checks/bin/check-doc-coverage-push.js +2818 -0
  36. package/dist/checks/bin/check-doc-coverage.js +2816 -0
  37. package/dist/checks/bin/check-first-push-dispatch.js +2888 -0
  38. package/dist/checks/bin/check-issue-assignment.js +2863 -0
  39. package/dist/checks/bin/check-no-disk-state.js +2811 -0
  40. package/dist/checks/bin/check-reader-resolvable-prose.js +2820 -0
  41. package/dist/checks/bin/check-registry-gates.js +2923 -0
  42. package/dist/checks/bin/check-review-gate.js +2826 -0
  43. package/dist/checks/bin/check-single-plan-pr.js +2810 -0
  44. package/dist/checks/bin/check-test-plan.js +2775 -0
  45. package/dist/index.js +1563 -446
  46. package/package.json +7 -4
  47. package/templates/custom-check.template.ts +1 -1
@@ -0,0 +1,2923 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/checks/bin/check-dispatch-readiness.ts
4
+ import { execFile as execFile4, execFileSync as execFileSync2 } from "node:child_process";
5
+ import { promisify as promisify4 } from "node:util";
6
+ // ../../../packages/aeg-core/src/anchored-region.ts
7
+ function maskCode(body) {
8
+ const fill = (line) => " ".repeat(line.length);
9
+ return maskIndentedCode(maskFencedCode(body, fill), fill).replace(/(`+)[^\n]*?\1/g, (m) => " ".repeat(m.length));
10
+ }
11
+ function stripCode(body, options = {}) {
12
+ const normalised = body.replace(/\r\n?/g, `
13
+ `);
14
+ const blank = () => "";
15
+ const blocksStripped = maskIndentedCode(maskFencedCode(normalised, blank), blank);
16
+ if (options.inlineSpans === "keep")
17
+ return blocksStripped;
18
+ return blocksStripped.replace(/(`+)[^\n]*?\1/g, "");
19
+ }
20
+ var FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})([^\r\n]*)\r?$/;
21
+ var FENCE_CLOSE = /^ {0,3}(`+|~+)[ \t]*\r?$/;
22
+ function maskFencedCode(body, fill) {
23
+ let fenceChar = null;
24
+ let fenceLen = 0;
25
+ return body.split(`
26
+ `).map((line) => {
27
+ if (fenceChar === null) {
28
+ const open = line.match(FENCE_OPEN);
29
+ if (!open)
30
+ return line;
31
+ const marker = open[1];
32
+ if (marker[0] === "`" && open[2].includes("`"))
33
+ return line;
34
+ fenceChar = marker[0];
35
+ fenceLen = marker.length;
36
+ return fill(line);
37
+ }
38
+ const close = line.match(FENCE_CLOSE);
39
+ if (close && close[1][0] === fenceChar && close[1].length >= fenceLen) {
40
+ fenceChar = null;
41
+ fenceLen = 0;
42
+ }
43
+ return fill(line);
44
+ }).join(`
45
+ `);
46
+ }
47
+ var LIST_MARKER = /^ {0,3}(?:[-*+]|\d{1,9}[.)])(?:\s|$)/;
48
+ function indentWidth(line) {
49
+ let width = 0;
50
+ for (const ch of line) {
51
+ if (ch === " ")
52
+ width += 1;
53
+ else if (ch === "\t")
54
+ width += 4;
55
+ else
56
+ break;
57
+ }
58
+ return width;
59
+ }
60
+ function maskIndentedCode(body, fill) {
61
+ const lines = body.split(`
62
+ `);
63
+ let inList = false;
64
+ let inCode = false;
65
+ let prevBlank = true;
66
+ const out = lines.map((line) => {
67
+ if (line.trim() === "") {
68
+ prevBlank = true;
69
+ return line;
70
+ }
71
+ const indent = indentWidth(line);
72
+ if (indent >= 4 && (inCode || prevBlank && !inList)) {
73
+ inCode = true;
74
+ prevBlank = false;
75
+ return fill(line);
76
+ }
77
+ inCode = false;
78
+ if (indent < 4 && LIST_MARKER.test(line))
79
+ inList = true;
80
+ else if (indent === 0)
81
+ inList = false;
82
+ prevBlank = false;
83
+ return line;
84
+ });
85
+ return out.join(`
86
+ `);
87
+ }
88
+ function anchoredRegion(body, field) {
89
+ const masked = maskCode(body);
90
+ const start = new RegExp(`<!--\\s*AEG:${field}:START\\s*-->`).exec(masked);
91
+ if (!start)
92
+ return null;
93
+ const afterStart = start.index + start[0].length;
94
+ const end = new RegExp(`<!--\\s*AEG:${field}:END\\s*-->`).exec(masked.slice(afterStart));
95
+ if (!end)
96
+ return null;
97
+ return body.slice(afterStart, afterStart + end.index);
98
+ }
99
+ // ../../../packages/aeg-forge-state/src/parse-rationale-deps.ts
100
+ var SECTION_HEADER = /\*\*Dependency rationale\*\*/i;
101
+ var NEXT_HEADER = /\*\*[A-Z][^*]*\*\*/;
102
+ var FIELD_LABEL = /^(Depends-on|Conflicts-with)\s*:\s*(.*)$/i;
103
+ var ID_TOKEN = /^(?:[\w.-]+\s+)?#?\d+[a-z]?$/i;
104
+ var SLUG_QUALIFIED_ID = /^([\w.-]+)\s+(#?\d+[a-z]?)$/i;
105
+ function isEmptyMarker(s) {
106
+ const t = s.trim();
107
+ return t === "" || t === "—" || t === "-" || t === "–";
108
+ }
109
+ function pushUnique(arr, ids) {
110
+ for (const id of ids) {
111
+ if (!arr.includes(id))
112
+ arr.push(id);
113
+ }
114
+ }
115
+ function splitIds(raw) {
116
+ if (isEmptyMarker(raw))
117
+ return [];
118
+ return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0 && !isEmptyMarker(s) && ID_TOKEN.test(s));
119
+ }
120
+ function resolveIds(raw, lastSlug) {
121
+ const ids = splitIds(raw);
122
+ const resolved = [];
123
+ for (const id of ids) {
124
+ const qualified = id.match(SLUG_QUALIFIED_ID);
125
+ if (qualified) {
126
+ lastSlug.current = qualified[1] ?? null;
127
+ resolved.push(id);
128
+ } else if (lastSlug.current) {
129
+ resolved.push(`${lastSlug.current} ${id}`);
130
+ lastSlug.current = null;
131
+ } else {
132
+ resolved.push(id);
133
+ }
134
+ }
135
+ return resolved;
136
+ }
137
+ function extractSection(body) {
138
+ const start = body.match(SECTION_HEADER);
139
+ if (!start || start.index === undefined)
140
+ return "";
141
+ const rest = body.slice(start.index + start[0].length);
142
+ const next = rest.match(NEXT_HEADER);
143
+ const end = next && next.index !== undefined ? next.index : rest.length;
144
+ return rest.slice(0, end);
145
+ }
146
+ function parseRationaleDeps(body) {
147
+ const section = extractSection(body);
148
+ const dependsOn = [];
149
+ const conflictsWith = [];
150
+ let current = null;
151
+ const assignedFields = new Set;
152
+ const lastSlugByField = {
153
+ dependsOn: { current: null },
154
+ conflictsWith: { current: null }
155
+ };
156
+ const spanPattern = /`([^`]*)`/g;
157
+ let match = spanPattern.exec(section);
158
+ while (match !== null) {
159
+ const content = (match[1] ?? "").trim();
160
+ const labelMatch = content.match(FIELD_LABEL);
161
+ if (labelMatch) {
162
+ const field = labelMatch[1]?.toLowerCase() === "conflicts-with" ? "conflictsWith" : "dependsOn";
163
+ if (!assignedFields.has(field)) {
164
+ assignedFields.add(field);
165
+ current = field;
166
+ const ids = resolveIds(labelMatch[2] ?? "", lastSlugByField[field]);
167
+ pushUnique(field === "dependsOn" ? dependsOn : conflictsWith, ids);
168
+ }
169
+ } else if (current) {
170
+ const ids = resolveIds(content, lastSlugByField[current]);
171
+ pushUnique(current === "dependsOn" ? dependsOn : conflictsWith, ids);
172
+ }
173
+ match = spanPattern.exec(section);
174
+ }
175
+ return { dependsOn, conflictsWith };
176
+ }
177
+ // ../../../packages/aeg-forge-state/src/gh.ts
178
+ import { execFile, execFileSync } from "node:child_process";
179
+ import { promisify } from "node:util";
180
+ var execFileAsync = promisify(execFile);
181
+ var systemEnv = {
182
+ ...process.env,
183
+ PATH: [process.env.PATH, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].filter(Boolean).join(":")
184
+ };
185
+ function run(args) {
186
+ return execFileSync("gh", args, { encoding: "utf8", env: systemEnv });
187
+ }
188
+ async function runAsync(args) {
189
+ const { stdout } = await execFileAsync("gh", args, {
190
+ encoding: "utf8",
191
+ env: systemEnv,
192
+ maxBuffer: 16 * 1024 * 1024
193
+ });
194
+ return stdout;
195
+ }
196
+ function ghApiGet(path) {
197
+ return JSON.parse(run(["api", path]));
198
+ }
199
+ function issueListByLabelArgs(owner, repo, label) {
200
+ return [
201
+ "issue",
202
+ "list",
203
+ "--repo",
204
+ `${owner}/${repo}`,
205
+ "--label",
206
+ label,
207
+ "--state",
208
+ "all",
209
+ "--json",
210
+ "number,title,body,state,labels,milestone",
211
+ "--limit",
212
+ "200"
213
+ ];
214
+ }
215
+ async function ghIssueListByLabelAsync(owner, repo, label) {
216
+ return JSON.parse(await runAsync(issueListByLabelArgs(owner, repo, label)));
217
+ }
218
+ function dedupeByNumber(issues) {
219
+ const byNumber = new Map;
220
+ for (const issue of issues)
221
+ if (!byNumber.has(issue.number))
222
+ byNumber.set(issue.number, issue);
223
+ return [...byNumber.values()];
224
+ }
225
+ async function ghIssueListByAnyLabelAsync(owner, repo, labels) {
226
+ const perLabel = await Promise.all(labels.map((l) => ghIssueListByLabelAsync(owner, repo, l)));
227
+ return dedupeByNumber(perLabel.flat());
228
+ }
229
+
230
+ // ../../../packages/aeg-forge-state/src/fetch-milestone.ts
231
+ function findMilestoneForSlug(owner, repo, slug) {
232
+ const milestones = ghApiGet(`repos/${owner}/${repo}/milestones?state=all&per_page=100`);
233
+ const match = milestones.find((m) => m.title === slug);
234
+ if (!match)
235
+ return null;
236
+ return {
237
+ goal: match.description ?? "",
238
+ lifecycle: match.state === "closed" ? "complete" : "active"
239
+ };
240
+ }
241
+
242
+ // ../../../packages/aeg-forge-state/src/labels.ts
243
+ var LABELS = [
244
+ {
245
+ key: "blocked",
246
+ id: "vinaya/blocked",
247
+ category: "state",
248
+ form: "literal",
249
+ carries: "Execution is halted pending an external unblock; wins over every other derived status."
250
+ },
251
+ {
252
+ key: "tier-0",
253
+ id: "vinaya/tier:0",
254
+ category: "tier",
255
+ form: "literal",
256
+ carries: "Lowest governance weight — typecheck, lint, tests, and a conforming PR body."
257
+ },
258
+ {
259
+ key: "tier-1",
260
+ id: "vinaya/tier:1",
261
+ category: "tier",
262
+ form: "literal",
263
+ carries: "Tier 0 plus spec/skill coverage and a passing verify-docs run."
264
+ },
265
+ {
266
+ key: "tier-3",
267
+ id: "vinaya/tier:3",
268
+ category: "tier",
269
+ form: "literal",
270
+ carries: "Tier 1 plus the reasoning for the change, stated in the pull request that makes it."
271
+ },
272
+ {
273
+ key: "tranche",
274
+ id: "vinaya/tranche:",
275
+ category: "tranche",
276
+ form: "prefix",
277
+ carries: "The tranche slug this task Issue belongs to — the forge's grouping key, matched by prefix."
278
+ },
279
+ {
280
+ key: "needs-execution-input",
281
+ id: "vinaya/needs:execution-input",
282
+ category: "needs",
283
+ form: "literal",
284
+ carries: "Waiting on a missing execution detail — a flag, a dependency, a value the brief did not carry."
285
+ },
286
+ {
287
+ key: "needs-strategy-input",
288
+ id: "vinaya/needs:strategy-input",
289
+ category: "needs",
290
+ form: "literal",
291
+ carries: "Waiting on a strategy call — the brief assumes an approach the codebase has moved away from."
292
+ },
293
+ {
294
+ key: "needs-principal-input",
295
+ id: "vinaya/needs:principal-input",
296
+ category: "needs",
297
+ form: "literal",
298
+ carries: "Waiting on the Principal — a product-level call no agent may make."
299
+ },
300
+ {
301
+ key: "needs-brief-correction",
302
+ id: "vinaya/needs:brief-correction",
303
+ category: "needs",
304
+ form: "literal",
305
+ carries: "Waiting on the Brief Author — the brief contradicts the surface it describes."
306
+ },
307
+ {
308
+ key: "waiver-docs",
309
+ id: "vinaya/waiver:docs",
310
+ category: "waiver",
311
+ form: "literal",
312
+ carries: "Doc-coverage gate excused for this PR — honored only when a principal applied it."
313
+ },
314
+ {
315
+ key: "waiver-review",
316
+ id: "vinaya/waiver:review",
317
+ category: "waiver",
318
+ form: "literal",
319
+ carries: "Review gate excused for this PR — honored only when a principal applied it."
320
+ },
321
+ {
322
+ key: "override-docs",
323
+ id: "vinaya/override:docs",
324
+ category: "waiver",
325
+ form: "literal",
326
+ carries: "The whole verify-docs gate suppressed for this PR — a Principal-only blunt override, wider than a waiver."
327
+ },
328
+ {
329
+ key: "incoherent",
330
+ id: "vinaya/incoherent",
331
+ category: "flag",
332
+ form: "literal",
333
+ carries: "Closed COMPLETED with no merged-PR link — done-but-unprovable, surfaced for a human."
334
+ },
335
+ {
336
+ key: "direct-main-push",
337
+ id: "vinaya/direct-main-push",
338
+ category: "flag",
339
+ form: "literal",
340
+ carries: "A commit reached main with no associated merged PR — ring-2 detection, never a mutation."
341
+ },
342
+ {
343
+ key: "dead-branch-push",
344
+ id: "vinaya/dead-branch-push",
345
+ category: "flag",
346
+ form: "literal",
347
+ carries: "Commits landed on a branch after its PR had already resolved — daily-drift detection."
348
+ },
349
+ {
350
+ key: "state-object",
351
+ id: "vinaya/state-object",
352
+ category: "kind",
353
+ form: "literal",
354
+ carries: "A permanent forge-native storage object, never actionable work — excluded from every backlog."
355
+ }
356
+ ];
357
+ var BY_KEY = new Map(LABELS.map((l) => [l.key, l]));
358
+ function entry(key) {
359
+ const found = BY_KEY.get(key);
360
+ if (!found)
361
+ throw new Error(`labels.ts: no LABELS entry for key '${key}'`);
362
+ return found;
363
+ }
364
+ function label(key) {
365
+ return entry(key).id;
366
+ }
367
+ function trancheLabel(slug) {
368
+ return `${label("tranche")}${slug}`;
369
+ }
370
+ function matchesLabel(key, name) {
371
+ const l = entry(key);
372
+ if (l.form === "prefix")
373
+ return name.startsWith(l.id);
374
+ return name === l.id;
375
+ }
376
+ function hasLabel(key, names) {
377
+ return names.some((n) => matchesLabel(key, n));
378
+ }
379
+ function trancheSlugOf(name) {
380
+ const prefix = entry("tranche").id;
381
+ return name.startsWith(prefix) ? name.slice(prefix.length) : null;
382
+ }
383
+ function findTrancheSlug(names) {
384
+ for (const n of names) {
385
+ const slug = trancheSlugOf(n);
386
+ if (slug !== null)
387
+ return slug;
388
+ }
389
+ return null;
390
+ }
391
+
392
+ // ../../../packages/aeg-forge-state/src/list-tasks.ts
393
+ var TITLE_PATTERN = /^\[([^\]]+)]\s*(\S+)\s*—\s*(.+)$/;
394
+ var PROJECT_SLUG = /^[a-z0-9][a-z0-9-]*$/i;
395
+ var PROJECT_FIELD = /^\s*(?:\*\*)?Project(?:\(s\))?(?:\*\*)?\s*:\s*(?:\*\*)?\s*(.+)$/im;
396
+ function projectsFromBody(body) {
397
+ const m = body.match(PROJECT_FIELD);
398
+ if (!m)
399
+ return [];
400
+ return (m[1] ?? "").split(",").map((s) => s.trim()).filter((s) => PROJECT_SLUG.test(s));
401
+ }
402
+ function taskFromIssue(issue) {
403
+ const m = issue.title.match(TITLE_PATTERN);
404
+ if (!m)
405
+ return null;
406
+ const id = (m[2] ?? "").trim();
407
+ const title = (m[3] ?? "").trim();
408
+ if (!id || !title)
409
+ return null;
410
+ const body = issue.body ?? "";
411
+ const { dependsOn, conflictsWith } = parseRationaleDeps(body);
412
+ return {
413
+ id,
414
+ title,
415
+ issue: issue.number,
416
+ projects: projectsFromBody(body),
417
+ dependsOn,
418
+ conflictsWith,
419
+ rationaleMarkdown: body
420
+ };
421
+ }
422
+ function compareTaskIds(a, b) {
423
+ const numA = a.match(/^(\d+)(.*)$/);
424
+ const numB = b.match(/^(\d+)(.*)$/);
425
+ if (numA && numB) {
426
+ const diff = Number(numA[1]) - Number(numB[1]);
427
+ if (diff !== 0)
428
+ return diff;
429
+ return (numA[2] ?? "").localeCompare(numB[2] ?? "");
430
+ }
431
+ return a.localeCompare(b);
432
+ }
433
+ function tasksFromIssues(issues) {
434
+ const tasks = [];
435
+ for (const issue of issues) {
436
+ const task = taskFromIssue(issue);
437
+ if (task)
438
+ tasks.push(task);
439
+ }
440
+ return tasks.sort((a, b) => compareTaskIds(a.id, b.id));
441
+ }
442
+ async function listTasksForSlugAsync(owner, repo, slug) {
443
+ return tasksFromIssues(await ghIssueListByAnyLabelAsync(owner, repo, [trancheLabel(slug)]));
444
+ }
445
+ function resolveTaskIssueRef(title, labels) {
446
+ const m = title.match(TITLE_PATTERN);
447
+ if (!m)
448
+ return null;
449
+ const taskId = (m[2] ?? "").trim();
450
+ if (!taskId)
451
+ return null;
452
+ const trancheSlug = findTrancheSlug(labels);
453
+ if (!trancheSlug)
454
+ return null;
455
+ return { trancheSlug, taskId };
456
+ }
457
+
458
+ // ../../../packages/aeg-forge-state/src/derive-from-forge.ts
459
+ async function deriveTrancheFromForge(owner, repo, slug, known) {
460
+ const milestone = known ?? findMilestoneForSlug(owner, repo, slug);
461
+ const tasks = await listTasksForSlugAsync(owner, repo, slug);
462
+ return {
463
+ name: slug,
464
+ lifecycle: milestone?.lifecycle ?? "active",
465
+ goal: milestone?.goal ?? "",
466
+ tasks,
467
+ backlog: []
468
+ };
469
+ }
470
+ // ../../../node_modules/universal-user-agent/index.js
471
+ function getUserAgent() {
472
+ if (typeof navigator === "object" && "userAgent" in navigator) {
473
+ return navigator.userAgent;
474
+ }
475
+ if (typeof process === "object" && process.version !== undefined) {
476
+ return `Node.js/${process.version.substr(1)} (${process.platform}; ${process.arch})`;
477
+ }
478
+ return "<environment undetectable>";
479
+ }
480
+
481
+ // ../../../node_modules/@octokit/endpoint/dist-bundle/index.js
482
+ var VERSION = "0.0.0-development";
483
+ var userAgent = `octokit-endpoint.js/${VERSION} ${getUserAgent()}`;
484
+ var DEFAULTS = {
485
+ method: "GET",
486
+ baseUrl: "https://api.github.com",
487
+ headers: {
488
+ accept: "application/vnd.github.v3+json",
489
+ "user-agent": userAgent
490
+ },
491
+ mediaType: {
492
+ format: ""
493
+ }
494
+ };
495
+ function lowercaseKeys(object) {
496
+ if (!object) {
497
+ return {};
498
+ }
499
+ return Object.keys(object).reduce((newObj, key) => {
500
+ newObj[key.toLowerCase()] = object[key];
501
+ return newObj;
502
+ }, {});
503
+ }
504
+ function isPlainObject(value) {
505
+ if (typeof value !== "object" || value === null)
506
+ return false;
507
+ if (Object.prototype.toString.call(value) !== "[object Object]")
508
+ return false;
509
+ const proto = Object.getPrototypeOf(value);
510
+ if (proto === null)
511
+ return true;
512
+ const Ctor = Object.prototype.hasOwnProperty.call(proto, "constructor") && proto.constructor;
513
+ return typeof Ctor === "function" && Ctor instanceof Ctor && Function.prototype.call(Ctor) === Function.prototype.call(value);
514
+ }
515
+ function mergeDeep(defaults, options) {
516
+ const result = Object.assign({}, defaults);
517
+ Object.keys(options).forEach((key) => {
518
+ if (isPlainObject(options[key])) {
519
+ if (!(key in defaults))
520
+ Object.assign(result, { [key]: options[key] });
521
+ else
522
+ result[key] = mergeDeep(defaults[key], options[key]);
523
+ } else {
524
+ Object.assign(result, { [key]: options[key] });
525
+ }
526
+ });
527
+ return result;
528
+ }
529
+ function removeUndefinedProperties(obj) {
530
+ for (const key in obj) {
531
+ if (obj[key] === undefined) {
532
+ delete obj[key];
533
+ }
534
+ }
535
+ return obj;
536
+ }
537
+ function merge(defaults, route, options) {
538
+ if (typeof route === "string") {
539
+ let [method, url] = route.split(" ");
540
+ options = Object.assign(url ? { method, url } : { url: method }, options);
541
+ } else {
542
+ options = Object.assign({}, route);
543
+ }
544
+ options.headers = lowercaseKeys(options.headers);
545
+ removeUndefinedProperties(options);
546
+ removeUndefinedProperties(options.headers);
547
+ const mergedOptions = mergeDeep(defaults || {}, options);
548
+ if (options.url === "/graphql") {
549
+ if (defaults && defaults.mediaType.previews?.length) {
550
+ mergedOptions.mediaType.previews = defaults.mediaType.previews.filter((preview) => !mergedOptions.mediaType.previews.includes(preview)).concat(mergedOptions.mediaType.previews);
551
+ }
552
+ mergedOptions.mediaType.previews = (mergedOptions.mediaType.previews || []).map((preview) => preview.replace(/-preview/, ""));
553
+ }
554
+ return mergedOptions;
555
+ }
556
+ function addQueryParameters(url, parameters) {
557
+ const separator = /\?/.test(url) ? "&" : "?";
558
+ const names = Object.keys(parameters);
559
+ if (names.length === 0) {
560
+ return url;
561
+ }
562
+ return url + separator + names.map((name) => {
563
+ if (name === "q") {
564
+ return "q=" + parameters.q.split("+").map(encodeURIComponent).join("+");
565
+ }
566
+ return `${name}=${encodeURIComponent(parameters[name])}`;
567
+ }).join("&");
568
+ }
569
+ var urlVariableRegex = /\{[^{}}]+\}/g;
570
+ function removeNonChars(variableName) {
571
+ return variableName.replace(/(?:^\W+)|(?:(?<!\W)\W+$)/g, "").split(/,/);
572
+ }
573
+ function extractUrlVariableNames(url) {
574
+ const matches = url.match(urlVariableRegex);
575
+ if (!matches) {
576
+ return [];
577
+ }
578
+ return matches.map(removeNonChars).reduce((a, b) => a.concat(b), []);
579
+ }
580
+ function omit(object, keysToOmit) {
581
+ const result = { __proto__: null };
582
+ for (const key of Object.keys(object)) {
583
+ if (keysToOmit.indexOf(key) === -1) {
584
+ result[key] = object[key];
585
+ }
586
+ }
587
+ return result;
588
+ }
589
+ function encodeReserved(str) {
590
+ return str.split(/(%[0-9A-Fa-f]{2})/g).map(function(part) {
591
+ if (!/%[0-9A-Fa-f]/.test(part)) {
592
+ part = encodeURI(part).replace(/%5B/g, "[").replace(/%5D/g, "]");
593
+ }
594
+ return part;
595
+ }).join("");
596
+ }
597
+ function encodeUnreserved(str) {
598
+ return encodeURIComponent(str).replace(/[!'()*]/g, function(c) {
599
+ return "%" + c.charCodeAt(0).toString(16).toUpperCase();
600
+ });
601
+ }
602
+ function encodeValue(operator, value, key) {
603
+ value = operator === "+" || operator === "#" ? encodeReserved(value) : encodeUnreserved(value);
604
+ if (key) {
605
+ return encodeUnreserved(key) + "=" + value;
606
+ } else {
607
+ return value;
608
+ }
609
+ }
610
+ function isDefined(value) {
611
+ return value !== undefined && value !== null;
612
+ }
613
+ function isKeyOperator(operator) {
614
+ return operator === ";" || operator === "&" || operator === "?";
615
+ }
616
+ function getValues(context, operator, key, modifier) {
617
+ var value = context[key], result = [];
618
+ if (isDefined(value) && value !== "") {
619
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
620
+ value = value.toString();
621
+ if (modifier && modifier !== "*") {
622
+ value = value.substring(0, parseInt(modifier, 10));
623
+ }
624
+ result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : ""));
625
+ } else {
626
+ if (modifier === "*") {
627
+ if (Array.isArray(value)) {
628
+ value.filter(isDefined).forEach(function(value2) {
629
+ result.push(encodeValue(operator, value2, isKeyOperator(operator) ? key : ""));
630
+ });
631
+ } else {
632
+ Object.keys(value).forEach(function(k) {
633
+ if (isDefined(value[k])) {
634
+ result.push(encodeValue(operator, value[k], k));
635
+ }
636
+ });
637
+ }
638
+ } else {
639
+ const tmp = [];
640
+ if (Array.isArray(value)) {
641
+ value.filter(isDefined).forEach(function(value2) {
642
+ tmp.push(encodeValue(operator, value2));
643
+ });
644
+ } else {
645
+ Object.keys(value).forEach(function(k) {
646
+ if (isDefined(value[k])) {
647
+ tmp.push(encodeUnreserved(k));
648
+ tmp.push(encodeValue(operator, value[k].toString()));
649
+ }
650
+ });
651
+ }
652
+ if (isKeyOperator(operator)) {
653
+ result.push(encodeUnreserved(key) + "=" + tmp.join(","));
654
+ } else if (tmp.length !== 0) {
655
+ result.push(tmp.join(","));
656
+ }
657
+ }
658
+ }
659
+ } else {
660
+ if (operator === ";") {
661
+ if (isDefined(value)) {
662
+ result.push(encodeUnreserved(key));
663
+ }
664
+ } else if (value === "" && (operator === "&" || operator === "?")) {
665
+ result.push(encodeUnreserved(key) + "=");
666
+ } else if (value === "") {
667
+ result.push("");
668
+ }
669
+ }
670
+ return result;
671
+ }
672
+ function parseUrl(template) {
673
+ return {
674
+ expand: expand.bind(null, template)
675
+ };
676
+ }
677
+ function expand(template, context) {
678
+ var operators = ["+", "#", ".", "/", ";", "?", "&"];
679
+ template = template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, function(_, expression, literal) {
680
+ if (expression) {
681
+ let operator = "";
682
+ const values = [];
683
+ if (operators.indexOf(expression.charAt(0)) !== -1) {
684
+ operator = expression.charAt(0);
685
+ expression = expression.substr(1);
686
+ }
687
+ expression.split(/,/g).forEach(function(variable) {
688
+ var tmp = /([^:\*]*)(?::(\d+)|(\*))?/.exec(variable);
689
+ values.push(getValues(context, operator, tmp[1], tmp[2] || tmp[3]));
690
+ });
691
+ if (operator && operator !== "+") {
692
+ var separator = ",";
693
+ if (operator === "?") {
694
+ separator = "&";
695
+ } else if (operator !== "#") {
696
+ separator = operator;
697
+ }
698
+ return (values.length !== 0 ? operator : "") + values.join(separator);
699
+ } else {
700
+ return values.join(",");
701
+ }
702
+ } else {
703
+ return encodeReserved(literal);
704
+ }
705
+ });
706
+ if (template === "/") {
707
+ return template;
708
+ } else {
709
+ return template.replace(/\/$/, "");
710
+ }
711
+ }
712
+ function parse(options) {
713
+ let method = options.method.toUpperCase();
714
+ let url = (options.url || "/").replace(/:([a-z]\w+)/g, "{$1}");
715
+ let headers = Object.assign({}, options.headers);
716
+ let body;
717
+ let parameters = omit(options, [
718
+ "method",
719
+ "baseUrl",
720
+ "url",
721
+ "headers",
722
+ "request",
723
+ "mediaType"
724
+ ]);
725
+ const urlVariableNames = extractUrlVariableNames(url);
726
+ url = parseUrl(url).expand(parameters);
727
+ if (!/^http/.test(url)) {
728
+ url = options.baseUrl + url;
729
+ }
730
+ const omittedParameters = Object.keys(options).filter((option) => urlVariableNames.includes(option)).concat("baseUrl");
731
+ const remainingParameters = omit(parameters, omittedParameters);
732
+ const isBinaryRequest = /application\/octet-stream/i.test(headers.accept);
733
+ if (!isBinaryRequest) {
734
+ if (options.mediaType.format) {
735
+ headers.accept = headers.accept.split(/,/).map((format) => format.replace(/application\/vnd(\.\w+)(\.v3)?(\.\w+)?(\+json)?$/, `application/vnd$1$2.${options.mediaType.format}`)).join(",");
736
+ }
737
+ if (url.endsWith("/graphql")) {
738
+ if (options.mediaType.previews?.length) {
739
+ const previewsFromAcceptHeader = headers.accept.match(/(?<![\w-])[\w-]+(?=-preview)/g) || [];
740
+ headers.accept = previewsFromAcceptHeader.concat(options.mediaType.previews).map((preview) => {
741
+ const format = options.mediaType.format ? `.${options.mediaType.format}` : "+json";
742
+ return `application/vnd.github.${preview}-preview${format}`;
743
+ }).join(",");
744
+ }
745
+ }
746
+ }
747
+ if (["GET", "HEAD"].includes(method)) {
748
+ url = addQueryParameters(url, remainingParameters);
749
+ } else {
750
+ if ("data" in remainingParameters) {
751
+ body = remainingParameters.data;
752
+ } else {
753
+ if (Object.keys(remainingParameters).length) {
754
+ body = remainingParameters;
755
+ }
756
+ }
757
+ }
758
+ if (!headers["content-type"] && typeof body !== "undefined") {
759
+ headers["content-type"] = "application/json; charset=utf-8";
760
+ }
761
+ if (["PATCH", "PUT"].includes(method) && typeof body === "undefined") {
762
+ body = "";
763
+ }
764
+ return Object.assign({ method, url, headers }, typeof body !== "undefined" ? { body } : null, options.request ? { request: options.request } : null);
765
+ }
766
+ function endpointWithDefaults(defaults, route, options) {
767
+ return parse(merge(defaults, route, options));
768
+ }
769
+ function withDefaults(oldDefaults, newDefaults) {
770
+ const DEFAULTS2 = merge(oldDefaults, newDefaults);
771
+ const endpoint2 = endpointWithDefaults.bind(null, DEFAULTS2);
772
+ return Object.assign(endpoint2, {
773
+ DEFAULTS: DEFAULTS2,
774
+ defaults: withDefaults.bind(null, DEFAULTS2),
775
+ merge: merge.bind(null, DEFAULTS2),
776
+ parse
777
+ });
778
+ }
779
+ var endpoint = withDefaults(null, DEFAULTS);
780
+
781
+ // ../../../node_modules/fast-content-type-parse/index.js
782
+ var NullObject = function NullObject2() {};
783
+ NullObject.prototype = Object.create(null);
784
+ var paramRE = /; *([!#$%&'*+.^\w`|~-]+)=("(?:[\v\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\v\u0020-\u00ff])*"|[!#$%&'*+.^\w`|~-]+) */gu;
785
+ var quotedPairRE = /\\([\v\u0020-\u00ff])/gu;
786
+ var mediaTypeRE = /^[!#$%&'*+.^\w|~-]+\/[!#$%&'*+.^\w|~-]+$/u;
787
+ var defaultContentType = { type: "", parameters: new NullObject };
788
+ Object.freeze(defaultContentType.parameters);
789
+ Object.freeze(defaultContentType);
790
+ function safeParse(header) {
791
+ if (typeof header !== "string") {
792
+ return defaultContentType;
793
+ }
794
+ let index = header.indexOf(";");
795
+ const type = index !== -1 ? header.slice(0, index).trim() : header.trim();
796
+ if (mediaTypeRE.test(type) === false) {
797
+ return defaultContentType;
798
+ }
799
+ const result = {
800
+ type: type.toLowerCase(),
801
+ parameters: new NullObject
802
+ };
803
+ if (index === -1) {
804
+ return result;
805
+ }
806
+ let key;
807
+ let match;
808
+ let value;
809
+ paramRE.lastIndex = index;
810
+ while (match = paramRE.exec(header)) {
811
+ if (match.index !== index) {
812
+ return defaultContentType;
813
+ }
814
+ index += match[0].length;
815
+ key = match[1].toLowerCase();
816
+ value = match[2];
817
+ if (value[0] === '"') {
818
+ value = value.slice(1, value.length - 1);
819
+ quotedPairRE.test(value) && (value = value.replace(quotedPairRE, "$1"));
820
+ }
821
+ result.parameters[key] = value;
822
+ }
823
+ if (index !== header.length) {
824
+ return defaultContentType;
825
+ }
826
+ return result;
827
+ }
828
+ var $safeParse = safeParse;
829
+
830
+ // ../../../node_modules/@octokit/request-error/dist-src/index.js
831
+ class RequestError extends Error {
832
+ name;
833
+ status;
834
+ request;
835
+ response;
836
+ constructor(message, statusCode, options) {
837
+ super(message);
838
+ this.name = "HttpError";
839
+ this.status = Number.parseInt(statusCode);
840
+ if (Number.isNaN(this.status)) {
841
+ this.status = 0;
842
+ }
843
+ if ("response" in options) {
844
+ this.response = options.response;
845
+ }
846
+ const requestCopy = Object.assign({}, options.request);
847
+ if (options.request.headers.authorization) {
848
+ requestCopy.headers = Object.assign({}, options.request.headers, {
849
+ authorization: options.request.headers.authorization.replace(/(?<! ) .*$/, " [REDACTED]")
850
+ });
851
+ }
852
+ requestCopy.url = requestCopy.url.replace(/\bclient_secret=\w+/g, "client_secret=[REDACTED]").replace(/\baccess_token=\w+/g, "access_token=[REDACTED]");
853
+ this.request = requestCopy;
854
+ }
855
+ }
856
+
857
+ // ../../../node_modules/@octokit/request/dist-bundle/index.js
858
+ var VERSION2 = "9.2.4";
859
+ var defaults_default = {
860
+ headers: {
861
+ "user-agent": `octokit-request.js/${VERSION2} ${getUserAgent()}`
862
+ }
863
+ };
864
+ function isPlainObject2(value) {
865
+ if (typeof value !== "object" || value === null)
866
+ return false;
867
+ if (Object.prototype.toString.call(value) !== "[object Object]")
868
+ return false;
869
+ const proto = Object.getPrototypeOf(value);
870
+ if (proto === null)
871
+ return true;
872
+ const Ctor = Object.prototype.hasOwnProperty.call(proto, "constructor") && proto.constructor;
873
+ return typeof Ctor === "function" && Ctor instanceof Ctor && Function.prototype.call(Ctor) === Function.prototype.call(value);
874
+ }
875
+ async function fetchWrapper(requestOptions) {
876
+ const fetch = requestOptions.request?.fetch || globalThis.fetch;
877
+ if (!fetch) {
878
+ throw new Error("fetch is not set. Please pass a fetch implementation as new Octokit({ request: { fetch }}). Learn more at https://github.com/octokit/octokit.js/#fetch-missing");
879
+ }
880
+ const log = requestOptions.request?.log || console;
881
+ const parseSuccessResponseBody = requestOptions.request?.parseSuccessResponseBody !== false;
882
+ const body = isPlainObject2(requestOptions.body) || Array.isArray(requestOptions.body) ? JSON.stringify(requestOptions.body) : requestOptions.body;
883
+ const requestHeaders = Object.fromEntries(Object.entries(requestOptions.headers).map(([name, value]) => [
884
+ name,
885
+ String(value)
886
+ ]));
887
+ let fetchResponse;
888
+ try {
889
+ fetchResponse = await fetch(requestOptions.url, {
890
+ method: requestOptions.method,
891
+ body,
892
+ redirect: requestOptions.request?.redirect,
893
+ headers: requestHeaders,
894
+ signal: requestOptions.request?.signal,
895
+ ...requestOptions.body && { duplex: "half" }
896
+ });
897
+ } catch (error) {
898
+ let message = "Unknown Error";
899
+ if (error instanceof Error) {
900
+ if (error.name === "AbortError") {
901
+ error.status = 500;
902
+ throw error;
903
+ }
904
+ message = error.message;
905
+ if (error.name === "TypeError" && "cause" in error) {
906
+ if (error.cause instanceof Error) {
907
+ message = error.cause.message;
908
+ } else if (typeof error.cause === "string") {
909
+ message = error.cause;
910
+ }
911
+ }
912
+ }
913
+ const requestError = new RequestError(message, 500, {
914
+ request: requestOptions
915
+ });
916
+ requestError.cause = error;
917
+ throw requestError;
918
+ }
919
+ const status = fetchResponse.status;
920
+ const url = fetchResponse.url;
921
+ const responseHeaders = {};
922
+ for (const [key, value] of fetchResponse.headers) {
923
+ responseHeaders[key] = value;
924
+ }
925
+ const octokitResponse = {
926
+ url,
927
+ status,
928
+ headers: responseHeaders,
929
+ data: ""
930
+ };
931
+ if ("deprecation" in responseHeaders) {
932
+ const matches = responseHeaders.link && responseHeaders.link.match(/<([^<>]+)>; rel="deprecation"/);
933
+ const deprecationLink = matches && matches.pop();
934
+ log.warn(`[@octokit/request] "${requestOptions.method} ${requestOptions.url}" is deprecated. It is scheduled to be removed on ${responseHeaders.sunset}${deprecationLink ? `. See ${deprecationLink}` : ""}`);
935
+ }
936
+ if (status === 204 || status === 205) {
937
+ return octokitResponse;
938
+ }
939
+ if (requestOptions.method === "HEAD") {
940
+ if (status < 400) {
941
+ return octokitResponse;
942
+ }
943
+ throw new RequestError(fetchResponse.statusText, status, {
944
+ response: octokitResponse,
945
+ request: requestOptions
946
+ });
947
+ }
948
+ if (status === 304) {
949
+ octokitResponse.data = await getResponseData(fetchResponse);
950
+ throw new RequestError("Not modified", status, {
951
+ response: octokitResponse,
952
+ request: requestOptions
953
+ });
954
+ }
955
+ if (status >= 400) {
956
+ octokitResponse.data = await getResponseData(fetchResponse);
957
+ throw new RequestError(toErrorMessage(octokitResponse.data), status, {
958
+ response: octokitResponse,
959
+ request: requestOptions
960
+ });
961
+ }
962
+ octokitResponse.data = parseSuccessResponseBody ? await getResponseData(fetchResponse) : fetchResponse.body;
963
+ return octokitResponse;
964
+ }
965
+ async function getResponseData(response) {
966
+ const contentType = response.headers.get("content-type");
967
+ if (!contentType) {
968
+ return response.text().catch(() => "");
969
+ }
970
+ const mimetype = $safeParse(contentType);
971
+ if (isJSONResponse(mimetype)) {
972
+ let text = "";
973
+ try {
974
+ text = await response.text();
975
+ return JSON.parse(text);
976
+ } catch (err) {
977
+ return text;
978
+ }
979
+ } else if (mimetype.type.startsWith("text/") || mimetype.parameters.charset?.toLowerCase() === "utf-8") {
980
+ return response.text().catch(() => "");
981
+ } else {
982
+ return response.arrayBuffer().catch(() => new ArrayBuffer(0));
983
+ }
984
+ }
985
+ function isJSONResponse(mimetype) {
986
+ return mimetype.type === "application/json" || mimetype.type === "application/scim+json";
987
+ }
988
+ function toErrorMessage(data) {
989
+ if (typeof data === "string") {
990
+ return data;
991
+ }
992
+ if (data instanceof ArrayBuffer) {
993
+ return "Unknown error";
994
+ }
995
+ if ("message" in data) {
996
+ const suffix = "documentation_url" in data ? ` - ${data.documentation_url}` : "";
997
+ return Array.isArray(data.errors) ? `${data.message}: ${data.errors.map((v) => JSON.stringify(v)).join(", ")}${suffix}` : `${data.message}${suffix}`;
998
+ }
999
+ return `Unknown error: ${JSON.stringify(data)}`;
1000
+ }
1001
+ function withDefaults2(oldEndpoint, newDefaults) {
1002
+ const endpoint2 = oldEndpoint.defaults(newDefaults);
1003
+ const newApi = function(route, parameters) {
1004
+ const endpointOptions = endpoint2.merge(route, parameters);
1005
+ if (!endpointOptions.request || !endpointOptions.request.hook) {
1006
+ return fetchWrapper(endpoint2.parse(endpointOptions));
1007
+ }
1008
+ const request2 = (route2, parameters2) => {
1009
+ return fetchWrapper(endpoint2.parse(endpoint2.merge(route2, parameters2)));
1010
+ };
1011
+ Object.assign(request2, {
1012
+ endpoint: endpoint2,
1013
+ defaults: withDefaults2.bind(null, endpoint2)
1014
+ });
1015
+ return endpointOptions.request.hook(request2, endpointOptions);
1016
+ };
1017
+ return Object.assign(newApi, {
1018
+ endpoint: endpoint2,
1019
+ defaults: withDefaults2.bind(null, endpoint2)
1020
+ });
1021
+ }
1022
+ var request = withDefaults2(endpoint, defaults_default);
1023
+
1024
+ // ../../../node_modules/@octokit/graphql/dist-bundle/index.js
1025
+ var VERSION3 = "0.0.0-development";
1026
+ function _buildMessageForResponseErrors(data) {
1027
+ return `Request failed due to following response errors:
1028
+ ` + data.errors.map((e) => ` - ${e.message}`).join(`
1029
+ `);
1030
+ }
1031
+ var GraphqlResponseError = class extends Error {
1032
+ constructor(request2, headers, response) {
1033
+ super(_buildMessageForResponseErrors(response));
1034
+ this.request = request2;
1035
+ this.headers = headers;
1036
+ this.response = response;
1037
+ this.errors = response.errors;
1038
+ this.data = response.data;
1039
+ if (Error.captureStackTrace) {
1040
+ Error.captureStackTrace(this, this.constructor);
1041
+ }
1042
+ }
1043
+ name = "GraphqlResponseError";
1044
+ errors;
1045
+ data;
1046
+ };
1047
+ var NON_VARIABLE_OPTIONS = [
1048
+ "method",
1049
+ "baseUrl",
1050
+ "url",
1051
+ "headers",
1052
+ "request",
1053
+ "query",
1054
+ "mediaType",
1055
+ "operationName"
1056
+ ];
1057
+ var FORBIDDEN_VARIABLE_OPTIONS = ["query", "method", "url"];
1058
+ var GHES_V3_SUFFIX_REGEX = /\/api\/v3\/?$/;
1059
+ function graphql(request2, query, options) {
1060
+ if (options) {
1061
+ if (typeof query === "string" && "query" in options) {
1062
+ return Promise.reject(new Error(`[@octokit/graphql] "query" cannot be used as variable name`));
1063
+ }
1064
+ for (const key in options) {
1065
+ if (!FORBIDDEN_VARIABLE_OPTIONS.includes(key))
1066
+ continue;
1067
+ return Promise.reject(new Error(`[@octokit/graphql] "${key}" cannot be used as variable name`));
1068
+ }
1069
+ }
1070
+ const parsedOptions = typeof query === "string" ? Object.assign({ query }, options) : query;
1071
+ const requestOptions = Object.keys(parsedOptions).reduce((result, key) => {
1072
+ if (NON_VARIABLE_OPTIONS.includes(key)) {
1073
+ result[key] = parsedOptions[key];
1074
+ return result;
1075
+ }
1076
+ if (!result.variables) {
1077
+ result.variables = {};
1078
+ }
1079
+ result.variables[key] = parsedOptions[key];
1080
+ return result;
1081
+ }, {});
1082
+ const baseUrl = parsedOptions.baseUrl || request2.endpoint.DEFAULTS.baseUrl;
1083
+ if (GHES_V3_SUFFIX_REGEX.test(baseUrl)) {
1084
+ requestOptions.url = baseUrl.replace(GHES_V3_SUFFIX_REGEX, "/api/graphql");
1085
+ }
1086
+ return request2(requestOptions).then((response) => {
1087
+ if (response.data.errors) {
1088
+ const headers = {};
1089
+ for (const key of Object.keys(response.headers)) {
1090
+ headers[key] = response.headers[key];
1091
+ }
1092
+ throw new GraphqlResponseError(requestOptions, headers, response.data);
1093
+ }
1094
+ return response.data.data;
1095
+ });
1096
+ }
1097
+ function withDefaults3(request2, newDefaults) {
1098
+ const newRequest = request2.defaults(newDefaults);
1099
+ const newApi = (query, options) => {
1100
+ return graphql(newRequest, query, options);
1101
+ };
1102
+ return Object.assign(newApi, {
1103
+ defaults: withDefaults3.bind(null, newRequest),
1104
+ endpoint: newRequest.endpoint
1105
+ });
1106
+ }
1107
+ var graphql2 = withDefaults3(request, {
1108
+ headers: {
1109
+ "user-agent": `octokit-graphql.js/${VERSION3} ${getUserAgent()}`
1110
+ },
1111
+ method: "POST",
1112
+ url: "/graphql"
1113
+ });
1114
+ // ../../../packages/aeg-forge-state/src/github-token.ts
1115
+ import { execFile as execFile2 } from "node:child_process";
1116
+ import { promisify as promisify2 } from "node:util";
1117
+ var execFileAsync2 = promisify2(execFile2);
1118
+ var systemEnv2 = {
1119
+ ...process.env,
1120
+ PATH: [process.env.PATH, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].filter(Boolean).join(":")
1121
+ };
1122
+ async function resolveGithubToken(explicit) {
1123
+ if (explicit && explicit.length > 0)
1124
+ return explicit;
1125
+ if (process.env.GITHUB_TOKEN)
1126
+ return process.env.GITHUB_TOKEN;
1127
+ if (process.env.GH_TOKEN)
1128
+ return process.env.GH_TOKEN;
1129
+ try {
1130
+ const { stdout } = await execFileAsync2("gh", ["auth", "token"], { env: systemEnv2, timeout: 5000 });
1131
+ const token = stdout.trim();
1132
+ return token.length > 0 ? token : null;
1133
+ } catch {
1134
+ return null;
1135
+ }
1136
+ }
1137
+
1138
+ // ../../../packages/aeg-forge-state/src/map-forge-facts.ts
1139
+ function mapForgeFacts(raw) {
1140
+ if (!raw.issue)
1141
+ return null;
1142
+ return {
1143
+ issueState: raw.issue.state === "OPEN" ? "open" : "closed",
1144
+ assigned: raw.issue.assigneesCount > 0,
1145
+ blockedLabel: hasLabel("blocked", raw.issue.labels),
1146
+ branchExists: raw.refExists,
1147
+ prState: mapPrState(raw.pullRequest?.state),
1148
+ reviewDecision: mapReviewDecision(raw.pullRequest?.reviewDecision),
1149
+ stateReason: mapStateReason(raw.issue.stateReason),
1150
+ closedAt: raw.issue.closedAt ?? null,
1151
+ mergedAt: raw.pullRequest?.mergedAt ?? null
1152
+ };
1153
+ }
1154
+ function mapStateReason(reason) {
1155
+ if (reason === "COMPLETED")
1156
+ return "completed";
1157
+ if (reason === "NOT_PLANNED")
1158
+ return "not_planned";
1159
+ return null;
1160
+ }
1161
+ function mapPrState(state) {
1162
+ if (state === "OPEN")
1163
+ return "open";
1164
+ if (state === "MERGED")
1165
+ return "merged";
1166
+ return "none";
1167
+ }
1168
+ function mapReviewDecision(decision) {
1169
+ if (decision === "APPROVED")
1170
+ return "approved";
1171
+ if (decision === "CHANGES_REQUESTED")
1172
+ return "changes_requested";
1173
+ return "none";
1174
+ }
1175
+
1176
+ // ../../../packages/aeg-forge-state/src/fetch-forge-facts.ts
1177
+ function buildBranchName(tranche, taskId) {
1178
+ return `task/${tranche}/${taskId}`;
1179
+ }
1180
+ async function fetchForgeFacts(input) {
1181
+ const token = await resolveGithubToken(input.token);
1182
+ if (!token) {
1183
+ return {
1184
+ facts: new Map,
1185
+ prRefs: new Map,
1186
+ unavailable: true,
1187
+ reason: "No GitHub token available (set GITHUB_TOKEN/GH_TOKEN or `gh auth login`)."
1188
+ };
1189
+ }
1190
+ const queriedTasks = input.tasks.filter((t) => t.issue !== null);
1191
+ if (queriedTasks.length === 0) {
1192
+ return { facts: new Map, prRefs: new Map, unavailable: false };
1193
+ }
1194
+ const client = graphql2.defaults({ headers: { authorization: `bearer ${token}` } });
1195
+ const query = buildBatchQuery(input.tranche, queriedTasks);
1196
+ let response;
1197
+ try {
1198
+ response = await client(query, { owner: input.owner, repo: input.repo });
1199
+ } catch (err) {
1200
+ return {
1201
+ facts: new Map,
1202
+ prRefs: new Map,
1203
+ unavailable: true,
1204
+ reason: `GitHub query failed: ${describeError(err)}`
1205
+ };
1206
+ }
1207
+ const facts = new Map;
1208
+ const prRefs = new Map;
1209
+ if (!response.repository) {
1210
+ return {
1211
+ facts,
1212
+ prRefs,
1213
+ unavailable: true,
1214
+ reason: `Repository ${input.owner}/${input.repo} not visible to this token.`
1215
+ };
1216
+ }
1217
+ for (const task of queriedTasks) {
1218
+ const alias = aliasFor(task.id);
1219
+ const raw = extractRawFromResponse(response.repository, alias);
1220
+ const mapped = mapForgeFacts(raw);
1221
+ if (mapped)
1222
+ facts.set(task.id, mapped);
1223
+ const pr = raw.pullRequest;
1224
+ if (pr && typeof pr.number === "number" && typeof pr.url === "string") {
1225
+ prRefs.set(task.id, { number: pr.number, url: pr.url, state: pr.state });
1226
+ }
1227
+ }
1228
+ return { facts, prRefs, unavailable: false };
1229
+ }
1230
+ function buildBatchQuery(tranche, tasks) {
1231
+ const perTask = tasks.map((task) => {
1232
+ const a = aliasFor(task.id);
1233
+ const branch = buildBranchName(tranche, task.id);
1234
+ return `
1235
+ ${a}_issue: issue(number: ${task.issue}) {
1236
+ state
1237
+ stateReason
1238
+ closedAt
1239
+ assignees(first: 1) { totalCount }
1240
+ labels(first: 50) { nodes { name } }
1241
+ timelineItems(last: 1, itemTypes: [CLOSED_EVENT]) {
1242
+ nodes {
1243
+ ... on ClosedEvent {
1244
+ closer {
1245
+ ... on PullRequest {
1246
+ number
1247
+ url
1248
+ state
1249
+ reviewDecision
1250
+ mergedAt
1251
+ }
1252
+ }
1253
+ }
1254
+ }
1255
+ }
1256
+ }
1257
+ ${a}_ref: ref(qualifiedName: ${JSON.stringify(`refs/heads/${branch}`)}) {
1258
+ name
1259
+ }
1260
+ ${a}_prs: pullRequests(
1261
+ first: 1,
1262
+ headRefName: ${JSON.stringify(branch)},
1263
+ orderBy: { field: CREATED_AT, direction: DESC }
1264
+ ) {
1265
+ nodes { number url state reviewDecision mergedAt }
1266
+ }`;
1267
+ }).join("");
1268
+ return `query ForgeFacts($owner: String!, $repo: String!) {
1269
+ repository(owner: $owner, name: $repo) {${perTask}
1270
+ }
1271
+ }`;
1272
+ }
1273
+ function aliasFor(taskId) {
1274
+ const sanitized = taskId.replace(/[^a-zA-Z0-9_]/g, "_");
1275
+ return `t_${sanitized}`;
1276
+ }
1277
+ function extractRawFromResponse(repository, alias) {
1278
+ const issue = repository[`${alias}_issue`];
1279
+ const ref = repository[`${alias}_ref`];
1280
+ const prs = repository[`${alias}_prs`];
1281
+ const rawCloser = issue?.timelineItems?.nodes?.[0]?.closer ?? null;
1282
+ const closingPr = rawCloser && typeof rawCloser.number === "number" && typeof rawCloser.url === "string" ? rawCloser : null;
1283
+ const branchPr = prs && prs.nodes.length > 0 && prs.nodes[0] ? prs.nodes[0] : null;
1284
+ return {
1285
+ issue: issue ? {
1286
+ state: issue.state,
1287
+ stateReason: issue.stateReason,
1288
+ closedAt: issue.closedAt ?? null,
1289
+ assigneesCount: issue.assignees.totalCount,
1290
+ labels: issue.labels.nodes.map((n) => n.name)
1291
+ } : null,
1292
+ refExists: Boolean(ref && ref.name.length > 0),
1293
+ pullRequest: closingPr ?? branchPr
1294
+ };
1295
+ }
1296
+ function describeError(err) {
1297
+ if (err instanceof Error)
1298
+ return err.message;
1299
+ if (typeof err === "string")
1300
+ return err;
1301
+ return "unknown error";
1302
+ }
1303
+ // ../../../packages/aeg-forge-state/src/fetch-open-issues.ts
1304
+ async function fetchOpenIssuesByLabel(slugs, owner, repo, token) {
1305
+ const result = new Map;
1306
+ if (slugs.length === 0)
1307
+ return result;
1308
+ const client = graphql2.defaults({ headers: { authorization: `bearer ${token}` } });
1309
+ const toAlias = (slug) => `tranche_${slug.replace(/-/g, "_")}`;
1310
+ const perSlug = slugs.map((slug) => `
1311
+ ${toAlias(slug)}: issues(states: [OPEN], labels: [${JSON.stringify(trancheLabel(slug))}], first: 100) {
1312
+ nodes { number body labels(first: 20) { nodes { name } } }
1313
+ }`).join("");
1314
+ const query = `query LabeledIssues($owner: String!, $repo: String!) {
1315
+ repository(owner: $owner, name: $repo) {${perSlug}
1316
+ }
1317
+ }`;
1318
+ let response;
1319
+ try {
1320
+ response = await client(query, { owner, repo });
1321
+ } catch {
1322
+ return result;
1323
+ }
1324
+ if (!response.repository)
1325
+ return result;
1326
+ for (const slug of slugs) {
1327
+ const conn = response.repository[toAlias(slug)];
1328
+ const issues = (conn?.nodes ?? []).map((n) => ({
1329
+ number: n.number,
1330
+ body: n.body ?? "",
1331
+ labels: n.labels?.nodes?.map((l) => l.name) ?? []
1332
+ }));
1333
+ result.set(slug, issues);
1334
+ }
1335
+ return result;
1336
+ }
1337
+ // ../../../packages/aeg-forge-state/src/fetch-task-issue-refs.ts
1338
+ async function fetchTaskIssueRefs(owner, repo, issueNumbers, token) {
1339
+ const result = new Map;
1340
+ if (issueNumbers.length === 0)
1341
+ return result;
1342
+ const resolvedToken = await resolveGithubToken(token);
1343
+ if (!resolvedToken)
1344
+ return result;
1345
+ const client = graphql2.defaults({ headers: { authorization: `bearer ${resolvedToken}` } });
1346
+ let response;
1347
+ try {
1348
+ response = await client(buildQuery(issueNumbers), { owner, repo });
1349
+ } catch {
1350
+ return result;
1351
+ }
1352
+ if (!response.repository)
1353
+ return result;
1354
+ for (const n of issueNumbers) {
1355
+ const node = response.repository[aliasFor2(n)];
1356
+ if (!node)
1357
+ continue;
1358
+ const labels = node.labels.nodes.map((l) => l.name);
1359
+ result.set(n, resolveTaskIssueRef(node.title, labels));
1360
+ }
1361
+ return result;
1362
+ }
1363
+ function aliasFor2(n) {
1364
+ return `i_${n}`;
1365
+ }
1366
+ function buildQuery(issueNumbers) {
1367
+ const perIssue = issueNumbers.map((n) => `
1368
+ ${aliasFor2(n)}: issue(number: ${n}) {
1369
+ title
1370
+ labels(first: 50) { nodes { name } }
1371
+ }`).join("");
1372
+ return `query TaskIssueRefs($owner: String!, $repo: String!) {
1373
+ repository(owner: $owner, name: $repo) {${perIssue}
1374
+ }
1375
+ }`;
1376
+ }
1377
+ // ../../../packages/aeg-forge-state/src/resolve-repo.ts
1378
+ import { execFile as execFile3 } from "node:child_process";
1379
+ import { promisify as promisify3 } from "node:util";
1380
+ var execFileAsync3 = promisify3(execFile3);
1381
+ var systemEnv3 = {
1382
+ ...process.env,
1383
+ PATH: [process.env.PATH, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].filter(Boolean).join(":")
1384
+ };
1385
+ // ../../../packages/aeg-core/src/state-machine-model.ts
1386
+ var FORGE_FACT_INPUTS = [
1387
+ {
1388
+ fact: "issueState",
1389
+ readsFrom: "Issue.state (OPEN | CLOSED)",
1390
+ meaning: "Whether the task Issue is still open — the anchor for the honest terminal statuses."
1391
+ },
1392
+ {
1393
+ fact: "assigned",
1394
+ readsFrom: "Issue.assignees (count > 0)",
1395
+ meaning: "An assignee exists. No longer affects derivation: assigned and unassigned are both todo."
1396
+ },
1397
+ {
1398
+ fact: "branchExists",
1399
+ readsFrom: "Ref refs/heads/task/<tranche>/<id>",
1400
+ meaning: "A task branch has been published — the todo → in-flight transition, written by git push, not by hand."
1401
+ },
1402
+ {
1403
+ fact: "prState",
1404
+ readsFrom: "PullRequest.state (OPEN | CLOSED | MERGED)",
1405
+ meaning: "Whether work is proposed or landed. CLOSED-without-merge collapses to none — AEG models open/merged/none."
1406
+ },
1407
+ {
1408
+ fact: "reviewDecision",
1409
+ readsFrom: "PullRequest.reviewDecision",
1410
+ meaning: "Only 'changes_requested' flips status; approved-but-unmerged stays in-review."
1411
+ },
1412
+ {
1413
+ fact: "blockedLabel",
1414
+ readsFrom: `Issue.labels contains '${label("blocked")}'`,
1415
+ meaning: "Execution halted pending an external unblock. Wins over every other rule."
1416
+ },
1417
+ {
1418
+ fact: "stateReason",
1419
+ readsFrom: "Issue.stateReason (COMPLETED | NOT_PLANNED | REOPENED | null)",
1420
+ meaning: "Separates a legitimate drop from an incoherent close on a closed, never-merged Issue."
1421
+ },
1422
+ {
1423
+ fact: "closedAt",
1424
+ readsFrom: "Issue.closedAt",
1425
+ meaning: "Timestamp for the coherence oracle grandfather cutoff. No derivation rule reads it."
1426
+ },
1427
+ {
1428
+ fact: "mergedAt",
1429
+ readsFrom: "PullRequest.mergedAt",
1430
+ meaning: "Timestamp for the coherence oracle grandfather cutoff. No derivation rule reads it."
1431
+ }
1432
+ ];
1433
+ var DERIVED_STATUSES = [
1434
+ "backlog",
1435
+ "todo",
1436
+ "in-flight",
1437
+ "in-review",
1438
+ "changes-requested",
1439
+ "merged",
1440
+ "blocked",
1441
+ "dropped",
1442
+ "incoherent"
1443
+ ];
1444
+ var DERIVABLE_STATUSES = DERIVED_STATUSES.filter((s) => s !== "backlog");
1445
+ // ../../../packages/aeg-core/src/file-classify.ts
1446
+ function isCodeFile(p) {
1447
+ if (p.endsWith(".md"))
1448
+ return false;
1449
+ return /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|sql|css)$/.test(p);
1450
+ }
1451
+ // ../../../packages/aeg-core/src/waiver-label.ts
1452
+ var WAIVER_LABEL = label("waiver-docs");
1453
+ var WAIVER_LABEL_REVIEW = label("waiver-review");
1454
+ var PRINCIPAL_ALLOWLIST = ["daniboomerang"];
1455
+ function isWaiverLabelActorVerified(opts) {
1456
+ if (!opts.labels.includes(opts.label))
1457
+ return false;
1458
+ if (opts.labelActor === null)
1459
+ return false;
1460
+ return opts.principalAllowlist.includes(opts.labelActor);
1461
+ }
1462
+
1463
+ // ../../../packages/aeg-core/src/doc-owners.ts
1464
+ var DOC_OWNERS_PATH = ".vinaya/doc-owners";
1465
+ function parseDocOwners(content) {
1466
+ const bindings = [];
1467
+ const errors = [];
1468
+ const lines = content.split(`
1469
+ `);
1470
+ for (let i = 0;i < lines.length; i++) {
1471
+ const raw = lines[i] ?? "";
1472
+ const stripped = raw.replace(/#.*$/, "").trim();
1473
+ if (!stripped)
1474
+ continue;
1475
+ const parts = stripped.split(/\s+/);
1476
+ if (parts.length < 2) {
1477
+ errors.push(`C5 doc-owners-parse: ${DOC_OWNERS_PATH}:${i + 1} — malformed binding (expected "<glob> <pointer>", got "${raw.trim()}").`);
1478
+ continue;
1479
+ }
1480
+ const [glob, ...pointerParts] = parts;
1481
+ bindings.push({ glob: glob ?? "", pointer: pointerParts.join(" "), lineNum: i + 1 });
1482
+ }
1483
+ return { bindings, errors };
1484
+ }
1485
+ function globToRegex(pat) {
1486
+ let re = "^";
1487
+ let i = 0;
1488
+ while (i < pat.length) {
1489
+ const c = pat[i];
1490
+ if (c === "*") {
1491
+ if (pat[i + 1] === "*") {
1492
+ re += ".*";
1493
+ i += 2;
1494
+ } else {
1495
+ re += "[^/]*";
1496
+ i += 1;
1497
+ }
1498
+ } else if (/[.+^$|(){}[\]\\?]/.test(c)) {
1499
+ re += `\\${c}`;
1500
+ i += 1;
1501
+ } else {
1502
+ re += c;
1503
+ i += 1;
1504
+ }
1505
+ }
1506
+ re += "$";
1507
+ return new RegExp(re);
1508
+ }
1509
+ function isUrlPointer(p) {
1510
+ return /^https?:\/\//.test(p);
1511
+ }
1512
+ function pointerToPath(p) {
1513
+ const idx = p.indexOf("#");
1514
+ return idx === -1 ? p : p.slice(0, idx);
1515
+ }
1516
+ var SEPARATOR = /(?:[ \t]*[—–][ \t]*|[ \t]+-[ \t]+)/.source;
1517
+ function readDocAcks(body) {
1518
+ const acks = [];
1519
+ const re = new RegExp(`^[ \\t]*Doc-ack[ \\t]*:[ \\t]*(.+?)${SEPARATOR}(.+?)[ \\t]*$`, "gim");
1520
+ for (const m of body.matchAll(re)) {
1521
+ acks.push({ surface: (m[1] ?? "").trim(), note: (m[2] ?? "").trim() });
1522
+ }
1523
+ return acks;
1524
+ }
1525
+ function evaluateC5(changed, docOwnersContent, prBody, fileExists, waiverActive) {
1526
+ const out = { errors: [], notes: [] };
1527
+ if (docOwnersContent === null)
1528
+ return out;
1529
+ const { bindings, errors: parseErrors } = parseDocOwners(docOwnersContent);
1530
+ for (const e of parseErrors)
1531
+ out.errors.push(e);
1532
+ if (bindings.length === 0)
1533
+ return out;
1534
+ const codeFiles = changed.filter(isCodeFile);
1535
+ const fired = [];
1536
+ for (const b of bindings) {
1537
+ const re = globToRegex(b.glob);
1538
+ if (codeFiles.some((f) => re.test(f)))
1539
+ fired.push(b);
1540
+ }
1541
+ if (fired.length === 0)
1542
+ return out;
1543
+ const acks = readDocAcks(prBody);
1544
+ for (const b of fired) {
1545
+ if (waiverActive) {
1546
+ out.notes.push(`C5 doc-waiver active for ${b.pointer} (binding ${DOC_OWNERS_PATH}:${b.lineNum})`);
1547
+ continue;
1548
+ }
1549
+ if (isUrlPointer(b.pointer)) {
1550
+ const acked = acks.some((a) => a.surface === b.pointer);
1551
+ if (!acked) {
1552
+ out.errors.push(`C5 doc-coverage: code change matched ${DOC_OWNERS_PATH}:${b.lineNum} (glob \`${b.glob}\` → ${b.pointer}). External pointer requires \`Doc-ack: ${b.pointer} — <note>\` in the PR body, or an actor-verified \`${WAIVER_LABEL}\` label to skip.`);
1553
+ }
1554
+ continue;
1555
+ }
1556
+ const pointerPath = pointerToPath(b.pointer);
1557
+ if (!fileExists(pointerPath)) {
1558
+ out.errors.push(`C5 doc-owners-dangling: ${DOC_OWNERS_PATH}:${b.lineNum} points to ${b.pointer}, which does not exist on disk. Fix the binding or add the doc.`);
1559
+ continue;
1560
+ }
1561
+ if (!changed.includes(pointerPath)) {
1562
+ out.errors.push(`C5 doc-coverage: code change matched ${DOC_OWNERS_PATH}:${b.lineNum} (glob \`${b.glob}\` → ${b.pointer}), but ${pointerPath} is not in the PR diff. Update it, or have a principal apply the \`${WAIVER_LABEL}\` label.`);
1563
+ }
1564
+ }
1565
+ return out;
1566
+ }
1567
+ // ../../../packages/aeg-core/src/pr-tier.ts
1568
+ function readTierFromPrBody(prBody) {
1569
+ const searchIn = anchoredRegion(prBody, "TIER") ?? prBody;
1570
+ const m = searchIn.match(/(\*\*)?\s*Tier\s*(\*\*)?\s*:\s*(\*\*)?\s*([013])\b/i);
1571
+ if (!m)
1572
+ return null;
1573
+ const t = Number(m[4]);
1574
+ return t === 0 || t === 1 || t === 3 ? t : null;
1575
+ }
1576
+ var OVERRIDE_BODY_TOKEN = `[${label("override-docs")}]`;
1577
+ function overrideActive(opts) {
1578
+ if (opts.overrideDocsEnv === "1")
1579
+ return true;
1580
+ const labels = (opts.prLabels || "").split(",").map((s) => s.trim());
1581
+ if (hasLabel("override-docs", labels))
1582
+ return true;
1583
+ if ((opts.prBody || "").includes(OVERRIDE_BODY_TOKEN))
1584
+ return true;
1585
+ return false;
1586
+ }
1587
+ // ../../../packages/aeg-core/src/premise-check.ts
1588
+ var ASSERTION_KINDS = new Set(["contains", "absent", "sha256"]);
1589
+
1590
+ // ../../../packages/aeg-core/src/brief-validation.ts
1591
+ function headerRegion(prBody) {
1592
+ const m = prBody.match(/^##\s/m);
1593
+ return m?.index !== undefined ? prBody.slice(0, m.index) : prBody;
1594
+ }
1595
+ function headerField(prBody, labelPattern, anchor) {
1596
+ const anchored = anchor !== undefined ? anchoredRegion(prBody, anchor) : null;
1597
+ const region = anchored ?? headerRegion(prBody);
1598
+ const re = new RegExp(`^(?:\\*\\*)?\\s*${labelPattern}\\s*(?:\\*\\*)?\\s*:\\s*(?:\\*\\*)?\\s*([^\\n·]+)`, "im");
1599
+ const m = region.match(re);
1600
+ if (!m)
1601
+ return null;
1602
+ const value = m[1].trim();
1603
+ return value.length > 0 ? value : null;
1604
+ }
1605
+ function normalize(text) {
1606
+ return text.replace(/[*_]/g, "").replace(/\s+/g, " ");
1607
+ }
1608
+ function headingCheck(prBody, keywordPattern, sectionName) {
1609
+ const re = new RegExp(`^#{1,4}\\s*(?:\\*\\*)?(?:\\d+[a-z]?\\.\\s*)?[^\\n]*${keywordPattern}`, "im");
1610
+ if (re.test(prBody))
1611
+ return { status: "pass", errors: [] };
1612
+ return {
1613
+ status: "fail",
1614
+ errors: [`brief-validation ${sectionName}: no "${sectionName}" section found in the PR body.`]
1615
+ };
1616
+ }
1617
+ function checkTierField(prBody, readTier) {
1618
+ if (readTier(prBody) !== null)
1619
+ return { status: "pass", errors: [] };
1620
+ return {
1621
+ status: "fail",
1622
+ errors: [
1623
+ "brief-validation tier: no `Tier:` field found in the PR body (expected `Tier: 0|1|3` or `**Tier:** 0|1|3`)."
1624
+ ]
1625
+ };
1626
+ }
1627
+ var TEST_PLAN_UNIT_TESTS_ONLY_RE = /(?:\*\*)?Test Plan(?:\*\*)?\s*:\s*(?:\*\*)?\s*unit-tests-only/i;
1628
+ function checkTestPlan(prBody) {
1629
+ if (TEST_PLAN_UNIT_TESTS_ONLY_RE.test(prBody))
1630
+ return { status: "pass", errors: [] };
1631
+ if (/\*\*\[(?:agent|principal)\]\*\*/.test(prBody))
1632
+ return { status: "pass", errors: [] };
1633
+ return {
1634
+ status: "fail",
1635
+ errors: [
1636
+ "brief-validation Test Plan: no Test Plan section found — expected `Test Plan: unit-tests-only`, or at least one `**[agent]**`/`**[principal]**`-tagged checklist item."
1637
+ ]
1638
+ };
1639
+ }
1640
+ function checkTestPlanExclusivity(prBody) {
1641
+ if (!TEST_PLAN_UNIT_TESTS_ONLY_RE.test(prBody))
1642
+ return { status: "pass", errors: [] };
1643
+ if (/^-\s*\[[ xX]\]\s*\*{2}\[(?:agent|principal)\]\*{2}/im.test(prBody)) {
1644
+ return {
1645
+ status: "fail",
1646
+ errors: [
1647
+ "brief-validation Test Plan shape: `Test Plan: unit-tests-only` and a `- [ ]`/`- [x]` tagged checkbox item are mutually exclusive (brief-authoring §9) — declare one form, not both."
1648
+ ]
1649
+ };
1650
+ }
1651
+ return { status: "pass", errors: [] };
1652
+ }
1653
+ function checkPrincipalPlaceholder(prBody) {
1654
+ const lineRe = /^-\s*\[[ xX]\]\s*\*{2}\[principal\]\*{2}(.*)$/gim;
1655
+ for (const m of prBody.matchAll(lineRe)) {
1656
+ const content = m[1] ?? "";
1657
+ if (/^\s*None\b/i.test(content)) {
1658
+ return {
1659
+ status: "fail",
1660
+ errors: [
1661
+ '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.'
1662
+ ]
1663
+ };
1664
+ }
1665
+ }
1666
+ return { status: "pass", errors: [] };
1667
+ }
1668
+ function checkSurfaceMap(prBody) {
1669
+ return headingCheck(prBody, "(?:technical\\s+)?surface map", "Technical surface map");
1670
+ }
1671
+ function checkDocUpdateList(prBody) {
1672
+ return headingCheck(prBody, "(?:documentation|doc)[- ]update(?:\\s+list)?", "Documentation-update list");
1673
+ }
1674
+ function checkWorktreeStep0(prBody) {
1675
+ if (/git worktree add/.test(prBody))
1676
+ return { status: "pass", errors: [] };
1677
+ return {
1678
+ status: "fail",
1679
+ errors: ["brief-validation worktree Step 0: no `git worktree add` command found in the PR body."]
1680
+ };
1681
+ }
1682
+ function checkStopConditions(prBody) {
1683
+ return headingCheck(prBody, "stop conditions", "Stop conditions");
1684
+ }
1685
+ function checkAutonomyClause(prBody) {
1686
+ const normalized = normalize(prBody).toLowerCase();
1687
+ if (/do not stop to ask clarifying questions/.test(normalized))
1688
+ return { status: "pass", errors: [] };
1689
+ return {
1690
+ status: "fail",
1691
+ errors: [
1692
+ 'brief-validation autonomy clause: the standing autonomy clause ("Do not stop to ask clarifying questions...") was not found in the PR body.'
1693
+ ]
1694
+ };
1695
+ }
1696
+ function checkProjectField(prBody) {
1697
+ if (headerField(prBody, "Project(?:\\(s\\))?", "PROJECT") !== null)
1698
+ return { status: "pass", errors: [] };
1699
+ return {
1700
+ status: "fail",
1701
+ errors: [
1702
+ "brief-validation Project: no `Project:` field found in the PR body header block (before the first `##` heading). Required per the brief-developer contract — expected `Project: <name>[, <name>]` or `**Project:** …`."
1703
+ ]
1704
+ };
1705
+ }
1706
+ function checkForField(prBody) {
1707
+ if (headerField(prBody, "For") !== null)
1708
+ return { status: "pass", errors: [] };
1709
+ return {
1710
+ status: "fail",
1711
+ errors: [
1712
+ "brief-validation For: no `For:` field found in the PR body header block (before the first `##` heading). Required per the brief-authoring skill — expected `For: <model + environment>` or `**For:** …`."
1713
+ ]
1714
+ };
1715
+ }
1716
+ function checkClosesN(prBody) {
1717
+ const closesPattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s{0,8}:?\s{0,8}#\d+/i;
1718
+ if (closesPattern.test(stripCode(prBody))) {
1719
+ return { status: "pass", errors: [] };
1720
+ }
1721
+ if (closesPattern.test(prBody)) {
1722
+ return {
1723
+ status: "fail",
1724
+ errors: [
1725
+ "brief-validation Closes #N: `Closes #N` found only inside a code span — GitHub won't auto-close it. Put a bare `Closes #N` on its own line inside the `AEG:CLOSES` anchor."
1726
+ ]
1727
+ };
1728
+ }
1729
+ return {
1730
+ status: "fail",
1731
+ errors: ["brief-validation Closes #N: no `Closes #<N>` (or Fixes/Resolves) reference found in the PR body."]
1732
+ };
1733
+ }
1734
+ function checkBriefSections(prBody, readTier, options = {}) {
1735
+ const { requireClosesN = true } = options;
1736
+ const results = [
1737
+ checkTierField(prBody, readTier),
1738
+ checkTestPlan(prBody),
1739
+ checkTestPlanExclusivity(prBody),
1740
+ checkPrincipalPlaceholder(prBody),
1741
+ checkSurfaceMap(prBody),
1742
+ checkDocUpdateList(prBody),
1743
+ checkWorktreeStep0(prBody),
1744
+ checkStopConditions(prBody),
1745
+ checkAutonomyClause(prBody),
1746
+ checkProjectField(prBody),
1747
+ checkForField(prBody),
1748
+ ...requireClosesN ? [checkClosesN(prBody)] : []
1749
+ ];
1750
+ return { errors: results.flatMap((r) => r.errors) };
1751
+ }
1752
+ // ../../../packages/aeg-core/src/issue-validation.ts
1753
+ function hasRationaleField(body, labelPattern) {
1754
+ const re = new RegExp(`(?:\\*\\*|^#{1,4}\\s+)\\s*(?:${labelPattern})`, "im");
1755
+ return re.test(body);
1756
+ }
1757
+ var RATIONALE_FIELDS = [
1758
+ { name: "Boundary", pattern: "Boundary" },
1759
+ { name: "Sizing", pattern: "Sizing" },
1760
+ { name: "Project(s) + blast radius", pattern: "Project\\(s\\)|Project(?:s)?\\s*\\+|blast radius" },
1761
+ { name: "Dependency rationale", pattern: "Dependency rationale|Depends[- ]on" },
1762
+ { name: "Traps to avoid", pattern: "Traps" },
1763
+ { name: "Suggested agent-class", pattern: "(?:Suggested\\s+)?agent-class" },
1764
+ { name: "Stop-and-escalate", pattern: "Stop-and-escalate" },
1765
+ { name: "Docs to keep coherent", pattern: "Docs to keep coherent|§7" }
1766
+ ];
1767
+ var DEPENDENCY_RATIONALE_FIELD_NAME = "Dependency rationale";
1768
+ function checkIssueRationale(body) {
1769
+ const errors = [];
1770
+ for (const f of RATIONALE_FIELDS) {
1771
+ if (!hasRationaleField(body, f.pattern)) {
1772
+ errors.push(`issue-validation ${f.name}: rationale field not found in the Issue body — every task Issue carries the full Planner's rationale (aeg-root/contracts/planner-brief.md).`);
1773
+ continue;
1774
+ }
1775
+ if (f.name === DEPENDENCY_RATIONALE_FIELD_NAME && !SECTION_HEADER.test(body)) {
1776
+ errors.push(`issue-validation ${f.name}: rationale field found, but not in the form amend-deps requires — ` + "write `**Dependency rationale** — …`, not `**Dependency rationale:** …`. " + "`amendRationaleDeps` (the only sanctioned way to edit Depends-on/Conflicts-with) locates this " + "section by the exact anchor `**Dependency rationale**`; a colon inside the bold breaks that match " + "and the Issue becomes unamendable.");
1777
+ }
1778
+ }
1779
+ return { status: errors.length > 0 ? "fail" : "pass", errors };
1780
+ }
1781
+ function isTaskIssueLabelSet(labels) {
1782
+ return hasLabel("tranche", labels);
1783
+ }
1784
+
1785
+ // ../../../packages/aeg-core/src/coherence-checks.ts
1786
+ var COHERENCE_ENFORCED_FROM = "2026-07-01";
1787
+ function isGrandfathered(isoDate) {
1788
+ if (!isoDate)
1789
+ return false;
1790
+ return isoDate.slice(0, 10) < COHERENCE_ENFORCED_FROM;
1791
+ }
1792
+ var R1_GRANDFATHERED_ISSUES = new Set([279, 280, 281, 282]);
1793
+ function checkA1(entries) {
1794
+ const failures = [];
1795
+ for (const e of entries) {
1796
+ if (!e.facts)
1797
+ continue;
1798
+ if (e.facts.stateReason === "not_planned")
1799
+ continue;
1800
+ if (e.facts.issueState === "closed" && e.facts.prState !== "merged") {
1801
+ failures.push({
1802
+ issue: e.task.issue,
1803
+ tranche: e.trancheSlug,
1804
+ task: e.task.id,
1805
+ reason: `Issue closed but closing PR is not merged (prState: ${e.facts.prState})`,
1806
+ grandfathered: isGrandfathered(e.facts.closedAt)
1807
+ });
1808
+ }
1809
+ }
1810
+ const activeFails = failures.filter((f) => !f.grandfathered);
1811
+ const status = activeFails.length > 0 ? "fail" : failures.length > 0 ? "info" : "pass";
1812
+ return {
1813
+ check: "A1",
1814
+ status,
1815
+ failures,
1816
+ note: status === "info" ? `${failures.length} grandfathered (pre-${COHERENCE_ENFORCED_FROM})` : undefined
1817
+ };
1818
+ }
1819
+ function checkA3(entries) {
1820
+ const failures = [];
1821
+ for (const e of entries) {
1822
+ if (!e.facts)
1823
+ continue;
1824
+ if (e.facts.prState === "merged" && e.facts.issueState !== "closed") {
1825
+ failures.push({
1826
+ issue: e.task.issue,
1827
+ tranche: e.trancheSlug,
1828
+ task: e.task.id,
1829
+ reason: "Closing PR is merged but Issue is still open (GitHub auto-close misfire)",
1830
+ grandfathered: isGrandfathered(e.facts.mergedAt)
1831
+ });
1832
+ }
1833
+ }
1834
+ const activeFails = failures.filter((f) => !f.grandfathered);
1835
+ const status = activeFails.length > 0 ? "fail" : failures.length > 0 ? "info" : "pass";
1836
+ return {
1837
+ check: "A3",
1838
+ status,
1839
+ failures,
1840
+ note: status === "info" ? `${failures.length} grandfathered (pre-${COHERENCE_ENFORCED_FROM})` : undefined
1841
+ };
1842
+ }
1843
+ function checkT1(entries) {
1844
+ const failures = [];
1845
+ for (const e of entries) {
1846
+ if (e.task.issue === null)
1847
+ continue;
1848
+ if (e.facts === undefined) {
1849
+ failures.push({
1850
+ issue: e.task.issue,
1851
+ tranche: e.trancheSlug,
1852
+ task: e.task.id,
1853
+ reason: `Issue #${e.task.issue} in topology does not resolve to a real GitHub Issue`
1854
+ });
1855
+ }
1856
+ }
1857
+ return { check: "T1", status: failures.length > 0 ? "fail" : "pass", failures };
1858
+ }
1859
+ function checkT2(openIssuesBySlug, topologyIssuesBySlug, ciTrancheSlug) {
1860
+ const failures = [];
1861
+ for (const [slug, openNums] of openIssuesBySlug) {
1862
+ if (ciTrancheSlug && slug !== ciTrancheSlug)
1863
+ continue;
1864
+ const topologySet = topologyIssuesBySlug.get(slug) ?? new Set;
1865
+ for (const num of openNums) {
1866
+ if (!topologySet.has(num)) {
1867
+ failures.push({
1868
+ issue: num,
1869
+ tranche: slug,
1870
+ reason: `Issue #${num} is open and labeled ${trancheLabel(slug)} but does not appear in the topology file`
1871
+ });
1872
+ }
1873
+ }
1874
+ }
1875
+ return { check: "T2", status: failures.length > 0 ? "fail" : "pass", failures };
1876
+ }
1877
+ function scopeT2ToPlanPr(result, isPlanPr) {
1878
+ if (isPlanPr || result.status !== "fail")
1879
+ return result;
1880
+ return {
1881
+ ...result,
1882
+ status: "info",
1883
+ note: result.note ?? "T2 findings are non-blocking outside plan PRs (point-of-power principle) — see aeg-root/enforcement.md."
1884
+ };
1885
+ }
1886
+ function checkT3(entries, ciTrancheSlug, enrichedEntries, forgeUnavailableSlugs) {
1887
+ const preEnforcement = new Set;
1888
+ if (enrichedEntries) {
1889
+ for (const e of enrichedEntries) {
1890
+ if (isGrandfathered(e.facts?.closedAt) || isGrandfathered(e.facts?.mergedAt)) {
1891
+ preEnforcement.add(e.trancheSlug);
1892
+ }
1893
+ }
1894
+ }
1895
+ const failures = [];
1896
+ for (const e of entries) {
1897
+ if (!e.archived && e.task.issue === null) {
1898
+ if (ciTrancheSlug && e.trancheSlug !== ciTrancheSlug)
1899
+ continue;
1900
+ if (forgeUnavailableSlugs?.has(e.trancheSlug)) {
1901
+ failures.push({
1902
+ issue: null,
1903
+ tranche: e.trancheSlug,
1904
+ task: e.task.id,
1905
+ reason: `Task ${e.task.id} in active tranche has no Issue ref (#TBD or empty), but forge data for tranche "${e.trancheSlug}" was unavailable — cannot evaluate grandfather status, not silently failed`,
1906
+ grandfathered: true
1907
+ });
1908
+ continue;
1909
+ }
1910
+ failures.push({
1911
+ issue: null,
1912
+ tranche: e.trancheSlug,
1913
+ task: e.task.id,
1914
+ reason: `Task ${e.task.id} in active tranche has no Issue ref (#TBD or empty) — the model requires all active tasks to have Issue numbers`,
1915
+ grandfathered: preEnforcement.has(e.trancheSlug)
1916
+ });
1917
+ }
1918
+ }
1919
+ const activeFails = failures.filter((f) => !f.grandfathered);
1920
+ const status = activeFails.length > 0 ? "fail" : failures.length > 0 ? "info" : "pass";
1921
+ return {
1922
+ check: "T3",
1923
+ status,
1924
+ failures,
1925
+ note: status === "info" ? `${failures.length} grandfathered (pre-${COHERENCE_ENFORCED_FROM})` : undefined
1926
+ };
1927
+ }
1928
+ function checkD1(entries, issueToEntry, taskToEntry) {
1929
+ const failures = [];
1930
+ for (const e of entries) {
1931
+ if (!e.facts)
1932
+ continue;
1933
+ if (e.facts.prState !== "open")
1934
+ continue;
1935
+ for (const dep of e.task.dependsOn) {
1936
+ const depEntry = resolveDepEntry(dep, e.trancheSlug, issueToEntry, taskToEntry);
1937
+ if (!depEntry)
1938
+ continue;
1939
+ const depFacts = depEntry.facts;
1940
+ const depClosed = depFacts?.issueState === "closed";
1941
+ if (!depClosed) {
1942
+ failures.push({
1943
+ issue: e.task.issue,
1944
+ tranche: e.trancheSlug,
1945
+ task: e.task.id,
1946
+ reason: `Task has open PR but depends-on ${dep} (issue #${depEntry.task.issue ?? "?"}) is not closed`
1947
+ });
1948
+ }
1949
+ }
1950
+ }
1951
+ return { check: "D1", status: failures.length > 0 ? "fail" : "pass", failures };
1952
+ }
1953
+ function resolveDepEntry(dep, trancheSlug, issueToEntry, taskToEntry) {
1954
+ const issueMatch = dep.match(/^#(\d+)$/);
1955
+ if (issueMatch?.[1])
1956
+ return issueToEntry.get(Number(issueMatch[1]));
1957
+ return taskToEntry.get(`${trancheSlug}/${dep}`) ?? taskToEntry.get(dep);
1958
+ }
1959
+ function checkR1(issuesBySlug, grandfatheredIssues) {
1960
+ const failures = [];
1961
+ for (const [slug, issues] of issuesBySlug) {
1962
+ for (const issue of issues) {
1963
+ if (!isTaskIssueLabelSet(issue.labels))
1964
+ continue;
1965
+ const { status: status2, errors } = checkIssueRationale(issue.body);
1966
+ if (status2 !== "fail")
1967
+ continue;
1968
+ failures.push({
1969
+ issue: issue.number,
1970
+ tranche: slug,
1971
+ reason: `Issue #${issue.number} fails the rationale gate: ${errors.join(" | ")}`,
1972
+ grandfathered: grandfatheredIssues.has(issue.number)
1973
+ });
1974
+ }
1975
+ }
1976
+ const activeFails = failures.filter((f) => !f.grandfathered);
1977
+ const status = activeFails.length > 0 ? "fail" : failures.length > 0 ? "info" : "pass";
1978
+ return {
1979
+ check: "R1",
1980
+ status,
1981
+ failures,
1982
+ note: status === "info" ? `${failures.length} grandfathered task Issue(s) predate the rationale grammar` : undefined
1983
+ };
1984
+ }
1985
+ function checkL1(files, entriesBySlug) {
1986
+ const failures = [];
1987
+ for (const f of files) {
1988
+ if (f.archived)
1989
+ continue;
1990
+ const entries = entriesBySlug.get(f.slug) ?? [];
1991
+ const withFacts = entries.filter((e) => e.facts !== undefined);
1992
+ if (withFacts.length === 0)
1993
+ continue;
1994
+ const allClosed = withFacts.every((e) => e.facts?.issueState === "closed");
1995
+ if (allClosed) {
1996
+ failures.push({
1997
+ tranche: f.slug,
1998
+ reason: "Active tranche has no open task-Issues — consider archiving to completed/"
1999
+ });
2000
+ }
2001
+ }
2002
+ return {
2003
+ check: "L1",
2004
+ status: "info",
2005
+ failures,
2006
+ note: failures.length > 0 ? `${failures.length} active tranche(s) with no open task-Issues — consider archiving (advisory)` : undefined
2007
+ };
2008
+ }
2009
+ function checkL3(files) {
2010
+ const active = files.filter((f) => !f.archived);
2011
+ return {
2012
+ check: "L3",
2013
+ status: "info",
2014
+ failures: [],
2015
+ note: `${active.length} active tranche(s): ${active.map((f) => f.slug).join(", ") || "(none)"}`
2016
+ };
2017
+ }
2018
+ function extractClosesReferences(prBody) {
2019
+ const closesPattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s{0,8}:?\s{0,8}#(\d+)/gi;
2020
+ const stripped = stripCode(prBody);
2021
+ const searchIn = anchoredRegion(stripped, "CLOSES") ?? stripped;
2022
+ const referenced = new Set;
2023
+ for (const hit of searchIn.matchAll(closesPattern)) {
2024
+ referenced.add(Number(hit[1]));
2025
+ }
2026
+ return referenced;
2027
+ }
2028
+ function checkClosesN2(branch, prBody, trancheFiles, taskIssueRefs) {
2029
+ const referenced = extractClosesReferences(prBody);
2030
+ if (taskIssueRefs) {
2031
+ for (const n of referenced) {
2032
+ const ref = taskIssueRefs.get(n);
2033
+ if (!ref)
2034
+ continue;
2035
+ const expectedBranch = `task/${ref.trancheSlug}/${ref.taskId}`;
2036
+ if (branch !== expectedBranch) {
2037
+ return {
2038
+ ok: false,
2039
+ message: `closes-n-reverse: branch "${branch}" closes #${n} (task ${ref.taskId} of tranche "${ref.trancheSlug}") but is not named "${expectedBranch}" — rename the branch and re-push, or if this work is intentionally outside AEG's dispatch flow, remove the Closes reference.`
2040
+ };
2041
+ }
2042
+ }
2043
+ }
2044
+ const m = branch.match(/^task\/([^/]+)\/([^/]+)$/);
2045
+ if (!m)
2046
+ return { ok: true };
2047
+ const trancheSlug = m[1];
2048
+ const taskId = m[2];
2049
+ const trancheFile = trancheFiles.find((f) => f.slug === trancheSlug);
2050
+ if (!trancheFile) {
2051
+ return {
2052
+ ok: false,
2053
+ message: `closes-n: branch "${branch}" references tranche "${trancheSlug}" but no topology file found at aeg-root/tranches/${trancheSlug}.md. Ensure the tranche file exists before opening the PR.`
2054
+ };
2055
+ }
2056
+ const task = trancheFile.tranche.tasks.find((t) => t.id === taskId);
2057
+ if (!task) {
2058
+ return {
2059
+ ok: false,
2060
+ message: `closes-n: branch "${branch}" references task "${taskId}" not found in ${trancheSlug} topology. Verify the task ID matches the tranche file.`
2061
+ };
2062
+ }
2063
+ if (task.issue === null) {
2064
+ return {
2065
+ ok: false,
2066
+ message: `closes-n: task "${taskId}" in "${trancheSlug}" has no Issue number (#TBD). The Planner must cut the Issue before this PR can be validated.`
2067
+ };
2068
+ }
2069
+ const expectedIssue = task.issue;
2070
+ if (!referenced.has(expectedIssue)) {
2071
+ return {
2072
+ ok: false,
2073
+ expectedIssue,
2074
+ message: `closes-n: PR body does not contain \`Closes #${expectedIssue}\` (required for task "${taskId}" in tranche "${trancheSlug}"). Add it to the PR body Summary section.`
2075
+ };
2076
+ }
2077
+ return { ok: true, expectedIssue };
2078
+ }
2079
+ // ../../../packages/aeg-core/src/verdict-extraction.ts
2080
+ function extractVerdict(comments, valuePattern, missingLabel) {
2081
+ const clearHits = comments.filter((c) => valuePattern.test(c));
2082
+ if (clearHits.length > 0) {
2083
+ const latest = clearHits[clearHits.length - 1];
2084
+ const m = latest.match(valuePattern);
2085
+ return { value: m[1].toUpperCase().replace(/[_-]/g, " "), danglingNote: null };
2086
+ }
2087
+ return {
2088
+ value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
2089
+ danglingNote: `no ${missingLabel} verdict comment found on this PR`
2090
+ };
2091
+ }
2092
+ function extractCodeReviewVerdict(comments) {
2093
+ return extractVerdict(comments, /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(APPROVE|REQUEST[ _-]?CHANGES|LGTM)(?![A-Za-z0-9])/im, "code-reviewer");
2094
+ }
2095
+ function extractSecurityReviewVerdict(comments) {
2096
+ return extractVerdict(comments, /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(PASS|FAIL)(?![A-Za-z0-9])/im, "security-review");
2097
+ }
2098
+ // ../../../packages/aeg-core/src/review-gate.ts
2099
+ function isReviewGateExemptBranch(branch) {
2100
+ return branch.startsWith("plan/");
2101
+ }
2102
+ function checkReviewGate(input) {
2103
+ const waived = isWaiverLabelActorVerified({
2104
+ label: WAIVER_LABEL_REVIEW,
2105
+ labels: input.labels,
2106
+ labelActor: input.waiverLabelActor,
2107
+ principalAllowlist: PRINCIPAL_ALLOWLIST
2108
+ });
2109
+ if (waived) {
2110
+ return {
2111
+ verdict: "pass",
2112
+ reason: `\`${WAIVER_LABEL_REVIEW}\` label is actor-verified — review requirement waived for this PR.`,
2113
+ waived: true
2114
+ };
2115
+ }
2116
+ const codeReview = extractCodeReviewVerdict(input.comments);
2117
+ const security = extractSecurityReviewVerdict(input.comments);
2118
+ const codeReviewClean = codeReview.value === "APPROVE";
2119
+ const securityClean = security.value === "PASS";
2120
+ if (codeReviewClean && securityClean) {
2121
+ return {
2122
+ verdict: "pass",
2123
+ reason: "code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS.",
2124
+ waived: false
2125
+ };
2126
+ }
2127
+ const problems = [];
2128
+ if (!codeReviewClean)
2129
+ problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
2130
+ if (!securityClean)
2131
+ problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
2132
+ return {
2133
+ verdict: "fail",
2134
+ reason: `${problems.join("; ")}. A principal can apply an actor-verified \`${WAIVER_LABEL_REVIEW}\` label to skip this requirement, or post the missing/clean verdict comment(s).`,
2135
+ waived: false
2136
+ };
2137
+ }
2138
+ // ../../../packages/aeg-core/src/markdown-table.ts
2139
+ function splitRow(line) {
2140
+ const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
2141
+ return trimmed.split("|").map((cell) => cell.trim());
2142
+ }
2143
+ var TABLE_ROW_PATTERN = /^\s*\|.*\|\s*$/;
2144
+ var SEPARATOR_ROW_PATTERN = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/;
2145
+ function findTable(lines, fromLine, stopAtHeading = true) {
2146
+ let i = fromLine - 1;
2147
+ while (i < lines.length) {
2148
+ const line = lines[i] ?? "";
2149
+ if (stopAtHeading && i > fromLine - 1 && /^##\s/.test(line))
2150
+ return null;
2151
+ if (TABLE_ROW_PATTERN.test(line)) {
2152
+ const headerLine = line;
2153
+ const sepLine = lines[i + 1] ?? "";
2154
+ if (!SEPARATOR_ROW_PATTERN.test(sepLine)) {
2155
+ i++;
2156
+ continue;
2157
+ }
2158
+ const headers = splitRow(headerLine);
2159
+ const rows = [];
2160
+ let j = i + 2;
2161
+ while (j < lines.length && TABLE_ROW_PATTERN.test(lines[j] ?? "")) {
2162
+ rows.push({ cells: splitRow(lines[j] ?? ""), line: j + 1 });
2163
+ j++;
2164
+ }
2165
+ return { headers, rows };
2166
+ }
2167
+ i++;
2168
+ }
2169
+ return null;
2170
+ }
2171
+ function findHeadingLine(lines, pattern) {
2172
+ for (let i = 0;i < lines.length; i++) {
2173
+ if (pattern.test(lines[i] ?? ""))
2174
+ return i + 1;
2175
+ }
2176
+ return null;
2177
+ }
2178
+ // ../../../packages/aeg-core/src/diagram-model.ts
2179
+ import matter from "gray-matter";
2180
+
2181
+ // ../../../packages/aeg-core/src/registry-parse.ts
2182
+ var RING_HEADINGS = [
2183
+ { ring: "ring0", pattern: /^##\s+Ring 0\b/ },
2184
+ { ring: "ring1", pattern: /^##\s+Ring 1\b/ },
2185
+ { ring: "ring2", pattern: /^##\s+Ring 2\b/ }
2186
+ ];
2187
+ function stripBackticks(cell) {
2188
+ const trimmed = cell.trim();
2189
+ if (trimmed.length >= 2 && trimmed.startsWith("`") && trimmed.endsWith("`")) {
2190
+ return trimmed.slice(1, -1);
2191
+ }
2192
+ return trimmed;
2193
+ }
2194
+ function stripBold(cell) {
2195
+ return cell.trim().replace(/\*\*/g, "");
2196
+ }
2197
+ function parseEnforcementRegistry(content) {
2198
+ const lines = content.split(`
2199
+ `);
2200
+ const result = [];
2201
+ for (const { ring, pattern } of RING_HEADINGS) {
2202
+ const headingLine = findHeadingLine(lines, pattern);
2203
+ if (headingLine === null)
2204
+ continue;
2205
+ const table = findTable(lines, headingLine + 1);
2206
+ if (!table)
2207
+ continue;
2208
+ const descriptionIndex = table.headers.findIndex((h) => h.trim().toLowerCase() === "description");
2209
+ const implementationIndex = table.headers.findIndex((h) => h.trim().toLowerCase() === "implementation");
2210
+ for (const row of table.rows) {
2211
+ const cells = row.cells;
2212
+ if (cells.length < 3)
2213
+ continue;
2214
+ const action = stripBold(cells[0] ?? "");
2215
+ const summary = stripBackticks(cells[1] ?? "");
2216
+ const category = stripBackticks(cells[2] ?? "");
2217
+ const implementation = stripBackticks((implementationIndex === -1 ? cells[cells.length - 1] : cells[implementationIndex]) ?? "");
2218
+ const description = descriptionIndex === -1 ? undefined : stripBackticks(cells[descriptionIndex] ?? "") || undefined;
2219
+ const spec = cells.length > 4 ? stripBackticks(cells[cells.length - 2] ?? "") : undefined;
2220
+ result.push({ ring, action, summary, category, description, spec, implementation, line: row.line });
2221
+ }
2222
+ }
2223
+ return result;
2224
+ }
2225
+ // ../../../packages/aeg-core/src/registry-checks.ts
2226
+ function checkG1(rows, existsFn) {
2227
+ const findings = [];
2228
+ for (const row of rows) {
2229
+ if (row.implementation === "")
2230
+ continue;
2231
+ if (!existsFn(row.implementation)) {
2232
+ findings.push({
2233
+ row: row.action,
2234
+ path: row.implementation,
2235
+ reason: `${row.ring} row "${row.action}" names implementation "${row.implementation}", which does not exist on disk`
2236
+ });
2237
+ }
2238
+ }
2239
+ return { check: "G1", status: findings.length > 0 ? "info" : "pass", findings };
2240
+ }
2241
+ function checkG2(rows, candidateFiles) {
2242
+ const implementations = new Set(rows.map((r) => r.implementation).filter((p) => p !== ""));
2243
+ const findings = [];
2244
+ for (const path of candidateFiles) {
2245
+ if (!implementations.has(path)) {
2246
+ findings.push({
2247
+ path,
2248
+ reason: `"${path}" is not named as the implementation of any row in enforcement.md's ring tables`
2249
+ });
2250
+ }
2251
+ }
2252
+ return { check: "G2", status: findings.length > 0 ? "info" : "pass", findings };
2253
+ }
2254
+ function checkG3(ring0Rows, crossingFiles) {
2255
+ const ring0Implementations = new Set(ring0Rows.map((r) => r.implementation));
2256
+ const findings = [];
2257
+ for (const path of crossingFiles) {
2258
+ if (!ring0Implementations.has(path)) {
2259
+ findings.push({
2260
+ path,
2261
+ reason: `"${path}" makes a GitHub-crossing call but is not named by any Ring-0 row's implementation — a seventh, unlisted way into GitHub`
2262
+ });
2263
+ }
2264
+ }
2265
+ return { check: "G3", status: findings.length > 0 ? "fail" : "pass", findings };
2266
+ }
2267
+ function checkG4(content, resolveFn) {
2268
+ const cited = new Set;
2269
+ for (const hit of content.matchAll(/#(\d{3,})\b/g)) {
2270
+ cited.add(Number(hit[1]));
2271
+ }
2272
+ const findings = [];
2273
+ for (const n of cited) {
2274
+ if (!resolveFn(n)) {
2275
+ findings.push({
2276
+ reason: `#${n} is cited in enforcement.md but does not resolve to a real Issue or PR in the forge`
2277
+ });
2278
+ }
2279
+ }
2280
+ return { check: "G4", status: findings.length > 0 ? "fail" : "pass", findings };
2281
+ }
2282
+ function checkG5(roles, contracts) {
2283
+ const roleIds = new Set(roles.map((r) => r.role_id));
2284
+ const findings = [];
2285
+ for (const contract of contracts) {
2286
+ if (!roleIds.has(contract.producer)) {
2287
+ findings.push({
2288
+ path: contract.file,
2289
+ reason: `contract "${contract.file}" names producer "${contract.producer}", which is not a real role_id`
2290
+ });
2291
+ }
2292
+ if (!roleIds.has(contract.consumer)) {
2293
+ findings.push({
2294
+ path: contract.file,
2295
+ reason: `contract "${contract.file}" names consumer "${contract.consumer}", which is not a real role_id`
2296
+ });
2297
+ }
2298
+ }
2299
+ for (const role of roles) {
2300
+ if (role.performs.length === 0) {
2301
+ findings.push({ path: role.file, reason: `role "${role.role_id}" has an empty performs array` });
2302
+ }
2303
+ if (role.refuses_when.trim() === "") {
2304
+ findings.push({ path: role.file, reason: `role "${role.role_id}" has an empty refuses_when` });
2305
+ }
2306
+ }
2307
+ return { check: "G5", status: findings.length > 0 ? "fail" : "pass", findings };
2308
+ }
2309
+ // ../../../packages/aeg-core/src/dead-branch-push-guard.ts
2310
+ function checkDeadBranchPush(input) {
2311
+ const { branch, prState, prNumber } = input;
2312
+ if (prState === "MERGED") {
2313
+ return {
2314
+ verdict: "refuse",
2315
+ reason: `Branch \`${branch}\` already has a MERGED PR (#${prNumber}) — this branch's work is done. Push a new task branch instead of pushing onto an already-resolved one.`
2316
+ };
2317
+ }
2318
+ if (prState === "CLOSED") {
2319
+ return {
2320
+ verdict: "refuse",
2321
+ reason: `Branch \`${branch}\` has a CLOSED PR (#${prNumber}) that was never merged — pushing here would resurrect abandoned/rejected work. Open a new task branch instead.`
2322
+ };
2323
+ }
2324
+ if (prState === "OPEN") {
2325
+ return {
2326
+ verdict: "allow",
2327
+ reason: `Branch \`${branch}\` has an OPEN PR (#${prNumber}) — pushing more commits to it is normal.`
2328
+ };
2329
+ }
2330
+ if (prState === "NONE") {
2331
+ return {
2332
+ verdict: "allow",
2333
+ reason: `Branch \`${branch}\` has no PR yet — a brand-new task branch legitimately has none.`
2334
+ };
2335
+ }
2336
+ return {
2337
+ verdict: "allow",
2338
+ reason: `Could not determine \`${branch}\`'s PR state (forge unreachable) — failing open rather than blocking the push on a transient issue.`
2339
+ };
2340
+ }
2341
+ // ../../../packages/aeg-core/src/reader-resolvable-prose.ts
2342
+ var SHIPS_PREFIX = "aeg-root/";
2343
+ var SHIPS_ARCHIVE_PREFIX = "aeg-root/tranches/completed/";
2344
+ function isSpecFile2(path) {
2345
+ return path.startsWith("apps/") && path.includes("/specs/") && path.endsWith(".md");
2346
+ }
2347
+ function isClaudeMdFile(path) {
2348
+ return path === "CLAUDE.md" || path.endsWith("/CLAUDE.md");
2349
+ }
2350
+ function classifyProseFile(path, readerFacingPrefix, readerFacingSuffix) {
2351
+ if (path.startsWith(SHIPS_PREFIX)) {
2352
+ return path.startsWith(SHIPS_ARCHIVE_PREFIX) ? "internal" : "ships";
2353
+ }
2354
+ if (path.startsWith(readerFacingPrefix) && path.endsWith(readerFacingSuffix)) {
2355
+ return "reader-facing";
2356
+ }
2357
+ if (isSpecFile2(path) || isClaudeMdFile(path))
2358
+ return "internal";
2359
+ return null;
2360
+ }
2361
+ var SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
2362
+ function stripNonProse(path, content) {
2363
+ if (path.endsWith(".md")) {
2364
+ return content.replace(/```.*?```/gs, (m) => m.replace(/[^\n]/g, "")).replace(/`[^`\n]*`/g, (m) => m.replace(/[^\n]/g, ""));
2365
+ }
2366
+ if (path.endsWith(".tsx") || path.endsWith(".ts")) {
2367
+ return content.replace(/\/\*.*?\*\//gs, (m) => m.replace(/[^\n]/g, "")).replace(/(^|[^:])\/\/[^\n]*/g, (m) => m.replace(/[^\n]/g, ""));
2368
+ }
2369
+ return content;
2370
+ }
2371
+ function lineAt(content, index) {
2372
+ let line = 1;
2373
+ for (let i = 0;i < index; i++) {
2374
+ if (content.charCodeAt(i) === 10)
2375
+ line++;
2376
+ }
2377
+ return line;
2378
+ }
2379
+ var FORGE_NUMBER_PATTERN = /#[0-9]{2,4}/g;
2380
+ var TRANCHE_SLUG_VN_PATTERN = /[a-z][a-z-]+-v[0-9]/g;
2381
+ function legacySlugPattern(legacySlugs) {
2382
+ if (legacySlugs.length === 0)
2383
+ return null;
2384
+ const alternation = legacySlugs.map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
2385
+ return new RegExp(`(^|[^a-zA-Z0-9\\n-])(${alternation})([^a-zA-Z0-9\\n-]|$)`, "g");
2386
+ }
2387
+ function checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuffix, legacySlugs = []) {
2388
+ const findings = [];
2389
+ const legacyPattern = legacySlugPattern(legacySlugs);
2390
+ const patterns = [
2391
+ { pattern: FORGE_NUMBER_PATTERN, what: "a forge number" },
2392
+ { pattern: TRANCHE_SLUG_VN_PATTERN, what: "an internal tranche slug" },
2393
+ ...legacyPattern ? [{ pattern: legacyPattern, what: "an internal tranche slug", group: 2 }] : []
2394
+ ];
2395
+ for (const file of files) {
2396
+ const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix);
2397
+ if (!cls || !SWEPT_CLASSES.has(cls))
2398
+ continue;
2399
+ const scrubbed = stripNonProse(file.path, file.content);
2400
+ for (const { pattern, what, group } of patterns) {
2401
+ pattern.lastIndex = 0;
2402
+ let match = pattern.exec(scrubbed);
2403
+ while (match !== null) {
2404
+ const cited = group !== undefined ? match[group] ?? match[0] : match[0];
2405
+ findings.push({
2406
+ file: file.path,
2407
+ line: lineAt(scrubbed, match.index),
2408
+ message: `references ${what} ("${cited}") a reader outside this repo's tracker cannot resolve`
2409
+ });
2410
+ match = pattern.exec(scrubbed);
2411
+ }
2412
+ }
2413
+ }
2414
+ return findings;
2415
+ }
2416
+ function escapeTerm(term) {
2417
+ return term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2418
+ }
2419
+ function containsWholeWord(text, term) {
2420
+ const escaped = escapeTerm(term);
2421
+ const re = new RegExp(`(^|[^a-zA-Z])${escaped}([^a-zA-Z]|$)`, "i");
2422
+ return re.test(text);
2423
+ }
2424
+ function findWholeWordLine(content, term) {
2425
+ const lines = content.split(`
2426
+ `);
2427
+ let lineNo = 0;
2428
+ for (const line of lines) {
2429
+ lineNo++;
2430
+ if (containsWholeWord(line, term))
2431
+ return lineNo;
2432
+ }
2433
+ return null;
2434
+ }
2435
+ function definesOrLinksGlossary(content, term) {
2436
+ const escaped = escapeTerm(term);
2437
+ const definesInline = new RegExp(`(^|[^a-zA-Z])${escaped}([^a-zA-Z]|$)[^\\n]{0,3}—`, "i").test(content);
2438
+ if (definesInline)
2439
+ return true;
2440
+ return /docs\/glossary|glossary\.md/i.test(content);
2441
+ }
2442
+ function checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix) {
2443
+ const findings = [];
2444
+ for (const file of files) {
2445
+ const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix);
2446
+ if (!cls || !SWEPT_CLASSES.has(cls))
2447
+ continue;
2448
+ const scrubbed = stripNonProse(file.path, file.content);
2449
+ for (const term of glossaryTerms) {
2450
+ const line = findWholeWordLine(scrubbed, term);
2451
+ if (line === null)
2452
+ continue;
2453
+ if (definesOrLinksGlossary(scrubbed, term))
2454
+ continue;
2455
+ findings.push({
2456
+ file: file.path,
2457
+ line,
2458
+ message: `uses coined term "${term}" without defining it inline or linking the glossary`
2459
+ });
2460
+ }
2461
+ }
2462
+ return findings;
2463
+ }
2464
+ function checkReaderResolvableProse(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix, legacySlugs = []) {
2465
+ return [
2466
+ ...checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuffix, legacySlugs),
2467
+ ...checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, readerFacingSuffix)
2468
+ ];
2469
+ }
2470
+ function parseGlossaryTerms(glossaryContent) {
2471
+ const terms = [];
2472
+ const re = /^\*\*(.+?)\*\* —/gm;
2473
+ let match = re.exec(glossaryContent);
2474
+ while (match !== null) {
2475
+ if (match[1] !== undefined)
2476
+ terms.push(match[1]);
2477
+ match = re.exec(glossaryContent);
2478
+ }
2479
+ return terms;
2480
+ }
2481
+ // ../../../packages/aeg-core/src/dispatch-gate.ts
2482
+ function checkDispatchReadiness(input) {
2483
+ const { trancheSlug, task } = input;
2484
+ const taskLabel = `task ${task.id} (tranche ${trancheSlug})`;
2485
+ const blockers = [];
2486
+ if (task.issue === null) {
2487
+ blockers.push(`dispatch-gate issue-existence: ${taskLabel} has no Issue (#TBD or blank) in the topology — not dispatchable until the Planner cuts the Issue.`);
2488
+ } else if (input.issue === null) {
2489
+ blockers.push(`dispatch-gate issue-existence: ${taskLabel} names Issue #${task.issue}, but it does not resolve to a real GitHub Issue (phantom reference).`);
2490
+ }
2491
+ if (input.issue !== null && !input.issueRationalePass) {
2492
+ blockers.push(`dispatch-gate rationale: Issue #${input.issue.number} for ${taskLabel} fails the rationale gate (checkIssueRationale) — the Planner must complete the eight-field rationale before this task is dispatchable.`);
2493
+ }
2494
+ for (const dep of input.dependsOn) {
2495
+ if (!dep.merged) {
2496
+ const issueStr = dep.issue !== null ? ` (#${dep.issue})` : "";
2497
+ blockers.push(`dispatch-gate depends-on: ${taskLabel} depends on ${dep.id}${issueStr}, whose PR is not merged yet — not dispatchable, it serializes behind it.`);
2498
+ }
2499
+ }
2500
+ for (const c of input.conflictsWith) {
2501
+ if (c.openOrInFlight) {
2502
+ const issueStr = c.issue !== null ? ` (#${c.issue})` : "";
2503
+ blockers.push(`dispatch-gate conflicts-with: ${taskLabel} conflicts with ${c.id}${issueStr}, whose PR is open or in-flight — not dispatchable until it merges.`);
2504
+ }
2505
+ }
2506
+ for (const proj of input.priorTrancheArchival) {
2507
+ if (proj.priorTrancheSlug !== null && !proj.archived) {
2508
+ blockers.push(`dispatch-gate prior-tranche-archival: project \`${proj.project}\`'s previous tranche \`${proj.priorTrancheSlug}\` is not archived — the Tranche Archivist must run before new work on this product.`);
2509
+ }
2510
+ }
2511
+ return { ready: blockers.length === 0, blockers };
2512
+ }
2513
+ // ../../../packages/aeg-core/src/single-plan-pr.ts
2514
+ function trancheSlugFromTopologyPath(path) {
2515
+ const m = path.match(/^aeg-root\/tranches\/([^/]+)\.md$/);
2516
+ if (!m)
2517
+ return null;
2518
+ const slug = m[1];
2519
+ if (slug === "README" || slug.endsWith(".tokens"))
2520
+ return null;
2521
+ return slug;
2522
+ }
2523
+ function checkSinglePlanPr(branchFiles, otherOpenPrs) {
2524
+ const touchedSlugs = new Set(branchFiles.map(trancheSlugFromTopologyPath).filter((s) => s !== null));
2525
+ if (touchedSlugs.size === 0)
2526
+ return { ok: true };
2527
+ for (const pr of otherOpenPrs) {
2528
+ const otherSlugs = new Set(pr.files.map(trancheSlugFromTopologyPath).filter((s) => s !== null));
2529
+ for (const slug of touchedSlugs) {
2530
+ if (otherSlugs.has(slug)) {
2531
+ return {
2532
+ ok: false,
2533
+ message: `single-plan-pr: another open PR (#${pr.number}) already touches tranche "${slug}"'s topology file. Only one open plan PR per tranche is allowed at a time — wait for #${pr.number} to merge or close, or coordinate with its author.`
2534
+ };
2535
+ }
2536
+ }
2537
+ }
2538
+ return { ok: true };
2539
+ }
2540
+ function touchesAnyTopology(files) {
2541
+ return files.some((f) => trancheSlugFromTopologyPath(f) !== null);
2542
+ }
2543
+ // ../../../packages/aeg-core/src/no-disk-state.ts
2544
+ var TOP_LEVEL_TOPOLOGY_FILE = /^aeg-root\/tranches\/[^/]+\.md$/;
2545
+ var ANY_DEPTH_TRANCHES_MD = /^aeg-root\/tranches\/.*\.md$/;
2546
+ var TOKENS_FILE = /\.tokens\.md$/;
2547
+ function isNewDiskStateFile(path, status) {
2548
+ if (TOP_LEVEL_TOPOLOGY_FILE.test(path))
2549
+ return true;
2550
+ if (status !== "added")
2551
+ return false;
2552
+ return ANY_DEPTH_TRANCHES_MD.test(path) || TOKENS_FILE.test(path);
2553
+ }
2554
+ // ../../../packages/aeg-core/src/branch-topology-gate.ts
2555
+ function taskBranchTopologyFields(branch) {
2556
+ if (!/^task\/.*\/.*$/.test(branch))
2557
+ return null;
2558
+ const parts = branch.split("/");
2559
+ return { tranche: parts[1] ?? "", taskId: parts[2] ?? "" };
2560
+ }
2561
+ function checkBranchTopology(input) {
2562
+ const { branch, tranche, taskId, topoPath, topology } = input;
2563
+ if (topology === null) {
2564
+ return {
2565
+ verdict: "refuse",
2566
+ reason: `✖ pre-push: branch \`${branch}\` names tranche \`${tranche}\`, but ${topoPath} does not exist.
2567
+ ` + " A task branch must belong to a real tranche."
2568
+ };
2569
+ }
2570
+ const hasRow = topology?.tasks.some((t) => t.id === taskId) ?? false;
2571
+ if (!hasRow) {
2572
+ return {
2573
+ verdict: "refuse",
2574
+ reason: `✖ pre-push: branch \`${branch}\` — no row with \`#\` = \`${taskId}\` in ${topoPath}.
2575
+ ` + ` The branch suffix must literal-match the topology's # column.
2576
+ ` + " If the plan PR adding this row hasn't merged yet, merge it first."
2577
+ };
2578
+ }
2579
+ return {
2580
+ verdict: "allow",
2581
+ reason: `Branch \`${branch}\` matches topology row \`${taskId}\` in ${topoPath}.`
2582
+ };
2583
+ }
2584
+ // ../../../packages/aeg-core/src/first-push-dispatch-gate.ts
2585
+ function parseTaskBranch(branch) {
2586
+ const m = /^task\/([^/]+)\/([^/]+)$/.exec(branch);
2587
+ return m ? { tranche: m[1], taskId: m[2] } : null;
2588
+ }
2589
+ function checkFirstPushDispatchGate(input) {
2590
+ const { branch, prExists, readiness } = input;
2591
+ if (parseTaskBranch(branch) === null) {
2592
+ return {
2593
+ verdict: "allow",
2594
+ reason: `Branch \`${branch}\` is not a task/<tranche>/<n> branch — the first-push dispatch gate only applies to task branches.`
2595
+ };
2596
+ }
2597
+ if (prExists) {
2598
+ return {
2599
+ verdict: "allow",
2600
+ reason: `Branch \`${branch}\` already has an open PR — dispatch readiness was already validated on its first push; later pushes are not re-blocked by a sibling task's state change.`
2601
+ };
2602
+ }
2603
+ if (readiness === "UNKNOWN") {
2604
+ return {
2605
+ verdict: "allow",
2606
+ reason: `verify-dispatch could not reach the forge for \`${branch}\` (repo/token unresolvable) — failing OPEN rather than blocking the push on a transient issue.`
2607
+ };
2608
+ }
2609
+ if (readiness === "NOT_READY") {
2610
+ return {
2611
+ verdict: "refuse",
2612
+ reason: `verify-dispatch reports NOT READY for \`${branch}\` — see the failing predicate printed above.`
2613
+ };
2614
+ }
2615
+ return {
2616
+ verdict: "allow",
2617
+ reason: `verify-dispatch reports READY TO DISPATCH for \`${branch}\`.`
2618
+ };
2619
+ }
2620
+ // ../../../packages/aeg-core/src/issue-assignment.ts
2621
+ function decideIssueAssignment(input) {
2622
+ const { branch, remoteRefExists, issue, assignees, login } = input;
2623
+ if (parseTaskBranch(branch) === null) {
2624
+ return {
2625
+ action: "skip",
2626
+ reason: `Branch \`${branch}\` is not a task/<tranche>/<n> branch — Issue self-assignment only applies to task branches.`
2627
+ };
2628
+ }
2629
+ if (remoteRefExists) {
2630
+ return {
2631
+ action: "skip",
2632
+ reason: `Branch \`${branch}\` already exists on the remote — not its first push, nothing to assign.`
2633
+ };
2634
+ }
2635
+ if (issue === null) {
2636
+ return {
2637
+ action: "skip",
2638
+ reason: `No Issue resolved for \`${branch}\` from its topology row — skipping self-assignment (the gate owns refusing an unplanned branch).`
2639
+ };
2640
+ }
2641
+ if (assignees === null) {
2642
+ return {
2643
+ action: "skip",
2644
+ reason: `Could not read Issue #${issue}'s current assignees — skipping rather than risking a double-assign (fail-open, never a guess).`
2645
+ };
2646
+ }
2647
+ if (assignees.length > 0) {
2648
+ return {
2649
+ action: "skip",
2650
+ reason: `Issue #${issue} is already assigned (${assignees.join(", ")}) — no-op.`
2651
+ };
2652
+ }
2653
+ if (login === null) {
2654
+ return {
2655
+ action: "skip",
2656
+ reason: "Could not resolve the authenticated `gh` login — never assigning anyone other than the actual pusher, so skipping."
2657
+ };
2658
+ }
2659
+ return {
2660
+ action: "assign",
2661
+ issue,
2662
+ login,
2663
+ reason: `First push of \`${branch}\` — assigning Issue #${issue} to @${login} (the authenticated pusher).`
2664
+ };
2665
+ }
2666
+ // ../../../packages/aeg-core/src/test-plan-section.ts
2667
+ var HEADING_FORM_RE = /^#{1,4}[ \t]*(?:\d+[a-z]?\.[ \t]*)?\*{0,2}Test Plan\*{0,2}[ \t]*$/im;
2668
+ var INLINE_FORM_RE = /^[ \t]*\*{0,2}Test Plan:?\*{0,2}/im;
2669
+ var NEXT_SECTION_RE = /\n(?:#{1,3}[ \t]|\*\*[A-Z][^*\n]*:\*\*)/;
2670
+ function locateTestPlanSection(body) {
2671
+ const region = anchoredRegion(body, "TEST-PLAN");
2672
+ if (region !== null)
2673
+ return { found: true, section: region };
2674
+ const headingMatch = HEADING_FORM_RE.exec(body);
2675
+ const match = headingMatch ?? INLINE_FORM_RE.exec(body);
2676
+ if (!match || match.index === undefined)
2677
+ return { found: false };
2678
+ const sectionBody = body.slice(match.index);
2679
+ const firstLineEnd = sectionBody.indexOf(`
2680
+ `);
2681
+ const searchFrom = firstLineEnd === -1 ? sectionBody.length : firstLineEnd + 1;
2682
+ const nextMatch = sectionBody.slice(searchFrom).match(NEXT_SECTION_RE);
2683
+ const sectionEnd = nextMatch ? searchFrom + (nextMatch.index ?? 0) : sectionBody.length;
2684
+ return { found: true, section: sectionBody.slice(0, sectionEnd) };
2685
+ }
2686
+ // ../../../packages/aeg-core/src/test-plan-gate.ts
2687
+ var TASK_BRANCH_PATTERN = /^task\/[^/]+\/[^/]+$/;
2688
+ function evaluateTestPlanGate(body, branch) {
2689
+ if (!body) {
2690
+ return {
2691
+ verdict: "pass",
2692
+ messages: [
2693
+ "PR_BODY env var is empty; nothing to check.",
2694
+ "PASS (no body — likely a local invocation; CI sets PR_BODY automatically)."
2695
+ ]
2696
+ };
2697
+ }
2698
+ const located = locateTestPlanSection(body);
2699
+ if (!located.found) {
2700
+ if (TASK_BRANCH_PATTERN.test(branch)) {
2701
+ return {
2702
+ verdict: "fail",
2703
+ messages: [
2704
+ `FAIL — no Test Plan section found in the PR body for task branch \`${branch}\`.`,
2705
+ "Searched for a `## Test Plan` / `## N. Test Plan` heading and an inline `**Test Plan:**`/`Test Plan:` marker — neither was found.",
2706
+ "Per, a task PR without a Test Plan is malformed, not exempt — add one (or `Test Plan: unit-tests-only` for a pure-logic brief with no runtime surface)."
2707
+ ]
2708
+ };
2709
+ }
2710
+ return {
2711
+ verdict: "pass",
2712
+ messages: [
2713
+ "No Test Plan section in PR body.",
2714
+ "PASS (advisory) — see aeg-root/roles/developer.md (Verification) and aeg-root/skills/brief-authoring/SKILL.md §9.",
2715
+ "Non-task branch (or BRANCH unset) — a brief that touches a runtime surface and has no Test Plan is malformed on a task branch; Brief Validation catches that case pre-dispatch."
2716
+ ]
2717
+ };
2718
+ }
2719
+ const section = located.section;
2720
+ if (/unit-tests-only/i.test(section)) {
2721
+ return {
2722
+ verdict: "pass",
2723
+ messages: ["Test Plan: unit-tests-only — pure-logic brief, CI unit-tests are the proof.", "PASS."]
2724
+ };
2725
+ }
2726
+ const checkboxLines = section.split(`
2727
+ `).map((line) => line.trimStart()).filter((line) => /^[-*]\s+\[[ xX]\]/.test(line));
2728
+ if (checkboxLines.length === 0) {
2729
+ return {
2730
+ verdict: "pass",
2731
+ messages: [
2732
+ "Test Plan section found but has no checkbox items.",
2733
+ "PASS (advisory) — the section may be empty or expressed in prose; verification is the Principal's call."
2734
+ ]
2735
+ };
2736
+ }
2737
+ const unchecked = checkboxLines.filter((line) => /^[-*]\s+\[\s\]/.test(line));
2738
+ const checked = checkboxLines.filter((line) => /^[-*]\s+\[[xX]\]/.test(line));
2739
+ const messages = [`Test Plan items: ${checked.length} ticked, ${unchecked.length} unticked.`];
2740
+ if (unchecked.length > 0) {
2741
+ messages.push("", "FAIL — the following Test Plan items are unticked:");
2742
+ for (const line of unchecked)
2743
+ messages.push(` ${line}`);
2744
+ messages.push("", "Per aeg-root/roles/developer.md (Verification), a PR is not mergeable while any Test Plan box is unticked.", "- [agent] items: the Developer-agent posts the actual command output as evidence and ticks the box.", "- [principal] items: the Principal runs the item in a real signed-in browser and ticks the box.", "Note: editing the PR body does not retrigger most workflows. If your PR body changes do not surface here, push an empty commit to re-run.");
2745
+ return { verdict: "fail", messages };
2746
+ }
2747
+ messages.push("All Test Plan items ticked — runtime verification complete.", "PASS.");
2748
+ return { verdict: "pass", messages };
2749
+ }
2750
+ // ../sources/src/forge-adapter.ts
2751
+ function createForgeSource(config) {
2752
+ return {
2753
+ async getTranche(slug) {
2754
+ return deriveTrancheFromForge(config.owner, config.repo, slug);
2755
+ }
2756
+ };
2757
+ }
2758
+ // ../sources/src/select-source.ts
2759
+ import { z } from "zod";
2760
+ var StateSourceConfigSchema = z.discriminatedUnion("kind", [
2761
+ z.object({ kind: z.literal("forge"), owner: z.string(), repo: z.string() }),
2762
+ z.object({ kind: z.literal("file"), root: z.string().optional() })
2763
+ ]);
2764
+ // src/checks/contract.ts
2765
+ var CHECK_SCHEMA_VERSION = 1;
2766
+ function emitCheckError(error) {
2767
+ process.stderr.write(`${JSON.stringify(error)}
2768
+ `);
2769
+ }
2770
+
2771
+ // src/checks/bin/check-dispatch-readiness.ts
2772
+ var CHECK_NAME = "dispatch-readiness";
2773
+ var execFileAsync4 = promisify4(execFile4);
2774
+ function git(args) {
2775
+ try {
2776
+ return execFileSync2("git", args, { encoding: "utf8" }).trim();
2777
+ } catch {
2778
+ return "";
2779
+ }
2780
+ }
2781
+ function resolveRepo2() {
2782
+ const fromEnv = process.env.AEG_REPO;
2783
+ if (fromEnv) {
2784
+ const m = fromEnv.match(/^([^/]+)\/(.+)$/);
2785
+ if (m?.[1] && m[2])
2786
+ return { owner: m[1], repo: m[2] };
2787
+ }
2788
+ const url = git(["remote", "get-url", "origin"]);
2789
+ const ssh = url.match(/^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
2790
+ if (ssh?.[1] && ssh[2])
2791
+ return { owner: ssh[1], repo: ssh[2] };
2792
+ const https = url.match(/^https?:\/\/(?:[^@]+@)?github\.com\/([^/]+)\/(.+?)(?:\.git)?\/?$/);
2793
+ if (https?.[1] && https[2])
2794
+ return { owner: https[1], repo: https[2] };
2795
+ return null;
2796
+ }
2797
+ async function resolveToken() {
2798
+ if (process.env.GITHUB_TOKEN)
2799
+ return process.env.GITHUB_TOKEN;
2800
+ if (process.env.GH_TOKEN)
2801
+ return process.env.GH_TOKEN;
2802
+ try {
2803
+ const { stdout } = await execFileAsync4("gh", ["auth", "token"]);
2804
+ const token = stdout.trim();
2805
+ return token.length > 0 ? token : null;
2806
+ } catch {
2807
+ return null;
2808
+ }
2809
+ }
2810
+ function currentBranch() {
2811
+ return process.env.BRANCH || git(["rev-parse", "--abbrev-ref", "HEAD"]);
2812
+ }
2813
+ function fail(message, prompt) {
2814
+ emitCheckError({
2815
+ schema: CHECK_SCHEMA_VERSION,
2816
+ check: CHECK_NAME,
2817
+ severity: "error",
2818
+ message,
2819
+ agent_recovery_prompt: prompt
2820
+ });
2821
+ process.exit(1);
2822
+ }
2823
+ function resolveEdge(id, taskById, factsByTaskId) {
2824
+ const target = taskById.get(id);
2825
+ if (target) {
2826
+ const facts = target.issue !== null ? factsByTaskId.get(target.id) : undefined;
2827
+ return { issue: target.issue, merged: facts?.prState === "merged", open: facts?.prState === "open" };
2828
+ }
2829
+ const direct = id.match(/^#(\d+)$/);
2830
+ return { issue: direct ? Number(direct[1]) : null, merged: false, open: false };
2831
+ }
2832
+ async function main() {
2833
+ const branch = currentBranch();
2834
+ const m = branch.match(/^task\/([^/]+)\/(.+)$/);
2835
+ if (!m) {
2836
+ process.exit(0);
2837
+ }
2838
+ const trancheSlug = m[1];
2839
+ const taskId = m[2];
2840
+ const repo = resolveRepo2();
2841
+ if (!repo) {
2842
+ fail("dispatch-gate severity:infra — could not resolve owner/repo.", "Set AEG_REPO=owner/repo, or confirm `git remote get-url origin` resolves to a GitHub URL, then re-run `vinaya check dispatch-readiness`.");
2843
+ }
2844
+ const source = createForgeSource({ owner: repo.owner, repo: repo.repo });
2845
+ let tranche;
2846
+ try {
2847
+ tranche = await source.getTranche(trancheSlug);
2848
+ } catch (err) {
2849
+ fail(`dispatch-gate severity:infra — could not derive tranche "${trancheSlug}" from the forge: ${err.message}`, "Confirm `gh auth status` passes and the tranche has a Milestone + labeled Issues on the forge, then re-run this check.");
2850
+ }
2851
+ const task = tranche.tasks.find((t) => t.id === taskId);
2852
+ if (!task) {
2853
+ fail(`dispatch-gate row-existence: task "${taskId}" is not present in tranche "${trancheSlug}"'s forge-derived task list.`, "Confirm the branch name matches a real, forge-registered task id, or wait for the Planner to open the task Issue before re-running.");
2854
+ }
2855
+ const taskRefs = tranche.tasks.map((t) => ({ id: t.id, issue: t.issue }));
2856
+ const snapshot = await fetchForgeFacts({
2857
+ owner: repo.owner,
2858
+ repo: repo.repo,
2859
+ tranche: trancheSlug,
2860
+ tasks: taskRefs
2861
+ });
2862
+ const token = await resolveToken() ?? "";
2863
+ const openIssuesBySlug = await fetchOpenIssuesByLabel([trancheSlug], repo.owner, repo.repo, token);
2864
+ const openIssues = openIssuesBySlug.get(trancheSlug) ?? [];
2865
+ const issueFacts = task.issue !== null ? snapshot.facts.get(task.id) : undefined;
2866
+ const issue = task.issue !== null && issueFacts ? { number: task.issue, state: issueFacts.issueState === "closed" ? "closed" : "open" } : null;
2867
+ const openIssueMatch = task.issue !== null ? openIssues.find((i) => i.number === task.issue) : undefined;
2868
+ const issueRationalePass = openIssueMatch ? checkIssueRationale(openIssueMatch.body).status !== "fail" : true;
2869
+ const taskById = new Map(tranche.tasks.map((t) => [t.id, t]));
2870
+ const factsByTaskId = snapshot.facts;
2871
+ const dependsOn = task.dependsOn.map((dep) => {
2872
+ const r = resolveEdge(dep, taskById, factsByTaskId);
2873
+ return { id: dep, issue: r.issue, merged: r.merged };
2874
+ });
2875
+ const conflictsWith = task.conflictsWith.map((c) => {
2876
+ const r = resolveEdge(c, taskById, factsByTaskId);
2877
+ return { id: c, issue: r.issue, openOrInFlight: r.open };
2878
+ });
2879
+ const priorTrancheArchival = [];
2880
+ const input = {
2881
+ trancheSlug,
2882
+ task,
2883
+ issue,
2884
+ issueRationalePass,
2885
+ dependsOn,
2886
+ conflictsWith,
2887
+ priorTask: null,
2888
+ priorTrancheArchival
2889
+ };
2890
+ const result = checkDispatchReadiness(input);
2891
+ if (!result.ready) {
2892
+ for (const blocker of result.blockers) {
2893
+ emitCheckError({
2894
+ schema: CHECK_SCHEMA_VERSION,
2895
+ check: CHECK_NAME,
2896
+ severity: "error",
2897
+ message: blocker,
2898
+ agent_recovery_prompt: recoveryPromptFor(blocker)
2899
+ });
2900
+ }
2901
+ process.exit(1);
2902
+ }
2903
+ process.exit(0);
2904
+ }
2905
+ function recoveryPromptFor(blocker) {
2906
+ if (blocker.startsWith("dispatch-gate issue-existence:")) {
2907
+ return "This task has no resolvable Issue yet. Wait for the Planner to cut the Issue (or fix the phantom reference in the topology), then re-run `vinaya check dispatch-readiness`.";
2908
+ }
2909
+ if (blocker.startsWith("dispatch-gate rationale:")) {
2910
+ return "The task's Issue fails the rationale gate. Ask the Planner to complete the eight-field rationale on the Issue body, then re-run `vinaya check dispatch-readiness`.";
2911
+ }
2912
+ if (blocker.startsWith("dispatch-gate depends-on:")) {
2913
+ return "A declared dependency is not merged yet. Do not start this task — wait for the named dependency PR to merge, then re-run `vinaya check dispatch-readiness`.";
2914
+ }
2915
+ if (blocker.startsWith("dispatch-gate conflicts-with:")) {
2916
+ return "A declared conflicting task has an open or in-flight PR. Wait for it to merge before continuing, then re-run `vinaya check dispatch-readiness`.";
2917
+ }
2918
+ if (blocker.startsWith("dispatch-gate prior-tranche-archival:")) {
2919
+ return "This project's previous tranche is not archived. Ask the Tranche Archivist to run first, then re-run `vinaya check dispatch-readiness`.";
2920
+ }
2921
+ return "Resolve the named dispatch blocker before continuing work on this task, then re-run `vinaya check dispatch-readiness`.";
2922
+ }
2923
+ main();