@attalabs/vinaya 0.21.0 → 0.23.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 +15 -4
- package/aeg-root/contracts/archivist-tranche-archivist.md +2 -2
- package/aeg-root/contracts/brief-developer.md +1 -0
- package/aeg-root/contracts/developer-reviewer.md +3 -3
- package/aeg-root/contracts/reviewer-archivist.md +3 -3
- package/aeg-root/enforcement.md +28 -21
- package/aeg-root/process.md +8 -4
- package/aeg-root/roles/archivist.md +2 -2
- package/aeg-root/roles/brief-author.md +9 -3
- package/aeg-root/roles/developer.md +63 -31
- package/aeg-root/roles/planner.md +2 -2
- package/aeg-root/roles/principal.md +14 -0
- package/aeg-root/roles/reviewer.md +32 -15
- package/aeg-root/roles/security.md +15 -5
- package/aeg-root/skills/aeg/SKILL.md +9 -5
- package/aeg-root/skills/aeg-roles/SKILL.md +2 -2
- package/aeg-root/skills/brief-authoring/SKILL.md +39 -27
- package/aeg-root/state-machine.md +9 -11
- package/aeg-root/templates/brief-template.md +6 -6
- package/aeg-root/templates/pr-report-template.md +12 -7
- package/aeg-root/tranche-model.md +9 -5
- package/dist/checks/bin/check-body-bare-digits.js +667 -148
- package/dist/checks/bin/check-branch-topology.js +714 -149
- package/dist/checks/bin/check-brief-shape.js +704 -147
- package/dist/checks/bin/check-changeset-coverage.js +1046 -166
- package/dist/checks/bin/check-closes-n.js +714 -149
- package/dist/checks/bin/check-coherence.js +729 -156
- package/dist/checks/bin/check-dead-branch-push.js +663 -144
- package/dist/checks/bin/check-dispatch-readiness.js +728 -155
- package/dist/checks/bin/check-doc-coverage-push.js +838 -261
- package/dist/checks/bin/check-doc-coverage.js +838 -261
- package/dist/checks/bin/check-doctrine-no-procedures.js +4794 -0
- package/dist/checks/bin/check-doctrine-portability.js +799 -155
- package/dist/checks/bin/check-evidence-fresh.js +1132 -206
- package/dist/checks/bin/check-exec-bits.js +4931 -0
- package/dist/checks/bin/check-first-push-dispatch.js +716 -151
- package/dist/checks/bin/check-issue-assignment.js +714 -149
- package/dist/checks/bin/check-main-branch-refusal.js +663 -144
- package/dist/checks/bin/check-no-disk-state.js +663 -144
- package/dist/checks/bin/check-pr-report-density.js +4461 -0
- package/dist/checks/bin/check-quoted-command.js +741 -166
- package/dist/checks/bin/check-reader-resolvable-prose.js +768 -173
- package/dist/checks/bin/check-registry-gates.js +726 -154
- package/dist/checks/bin/check-retired-vocabulary.js +760 -165
- package/dist/checks/bin/check-review-gate.js +783 -148
- package/dist/checks/bin/check-single-plan-pr.js +663 -144
- package/dist/checks/bin/check-test-plan.js +664 -145
- package/dist/checks/bin/check-token-collection-wired.js +665 -146
- package/dist/checks/bin/check-token-report.js +4509 -0
- package/dist/checks/bin/check-workspace-escape.js +1129 -183
- package/dist/index.js +8458 -5403
- package/package.json +1 -1
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/actions.ts +173 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/anchored-region.ts +113 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/archive-task.ts +206 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/baseline-capture.ts +65 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/blast-radius-domains.ts +192 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/branch-topology-gate.ts +85 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/brief-validation.ts +511 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/claude-code-transcript.ts +460 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/coherence-checks.ts +787 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-audit.ts +55 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dead-branch-push-guard.ts +77 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-section7.ts +66 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/derive-tranche.ts +94 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/diagram-model.ts +372 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/direct-main-push.ts +39 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/dispatch-gate.ts +224 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doc-owners.ts +314 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/build-doc-nav.ts +38 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/docs-coherence.ts +136 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/index.ts +29 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/legacy-anchors.ts +48 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/nav-helpers.ts +17 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/node-route.ts +134 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/parse-doc.ts +43 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/published-prose.ts +306 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/surfaced-manifest.ts +89 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/docs/types.ts +33 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-portability.ts +170 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/doctrine-source.ts +23 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/ensure-label.ts +28 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/file-classify.ts +51 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/first-push-dispatch-gate.ts +80 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/gate-audience.ts +168 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/index.ts +301 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-assignment.ts +102 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/issue-validation.ts +746 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/leftover-detection.ts +68 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/local-anchor-coverage.ts +245 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/main-branch-refusal.ts +64 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/manifest-validity.ts +81 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/markdown-table.ts +76 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/metering-io-guard.ts +119 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/milestone-validation.ts +240 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/no-disk-state.ts +50 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-ledger.ts +77 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-registry.ts +105 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-token-report.ts +165 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/parse-tranche.ts +183 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/pr-tier.ts +81 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/premise-check.ts +125 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/quoted-command.ts +275 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/reader-resolvable-prose.ts +320 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-checks.ts +255 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-parse.ts +164 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/registry-scaffold.ts +216 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/report-tokens.ts +261 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/retired-vocabulary.ts +163 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/review-gate.ts +269 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/single-plan-pr.ts +73 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-machine-model.ts +269 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/state-source.ts +14 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/status-block.ts +8 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/sum-ledger.ts +26 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/symbol-collisions.ts +78 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-gate.ts +104 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/test-plan-section.ts +65 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/types.ts +174 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/verdict-extraction.ts +140 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/vocabulary-citation.ts +114 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/waiver-label.ts +52 -0
- package/studio-standalone/_node_modules/@attalabs/aeg-core/src/workspace-escape.ts +172 -0
- 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/[root-of-the-server]__02a-3g8._.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]__02c71kv._.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]__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]__0ozbaq0._.js → [root-of-the-server]__1mcitst._.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/_0o54suy._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1-6rzip._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1fqw88f._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_1mrtj0f._.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/apps_vinaya-studio_web_src_app_studio_06it6s-._.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_libraries_animate_components_index_ts_186fje1._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_brutal_components_index_ts_0n_lcc4._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_retro_components_index_ts_0wvpm_z._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/packages_ui_libraries_retro_components_index_ts_1_cfi48._.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/00pi8vhg8w6gi.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0r4i_ks9z02bw.js → 03q0q9n6-xah8.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2_k6j1o9q5nn4.js → 0jer_drkl1e4r.js} +4 -4
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0vc2u252q5q6o.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1k916ih3p4nsg.js → 13g16cu00g570.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2hmannd4bjhzy.js → 1sfxfunirkcr6.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1t2kft7bxzuxn.js → 206ajg8f1a-_h.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1hfhxjy577l5d.js → 20x-t0a0v129z.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{25kbt5k-xb2a5.js → 279qq4twinc97.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{112g08xpx3kro.js → 2ka8dg687auhj.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/2ou_81ahqw31e.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0up5x3jg-cwov.js → 2rn89uwtf2lwc.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{3mqu_uhluy19i.css → 3qc67qhcvbg0o.css} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1yqs2zgvwa4p1.js → 3zugx9esh2x51.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0_jnwbarv-cqk.js +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1ybrihl5_aocy.js +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3r5efmleneaco.js +0 -1
- /package/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/{crNCHQ9uAgKOYC6DKkZma → 5xy_GA7G5KRuE3HDumIiw}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{crNCHQ9uAgKOYC6DKkZma → 5xy_GA7G5KRuE3HDumIiw}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{crNCHQ9uAgKOYC6DKkZma → 5xy_GA7G5KRuE3HDumIiw}/_ssgManifest.js +0 -0
|
@@ -1,36 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
2
4
|
|
|
3
5
|
// src/checks/bin/check-evidence-fresh.ts
|
|
4
|
-
import { execFileSync as
|
|
5
|
-
|
|
6
|
-
// src/checks/contract.ts
|
|
7
|
-
var CHECK_SCHEMA_VERSION = 1;
|
|
8
|
-
function emitCheckError(error) {
|
|
9
|
-
process.stderr.write(`${JSON.stringify(error)}
|
|
10
|
-
`);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// src/lib/numstat.ts
|
|
14
|
-
function summariseNumstat(numstat) {
|
|
15
|
-
const rows = numstat.split(`
|
|
16
|
-
`).filter((l) => l.trim() !== "");
|
|
17
|
-
let added = 0;
|
|
18
|
-
let deleted = 0;
|
|
19
|
-
let binary = 0;
|
|
20
|
-
for (const row of rows) {
|
|
21
|
-
const [a, d] = row.split("\t");
|
|
22
|
-
if (a === "-" || d === "-") {
|
|
23
|
-
binary++;
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
added += Number(a) || 0;
|
|
27
|
-
deleted += Number(d) || 0;
|
|
28
|
-
}
|
|
29
|
-
const files = `${rows.length} file${rows.length === 1 ? "" : "s"} changed`;
|
|
30
|
-
const bin = binary > 0 ? `, ${binary} binary` : "";
|
|
31
|
-
return `${files}, ${added} insertion${added === 1 ? "" : "s"}(+), ${deleted} deletion${deleted === 1 ? "" : "s"}(-)${bin}`;
|
|
32
|
-
}
|
|
33
|
-
var EVIDENCE_SUMMARY_PREFIX = "Summary: ";
|
|
6
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
34
7
|
// ../../packages/aeg-core/src/gate-audience.ts
|
|
35
8
|
var GATE_AUDIENCE = {
|
|
36
9
|
"check-branch-topology": { shippedAs: "branch-topology", ring: 0 },
|
|
@@ -239,17 +212,18 @@ function splitIds(raw) {
|
|
|
239
212
|
return [];
|
|
240
213
|
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0 && !isEmptyMarker(s) && ID_TOKEN.test(s));
|
|
241
214
|
}
|
|
242
|
-
function resolveIds(raw
|
|
215
|
+
function resolveIds(raw) {
|
|
243
216
|
const ids = splitIds(raw);
|
|
244
217
|
const resolved = [];
|
|
218
|
+
let lastSlug = null;
|
|
245
219
|
for (const id of ids) {
|
|
246
220
|
const qualified = id.match(SLUG_QUALIFIED_ID);
|
|
247
221
|
if (qualified) {
|
|
248
|
-
lastSlug
|
|
222
|
+
lastSlug = qualified[1] ?? null;
|
|
249
223
|
resolved.push(id);
|
|
250
|
-
} else if (lastSlug
|
|
251
|
-
resolved.push(`${lastSlug
|
|
252
|
-
lastSlug
|
|
224
|
+
} else if (lastSlug) {
|
|
225
|
+
resolved.push(`${lastSlug} ${id}`);
|
|
226
|
+
lastSlug = null;
|
|
253
227
|
} else {
|
|
254
228
|
resolved.push(id);
|
|
255
229
|
}
|
|
@@ -269,12 +243,7 @@ function parseRationaleDeps(body) {
|
|
|
269
243
|
const section = extractSection(body);
|
|
270
244
|
const dependsOn = [];
|
|
271
245
|
const conflictsWith = [];
|
|
272
|
-
let current = null;
|
|
273
246
|
const assignedFields = new Set;
|
|
274
|
-
const lastSlugByField = {
|
|
275
|
-
dependsOn: { current: null },
|
|
276
|
-
conflictsWith: { current: null }
|
|
277
|
-
};
|
|
278
247
|
const spanPattern = /`([^`]*)`/g;
|
|
279
248
|
let match = spanPattern.exec(section);
|
|
280
249
|
while (match !== null) {
|
|
@@ -284,13 +253,9 @@ function parseRationaleDeps(body) {
|
|
|
284
253
|
const field = labelMatch[1]?.toLowerCase() === "conflicts-with" ? "conflictsWith" : "dependsOn";
|
|
285
254
|
if (!assignedFields.has(field)) {
|
|
286
255
|
assignedFields.add(field);
|
|
287
|
-
|
|
288
|
-
const ids = resolveIds(labelMatch[2] ?? "", lastSlugByField[field]);
|
|
256
|
+
const ids = resolveIds(labelMatch[2] ?? "");
|
|
289
257
|
pushUnique(field === "dependsOn" ? dependsOn : conflictsWith, ids);
|
|
290
258
|
}
|
|
291
|
-
} else if (current) {
|
|
292
|
-
const ids = resolveIds(content, lastSlugByField[current]);
|
|
293
|
-
pushUnique(current === "dependsOn" ? dependsOn : conflictsWith, ids);
|
|
294
259
|
}
|
|
295
260
|
match = spanPattern.exec(section);
|
|
296
261
|
}
|
|
@@ -1755,6 +1720,48 @@ function splitTableRow(row) {
|
|
|
1755
1720
|
parts.pop();
|
|
1756
1721
|
return parts.map((p) => p.split(PLACEHOLDER).join("|"));
|
|
1757
1722
|
}
|
|
1723
|
+
// ../../packages/aeg-core/src/blast-radius-domains.ts
|
|
1724
|
+
function resolveWorkspaceEntry(entry2, listDirs) {
|
|
1725
|
+
if (entry2.endsWith("/*")) {
|
|
1726
|
+
const baseDir = entry2.slice(0, -2);
|
|
1727
|
+
return listDirs(baseDir).map((name) => `${baseDir}/${name}`);
|
|
1728
|
+
}
|
|
1729
|
+
if (entry2.includes("*"))
|
|
1730
|
+
return [];
|
|
1731
|
+
return [entry2];
|
|
1732
|
+
}
|
|
1733
|
+
// ../../packages/aeg-core/src/consumer-enumeration.ts
|
|
1734
|
+
function deriveWorkspaceMemberDirs(workspaces, listDirs) {
|
|
1735
|
+
const positive = workspaces.filter((entry2) => !entry2.startsWith("!"));
|
|
1736
|
+
const negative = workspaces.filter((entry2) => entry2.startsWith("!")).map((entry2) => entry2.slice(1));
|
|
1737
|
+
const excluded = new Set;
|
|
1738
|
+
for (const entry2 of negative) {
|
|
1739
|
+
for (const member of resolveWorkspaceEntry(entry2, listDirs))
|
|
1740
|
+
excluded.add(member);
|
|
1741
|
+
}
|
|
1742
|
+
const members = new Set;
|
|
1743
|
+
for (const entry2 of positive) {
|
|
1744
|
+
for (const member of resolveWorkspaceEntry(entry2, listDirs)) {
|
|
1745
|
+
if (!excluded.has(member))
|
|
1746
|
+
members.add(member);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
return [...members].sort();
|
|
1750
|
+
}
|
|
1751
|
+
function buildConsumersOf(workspaces, listDirs, readManifest) {
|
|
1752
|
+
const members = deriveWorkspaceMemberDirs(workspaces, listDirs);
|
|
1753
|
+
return (pkg) => {
|
|
1754
|
+
const target = `@attalabs/${pkg}`;
|
|
1755
|
+
return members.filter((dir) => {
|
|
1756
|
+
if (dir === `packages/${pkg}`)
|
|
1757
|
+
return false;
|
|
1758
|
+
const manifest = readManifest(dir);
|
|
1759
|
+
if (!manifest)
|
|
1760
|
+
return false;
|
|
1761
|
+
return target in (manifest.dependencies ?? {}) || target in (manifest.devDependencies ?? {});
|
|
1762
|
+
});
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1758
1765
|
// ../../packages/aeg-core/src/state-machine-model.ts
|
|
1759
1766
|
var FORGE_FACT_INPUTS = [
|
|
1760
1767
|
{
|
|
@@ -1820,6 +1827,156 @@ var DERIVED_STATUSES = [
|
|
|
1820
1827
|
"incoherent"
|
|
1821
1828
|
];
|
|
1822
1829
|
var DERIVABLE_STATUSES = DERIVED_STATUSES.filter((s) => s !== "backlog");
|
|
1830
|
+
// ../../packages/aeg-core/src/parse-ledger.ts
|
|
1831
|
+
function rowFromCells(cells) {
|
|
1832
|
+
if (cells.length < 7)
|
|
1833
|
+
return null;
|
|
1834
|
+
const phase = (cells[0] ?? "").trim();
|
|
1835
|
+
if (!phase)
|
|
1836
|
+
return null;
|
|
1837
|
+
return {
|
|
1838
|
+
phase,
|
|
1839
|
+
role: (cells[1] ?? "").trim(),
|
|
1840
|
+
agentModel: (cells[2] ?? "").trim(),
|
|
1841
|
+
tokensIn: parseIntCell(cells[3] ?? ""),
|
|
1842
|
+
tokensOut: parseIntCell(cells[4] ?? ""),
|
|
1843
|
+
cost: parseCostCell(cells[5] ?? ""),
|
|
1844
|
+
date: (cells[6] ?? "").trim()
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
function parseIntCell(cell) {
|
|
1848
|
+
const trimmed = cell.replace(/`/g, "").trim();
|
|
1849
|
+
if (!trimmed || isEmDashOrDash(trimmed))
|
|
1850
|
+
return null;
|
|
1851
|
+
const normalized = trimmed.replace(/[,_\s]/g, "");
|
|
1852
|
+
if (!/^-?\d+$/.test(normalized))
|
|
1853
|
+
return null;
|
|
1854
|
+
return Number(normalized);
|
|
1855
|
+
}
|
|
1856
|
+
function parseCostCell(cell) {
|
|
1857
|
+
const trimmed = cell.replace(/`/g, "").trim();
|
|
1858
|
+
if (!trimmed || isEmDashOrDash(trimmed))
|
|
1859
|
+
return null;
|
|
1860
|
+
const normalized = trimmed.replace(/^\$/, "").replace(/[,_\s]/g, "");
|
|
1861
|
+
if (!/^-?\d+(\.\d+)?$/.test(normalized))
|
|
1862
|
+
return null;
|
|
1863
|
+
return Number(normalized);
|
|
1864
|
+
}
|
|
1865
|
+
function isEmDashOrDash(s) {
|
|
1866
|
+
const t = s.trim();
|
|
1867
|
+
return t === "—" || t === "-" || t === "–";
|
|
1868
|
+
}
|
|
1869
|
+
// ../../packages/aeg-core/src/waiver-label.ts
|
|
1870
|
+
var WAIVER_LABEL = label("waiver-docs");
|
|
1871
|
+
var WAIVER_LABEL_REVIEW = label("waiver-review");
|
|
1872
|
+
var PRINCIPAL_ALLOWLIST = ["daniboomerang"];
|
|
1873
|
+
function isPrincipal(login, principalAllowlist) {
|
|
1874
|
+
if (login === null)
|
|
1875
|
+
return false;
|
|
1876
|
+
const normalized = login.toLowerCase();
|
|
1877
|
+
return principalAllowlist.some((p) => p.toLowerCase() === normalized);
|
|
1878
|
+
}
|
|
1879
|
+
function isWaiverLabelActorVerified(opts) {
|
|
1880
|
+
if (!opts.labels.includes(opts.label))
|
|
1881
|
+
return false;
|
|
1882
|
+
return isPrincipal(opts.labelActor, opts.principalAllowlist);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// ../../packages/aeg-core/src/parse-token-report.ts
|
|
1886
|
+
var TOKEN_REPORT_HEADING = /^\s*(?:#{1,6}\s*Token report\s*|\*\*Token report\*\*)\s*$/i;
|
|
1887
|
+
var INLINE_FIELD_LIST = /Phase:\s*([^|]+?)\s*\|\s*Role:\s*([^|]+?)\s*\|\s*Agent\/Model:\s*([^|]+?)\s*\|\s*Tokens in:\s*([^|]+?)\s*\|\s*Tokens out:\s*([^|]+?)\s*\|\s*Cost:\s*([^|]+?)\s*\|\s*Date:\s*(.+?)\s*$/im;
|
|
1888
|
+
function parseTokenReportEntries(body) {
|
|
1889
|
+
const lines = body.split(/\r?\n/);
|
|
1890
|
+
const headingIdxs = [];
|
|
1891
|
+
lines.forEach((line, i) => {
|
|
1892
|
+
if (TOKEN_REPORT_HEADING.test(line))
|
|
1893
|
+
headingIdxs.push(i);
|
|
1894
|
+
});
|
|
1895
|
+
const out = [];
|
|
1896
|
+
for (let h = 0;h < headingIdxs.length; h++) {
|
|
1897
|
+
const start = headingIdxs[h] + 1;
|
|
1898
|
+
const end = h + 1 < headingIdxs.length ? headingIdxs[h + 1] : lines.length;
|
|
1899
|
+
const section = lines.slice(start, end).join(`
|
|
1900
|
+
`);
|
|
1901
|
+
const tableRows = parseTableSection(section);
|
|
1902
|
+
if (tableRows.length > 0) {
|
|
1903
|
+
out.push(...tableRows);
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
const inlineRow = parseInlineFieldList(section);
|
|
1907
|
+
if (inlineRow)
|
|
1908
|
+
out.push(inlineRow);
|
|
1909
|
+
}
|
|
1910
|
+
return out;
|
|
1911
|
+
}
|
|
1912
|
+
function parseTableSection(section) {
|
|
1913
|
+
const lines = section.split(/\r?\n/);
|
|
1914
|
+
let i = 0;
|
|
1915
|
+
for (;i < lines.length; i++) {
|
|
1916
|
+
if ((lines[i] ?? "").trim().startsWith("|"))
|
|
1917
|
+
break;
|
|
1918
|
+
}
|
|
1919
|
+
if (i + 1 >= lines.length)
|
|
1920
|
+
return [];
|
|
1921
|
+
i += 2;
|
|
1922
|
+
const out = [];
|
|
1923
|
+
for (;i < lines.length; i++) {
|
|
1924
|
+
const trimmed = (lines[i] ?? "").trim();
|
|
1925
|
+
if (!trimmed.startsWith("|"))
|
|
1926
|
+
break;
|
|
1927
|
+
const row = rowFromCells(splitTableRow(trimmed));
|
|
1928
|
+
if (row)
|
|
1929
|
+
out.push(row);
|
|
1930
|
+
}
|
|
1931
|
+
return out;
|
|
1932
|
+
}
|
|
1933
|
+
function parseInlineFieldList(section) {
|
|
1934
|
+
const m = section.match(INLINE_FIELD_LIST);
|
|
1935
|
+
if (!m)
|
|
1936
|
+
return null;
|
|
1937
|
+
return rowFromCells([m[1], m[2], m[3], m[4], m[5], m[6], m[7]]);
|
|
1938
|
+
}
|
|
1939
|
+
// ../../packages/aeg-core/src/verdict-extraction.ts
|
|
1940
|
+
var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
|
|
1941
|
+
function firstThreeLines(comment) {
|
|
1942
|
+
return comment.split(`
|
|
1943
|
+
`).slice(0, 3).join(`
|
|
1944
|
+
`);
|
|
1945
|
+
}
|
|
1946
|
+
function extractHeadSha(comment) {
|
|
1947
|
+
const m = firstThreeLines(comment).match(HEAD_SHA_PATTERN);
|
|
1948
|
+
return m ? m[1].toLowerCase() : null;
|
|
1949
|
+
}
|
|
1950
|
+
function extractVerdict(comments, valuePattern, missingLabel) {
|
|
1951
|
+
const candidates = comments.filter((c) => valuePattern.test(c));
|
|
1952
|
+
if (candidates.length === 0) {
|
|
1953
|
+
return {
|
|
1954
|
+
value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
|
|
1955
|
+
headSha: null,
|
|
1956
|
+
danglingNote: `no ${missingLabel} verdict comment found on this PR`
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
const latest = candidates[candidates.length - 1];
|
|
1960
|
+
const m = firstThreeLines(latest).match(valuePattern);
|
|
1961
|
+
if (!m) {
|
|
1962
|
+
return {
|
|
1963
|
+
value: `the most recent ${missingLabel} comment's VERDICT line is not within its first three lines — DANGLING, see below`,
|
|
1964
|
+
headSha: null,
|
|
1965
|
+
danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-three-line read window`
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
return {
|
|
1969
|
+
value: m[1].toUpperCase().replace(/[_-]/g, " "),
|
|
1970
|
+
headSha: extractHeadSha(latest),
|
|
1971
|
+
danglingNote: null
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
function extractCodeReviewVerdict(comments) {
|
|
1975
|
+
return extractVerdict(comments, /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(APPROVE|REQUEST[ _-]?CHANGES|LGTM)(?![A-Za-z0-9])/im, "code-reviewer");
|
|
1976
|
+
}
|
|
1977
|
+
function extractSecurityReviewVerdict(comments) {
|
|
1978
|
+
return extractVerdict(comments, /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(PASS|FAIL)(?![A-Za-z0-9])/im, "security-review");
|
|
1979
|
+
}
|
|
1823
1980
|
// ../../packages/aeg-core/src/report-tokens.ts
|
|
1824
1981
|
var DASH_LOOKALIKES = {
|
|
1825
1982
|
"-": "‑",
|
|
@@ -1828,6 +1985,7 @@ var DASH_LOOKALIKES = {
|
|
|
1828
1985
|
};
|
|
1829
1986
|
var DASH_CHARS = new Set(Object.keys(DASH_LOOKALIKES));
|
|
1830
1987
|
// ../../packages/aeg-core/src/claude-code-transcript.ts
|
|
1988
|
+
import { createHash } from "node:crypto";
|
|
1831
1989
|
function summarizeTranscript(jsonl) {
|
|
1832
1990
|
const seen = new Set;
|
|
1833
1991
|
const components = {
|
|
@@ -1876,7 +2034,14 @@ function numberOr(value, fallback) {
|
|
|
1876
2034
|
function sanitizeKey(value) {
|
|
1877
2035
|
return value.replace(/[^A-Za-z0-9]+/g, "-");
|
|
1878
2036
|
}
|
|
2037
|
+
function collisionResistantKey(value) {
|
|
2038
|
+
const digest = createHash("sha256").update(value).digest("hex");
|
|
2039
|
+
return `${sanitizeKey(value)}-${digest}`;
|
|
2040
|
+
}
|
|
1879
2041
|
function transcriptPointerPath(projectDir, tmpDir) {
|
|
2042
|
+
return `${tmpDir}/claude-transcript-${collisionResistantKey(projectDir)}.txt`;
|
|
2043
|
+
}
|
|
2044
|
+
function legacyTranscriptPointerPath(projectDir, tmpDir) {
|
|
1880
2045
|
return `${tmpDir}/claude-transcript-${sanitizeKey(projectDir)}.txt`;
|
|
1881
2046
|
}
|
|
1882
2047
|
function resolvePointer(explicitTranscriptPath, deps) {
|
|
@@ -1884,13 +2049,15 @@ function resolvePointer(explicitTranscriptPath, deps) {
|
|
|
1884
2049
|
return { path: explicitTranscriptPath, corroborated: true };
|
|
1885
2050
|
const projectDir = deps.env.CLAUDE_PROJECT_DIR ?? deps.cwd;
|
|
1886
2051
|
const tmpDir = deps.env.TMPDIR ?? "/tmp";
|
|
1887
|
-
const
|
|
2052
|
+
const primaryPointerPath = transcriptPointerPath(projectDir, tmpDir);
|
|
2053
|
+
const legacyPointerPath = legacyTranscriptPointerPath(projectDir, tmpDir);
|
|
1888
2054
|
const currentSessionId = deps.env.CLAUDE_CODE_SESSION_ID;
|
|
1889
|
-
|
|
2055
|
+
const pointerPath = deps.exists(primaryPointerPath) ? primaryPointerPath : deps.exists(legacyPointerPath) ? legacyPointerPath : undefined;
|
|
2056
|
+
if (!pointerPath) {
|
|
1890
2057
|
return {
|
|
1891
2058
|
pointerExisted: false,
|
|
1892
2059
|
corroborated: false,
|
|
1893
|
-
error: `No transcript pointer at ${
|
|
2060
|
+
error: `No transcript pointer at ${primaryPointerPath} (nor its pre-migration name ${legacyPointerPath}) ` + "and no --transcript given. Either this repo installs no track-transcript.sh Stop hook (lacking one " + "is not a defect — name the transcript directly instead), or no session has completed a turn yet."
|
|
1894
2061
|
};
|
|
1895
2062
|
}
|
|
1896
2063
|
let contents;
|
|
@@ -1965,28 +2132,15 @@ function isTokenCollectionWiringBroken(capability) {
|
|
|
1965
2132
|
return false;
|
|
1966
2133
|
return capability.reason !== "no-transcript-resolved";
|
|
1967
2134
|
}
|
|
2135
|
+
// ../../packages/aeg-core/src/metering-io-guard.ts
|
|
2136
|
+
import { closeSync, constants as fsConstants, fstatSync, openSync, readFileSync } from "node:fs";
|
|
2137
|
+
var GUARD_OPEN_FLAGS = fsConstants.O_RDONLY | fsConstants.O_NONBLOCK | fsConstants.O_NOFOLLOW;
|
|
1968
2138
|
// ../../packages/aeg-core/src/file-classify.ts
|
|
1969
2139
|
function isCodeFile(p) {
|
|
1970
2140
|
if (p.endsWith(".md"))
|
|
1971
2141
|
return false;
|
|
1972
2142
|
return /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|sql|css)$/.test(p);
|
|
1973
2143
|
}
|
|
1974
|
-
// ../../packages/aeg-core/src/waiver-label.ts
|
|
1975
|
-
var WAIVER_LABEL = label("waiver-docs");
|
|
1976
|
-
var WAIVER_LABEL_REVIEW = label("waiver-review");
|
|
1977
|
-
var PRINCIPAL_ALLOWLIST = ["daniboomerang"];
|
|
1978
|
-
function isPrincipal(login, principalAllowlist) {
|
|
1979
|
-
if (login === null)
|
|
1980
|
-
return false;
|
|
1981
|
-
const normalized = login.toLowerCase();
|
|
1982
|
-
return principalAllowlist.some((p) => p.toLowerCase() === normalized);
|
|
1983
|
-
}
|
|
1984
|
-
function isWaiverLabelActorVerified(opts) {
|
|
1985
|
-
if (!opts.labels.includes(opts.label))
|
|
1986
|
-
return false;
|
|
1987
|
-
return isPrincipal(opts.labelActor, opts.principalAllowlist);
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
2144
|
// ../../packages/aeg-core/src/doc-owners.ts
|
|
1991
2145
|
var DOC_OWNERS_PATH = ".vinaya/doc-owners";
|
|
1992
2146
|
function parseDocOwners(content) {
|
|
@@ -2171,7 +2325,7 @@ function overrideActive(opts) {
|
|
|
2171
2325
|
return false;
|
|
2172
2326
|
}
|
|
2173
2327
|
// ../../packages/aeg-core/src/premise-check.ts
|
|
2174
|
-
import { createHash } from "node:crypto";
|
|
2328
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2175
2329
|
var ASSERTION_KINDS = new Set(["contains", "absent", "sha256"]);
|
|
2176
2330
|
function isPremiseHeader(line) {
|
|
2177
2331
|
const stripped = line.replace(/[*#]/g, "").trim();
|
|
@@ -2203,6 +2357,32 @@ function parsePremiseBlock(prBody) {
|
|
|
2203
2357
|
}
|
|
2204
2358
|
return assertions;
|
|
2205
2359
|
}
|
|
2360
|
+
function premiseBlockText(prBody) {
|
|
2361
|
+
const searchIn = anchoredRegion(prBody, "PREMISE") ?? prBody;
|
|
2362
|
+
const lines = searchIn.split(/\r?\n/);
|
|
2363
|
+
let startIdx = -1;
|
|
2364
|
+
let endIdxExclusive = lines.length;
|
|
2365
|
+
for (let i = 0;i < lines.length; i++) {
|
|
2366
|
+
const line = lines[i].trim();
|
|
2367
|
+
if (startIdx === -1) {
|
|
2368
|
+
if (isPremiseHeader(line))
|
|
2369
|
+
startIdx = i;
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
if (line === "") {
|
|
2373
|
+
endIdxExclusive = i;
|
|
2374
|
+
break;
|
|
2375
|
+
}
|
|
2376
|
+
if (!PREMISE_LINE.test(line)) {
|
|
2377
|
+
endIdxExclusive = i;
|
|
2378
|
+
break;
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
if (startIdx === -1)
|
|
2382
|
+
return null;
|
|
2383
|
+
return lines.slice(startIdx, endIdxExclusive).join(`
|
|
2384
|
+
`);
|
|
2385
|
+
}
|
|
2206
2386
|
function checkPremises(assertions, fileReader) {
|
|
2207
2387
|
const failures = [];
|
|
2208
2388
|
for (const a of assertions) {
|
|
@@ -2216,7 +2396,7 @@ function checkPremises(assertions, fileReader) {
|
|
|
2216
2396
|
} else if (a.kind === "absent" && content.includes(a.value)) {
|
|
2217
2397
|
failures.push(`premise-check: ${a.path} now contains "${a.value}", but the brief pinned it absent — the premise has moved; re-dig before proceeding.`);
|
|
2218
2398
|
} else if (a.kind === "sha256") {
|
|
2219
|
-
const actual =
|
|
2399
|
+
const actual = createHash2("sha256").update(content).digest("hex");
|
|
2220
2400
|
if (actual !== a.value.toLowerCase()) {
|
|
2221
2401
|
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.`);
|
|
2222
2402
|
}
|
|
@@ -2289,15 +2469,18 @@ function testPlanRegion(prBody) {
|
|
|
2289
2469
|
return located.found ? located.section : prBody;
|
|
2290
2470
|
}
|
|
2291
2471
|
function checkTestPlan(prBody) {
|
|
2292
|
-
const
|
|
2472
|
+
const located = locateTestPlanSection(prBody);
|
|
2473
|
+
const region = located.found ? located.section : prBody;
|
|
2293
2474
|
if (TEST_PLAN_UNIT_TESTS_ONLY_RE.test(region))
|
|
2294
2475
|
return { status: "pass", errors: [] };
|
|
2295
2476
|
if (/\*\*\[(?:agent|principal)\]\*\*/.test(region))
|
|
2296
2477
|
return { status: "pass", errors: [] };
|
|
2478
|
+
if (located.found && extractFencedBlocks(region).length > 0)
|
|
2479
|
+
return { status: "pass", errors: [] };
|
|
2297
2480
|
return {
|
|
2298
2481
|
status: "fail",
|
|
2299
2482
|
errors: [
|
|
2300
|
-
"brief-validation Test Plan: no Test Plan section found — expected `Test Plan: unit-tests-only`, or at least one `**[agent]**`/`**[principal]**`-tagged checklist item."
|
|
2483
|
+
"brief-validation Test Plan: no Test Plan section found — expected `Test Plan: unit-tests-only`, a fenced `[agent]` command list, or at least one `**[agent]**`/`**[principal]**`-tagged checklist item."
|
|
2301
2484
|
]
|
|
2302
2485
|
};
|
|
2303
2486
|
}
|
|
@@ -2420,8 +2603,162 @@ function isBriefShaped(prBody) {
|
|
|
2420
2603
|
const stripped = stripCode(prBody);
|
|
2421
2604
|
return BRIEF_SHAPE_MARKERS.filter((check) => check(stripped).status === "pass").length >= 2;
|
|
2422
2605
|
}
|
|
2606
|
+
var COMMAND_WORDS = ["export", "bun", "gh", "git", "grep", "sed", "cat", "diff", "vinaya"];
|
|
2607
|
+
var BRIEF_RULES_SINCE_PR = 394;
|
|
2608
|
+
var AGENT_BOXES_REFUSED_SINCE_PR = 396;
|
|
2609
|
+
var AGENT_BOX_LINE_RE = /^-\s*\[[ xX]\]\s*\*{2}\[agent\]\*{2}/im;
|
|
2610
|
+
function checkNoAgentBoxes(prBody) {
|
|
2611
|
+
const region = testPlanRegion(prBody);
|
|
2612
|
+
if (!AGENT_BOX_LINE_RE.test(region))
|
|
2613
|
+
return { status: "pass", errors: [] };
|
|
2614
|
+
return {
|
|
2615
|
+
status: "fail",
|
|
2616
|
+
errors: [
|
|
2617
|
+
"brief-validation no agent boxes: the Test Plan carries a checkbox `[agent]` item — an agent never ticks a box or edits a PR body (Principal ruling, 2026-09-03, after PR #395). Render the `[agent]` half as a fenced command list instead; `vinaya pr report` runs each command from the PR head and writes its actual output into the AEG:EVIDENCE block. `[principal]` checkboxes are unaffected."
|
|
2618
|
+
]
|
|
2619
|
+
};
|
|
2620
|
+
}
|
|
2621
|
+
function rolloutThresholdFor(message) {
|
|
2622
|
+
if (message.startsWith("brief-validation unpinned code claim:") || message.startsWith("brief-validation commands carry output:") || message.startsWith("brief-validation consumer tests:") || message.startsWith("brief-validation defeat cases:")) {
|
|
2623
|
+
return BRIEF_RULES_SINCE_PR;
|
|
2624
|
+
}
|
|
2625
|
+
if (message.startsWith("brief-validation no agent boxes:"))
|
|
2626
|
+
return AGENT_BOXES_REFUSED_SINCE_PR;
|
|
2627
|
+
return null;
|
|
2628
|
+
}
|
|
2629
|
+
function partitionBriefErrorsByRollout(errors, prNumber) {
|
|
2630
|
+
const blocking = [];
|
|
2631
|
+
const info = [];
|
|
2632
|
+
for (const e of errors) {
|
|
2633
|
+
const threshold = rolloutThresholdFor(e);
|
|
2634
|
+
const grandfathered = threshold !== null && prNumber !== null && prNumber < threshold;
|
|
2635
|
+
(grandfathered ? info : blocking).push(e);
|
|
2636
|
+
}
|
|
2637
|
+
return { blocking, info };
|
|
2638
|
+
}
|
|
2639
|
+
function extractFencedBlocks(text) {
|
|
2640
|
+
const re = /^[ \t]*(`{3,}|~{3,})([^\n]*)\n([\s\S]*?)^[ \t]*\1[ \t]*$/gm;
|
|
2641
|
+
const blocks = [];
|
|
2642
|
+
let m = re.exec(text);
|
|
2643
|
+
while (m !== null) {
|
|
2644
|
+
const lang = m[2].trim().toLowerCase().split(/\s+/)[0] ?? "";
|
|
2645
|
+
blocks.push({ start: m.index, end: m.index + m[0].length, lang, content: m[3] });
|
|
2646
|
+
m = re.exec(text);
|
|
2647
|
+
}
|
|
2648
|
+
return blocks;
|
|
2649
|
+
}
|
|
2650
|
+
function firstNonBlankLine(text) {
|
|
2651
|
+
const line = text.split(`
|
|
2652
|
+
`).find((l) => l.trim().length > 0);
|
|
2653
|
+
return line?.trim() ?? "";
|
|
2654
|
+
}
|
|
2655
|
+
var FILE_LINE_RE = /\b[\w./-]+\.(?:tsx?|jsx?|mjs|cjs|md|mdx|json|ya?ml|sh|py|toml):\d+\b/g;
|
|
2656
|
+
function checkNoUnpinnedCodeClaims(prBody) {
|
|
2657
|
+
const premiseText = premiseBlockText(prBody);
|
|
2658
|
+
const withoutPremise = premiseText ? prBody.replace(premiseText, "") : prBody;
|
|
2659
|
+
const scanned = stripCode(withoutPremise, { inlineSpans: "keep" });
|
|
2660
|
+
const matches = scanned.match(FILE_LINE_RE) ?? [];
|
|
2661
|
+
if (matches.length === 0)
|
|
2662
|
+
return { status: "pass", errors: [] };
|
|
2663
|
+
return {
|
|
2664
|
+
status: "fail",
|
|
2665
|
+
errors: matches.map((m) => `brief-validation unpinned code claim: "${m}" states a code fact by file-and-line reference outside a \`Premise:\` pin and outside a fenced code block — pin it in \`Premise:\`, or show the executed command and its output instead.`)
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2668
|
+
function extractNumberedSection(prBody, num) {
|
|
2669
|
+
const startRe = new RegExp(`^#{1,4}\\s*(?:\\*\\*)?${num}[a-z]?\\.\\s`, "im");
|
|
2670
|
+
const start = startRe.exec(prBody);
|
|
2671
|
+
if (!start)
|
|
2672
|
+
return null;
|
|
2673
|
+
const afterStart = start.index + start[0].length;
|
|
2674
|
+
const rest = prBody.slice(afterStart);
|
|
2675
|
+
const next = /^#{1,4}\s/m.exec(rest);
|
|
2676
|
+
return next ? rest.slice(0, next.index) : rest;
|
|
2677
|
+
}
|
|
2678
|
+
function commandWordOf(line) {
|
|
2679
|
+
return line.trim().split(/\s+/)[0] ?? "";
|
|
2680
|
+
}
|
|
2681
|
+
function isCommandBlock(content) {
|
|
2682
|
+
return COMMAND_WORDS.includes(commandWordOf(firstNonBlankLine(content)));
|
|
2683
|
+
}
|
|
2684
|
+
function isStep0Block(content) {
|
|
2685
|
+
return firstNonBlankLine(content).startsWith("git worktree add");
|
|
2686
|
+
}
|
|
2687
|
+
function checkCommandsCarryOutput(prBody) {
|
|
2688
|
+
const errors = [];
|
|
2689
|
+
for (const [label2, num] of [
|
|
2690
|
+
["5", 5],
|
|
2691
|
+
["6", 6]
|
|
2692
|
+
]) {
|
|
2693
|
+
const section = extractNumberedSection(prBody, num);
|
|
2694
|
+
if (!section)
|
|
2695
|
+
continue;
|
|
2696
|
+
const blocks = extractFencedBlocks(section);
|
|
2697
|
+
for (let i = 0;i < blocks.length; i++) {
|
|
2698
|
+
const block = blocks[i];
|
|
2699
|
+
if (isStep0Block(block.content) || !isCommandBlock(block.content))
|
|
2700
|
+
continue;
|
|
2701
|
+
const next = blocks[i + 1];
|
|
2702
|
+
if (!next || isCommandBlock(next.content)) {
|
|
2703
|
+
errors.push(`brief-validation commands carry output: §${label2} has a command block ("${firstNonBlankLine(block.content)}") not immediately followed by a non-command output block — the very next fenced block must hold that command's actual output, not another command.`);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
return errors.length === 0 ? { status: "pass", errors: [] } : { status: "fail", errors };
|
|
2708
|
+
}
|
|
2709
|
+
var CONSUMER_TESTS_SENTINEL_RE = /consumer-tests\s*:\s*none\s*[-—–]\s*\S/i;
|
|
2710
|
+
function packagesNamedIn(text) {
|
|
2711
|
+
const re = /\bpackages\/([A-Za-z0-9_-]+)\//g;
|
|
2712
|
+
const pkgs = new Set;
|
|
2713
|
+
let m = re.exec(text);
|
|
2714
|
+
while (m !== null) {
|
|
2715
|
+
pkgs.add(m[1]);
|
|
2716
|
+
m = re.exec(text);
|
|
2717
|
+
}
|
|
2718
|
+
return [...pkgs];
|
|
2719
|
+
}
|
|
2720
|
+
function hasTestPathForConsumer(text, consumerDir) {
|
|
2721
|
+
const escaped = consumerDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2722
|
+
const re = new RegExp(`${escaped}\\/[\\w./-]*\\.test\\.[A-Za-z0-9]+`);
|
|
2723
|
+
return re.test(text);
|
|
2724
|
+
}
|
|
2725
|
+
function checkConsumerTests(prBody, consumersOf) {
|
|
2726
|
+
const section4 = extractNumberedSection(prBody, 4);
|
|
2727
|
+
if (!section4)
|
|
2728
|
+
return { status: "pass", errors: [] };
|
|
2729
|
+
if (CONSUMER_TESTS_SENTINEL_RE.test(section4))
|
|
2730
|
+
return { status: "pass", errors: [] };
|
|
2731
|
+
const errors = [];
|
|
2732
|
+
for (const pkg of packagesNamedIn(section4)) {
|
|
2733
|
+
for (const consumerDir of consumersOf(pkg)) {
|
|
2734
|
+
if (hasTestPathForConsumer(section4, consumerDir))
|
|
2735
|
+
continue;
|
|
2736
|
+
errors.push(`brief-validation consumer tests: §4 names a path under packages/${pkg}/, and ${consumerDir} depends on @attalabs/${pkg}, but no test path under ${consumerDir} is named in §4 — name one, or add \`consumer-tests: none — <reason>\`.`);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
return errors.length === 0 ? { status: "pass", errors: [] } : { status: "fail", errors };
|
|
2740
|
+
}
|
|
2741
|
+
var CHECK_NAME_RE = /\bcheck-[a-z0-9-]+(?:\.ts)?\b/;
|
|
2742
|
+
var FORGE_WRITE_COMMAND_RE = /\bgh\s+(?:pr|issue)\s+(?:create|merge|close|comment|edit|review)\b|\bgit\s+push\b|\bvinaya\s+pr\s+(?:create|report\s+--write)\b/i;
|
|
2743
|
+
var DEFEAT_CASES_RE = /defeat cases\s*:/i;
|
|
2744
|
+
function checkDefeatCases(prBody) {
|
|
2745
|
+
const section4 = extractNumberedSection(prBody, 4);
|
|
2746
|
+
if (!section4)
|
|
2747
|
+
return { status: "pass", errors: [] };
|
|
2748
|
+
if (!CHECK_NAME_RE.test(section4) && !FORGE_WRITE_COMMAND_RE.test(section4))
|
|
2749
|
+
return { status: "pass", errors: [] };
|
|
2750
|
+
const section6 = extractNumberedSection(prBody, 6) ?? "";
|
|
2751
|
+
if (DEFEAT_CASES_RE.test(section6))
|
|
2752
|
+
return { status: "pass", errors: [] };
|
|
2753
|
+
return {
|
|
2754
|
+
status: "fail",
|
|
2755
|
+
errors: [
|
|
2756
|
+
"brief-validation defeat cases: §4 names a check or a forge-writing command, but §6 carries no `Defeat cases:` line — name the inputs that would defeat this check, or that this command must not accidentally trigger on."
|
|
2757
|
+
]
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2423
2760
|
function checkBriefSections(prBody, readTier, options = {}) {
|
|
2424
|
-
const { requireClosesN = true } = options;
|
|
2761
|
+
const { requireClosesN = true, consumersOf = () => [] } = options;
|
|
2425
2762
|
const results = [
|
|
2426
2763
|
checkTierField(prBody, readTier),
|
|
2427
2764
|
checkTestPlan(prBody),
|
|
@@ -2434,40 +2771,249 @@ function checkBriefSections(prBody, readTier, options = {}) {
|
|
|
2434
2771
|
checkAutonomyClause(prBody),
|
|
2435
2772
|
checkProjectField(prBody),
|
|
2436
2773
|
checkForField(prBody),
|
|
2774
|
+
checkNoUnpinnedCodeClaims(prBody),
|
|
2775
|
+
checkNoAgentBoxes(prBody),
|
|
2776
|
+
checkCommandsCarryOutput(prBody),
|
|
2777
|
+
checkConsumerTests(prBody, consumersOf),
|
|
2778
|
+
checkDefeatCases(prBody),
|
|
2437
2779
|
...requireClosesN ? [checkClosesN(prBody)] : []
|
|
2438
2780
|
];
|
|
2439
2781
|
return { errors: results.flatMap((r) => r.errors) };
|
|
2440
2782
|
}
|
|
2441
|
-
// ../../packages/aeg-core/src/
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
{ name: "Suggested agent-class", pattern: "(?:Suggested\\s+)?agent-class" },
|
|
2453
|
-
{ name: "Stop-and-escalate", pattern: "Stop-and-escalate" },
|
|
2454
|
-
{ name: "Docs to keep coherent", pattern: "Docs to keep coherent|§7" }
|
|
2783
|
+
// ../../packages/aeg-core/src/doctrine-portability.ts
|
|
2784
|
+
var DEFAULT_SHIPS_PREFIX = "aeg-root/";
|
|
2785
|
+
var STATIC_PORTABLE_PREFIXES = [
|
|
2786
|
+
"roles/",
|
|
2787
|
+
"contracts/",
|
|
2788
|
+
"skills/",
|
|
2789
|
+
".github/",
|
|
2790
|
+
".vinaya/",
|
|
2791
|
+
".claude/",
|
|
2792
|
+
".git/",
|
|
2793
|
+
".husky/"
|
|
2455
2794
|
];
|
|
2456
|
-
var
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2795
|
+
var EXEMPT_LITERALS = new Set([
|
|
2796
|
+
"path/inside/the/shipped/diff.ts",
|
|
2797
|
+
"path/inside/the/surface.ts",
|
|
2798
|
+
"apps/x/specs/..."
|
|
2799
|
+
]);
|
|
2800
|
+
var CITED_PATH_PATTERN = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]*)+$/;
|
|
2801
|
+
var NON_PATH_TOP_SEGMENTS = new Set(["origin", "refs", "HEAD", "vinaya", "fix"]);
|
|
2802
|
+
var VENDOR_NAME_SOURCE = "\\bclaude code\\b|\\bclaude\\b|\\banthropic\\b|\\bchatgpt\\b|\\bopenai\\b|\\bgpt\\b|\\bgemini\\b|\\bcodex\\b|\\bgrok\\b|\\bdeepseek\\b|\\bopus\\b|\\bsonnet\\b|\\bhaiku\\b";
|
|
2803
|
+
var VENDOR_EXAMPLE_START = /<!--\s*AEG:VENDOR-EXAMPLE:START\s*-->/;
|
|
2804
|
+
var VENDOR_EXAMPLE_END = /<!--\s*AEG:VENDOR-EXAMPLE:END\s*-->/;
|
|
2805
|
+
function maskVendorExampleRegion(masked) {
|
|
2806
|
+
const start = VENDOR_EXAMPLE_START.exec(masked);
|
|
2807
|
+
if (!start)
|
|
2808
|
+
return masked;
|
|
2809
|
+
const afterStart = start.index + start[0].length;
|
|
2810
|
+
const end = VENDOR_EXAMPLE_END.exec(masked.slice(afterStart));
|
|
2811
|
+
if (!end)
|
|
2812
|
+
return masked;
|
|
2813
|
+
const regionEnd = afterStart + end.index + end[0].length;
|
|
2814
|
+
const region = masked.slice(start.index, regionEnd);
|
|
2815
|
+
const blanked = region.replace(/[^\n]/g, " ");
|
|
2816
|
+
return masked.slice(0, start.index) + blanked + masked.slice(regionEnd);
|
|
2817
|
+
}
|
|
2818
|
+
function extractVendorMentions(content) {
|
|
2819
|
+
const scoped = maskVendorExampleRegion(maskCode(content));
|
|
2820
|
+
const found = [];
|
|
2821
|
+
const pattern = new RegExp(VENDOR_NAME_SOURCE, "gi");
|
|
2822
|
+
let match = pattern.exec(scoped);
|
|
2823
|
+
while (match !== null) {
|
|
2824
|
+
found.push({ name: match[0], index: match.index });
|
|
2825
|
+
match = pattern.exec(scoped);
|
|
2467
2826
|
}
|
|
2468
|
-
return
|
|
2827
|
+
return found;
|
|
2469
2828
|
}
|
|
2470
|
-
function
|
|
2829
|
+
function extractCitedPaths(content) {
|
|
2830
|
+
const found = [];
|
|
2831
|
+
const spanPattern = /`([^`\n]+)`/g;
|
|
2832
|
+
let match = spanPattern.exec(content);
|
|
2833
|
+
while (match !== null) {
|
|
2834
|
+
const cited = match[1] ?? "";
|
|
2835
|
+
const topSegment = cited.slice(0, cited.indexOf("/"));
|
|
2836
|
+
if (CITED_PATH_PATTERN.test(cited) && !NON_PATH_TOP_SEGMENTS.has(topSegment)) {
|
|
2837
|
+
found.push({ cited, index: match.index });
|
|
2838
|
+
}
|
|
2839
|
+
match = spanPattern.exec(content);
|
|
2840
|
+
}
|
|
2841
|
+
return found;
|
|
2842
|
+
}
|
|
2843
|
+
function lineAtIndex(content, index) {
|
|
2844
|
+
let line = 1;
|
|
2845
|
+
for (let i = 0;i < index; i++) {
|
|
2846
|
+
if (content.charCodeAt(i) === 10)
|
|
2847
|
+
line++;
|
|
2848
|
+
}
|
|
2849
|
+
return line;
|
|
2850
|
+
}
|
|
2851
|
+
function isPortable(cited, shipsPrefix) {
|
|
2852
|
+
if (cited.startsWith(shipsPrefix))
|
|
2853
|
+
return true;
|
|
2854
|
+
return STATIC_PORTABLE_PREFIXES.some((prefix) => cited.startsWith(prefix));
|
|
2855
|
+
}
|
|
2856
|
+
function checkDoctrinePortability(files, shipsPrefix = DEFAULT_SHIPS_PREFIX) {
|
|
2857
|
+
const findings = [];
|
|
2858
|
+
for (const file of files) {
|
|
2859
|
+
if (!file.path.startsWith(shipsPrefix))
|
|
2860
|
+
continue;
|
|
2861
|
+
for (const { cited, index } of extractCitedPaths(file.content)) {
|
|
2862
|
+
if (EXEMPT_LITERALS.has(cited))
|
|
2863
|
+
continue;
|
|
2864
|
+
if (isPortable(cited, shipsPrefix))
|
|
2865
|
+
continue;
|
|
2866
|
+
findings.push({
|
|
2867
|
+
file: file.path,
|
|
2868
|
+
line: lineAtIndex(file.content, index),
|
|
2869
|
+
cited,
|
|
2870
|
+
message: `cites "${cited}", a path that only exists in the authoring repository — not portable doctrine`,
|
|
2871
|
+
kind: "path"
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
for (const { name, index } of extractVendorMentions(file.content)) {
|
|
2875
|
+
findings.push({
|
|
2876
|
+
file: file.path,
|
|
2877
|
+
line: lineAtIndex(file.content, index),
|
|
2878
|
+
cited: name,
|
|
2879
|
+
message: `names "${name}" outside the one fenced vendor-example home (an <!-- AEG:VENDOR-EXAMPLE:START --> … <!-- AEG:VENDOR-EXAMPLE:END --> pair) — portable doctrine refers to a host generically everywhere else`,
|
|
2880
|
+
kind: "vendor-name"
|
|
2881
|
+
});
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
return findings;
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// ../../packages/aeg-core/src/doctrine-no-procedures.ts
|
|
2888
|
+
function vendorExampleSpan(content) {
|
|
2889
|
+
const start = VENDOR_EXAMPLE_START.exec(content);
|
|
2890
|
+
if (!start)
|
|
2891
|
+
return null;
|
|
2892
|
+
const afterStart = start.index + start[0].length;
|
|
2893
|
+
const end = VENDOR_EXAMPLE_END.exec(content.slice(afterStart));
|
|
2894
|
+
if (!end)
|
|
2895
|
+
return null;
|
|
2896
|
+
return { start: start.index, end: afterStart + end.index + end[0].length };
|
|
2897
|
+
}
|
|
2898
|
+
function isCommandLine(line) {
|
|
2899
|
+
const word = line.trim().split(/\s+/)[0] ?? "";
|
|
2900
|
+
return COMMAND_WORDS.includes(word);
|
|
2901
|
+
}
|
|
2902
|
+
var SHELL_LANGS = new Set(["", "sh", "bash", "shell", "console"]);
|
|
2903
|
+
function procedureLineNumberAt(content, index) {
|
|
2904
|
+
return content.slice(0, index).split(`
|
|
2905
|
+
`).length;
|
|
2906
|
+
}
|
|
2907
|
+
function isTemplatePath(path) {
|
|
2908
|
+
return /(^|\/)templates\//.test(path);
|
|
2909
|
+
}
|
|
2910
|
+
function checkDoctrineNoProcedures(files) {
|
|
2911
|
+
const findings = [];
|
|
2912
|
+
for (const file of files) {
|
|
2913
|
+
if (isTemplatePath(file.path))
|
|
2914
|
+
continue;
|
|
2915
|
+
const span = vendorExampleSpan(file.content);
|
|
2916
|
+
for (const block of extractFencedBlocks(file.content)) {
|
|
2917
|
+
if (span && block.start >= span.start && block.end <= span.end)
|
|
2918
|
+
continue;
|
|
2919
|
+
if (!SHELL_LANGS.has(block.lang))
|
|
2920
|
+
continue;
|
|
2921
|
+
const commandLineCount = block.content.split(`
|
|
2922
|
+
`).filter(isCommandLine).length;
|
|
2923
|
+
if (commandLineCount >= 2) {
|
|
2924
|
+
findings.push({
|
|
2925
|
+
file: file.path,
|
|
2926
|
+
line: procedureLineNumberAt(file.content, block.start),
|
|
2927
|
+
message: `this fenced block has ${commandLineCount} shell-command lines — this sequence is a \`vinaya\` command, name it (or move it inside the \`AEG:VENDOR-EXAMPLE\` anchor / a \`templates/\` file).`
|
|
2928
|
+
});
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
return findings;
|
|
2933
|
+
}
|
|
2934
|
+
// ../../packages/aeg-core/src/pr-report-density.ts
|
|
2935
|
+
function headingSectionBody(prBody, heading) {
|
|
2936
|
+
const headingRe = new RegExp(`^##\\s+${heading}\\s*$`, "im");
|
|
2937
|
+
const m = headingRe.exec(prBody);
|
|
2938
|
+
if (!m)
|
|
2939
|
+
return null;
|
|
2940
|
+
const start = m.index + m[0].length;
|
|
2941
|
+
const rest = prBody.slice(start);
|
|
2942
|
+
const nextHeading = /^#{1,2}\s/m.exec(rest);
|
|
2943
|
+
return nextHeading ? rest.slice(0, nextHeading.index) : rest;
|
|
2944
|
+
}
|
|
2945
|
+
function stripAnchorBlocks(sectionText) {
|
|
2946
|
+
let result = sectionText;
|
|
2947
|
+
for (const field of ["CLOSES", "PROJECT", "TIER", "PREMISE", "TEST-PLAN", "EVIDENCE"]) {
|
|
2948
|
+
for (;; ) {
|
|
2949
|
+
const bounds = anchoredRegionBounds(result, field);
|
|
2950
|
+
if (!bounds)
|
|
2951
|
+
break;
|
|
2952
|
+
result = result.slice(0, bounds.outerStart) + result.slice(bounds.outerEnd);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
return result;
|
|
2956
|
+
}
|
|
2957
|
+
function paragraphCount(sectionText) {
|
|
2958
|
+
const stripped = stripCode(stripAnchorBlocks(sectionText)).trim();
|
|
2959
|
+
if (stripped.length === 0)
|
|
2960
|
+
return 0;
|
|
2961
|
+
return stripped.split(/\n[ \t]*\n/).map((block) => block.trim()).filter((block) => block.length > 0).length;
|
|
2962
|
+
}
|
|
2963
|
+
function checkSectionDensity(prBody, heading) {
|
|
2964
|
+
const body = headingSectionBody(prBody, heading);
|
|
2965
|
+
if (body === null)
|
|
2966
|
+
return { status: "pass", errors: [] };
|
|
2967
|
+
const count = paragraphCount(body);
|
|
2968
|
+
if (count <= 1)
|
|
2969
|
+
return { status: "pass", errors: [] };
|
|
2970
|
+
return {
|
|
2971
|
+
status: "fail",
|
|
2972
|
+
errors: [
|
|
2973
|
+
`pr-report-density ${heading}: "## ${heading}" holds ${count} paragraphs — the canonical PR-report form (aeg-root/roles/developer.md § PR body) requires exactly one. Collapse it to one paragraph; move the rest into a section of your own below the canonical four ("Add anything you want beneath the four sections", developer.md), a commit message, or the changeset — never into the AEG:EVIDENCE block, which is emitted only, never hand-typed.`
|
|
2974
|
+
]
|
|
2975
|
+
};
|
|
2976
|
+
}
|
|
2977
|
+
function checkSummaryDensity(prBody) {
|
|
2978
|
+
return checkSectionDensity(prBody, "Summary");
|
|
2979
|
+
}
|
|
2980
|
+
function checkScopeDensity(prBody) {
|
|
2981
|
+
return checkSectionDensity(prBody, "Scope");
|
|
2982
|
+
}
|
|
2983
|
+
function checkPrReportDensity(prBody) {
|
|
2984
|
+
const results = [checkSummaryDensity(prBody), checkScopeDensity(prBody)];
|
|
2985
|
+
return { errors: results.flatMap((r) => r.errors) };
|
|
2986
|
+
}
|
|
2987
|
+
// ../../packages/aeg-core/src/issue-validation.ts
|
|
2988
|
+
function hasRationaleField(body, labelPattern) {
|
|
2989
|
+
const re = new RegExp(`(?:\\*\\*|^#{1,4}\\s+)\\s*(?:${labelPattern})`, "im");
|
|
2990
|
+
return re.test(body);
|
|
2991
|
+
}
|
|
2992
|
+
var RATIONALE_FIELDS = [
|
|
2993
|
+
{ name: "Boundary", pattern: "Boundary" },
|
|
2994
|
+
{ name: "Sizing", pattern: "Sizing" },
|
|
2995
|
+
{ name: "Project(s) + blast radius", pattern: "Project\\(s\\)|Project(?:s)?\\s*\\+|blast radius" },
|
|
2996
|
+
{ name: "Dependency rationale", pattern: "Dependency rationale|Depends[- ]on" },
|
|
2997
|
+
{ name: "Traps to avoid", pattern: "Traps" },
|
|
2998
|
+
{ name: "Suggested agent-class", pattern: "(?:Suggested\\s+)?agent-class" },
|
|
2999
|
+
{ name: "Stop-and-escalate", pattern: "Stop-and-escalate" },
|
|
3000
|
+
{ name: "Docs to keep coherent", pattern: "Docs to keep coherent|§7" }
|
|
3001
|
+
];
|
|
3002
|
+
var DEPENDENCY_RATIONALE_FIELD_NAME = "Dependency rationale";
|
|
3003
|
+
function checkIssueRationale(body) {
|
|
3004
|
+
const errors = [];
|
|
3005
|
+
for (const f of RATIONALE_FIELDS) {
|
|
3006
|
+
if (!hasRationaleField(body, f.pattern)) {
|
|
3007
|
+
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).`);
|
|
3008
|
+
continue;
|
|
3009
|
+
}
|
|
3010
|
+
if (f.name === DEPENDENCY_RATIONALE_FIELD_NAME && !SECTION_HEADER.test(body)) {
|
|
3011
|
+
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.");
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
return { status: errors.length > 0 ? "fail" : "pass", errors };
|
|
3015
|
+
}
|
|
3016
|
+
function isTaskIssueLabelSet(labels) {
|
|
2471
3017
|
return hasLabel("tranche", labels);
|
|
2472
3018
|
}
|
|
2473
3019
|
var TYPE_LABEL_IDS = LABELS.filter((l) => l.category === "type").map((l) => l.id);
|
|
@@ -2689,6 +3235,17 @@ function checkD1(entries, issueToEntry, taskToEntry) {
|
|
|
2689
3235
|
const depEntry = resolveDepEntry(dep, e.trancheSlug, issueToEntry, taskToEntry);
|
|
2690
3236
|
if (!depEntry)
|
|
2691
3237
|
continue;
|
|
3238
|
+
const sameTask = depEntry.trancheSlug === e.trancheSlug && depEntry.task.id === e.task.id;
|
|
3239
|
+
const sameIssue = depEntry.task.issue !== null && e.task.issue !== null && depEntry.task.issue === e.task.issue;
|
|
3240
|
+
if (sameTask || sameIssue) {
|
|
3241
|
+
failures.push({
|
|
3242
|
+
issue: e.task.issue,
|
|
3243
|
+
tranche: e.trancheSlug,
|
|
3244
|
+
task: e.task.id,
|
|
3245
|
+
reason: `INTERNAL: parsed a self-dependency (depends-on ${dep}) — this is a parser bug in parseRationaleDeps, not a real dependency. Please report it upstream.`
|
|
3246
|
+
});
|
|
3247
|
+
continue;
|
|
3248
|
+
}
|
|
2692
3249
|
const depFacts = depEntry.facts;
|
|
2693
3250
|
const depClosed = depFacts?.issueState === "closed";
|
|
2694
3251
|
if (!depClosed) {
|
|
@@ -2832,41 +3389,24 @@ function checkClosesN2(branch, prBody, trancheFiles, taskIssueRefs) {
|
|
|
2832
3389
|
}
|
|
2833
3390
|
return { ok: true, expectedIssue };
|
|
2834
3391
|
}
|
|
2835
|
-
// ../../packages/aeg-core/src/verdict-extraction.ts
|
|
2836
|
-
var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
|
|
2837
|
-
function extractHeadSha(comment) {
|
|
2838
|
-
const m = comment.match(HEAD_SHA_PATTERN);
|
|
2839
|
-
return m ? m[1].toLowerCase() : null;
|
|
2840
|
-
}
|
|
2841
|
-
function extractVerdict(comments, valuePattern, missingLabel) {
|
|
2842
|
-
const clearHits = comments.filter((c) => valuePattern.test(c));
|
|
2843
|
-
if (clearHits.length > 0) {
|
|
2844
|
-
const latest = clearHits[clearHits.length - 1];
|
|
2845
|
-
const m = latest.match(valuePattern);
|
|
2846
|
-
return {
|
|
2847
|
-
value: m[1].toUpperCase().replace(/[_-]/g, " "),
|
|
2848
|
-
headSha: extractHeadSha(latest),
|
|
2849
|
-
danglingNote: null
|
|
2850
|
-
};
|
|
2851
|
-
}
|
|
2852
|
-
return {
|
|
2853
|
-
value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
|
|
2854
|
-
headSha: null,
|
|
2855
|
-
danglingNote: `no ${missingLabel} verdict comment found on this PR`
|
|
2856
|
-
};
|
|
2857
|
-
}
|
|
2858
|
-
function extractCodeReviewVerdict(comments) {
|
|
2859
|
-
return extractVerdict(comments, /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(APPROVE|REQUEST[ _-]?CHANGES|LGTM)(?![A-Za-z0-9])/im, "code-reviewer");
|
|
2860
|
-
}
|
|
2861
|
-
function extractSecurityReviewVerdict(comments) {
|
|
2862
|
-
return extractVerdict(comments, /^[ \t]*(?:\*{1,3}|_{1,3})?VERDICT:\s*(PASS|FAIL)(?![A-Za-z0-9])/im, "security-review");
|
|
2863
|
-
}
|
|
2864
3392
|
// ../../packages/aeg-core/src/review-gate.ts
|
|
2865
3393
|
function isBoundToHead(extraction, headSha) {
|
|
2866
3394
|
if (!extraction.headSha)
|
|
2867
3395
|
return false;
|
|
2868
3396
|
return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase());
|
|
2869
3397
|
}
|
|
3398
|
+
function isBoundByPatchIdentity(extraction, headSha, patchIdOf) {
|
|
3399
|
+
if (patchIdOf === undefined || !extraction.headSha)
|
|
3400
|
+
return false;
|
|
3401
|
+
const judged = patchIdOf(extraction.headSha);
|
|
3402
|
+
const current = patchIdOf(headSha);
|
|
3403
|
+
if (judged === null || current === null)
|
|
3404
|
+
return false;
|
|
3405
|
+
return judged === current;
|
|
3406
|
+
}
|
|
3407
|
+
function isBoundToPatch(extraction, headSha, patchIdOf) {
|
|
3408
|
+
return isBoundToHead(extraction, headSha) || isBoundByPatchIdentity(extraction, headSha, patchIdOf);
|
|
3409
|
+
}
|
|
2870
3410
|
function checkReviewGate(input) {
|
|
2871
3411
|
const principalAllowlist = input.principalAllowlist ?? PRINCIPAL_ALLOWLIST;
|
|
2872
3412
|
const waived = isWaiverLabelActorVerified({
|
|
@@ -2882,6 +3422,8 @@ function checkReviewGate(input) {
|
|
|
2882
3422
|
waived: true
|
|
2883
3423
|
};
|
|
2884
3424
|
}
|
|
3425
|
+
const reportedMechanicalChecks = input.mechanicalChecks.filter((c) => c.bucket !== "skipping" && c.bucket !== "neutral");
|
|
3426
|
+
const mechanicalChecksClean = reportedMechanicalChecks.length > 0 && reportedMechanicalChecks.every((c) => c.bucket === "pass");
|
|
2885
3427
|
const verified = input.comments.filter((c) => isPrincipal(c.author, principalAllowlist));
|
|
2886
3428
|
const ignoredCount = input.comments.filter((c) => !isPrincipal(c.author, principalAllowlist) && c.body.includes("VERDICT")).length;
|
|
2887
3429
|
const verifiedBodies = verified.map((c) => c.body);
|
|
@@ -2889,12 +3431,12 @@ function checkReviewGate(input) {
|
|
|
2889
3431
|
const security = extractSecurityReviewVerdict(verifiedBodies);
|
|
2890
3432
|
const codeReviewClean = codeReview.value === "APPROVE";
|
|
2891
3433
|
const securityClean = security.value === "PASS";
|
|
2892
|
-
const codeReviewBound =
|
|
2893
|
-
const securityBound =
|
|
2894
|
-
if (codeReviewClean && codeReviewBound && securityClean && securityBound) {
|
|
3434
|
+
const codeReviewBound = isBoundToPatch(codeReview, input.headSha, input.patchIdOf);
|
|
3435
|
+
const securityBound = isBoundToPatch(security, input.headSha, input.patchIdOf);
|
|
3436
|
+
if (codeReviewClean && codeReviewBound && securityClean && securityBound && mechanicalChecksClean) {
|
|
2895
3437
|
return {
|
|
2896
3438
|
verdict: "pass",
|
|
2897
|
-
reason: `code-reviewer verdict is a clean APPROVE and security-review verdict is a clean PASS, both covering head ${input.headSha}.`,
|
|
3439
|
+
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.`,
|
|
2898
3440
|
waived: false
|
|
2899
3441
|
};
|
|
2900
3442
|
}
|
|
@@ -2909,6 +3451,9 @@ function checkReviewGate(input) {
|
|
|
2909
3451
|
} else if (!securityBound) {
|
|
2910
3452
|
problems.push(`the newest security-review verdict covers ${security.headSha ?? "no recorded commit"}, head is ${input.headSha}`);
|
|
2911
3453
|
}
|
|
3454
|
+
if (!mechanicalChecksClean) {
|
|
3455
|
+
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(", ")}`);
|
|
3456
|
+
}
|
|
2912
3457
|
const ignoredNote = ignoredCount > 0 ? ` ${ignoredCount} verdict-shaped comment(s) from authors outside the principal allowlist were ignored.` : "";
|
|
2913
3458
|
return {
|
|
2914
3459
|
verdict: "fail",
|
|
@@ -3188,72 +3733,6 @@ function compareToBaseline(current, baseline) {
|
|
|
3188
3733
|
perTool
|
|
3189
3734
|
};
|
|
3190
3735
|
}
|
|
3191
|
-
// ../../packages/aeg-core/src/doctrine-portability.ts
|
|
3192
|
-
var DEFAULT_SHIPS_PREFIX = "aeg-root/";
|
|
3193
|
-
var STATIC_PORTABLE_PREFIXES = [
|
|
3194
|
-
"roles/",
|
|
3195
|
-
"contracts/",
|
|
3196
|
-
"skills/",
|
|
3197
|
-
".github/",
|
|
3198
|
-
".vinaya/",
|
|
3199
|
-
".claude/",
|
|
3200
|
-
".git/",
|
|
3201
|
-
".husky/"
|
|
3202
|
-
];
|
|
3203
|
-
var EXEMPT_LITERALS = new Set([
|
|
3204
|
-
"path/inside/the/shipped/diff.ts",
|
|
3205
|
-
"path/inside/the/surface.ts",
|
|
3206
|
-
"apps/x/specs/..."
|
|
3207
|
-
]);
|
|
3208
|
-
var CITED_PATH_PATTERN = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]*)+$/;
|
|
3209
|
-
var NON_PATH_TOP_SEGMENTS = new Set(["origin", "refs", "HEAD", "vinaya", "fix"]);
|
|
3210
|
-
function extractCitedPaths(content) {
|
|
3211
|
-
const found = [];
|
|
3212
|
-
const spanPattern = /`([^`\n]+)`/g;
|
|
3213
|
-
let match = spanPattern.exec(content);
|
|
3214
|
-
while (match !== null) {
|
|
3215
|
-
const cited = match[1] ?? "";
|
|
3216
|
-
const topSegment = cited.slice(0, cited.indexOf("/"));
|
|
3217
|
-
if (CITED_PATH_PATTERN.test(cited) && !NON_PATH_TOP_SEGMENTS.has(topSegment)) {
|
|
3218
|
-
found.push({ cited, index: match.index });
|
|
3219
|
-
}
|
|
3220
|
-
match = spanPattern.exec(content);
|
|
3221
|
-
}
|
|
3222
|
-
return found;
|
|
3223
|
-
}
|
|
3224
|
-
function lineAtIndex(content, index) {
|
|
3225
|
-
let line = 1;
|
|
3226
|
-
for (let i = 0;i < index; i++) {
|
|
3227
|
-
if (content.charCodeAt(i) === 10)
|
|
3228
|
-
line++;
|
|
3229
|
-
}
|
|
3230
|
-
return line;
|
|
3231
|
-
}
|
|
3232
|
-
function isPortable(cited, shipsPrefix) {
|
|
3233
|
-
if (cited.startsWith(shipsPrefix))
|
|
3234
|
-
return true;
|
|
3235
|
-
return STATIC_PORTABLE_PREFIXES.some((prefix) => cited.startsWith(prefix));
|
|
3236
|
-
}
|
|
3237
|
-
function checkDoctrinePortability(files, shipsPrefix = DEFAULT_SHIPS_PREFIX) {
|
|
3238
|
-
const findings = [];
|
|
3239
|
-
for (const file of files) {
|
|
3240
|
-
if (!file.path.startsWith(shipsPrefix))
|
|
3241
|
-
continue;
|
|
3242
|
-
for (const { cited, index } of extractCitedPaths(file.content)) {
|
|
3243
|
-
if (EXEMPT_LITERALS.has(cited))
|
|
3244
|
-
continue;
|
|
3245
|
-
if (isPortable(cited, shipsPrefix))
|
|
3246
|
-
continue;
|
|
3247
|
-
findings.push({
|
|
3248
|
-
file: file.path,
|
|
3249
|
-
line: lineAtIndex(file.content, index),
|
|
3250
|
-
cited,
|
|
3251
|
-
message: `cites "${cited}", a path that only exists in the authoring repository — not portable doctrine`
|
|
3252
|
-
});
|
|
3253
|
-
}
|
|
3254
|
-
}
|
|
3255
|
-
return findings;
|
|
3256
|
-
}
|
|
3257
3736
|
// ../../packages/aeg-core/src/reader-resolvable-prose.ts
|
|
3258
3737
|
var SHIPS_PREFIX = "aeg-root/";
|
|
3259
3738
|
function shipsArchivePrefix(shipsPrefix) {
|
|
@@ -3589,6 +4068,12 @@ function scanRetiredVocabulary(files) {
|
|
|
3589
4068
|
function isHandClosedByRecognizedPrincipal(dep, principalAllowlist) {
|
|
3590
4069
|
return dep.issueState === "closed" && dep.stateReason === "completed" && isPrincipal(dep.closedByActor ?? null, principalAllowlist);
|
|
3591
4070
|
}
|
|
4071
|
+
function isSelfDependency(dep, task, issue) {
|
|
4072
|
+
if (dep.issue !== null && issue !== null && dep.issue === issue.number)
|
|
4073
|
+
return true;
|
|
4074
|
+
const bare = dep.id.trim().replace(/^#/, "");
|
|
4075
|
+
return bare.toLowerCase() === task.id.trim().toLowerCase();
|
|
4076
|
+
}
|
|
3592
4077
|
function checkDispatchReadiness(input) {
|
|
3593
4078
|
const { trancheSlug, task } = input;
|
|
3594
4079
|
const taskLabel = `task ${task.id} (tranche ${trancheSlug})`;
|
|
@@ -3603,6 +4088,11 @@ function checkDispatchReadiness(input) {
|
|
|
3603
4088
|
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.`);
|
|
3604
4089
|
}
|
|
3605
4090
|
for (const dep of input.dependsOn) {
|
|
4091
|
+
if (isSelfDependency(dep, task, input.issue)) {
|
|
4092
|
+
const issueStr = input.issue !== null ? `#${input.issue.number}` : "?";
|
|
4093
|
+
blockers.push(`dispatch-gate INTERNAL: parsed a self-dependency for task ${task.id} (${issueStr}) — this is a parser bug in parseRationaleDeps, not a real dependency. Please report it upstream. Re-run once the rationale is corrected or the fix ships.`);
|
|
4094
|
+
continue;
|
|
4095
|
+
}
|
|
3606
4096
|
if (dep.resolved === false) {
|
|
3607
4097
|
blockers.push(`dispatch-gate depends-on: ${taskLabel} depends on "${dep.id}", which is UNRESOLVABLE — the resolver could not find a matching tranche/task/Issue for this edge (not a claim about merge status). Not dispatchable until the edge is corrected.`);
|
|
3608
4098
|
continue;
|
|
@@ -3842,27 +4332,27 @@ function evaluateTestPlanGate(body, branch) {
|
|
|
3842
4332
|
};
|
|
3843
4333
|
}
|
|
3844
4334
|
const checkboxLines = section.split(`
|
|
3845
|
-
`).map((line) => line.trimStart()).filter((line) => /^[-*]\s+\[[ xX]\]
|
|
4335
|
+
`).map((line) => line.trimStart()).filter((line) => /^[-*]\s+\[[ xX]\]\s*\*{0,2}\[principal\]\*{0,2}/i.test(line));
|
|
3846
4336
|
if (checkboxLines.length === 0) {
|
|
3847
4337
|
return {
|
|
3848
4338
|
verdict: "pass",
|
|
3849
4339
|
messages: [
|
|
3850
|
-
"Test Plan section found but has no checkbox items.",
|
|
4340
|
+
"Test Plan section found but has no `[principal]` checkbox items.",
|
|
3851
4341
|
"PASS (advisory) — the section may be empty or expressed in prose; verification is the Principal's call."
|
|
3852
4342
|
]
|
|
3853
4343
|
};
|
|
3854
4344
|
}
|
|
3855
4345
|
const unchecked = checkboxLines.filter((line) => /^[-*]\s+\[\s\]/.test(line));
|
|
3856
4346
|
const checked = checkboxLines.filter((line) => /^[-*]\s+\[[xX]\]/.test(line));
|
|
3857
|
-
const messages = [`Test Plan items: ${checked.length} ticked, ${unchecked.length} unticked.`];
|
|
4347
|
+
const messages = [`Test Plan [principal] items: ${checked.length} ticked, ${unchecked.length} unticked.`];
|
|
3858
4348
|
if (unchecked.length > 0) {
|
|
3859
|
-
messages.push("", "FAIL — the following Test Plan items are unticked:");
|
|
4349
|
+
messages.push("", "FAIL — the following [principal] Test Plan items are unticked:");
|
|
3860
4350
|
for (const line of unchecked)
|
|
3861
4351
|
messages.push(` ${line}`);
|
|
3862
|
-
messages.push("", "Per aeg-root/roles/developer.md (Verification), a PR is not mergeable while any Test Plan box is unticked.", "
|
|
4352
|
+
messages.push("", "Per aeg-root/roles/developer.md (Verification), a PR is not mergeable while any [principal] Test Plan box is unticked.", "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.");
|
|
3863
4353
|
return { verdict: "fail", messages };
|
|
3864
4354
|
}
|
|
3865
|
-
messages.push("All Test Plan items ticked — runtime verification complete.", "PASS.");
|
|
4355
|
+
messages.push("All [principal] Test Plan items ticked — runtime verification complete.", "PASS.");
|
|
3866
4356
|
return { verdict: "pass", messages };
|
|
3867
4357
|
}
|
|
3868
4358
|
// ../../packages/aeg-core/src/workspace-escape.ts
|
|
@@ -3942,6 +4432,38 @@ function findWorkspaceEscapes(files, knownPaths, workspaceDirs = DEFAULT_WORKSPA
|
|
|
3942
4432
|
}
|
|
3943
4433
|
return findings;
|
|
3944
4434
|
}
|
|
4435
|
+
// src/checks/contract.ts
|
|
4436
|
+
var CHECK_SCHEMA_VERSION = 1;
|
|
4437
|
+
function emitCheckError(error) {
|
|
4438
|
+
process.stderr.write(`${JSON.stringify(error)}
|
|
4439
|
+
`);
|
|
4440
|
+
}
|
|
4441
|
+
|
|
4442
|
+
// src/checks/runner.ts
|
|
4443
|
+
var activeKillers = new Set;
|
|
4444
|
+
|
|
4445
|
+
// src/lib/numstat.ts
|
|
4446
|
+
function summariseNumstat(numstat) {
|
|
4447
|
+
const rows = numstat.split(`
|
|
4448
|
+
`).filter((l) => l.trim() !== "");
|
|
4449
|
+
let added = 0;
|
|
4450
|
+
let deleted = 0;
|
|
4451
|
+
let binary = 0;
|
|
4452
|
+
for (const row of rows) {
|
|
4453
|
+
const [a, d] = row.split("\t");
|
|
4454
|
+
if (a === "-" || d === "-") {
|
|
4455
|
+
binary++;
|
|
4456
|
+
continue;
|
|
4457
|
+
}
|
|
4458
|
+
added += Number(a) || 0;
|
|
4459
|
+
deleted += Number(d) || 0;
|
|
4460
|
+
}
|
|
4461
|
+
const files = `${rows.length} file${rows.length === 1 ? "" : "s"} changed`;
|
|
4462
|
+
const bin = binary > 0 ? `, ${binary} binary` : "";
|
|
4463
|
+
return `${files}, ${added} insertion${added === 1 ? "" : "s"}(+), ${deleted} deletion${deleted === 1 ? "" : "s"}(-)${bin}`;
|
|
4464
|
+
}
|
|
4465
|
+
var EVIDENCE_SUMMARY_PREFIX = "Summary: ";
|
|
4466
|
+
|
|
3945
4467
|
// src/checks/scan-context.ts
|
|
3946
4468
|
var ZERO_WIDTH = /[\u200B-\u200D\u2060\uFEFF]/g;
|
|
3947
4469
|
var HTML_ENTITIES = {
|
|
@@ -3995,10 +4517,386 @@ function summaryLineIndex(maskedRegion) {
|
|
|
3995
4517
|
return index === -1 ? null : index;
|
|
3996
4518
|
}
|
|
3997
4519
|
|
|
4520
|
+
// src/lib/package-root.ts
|
|
4521
|
+
import { existsSync } from "node:fs";
|
|
4522
|
+
import { dirname, join } from "node:path";
|
|
4523
|
+
import { fileURLToPath } from "node:url";
|
|
4524
|
+
function packageRoot(moduleUrl) {
|
|
4525
|
+
let dir = dirname(fileURLToPath(moduleUrl));
|
|
4526
|
+
while (!existsSync(join(dir, "package.json"))) {
|
|
4527
|
+
if (existsSync(join(dir, ".git")))
|
|
4528
|
+
break;
|
|
4529
|
+
const parent = dirname(dir);
|
|
4530
|
+
if (parent === dir)
|
|
4531
|
+
break;
|
|
4532
|
+
dir = parent;
|
|
4533
|
+
}
|
|
4534
|
+
return dir;
|
|
4535
|
+
}
|
|
4536
|
+
|
|
4537
|
+
// src/lib/config.ts
|
|
4538
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
4539
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, realpathSync, writeFileSync } from "node:fs";
|
|
4540
|
+
import { homedir } from "node:os";
|
|
4541
|
+
import { dirname as dirname2, join as join2, resolve } from "node:path";
|
|
4542
|
+
import { z } from "zod";
|
|
4543
|
+
|
|
4544
|
+
// src/lib/agent-vendors.ts
|
|
4545
|
+
var AGENT_VENDORS = ["skills", "claude", "gemini"];
|
|
4546
|
+
|
|
4547
|
+
// src/lib/config.ts
|
|
4548
|
+
var EnvEntrySchema = z.union([
|
|
4549
|
+
z.literal(true),
|
|
4550
|
+
z.object({ optional: z.literal(true) }),
|
|
4551
|
+
z.object({ anyOf: z.array(z.string()).min(2) }),
|
|
4552
|
+
z.string()
|
|
4553
|
+
]);
|
|
4554
|
+
var CheckEntrySchema = z.object({
|
|
4555
|
+
run: z.string(),
|
|
4556
|
+
scope: z.enum(["diff", "full"]),
|
|
4557
|
+
include: z.array(z.string()).optional(),
|
|
4558
|
+
args: z.array(z.string()).optional(),
|
|
4559
|
+
timeoutMs: z.number().optional(),
|
|
4560
|
+
env: z.record(z.string(), EnvEntrySchema).optional(),
|
|
4561
|
+
requiresOpenPr: z.boolean().optional(),
|
|
4562
|
+
ownWorkflow: z.boolean().optional()
|
|
4563
|
+
}).superRefine((entry2, ctx) => {
|
|
4564
|
+
if (!entry2.env)
|
|
4565
|
+
return;
|
|
4566
|
+
for (const [key, value] of Object.entries(entry2.env)) {
|
|
4567
|
+
if (typeof value !== "object" || value === null || !("anyOf" in value))
|
|
4568
|
+
continue;
|
|
4569
|
+
const members = value.anyOf;
|
|
4570
|
+
if (new Set(members).size !== members.length) {
|
|
4571
|
+
ctx.addIssue({
|
|
4572
|
+
code: z.ZodIssueCode.custom,
|
|
4573
|
+
message: `env.${key}.anyOf has duplicate members`,
|
|
4574
|
+
path: ["env", key, "anyOf"]
|
|
4575
|
+
});
|
|
4576
|
+
}
|
|
4577
|
+
if (!members.includes(key)) {
|
|
4578
|
+
ctx.addIssue({
|
|
4579
|
+
code: z.ZodIssueCode.custom,
|
|
4580
|
+
message: `env.${key}.anyOf must include "${key}" itself as a member`,
|
|
4581
|
+
path: ["env", key, "anyOf"]
|
|
4582
|
+
});
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4585
|
+
});
|
|
4586
|
+
var RoleEntrySchema = z.object({
|
|
4587
|
+
contract: z.string().refine((p) => p.includes("/"), {
|
|
4588
|
+
message: 'must be a path (contain at least one "/"), not a bare filename'
|
|
4589
|
+
})
|
|
4590
|
+
});
|
|
4591
|
+
var BRIEF_BUILTINS = [
|
|
4592
|
+
"tier",
|
|
4593
|
+
"testPlan",
|
|
4594
|
+
"testPlanExclusivity",
|
|
4595
|
+
"principalPlaceholder",
|
|
4596
|
+
"surfaceMap",
|
|
4597
|
+
"docUpdateList",
|
|
4598
|
+
"worktreeStep0",
|
|
4599
|
+
"stopConditions",
|
|
4600
|
+
"autonomyClause",
|
|
4601
|
+
"project",
|
|
4602
|
+
"for",
|
|
4603
|
+
"closesN",
|
|
4604
|
+
"premiseCoverage",
|
|
4605
|
+
"issueRationale",
|
|
4606
|
+
"milestoneShape"
|
|
4607
|
+
];
|
|
4608
|
+
var BriefSectionSchema = z.union([
|
|
4609
|
+
z.object({ builtin: z.enum(BRIEF_BUILTINS) }),
|
|
4610
|
+
z.object({ heading: z.string().min(1), name: z.string().optional() }),
|
|
4611
|
+
z.object({ field: z.string().min(1), name: z.string().optional() }),
|
|
4612
|
+
z.object({ phrase: z.string().min(1), name: z.string().optional() })
|
|
4613
|
+
]);
|
|
4614
|
+
var BriefSchemaSchema = z.object({
|
|
4615
|
+
pr: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
|
|
4616
|
+
issue: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
|
|
4617
|
+
milestone: z.object({ sections: z.array(BriefSectionSchema) }).optional(),
|
|
4618
|
+
ack: z.array(z.enum(BRIEF_BUILTINS)).optional()
|
|
4619
|
+
});
|
|
4620
|
+
function isSafeRepoRelPath(p) {
|
|
4621
|
+
if (p.length === 0)
|
|
4622
|
+
return false;
|
|
4623
|
+
if (p.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(p))
|
|
4624
|
+
return false;
|
|
4625
|
+
return p.split(/[\\/]/).every((seg) => seg !== ".." && seg !== "");
|
|
4626
|
+
}
|
|
4627
|
+
var SafeRepoRelPath = z.string().refine(isSafeRepoRelPath, {
|
|
4628
|
+
message: "must be a repo-root-relative path with no `..` segment or absolute root"
|
|
4629
|
+
});
|
|
4630
|
+
var CANONICAL_HOOK_BLOCK_PREFIXES = [".git/", ".husky/", ".vinaya/hooks/", ".claude/hooks/"];
|
|
4631
|
+
function isCanonicalHookBlockPath(p) {
|
|
4632
|
+
return CANONICAL_HOOK_BLOCK_PREFIXES.some((prefix) => p.startsWith(prefix) && p.length > prefix.length);
|
|
4633
|
+
}
|
|
4634
|
+
var ManagedHookBlockPath = SafeRepoRelPath.refine(isCanonicalHookBlockPath, {
|
|
4635
|
+
message: `must start with one of ${CANONICAL_HOOK_BLOCK_PREFIXES.join(", ")} (byte-exact, case-sensitive)`
|
|
4636
|
+
});
|
|
4637
|
+
var ManagedBlockRecordSchema = z.object({
|
|
4638
|
+
path: ManagedHookBlockPath,
|
|
4639
|
+
marker: z.string(),
|
|
4640
|
+
comment: z.enum(["hash", "html"])
|
|
4641
|
+
});
|
|
4642
|
+
var ManagedManifestSchema = z.object({
|
|
4643
|
+
version: z.number().int().positive(),
|
|
4644
|
+
files: z.array(SafeRepoRelPath),
|
|
4645
|
+
blocks: z.array(ManagedBlockRecordSchema),
|
|
4646
|
+
labels: z.array(z.string()),
|
|
4647
|
+
agents: z.array(z.enum(AGENT_VENDORS)).optional()
|
|
4648
|
+
});
|
|
4649
|
+
var ProjectEntrySchema = z.object({
|
|
4650
|
+
name: z.string().min(1),
|
|
4651
|
+
description: z.string().min(1).optional(),
|
|
4652
|
+
path: z.string().min(1).optional()
|
|
4653
|
+
});
|
|
4654
|
+
function parseTokensCollectDeclaration(value) {
|
|
4655
|
+
const m = value.trim().match(/^(\S+)\s+(\S+)$/);
|
|
4656
|
+
if (!m)
|
|
4657
|
+
return null;
|
|
4658
|
+
const [, interpreter, script] = m;
|
|
4659
|
+
if (!isSafeRepoRelPath(script))
|
|
4660
|
+
return null;
|
|
4661
|
+
return { interpreter, script };
|
|
4662
|
+
}
|
|
4663
|
+
var VinayaConfigSchema = z.object({
|
|
4664
|
+
rings: z.object({
|
|
4665
|
+
ring1_forgeWriteInterception: z.boolean(),
|
|
4666
|
+
ring2_asyncAudits: z.boolean()
|
|
4667
|
+
}).optional(),
|
|
4668
|
+
checks: z.record(z.string(), CheckEntrySchema).optional(),
|
|
4669
|
+
roles: z.record(z.string(), RoleEntrySchema).optional(),
|
|
4670
|
+
briefSchema: BriefSchemaSchema.optional(),
|
|
4671
|
+
managed: ManagedManifestSchema.optional(),
|
|
4672
|
+
principals: z.array(z.string()).min(1).optional(),
|
|
4673
|
+
releaseActor: z.string().min(1).optional(),
|
|
4674
|
+
ci: z.object({ setup: z.string().min(1) }).optional(),
|
|
4675
|
+
tokens: z.object({
|
|
4676
|
+
collect: z.string().min(1).refine((v) => parseTokensCollectDeclaration(v) !== null, {
|
|
4677
|
+
message: 'tokens.collect must be exactly "<interpreter> <repo-relative-script-path>" — two whitespace-separated tokens, no flags, no shell syntax, no quoting'
|
|
4678
|
+
})
|
|
4679
|
+
}).optional(),
|
|
4680
|
+
blastRadius: z.object({ extraDomains: z.array(z.string()).optional() }).optional(),
|
|
4681
|
+
proseGates: z.object({
|
|
4682
|
+
doctrineRoot: z.string().min(1).optional(),
|
|
4683
|
+
readerFacingPrefix: z.string().min(1).optional(),
|
|
4684
|
+
readerFacingSuffix: z.string().min(1).optional(),
|
|
4685
|
+
legacySlugDir: z.string().min(1).optional()
|
|
4686
|
+
}).optional(),
|
|
4687
|
+
projects: z.array(ProjectEntrySchema).optional()
|
|
4688
|
+
});
|
|
4689
|
+
var GLOBAL_VINAYA_HOME = join2(homedir(), ".vinaya");
|
|
4690
|
+
var GLOBAL_CONFIG_PATH = join2(GLOBAL_VINAYA_HOME, "config.json");
|
|
4691
|
+
var LOCAL_CONFIG_FILENAME = "vinaya.config.json";
|
|
4692
|
+
function findLocalConfig() {
|
|
4693
|
+
let dir = process.cwd();
|
|
4694
|
+
while (true) {
|
|
4695
|
+
const candidate = join2(dir, LOCAL_CONFIG_FILENAME);
|
|
4696
|
+
if (existsSync2(candidate))
|
|
4697
|
+
return candidate;
|
|
4698
|
+
if (existsSync2(join2(dir, ".git")))
|
|
4699
|
+
return null;
|
|
4700
|
+
const parent = dirname2(dir);
|
|
4701
|
+
if (parent === dir)
|
|
4702
|
+
return null;
|
|
4703
|
+
dir = parent;
|
|
4704
|
+
}
|
|
4705
|
+
}
|
|
4706
|
+
function configPath() {
|
|
4707
|
+
const local = findLocalConfig();
|
|
4708
|
+
if (local)
|
|
4709
|
+
return local;
|
|
4710
|
+
if (existsSync2(GLOBAL_CONFIG_PATH))
|
|
4711
|
+
return GLOBAL_CONFIG_PATH;
|
|
4712
|
+
return null;
|
|
4713
|
+
}
|
|
4714
|
+
function globalChecksIgnoredWarning(path) {
|
|
4715
|
+
return `${path}: "checks" registration in the global config is ignored — checks may only be registered from a repo-local vinaya.config.json.`;
|
|
4716
|
+
}
|
|
4717
|
+
function globalPrincipalsIgnoredWarning(path) {
|
|
4718
|
+
return `${path}: "principals" in the global config is ignored — principals may only be declared from a repo-local vinaya.config.json.`;
|
|
4719
|
+
}
|
|
4720
|
+
function globalRolesIgnoredWarning(path) {
|
|
4721
|
+
return `${path}: "roles" registration in the global config is ignored — roles may only be registered from a repo-local vinaya.config.json.`;
|
|
4722
|
+
}
|
|
4723
|
+
function globalReleaseActorIgnoredWarning(path) {
|
|
4724
|
+
return `${path}: "releaseActor" in the global config is ignored — releaseActor may only be declared from a repo-local vinaya.config.json.`;
|
|
4725
|
+
}
|
|
4726
|
+
function globalTokensCollectIgnoredWarning(path) {
|
|
4727
|
+
return `${path}: "tokens" in the global config is ignored — tokens.collect may only be declared from a repo-local vinaya.config.json.`;
|
|
4728
|
+
}
|
|
4729
|
+
function stripGlobalOnlyKeys(config, path) {
|
|
4730
|
+
if (path !== GLOBAL_CONFIG_PATH)
|
|
4731
|
+
return config;
|
|
4732
|
+
let result = config;
|
|
4733
|
+
if (result.checks && Object.keys(result.checks).length > 0) {
|
|
4734
|
+
console.error(`⚠ ${globalChecksIgnoredWarning(path)}`);
|
|
4735
|
+
result = { ...result, checks: undefined };
|
|
4736
|
+
}
|
|
4737
|
+
if (result.roles && Object.keys(result.roles).length > 0) {
|
|
4738
|
+
console.error(`⚠ ${globalRolesIgnoredWarning(path)}`);
|
|
4739
|
+
result = { ...result, roles: undefined };
|
|
4740
|
+
}
|
|
4741
|
+
if (result.principals && result.principals.length > 0) {
|
|
4742
|
+
console.error(`⚠ ${globalPrincipalsIgnoredWarning(path)}`);
|
|
4743
|
+
result = { ...result, principals: undefined };
|
|
4744
|
+
}
|
|
4745
|
+
if (result.releaseActor) {
|
|
4746
|
+
console.error(`⚠ ${globalReleaseActorIgnoredWarning(path)}`);
|
|
4747
|
+
result = { ...result, releaseActor: undefined };
|
|
4748
|
+
}
|
|
4749
|
+
if (result.tokens) {
|
|
4750
|
+
console.error(`⚠ ${globalTokensCollectIgnoredWarning(path)}`);
|
|
4751
|
+
result = { ...result, tokens: undefined };
|
|
4752
|
+
}
|
|
4753
|
+
return result;
|
|
4754
|
+
}
|
|
4755
|
+
function resolvePrincipalAllowlist(config) {
|
|
4756
|
+
return config?.principals ?? PRINCIPAL_ALLOWLIST;
|
|
4757
|
+
}
|
|
4758
|
+
function resolveReleaseActor(config) {
|
|
4759
|
+
return config?.releaseActor ?? DEFAULT_RELEASE_ACTOR;
|
|
4760
|
+
}
|
|
4761
|
+
function trustAnchorRepo() {
|
|
4762
|
+
const wellFormed = (slug) => /^[^/\s]+\/[^/\s]+$/.test(slug) ? slug : null;
|
|
4763
|
+
const fromRunner = process.env.GITHUB_REPOSITORY?.trim();
|
|
4764
|
+
if (fromRunner) {
|
|
4765
|
+
const validated = wellFormed(fromRunner);
|
|
4766
|
+
if (validated)
|
|
4767
|
+
return validated;
|
|
4768
|
+
}
|
|
4769
|
+
try {
|
|
4770
|
+
const url = execFileSync2("git", ["remote", "get-url", "origin"], {
|
|
4771
|
+
encoding: "utf-8",
|
|
4772
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4773
|
+
}).trim();
|
|
4774
|
+
const m = url.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/);
|
|
4775
|
+
return m?.[1] && m[2] ? wellFormed(`${m[1]}/${m[2]}`) : null;
|
|
4776
|
+
} catch {
|
|
4777
|
+
return null;
|
|
4778
|
+
}
|
|
4779
|
+
}
|
|
4780
|
+
function ghFetchTrustAnchorConfig() {
|
|
4781
|
+
const repo = trustAnchorRepo();
|
|
4782
|
+
if (!repo)
|
|
4783
|
+
throw new Error("could not resolve repo identity for the trust-anchor read");
|
|
4784
|
+
return execFileSync2("gh", ["api", `repos/${repo}/contents/${LOCAL_CONFIG_FILENAME}`, "--jq", ".content"], {
|
|
4785
|
+
encoding: "utf-8",
|
|
4786
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4787
|
+
});
|
|
4788
|
+
}
|
|
4789
|
+
function firstLine(err) {
|
|
4790
|
+
return err?.message?.split(`
|
|
4791
|
+
`)[0] ?? "unknown error";
|
|
4792
|
+
}
|
|
4793
|
+
function isMissingFileError(err) {
|
|
4794
|
+
const stderr = err?.stderr;
|
|
4795
|
+
const haystack = [
|
|
4796
|
+
err?.message ?? "",
|
|
4797
|
+
typeof stderr === "string" ? stderr : stderr?.toString() ?? ""
|
|
4798
|
+
].join(`
|
|
4799
|
+
`);
|
|
4800
|
+
return /\b404\b|not found/i.test(haystack);
|
|
4801
|
+
}
|
|
4802
|
+
function loadTrustAnchorConfig(fetcher = ghFetchTrustAnchorConfig) {
|
|
4803
|
+
const warn = (why) => process.stdout.write(`⚠ could not read the trust-anchor config (\`principals\`/\`releaseActor\`) from the default branch (${why}) — falling back to vinaya's built-in defaults.
|
|
4804
|
+
`);
|
|
4805
|
+
let base64;
|
|
4806
|
+
try {
|
|
4807
|
+
base64 = fetcher().trim();
|
|
4808
|
+
} catch (err) {
|
|
4809
|
+
if (!isMissingFileError(err))
|
|
4810
|
+
warn(firstLine(err));
|
|
4811
|
+
return null;
|
|
4812
|
+
}
|
|
4813
|
+
if (!base64)
|
|
4814
|
+
return null;
|
|
4815
|
+
try {
|
|
4816
|
+
const raw = Buffer.from(base64, "base64").toString("utf-8");
|
|
4817
|
+
const parsed = VinayaConfigSchema.safeParse(JSON.parse(raw));
|
|
4818
|
+
if (parsed.success)
|
|
4819
|
+
return parsed.data;
|
|
4820
|
+
warn("it did not satisfy the config schema");
|
|
4821
|
+
return null;
|
|
4822
|
+
} catch {
|
|
4823
|
+
warn("it was not readable as JSON");
|
|
4824
|
+
return null;
|
|
4825
|
+
}
|
|
4826
|
+
}
|
|
4827
|
+
function loadConfig() {
|
|
4828
|
+
const path = configPath();
|
|
4829
|
+
if (!path)
|
|
4830
|
+
return null;
|
|
4831
|
+
try {
|
|
4832
|
+
const raw = JSON.parse(readFileSync2(path, "utf-8"));
|
|
4833
|
+
return stripGlobalOnlyKeys(VinayaConfigSchema.parse(raw), path);
|
|
4834
|
+
} catch {
|
|
4835
|
+
return null;
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
var TOKENS_COLLECT_TRUST_PATH = join2(GLOBAL_VINAYA_HOME, "tokens-collect-trust.json");
|
|
4839
|
+
|
|
4840
|
+
// src/commands/tokens.ts
|
|
4841
|
+
var USAGE = [
|
|
4842
|
+
'Usage: vinaya tokens --phase "<task-id>: develop" --role Developer',
|
|
4843
|
+
" [--model <id>] [--transcript <path>] [--in <tokens-in> --out <tokens-out>]",
|
|
4844
|
+
"",
|
|
4845
|
+
" --transcript Read this transcript directly. Supported primary route — use it whenever you know",
|
|
4846
|
+
" which transcript is yours, and always in a repo with no track-transcript.sh hook.",
|
|
4847
|
+
" (omitted) Resolve via the Stop-hook pointer file, if this repo installs that hook.",
|
|
4848
|
+
" --in/--out Manual entry: the exact token figures, for a host whose usage arrives by some other",
|
|
4849
|
+
" means than a Claude Code transcript. Both required together; skips transcript",
|
|
4850
|
+
" resolution entirely.",
|
|
4851
|
+
"",
|
|
4852
|
+
"Usage: vinaya tokens --trust-collect",
|
|
4853
|
+
" Approves this repo's declared tokens.collect command, pinned to the script's exact current",
|
|
4854
|
+
" content, on this machine. Run this once before tokens.collect will ever execute — a changed",
|
|
4855
|
+
" interpreter, script path, or script CONTENT needs its own fresh approval. Does not take",
|
|
4856
|
+
" --phase/--role and never emits a Tokens: line."
|
|
4857
|
+
].join(`
|
|
4858
|
+
`);
|
|
4859
|
+
|
|
4860
|
+
// src/commands/pr-report.ts
|
|
4861
|
+
var FAILING_STATUSES = new Set(["fail", "error", "timeout"]);
|
|
4862
|
+
function extractAgentCommandLines(prBody) {
|
|
4863
|
+
const located = locateTestPlanSection(prBody);
|
|
4864
|
+
if (!located.found)
|
|
4865
|
+
return [];
|
|
4866
|
+
if (/unit-tests-only/i.test(located.section))
|
|
4867
|
+
return [];
|
|
4868
|
+
const blocks = extractFencedBlocks(located.section);
|
|
4869
|
+
const first = blocks[0];
|
|
4870
|
+
if (!first)
|
|
4871
|
+
return [];
|
|
4872
|
+
return first.content.split(`
|
|
4873
|
+
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
4874
|
+
}
|
|
4875
|
+
function agentCommandText(line) {
|
|
4876
|
+
const idx = line.indexOf("→");
|
|
4877
|
+
return (idx === -1 ? line : line.slice(0, idx)).trim();
|
|
4878
|
+
}
|
|
4879
|
+
var TOKEN_ROW_REMEDY = [
|
|
4880
|
+
"Nothing here established that this host cannot meter — only that this session resolved no",
|
|
4881
|
+
"transcript of its own. Report real figures by naming your own transcript:",
|
|
4882
|
+
"",
|
|
4883
|
+
" vinaya pr report --write <body-file> --transcript <path>",
|
|
4884
|
+
"",
|
|
4885
|
+
"or, on a host whose usage arrives by some other means, format figures you already hold with:",
|
|
4886
|
+
"",
|
|
4887
|
+
' vinaya tokens --phase "<task-id>: develop" --role Developer --in <tokens-in> --out <tokens-out>',
|
|
4888
|
+
"",
|
|
4889
|
+
"and transcribe them into the `## Token report` table. That command prints a `Tokens:` line, not",
|
|
4890
|
+
"a table row — the table takes | Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date |,",
|
|
4891
|
+
"and a line inside the block that does not start with `|` truncates the table for every row after it."
|
|
4892
|
+
].join(`
|
|
4893
|
+
`);
|
|
4894
|
+
var USAGE2 = "Usage: vinaya pr report [--write <body-file> | --push <pr>] [--phase <phase>] [--role <role>] " + "[--model <id>] [--transcript <path>]";
|
|
4895
|
+
|
|
3998
4896
|
// src/checks/evidence-fresh-logic.ts
|
|
3999
4897
|
var HEAD_LINE = /^Head:\s*([0-9a-f]{7,40})\s*$/m;
|
|
4000
4898
|
var FENCE = /```[^\n]*\n?([\s\S]*?)```/g;
|
|
4001
|
-
function compareEvidenceBlock(resolved, resolvedHead, actualNumstat) {
|
|
4899
|
+
function compareEvidenceBlock(resolved, resolvedHead, actualNumstat, expectedGroupCCommandLines) {
|
|
4002
4900
|
const region = resolved.region;
|
|
4003
4901
|
const headMatch = region.match(HEAD_LINE);
|
|
4004
4902
|
const fences = [...region.matchAll(FENCE)].map((m) => (m[1] ?? "").trim());
|
|
@@ -4036,6 +4934,33 @@ function compareEvidenceBlock(resolved, resolvedHead, actualNumstat) {
|
|
|
4036
4934
|
` block: ${JSON.stringify(storedSummary)}`,
|
|
4037
4935
|
` actual: ${JSON.stringify(expectedSummary)}`
|
|
4038
4936
|
].join(`
|
|
4937
|
+
`));
|
|
4938
|
+
}
|
|
4939
|
+
}
|
|
4940
|
+
const hasGroupCSection = /^### Group C — Test Plan commands/m.test(resolved.maskedRegion);
|
|
4941
|
+
if (expectedGroupCCommandLines !== undefined && hasGroupCSection) {
|
|
4942
|
+
const HEADING_PREFIX = /^####\s+C\d+:/;
|
|
4943
|
+
const COMMAND_HEADING_LINE = /^####\s+C\d+:\s+`(.*)`\s*$/;
|
|
4944
|
+
const maskedLines = resolved.maskedRegion.split(`
|
|
4945
|
+
`);
|
|
4946
|
+
const rawLines = region.split(`
|
|
4947
|
+
`);
|
|
4948
|
+
const storedCommands = [];
|
|
4949
|
+
for (let i = 0;i < maskedLines.length; i++) {
|
|
4950
|
+
if (!HEADING_PREFIX.test(maskedLines[i]))
|
|
4951
|
+
continue;
|
|
4952
|
+
const match = COMMAND_HEADING_LINE.exec(rawLines[i]);
|
|
4953
|
+
if (match)
|
|
4954
|
+
storedCommands.push(match[1]);
|
|
4955
|
+
}
|
|
4956
|
+
const expected = expectedGroupCCommandLines;
|
|
4957
|
+
const mismatch = storedCommands.length !== expected.length || storedCommands.some((c, i) => c !== expected[i]);
|
|
4958
|
+
if (mismatch) {
|
|
4959
|
+
errors.push([
|
|
4960
|
+
"evidence-fresh: Group C's command headings do not match the PR body's own §9 Test Plan list.",
|
|
4961
|
+
` block: ${JSON.stringify(storedCommands)}`,
|
|
4962
|
+
` expected: ${JSON.stringify(expected)}`
|
|
4963
|
+
].join(`
|
|
4039
4964
|
`));
|
|
4040
4965
|
}
|
|
4041
4966
|
}
|
|
@@ -4046,7 +4971,7 @@ function compareEvidenceBlock(resolved, resolvedHead, actualNumstat) {
|
|
|
4046
4971
|
var CHECK_NAME = "evidence-fresh";
|
|
4047
4972
|
function git(args) {
|
|
4048
4973
|
try {
|
|
4049
|
-
return
|
|
4974
|
+
return execFileSync3("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
4050
4975
|
} catch {
|
|
4051
4976
|
return "";
|
|
4052
4977
|
}
|
|
@@ -4065,7 +4990,7 @@ class GitCommandError extends Error {
|
|
|
4065
4990
|
}
|
|
4066
4991
|
function gitStrict(args) {
|
|
4067
4992
|
try {
|
|
4068
|
-
return
|
|
4993
|
+
return execFileSync3("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
4069
4994
|
} catch (err) {
|
|
4070
4995
|
const stderr = err.stderr;
|
|
4071
4996
|
throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
|
|
@@ -4083,7 +5008,7 @@ function resolveMergeBase(head) {
|
|
|
4083
5008
|
}
|
|
4084
5009
|
function fetchHeadSha(prNumber) {
|
|
4085
5010
|
try {
|
|
4086
|
-
const out =
|
|
5011
|
+
const out = execFileSync3("gh", ["pr", "view", String(prNumber), "--json", "headRefOid", "-q", ".headRefOid"], {
|
|
4087
5012
|
encoding: "utf8",
|
|
4088
5013
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4089
5014
|
}).trim();
|
|
@@ -4141,7 +5066,8 @@ function main() {
|
|
|
4141
5066
|
});
|
|
4142
5067
|
process.exit(1);
|
|
4143
5068
|
}
|
|
4144
|
-
const
|
|
5069
|
+
const expectedGroupCCommandLines = extractAgentCommandLines(body).map(agentCommandText);
|
|
5070
|
+
const result = compareEvidenceBlock(resolved, resolvedHead, actualNumstat, expectedGroupCCommandLines);
|
|
4145
5071
|
if (result.status === "fail") {
|
|
4146
5072
|
for (const message of result.errors) {
|
|
4147
5073
|
emitCheckError({
|