@attalabs/vinaya 0.24.1 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -3
- package/aeg-root/contracts/architect-planner.md +5 -5
- package/aeg-root/contracts/developer-reviewer.md +10 -7
- package/aeg-root/contracts/planner-developer.md +142 -0
- package/aeg-root/contracts/reviewer-archivist.md +2 -2
- package/aeg-root/contracts/tranche-archivist-planner.md +12 -11
- package/aeg-root/enforcement.md +25 -11
- package/aeg-root/process.md +53 -75
- package/aeg-root/roles/archivist.md +3 -3
- package/aeg-root/roles/developer.md +30 -24
- package/aeg-root/roles/planner.md +100 -21
- package/aeg-root/roles/principal.md +14 -11
- package/aeg-root/roles/reviewer.md +33 -17
- package/aeg-root/roles/security.md +28 -13
- package/aeg-root/roles/tranche-archivist.md +2 -2
- package/aeg-root/skills/aeg/SKILL.md +5 -5
- package/aeg-root/skills/aeg-roles/SKILL.md +7 -7
- package/aeg-root/state-machine.md +36 -35
- package/aeg-root/task-model.md +3 -3
- package/aeg-root/templates/brief-template.md +22 -6
- package/aeg-root/templates/issue-rationale-template.md +35 -2
- package/aeg-root/templates/pr-report-template.md +5 -13
- package/aeg-root/tranche-model.md +21 -21
- package/dist/checks/bin/check-body-bare-digits.js +684 -142
- package/dist/checks/bin/check-branch-topology.js +741 -88
- package/dist/checks/bin/check-brief-shape.js +708 -83
- package/dist/checks/bin/check-changeset-coverage.js +1445 -149
- package/dist/checks/bin/check-closes-n.js +741 -88
- package/dist/checks/bin/check-coherence.js +808 -149
- package/dist/checks/bin/check-dead-branch-push.js +617 -81
- package/dist/checks/bin/check-dispatch-readiness.js +811 -152
- package/dist/checks/bin/check-doc-coverage-push.js +1445 -149
- package/dist/checks/bin/check-doc-coverage.js +1447 -151
- package/dist/checks/bin/check-doctrine-no-procedures.js +684 -142
- package/dist/checks/bin/check-doctrine-portability.js +1445 -149
- package/dist/checks/bin/check-evidence-fresh.js +684 -142
- package/dist/checks/bin/check-exec-bits.js +1445 -149
- package/dist/checks/bin/check-first-push-dispatch.js +808 -149
- package/dist/checks/bin/check-issue-assignment.js +741 -88
- package/dist/checks/bin/check-main-branch-refusal.js +618 -82
- package/dist/checks/bin/check-no-disk-state.js +617 -81
- package/dist/checks/bin/check-pr-premise-reassert.js +5401 -0
- package/dist/checks/bin/check-pr-report-density.js +617 -81
- package/dist/checks/bin/check-quoted-command.js +1443 -147
- package/dist/checks/bin/check-reader-resolvable-prose.js +1481 -153
- package/dist/checks/bin/check-registry-gates.js +659 -85
- package/dist/checks/bin/check-retired-vocabulary.js +1443 -147
- package/dist/checks/bin/check-review-gate.js +758 -145
- package/dist/checks/bin/check-single-plan-pr.js +617 -81
- package/dist/checks/bin/check-surface-scope.js +5722 -0
- package/dist/checks/bin/check-test-plan.js +617 -81
- package/dist/checks/bin/check-token-collection-wired.js +617 -81
- package/dist/checks/bin/check-token-report.js +629 -88
- package/dist/checks/bin/check-workspace-escape.js +1443 -147
- package/dist/index.js +8548 -4335
- package/package.json +1 -1
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-render.ts +2 -2
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +26 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/api/coherence/route.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/backlog/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/[name]/tranches/[slug]/tasks/[taskId]/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/projects/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page.js.nft.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/studio/tranches/page_client-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/1q96_modules_@clerk_nextjs_dist_esm_app-router_client_keyless-creator-reader_0lom2js.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0053k9k._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0112h-k._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0o771t1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__1pndh3_._.js → [root-of-the-server]__0p8q38b._.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0puovz5._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_03x_w6q._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0gvm3og._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0lwxg63._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0wxycau._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1n0cnq-._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/node_modules_1vo08dj._.js +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_topbar_index_tsx_1h1gs1y._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/middleware-build-manifest.js +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/pages/500.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/server-reference-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3m1kgax7j2vgs.js → 0bn8c8v5q429o.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0doqwpd81sjzn.js → 0keji7wvbe1d1.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1maoxbrl3pv5d.css +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1uogpj5w2n5ju.js → 3-6a3tinbdb-7.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0zebsmmk1bvnb.js → 3_9xytdmiv1xi.js} +4 -4
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- package/aeg-root/contracts/brief-developer.md +0 -141
- package/aeg-root/contracts/planner-brief.md +0 -130
- package/aeg-root/roles/brief-author.md +0 -114
- package/aeg-root/skills/brief-authoring/SKILL.md +0 -509
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3qc67qhcvbg0o.css +0 -1
- /package/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/vinaya/studio-standalone/_node_modules/@attalabs/{aeg-core → vinaya/studio-standalone/_node_modules/@attalabs/aeg-core}/bin/verify-coherence.ts +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{QY2GhiI47-765D9DHGy1I → DMofV2-9FrHE7100GfrpI}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{QY2GhiI47-765D9DHGy1I → DMofV2-9FrHE7100GfrpI}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{QY2GhiI47-765D9DHGy1I → DMofV2-9FrHE7100GfrpI}/_ssgManifest.js +0 -0
|
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
5
|
// src/checks/bin/check-brief-shape.ts
|
|
6
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
6
7
|
import { readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
7
8
|
// ../../packages/aeg-core/src/gate-audience.ts
|
|
8
9
|
var GATE_AUDIENCE = {
|
|
@@ -27,6 +28,15 @@ var GATE_AUDIENCE = {
|
|
|
27
28
|
internal: "A composite that shells this repo's own `typecheck`/`lint`/`test`/`build` scripts by name. Those names are this monorepo's toolchain (bun + turbo + biome), not an adopter's; the shipped equivalent of its intent is `vinaya check --all`, which is portable by construction."
|
|
28
29
|
}
|
|
29
30
|
};
|
|
31
|
+
var NON_GATE_BINS = [
|
|
32
|
+
"archive-task",
|
|
33
|
+
"assign-task-issue",
|
|
34
|
+
"dead-branch-audit",
|
|
35
|
+
"eval-agent-compliance",
|
|
36
|
+
"open-issue",
|
|
37
|
+
"open-pr",
|
|
38
|
+
"report-tokens"
|
|
39
|
+
];
|
|
30
40
|
function isShipped(a) {
|
|
31
41
|
return "shippedAs" in a;
|
|
32
42
|
}
|
|
@@ -387,7 +397,7 @@ var LABELS = [
|
|
|
387
397
|
id: "vinaya/needs:brief-correction",
|
|
388
398
|
category: "needs",
|
|
389
399
|
form: "literal",
|
|
390
|
-
carries: "Waiting on the
|
|
400
|
+
carries: "Waiting on the Planner — the brief contradicts the surface it describes."
|
|
391
401
|
},
|
|
392
402
|
{
|
|
393
403
|
key: "waiver-docs",
|
|
@@ -1938,13 +1948,18 @@ function parseInlineFieldList(section) {
|
|
|
1938
1948
|
}
|
|
1939
1949
|
// ../../packages/aeg-core/src/verdict-extraction.ts
|
|
1940
1950
|
var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
|
|
1941
|
-
|
|
1951
|
+
var OBJECTIVES_VERSION_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Objectives version:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
|
|
1952
|
+
function firstFiveLines(comment) {
|
|
1942
1953
|
return comment.split(`
|
|
1943
|
-
`).slice(0,
|
|
1954
|
+
`).slice(0, 5).join(`
|
|
1944
1955
|
`);
|
|
1945
1956
|
}
|
|
1946
1957
|
function extractHeadSha(comment) {
|
|
1947
|
-
const m =
|
|
1958
|
+
const m = firstFiveLines(comment).match(HEAD_SHA_PATTERN);
|
|
1959
|
+
return m ? m[1].toLowerCase() : null;
|
|
1960
|
+
}
|
|
1961
|
+
function extractObjectivesVersion(comment) {
|
|
1962
|
+
const m = firstFiveLines(comment).match(OBJECTIVES_VERSION_PATTERN);
|
|
1948
1963
|
return m ? m[1].toLowerCase() : null;
|
|
1949
1964
|
}
|
|
1950
1965
|
function extractVerdict(comments, valuePattern, missingLabel) {
|
|
@@ -1953,21 +1968,24 @@ function extractVerdict(comments, valuePattern, missingLabel) {
|
|
|
1953
1968
|
return {
|
|
1954
1969
|
value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
|
|
1955
1970
|
headSha: null,
|
|
1971
|
+
objectivesVersion: null,
|
|
1956
1972
|
danglingNote: `no ${missingLabel} verdict comment found on this PR`
|
|
1957
1973
|
};
|
|
1958
1974
|
}
|
|
1959
1975
|
const latest = candidates[candidates.length - 1];
|
|
1960
|
-
const m =
|
|
1976
|
+
const m = firstFiveLines(latest).match(valuePattern);
|
|
1961
1977
|
if (!m) {
|
|
1962
1978
|
return {
|
|
1963
|
-
value: `the most recent ${missingLabel} comment's VERDICT line is not within its first
|
|
1979
|
+
value: `the most recent ${missingLabel} comment's VERDICT line is not within its first five lines — DANGLING, see below`,
|
|
1964
1980
|
headSha: null,
|
|
1965
|
-
|
|
1981
|
+
objectivesVersion: null,
|
|
1982
|
+
danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
|
|
1966
1983
|
};
|
|
1967
1984
|
}
|
|
1968
1985
|
return {
|
|
1969
1986
|
value: m[1].toUpperCase().replace(/[_-]/g, " "),
|
|
1970
1987
|
headSha: extractHeadSha(latest),
|
|
1988
|
+
objectivesVersion: extractObjectivesVersion(latest),
|
|
1971
1989
|
danglingNote: null
|
|
1972
1990
|
};
|
|
1973
1991
|
}
|
|
@@ -2324,8 +2342,105 @@ function overrideActive(opts) {
|
|
|
2324
2342
|
return true;
|
|
2325
2343
|
return false;
|
|
2326
2344
|
}
|
|
2327
|
-
// ../../packages/aeg-core/src/
|
|
2345
|
+
// ../../packages/aeg-core/src/objectives.ts
|
|
2328
2346
|
import { createHash as createHash2 } from "node:crypto";
|
|
2347
|
+
var HEADING_RE = /^##[ \t]*Objectives[ \t]*$/im;
|
|
2348
|
+
var NEXT_HEADING_RE = /^##[ \t]/m;
|
|
2349
|
+
var OBJECTIVE_LINE_RE = /^O(\d+)\.[ \t]*(.*)$/;
|
|
2350
|
+
function hasBacktickedPath(text) {
|
|
2351
|
+
let i = 0;
|
|
2352
|
+
while (i < text.length) {
|
|
2353
|
+
const start = text.indexOf("`", i);
|
|
2354
|
+
if (start === -1)
|
|
2355
|
+
return false;
|
|
2356
|
+
const end = text.indexOf("`", start + 1);
|
|
2357
|
+
if (end === -1)
|
|
2358
|
+
return false;
|
|
2359
|
+
if (text.slice(start + 1, end).includes("/"))
|
|
2360
|
+
return true;
|
|
2361
|
+
i = end + 1;
|
|
2362
|
+
}
|
|
2363
|
+
return false;
|
|
2364
|
+
}
|
|
2365
|
+
var MIN_WORDS_OUTSIDE_BACKTICKS = 3;
|
|
2366
|
+
function stripObjectiveBackticks(text) {
|
|
2367
|
+
return text.replace(/`[^`\n]*`/g, " ");
|
|
2368
|
+
}
|
|
2369
|
+
function wordCount(text) {
|
|
2370
|
+
return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
|
|
2371
|
+
}
|
|
2372
|
+
function objectivesSectionText(body) {
|
|
2373
|
+
const heading = HEADING_RE.exec(body);
|
|
2374
|
+
if (!heading)
|
|
2375
|
+
return null;
|
|
2376
|
+
const afterHeading = body.slice(heading.index + heading[0].length);
|
|
2377
|
+
const next = NEXT_HEADING_RE.exec(afterHeading);
|
|
2378
|
+
return next ? afterHeading.slice(0, next.index) : afterHeading;
|
|
2379
|
+
}
|
|
2380
|
+
function hasObjectivesHeading(body) {
|
|
2381
|
+
return HEADING_RE.test(body);
|
|
2382
|
+
}
|
|
2383
|
+
function objectivesOf(body) {
|
|
2384
|
+
const section = objectivesSectionText(body);
|
|
2385
|
+
if (section === null) {
|
|
2386
|
+
return { ok: false, errors: ["no `## Objectives` heading found in the body."] };
|
|
2387
|
+
}
|
|
2388
|
+
const lines = section.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
2389
|
+
if (lines.length === 0) {
|
|
2390
|
+
return { ok: false, errors: ["the `## Objectives` section has no `O<n>.` lines."] };
|
|
2391
|
+
}
|
|
2392
|
+
const objectives = [];
|
|
2393
|
+
const errors = [];
|
|
2394
|
+
for (const line of lines) {
|
|
2395
|
+
const m = OBJECTIVE_LINE_RE.exec(line);
|
|
2396
|
+
if (!m) {
|
|
2397
|
+
errors.push(`"${line}" is not a well-formed objective line — expected \`O<n>. <sentence>\`, e.g. \`O1. …\`.`);
|
|
2398
|
+
continue;
|
|
2399
|
+
}
|
|
2400
|
+
const n = m[1];
|
|
2401
|
+
const text = m[2].trim();
|
|
2402
|
+
if (text.length === 0) {
|
|
2403
|
+
errors.push(`O${n} has no sentence — every objective is one observable sentence.`);
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
if (hasBacktickedPath(text) && wordCount(stripObjectiveBackticks(text)) < MIN_WORDS_OUTSIDE_BACKTICKS) {
|
|
2407
|
+
errors.push(`O${n} is little more than a file path — an objective states an observable outcome, never a bare path (the Planner maps it to files).`);
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
objectives.push({ id: `O${n}`, text });
|
|
2411
|
+
}
|
|
2412
|
+
for (let i = 0;i < objectives.length; i++) {
|
|
2413
|
+
const expected = i + 1;
|
|
2414
|
+
const actual = Number.parseInt(objectives[i].id.slice(1), 10);
|
|
2415
|
+
if (actual !== expected) {
|
|
2416
|
+
errors.push(`numbering is not contiguous from O1 — expected O${expected}, found ${objectives[i].id}.`);
|
|
2417
|
+
break;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
if (errors.length > 0)
|
|
2421
|
+
return { ok: false, errors };
|
|
2422
|
+
return { ok: true, objectives };
|
|
2423
|
+
}
|
|
2424
|
+
function normalizeText(text) {
|
|
2425
|
+
return text.trim().replace(/\s+/g, " ");
|
|
2426
|
+
}
|
|
2427
|
+
function objectivesVersion(objectives) {
|
|
2428
|
+
const normalized = objectives.map((o) => `${o.id} ${normalizeText(o.text)}`).join(`
|
|
2429
|
+
`);
|
|
2430
|
+
return createHash2("sha256").update(normalized).digest("hex");
|
|
2431
|
+
}
|
|
2432
|
+
function isIssueNotFoundError(err) {
|
|
2433
|
+
const stderr = err?.stderr;
|
|
2434
|
+
const haystack = [
|
|
2435
|
+
err?.message ?? "",
|
|
2436
|
+
typeof stderr === "string" ? stderr : stderr?.toString() ?? ""
|
|
2437
|
+
].join(`
|
|
2438
|
+
`);
|
|
2439
|
+
return /could not resolve to an (?:issue|pull request)|\b404\b|not found/i.test(haystack);
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
// ../../packages/aeg-core/src/premise-check.ts
|
|
2443
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
2329
2444
|
var ASSERTION_KINDS = new Set(["contains", "absent", "sha256"]);
|
|
2330
2445
|
function isPremiseHeader(line) {
|
|
2331
2446
|
const stripped = line.replace(/[*#]/g, "").trim();
|
|
@@ -2396,7 +2511,7 @@ function checkPremises(assertions, fileReader) {
|
|
|
2396
2511
|
} else if (a.kind === "absent" && content.includes(a.value)) {
|
|
2397
2512
|
failures.push(`premise-check: ${a.path} now contains "${a.value}", but the brief pinned it absent — the premise has moved; re-dig before proceeding.`);
|
|
2398
2513
|
} else if (a.kind === "sha256") {
|
|
2399
|
-
const actual =
|
|
2514
|
+
const actual = createHash3("sha256").update(content).digest("hex");
|
|
2400
2515
|
if (actual !== a.value.toLowerCase()) {
|
|
2401
2516
|
failures.push(`premise-check: ${a.path} sha256 mismatch (brief pinned ${a.value}, file is now ${actual}) — the file changed since this brief was authored; re-dig before proceeding.`);
|
|
2402
2517
|
}
|
|
@@ -2501,18 +2616,14 @@ function checkTestPlanExclusivity(prBody) {
|
|
|
2501
2616
|
function checkPrincipalPlaceholder(prBody) {
|
|
2502
2617
|
const region = testPlanRegion(prBody);
|
|
2503
2618
|
const lineRe = /^-\s*\[[ xX]\]\s*\*{2}\[principal\]\*{2}(.*)$/gim;
|
|
2619
|
+
const errors = [];
|
|
2504
2620
|
for (const m of region.matchAll(lineRe)) {
|
|
2505
2621
|
const content = m[1] ?? "";
|
|
2506
2622
|
if (/^\s*None\b/i.test(content)) {
|
|
2507
|
-
|
|
2508
|
-
status: "fail",
|
|
2509
|
-
errors: [
|
|
2510
|
-
'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.'
|
|
2511
|
-
]
|
|
2512
|
-
};
|
|
2623
|
+
errors.push(`brief-validation Test Plan shape: a \`**[principal]**\` checkbox item is a "None" placeholder ("${content.trim()}") — if there is no principal-runnable surface, omit the item entirely; an untickable placeholder box blocks the merge gate forever.`);
|
|
2513
2624
|
}
|
|
2514
2625
|
}
|
|
2515
|
-
return { status: "pass", errors: [] };
|
|
2626
|
+
return errors.length > 0 ? { status: "fail", errors } : { status: "pass", errors: [] };
|
|
2516
2627
|
}
|
|
2517
2628
|
function checkSurfaceMap(prBody) {
|
|
2518
2629
|
return headingCheck(prBody, "(?:technical\\s+)?surface map", "Technical surface map");
|
|
@@ -2757,8 +2868,55 @@ function checkDefeatCases(prBody) {
|
|
|
2757
2868
|
]
|
|
2758
2869
|
};
|
|
2759
2870
|
}
|
|
2871
|
+
var PART_CITATION_RE = /Part\s+\d+\s*\(([^)]*)\)/gi;
|
|
2872
|
+
var OBJECTIVE_REF_RE = /O(\d+)/g;
|
|
2873
|
+
function citedObjectiveIds(section6) {
|
|
2874
|
+
const ids = new Set;
|
|
2875
|
+
for (const m of section6.matchAll(PART_CITATION_RE)) {
|
|
2876
|
+
for (const r of m[1].matchAll(OBJECTIVE_REF_RE))
|
|
2877
|
+
ids.add(Number.parseInt(r[1], 10));
|
|
2878
|
+
}
|
|
2879
|
+
return ids;
|
|
2880
|
+
}
|
|
2881
|
+
function checkObjectivesCopy(prBody, issueObjectives) {
|
|
2882
|
+
const parsed = objectivesOf(prBody);
|
|
2883
|
+
if (!parsed.ok) {
|
|
2884
|
+
return { status: "fail", errors: parsed.errors.map((e) => `brief-validation objectives copy: ${e}`) };
|
|
2885
|
+
}
|
|
2886
|
+
if (objectivesVersion(parsed.objectives) !== objectivesVersion(issueObjectives)) {
|
|
2887
|
+
return {
|
|
2888
|
+
status: "fail",
|
|
2889
|
+
errors: [
|
|
2890
|
+
"brief-validation objectives copy: the brief's `## Objectives` section does not match the Issue's — copy the Issue's `## Objectives` section (aeg-root/roles/developer.md)."
|
|
2891
|
+
]
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
return { status: "pass", errors: [] };
|
|
2895
|
+
}
|
|
2896
|
+
function checkObjectivesCoverage(prBody) {
|
|
2897
|
+
const parsed = objectivesOf(prBody);
|
|
2898
|
+
if (!parsed.ok) {
|
|
2899
|
+
return { status: "fail", errors: parsed.errors.map((e) => `brief-validation objectives coverage: ${e}`) };
|
|
2900
|
+
}
|
|
2901
|
+
const declaredIds = new Set(parsed.objectives.map((o) => Number.parseInt(o.id.slice(1), 10)));
|
|
2902
|
+
const section6 = extractNumberedSection(prBody, 6) ?? "";
|
|
2903
|
+
const cited = citedObjectiveIds(section6);
|
|
2904
|
+
const maxDeclared = Math.max(...declaredIds, 0);
|
|
2905
|
+
const errors = [];
|
|
2906
|
+
for (const id of declaredIds) {
|
|
2907
|
+
if (!cited.has(id)) {
|
|
2908
|
+
errors.push(`brief-validation objectives coverage: O${id} is not cited by any Part in §6 — every objective must be cited by at least one Part.`);
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
for (const id of cited) {
|
|
2912
|
+
if (!declaredIds.has(id)) {
|
|
2913
|
+
errors.push(`brief-validation objectives coverage: a Part in §6 cites O${id}, but the Objectives section ends at O${maxDeclared}.`);
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
return errors.length === 0 ? { status: "pass", errors: [] } : { status: "fail", errors };
|
|
2917
|
+
}
|
|
2760
2918
|
function checkBriefSections(prBody, readTier, options = {}) {
|
|
2761
|
-
const { requireClosesN = true, consumersOf = () => [] } = options;
|
|
2919
|
+
const { requireClosesN = true, consumersOf = () => [], issueObjectives } = options;
|
|
2762
2920
|
const results = [
|
|
2763
2921
|
checkTierField(prBody, readTier),
|
|
2764
2922
|
checkTestPlan(prBody),
|
|
@@ -2776,10 +2934,23 @@ function checkBriefSections(prBody, readTier, options = {}) {
|
|
|
2776
2934
|
checkCommandsCarryOutput(prBody),
|
|
2777
2935
|
checkConsumerTests(prBody, consumersOf),
|
|
2778
2936
|
checkDefeatCases(prBody),
|
|
2937
|
+
...issueObjectives !== undefined ? [checkObjectivesCopy(prBody, issueObjectives), checkObjectivesCoverage(prBody)] : [],
|
|
2779
2938
|
...requireClosesN ? [checkClosesN(prBody)] : []
|
|
2780
2939
|
];
|
|
2781
2940
|
return { errors: results.flatMap((r) => r.errors) };
|
|
2782
2941
|
}
|
|
2942
|
+
var AEG_BRIEF_V1_MARKER = "<!-- aeg:brief:v1 -->";
|
|
2943
|
+
function contentAfterTwoLines(body) {
|
|
2944
|
+
const first = body.indexOf(`
|
|
2945
|
+
`);
|
|
2946
|
+
if (first === -1)
|
|
2947
|
+
return "";
|
|
2948
|
+
const second = body.indexOf(`
|
|
2949
|
+
`, first + 1);
|
|
2950
|
+
if (second === -1)
|
|
2951
|
+
return "";
|
|
2952
|
+
return body.slice(second + 1);
|
|
2953
|
+
}
|
|
2783
2954
|
// ../../packages/aeg-core/src/doctrine-portability.ts
|
|
2784
2955
|
var DEFAULT_SHIPS_PREFIX = "aeg-root/";
|
|
2785
2956
|
var STATIC_PORTABLE_PREFIXES = [
|
|
@@ -2974,14 +3145,14 @@ function checkSectionDensity(prBody, heading) {
|
|
|
2974
3145
|
]
|
|
2975
3146
|
};
|
|
2976
3147
|
}
|
|
2977
|
-
function
|
|
2978
|
-
return checkSectionDensity(prBody, "
|
|
3148
|
+
function checkDecisionsDensity(prBody) {
|
|
3149
|
+
return checkSectionDensity(prBody, "Decisions");
|
|
2979
3150
|
}
|
|
2980
3151
|
function checkScopeDensity(prBody) {
|
|
2981
3152
|
return checkSectionDensity(prBody, "Scope");
|
|
2982
3153
|
}
|
|
2983
3154
|
function checkPrReportDensity(prBody) {
|
|
2984
|
-
const results = [
|
|
3155
|
+
const results = [checkDecisionsDensity(prBody), checkScopeDensity(prBody)];
|
|
2985
3156
|
return { errors: results.flatMap((r) => r.errors) };
|
|
2986
3157
|
}
|
|
2987
3158
|
// ../../packages/aeg-core/src/issue-validation.ts
|
|
@@ -3013,6 +3184,75 @@ function checkIssueRationale(body) {
|
|
|
3013
3184
|
}
|
|
3014
3185
|
return { status: errors.length > 0 ? "fail" : "pass", errors };
|
|
3015
3186
|
}
|
|
3187
|
+
var OBJECTIVES_SINCE_ISSUE = 404;
|
|
3188
|
+
function checkIssueObjectives(body, issueNumber) {
|
|
3189
|
+
if (issueNumber !== null && issueNumber < OBJECTIVES_SINCE_ISSUE)
|
|
3190
|
+
return { status: "pass", errors: [] };
|
|
3191
|
+
const result = objectivesOf(body);
|
|
3192
|
+
if (result.ok)
|
|
3193
|
+
return { status: "pass", errors: [] };
|
|
3194
|
+
return { status: "fail", errors: result.errors.map((e) => `issue-validation objectives: ${e}`) };
|
|
3195
|
+
}
|
|
3196
|
+
function topLevelSectionText(body, headingName) {
|
|
3197
|
+
const headingRe = new RegExp(`^##[ \\t]*${headingName}[ \\t]*$`, "im");
|
|
3198
|
+
const heading = headingRe.exec(body);
|
|
3199
|
+
if (!heading)
|
|
3200
|
+
return null;
|
|
3201
|
+
const afterHeading = body.slice(heading.index + heading[0].length);
|
|
3202
|
+
const next = /^##[ \t]/m.exec(afterHeading);
|
|
3203
|
+
return next ? afterHeading.slice(0, next.index) : afterHeading;
|
|
3204
|
+
}
|
|
3205
|
+
function looksLikeFilePath(entry2) {
|
|
3206
|
+
const stripped = entry2.replace(/\/\*\*?$/, "");
|
|
3207
|
+
const lastSegment = stripped.split("/").pop() ?? stripped;
|
|
3208
|
+
const extMatch = /\.[A-Za-z0-9]{1,6}$/.exec(lastSegment);
|
|
3209
|
+
return extMatch !== null && extMatch.index > 0;
|
|
3210
|
+
}
|
|
3211
|
+
function splitGlobList(raw) {
|
|
3212
|
+
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3213
|
+
}
|
|
3214
|
+
function parseIssueSurface(body) {
|
|
3215
|
+
const section = topLevelSectionText(body, "Surface");
|
|
3216
|
+
if (section === null)
|
|
3217
|
+
return { ok: false, errors: ["no `## Surface` heading found in the body."] };
|
|
3218
|
+
const inLine = /^in:\s*(.+)$/im.exec(section);
|
|
3219
|
+
const outLine = /^out:\s*(.+)$/im.exec(section);
|
|
3220
|
+
const errors = [];
|
|
3221
|
+
if (!inLine)
|
|
3222
|
+
errors.push("`## Surface` has no `in:` line.");
|
|
3223
|
+
if (!outLine)
|
|
3224
|
+
errors.push("`## Surface` has no `out:` line.");
|
|
3225
|
+
if (errors.length > 0)
|
|
3226
|
+
return { ok: false, errors };
|
|
3227
|
+
const inGlobs = splitGlobList(inLine[1]);
|
|
3228
|
+
const outGlobs = splitGlobList(outLine[1]);
|
|
3229
|
+
for (const g of [...inGlobs, ...outGlobs]) {
|
|
3230
|
+
if (looksLikeFilePath(g)) {
|
|
3231
|
+
errors.push(`\`${g}\` in \`## Surface\` looks like a file path — Surface entries are directory-level globs, never file paths.`);
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
if (inGlobs.length === 0)
|
|
3235
|
+
errors.push("`## Surface`'s `in:` line resolved to zero globs.");
|
|
3236
|
+
if (errors.length > 0)
|
|
3237
|
+
return { ok: false, errors };
|
|
3238
|
+
return { ok: true, value: { in: inGlobs, out: outGlobs } };
|
|
3239
|
+
}
|
|
3240
|
+
function globCoversPath(glob, path) {
|
|
3241
|
+
const g = glob.replace(/\/\*\*?$/, "").replace(/\/+$/, "");
|
|
3242
|
+
const p = path.replace(/\/+$/, "");
|
|
3243
|
+
return g === p || g.startsWith(`${p}/`) || p.startsWith(`${g}/`);
|
|
3244
|
+
}
|
|
3245
|
+
function checkSurfaceScope(changedFiles, outGlobs) {
|
|
3246
|
+
if (outGlobs.length === 0)
|
|
3247
|
+
return { ok: true };
|
|
3248
|
+
const violations = [];
|
|
3249
|
+
for (const file of changedFiles) {
|
|
3250
|
+
const glob = outGlobs.find((g) => globCoversPath(g, file));
|
|
3251
|
+
if (glob)
|
|
3252
|
+
violations.push({ file, glob });
|
|
3253
|
+
}
|
|
3254
|
+
return violations.length > 0 ? { ok: false, violations } : { ok: true };
|
|
3255
|
+
}
|
|
3016
3256
|
function isTaskIssueLabelSet(labels) {
|
|
3017
3257
|
return hasLabel("tranche", labels);
|
|
3018
3258
|
}
|
|
@@ -3077,6 +3317,8 @@ function checkProjectsRegistered(body, _labels, registeredNames) {
|
|
|
3077
3317
|
}
|
|
3078
3318
|
return errors.length > 0 ? { status: "fail", errors } : { status: "pass", errors: [] };
|
|
3079
3319
|
}
|
|
3320
|
+
var DOC_PATH_RE = /(?:(?:aeg-root|apps|packages|specs|docs|tools|\.claude|\.github)\/[\w./@-]*\.(?:md|mdx)|\.claude\/(?:skills|rules)\/[\w./-]+|\b(?:docs-index|decisions|projects|state-machine|enforcement|process|README|CLAUDE)\.md\b|\b[\w-]+-(?:spec|decisions|backlog)\.md\b)/i;
|
|
3321
|
+
var DOC_PATH_RE_GLOBAL = new RegExp(DOC_PATH_RE.source, "gi");
|
|
3080
3322
|
|
|
3081
3323
|
// ../../packages/aeg-core/src/coherence-checks.ts
|
|
3082
3324
|
var COHERENCE_ENFORCED_FROM = "2026-07-01";
|
|
@@ -3274,7 +3516,8 @@ function checkR1(issuesBySlug, grandfatheredIssues, registeredNames = []) {
|
|
|
3274
3516
|
continue;
|
|
3275
3517
|
const errors = [
|
|
3276
3518
|
...checkIssueRationale(issue.body).errors,
|
|
3277
|
-
...checkProjectsRegistered(issue.body, issue.labels, registeredNames).errors
|
|
3519
|
+
...checkProjectsRegistered(issue.body, issue.labels, registeredNames).errors,
|
|
3520
|
+
...checkIssueObjectives(issue.body, issue.number).errors
|
|
3278
3521
|
];
|
|
3279
3522
|
if (errors.length === 0)
|
|
3280
3523
|
continue;
|
|
@@ -3389,6 +3632,27 @@ function checkClosesN2(branch, prBody, trancheFiles, taskIssueRefs) {
|
|
|
3389
3632
|
}
|
|
3390
3633
|
return { ok: true, expectedIssue };
|
|
3391
3634
|
}
|
|
3635
|
+
// ../../packages/aeg-core/src/archive-task.ts
|
|
3636
|
+
function closesRefs(text) {
|
|
3637
|
+
return [...new Set([...text.matchAll(/Closes #(\d+)/gi)].map((m) => Number(m[1])))];
|
|
3638
|
+
}
|
|
3639
|
+
function extractIssue(body) {
|
|
3640
|
+
const stripped = stripCode(body);
|
|
3641
|
+
const bodyNums = closesRefs(stripped);
|
|
3642
|
+
const anchored = anchoredRegion(stripped, "CLOSES");
|
|
3643
|
+
if (anchored !== null) {
|
|
3644
|
+
const anchorNums = closesRefs(anchored);
|
|
3645
|
+
if (anchorNums.length === 0)
|
|
3646
|
+
return { issue: null, extraIssues: bodyNums, outsideHeader: false };
|
|
3647
|
+
const issue2 = anchorNums[0];
|
|
3648
|
+
return { issue: issue2, extraIssues: bodyNums.filter((n) => n !== issue2), outsideHeader: false };
|
|
3649
|
+
}
|
|
3650
|
+
const headerNums = closesRefs(headerRegion(stripped));
|
|
3651
|
+
if (bodyNums.length === 0)
|
|
3652
|
+
return { issue: null, extraIssues: [], outsideHeader: false };
|
|
3653
|
+
const issue = headerNums.length > 0 ? headerNums[0] : bodyNums[0];
|
|
3654
|
+
return { issue, extraIssues: bodyNums.filter((n) => n !== issue), outsideHeader: headerNums.length === 0 };
|
|
3655
|
+
}
|
|
3392
3656
|
// ../../packages/aeg-core/src/review-gate.ts
|
|
3393
3657
|
function isBoundToHead(extraction, headSha) {
|
|
3394
3658
|
if (!extraction.headSha)
|
|
@@ -3407,6 +3671,11 @@ function isBoundByPatchIdentity(extraction, headSha, patchIdOf) {
|
|
|
3407
3671
|
function isBoundToPatch(extraction, headSha, patchIdOf) {
|
|
3408
3672
|
return isBoundToHead(extraction, headSha) || isBoundByPatchIdentity(extraction, headSha, patchIdOf);
|
|
3409
3673
|
}
|
|
3674
|
+
function isBoundToObjectives(extraction, currentVersion) {
|
|
3675
|
+
if (currentVersion === null)
|
|
3676
|
+
return true;
|
|
3677
|
+
return extraction.objectivesVersion === currentVersion;
|
|
3678
|
+
}
|
|
3410
3679
|
function checkReviewGate(input) {
|
|
3411
3680
|
const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
|
|
3412
3681
|
const waived = isWaiverLabelActorVerified({
|
|
@@ -3433,7 +3702,9 @@ function checkReviewGate(input) {
|
|
|
3433
3702
|
const securityClean = security.value === "PASS";
|
|
3434
3703
|
const codeReviewBound = isBoundToPatch(codeReview, input.headSha, input.patchIdOf);
|
|
3435
3704
|
const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
|
|
3436
|
-
|
|
3705
|
+
const codeReviewObjectivesBound = isBoundToObjectives(codeReview, input.objectivesVersion);
|
|
3706
|
+
const securityObjectivesBound = isBoundToObjectives(security, input.objectivesVersion);
|
|
3707
|
+
if (codeReviewClean && codeReviewBound && codeReviewObjectivesBound && securityClean && securityBound && securityObjectivesBound && mechanicalChecksClean) {
|
|
3437
3708
|
return {
|
|
3438
3709
|
verdict: "pass",
|
|
3439
3710
|
reason: `code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS, both covering head ${input.headSha}, and every reported mechanical check is green.`,
|
|
@@ -3445,11 +3716,15 @@ function checkReviewGate(input) {
|
|
|
3445
3716
|
problems.push(`code-reviewer verdict is not a clean APPROVE (found: ${codeReview.value})`);
|
|
3446
3717
|
} else if (!codeReviewBound) {
|
|
3447
3718
|
problems.push(`the newest code-review verdict covers ${codeReview.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
|
|
3719
|
+
} else if (!codeReviewObjectivesBound) {
|
|
3720
|
+
problems.push(`the newest code-review verdict was cast against objectives version ${codeReview.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
|
|
3448
3721
|
}
|
|
3449
3722
|
if (!securityClean) {
|
|
3450
3723
|
problems.push(`security-review verdict is not a clean PASS (found: ${security.value})`);
|
|
3451
3724
|
} else if (!securityBound) {
|
|
3452
3725
|
problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
|
|
3726
|
+
} else if (!securityObjectivesBound) {
|
|
3727
|
+
problems.push(`the newest security-review verdict was cast against objectives version ${security.objectivesVersion ?? "none"}, the Issue's list is now ${input.objectivesVersion}`);
|
|
3453
3728
|
}
|
|
3454
3729
|
if (!mechanicalChecksClean) {
|
|
3455
3730
|
problems.push(reportedMechanicalChecks.length === 0 ? "no mechanical checks have reported for this head yet" : `mechanical check(s) not green: ${reportedMechanicalChecks.filter((c) => c.bucket !== "pass").map((c) => `${c.name} (${c.bucket})`).join(", ")}`);
|
|
@@ -3573,16 +3848,33 @@ function checkG1(rows, existsFn) {
|
|
|
3573
3848
|
return { check: "G1", status: findings.length > 0 ? "fail" : "pass", findings };
|
|
3574
3849
|
}
|
|
3575
3850
|
var SCAFFOLD_PLACEHOLDER = "[undocumented — fill in why]";
|
|
3851
|
+
var NON_GATE_BIN_NAMES = NON_GATE_BINS;
|
|
3852
|
+
var TWIN_CANDIDATE_EXCEPTIONS = {
|
|
3853
|
+
"apps/cli/src/checks/bin/check-dead-branch-push.ts": 'Ring 0 "Pushing to a branch whose pull request already resolved" (implementation: check-push-target.ts)',
|
|
3854
|
+
"apps/cli/src/checks/bin/check-issue-assignment.ts": `Ring 0 "A task branch's first push (Issue self-assignment)" (implementation: assign-task-issue.ts)`,
|
|
3855
|
+
"apps/cli/src/checks/bin/check-closes-n.ts": 'Ring 1 "Closes linkage" (implementation: verify-coherence.ts)'
|
|
3856
|
+
};
|
|
3857
|
+
var NON_GATE_HOOK_SCRIPTS = [".claude/hooks/track-transcript.sh"];
|
|
3576
3858
|
function checkG2(rows, candidateFiles) {
|
|
3577
3859
|
const implementations = new Set(rows.map((r) => r.implementation).filter((p) => p !== ""));
|
|
3860
|
+
const claimedNames = new Set(rows.flatMap((r) => claimedCheckNames(r.implementation)));
|
|
3578
3861
|
const findings = [];
|
|
3579
3862
|
for (const path of candidateFiles) {
|
|
3580
|
-
if (
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3863
|
+
if (implementations.has(path))
|
|
3864
|
+
continue;
|
|
3865
|
+
if (path.startsWith(AEG_CORE_BIN_PATH_PREFIX) && NON_GATE_BIN_NAMES.includes(basenameNoExt(path)))
|
|
3866
|
+
continue;
|
|
3867
|
+
if (NON_GATE_HOOK_SCRIPTS.includes(path))
|
|
3868
|
+
continue;
|
|
3869
|
+
if (path in TWIN_CANDIDATE_EXCEPTIONS)
|
|
3870
|
+
continue;
|
|
3871
|
+
const candidateNames = claimedCheckNames(path);
|
|
3872
|
+
if (candidateNames.length > 0 && candidateNames.some((n) => claimedNames.has(n)))
|
|
3873
|
+
continue;
|
|
3874
|
+
findings.push({
|
|
3875
|
+
path,
|
|
3876
|
+
reason: `"${path}" is not named as the implementation of any row in enforcement.md's ring tables`
|
|
3877
|
+
});
|
|
3586
3878
|
}
|
|
3587
3879
|
for (const row of rows) {
|
|
3588
3880
|
const carriesPlaceholder = [row.summary, row.description, row.spec].some((cell) => cell === SCAFFOLD_PLACEHOLDER);
|
|
@@ -3594,7 +3886,7 @@ function checkG2(rows, candidateFiles) {
|
|
|
3594
3886
|
});
|
|
3595
3887
|
}
|
|
3596
3888
|
}
|
|
3597
|
-
return { check: "G2", status: findings.length > 0 ? "
|
|
3889
|
+
return { check: "G2", status: findings.length > 0 ? "fail" : "pass", findings };
|
|
3598
3890
|
}
|
|
3599
3891
|
function checkG3(ring0Rows, crossingFiles) {
|
|
3600
3892
|
const ring0Implementations = new Set(ring0Rows.map((r) => r.implementation));
|
|
@@ -3685,55 +3977,20 @@ function checkG6(rows, registeredCheckNames) {
|
|
|
3685
3977
|
}
|
|
3686
3978
|
return { check: "G6", status: findings.length > 0 ? "fail" : "pass", findings };
|
|
3687
3979
|
}
|
|
3688
|
-
// ../../packages/aeg-core/src/
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
return {
|
|
3699
|
-
verdict: "refuse",
|
|
3700
|
-
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.`
|
|
3701
|
-
};
|
|
3702
|
-
}
|
|
3703
|
-
if (prState === "OPEN") {
|
|
3704
|
-
return {
|
|
3705
|
-
verdict: "allow",
|
|
3706
|
-
reason: `Branch \`${branch}\` has an OPEN PR (#${prNumber}) — pushing more commits to it is normal.`
|
|
3707
|
-
};
|
|
3708
|
-
}
|
|
3709
|
-
if (prState === "NONE") {
|
|
3710
|
-
return {
|
|
3711
|
-
verdict: "allow",
|
|
3712
|
-
reason: `Branch \`${branch}\` has no PR yet — a brand-new task branch legitimately has none.`
|
|
3713
|
-
};
|
|
3714
|
-
}
|
|
3715
|
-
return {
|
|
3716
|
-
verdict: "allow",
|
|
3717
|
-
reason: `Could not determine \`${branch}\`'s PR state (forge unreachable) — failing open rather than blocking the push on a transient issue.`
|
|
3718
|
-
};
|
|
3719
|
-
}
|
|
3720
|
-
// ../../packages/aeg-core/src/baseline-capture.ts
|
|
3721
|
-
function captureBaseline(counts, capturedAt) {
|
|
3722
|
-
return counts.map((c) => ({ tool: c.tool, findingCount: c.findingCount, capturedAt }));
|
|
3980
|
+
// ../../packages/aeg-core/src/reader-resolvable-prose.ts
|
|
3981
|
+
var PRODUCT_SLUG_SCOPE = [
|
|
3982
|
+
"apps/cli/src",
|
|
3983
|
+
".github/workflows",
|
|
3984
|
+
".vinaya",
|
|
3985
|
+
"apps/cli/README.md",
|
|
3986
|
+
"packages/sources/README.md"
|
|
3987
|
+
];
|
|
3988
|
+
function isUnderOrEqual(path, prefix) {
|
|
3989
|
+
return path === prefix || path.startsWith(`${prefix}/`);
|
|
3723
3990
|
}
|
|
3724
|
-
function
|
|
3725
|
-
|
|
3726
|
-
const baselineEntry = baseline.find((b) => b.tool === c.tool);
|
|
3727
|
-
const baselineCount = baselineEntry ? baselineEntry.findingCount : c.findingCount;
|
|
3728
|
-
return { tool: c.tool, baseline: baselineCount, current: c.findingCount, delta: c.findingCount - baselineCount };
|
|
3729
|
-
});
|
|
3730
|
-
return {
|
|
3731
|
-
withinBudget: perTool.every((t) => t.delta <= 0),
|
|
3732
|
-
delta: perTool.reduce((sum, t) => sum + t.delta, 0),
|
|
3733
|
-
perTool
|
|
3734
|
-
};
|
|
3991
|
+
function isProductScopeFile(path) {
|
|
3992
|
+
return PRODUCT_SLUG_SCOPE.some((prefix) => isUnderOrEqual(path, prefix));
|
|
3735
3993
|
}
|
|
3736
|
-
// ../../packages/aeg-core/src/reader-resolvable-prose.ts
|
|
3737
3994
|
var SHIPS_PREFIX = "aeg-root/";
|
|
3738
3995
|
function shipsArchivePrefix(shipsPrefix) {
|
|
3739
3996
|
return `${shipsPrefix}tranches/completed/`;
|
|
@@ -3753,6 +4010,8 @@ function classifyProseFile(path, readerFacingPrefix, readerFacingSuffix, shipsPr
|
|
|
3753
4010
|
}
|
|
3754
4011
|
if (isSpecFile2(path) || isClaudeMdFile(path))
|
|
3755
4012
|
return "internal";
|
|
4013
|
+
if (isProductScopeFile(path))
|
|
4014
|
+
return "product";
|
|
3756
4015
|
return null;
|
|
3757
4016
|
}
|
|
3758
4017
|
var SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
|
|
@@ -3765,6 +4024,9 @@ function stripNonProse(path, content) {
|
|
|
3765
4024
|
}
|
|
3766
4025
|
return content;
|
|
3767
4026
|
}
|
|
4027
|
+
function stripNonProseForProduct(path, content) {
|
|
4028
|
+
return path.endsWith(".md") ? stripNonProse(path, content) : content;
|
|
4029
|
+
}
|
|
3768
4030
|
function lineAt(content, index) {
|
|
3769
4031
|
let line = 1;
|
|
3770
4032
|
for (let i = 0;i < index; i++) {
|
|
@@ -3789,12 +4051,19 @@ function checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuff
|
|
|
3789
4051
|
{ pattern: TRANCHE_SLUG_VN_PATTERN, what: "an internal tranche slug" },
|
|
3790
4052
|
...legacyPattern ? [{ pattern: legacyPattern, what: "an internal tranche slug", group: 2 }] : []
|
|
3791
4053
|
];
|
|
4054
|
+
const productPatterns = [
|
|
4055
|
+
{ pattern: TRANCHE_SLUG_VN_PATTERN, what: "an internal tranche slug in product code" }
|
|
4056
|
+
];
|
|
3792
4057
|
for (const file of files) {
|
|
3793
4058
|
const cls = classifyProseFile(file.path, readerFacingPrefix, readerFacingSuffix, shipsPrefix);
|
|
3794
|
-
if (!cls ||
|
|
4059
|
+
if (!cls || cls === "internal")
|
|
3795
4060
|
continue;
|
|
3796
|
-
|
|
3797
|
-
|
|
4061
|
+
if (cls !== "product" && !SWEPT_CLASSES.has(cls))
|
|
4062
|
+
continue;
|
|
4063
|
+
const blocking = cls === "product";
|
|
4064
|
+
const scrubbed = blocking ? stripNonProseForProduct(file.path, file.content) : stripNonProse(file.path, file.content);
|
|
4065
|
+
const patternsToRun = blocking ? productPatterns : patterns;
|
|
4066
|
+
for (const { pattern, what, group } of patternsToRun) {
|
|
3798
4067
|
pattern.lastIndex = 0;
|
|
3799
4068
|
let match = pattern.exec(scrubbed);
|
|
3800
4069
|
while (match !== null) {
|
|
@@ -3802,7 +4071,8 @@ function checkUnresolvableReferences(files, readerFacingPrefix, readerFacingSuff
|
|
|
3802
4071
|
findings.push({
|
|
3803
4072
|
file: file.path,
|
|
3804
4073
|
line: lineAt(scrubbed, match.index),
|
|
3805
|
-
message: `references ${what} ("${cited}") a reader outside this repo's tracker cannot resolve
|
|
4074
|
+
message: `references ${what} ("${cited}") a reader outside this repo's tracker cannot resolve`,
|
|
4075
|
+
blocking
|
|
3806
4076
|
});
|
|
3807
4077
|
match = pattern.exec(scrubbed);
|
|
3808
4078
|
}
|
|
@@ -3852,7 +4122,8 @@ function checkUndefinedVocabulary(files, glossaryTerms, readerFacingPrefix, read
|
|
|
3852
4122
|
findings.push({
|
|
3853
4123
|
file: file.path,
|
|
3854
4124
|
line,
|
|
3855
|
-
message: `uses coined term "${term}" without defining it inline or linking the glossary
|
|
4125
|
+
message: `uses coined term "${term}" without defining it inline or linking the glossary`,
|
|
4126
|
+
blocking: false
|
|
3856
4127
|
});
|
|
3857
4128
|
}
|
|
3858
4129
|
}
|
|
@@ -3875,6 +4146,7 @@ function parseGlossaryTerms(glossaryContent) {
|
|
|
3875
4146
|
}
|
|
3876
4147
|
return terms;
|
|
3877
4148
|
}
|
|
4149
|
+
|
|
3878
4150
|
// ../../packages/aeg-core/src/quoted-command.ts
|
|
3879
4151
|
var QUOTED_COMMAND_SHIPS_PREFIX = "aeg-root/";
|
|
3880
4152
|
var QUOTED_COMMAND_SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
|
|
@@ -3969,6 +4241,54 @@ function evaluateCitedQuotes(citedQuotes, citedFileContents) {
|
|
|
3969
4241
|
}
|
|
3970
4242
|
return findings;
|
|
3971
4243
|
}
|
|
4244
|
+
// ../../packages/aeg-core/src/dead-branch-push-guard.ts
|
|
4245
|
+
function checkDeadBranchPush(input) {
|
|
4246
|
+
const { branch, prState, prNumber } = input;
|
|
4247
|
+
if (prState === "MERGED") {
|
|
4248
|
+
return {
|
|
4249
|
+
verdict: "refuse",
|
|
4250
|
+
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.`
|
|
4251
|
+
};
|
|
4252
|
+
}
|
|
4253
|
+
if (prState === "CLOSED") {
|
|
4254
|
+
return {
|
|
4255
|
+
verdict: "refuse",
|
|
4256
|
+
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.`
|
|
4257
|
+
};
|
|
4258
|
+
}
|
|
4259
|
+
if (prState === "OPEN") {
|
|
4260
|
+
return {
|
|
4261
|
+
verdict: "allow",
|
|
4262
|
+
reason: `Branch \`${branch}\` has an OPEN PR (#${prNumber}) — pushing more commits to it is normal.`
|
|
4263
|
+
};
|
|
4264
|
+
}
|
|
4265
|
+
if (prState === "NONE") {
|
|
4266
|
+
return {
|
|
4267
|
+
verdict: "allow",
|
|
4268
|
+
reason: `Branch \`${branch}\` has no PR yet — a brand-new task branch legitimately has none.`
|
|
4269
|
+
};
|
|
4270
|
+
}
|
|
4271
|
+
return {
|
|
4272
|
+
verdict: "allow",
|
|
4273
|
+
reason: `Could not determine \`${branch}\`'s PR state (forge unreachable) — failing open rather than blocking the push on a transient issue.`
|
|
4274
|
+
};
|
|
4275
|
+
}
|
|
4276
|
+
// ../../packages/aeg-core/src/baseline-capture.ts
|
|
4277
|
+
function captureBaseline(counts, capturedAt) {
|
|
4278
|
+
return counts.map((c) => ({ tool: c.tool, findingCount: c.findingCount, capturedAt }));
|
|
4279
|
+
}
|
|
4280
|
+
function compareToBaseline(current, baseline) {
|
|
4281
|
+
const perTool = current.map((c) => {
|
|
4282
|
+
const baselineEntry = baseline.find((b) => b.tool === c.tool);
|
|
4283
|
+
const baselineCount = baselineEntry ? baselineEntry.findingCount : c.findingCount;
|
|
4284
|
+
return { tool: c.tool, baseline: baselineCount, current: c.findingCount, delta: c.findingCount - baselineCount };
|
|
4285
|
+
});
|
|
4286
|
+
return {
|
|
4287
|
+
withinBudget: perTool.every((t) => t.delta <= 0),
|
|
4288
|
+
delta: perTool.reduce((sum, t) => sum + t.delta, 0),
|
|
4289
|
+
perTool
|
|
4290
|
+
};
|
|
4291
|
+
}
|
|
3972
4292
|
// ../../packages/aeg-core/src/retired-vocabulary.ts
|
|
3973
4293
|
var RETIRED_PATTERNS = [
|
|
3974
4294
|
String.raw`\bD-[0-9]{3}\b`,
|
|
@@ -4438,6 +4758,223 @@ function findWorkspaceEscapes(files, knownPaths, workspaceDirs = DEFAULT_WORKSPA
|
|
|
4438
4758
|
}
|
|
4439
4759
|
return findings;
|
|
4440
4760
|
}
|
|
4761
|
+
// ../../packages/aeg-core/src/log/schema.ts
|
|
4762
|
+
import { z } from "zod";
|
|
4763
|
+
var ROLE_VALUES = [
|
|
4764
|
+
"planner",
|
|
4765
|
+
"developer",
|
|
4766
|
+
"code-reviewer",
|
|
4767
|
+
"security",
|
|
4768
|
+
"principal",
|
|
4769
|
+
"archivist",
|
|
4770
|
+
"architect"
|
|
4771
|
+
];
|
|
4772
|
+
var RoleSchema = z.enum(ROLE_VALUES);
|
|
4773
|
+
var HOST_VALUES = ["hook", "ci", "cli", "loop"];
|
|
4774
|
+
var HostSchema = z.enum(HOST_VALUES);
|
|
4775
|
+
var RUN_ID_PATTERN = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
4776
|
+
var HeaderMetaSchema = z.object({
|
|
4777
|
+
schema: z.literal(1),
|
|
4778
|
+
ts: z.string(),
|
|
4779
|
+
run_id: z.string().regex(RUN_ID_PATTERN),
|
|
4780
|
+
seq: z.number().int().nonnegative(),
|
|
4781
|
+
repo: z.string().nullable(),
|
|
4782
|
+
vinaya: z.string(),
|
|
4783
|
+
doctrine: z.string(),
|
|
4784
|
+
host: HostSchema,
|
|
4785
|
+
machine: z.string()
|
|
4786
|
+
}).strict();
|
|
4787
|
+
var SubjectSchema = z.object({
|
|
4788
|
+
issue: z.number().int().nullable(),
|
|
4789
|
+
pr: z.number().int().optional(),
|
|
4790
|
+
sha: z.string().optional(),
|
|
4791
|
+
role: z.union([RoleSchema, z.literal("unattributed")]),
|
|
4792
|
+
round: z.number().int().optional(),
|
|
4793
|
+
objectives_version: z.string().optional()
|
|
4794
|
+
}).strict();
|
|
4795
|
+
var HeaderSchema = z.object({
|
|
4796
|
+
meta: HeaderMetaSchema,
|
|
4797
|
+
subject: SubjectSchema
|
|
4798
|
+
}).strict();
|
|
4799
|
+
var envelopeTail = {
|
|
4800
|
+
duration_ms: z.number().nonnegative().optional(),
|
|
4801
|
+
payload: z.object({}).strict()
|
|
4802
|
+
};
|
|
4803
|
+
var DispatchOutcomeSchema = z.discriminatedUnion("type", [
|
|
4804
|
+
z.object({ type: z.literal("pr_opened"), pr: z.number().int(), head: z.string() }).strict(),
|
|
4805
|
+
z.object({
|
|
4806
|
+
type: z.literal("round_pushed"),
|
|
4807
|
+
pr: z.number().int(),
|
|
4808
|
+
head: z.string(),
|
|
4809
|
+
comment_id: z.number().int()
|
|
4810
|
+
}).strict(),
|
|
4811
|
+
z.object({
|
|
4812
|
+
type: z.literal("verdict"),
|
|
4813
|
+
verdict: z.enum(["APPROVE", "REQUEST CHANGES", "PASS", "FAIL"]),
|
|
4814
|
+
head: z.string(),
|
|
4815
|
+
comment_id: z.number().int(),
|
|
4816
|
+
objectives: z.array(z.object({ id: z.string(), met: z.boolean() }).strict()),
|
|
4817
|
+
findings: z.array(z.object({ id: z.string(), severity: z.string(), state: z.string().optional() }).strict())
|
|
4818
|
+
}).strict(),
|
|
4819
|
+
z.object({
|
|
4820
|
+
type: z.literal("escalation"),
|
|
4821
|
+
class: z.enum(["authority", "strategy", "product"]),
|
|
4822
|
+
comment_id: z.number().int()
|
|
4823
|
+
}).strict(),
|
|
4824
|
+
z.object({ type: z.literal("brief"), comment_id: z.number().int(), hash: z.string() }).strict(),
|
|
4825
|
+
z.object({ type: z.literal("plan"), issues: z.array(z.number().int()) }).strict(),
|
|
4826
|
+
z.object({ type: z.literal("archive"), provenance_comment_id: z.number().int() }).strict()
|
|
4827
|
+
]);
|
|
4828
|
+
var dispatchShared = {
|
|
4829
|
+
meta: HeaderMetaSchema,
|
|
4830
|
+
subject: SubjectSchema,
|
|
4831
|
+
kind: z.literal("dispatch"),
|
|
4832
|
+
...envelopeTail,
|
|
4833
|
+
target_role: RoleSchema,
|
|
4834
|
+
model: z.string(),
|
|
4835
|
+
round: z.number().int().optional(),
|
|
4836
|
+
effect_id: z.string()
|
|
4837
|
+
};
|
|
4838
|
+
var dispatchUsageField = z.object({ input: z.number().nonnegative(), output: z.number().nonnegative() }).strict().nullable();
|
|
4839
|
+
var DispatchEventSchema = z.discriminatedUnion("event", [
|
|
4840
|
+
z.object({ ...dispatchShared, event: z.literal("dispatched"), prompt_hash: z.string() }).strict(),
|
|
4841
|
+
z.object({
|
|
4842
|
+
...dispatchShared,
|
|
4843
|
+
event: z.literal("outcome_received"),
|
|
4844
|
+
outcome: DispatchOutcomeSchema,
|
|
4845
|
+
usage: dispatchUsageField
|
|
4846
|
+
}).strict(),
|
|
4847
|
+
z.object({
|
|
4848
|
+
...dispatchShared,
|
|
4849
|
+
event: z.literal("dispatch_failed"),
|
|
4850
|
+
reason: z.enum(["timeout", "crash", "refused", "unattributed_write"]),
|
|
4851
|
+
usage: dispatchUsageField
|
|
4852
|
+
}).strict()
|
|
4853
|
+
]);
|
|
4854
|
+
var loopShared = {
|
|
4855
|
+
meta: HeaderMetaSchema,
|
|
4856
|
+
subject: SubjectSchema,
|
|
4857
|
+
kind: z.literal("dev_review_loop"),
|
|
4858
|
+
...envelopeTail,
|
|
4859
|
+
loop_id: z.string()
|
|
4860
|
+
};
|
|
4861
|
+
var DevReviewLoopEventSchema = z.discriminatedUnion("event", [
|
|
4862
|
+
z.object({
|
|
4863
|
+
...loopShared,
|
|
4864
|
+
event: z.literal("loop_started"),
|
|
4865
|
+
task: z.number().int(),
|
|
4866
|
+
policy: z.object({
|
|
4867
|
+
max_rounds: z.number().int().nonnegative(),
|
|
4868
|
+
reviewers: z.array(RoleSchema),
|
|
4869
|
+
models: z.record(RoleSchema, z.string())
|
|
4870
|
+
}).strict()
|
|
4871
|
+
}).strict(),
|
|
4872
|
+
z.object({ ...loopShared, event: z.literal("round_started"), round: z.number().int(), base_head: z.string() }).strict(),
|
|
4873
|
+
z.object({
|
|
4874
|
+
...loopShared,
|
|
4875
|
+
event: z.literal("gate_result_read"),
|
|
4876
|
+
round: z.number().int(),
|
|
4877
|
+
head: z.string(),
|
|
4878
|
+
green: z.boolean()
|
|
4879
|
+
}).strict(),
|
|
4880
|
+
z.object({
|
|
4881
|
+
...loopShared,
|
|
4882
|
+
event: z.literal("verdicts_read"),
|
|
4883
|
+
round: z.number().int(),
|
|
4884
|
+
head: z.string(),
|
|
4885
|
+
all_approve: z.boolean(),
|
|
4886
|
+
blockers: z.number().int().nonnegative()
|
|
4887
|
+
}).strict(),
|
|
4888
|
+
z.object({
|
|
4889
|
+
...loopShared,
|
|
4890
|
+
event: z.literal("findings_compared"),
|
|
4891
|
+
round: z.number().int(),
|
|
4892
|
+
open: z.array(z.string()),
|
|
4893
|
+
resolved: z.array(z.string()),
|
|
4894
|
+
new: z.array(z.string()),
|
|
4895
|
+
recurring: z.array(z.string())
|
|
4896
|
+
}).strict(),
|
|
4897
|
+
z.object({
|
|
4898
|
+
...loopShared,
|
|
4899
|
+
event: z.literal("stop_condition_met"),
|
|
4900
|
+
round: z.number().int(),
|
|
4901
|
+
condition: z.enum([
|
|
4902
|
+
"green",
|
|
4903
|
+
"max_rounds",
|
|
4904
|
+
"no_progress",
|
|
4905
|
+
"escalated",
|
|
4906
|
+
"principal_stop",
|
|
4907
|
+
"confidence",
|
|
4908
|
+
"reappearance"
|
|
4909
|
+
])
|
|
4910
|
+
}).strict(),
|
|
4911
|
+
z.object({
|
|
4912
|
+
...loopShared,
|
|
4913
|
+
event: z.literal("paused"),
|
|
4914
|
+
round: z.number().int(),
|
|
4915
|
+
reason: z.enum(["escalation", "principal_item", "refreeze_needed"])
|
|
4916
|
+
}).strict(),
|
|
4917
|
+
z.object({
|
|
4918
|
+
...loopShared,
|
|
4919
|
+
event: z.literal("resumed"),
|
|
4920
|
+
round: z.number().int(),
|
|
4921
|
+
by: z.literal("principal")
|
|
4922
|
+
}).strict(),
|
|
4923
|
+
z.object({
|
|
4924
|
+
...loopShared,
|
|
4925
|
+
event: z.literal("round_ended"),
|
|
4926
|
+
round: z.number().int(),
|
|
4927
|
+
base_head: z.string(),
|
|
4928
|
+
head: z.string(),
|
|
4929
|
+
files_changed: z.number().int().nonnegative(),
|
|
4930
|
+
insertions: z.number().int().nonnegative(),
|
|
4931
|
+
deletions: z.number().int().nonnegative(),
|
|
4932
|
+
wall_ms: z.number().nonnegative(),
|
|
4933
|
+
outcome: z.enum(["green", "changes_requested", "escalated"])
|
|
4934
|
+
}).strict(),
|
|
4935
|
+
z.object({
|
|
4936
|
+
...loopShared,
|
|
4937
|
+
event: z.literal("journal_finalized"),
|
|
4938
|
+
rounds: z.number().int().nonnegative(),
|
|
4939
|
+
total_wall_ms: z.number().nonnegative(),
|
|
4940
|
+
time_to_green_ms: z.number().nonnegative().nullable(),
|
|
4941
|
+
files_changed_total: z.number().int().nonnegative(),
|
|
4942
|
+
final_head: z.string(),
|
|
4943
|
+
result: z.enum(["merged_ready", "stopped"])
|
|
4944
|
+
}).strict()
|
|
4945
|
+
]);
|
|
4946
|
+
var ForgeOpSchema = z.enum([
|
|
4947
|
+
"pr.create",
|
|
4948
|
+
"pr.comment",
|
|
4949
|
+
"pr.body.replace",
|
|
4950
|
+
"pr.refreeze",
|
|
4951
|
+
"issue.create",
|
|
4952
|
+
"issue.edit",
|
|
4953
|
+
"issue.comment",
|
|
4954
|
+
"milestone.create",
|
|
4955
|
+
"milestone.edit",
|
|
4956
|
+
"milestone.close",
|
|
4957
|
+
"label.add",
|
|
4958
|
+
"label.remove"
|
|
4959
|
+
]);
|
|
4960
|
+
var ForgeWriteTargetSchema = z.object({
|
|
4961
|
+
issue: z.number().int().optional(),
|
|
4962
|
+
pr: z.number().int().optional()
|
|
4963
|
+
}).strict();
|
|
4964
|
+
var forgeWriteShared = {
|
|
4965
|
+
meta: HeaderMetaSchema,
|
|
4966
|
+
subject: SubjectSchema,
|
|
4967
|
+
kind: z.literal("forge_write"),
|
|
4968
|
+
...envelopeTail,
|
|
4969
|
+
op: ForgeOpSchema,
|
|
4970
|
+
target: ForgeWriteTargetSchema
|
|
4971
|
+
};
|
|
4972
|
+
var ForgeWriteEventSchema = z.discriminatedUnion("event", [
|
|
4973
|
+
z.object({ ...forgeWriteShared, event: z.literal("validated") }).strict(),
|
|
4974
|
+
z.object({ ...forgeWriteShared, event: z.literal("refused"), reason: z.string() }).strict(),
|
|
4975
|
+
z.object({ ...forgeWriteShared, event: z.literal("written"), comment_ids: z.array(z.string()) }).strict()
|
|
4976
|
+
]);
|
|
4977
|
+
var LogEventSchema = z.union([DispatchEventSchema, DevReviewLoopEventSchema, ForgeWriteEventSchema]);
|
|
4441
4978
|
// src/checks/contract.ts
|
|
4442
4979
|
var CHECK_SCHEMA_VERSION = 1;
|
|
4443
4980
|
function emitCheckError(error) {
|
|
@@ -4447,6 +4984,39 @@ function emitCheckError(error) {
|
|
|
4447
4984
|
|
|
4448
4985
|
// src/checks/bin/check-brief-shape.ts
|
|
4449
4986
|
var CHECK_NAME = "brief-shape";
|
|
4987
|
+
function fetchIssueComments(issueNumber) {
|
|
4988
|
+
const out = execFileSync2("gh", ["issue", "view", String(issueNumber), "--json", "comments"], {
|
|
4989
|
+
encoding: "utf8",
|
|
4990
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4991
|
+
});
|
|
4992
|
+
return JSON.parse(out);
|
|
4993
|
+
}
|
|
4994
|
+
function resolveGradedBody(prBody, taskBranch) {
|
|
4995
|
+
if (!taskBranch)
|
|
4996
|
+
return { ok: true, body: prBody };
|
|
4997
|
+
const { issue } = extractIssue(prBody);
|
|
4998
|
+
if (issue === null) {
|
|
4999
|
+
return { ok: false, message: "not dispatched — no `Closes #N` in the PR body to resolve the task Issue." };
|
|
5000
|
+
}
|
|
5001
|
+
let json;
|
|
5002
|
+
try {
|
|
5003
|
+
json = fetchIssueComments(issue);
|
|
5004
|
+
} catch (err) {
|
|
5005
|
+
if (isIssueNotFoundError(err)) {
|
|
5006
|
+
return { ok: true, body: prBody };
|
|
5007
|
+
}
|
|
5008
|
+
return {
|
|
5009
|
+
ok: false,
|
|
5010
|
+
message: `could not fetch Issue #${issue}'s comments (\`gh issue view\`) to grade the dispatched brief: ${err instanceof Error ? err.message : String(err)}`
|
|
5011
|
+
};
|
|
5012
|
+
}
|
|
5013
|
+
const comment = json.comments.find((c) => c.body.split(`
|
|
5014
|
+
`)[0] === AEG_BRIEF_V1_MARKER);
|
|
5015
|
+
if (!comment) {
|
|
5016
|
+
return { ok: false, message: `not dispatched — no \`aeg:brief:v1\` comment on Issue #${issue}.` };
|
|
5017
|
+
}
|
|
5018
|
+
return { ok: true, body: contentAfterTwoLines(comment.body) };
|
|
5019
|
+
}
|
|
4450
5020
|
function listDirs(dir) {
|
|
4451
5021
|
try {
|
|
4452
5022
|
return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
@@ -4470,6 +5040,37 @@ function buildConsumersOf2() {
|
|
|
4470
5040
|
const workspaces = Array.isArray(root.workspaces) ? root.workspaces : [];
|
|
4471
5041
|
return buildConsumersOf(workspaces, listDirs, readManifest);
|
|
4472
5042
|
}
|
|
5043
|
+
function fetchIssueBody(issueNumber) {
|
|
5044
|
+
return execFileSync2("gh", ["issue", "view", String(issueNumber), "--json", "body", "--jq", ".body"], {
|
|
5045
|
+
encoding: "utf8",
|
|
5046
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5047
|
+
});
|
|
5048
|
+
}
|
|
5049
|
+
function resolveObjectivesApplicability(prBody, taskBranch) {
|
|
5050
|
+
if (!taskBranch) {
|
|
5051
|
+
if (!hasObjectivesHeading(prBody))
|
|
5052
|
+
return { applies: false };
|
|
5053
|
+
const own = objectivesOf(prBody);
|
|
5054
|
+
return { applies: true, objectives: own.ok ? own.objectives : [] };
|
|
5055
|
+
}
|
|
5056
|
+
const { issue } = extractIssue(prBody);
|
|
5057
|
+
if (issue === null || issue < OBJECTIVES_SINCE_ISSUE)
|
|
5058
|
+
return { applies: false };
|
|
5059
|
+
try {
|
|
5060
|
+
const parsed = objectivesOf(fetchIssueBody(issue));
|
|
5061
|
+
return parsed.ok ? { applies: true, objectives: parsed.objectives } : { applies: false };
|
|
5062
|
+
} catch (err) {
|
|
5063
|
+
if (isIssueNotFoundError(err)) {
|
|
5064
|
+
process.stdout.write(`brief-shape: Issue #${issue} does not resolve (\`gh issue view\`) — skipping the objectives checks for this run.
|
|
5065
|
+
`);
|
|
5066
|
+
return { applies: false };
|
|
5067
|
+
}
|
|
5068
|
+
return {
|
|
5069
|
+
applies: true,
|
|
5070
|
+
fetchError: `could not fetch Issue #${issue}'s body (\`gh issue view\`) to compare Objectives: ${err.message}`
|
|
5071
|
+
};
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
4473
5074
|
function main() {
|
|
4474
5075
|
const prBody = process.env.PR_BODY ?? "";
|
|
4475
5076
|
if (!prBody) {
|
|
@@ -4480,9 +5081,33 @@ function main() {
|
|
|
4480
5081
|
if (branch && !taskBranch && !isBriefShaped(prBody)) {
|
|
4481
5082
|
process.exit(0);
|
|
4482
5083
|
}
|
|
4483
|
-
const
|
|
5084
|
+
const gradedBodyResolution = resolveGradedBody(prBody, taskBranch);
|
|
5085
|
+
if (!gradedBodyResolution.ok) {
|
|
5086
|
+
emitCheckError({
|
|
5087
|
+
schema: CHECK_SCHEMA_VERSION,
|
|
5088
|
+
check: CHECK_NAME,
|
|
5089
|
+
severity: "error",
|
|
5090
|
+
message: `brief-shape: ${gradedBodyResolution.message}`,
|
|
5091
|
+
agent_recovery_prompt: "Dispatch this task with `vinaya task dispatch <tranche> <n>` (posts the `aeg:brief:v1` Issue comment), or check `gh auth status`/network, then re-run `vinaya check brief-shape`."
|
|
5092
|
+
});
|
|
5093
|
+
process.exit(1);
|
|
5094
|
+
}
|
|
5095
|
+
const gradedBody = gradedBodyResolution.body;
|
|
5096
|
+
const objectivesResolution = resolveObjectivesApplicability(prBody, taskBranch);
|
|
5097
|
+
if (objectivesResolution.applies && "fetchError" in objectivesResolution) {
|
|
5098
|
+
emitCheckError({
|
|
5099
|
+
schema: CHECK_SCHEMA_VERSION,
|
|
5100
|
+
check: CHECK_NAME,
|
|
5101
|
+
severity: "error",
|
|
5102
|
+
message: `brief-validation objectives copy: ${objectivesResolution.fetchError}`,
|
|
5103
|
+
agent_recovery_prompt: "Check `gh auth status` and network, then re-run `vinaya check brief-shape`."
|
|
5104
|
+
});
|
|
5105
|
+
process.exit(1);
|
|
5106
|
+
}
|
|
5107
|
+
const { errors } = checkBriefSections(gradedBody, readTierFromPrBody, {
|
|
4484
5108
|
requireClosesN: taskBranch,
|
|
4485
|
-
consumersOf: buildConsumersOf2()
|
|
5109
|
+
consumersOf: buildConsumersOf2(),
|
|
5110
|
+
issueObjectives: objectivesResolution.applies ? objectivesResolution.objectives : undefined
|
|
4486
5111
|
});
|
|
4487
5112
|
const parsedPrNumber = Number.parseInt(process.env.PR_NUMBER ?? "", 10);
|
|
4488
5113
|
const prNumber = Number.isInteger(parsedPrNumber) ? parsedPrNumber : null;
|