@attalabs/vinaya 0.20.1 → 0.22.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 +76 -1
- package/aeg-root/enforcement.md +15 -7
- package/aeg-root/milestone-model.md +3 -1
- package/aeg-root/roles/architect.md +2 -2
- package/aeg-root/roles/archivist.md +2 -2
- package/aeg-root/roles/developer.md +3 -2
- package/aeg-root/roles/planner.md +2 -2
- package/aeg-root/roles/principal.md +1 -0
- package/aeg-root/roles/tranche-archivist.md +2 -2
- package/aeg-root/templates/issue-rationale-template.md +4 -4
- package/aeg-root/templates/pr-report-template.md +2 -0
- package/aeg-root/tranche-model.md +4 -4
- package/dist/checks/bin/check-body-bare-digits.js +392 -10
- package/dist/checks/bin/check-branch-topology.js +382 -2
- package/dist/checks/bin/check-brief-shape.js +357 -2
- package/dist/checks/bin/check-changeset-coverage.js +4262 -0
- package/dist/checks/bin/check-closes-n.js +382 -2
- package/dist/checks/bin/check-coherence.js +416 -8
- package/dist/checks/bin/check-dead-branch-push.js +357 -2
- package/dist/checks/bin/check-dispatch-readiness.js +492 -25
- package/dist/checks/bin/check-doc-coverage-push.js +392 -9
- package/dist/checks/bin/check-doc-coverage.js +392 -9
- package/dist/checks/bin/check-doctrine-portability.js +391 -8
- package/dist/checks/bin/check-evidence-fresh.js +357 -2
- package/dist/checks/bin/check-first-push-dispatch.js +481 -17
- package/dist/checks/bin/check-issue-assignment.js +382 -2
- package/dist/checks/bin/check-main-branch-refusal.js +357 -2
- package/dist/checks/bin/check-no-disk-state.js +357 -2
- package/dist/checks/bin/check-quoted-command.js +4514 -0
- package/dist/checks/bin/check-reader-resolvable-prose.js +430 -21
- package/dist/checks/bin/check-registry-gates.js +410 -7
- package/dist/checks/bin/check-retired-vocabulary.js +422 -13
- package/dist/checks/bin/check-review-gate.js +389 -6
- package/dist/checks/bin/check-single-plan-pr.js +357 -2
- package/dist/checks/bin/check-test-plan.js +357 -2
- package/dist/checks/bin/check-token-collection-wired.js +4081 -0
- package/dist/checks/bin/check-token-report.js +4096 -0
- package/dist/checks/bin/check-workspace-escape.js +359 -4
- package/dist/index.js +1607 -641
- package/package.json +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/BUILD_ID +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/build-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/prerender-manifest.json +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.html +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page/server-reference-manifest.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/app/_not-found/page.js +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 +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 +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 +3 -3
- 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 +3 -3
- 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 +3 -3
- 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 +3 -3
- 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 +3 -3
- 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 +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]__040smoa._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{[root-of-the-server]__16cuydh._.js → [root-of-the-server]__069-_41._.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__0cxtq46._.js +25 -18
- 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]__0ykun-j._.js → [root-of-the-server]__0z6dav-._.js} +3 -3
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__18kqy4q._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__19jyp3m._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1hs0dcu._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1wc4-ip._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1widio3._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1yoa8tx._.js +3 -0
- 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/_0wxycau._.js +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/_0x999pb._.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 +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_0zys35s._.js → _1mqrlbl._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/{_1c9b9j-._.js → _1mrtj0f._.js} +2 -2
- 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_18kqbvs._.js → apps_vinaya-studio_web_src_app_studio_06it6s-._.js} +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/apps_vinaya-studio_web_src_app_studio_projects_[name]_tranches_[slug]_05ufo8i._.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/03q0q9n6-xah8.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/0vc2u252q5q6o.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/13g16cu00g570.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{1lk7h-0buf4ab.js → 1hufpuody6vjv.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1sfxfunirkcr6.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{29-foak16tjfu.js → 206ajg8f1a-_h.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0ea8-8nopz29h.js → 267o-tsrnuwnv.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0msr2a1i-x-br.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/{0up5x3jg-cwov.js → 2rn89uwtf2lwc.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2twnwe77d4n-m.js → 37hnluhzj9bei.js} +4 -4
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3qc67qhcvbg0o.css +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{30ojnsx3p4lp4.js → 3zugx9esh2x51.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/package.json +2 -2
- package/studio-standalone/apps/vinaya-studio/web/.next/server/chunks/ssr/[root-of-the-server]__1rku4gz._.js +0 -3
- 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/0la0hhmjprdxm.css +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/25j-y57ng11hl.js +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/30lyvn0yzwdiz.js +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/35u8inxa732is.js +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3r5efmleneaco.js +0 -1
- /package/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/{b2VfZK2AItZwlghK_pUN0 → Fmyc42qzLwFcWWHLLNy-G}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{b2VfZK2AItZwlghK_pUN0 → Fmyc42qzLwFcWWHLLNy-G}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{b2VfZK2AItZwlghK_pUN0 → Fmyc42qzLwFcWWHLLNy-G}/_ssgManifest.js +0 -0
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { dirname as dirname9, join as join27 } from "node:path";
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7
7
|
|
|
8
8
|
// src/commands/archive.ts
|
|
9
|
-
import { execFileSync as
|
|
9
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
10
10
|
// ../../packages/aeg-forge-state/src/strip-code.ts
|
|
11
11
|
function maskCode(body) {
|
|
12
12
|
const fill = (line) => " ".repeat(line.length);
|
|
@@ -164,6 +164,16 @@ function maskIndentedCode(body, fill) {
|
|
|
164
164
|
|
|
165
165
|
// ../../packages/aeg-forge-state/src/parse-rationale-deps.ts
|
|
166
166
|
var SECTION_HEADER = /\*\*Dependency rationale\*\*/i;
|
|
167
|
+
// ../../packages/aeg-forge-state/src/check-milestone-attachment.ts
|
|
168
|
+
function checkMilestoneAttachment(labeledIssueNumbers, attachedIssueNumbers) {
|
|
169
|
+
const attachedSet = new Set(attachedIssueNumbers);
|
|
170
|
+
const labeledSet = new Set(labeledIssueNumbers);
|
|
171
|
+
const unattached = labeledIssueNumbers.filter((n) => !attachedSet.has(n));
|
|
172
|
+
const foreign = attachedIssueNumbers.filter((n) => !labeledSet.has(n));
|
|
173
|
+
if (unattached.length === 0 && foreign.length === 0)
|
|
174
|
+
return { status: "clean" };
|
|
175
|
+
return { status: "mismatch", unattached, foreign };
|
|
176
|
+
}
|
|
167
177
|
// ../../packages/aeg-forge-state/src/gh.ts
|
|
168
178
|
import { execFile, execFileSync } from "node:child_process";
|
|
169
179
|
import { promisify } from "node:util";
|
|
@@ -172,6 +182,12 @@ var systemEnv = {
|
|
|
172
182
|
...process.env,
|
|
173
183
|
PATH: [process.env.PATH, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"].filter(Boolean).join(":")
|
|
174
184
|
};
|
|
185
|
+
function run(args) {
|
|
186
|
+
return execFileSync("gh", args, { encoding: "utf8", env: systemEnv });
|
|
187
|
+
}
|
|
188
|
+
function ghApiGet(path) {
|
|
189
|
+
return JSON.parse(run(["api", path]));
|
|
190
|
+
}
|
|
175
191
|
|
|
176
192
|
// ../../packages/aeg-forge-state/src/labels.ts
|
|
177
193
|
var LABELS = [
|
|
@@ -435,6 +451,47 @@ function parseFieldValue(value) {
|
|
|
435
451
|
function projectsFromBody(body) {
|
|
436
452
|
return projectFieldFromBody(body).names;
|
|
437
453
|
}
|
|
454
|
+
|
|
455
|
+
// ../../packages/aeg-forge-state/src/fetch-milestone.ts
|
|
456
|
+
var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
|
|
457
|
+
var NEXT_HEADING = /^#{1,6}\s+\S/m;
|
|
458
|
+
var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
|
|
459
|
+
function intentGoalForSlug(description, slug) {
|
|
460
|
+
const text = stripCode(description, { inlineSpans: "keep" });
|
|
461
|
+
const start = text.match(INTENTS_HEADING);
|
|
462
|
+
if (!start || start.index === undefined)
|
|
463
|
+
return "";
|
|
464
|
+
const rest = text.slice(start.index + start[0].length);
|
|
465
|
+
const next = rest.match(NEXT_HEADING);
|
|
466
|
+
const section = rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
|
|
467
|
+
for (const line of section.split(`
|
|
468
|
+
`)) {
|
|
469
|
+
const trimmed = line.trim();
|
|
470
|
+
if (trimmed.length === 0)
|
|
471
|
+
continue;
|
|
472
|
+
const m = trimmed.match(INTENT_BULLET);
|
|
473
|
+
if (m && (m[1] ?? "").toLowerCase() === slug.toLowerCase())
|
|
474
|
+
return (m[2] ?? "").trim();
|
|
475
|
+
}
|
|
476
|
+
return "";
|
|
477
|
+
}
|
|
478
|
+
function matchesLegacyMilestone(milestones, slug) {
|
|
479
|
+
return milestones.find((m) => m.title === slug) ?? null;
|
|
480
|
+
}
|
|
481
|
+
function resolveMilestoneAttachTarget(milestones, slug) {
|
|
482
|
+
const legacy = matchesLegacyMilestone(milestones, slug);
|
|
483
|
+
if (legacy?.state === "open")
|
|
484
|
+
return { number: legacy.number, title: legacy.title };
|
|
485
|
+
const intentMatch = milestones.find((m) => m.state === "open" && intentGoalForSlug(m.description ?? "", slug) !== "");
|
|
486
|
+
return intentMatch ? { number: intentMatch.number, title: intentMatch.title } : null;
|
|
487
|
+
}
|
|
488
|
+
function findMilestoneAttachTargetForSlug(owner, repo, slug) {
|
|
489
|
+
const milestones = ghApiGet(`repos/${owner}/${repo}/milestones?state=all&per_page=100`);
|
|
490
|
+
return resolveMilestoneAttachTarget(milestones, slug);
|
|
491
|
+
}
|
|
492
|
+
function hasExplicitMilestoneFlag(args) {
|
|
493
|
+
return args.some((a) => a === "--milestone" || a === "-m" || a.startsWith("--milestone="));
|
|
494
|
+
}
|
|
438
495
|
// ../../packages/aeg-forge-state/src/github-token.ts
|
|
439
496
|
import { execFile as execFile2 } from "node:child_process";
|
|
440
497
|
import { promisify as promisify2 } from "node:util";
|
|
@@ -732,14 +789,65 @@ var DERIVED_STATUSES = [
|
|
|
732
789
|
];
|
|
733
790
|
var DERIVABLE_STATUSES = DERIVED_STATUSES.filter((s) => s !== "backlog");
|
|
734
791
|
// ../../packages/aeg-core/src/report-tokens.ts
|
|
735
|
-
function
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
792
|
+
function stripNewlines(value) {
|
|
793
|
+
return value.replace(/\r?\n/g, " ");
|
|
794
|
+
}
|
|
795
|
+
function sanitizeForTableCell(value) {
|
|
796
|
+
return stripNewlines(value).replace(/\|/g, "\\|");
|
|
797
|
+
}
|
|
798
|
+
var DASH_LOOKALIKES = {
|
|
799
|
+
"-": "‑",
|
|
800
|
+
"–": "‒",
|
|
801
|
+
"—": "―"
|
|
802
|
+
};
|
|
803
|
+
var DASH_CHARS = new Set(Object.keys(DASH_LOOKALIKES));
|
|
804
|
+
var PIPE_LOOKALIKE = "|";
|
|
805
|
+
function sanitizeForTokensLine(value) {
|
|
806
|
+
const stripped = stripNewlines(value);
|
|
807
|
+
let result = "";
|
|
808
|
+
for (let i = 0;i < stripped.length; i++) {
|
|
809
|
+
const ch = stripped[i];
|
|
810
|
+
if (ch === "|") {
|
|
811
|
+
result += PIPE_LOOKALIKE;
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
if (DASH_CHARS.has(ch)) {
|
|
815
|
+
const leftReachable = i === 0 || /\s/.test(stripped[i - 1]);
|
|
816
|
+
const rightReachable = i === stripped.length - 1 || /\s/.test(stripped[i + 1]);
|
|
817
|
+
if (leftReachable && rightReachable) {
|
|
818
|
+
result += DASH_LOOKALIKES[ch];
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
result += ch;
|
|
739
823
|
}
|
|
824
|
+
return result;
|
|
825
|
+
}
|
|
826
|
+
function renderCells(input) {
|
|
827
|
+
const model = input.modelOverride ?? input.summary?.model ?? null;
|
|
828
|
+
if (!input.summary)
|
|
829
|
+
return { model, tokensIn: "—", tokensOut: "—" };
|
|
740
830
|
const { inputTokens, cacheCreationInputTokens, cacheReadInputTokens, outputTokens } = input.summary.components;
|
|
741
831
|
const tokensIn = inputTokens + cacheCreationInputTokens + cacheReadInputTokens;
|
|
742
|
-
return
|
|
832
|
+
return { model, tokensIn: String(tokensIn), tokensOut: String(outputTokens) };
|
|
833
|
+
}
|
|
834
|
+
function formatTokensLine(input) {
|
|
835
|
+
const { model, tokensIn, tokensOut } = renderCells(input);
|
|
836
|
+
const phase = sanitizeForTokensLine(input.phase);
|
|
837
|
+
const role = sanitizeForTokensLine(input.role);
|
|
838
|
+
const safeModel = model === null ? "—" : sanitizeForTokensLine(model);
|
|
839
|
+
if (!input.summary) {
|
|
840
|
+
return `Tokens: ${phase} — ${role} — ${safeModel} — —`;
|
|
841
|
+
}
|
|
842
|
+
return `Tokens: ${phase} — ${role} — ${safeModel} — ${tokensIn}/${tokensOut}/—`;
|
|
843
|
+
}
|
|
844
|
+
function formatTokenReportRow(input) {
|
|
845
|
+
const { model, tokensIn, tokensOut } = renderCells(input);
|
|
846
|
+
const phase = sanitizeForTableCell(input.phase);
|
|
847
|
+
const role = sanitizeForTableCell(input.role);
|
|
848
|
+
const safeModel = model === null ? "—" : sanitizeForTableCell(model);
|
|
849
|
+
const date = sanitizeForTableCell(input.date);
|
|
850
|
+
return `| ${phase} | ${role} | ${safeModel} | ${tokensIn} | ${tokensOut} | — | ${date} |`;
|
|
743
851
|
}
|
|
744
852
|
function formatBreakdown(summary) {
|
|
745
853
|
const { inputTokens, cacheCreationInputTokens, cacheReadInputTokens, outputTokens } = summary.components;
|
|
@@ -754,6 +862,7 @@ function formatBreakdown(summary) {
|
|
|
754
862
|
`);
|
|
755
863
|
}
|
|
756
864
|
// ../../packages/aeg-core/src/claude-code-transcript.ts
|
|
865
|
+
import { createHash } from "node:crypto";
|
|
757
866
|
function summarizeTranscript(jsonl) {
|
|
758
867
|
const seen = new Set;
|
|
759
868
|
const components = {
|
|
@@ -802,48 +911,79 @@ function numberOr(value, fallback) {
|
|
|
802
911
|
function sanitizeKey(value) {
|
|
803
912
|
return value.replace(/[^A-Za-z0-9]+/g, "-");
|
|
804
913
|
}
|
|
914
|
+
function collisionResistantKey(value) {
|
|
915
|
+
const digest = createHash("sha256").update(value).digest("hex");
|
|
916
|
+
return `${sanitizeKey(value)}-${digest}`;
|
|
917
|
+
}
|
|
805
918
|
function transcriptPointerPath(projectDir, tmpDir) {
|
|
919
|
+
return `${tmpDir}/claude-transcript-${collisionResistantKey(projectDir)}.txt`;
|
|
920
|
+
}
|
|
921
|
+
function legacyTranscriptPointerPath(projectDir, tmpDir) {
|
|
806
922
|
return `${tmpDir}/claude-transcript-${sanitizeKey(projectDir)}.txt`;
|
|
807
923
|
}
|
|
808
924
|
function resolvePointer(explicitTranscriptPath, deps) {
|
|
809
925
|
if (explicitTranscriptPath)
|
|
810
|
-
return { path: explicitTranscriptPath };
|
|
926
|
+
return { path: explicitTranscriptPath, corroborated: true };
|
|
811
927
|
const projectDir = deps.env.CLAUDE_PROJECT_DIR ?? deps.cwd;
|
|
812
928
|
const tmpDir = deps.env.TMPDIR ?? "/tmp";
|
|
813
|
-
const
|
|
814
|
-
|
|
929
|
+
const primaryPointerPath = transcriptPointerPath(projectDir, tmpDir);
|
|
930
|
+
const legacyPointerPath = legacyTranscriptPointerPath(projectDir, tmpDir);
|
|
931
|
+
const currentSessionId = deps.env.CLAUDE_CODE_SESSION_ID;
|
|
932
|
+
const pointerPath = deps.exists(primaryPointerPath) ? primaryPointerPath : deps.exists(legacyPointerPath) ? legacyPointerPath : undefined;
|
|
933
|
+
if (!pointerPath) {
|
|
815
934
|
return {
|
|
816
|
-
|
|
935
|
+
pointerExisted: false,
|
|
936
|
+
corroborated: false,
|
|
937
|
+
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."
|
|
817
938
|
};
|
|
818
939
|
}
|
|
819
940
|
let contents;
|
|
820
941
|
try {
|
|
821
|
-
contents = deps.readFile(pointerPath).
|
|
942
|
+
contents = deps.readFile(pointerPath).replace(/\r?\n+$/, "");
|
|
822
943
|
} catch (err) {
|
|
823
|
-
return {
|
|
944
|
+
return {
|
|
945
|
+
pointerExisted: true,
|
|
946
|
+
oursByLocation: true,
|
|
947
|
+
error: `Transcript pointer at ${pointerPath} could not be read: ${err.message}. ` + "Remove that file to clear this — the Stop hook rewrites it on the next turn."
|
|
948
|
+
};
|
|
824
949
|
}
|
|
825
950
|
const [pointerSessionId, transcriptPath] = contents.split("\t");
|
|
826
951
|
if (!transcriptPath) {
|
|
827
|
-
return {
|
|
952
|
+
return {
|
|
953
|
+
pointerExisted: true,
|
|
954
|
+
oursByLocation: true,
|
|
955
|
+
error: `Transcript pointer file ${pointerPath} is malformed: "${contents.slice(0, 120)}${contents.length > 120 ? "…" : ""}". ` + "Remove that file to clear this — the Stop hook rewrites it on the next turn."
|
|
956
|
+
};
|
|
828
957
|
}
|
|
829
|
-
const currentSessionId = deps.env.CLAUDE_CODE_SESSION_ID;
|
|
830
958
|
if (currentSessionId && pointerSessionId && currentSessionId !== pointerSessionId) {
|
|
831
959
|
return {
|
|
960
|
+
pointerExisted: true,
|
|
961
|
+
corroborated: false,
|
|
832
962
|
error: `Transcript pointer at ${pointerPath} is stale: written for session ${pointerSessionId}, ` + `but this session is ${currentSessionId}. Name your own transcript with --transcript instead of ` + "reporting another session's figures as yours."
|
|
833
963
|
};
|
|
834
964
|
}
|
|
835
|
-
return { path: transcriptPath };
|
|
965
|
+
return { path: transcriptPath, corroborated: Boolean(currentSessionId && pointerSessionId) };
|
|
836
966
|
}
|
|
837
967
|
function resolveMeteringCapability(deps, explicitTranscriptPath) {
|
|
838
968
|
const resolved = resolvePointer(explicitTranscriptPath, deps);
|
|
839
969
|
if ("error" in resolved) {
|
|
840
|
-
|
|
970
|
+
const ours = resolved.pointerExisted && (resolved.corroborated || resolved.oursByLocation);
|
|
971
|
+
if (ours)
|
|
972
|
+
return { capable: false, reason: "pointer-unusable", detail: resolved.error };
|
|
973
|
+
return {
|
|
974
|
+
capable: false,
|
|
975
|
+
reason: "no-transcript-resolved",
|
|
976
|
+
detail: resolved.pointerExisted ? `A transcript pointer exists but belongs to another session, so this session has no wiring of its own to use. (Underlying condition, for diagnosis only: ${resolved.error})` : resolved.error
|
|
977
|
+
};
|
|
841
978
|
}
|
|
979
|
+
const downstream = (r, detail) => resolved.corroborated ? { reason: r, detail } : {
|
|
980
|
+
reason: "no-transcript-resolved",
|
|
981
|
+
detail: `A transcript pointer was found but could not be shown to belong to this session, so it is not treated as this session's wiring. (Underlying condition, for diagnosis only: ${detail})`
|
|
982
|
+
};
|
|
842
983
|
if (!deps.exists(resolved.path)) {
|
|
843
984
|
return {
|
|
844
985
|
capable: false,
|
|
845
|
-
|
|
846
|
-
detail: `Resolved transcript path ${resolved.path} does not exist.`
|
|
986
|
+
...downstream("transcript-unreadable", `Resolved transcript path ${resolved.path} is not readable as a regular file owned by this user.`)
|
|
847
987
|
};
|
|
848
988
|
}
|
|
849
989
|
let jsonl;
|
|
@@ -852,20 +992,70 @@ function resolveMeteringCapability(deps, explicitTranscriptPath) {
|
|
|
852
992
|
} catch (err) {
|
|
853
993
|
return {
|
|
854
994
|
capable: false,
|
|
855
|
-
|
|
856
|
-
detail: `Transcript at ${resolved.path} could not be read: ${err.message}`
|
|
995
|
+
...downstream("transcript-unreadable", `Transcript at ${resolved.path} could not be read: ${err.message}`)
|
|
857
996
|
};
|
|
858
997
|
}
|
|
859
998
|
const summary = summarizeTranscript(jsonl);
|
|
860
999
|
if (summary.messageCount === 0) {
|
|
861
1000
|
return {
|
|
862
1001
|
capable: false,
|
|
863
|
-
|
|
864
|
-
detail: `Transcript at ${resolved.path} yielded zero assistant messages with usage data — ` + "it's empty, unparseable, or not yet flushed to disk."
|
|
1002
|
+
...downstream("transcript-empty", `Transcript at ${resolved.path} yielded zero assistant messages with usage data — ` + "it's empty, unparseable, or not yet flushed to disk.")
|
|
865
1003
|
};
|
|
866
1004
|
}
|
|
867
1005
|
return { capable: true, transcriptPath: resolved.path, summary };
|
|
868
1006
|
}
|
|
1007
|
+
// ../../packages/aeg-core/src/metering-io-guard.ts
|
|
1008
|
+
import { closeSync, constants as fsConstants, fstatSync, openSync, readFileSync } from "node:fs";
|
|
1009
|
+
var GUARD_OPEN_FLAGS = fsConstants.O_RDONLY | fsConstants.O_NONBLOCK | fsConstants.O_NOFOLLOW;
|
|
1010
|
+
function isTrustedMeteringStat(stat, ownUid) {
|
|
1011
|
+
if (!stat.isFile())
|
|
1012
|
+
return false;
|
|
1013
|
+
if (typeof ownUid === "number" && stat.uid !== ownUid)
|
|
1014
|
+
return false;
|
|
1015
|
+
return true;
|
|
1016
|
+
}
|
|
1017
|
+
function ownUid() {
|
|
1018
|
+
return typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
1019
|
+
}
|
|
1020
|
+
function guardedOpen(path) {
|
|
1021
|
+
try {
|
|
1022
|
+
return openSync(path, GUARD_OPEN_FLAGS);
|
|
1023
|
+
} catch {
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
function hardenedExists(path) {
|
|
1028
|
+
const fd = guardedOpen(path);
|
|
1029
|
+
if (fd === undefined)
|
|
1030
|
+
return false;
|
|
1031
|
+
try {
|
|
1032
|
+
return isTrustedMeteringStat(fstatSync(fd), ownUid());
|
|
1033
|
+
} finally {
|
|
1034
|
+
closeSync(fd);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
function hardenedReadFile(path) {
|
|
1038
|
+
const fd = guardedOpen(path);
|
|
1039
|
+
if (fd === undefined) {
|
|
1040
|
+
throw new Error(`refusing to read ${path}: not openable as a plain, non-symlinked file`);
|
|
1041
|
+
}
|
|
1042
|
+
try {
|
|
1043
|
+
if (!isTrustedMeteringStat(fstatSync(fd), ownUid())) {
|
|
1044
|
+
throw new Error(`refusing to read ${path}: not a regular file owned by this user`);
|
|
1045
|
+
}
|
|
1046
|
+
return readFileSync(fd, "utf8");
|
|
1047
|
+
} finally {
|
|
1048
|
+
closeSync(fd);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
function hardenedMeteringDeps() {
|
|
1052
|
+
return {
|
|
1053
|
+
env: process.env,
|
|
1054
|
+
cwd: process.cwd(),
|
|
1055
|
+
exists: hardenedExists,
|
|
1056
|
+
readFile: hardenedReadFile
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
869
1059
|
// ../../packages/aeg-core/src/file-classify.ts
|
|
870
1060
|
function isCodeFile(p) {
|
|
871
1061
|
if (p.endsWith(".md"))
|
|
@@ -1453,6 +1643,8 @@ var EXEMPT_LITERALS = new Set([
|
|
|
1453
1643
|
var NON_PATH_TOP_SEGMENTS = new Set(["origin", "refs", "HEAD", "vinaya", "fix"]);
|
|
1454
1644
|
// ../../packages/aeg-core/src/reader-resolvable-prose.ts
|
|
1455
1645
|
var SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
|
|
1646
|
+
// ../../packages/aeg-core/src/quoted-command.ts
|
|
1647
|
+
var QUOTED_COMMAND_SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
|
|
1456
1648
|
// ../../packages/aeg-core/src/retired-vocabulary.ts
|
|
1457
1649
|
var RETIRED_PATTERNS = [
|
|
1458
1650
|
String.raw`\bD-[0-9]{3}\b`,
|
|
@@ -1545,15 +1737,15 @@ function releaseFieldFromText(text) {
|
|
|
1545
1737
|
const raw = unwrapValue2(m[1] ?? "");
|
|
1546
1738
|
return { declared: true, value: RELEASE_VALUE.test(raw) ? raw : null };
|
|
1547
1739
|
}
|
|
1548
|
-
var
|
|
1549
|
-
var
|
|
1550
|
-
var
|
|
1740
|
+
var INTENTS_HEADING2 = /^#{1,6}\s*Tranche intents\s*$/im;
|
|
1741
|
+
var NEXT_HEADING2 = /^#{1,6}\s+\S/m;
|
|
1742
|
+
var INTENT_BULLET2 = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
|
|
1551
1743
|
function intentsBlock(text) {
|
|
1552
|
-
const start = text.match(
|
|
1744
|
+
const start = text.match(INTENTS_HEADING2);
|
|
1553
1745
|
if (!start || start.index === undefined)
|
|
1554
1746
|
return null;
|
|
1555
1747
|
const rest = text.slice(start.index + start[0].length);
|
|
1556
|
-
const next = rest.match(
|
|
1748
|
+
const next = rest.match(NEXT_HEADING2);
|
|
1557
1749
|
const sectionEnd = next && next.index !== undefined ? next.index : rest.length;
|
|
1558
1750
|
return { start: start.index, end: start.index + start[0].length + sectionEnd, section: rest.slice(0, sectionEnd) };
|
|
1559
1751
|
}
|
|
@@ -1570,7 +1762,7 @@ function parseIntents(text) {
|
|
|
1570
1762
|
const trimmed = line.trim();
|
|
1571
1763
|
if (trimmed.length === 0)
|
|
1572
1764
|
continue;
|
|
1573
|
-
const m = trimmed.match(
|
|
1765
|
+
const m = trimmed.match(INTENT_BULLET2);
|
|
1574
1766
|
if (!m)
|
|
1575
1767
|
return { intents: [], malformed: true };
|
|
1576
1768
|
intents.push({ slug: (m[1] ?? "").toLowerCase(), goal: (m[2] ?? "").trim() });
|
|
@@ -1628,16 +1820,16 @@ function checkAdoptable(facts) {
|
|
|
1628
1820
|
}
|
|
1629
1821
|
// src/lib/detect.ts
|
|
1630
1822
|
import { execFile as execFile4 } from "node:child_process";
|
|
1631
|
-
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as
|
|
1823
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync } from "node:fs";
|
|
1632
1824
|
import { join as join7 } from "node:path";
|
|
1633
1825
|
import { promisify as promisify4 } from "node:util";
|
|
1634
1826
|
|
|
1635
1827
|
// src/lib/artifacts.ts
|
|
1636
|
-
import { readFileSync as
|
|
1828
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1637
1829
|
import { join as join4 } from "node:path";
|
|
1638
1830
|
|
|
1639
1831
|
// src/commands/doctrine.ts
|
|
1640
|
-
import { existsSync as existsSync2, readdirSync, readFileSync } from "node:fs";
|
|
1832
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
1641
1833
|
import { dirname as dirname2, join as join2, sep } from "node:path";
|
|
1642
1834
|
import matter2 from "gray-matter";
|
|
1643
1835
|
|
|
@@ -1675,17 +1867,20 @@ function resolveDoctrineRoot(pkg = packageRoot(import.meta.url)) {
|
|
|
1675
1867
|
if (!pkg.split(sep).includes("node_modules"))
|
|
1676
1868
|
candidates.push(join2(dirname2(dirname2(pkg)), "aeg-root"));
|
|
1677
1869
|
for (const root of candidates) {
|
|
1678
|
-
if (
|
|
1870
|
+
if (hasDoctrineEntry(root))
|
|
1679
1871
|
return root;
|
|
1680
1872
|
}
|
|
1681
1873
|
return null;
|
|
1682
1874
|
}
|
|
1875
|
+
function hasDoctrineEntry(root) {
|
|
1876
|
+
return existsSync2(join2(root, ...ENTRY_SEGMENTS));
|
|
1877
|
+
}
|
|
1683
1878
|
function listRoleNames(root) {
|
|
1684
1879
|
const rolesDir = join2(root, "roles");
|
|
1685
1880
|
if (!existsSync2(rolesDir))
|
|
1686
1881
|
return [];
|
|
1687
1882
|
return readdirSync(rolesDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -".md".length)).filter((roleName) => {
|
|
1688
|
-
const { data } = matter2(
|
|
1883
|
+
const { data } = matter2(readFileSync2(join2(rolesDir, `${roleName}.md`), "utf8"));
|
|
1689
1884
|
return data.actor !== "human";
|
|
1690
1885
|
}).sort();
|
|
1691
1886
|
}
|
|
@@ -1724,7 +1919,7 @@ function doctrineCommand(args) {
|
|
|
1724
1919
|
}
|
|
1725
1920
|
|
|
1726
1921
|
// src/lib/agents-skills-emitter.ts
|
|
1727
|
-
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as
|
|
1922
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
1728
1923
|
import { join as join3 } from "node:path";
|
|
1729
1924
|
import matter3 from "gray-matter";
|
|
1730
1925
|
var AGENTS_SKILLS_GROUP = "Agent skills (.agents/skills/)";
|
|
@@ -1736,7 +1931,7 @@ function discoverRoleNames(doctrineRoot) {
|
|
|
1736
1931
|
if (!existsSync3(rolesDir))
|
|
1737
1932
|
return [];
|
|
1738
1933
|
return readdirSync2(rolesDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -".md".length)).filter((roleName) => {
|
|
1739
|
-
const { data } = matter3(
|
|
1934
|
+
const { data } = matter3(readFileSync3(join3(rolesDir, `${roleName}.md`), "utf8"));
|
|
1740
1935
|
return data.actor !== "human";
|
|
1741
1936
|
}).sort();
|
|
1742
1937
|
}
|
|
@@ -1808,9 +2003,11 @@ process.stdin.on("end", () => {
|
|
|
1808
2003
|
return
|
|
1809
2004
|
}
|
|
1810
2005
|
if (!hook.transcript_path) return
|
|
2006
|
+
const crypto = require("crypto")
|
|
1811
2007
|
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd()
|
|
1812
2008
|
const tmpDir = process.env.TMPDIR || "/tmp"
|
|
1813
|
-
const
|
|
2009
|
+
const digest = crypto.createHash("sha256").update(projectDir).digest("hex")
|
|
2010
|
+
const key = projectDir.replace(/[^A-Za-z0-9]+/g, "-") + "-" + digest
|
|
1814
2011
|
const pointerPath = tmpDir + "/claude-transcript-" + key + ".txt"
|
|
1815
2012
|
const content = (hook.session_id || "") + "\\t" + hook.transcript_path + "\\n"
|
|
1816
2013
|
const scratchPath = pointerPath + "." + process.pid + "." + Math.random().toString(36).slice(2) + ".tmp"
|
|
@@ -1927,7 +2124,7 @@ function starterConfig() {
|
|
|
1927
2124
|
};
|
|
1928
2125
|
}
|
|
1929
2126
|
function ownVersion() {
|
|
1930
|
-
const pkg = JSON.parse(
|
|
2127
|
+
const pkg = JSON.parse(readFileSync4(join4(packageRoot(import.meta.url), "package.json"), "utf-8"));
|
|
1931
2128
|
return pkg.version;
|
|
1932
2129
|
}
|
|
1933
2130
|
var SETUP_BUN_SHA = "0c5077e51419868618aeaa5fe8019c62421857d6";
|
|
@@ -2837,14 +3034,15 @@ function buildInitOps(ctx) {
|
|
|
2837
3034
|
|
|
2838
3035
|
// src/lib/ops.ts
|
|
2839
3036
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
2840
|
-
import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as
|
|
2841
|
-
import { dirname as dirname4, isAbsolute, join as join6, resolve, sep as sep2 } from "node:path";
|
|
3037
|
+
import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3038
|
+
import { dirname as dirname4, isAbsolute, join as join6, resolve as resolve2, sep as sep2 } from "node:path";
|
|
2842
3039
|
|
|
2843
3040
|
// src/lib/config.ts
|
|
2844
3041
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2845
|
-
import {
|
|
3042
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3043
|
+
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync5, realpathSync, writeFileSync } from "node:fs";
|
|
2846
3044
|
import { homedir } from "node:os";
|
|
2847
|
-
import { dirname as dirname3, join as join5 } from "node:path";
|
|
3045
|
+
import { dirname as dirname3, join as join5, resolve } from "node:path";
|
|
2848
3046
|
import { z } from "zod";
|
|
2849
3047
|
|
|
2850
3048
|
// src/lib/agent-vendors.ts
|
|
@@ -2995,6 +3193,20 @@ function resolveAgentVendors(manifest) {
|
|
|
2995
3193
|
return new Set(AGENT_VENDORS);
|
|
2996
3194
|
return new Set(manifest.agents);
|
|
2997
3195
|
}
|
|
3196
|
+
var ProjectEntrySchema = z.object({
|
|
3197
|
+
name: z.string().min(1),
|
|
3198
|
+
description: z.string().min(1).optional(),
|
|
3199
|
+
path: z.string().min(1).optional()
|
|
3200
|
+
});
|
|
3201
|
+
function parseTokensCollectDeclaration(value) {
|
|
3202
|
+
const m = value.trim().match(/^(\S+)\s+(\S+)$/);
|
|
3203
|
+
if (!m)
|
|
3204
|
+
return null;
|
|
3205
|
+
const [, interpreter, script] = m;
|
|
3206
|
+
if (!isSafeRepoRelPath(script))
|
|
3207
|
+
return null;
|
|
3208
|
+
return { interpreter, script };
|
|
3209
|
+
}
|
|
2998
3210
|
var VinayaConfigSchema = z.object({
|
|
2999
3211
|
rings: z.object({
|
|
3000
3212
|
ring1_forgeWriteInterception: z.boolean(),
|
|
@@ -3007,13 +3219,19 @@ var VinayaConfigSchema = z.object({
|
|
|
3007
3219
|
principals: z.array(z.string()).min(1).optional(),
|
|
3008
3220
|
releaseActor: z.string().min(1).optional(),
|
|
3009
3221
|
ci: z.object({ setup: z.string().min(1) }).optional(),
|
|
3222
|
+
tokens: z.object({
|
|
3223
|
+
collect: z.string().min(1).refine((v) => parseTokensCollectDeclaration(v) !== null, {
|
|
3224
|
+
message: 'tokens.collect must be exactly "<interpreter> <repo-relative-script-path>" — two whitespace-separated tokens, no flags, no shell syntax, no quoting'
|
|
3225
|
+
})
|
|
3226
|
+
}).optional(),
|
|
3010
3227
|
blastRadius: z.object({ extraDomains: z.array(z.string()).optional() }).optional(),
|
|
3011
3228
|
proseGates: z.object({
|
|
3012
3229
|
doctrineRoot: z.string().min(1).optional(),
|
|
3013
3230
|
readerFacingPrefix: z.string().min(1).optional(),
|
|
3014
3231
|
readerFacingSuffix: z.string().min(1).optional(),
|
|
3015
3232
|
legacySlugDir: z.string().min(1).optional()
|
|
3016
|
-
}).optional()
|
|
3233
|
+
}).optional(),
|
|
3234
|
+
projects: z.array(ProjectEntrySchema).optional()
|
|
3017
3235
|
});
|
|
3018
3236
|
var GLOBAL_VINAYA_HOME = join5(homedir(), ".vinaya");
|
|
3019
3237
|
var GLOBAL_CONFIG_PATH = join5(GLOBAL_VINAYA_HOME, "config.json");
|
|
@@ -3052,6 +3270,9 @@ function globalRolesIgnoredWarning(path) {
|
|
|
3052
3270
|
function globalReleaseActorIgnoredWarning(path) {
|
|
3053
3271
|
return `${path}: "releaseActor" in the global config is ignored — releaseActor may only be declared from a repo-local vinaya.config.json.`;
|
|
3054
3272
|
}
|
|
3273
|
+
function globalTokensCollectIgnoredWarning(path) {
|
|
3274
|
+
return `${path}: "tokens" in the global config is ignored — tokens.collect may only be declared from a repo-local vinaya.config.json.`;
|
|
3275
|
+
}
|
|
3055
3276
|
function stripGlobalOnlyKeys(config, path) {
|
|
3056
3277
|
if (path !== GLOBAL_CONFIG_PATH)
|
|
3057
3278
|
return config;
|
|
@@ -3072,6 +3293,10 @@ function stripGlobalOnlyKeys(config, path) {
|
|
|
3072
3293
|
console.error(`⚠ ${globalReleaseActorIgnoredWarning(path)}`);
|
|
3073
3294
|
result = { ...result, releaseActor: undefined };
|
|
3074
3295
|
}
|
|
3296
|
+
if (result.tokens) {
|
|
3297
|
+
console.error(`⚠ ${globalTokensCollectIgnoredWarning(path)}`);
|
|
3298
|
+
result = { ...result, tokens: undefined };
|
|
3299
|
+
}
|
|
3075
3300
|
return result;
|
|
3076
3301
|
}
|
|
3077
3302
|
function resolvePrincipalAllowlist(config) {
|
|
@@ -3148,7 +3373,7 @@ function loadConfig() {
|
|
|
3148
3373
|
if (!path)
|
|
3149
3374
|
return null;
|
|
3150
3375
|
try {
|
|
3151
|
-
const raw = JSON.parse(
|
|
3376
|
+
const raw = JSON.parse(readFileSync5(path, "utf-8"));
|
|
3152
3377
|
return stripGlobalOnlyKeys(VinayaConfigSchema.parse(raw), path);
|
|
3153
3378
|
} catch {
|
|
3154
3379
|
return null;
|
|
@@ -3159,7 +3384,7 @@ function readRepoCiSetup(repoRoot) {
|
|
|
3159
3384
|
if (!existsSync4(p))
|
|
3160
3385
|
return null;
|
|
3161
3386
|
try {
|
|
3162
|
-
return VinayaConfigSchema.parse(JSON.parse(
|
|
3387
|
+
return VinayaConfigSchema.parse(JSON.parse(readFileSync5(p, "utf-8"))).ci?.setup ?? null;
|
|
3163
3388
|
} catch {
|
|
3164
3389
|
return null;
|
|
3165
3390
|
}
|
|
@@ -3170,7 +3395,7 @@ function loadConfigChecked() {
|
|
|
3170
3395
|
return { ok: true, config: null };
|
|
3171
3396
|
let raw;
|
|
3172
3397
|
try {
|
|
3173
|
-
raw = JSON.parse(
|
|
3398
|
+
raw = JSON.parse(readFileSync5(path, "utf-8"));
|
|
3174
3399
|
} catch (err) {
|
|
3175
3400
|
return { ok: false, path, error: `invalid JSON: ${err.message}` };
|
|
3176
3401
|
}
|
|
@@ -3181,6 +3406,69 @@ function loadConfigChecked() {
|
|
|
3181
3406
|
}
|
|
3182
3407
|
return { ok: true, config: stripGlobalOnlyKeys(parsed.data, path) };
|
|
3183
3408
|
}
|
|
3409
|
+
var TOKENS_COLLECT_TRUST_PATH = join5(GLOBAL_VINAYA_HOME, "tokens-collect-trust.json");
|
|
3410
|
+
function readTokensCollectTrustStore(storePath) {
|
|
3411
|
+
try {
|
|
3412
|
+
if (!existsSync4(storePath))
|
|
3413
|
+
return {};
|
|
3414
|
+
const raw = JSON.parse(readFileSync5(storePath, "utf-8"));
|
|
3415
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
3416
|
+
} catch {
|
|
3417
|
+
return {};
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
function writeTokensCollectTrustStore(storePath, store) {
|
|
3421
|
+
const dir = dirname3(storePath);
|
|
3422
|
+
if (!existsSync4(dir))
|
|
3423
|
+
mkdirSync(dir, { recursive: true });
|
|
3424
|
+
writeFileSync(storePath, JSON.stringify(store, null, 2), "utf-8");
|
|
3425
|
+
}
|
|
3426
|
+
var GIT_IDENTITY_TIMEOUT_MS = 5000;
|
|
3427
|
+
function gitCommonDir(cwd = process.cwd()) {
|
|
3428
|
+
try {
|
|
3429
|
+
const raw = execFileSync2("git", ["rev-parse", "--git-common-dir"], {
|
|
3430
|
+
cwd,
|
|
3431
|
+
encoding: "utf-8",
|
|
3432
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3433
|
+
timeout: GIT_IDENTITY_TIMEOUT_MS
|
|
3434
|
+
}).trim();
|
|
3435
|
+
if (!raw)
|
|
3436
|
+
return null;
|
|
3437
|
+
return realpathSync(resolve(cwd, raw));
|
|
3438
|
+
} catch {
|
|
3439
|
+
return null;
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
function repoLocalConfigDir() {
|
|
3443
|
+
const local = findLocalConfig();
|
|
3444
|
+
return local ? dirname3(local) : null;
|
|
3445
|
+
}
|
|
3446
|
+
function gitBlobHash(absolutePath, cwd) {
|
|
3447
|
+
try {
|
|
3448
|
+
const raw = execFileSync2("git", ["hash-object", absolutePath], {
|
|
3449
|
+
cwd,
|
|
3450
|
+
encoding: "utf-8",
|
|
3451
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3452
|
+
timeout: GIT_IDENTITY_TIMEOUT_MS
|
|
3453
|
+
}).trim();
|
|
3454
|
+
return raw || null;
|
|
3455
|
+
} catch {
|
|
3456
|
+
return null;
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
function tokensCollectTrustKey(repoGitCommonDir, interpreter, script) {
|
|
3460
|
+
return createHash2("sha256").update(JSON.stringify([repoGitCommonDir, interpreter, script])).digest("hex");
|
|
3461
|
+
}
|
|
3462
|
+
function getTokensCollectTrust(repoGitCommonDir, interpreter, script, storePath = TOKENS_COLLECT_TRUST_PATH) {
|
|
3463
|
+
const key = tokensCollectTrustKey(repoGitCommonDir, interpreter, script);
|
|
3464
|
+
return readTokensCollectTrustStore(storePath)[key] ?? null;
|
|
3465
|
+
}
|
|
3466
|
+
function trustTokensCollectCommand(repoGitCommonDir, interpreter, script, scriptBlobHash, storePath = TOKENS_COLLECT_TRUST_PATH) {
|
|
3467
|
+
const store = readTokensCollectTrustStore(storePath);
|
|
3468
|
+
const key = tokensCollectTrustKey(repoGitCommonDir, interpreter, script);
|
|
3469
|
+
store[key] = { interpreter, script, scriptBlobHash, trustedAt: new Date().toISOString() };
|
|
3470
|
+
writeTokensCollectTrustStore(storePath, store);
|
|
3471
|
+
}
|
|
3184
3472
|
|
|
3185
3473
|
// src/lib/ops.ts
|
|
3186
3474
|
var MARKER_NS = "vinaya:managed";
|
|
@@ -3204,9 +3492,9 @@ var GIT_DIR_PREFIX = ".git/";
|
|
|
3204
3492
|
function resolveManagedBlockPath(repoRoot, opPath) {
|
|
3205
3493
|
if (!opPath.startsWith(GIT_DIR_PREFIX))
|
|
3206
3494
|
return join6(repoRoot, opPath);
|
|
3207
|
-
return join6(
|
|
3495
|
+
return join6(gitCommonDir2(repoRoot), opPath.slice(GIT_DIR_PREFIX.length));
|
|
3208
3496
|
}
|
|
3209
|
-
function
|
|
3497
|
+
function gitCommonDir2(repoRoot) {
|
|
3210
3498
|
try {
|
|
3211
3499
|
const commonDir = execFileSync3("git", ["-C", repoRoot, "rev-parse", "--git-common-dir"], {
|
|
3212
3500
|
encoding: "utf8"
|
|
@@ -3219,8 +3507,8 @@ function gitCommonDir(repoRoot) {
|
|
|
3219
3507
|
}
|
|
3220
3508
|
}
|
|
3221
3509
|
function containedAbs(repoRoot, relPath) {
|
|
3222
|
-
const root =
|
|
3223
|
-
const target =
|
|
3510
|
+
const root = resolve2(repoRoot);
|
|
3511
|
+
const target = resolve2(root, relPath);
|
|
3224
3512
|
if (target === root)
|
|
3225
3513
|
return null;
|
|
3226
3514
|
return target === root || target.startsWith(root + sep2) ? target : null;
|
|
@@ -3228,9 +3516,9 @@ function containedAbs(repoRoot, relPath) {
|
|
|
3228
3516
|
function containedManagedBlockAbs(repoRoot, relPath) {
|
|
3229
3517
|
if (!relPath.startsWith(GIT_DIR_PREFIX))
|
|
3230
3518
|
return containedAbs(repoRoot, relPath);
|
|
3231
|
-
const commonDir =
|
|
3232
|
-
const hooksRoot =
|
|
3233
|
-
const target =
|
|
3519
|
+
const commonDir = gitCommonDir2(repoRoot);
|
|
3520
|
+
const hooksRoot = resolve2(join6(commonDir, "hooks"));
|
|
3521
|
+
const target = resolve2(join6(commonDir, relPath.slice(GIT_DIR_PREFIX.length)));
|
|
3234
3522
|
if (target === hooksRoot)
|
|
3235
3523
|
return null;
|
|
3236
3524
|
return target.startsWith(hooksRoot + sep2) ? target : null;
|
|
@@ -3239,7 +3527,7 @@ function fileContains(repoRoot, relPath, needle) {
|
|
|
3239
3527
|
const p = resolveManagedBlockPath(repoRoot, relPath);
|
|
3240
3528
|
if (!existsSync5(p))
|
|
3241
3529
|
return false;
|
|
3242
|
-
return
|
|
3530
|
+
return readFileSync6(p, "utf-8").includes(needle);
|
|
3243
3531
|
}
|
|
3244
3532
|
function planInstall(ops, repoRoot, ownedFiles = new Set) {
|
|
3245
3533
|
const entries = [];
|
|
@@ -3349,7 +3637,7 @@ function writeFileWithDirs(target, content, mode) {
|
|
|
3349
3637
|
}
|
|
3350
3638
|
function appendBlock(repoRoot, op) {
|
|
3351
3639
|
const target = resolveManagedBlockPath(repoRoot, op.path);
|
|
3352
|
-
const existing =
|
|
3640
|
+
const existing = readFileSync6(target, "utf-8");
|
|
3353
3641
|
const sep3 = existing.endsWith(`
|
|
3354
3642
|
`) ? `
|
|
3355
3643
|
` : `
|
|
@@ -3471,7 +3759,7 @@ function planEject(manifest, repoRoot) {
|
|
|
3471
3759
|
});
|
|
3472
3760
|
continue;
|
|
3473
3761
|
}
|
|
3474
|
-
const content =
|
|
3762
|
+
const content = readFileSync6(p, "utf-8");
|
|
3475
3763
|
const stripped = stripBlockFromContent(content, b.marker, b.comment);
|
|
3476
3764
|
const removesHost = stripped !== null && blockStripLeavesEmpty(stripped);
|
|
3477
3765
|
actions.push({
|
|
@@ -3523,7 +3811,7 @@ function applyEject(plan, repoRoot) {
|
|
|
3523
3811
|
const p = containedManagedBlockAbs(repoRoot, a.path);
|
|
3524
3812
|
if (p === null)
|
|
3525
3813
|
continue;
|
|
3526
|
-
const content =
|
|
3814
|
+
const content = readFileSync6(p, "utf-8");
|
|
3527
3815
|
const stripped = stripBlockFromContent(content, a.marker, a.comment);
|
|
3528
3816
|
if (stripped === null)
|
|
3529
3817
|
continue;
|
|
@@ -3654,7 +3942,7 @@ function foreignRawHooks(repoRoot) {
|
|
|
3654
3942
|
return activeRawHooks(repoRoot).filter((f) => {
|
|
3655
3943
|
const abs2 = resolveManagedBlockPath(repoRoot, `.git/hooks/${f}`);
|
|
3656
3944
|
try {
|
|
3657
|
-
const stripped = stripBlockFromContent(
|
|
3945
|
+
const stripped = stripBlockFromContent(readFileSync7(abs2, "utf-8"), f, "hash");
|
|
3658
3946
|
return stripped === null || !blockStripLeavesEmpty(stripped);
|
|
3659
3947
|
} catch {
|
|
3660
3948
|
return true;
|
|
@@ -3766,8 +4054,8 @@ function prompt(question) {
|
|
|
3766
4054
|
setupStdinReader();
|
|
3767
4055
|
if (stdinEnded && pendingResolvers.length === 0)
|
|
3768
4056
|
return Promise.resolve("");
|
|
3769
|
-
return new Promise((
|
|
3770
|
-
pendingResolvers.push(
|
|
4057
|
+
return new Promise((resolve3) => {
|
|
4058
|
+
pendingResolvers.push(resolve3);
|
|
3771
4059
|
if (stdinEnded)
|
|
3772
4060
|
flushLines();
|
|
3773
4061
|
});
|
|
@@ -3786,83 +4074,354 @@ function closeStdin() {
|
|
|
3786
4074
|
}
|
|
3787
4075
|
}
|
|
3788
4076
|
|
|
3789
|
-
// src/commands/
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
4077
|
+
// src/commands/tokens.ts
|
|
4078
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
4079
|
+
import { resolve as resolvePath } from "node:path";
|
|
4080
|
+
var USAGE = [
|
|
4081
|
+
'Usage: vinaya tokens --phase "<task-id>: develop" --role Developer',
|
|
4082
|
+
" [--model <id>] [--transcript <path>] [--in <tokens-in> --out <tokens-out>]",
|
|
4083
|
+
"",
|
|
4084
|
+
" --transcript Read this transcript directly. Supported primary route — use it whenever you know",
|
|
4085
|
+
" which transcript is yours, and always in a repo with no track-transcript.sh hook.",
|
|
4086
|
+
" (omitted) Resolve via the Stop-hook pointer file, if this repo installs that hook.",
|
|
4087
|
+
" --in/--out Manual entry: the exact token figures, for a host whose usage arrives by some other",
|
|
4088
|
+
" means than a Claude Code transcript. Both required together; skips transcript",
|
|
4089
|
+
" resolution entirely.",
|
|
4090
|
+
"",
|
|
4091
|
+
"Usage: vinaya tokens --trust-collect",
|
|
4092
|
+
" Approves this repo's declared tokens.collect command, pinned to the script's exact current",
|
|
4093
|
+
" content, on this machine. Run this once before tokens.collect will ever execute — a changed",
|
|
4094
|
+
" interpreter, script path, or script CONTENT needs its own fresh approval. Does not take",
|
|
4095
|
+
" --phase/--role and never emits a Tokens: line."
|
|
4096
|
+
].join(`
|
|
4097
|
+
`);
|
|
4098
|
+
function parseArgs(argv) {
|
|
4099
|
+
let phase;
|
|
4100
|
+
let role;
|
|
4101
|
+
let model;
|
|
4102
|
+
let transcriptPath;
|
|
4103
|
+
let tokensInRaw;
|
|
4104
|
+
let tokensOutRaw;
|
|
4105
|
+
for (let i = 0;i < argv.length; i++) {
|
|
4106
|
+
const arg = argv[i];
|
|
4107
|
+
if (arg === "--phase")
|
|
4108
|
+
phase = argv[++i];
|
|
4109
|
+
else if (arg === "--role")
|
|
4110
|
+
role = argv[++i];
|
|
4111
|
+
else if (arg === "--model")
|
|
4112
|
+
model = argv[++i];
|
|
4113
|
+
else if (arg === "--transcript")
|
|
4114
|
+
transcriptPath = argv[++i];
|
|
4115
|
+
else if (arg === "--in")
|
|
4116
|
+
tokensInRaw = argv[++i];
|
|
4117
|
+
else if (arg === "--out")
|
|
4118
|
+
tokensOutRaw = argv[++i];
|
|
4119
|
+
}
|
|
4120
|
+
if (!phase || !role) {
|
|
4121
|
+
throw new Error(USAGE);
|
|
4122
|
+
}
|
|
4123
|
+
if (tokensInRaw === undefined !== (tokensOutRaw === undefined)) {
|
|
4124
|
+
throw new Error("vinaya tokens: --in and --out must be given together, or not at all.");
|
|
4125
|
+
}
|
|
4126
|
+
const tokensIn = tokensInRaw === undefined ? undefined : Number(tokensInRaw);
|
|
4127
|
+
const tokensOut = tokensOutRaw === undefined ? undefined : Number(tokensOutRaw);
|
|
4128
|
+
if (tokensIn !== undefined && (!Number.isFinite(tokensIn) || tokensIn < 0)) {
|
|
4129
|
+
throw new Error(`vinaya tokens: --in must be a non-negative integer, got "${tokensInRaw}".`);
|
|
4130
|
+
}
|
|
4131
|
+
if (tokensOut !== undefined && (!Number.isFinite(tokensOut) || tokensOut < 0)) {
|
|
4132
|
+
throw new Error(`vinaya tokens: --out must be a non-negative integer, got "${tokensOutRaw}".`);
|
|
4133
|
+
}
|
|
4134
|
+
return { phase, role, model, transcriptPath, tokensIn, tokensOut };
|
|
3795
4135
|
}
|
|
3796
|
-
function
|
|
3797
|
-
return
|
|
4136
|
+
function manualSummary(tokensIn, tokensOut) {
|
|
4137
|
+
return {
|
|
4138
|
+
components: {
|
|
4139
|
+
inputTokens: tokensIn,
|
|
4140
|
+
outputTokens: tokensOut,
|
|
4141
|
+
cacheCreationInputTokens: 0,
|
|
4142
|
+
cacheReadInputTokens: 0
|
|
4143
|
+
},
|
|
4144
|
+
model: null,
|
|
4145
|
+
messageCount: 1
|
|
4146
|
+
};
|
|
3798
4147
|
}
|
|
3799
|
-
function
|
|
3800
|
-
return
|
|
4148
|
+
function declaredSummary(components, model) {
|
|
4149
|
+
return { components, model, messageCount: 1 };
|
|
3801
4150
|
}
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
4151
|
+
var COLLECT_FIELDS = ["inputTokens", "outputTokens", "cacheCreationInputTokens", "cacheReadInputTokens"];
|
|
4152
|
+
function parseDeclaredCollectOutput(raw, command) {
|
|
4153
|
+
const trimmed = raw.trim();
|
|
4154
|
+
const fail = (why) => {
|
|
4155
|
+
throw new Error(`vinaya tokens: declared tokens.collect command "${command}" ${why}
|
|
4156
|
+
` + "Expected stdout to be a JSON object: " + `{"inputTokens":N,"outputTokens":N,"cacheCreationInputTokens":N,"cacheReadInputTokens":N,"model":"…"|null}
|
|
4157
|
+
` + `Got: ${trimmed.slice(0, 500)}`);
|
|
4158
|
+
};
|
|
4159
|
+
let parsed;
|
|
4160
|
+
try {
|
|
4161
|
+
parsed = JSON.parse(trimmed);
|
|
4162
|
+
} catch (err) {
|
|
4163
|
+
fail(`did not print valid JSON on stdout (${err.message}).`);
|
|
3807
4164
|
}
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
async function runArchive(args, deps) {
|
|
3811
|
-
if (ring2Accelerated()) {
|
|
3812
|
-
process.stdout.write("[vinaya archive] rings.ring2_asyncAudits is `true` (opt-in accelerator) — skipping, nothing changed.\n");
|
|
3813
|
-
return 0;
|
|
4165
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4166
|
+
fail("printed JSON that is not an object.");
|
|
3814
4167
|
}
|
|
3815
|
-
const
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
4168
|
+
const obj = parsed;
|
|
4169
|
+
const components = {};
|
|
4170
|
+
for (const field of COLLECT_FIELDS) {
|
|
4171
|
+
const value = obj[field];
|
|
4172
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
4173
|
+
fail(`printed an invalid "${field}" (must be a non-negative number), got ${JSON.stringify(value)}.`);
|
|
4174
|
+
}
|
|
4175
|
+
components[field] = value;
|
|
3819
4176
|
}
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
4177
|
+
const model = obj.model;
|
|
4178
|
+
if (model !== undefined && model !== null && typeof model !== "string") {
|
|
4179
|
+
fail(`printed a non-string "model" field: ${JSON.stringify(model)}.`);
|
|
3823
4180
|
}
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
`);
|
|
3830
|
-
return 0;
|
|
4181
|
+
return declaredSummary(components, typeof model === "string" ? model : null);
|
|
4182
|
+
}
|
|
4183
|
+
function resolveDeclaredCollect(command, deps) {
|
|
4184
|
+
const parsed = parseTokensCollectDeclaration(command);
|
|
4185
|
+
if (!parsed) {
|
|
4186
|
+
throw new Error(`vinaya tokens: declared tokens.collect "${command}" is not shaped "<interpreter> <script-path>" — ` + "this should have been rejected at config-load time; the config may have been edited after loading.");
|
|
3831
4187
|
}
|
|
3832
|
-
const
|
|
3833
|
-
|
|
3834
|
-
"
|
|
3835
|
-
"pr",
|
|
3836
|
-
"view",
|
|
3837
|
-
String(prNumber),
|
|
3838
|
-
"-R",
|
|
3839
|
-
repoFlag,
|
|
3840
|
-
"--json",
|
|
3841
|
-
"number,headRefName,body,mergedAt,comments"
|
|
3842
|
-
]);
|
|
3843
|
-
const ref = taskRefFromBranch(pr.headRefName);
|
|
3844
|
-
let issueLabels = [];
|
|
3845
|
-
if (ref === null) {
|
|
3846
|
-
const { issue: candidateIssue } = extractIssue(pr.body);
|
|
3847
|
-
if (candidateIssue !== null) {
|
|
3848
|
-
issueLabels = shJson([
|
|
3849
|
-
"gh",
|
|
3850
|
-
"issue",
|
|
3851
|
-
"view",
|
|
3852
|
-
String(candidateIssue),
|
|
3853
|
-
"-R",
|
|
3854
|
-
repoFlag,
|
|
3855
|
-
"--json",
|
|
3856
|
-
"labels"
|
|
3857
|
-
]).labels.map((l) => l.name);
|
|
3858
|
-
}
|
|
4188
|
+
const repoConfigDir = deps.repoConfigDir();
|
|
4189
|
+
if (repoConfigDir === null) {
|
|
4190
|
+
throw new Error("vinaya tokens: declared tokens.collect command could not be resolved — this repo's local " + "vinaya.config.json directory could not be found. Refusing to run an unresolvable command.");
|
|
3859
4191
|
}
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
return 0;
|
|
4192
|
+
const repoGitCommonDir = deps.gitCommonDir();
|
|
4193
|
+
if (repoGitCommonDir === null) {
|
|
4194
|
+
throw new Error("vinaya tokens: declared tokens.collect command could not be verified — this repo's git common " + "directory could not be resolved. Refusing to run an unverifiable command.");
|
|
3864
4195
|
}
|
|
3865
|
-
const
|
|
4196
|
+
const scriptAbsolutePath = resolvePath(repoConfigDir, parsed.script);
|
|
4197
|
+
if (!deps.exists(scriptAbsolutePath)) {
|
|
4198
|
+
throw new Error(`vinaya tokens: declared tokens.collect script "${parsed.script}" does not exist at ${scriptAbsolutePath}.`);
|
|
4199
|
+
}
|
|
4200
|
+
return { interpreter: parsed.interpreter, script: parsed.script, scriptAbsolutePath, repoGitCommonDir };
|
|
4201
|
+
}
|
|
4202
|
+
function runDeclaredCollect(command, deps) {
|
|
4203
|
+
const { interpreter, script, scriptAbsolutePath, repoGitCommonDir } = resolveDeclaredCollect(command, deps);
|
|
4204
|
+
const currentHash = deps.scriptContentHash(scriptAbsolutePath, deps.cwd);
|
|
4205
|
+
if (currentHash === null) {
|
|
4206
|
+
throw new Error(`vinaya tokens: declared tokens.collect script "${script}" could not be hashed for verification ` + "(git unavailable, or the file could not be read). Refusing to run an unverifiable script.");
|
|
4207
|
+
}
|
|
4208
|
+
const trust = deps.getTrust(repoGitCommonDir, interpreter, script);
|
|
4209
|
+
if (trust === null) {
|
|
4210
|
+
throw new Error(`vinaya tokens: declared tokens.collect is not yet trusted on this machine:
|
|
4211
|
+
` + ` ${interpreter} ${script}
|
|
4212
|
+
` + "Run `vinaya tokens --trust-collect` once to approve it. Refusing — no `Tokens:` line emitted, no zeros.");
|
|
4213
|
+
}
|
|
4214
|
+
if (trust.scriptBlobHash !== currentHash) {
|
|
4215
|
+
throw new Error(`vinaya tokens: declared tokens.collect script "${script}" no longer matches its trusted content ` + `(approved at ${trust.scriptBlobHash}, now ${currentHash}).
|
|
4216
|
+
` + "Run `vinaya tokens --trust-collect` again to approve the new content. Refusing — no `Tokens:` line emitted, no zeros.");
|
|
4217
|
+
}
|
|
4218
|
+
deps.warn(`⚠ vinaya tokens: running declared tokens.collect: ${interpreter} ${script}`);
|
|
4219
|
+
let raw;
|
|
4220
|
+
try {
|
|
4221
|
+
raw = deps.runScript(interpreter, scriptAbsolutePath, deps.cwd);
|
|
4222
|
+
} catch (err) {
|
|
4223
|
+
throw new Error(`vinaya tokens: declared tokens.collect "${interpreter} ${script}" failed: ${err.message}`);
|
|
4224
|
+
}
|
|
4225
|
+
return parseDeclaredCollectOutput(raw, command);
|
|
4226
|
+
}
|
|
4227
|
+
var COLLECT_COMMAND_TIMEOUT_MS = 30000;
|
|
4228
|
+
function realDeps() {
|
|
4229
|
+
return {
|
|
4230
|
+
...hardenedMeteringDeps(),
|
|
4231
|
+
loadConfig,
|
|
4232
|
+
repoConfigDir: repoLocalConfigDir,
|
|
4233
|
+
runScript: (interpreter, scriptAbsolutePath, cwd) => execFileSync4(interpreter, [scriptAbsolutePath], { cwd, encoding: "utf-8", timeout: COLLECT_COMMAND_TIMEOUT_MS }),
|
|
4234
|
+
warn: (message) => console.error(message),
|
|
4235
|
+
gitCommonDir,
|
|
4236
|
+
scriptContentHash: gitBlobHash,
|
|
4237
|
+
getTrust: getTokensCollectTrust,
|
|
4238
|
+
trustCollect: trustTokensCollectCommand
|
|
4239
|
+
};
|
|
4240
|
+
}
|
|
4241
|
+
function buildTokensResult(parsed, deps) {
|
|
4242
|
+
if (parsed.tokensIn !== undefined && parsed.tokensOut !== undefined) {
|
|
4243
|
+
const summary = manualSummary(parsed.tokensIn, parsed.tokensOut);
|
|
4244
|
+
return {
|
|
4245
|
+
line: formatTokensLine({ phase: parsed.phase, role: parsed.role, summary, modelOverride: parsed.model }),
|
|
4246
|
+
breakdown: undefined
|
|
4247
|
+
};
|
|
4248
|
+
}
|
|
4249
|
+
const declaredCommand = deps.loadConfig()?.tokens?.collect;
|
|
4250
|
+
if (declaredCommand) {
|
|
4251
|
+
const summary = runDeclaredCollect(declaredCommand, deps);
|
|
4252
|
+
return {
|
|
4253
|
+
line: formatTokensLine({ phase: parsed.phase, role: parsed.role, summary, modelOverride: parsed.model }),
|
|
4254
|
+
breakdown: formatBreakdown(summary)
|
|
4255
|
+
};
|
|
4256
|
+
}
|
|
4257
|
+
const capability = resolveMeteringCapability(deps, parsed.transcriptPath);
|
|
4258
|
+
if (!capability.capable) {
|
|
4259
|
+
throw new Error(`vinaya tokens: refused — could not resolve real usage figures (${capability.reason}).
|
|
4260
|
+
${capability.detail}
|
|
4261
|
+
|
|
4262
|
+
${USAGE}`);
|
|
4263
|
+
}
|
|
4264
|
+
return {
|
|
4265
|
+
line: formatTokensLine({
|
|
4266
|
+
phase: parsed.phase,
|
|
4267
|
+
role: parsed.role,
|
|
4268
|
+
summary: capability.summary,
|
|
4269
|
+
modelOverride: parsed.model
|
|
4270
|
+
}),
|
|
4271
|
+
breakdown: formatBreakdown(capability.summary)
|
|
4272
|
+
};
|
|
4273
|
+
}
|
|
4274
|
+
function runTrustCollect(deps) {
|
|
4275
|
+
const declaredCommand = deps.loadConfig()?.tokens?.collect;
|
|
4276
|
+
if (!declaredCommand) {
|
|
4277
|
+
return {
|
|
4278
|
+
ok: false,
|
|
4279
|
+
message: "vinaya tokens --trust-collect: no tokens.collect declared in this repo's vinaya.config.json — nothing to trust."
|
|
4280
|
+
};
|
|
4281
|
+
}
|
|
4282
|
+
let resolved;
|
|
4283
|
+
try {
|
|
4284
|
+
resolved = resolveDeclaredCollect(declaredCommand, deps);
|
|
4285
|
+
} catch (err) {
|
|
4286
|
+
return { ok: false, message: err.message };
|
|
4287
|
+
}
|
|
4288
|
+
const { interpreter, script, scriptAbsolutePath, repoGitCommonDir } = resolved;
|
|
4289
|
+
const scriptBlobHash = deps.scriptContentHash(scriptAbsolutePath, deps.cwd);
|
|
4290
|
+
if (scriptBlobHash === null) {
|
|
4291
|
+
return {
|
|
4292
|
+
ok: false,
|
|
4293
|
+
message: `vinaya tokens --trust-collect: script "${script}" could not be hashed (git unavailable, or the file could not be read) — refusing to trust blindly.`
|
|
4294
|
+
};
|
|
4295
|
+
}
|
|
4296
|
+
deps.trustCollect(repoGitCommonDir, interpreter, script, scriptBlobHash);
|
|
4297
|
+
return { ok: true, interpreter, script, scriptBlobHash };
|
|
4298
|
+
}
|
|
4299
|
+
function tokensCommand(argv) {
|
|
4300
|
+
if (argv.includes("--trust-collect")) {
|
|
4301
|
+
const outcome = runTrustCollect(realDeps());
|
|
4302
|
+
if (!outcome.ok) {
|
|
4303
|
+
console.error(outcome.message);
|
|
4304
|
+
process.exit(1);
|
|
4305
|
+
}
|
|
4306
|
+
process.stdout.write("Trusted. Future `vinaya tokens` runs on this machine execute it without asking again until the " + `interpreter, script path, or script content changes.
|
|
4307
|
+
${outcome.interpreter} ${outcome.script}
|
|
4308
|
+
` + ` content hash: ${outcome.scriptBlobHash}
|
|
4309
|
+
`);
|
|
4310
|
+
return;
|
|
4311
|
+
}
|
|
4312
|
+
let parsed;
|
|
4313
|
+
try {
|
|
4314
|
+
parsed = parseArgs(argv);
|
|
4315
|
+
} catch (err) {
|
|
4316
|
+
console.error(err.message);
|
|
4317
|
+
process.exit(2);
|
|
4318
|
+
}
|
|
4319
|
+
let result;
|
|
4320
|
+
try {
|
|
4321
|
+
result = buildTokensResult(parsed, realDeps());
|
|
4322
|
+
} catch (err) {
|
|
4323
|
+
console.error(err.message);
|
|
4324
|
+
process.exit(1);
|
|
4325
|
+
}
|
|
4326
|
+
if (result.breakdown)
|
|
4327
|
+
console.error(result.breakdown);
|
|
4328
|
+
process.stdout.write(`${result.line}
|
|
4329
|
+
`);
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
// src/commands/archive.ts
|
|
4333
|
+
function ring2Accelerated() {
|
|
4334
|
+
return loadConfig()?.rings?.ring2_asyncAudits === true;
|
|
4335
|
+
}
|
|
4336
|
+
function realDeps2() {
|
|
4337
|
+
return { detectRepo: detectGitRepo, meteringCapability: () => resolveMeteringCapability(realDeps()) };
|
|
4338
|
+
}
|
|
4339
|
+
function isEmptySummary(summary) {
|
|
4340
|
+
const { inputTokens, outputTokens, cacheCreationInputTokens, cacheReadInputTokens } = summary.components;
|
|
4341
|
+
return inputTokens === 0 && outputTokens === 0 && cacheCreationInputTokens === 0 && cacheReadInputTokens === 0;
|
|
4342
|
+
}
|
|
4343
|
+
function renderArchiveTokensLine(capability, phase, role) {
|
|
4344
|
+
if (!capability.capable) {
|
|
4345
|
+
return { line: formatTokensLine({ phase, role, summary: null }), dangling: null };
|
|
4346
|
+
}
|
|
4347
|
+
if (isEmptySummary(capability.summary)) {
|
|
4348
|
+
return {
|
|
4349
|
+
line: null,
|
|
4350
|
+
dangling: `Archivist Tokens: line omitted — metering probe reports capable (transcript ${capability.transcriptPath}) ` + `but summarized to zero tokens across ${capability.summary.messageCount} message(s); posting a real zero ` + "would misrepresent the turn, and `—` is sanctioned only for a genuinely incapable host."
|
|
4351
|
+
};
|
|
4352
|
+
}
|
|
4353
|
+
return { line: formatTokensLine({ phase, role, summary: capability.summary }), dangling: null };
|
|
4354
|
+
}
|
|
4355
|
+
function sh(args, input) {
|
|
4356
|
+
return execFileSync5(args[0], args.slice(1), { encoding: "utf8", input, env: process.env }).trim();
|
|
4357
|
+
}
|
|
4358
|
+
function shJson(args) {
|
|
4359
|
+
return JSON.parse(sh(args));
|
|
4360
|
+
}
|
|
4361
|
+
function parseMergeSha(args) {
|
|
4362
|
+
for (const a of args) {
|
|
4363
|
+
const m = a.match(/^--merge-sha=(.+)$/);
|
|
4364
|
+
if (m)
|
|
4365
|
+
return m[1];
|
|
4366
|
+
}
|
|
4367
|
+
return null;
|
|
4368
|
+
}
|
|
4369
|
+
async function runArchive(args, deps) {
|
|
4370
|
+
if (ring2Accelerated()) {
|
|
4371
|
+
process.stdout.write("[vinaya archive] rings.ring2_asyncAudits is `true` (opt-in accelerator) — skipping, nothing changed.\n");
|
|
4372
|
+
return 0;
|
|
4373
|
+
}
|
|
4374
|
+
const repo = await deps.detectRepo();
|
|
4375
|
+
if (!repo) {
|
|
4376
|
+
console.error("Error: not a git repository. Run `vinaya archive` from inside your repo.");
|
|
4377
|
+
return 1;
|
|
4378
|
+
}
|
|
4379
|
+
if (!repo.owner || !repo.repo) {
|
|
4380
|
+
console.error("Error: could not resolve a GitHub owner/repo from the `origin` remote.");
|
|
4381
|
+
return 1;
|
|
4382
|
+
}
|
|
4383
|
+
const repoFlag = `${repo.owner}/${repo.repo}`;
|
|
4384
|
+
const mergeSha = parseMergeSha(args) ?? sh(["git", "rev-parse", "HEAD"]);
|
|
4385
|
+
const associated = shJson(["gh", "api", `repos/${repoFlag}/commits/${mergeSha}/pulls`]);
|
|
4386
|
+
if (associated.length === 0) {
|
|
4387
|
+
process.stdout.write(`[vinaya archive] no associated PR for merge ${mergeSha} — skip.
|
|
4388
|
+
`);
|
|
4389
|
+
return 0;
|
|
4390
|
+
}
|
|
4391
|
+
const prNumber = associated[0].number;
|
|
4392
|
+
const pr = shJson([
|
|
4393
|
+
"gh",
|
|
4394
|
+
"pr",
|
|
4395
|
+
"view",
|
|
4396
|
+
String(prNumber),
|
|
4397
|
+
"-R",
|
|
4398
|
+
repoFlag,
|
|
4399
|
+
"--json",
|
|
4400
|
+
"number,headRefName,body,mergedAt,comments"
|
|
4401
|
+
]);
|
|
4402
|
+
const ref = taskRefFromBranch(pr.headRefName);
|
|
4403
|
+
let issueLabels = [];
|
|
4404
|
+
if (ref === null) {
|
|
4405
|
+
const { issue: candidateIssue } = extractIssue(pr.body);
|
|
4406
|
+
if (candidateIssue !== null) {
|
|
4407
|
+
issueLabels = shJson([
|
|
4408
|
+
"gh",
|
|
4409
|
+
"issue",
|
|
4410
|
+
"view",
|
|
4411
|
+
String(candidateIssue),
|
|
4412
|
+
"-R",
|
|
4413
|
+
repoFlag,
|
|
4414
|
+
"--json",
|
|
4415
|
+
"labels"
|
|
4416
|
+
]).labels.map((l) => l.name);
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4419
|
+
if (!isEligibleForProvenance(ref, issueLabels)) {
|
|
4420
|
+
process.stdout.write(`[vinaya archive] non-task branch (${pr.headRefName}) and closing Issue carries no vinaya/tranche:* label — skip.
|
|
4421
|
+
`);
|
|
4422
|
+
return 0;
|
|
4423
|
+
}
|
|
4424
|
+
const comments = pr.comments.map((c) => c.body);
|
|
3866
4425
|
if (hasProvenance(comments)) {
|
|
3867
4426
|
process.stdout.write(`[vinaya archive] provenance already present on PR #${pr.number} — skip (idempotent).
|
|
3868
4427
|
`);
|
|
@@ -3877,13 +4436,23 @@ async function runArchive(args, deps) {
|
|
|
3877
4436
|
comments
|
|
3878
4437
|
};
|
|
3879
4438
|
const { block, issue, dangling } = buildProvenanceBlock(facts);
|
|
4439
|
+
const phase = `${ref ? ref.taskId : pr.headRefName}: archive`;
|
|
4440
|
+
const tokensResult = renderArchiveTokensLine(deps.meteringCapability(), phase, "Archivist");
|
|
4441
|
+
const tokensAddition = tokensResult.line !== null ? tokensResult.line : `DANGLING (tokens): ${tokensResult.dangling}`;
|
|
4442
|
+
const blockWithTokens = `${block}
|
|
4443
|
+
|
|
4444
|
+
${tokensAddition}`;
|
|
3880
4445
|
process.stdout.write(`[vinaya archive] posting provenance block to PR #${pr.number}...
|
|
3881
4446
|
`);
|
|
3882
|
-
sh(["gh", "pr", "comment", String(pr.number), "-R", repoFlag, "--body-file", "-"],
|
|
4447
|
+
sh(["gh", "pr", "comment", String(pr.number), "-R", repoFlag, "--body-file", "-"], blockWithTokens);
|
|
3883
4448
|
process.stdout.write(`[vinaya archive] provenance block posted to PR #${pr.number}.
|
|
3884
4449
|
`);
|
|
3885
4450
|
if (dangling.length > 0) {
|
|
3886
4451
|
process.stdout.write(`[vinaya archive] DANGLING (${dangling.length}): ${dangling.join("; ")}
|
|
4452
|
+
`);
|
|
4453
|
+
}
|
|
4454
|
+
if (tokensResult.dangling !== null) {
|
|
4455
|
+
process.stdout.write(`[vinaya archive] DANGLING (tokens): ${tokensResult.dangling}
|
|
3887
4456
|
`);
|
|
3888
4457
|
}
|
|
3889
4458
|
if (issue !== null) {
|
|
@@ -3915,7 +4484,7 @@ async function runArchive(args, deps) {
|
|
|
3915
4484
|
return 0;
|
|
3916
4485
|
}
|
|
3917
4486
|
async function archiveCommand(args) {
|
|
3918
|
-
process.exit(await runArchive(args,
|
|
4487
|
+
process.exit(await runArchive(args, realDeps2()));
|
|
3919
4488
|
}
|
|
3920
4489
|
function parseTrancheArgs(args) {
|
|
3921
4490
|
const yes = args.includes("--yes");
|
|
@@ -3994,19 +4563,19 @@ async function runArchiveTranche(args, deps) {
|
|
|
3994
4563
|
return 0;
|
|
3995
4564
|
}
|
|
3996
4565
|
async function archiveTrancheCommand(args) {
|
|
3997
|
-
process.exit(await runArchiveTranche(args,
|
|
4566
|
+
process.exit(await runArchiveTranche(args, realDeps2()));
|
|
3998
4567
|
}
|
|
3999
4568
|
|
|
4000
4569
|
// src/commands/audit.ts
|
|
4001
|
-
import { execFileSync as
|
|
4570
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
4002
4571
|
function ring2Accelerated2() {
|
|
4003
4572
|
return loadConfig()?.rings?.ring2_asyncAudits === true;
|
|
4004
4573
|
}
|
|
4005
|
-
function
|
|
4574
|
+
function realDeps3() {
|
|
4006
4575
|
return { detectRepo: detectGitRepo };
|
|
4007
4576
|
}
|
|
4008
4577
|
function defaultSleep(ms) {
|
|
4009
|
-
return new Promise((
|
|
4578
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4010
4579
|
}
|
|
4011
4580
|
var COMMAND_TIMEOUT_MS = 20000;
|
|
4012
4581
|
var DEAD_BRANCH_LABEL = label("dead-branch-push");
|
|
@@ -4017,7 +4586,7 @@ var DIRECT_PUSH_MARKER = `<!-- ${DIRECT_PUSH_LABEL} -->`;
|
|
|
4017
4586
|
var DIRECT_PUSH_LABEL_DESCRIPTION = "A commit landed on main with no associated merged PR (ring-2 detection)";
|
|
4018
4587
|
function shSoft(args, input) {
|
|
4019
4588
|
try {
|
|
4020
|
-
return
|
|
4589
|
+
return execFileSync6(args[0], args.slice(1), {
|
|
4021
4590
|
encoding: "utf8",
|
|
4022
4591
|
input,
|
|
4023
4592
|
timeout: COMMAND_TIMEOUT_MS
|
|
@@ -4041,7 +4610,7 @@ function shSoftJson(args) {
|
|
|
4041
4610
|
}
|
|
4042
4611
|
}
|
|
4043
4612
|
function sh2(args, input) {
|
|
4044
|
-
return
|
|
4613
|
+
return execFileSync6(args[0], args.slice(1), { encoding: "utf8", input }).trim();
|
|
4045
4614
|
}
|
|
4046
4615
|
function listTaskBranches() {
|
|
4047
4616
|
const out = shSoft(["git", "ls-remote", "--heads", "origin", "task/*"]);
|
|
@@ -4297,11 +4866,11 @@ async function runAudit(args, deps) {
|
|
|
4297
4866
|
return failed ? 1 : 0;
|
|
4298
4867
|
}
|
|
4299
4868
|
async function auditCommand(args) {
|
|
4300
|
-
process.exit(await runAudit(args,
|
|
4869
|
+
process.exit(await runAudit(args, realDeps3()));
|
|
4301
4870
|
}
|
|
4302
4871
|
|
|
4303
4872
|
// src/commands/check.ts
|
|
4304
|
-
import { execFileSync as
|
|
4873
|
+
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
4305
4874
|
import { dirname as dirname5 } from "node:path";
|
|
4306
4875
|
|
|
4307
4876
|
// src/checks/contract.ts
|
|
@@ -4451,6 +5020,21 @@ var REGISTRY = [
|
|
|
4451
5020
|
},
|
|
4452
5021
|
1
|
|
4453
5022
|
],
|
|
5023
|
+
[
|
|
5024
|
+
{
|
|
5025
|
+
name: "token-report",
|
|
5026
|
+
run: bin("check-token-report"),
|
|
5027
|
+
scope: "diff",
|
|
5028
|
+
timeoutMs: 15000,
|
|
5029
|
+
requiresOpenPr: true,
|
|
5030
|
+
env: {
|
|
5031
|
+
PR_BODY: { optional: true },
|
|
5032
|
+
CLAUDE_PROJECT_DIR: { optional: true },
|
|
5033
|
+
CLAUDE_CODE_SESSION_ID: { optional: true }
|
|
5034
|
+
}
|
|
5035
|
+
},
|
|
5036
|
+
1
|
|
5037
|
+
],
|
|
4454
5038
|
[
|
|
4455
5039
|
{
|
|
4456
5040
|
name: "no-disk-state",
|
|
@@ -4627,6 +5211,26 @@ var REGISTRY = [
|
|
|
4627
5211
|
},
|
|
4628
5212
|
0
|
|
4629
5213
|
],
|
|
5214
|
+
[
|
|
5215
|
+
{
|
|
5216
|
+
name: "changeset-coverage",
|
|
5217
|
+
run: bin("check-changeset-coverage"),
|
|
5218
|
+
scope: "diff",
|
|
5219
|
+
timeoutMs: 15000,
|
|
5220
|
+
env: {}
|
|
5221
|
+
},
|
|
5222
|
+
0
|
|
5223
|
+
],
|
|
5224
|
+
[
|
|
5225
|
+
{
|
|
5226
|
+
name: "quoted-command",
|
|
5227
|
+
run: bin("check-quoted-command"),
|
|
5228
|
+
scope: "diff",
|
|
5229
|
+
timeoutMs: 15000,
|
|
5230
|
+
env: {}
|
|
5231
|
+
},
|
|
5232
|
+
0
|
|
5233
|
+
],
|
|
4630
5234
|
[
|
|
4631
5235
|
{
|
|
4632
5236
|
name: "main-branch-refusal",
|
|
@@ -4636,6 +5240,19 @@ var REGISTRY = [
|
|
|
4636
5240
|
env: {}
|
|
4637
5241
|
},
|
|
4638
5242
|
0
|
|
5243
|
+
],
|
|
5244
|
+
[
|
|
5245
|
+
{
|
|
5246
|
+
name: "token-collection-wired",
|
|
5247
|
+
run: bin("check-token-collection-wired"),
|
|
5248
|
+
scope: "full",
|
|
5249
|
+
timeoutMs: 15000,
|
|
5250
|
+
env: {
|
|
5251
|
+
CLAUDE_PROJECT_DIR: { optional: true },
|
|
5252
|
+
CLAUDE_CODE_SESSION_ID: { optional: true }
|
|
5253
|
+
}
|
|
5254
|
+
},
|
|
5255
|
+
0
|
|
4639
5256
|
]
|
|
4640
5257
|
];
|
|
4641
5258
|
function coreCheckRegistry() {
|
|
@@ -4843,16 +5460,16 @@ async function runOne(spec, timeoutMs, callerEnv) {
|
|
|
4843
5460
|
const timer = setTimeout(() => {
|
|
4844
5461
|
timedOut = true;
|
|
4845
5462
|
killTree("SIGTERM");
|
|
4846
|
-
escalationDone = new Promise((
|
|
5463
|
+
escalationDone = new Promise((resolve3) => {
|
|
4847
5464
|
const graceTimer = setTimeout(() => {
|
|
4848
5465
|
const pid = proc.pid;
|
|
4849
5466
|
if (pid !== undefined && groupStillAlive(pid))
|
|
4850
5467
|
killTree("SIGKILL");
|
|
4851
|
-
|
|
5468
|
+
resolve3();
|
|
4852
5469
|
}, KILL_GRACE_MS);
|
|
4853
5470
|
cancelEscalation = () => {
|
|
4854
5471
|
clearTimeout(graceTimer);
|
|
4855
|
-
|
|
5472
|
+
resolve3();
|
|
4856
5473
|
};
|
|
4857
5474
|
});
|
|
4858
5475
|
}, timeoutMs);
|
|
@@ -4863,11 +5480,11 @@ async function runOne(spec, timeoutMs, callerEnv) {
|
|
|
4863
5480
|
});
|
|
4864
5481
|
proc.stdout.resume();
|
|
4865
5482
|
let spawnError;
|
|
4866
|
-
const exitCode = await new Promise((
|
|
4867
|
-
proc.on("close", (code) =>
|
|
5483
|
+
const exitCode = await new Promise((resolve3) => {
|
|
5484
|
+
proc.on("close", (code) => resolve3(code));
|
|
4868
5485
|
proc.on("error", (err) => {
|
|
4869
5486
|
spawnError = err;
|
|
4870
|
-
|
|
5487
|
+
resolve3(null);
|
|
4871
5488
|
});
|
|
4872
5489
|
});
|
|
4873
5490
|
clearTimeout(timer);
|
|
@@ -4955,7 +5572,7 @@ async function runChecks(specs, opts) {
|
|
|
4955
5572
|
}
|
|
4956
5573
|
|
|
4957
5574
|
// src/roles/plan.ts
|
|
4958
|
-
import { existsSync as existsSync8, readFileSync as
|
|
5575
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9 } from "node:fs";
|
|
4959
5576
|
import { join as join10 } from "node:path";
|
|
4960
5577
|
|
|
4961
5578
|
// ../../packages/sources/src/commands.ts
|
|
@@ -5110,6 +5727,18 @@ var COMMANDS = [
|
|
|
5110
5727
|
],
|
|
5111
5728
|
status: "shipped"
|
|
5112
5729
|
},
|
|
5730
|
+
{
|
|
5731
|
+
name: "pr verify-evidence",
|
|
5732
|
+
description: "Prove a pull request's AEG:EVIDENCE region was machine-generated — regenerate it and compare",
|
|
5733
|
+
flags: [],
|
|
5734
|
+
details: [
|
|
5735
|
+
"Deliberately a command, not a check: `evidence-fresh` runs inside `vinaya check --all`, and `pr report` runs `vinaya check --all --diff-only`, so a check that regenerated the block would run the suite containing itself. That recursion is why `evidence-fresh` verifies Group A only and leaves Group B attested.",
|
|
5736
|
+
"Must be run from the repository ROOT of a CLEAN checkout at the pull request's head, and refuses otherwise. The regeneration inherits the working directory and several gates resolve their scan root from it, so a subdirectory silently narrows what the fresh run inspects. On cleanliness: Group B is regenerated by diff-scoped checks, so uncommitted or untracked files change which files are scanned and can produce a MATCH a clean checkout would not. Ignored paths are deliberately NOT inspected — `git diff` never reports one, so they cannot change the scope, and including them would refuse in every real checkout since `node_modules` is ignored and always present. The head is read from the forge and both a mismatch AND an unreadable head refuse — the verdict is bound to a commit, never merely attested. `BASE_SHA` is refused as well, since it steers the regeneration's merge-base.",
|
|
5737
|
+
"Exits 0 on MATCH; 1 on DIFFERS, HIDDEN, or no block; 2 on a refusal (dirty worktree, wrong head, usage error). HIDDEN means the anchor pair sits inside a collapsed `<details>` block, where `body-bare-digits` blanks every digit and nothing can verify what it claims.",
|
|
5738
|
+
"Comparison is a multiset of repo-relative, control-stripped lines: absolute paths (local AND foreign, so a block generated in CI compares against one generated on a laptop) and line order are ignored — a reorder is not a fabrication. Only the AEG:EVIDENCE region is read, since AEG:TOKENS appends by design. A moved merge-base is reported ALONGSIDE the differing lines, never instead of them."
|
|
5739
|
+
],
|
|
5740
|
+
status: "shipped"
|
|
5741
|
+
},
|
|
5113
5742
|
{
|
|
5114
5743
|
name: "issue create",
|
|
5115
5744
|
description: "Open an issue after full brief-schema validation",
|
|
@@ -5186,6 +5815,19 @@ var COMMANDS = [
|
|
|
5186
5815
|
],
|
|
5187
5816
|
status: "shipped"
|
|
5188
5817
|
},
|
|
5818
|
+
{
|
|
5819
|
+
name: "milestone close",
|
|
5820
|
+
description: "Close a tranche's Milestone after verifying every labeled Issue is actually attached",
|
|
5821
|
+
flags: [
|
|
5822
|
+
{ flag: "--slug", description: "The tranche whose Milestone is being closed" },
|
|
5823
|
+
{ flag: "--validate-only", description: "Run every gate and report PASS without closing the Milestone" },
|
|
5824
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
5825
|
+
],
|
|
5826
|
+
details: [
|
|
5827
|
+
"Resolves the target Milestone the same legacy-or-intent-declared way Issue create auto-attach does, then refuses to close on any mismatch between the label's Issues and the Milestone's natively attached Issues — naming each unattached or foreign Issue and its repair path (`gh issue edit <n> --milestone <title>`, or `vinaya milestone adopt`) — before the PATCH ever reaches the forge."
|
|
5828
|
+
],
|
|
5829
|
+
status: "shipped"
|
|
5830
|
+
},
|
|
5189
5831
|
{
|
|
5190
5832
|
name: "review post",
|
|
5191
5833
|
description: "Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR",
|
|
@@ -5373,7 +6015,7 @@ var COMMANDS = [
|
|
|
5373
6015
|
}
|
|
5374
6016
|
];
|
|
5375
6017
|
// ../../packages/sources/src/doctrine-file-adapter.ts
|
|
5376
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
6018
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync8 } from "node:fs";
|
|
5377
6019
|
import { join as join9 } from "node:path";
|
|
5378
6020
|
|
|
5379
6021
|
// ../../packages/sources/src/file-adapter.ts
|
|
@@ -5384,14 +6026,14 @@ function readMarkdownDir(root, dir) {
|
|
|
5384
6026
|
const dirPath = join9(root, dir);
|
|
5385
6027
|
return readdirSync4(dirPath).filter((f) => f.endsWith(".md")).sort().map((file) => {
|
|
5386
6028
|
const path = join9(dir, file);
|
|
5387
|
-
return { path, content:
|
|
6029
|
+
return { path, content: readFileSync8(join9(root, path), "utf-8") };
|
|
5388
6030
|
});
|
|
5389
6031
|
}
|
|
5390
6032
|
function createFileDoctrineSource(config = {}) {
|
|
5391
6033
|
const root = config.root ?? DEFAULT_GOVERNANCE_ROOT;
|
|
5392
6034
|
return {
|
|
5393
6035
|
async getDoctrine() {
|
|
5394
|
-
const enforcement =
|
|
6036
|
+
const enforcement = readFileSync8(join9(root, "enforcement.md"), "utf-8");
|
|
5395
6037
|
const roles = readMarkdownDir(root, "roles");
|
|
5396
6038
|
const contracts = readMarkdownDir(root, "contracts");
|
|
5397
6039
|
return { enforcement, roles, contracts };
|
|
@@ -5570,7 +6212,7 @@ function readContract(configDir, contractPath) {
|
|
|
5570
6212
|
return { ok: false, errors: [`contract file "${contractPath}" does not exist (resolved to "${absolute}")`] };
|
|
5571
6213
|
}
|
|
5572
6214
|
try {
|
|
5573
|
-
return validateRoleContract(
|
|
6215
|
+
return validateRoleContract(readFileSync9(absolute, "utf-8"));
|
|
5574
6216
|
} catch (err) {
|
|
5575
6217
|
return { ok: false, errors: [`could not read "${contractPath}": ${err.message}`] };
|
|
5576
6218
|
}
|
|
@@ -5603,7 +6245,7 @@ async function buildRolePlan(configDir, configRoles) {
|
|
|
5603
6245
|
// src/commands/check.ts
|
|
5604
6246
|
function git(args) {
|
|
5605
6247
|
try {
|
|
5606
|
-
return
|
|
6248
|
+
return execFileSync7("git", args, { encoding: "utf8" }).trim();
|
|
5607
6249
|
} catch {
|
|
5608
6250
|
return "";
|
|
5609
6251
|
}
|
|
@@ -5841,7 +6483,7 @@ ${renderRolesTable(rolePlan)}
|
|
|
5841
6483
|
}
|
|
5842
6484
|
|
|
5843
6485
|
// src/commands/commit-msg.ts
|
|
5844
|
-
import { readFileSync as
|
|
6486
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
5845
6487
|
function commitMsgCommand(args) {
|
|
5846
6488
|
const [messageFile, source] = args;
|
|
5847
6489
|
if (!messageFile) {
|
|
@@ -5852,7 +6494,7 @@ function commitMsgCommand(args) {
|
|
|
5852
6494
|
}
|
|
5853
6495
|
if (source === "merge")
|
|
5854
6496
|
return;
|
|
5855
|
-
const firstLine2 =
|
|
6497
|
+
const firstLine2 = readFileSync10(messageFile, "utf-8").split(`
|
|
5856
6498
|
`, 1)[0] ?? "";
|
|
5857
6499
|
if (COMMIT_TYPE_STYLE.test(firstLine2))
|
|
5858
6500
|
return;
|
|
@@ -5864,14 +6506,14 @@ function commitMsgCommand(args) {
|
|
|
5864
6506
|
}
|
|
5865
6507
|
|
|
5866
6508
|
// src/commands/demo.ts
|
|
5867
|
-
import { execFileSync as
|
|
5868
|
-
import { existsSync as existsSync9, readFileSync as
|
|
6509
|
+
import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
|
|
6510
|
+
import { existsSync as existsSync9, readFileSync as readFileSync11, rmSync as rmSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5869
6511
|
import { isAbsolute as isAbsolute2, join as join11 } from "node:path";
|
|
5870
6512
|
var DEMO_BRANCH_PREFIX = "vinaya/demo-break-";
|
|
5871
6513
|
var FIXTURE_PATH = ".vinaya-demo-brief.md";
|
|
5872
6514
|
var STATE_FILENAME = "vinaya-demo-state.json";
|
|
5873
6515
|
function gitCapture(repoRoot, args) {
|
|
5874
|
-
return
|
|
6516
|
+
return execFileSync8("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
5875
6517
|
}
|
|
5876
6518
|
function currentBranchName(repoRoot) {
|
|
5877
6519
|
try {
|
|
@@ -5882,7 +6524,7 @@ function currentBranchName(repoRoot) {
|
|
|
5882
6524
|
}
|
|
5883
6525
|
function branchExists(repoRoot, name) {
|
|
5884
6526
|
try {
|
|
5885
|
-
|
|
6527
|
+
execFileSync8("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], {
|
|
5886
6528
|
cwd: repoRoot,
|
|
5887
6529
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5888
6530
|
});
|
|
@@ -5910,7 +6552,7 @@ function recoverFromCrash(repoRoot, statePath) {
|
|
|
5910
6552
|
let state = null;
|
|
5911
6553
|
if (existsSync9(statePath)) {
|
|
5912
6554
|
try {
|
|
5913
|
-
state = JSON.parse(
|
|
6555
|
+
state = JSON.parse(readFileSync11(statePath, "utf-8"));
|
|
5914
6556
|
} catch {
|
|
5915
6557
|
state = null;
|
|
5916
6558
|
}
|
|
@@ -6084,12 +6726,12 @@ async function demoBreakCommand(args) {
|
|
|
6084
6726
|
}
|
|
6085
6727
|
|
|
6086
6728
|
// src/commands/doctor.ts
|
|
6087
|
-
import { execFileSync as
|
|
6088
|
-
import { existsSync as
|
|
6089
|
-
import { delimiter, join as
|
|
6729
|
+
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
6730
|
+
import { existsSync as existsSync12, readdirSync as readdirSync6, readFileSync as readFileSync15, statSync as statSync2 } from "node:fs";
|
|
6731
|
+
import { delimiter, join as join14 } from "node:path";
|
|
6090
6732
|
|
|
6091
6733
|
// src/lib/self-host.ts
|
|
6092
|
-
import { readdirSync as readdirSync5, readFileSync as
|
|
6734
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync12, realpathSync as realpathSync2 } from "node:fs";
|
|
6093
6735
|
import { join as join12, sep as sep3 } from "node:path";
|
|
6094
6736
|
var VINAYA_PACKAGE_NAME = "@attalabs/vinaya";
|
|
6095
6737
|
var DEFAULT_BIN = "dist/index.js";
|
|
@@ -6108,7 +6750,7 @@ function isSafeRelPath(p) {
|
|
|
6108
6750
|
}
|
|
6109
6751
|
function readPackageJson(path) {
|
|
6110
6752
|
try {
|
|
6111
|
-
const parsed = JSON.parse(
|
|
6753
|
+
const parsed = JSON.parse(readFileSync12(path, "utf-8"));
|
|
6112
6754
|
return parsed && typeof parsed === "object" ? parsed : null;
|
|
6113
6755
|
} catch {
|
|
6114
6756
|
return null;
|
|
@@ -6162,8 +6804,8 @@ function expandPattern(repoRoot, pattern) {
|
|
|
6162
6804
|
}
|
|
6163
6805
|
function resolvesInsideRepo(repoRoot, rel) {
|
|
6164
6806
|
try {
|
|
6165
|
-
const root =
|
|
6166
|
-
const target =
|
|
6807
|
+
const root = realpathSync2(repoRoot);
|
|
6808
|
+
const target = realpathSync2(join12(repoRoot, rel));
|
|
6167
6809
|
return target === root || target.startsWith(`${root}${sep3}`);
|
|
6168
6810
|
} catch {
|
|
6169
6811
|
return false;
|
|
@@ -6203,7 +6845,7 @@ function detectVendoredVinaya(repoRoot) {
|
|
|
6203
6845
|
}
|
|
6204
6846
|
|
|
6205
6847
|
// src/lib/env-lint.ts
|
|
6206
|
-
import { existsSync as existsSync10, readFileSync as
|
|
6848
|
+
import { existsSync as existsSync10, readFileSync as readFileSync13 } from "node:fs";
|
|
6207
6849
|
var ENV_READ_PATTERN = /\b(?:process\.env|Bun\.env|Deno\.env)\b/;
|
|
6208
6850
|
function checksMissingEnvDeclaration(specs) {
|
|
6209
6851
|
const names = [];
|
|
@@ -6214,7 +6856,7 @@ function checksMissingEnvDeclaration(specs) {
|
|
|
6214
6856
|
continue;
|
|
6215
6857
|
let source;
|
|
6216
6858
|
try {
|
|
6217
|
-
source =
|
|
6859
|
+
source = readFileSync13(spec.run, "utf8");
|
|
6218
6860
|
} catch {
|
|
6219
6861
|
continue;
|
|
6220
6862
|
}
|
|
@@ -6227,49 +6869,175 @@ function envDeclarationWarning(name) {
|
|
|
6227
6869
|
return `check "${name}" reads environment variables directly but declares no \`env\` — it will lose environment access at the next minor. Declare \`env\` for it (CheckSpec['env'] in src/checks/contract.ts, or vinaya.config.json's checks.<name>.env).`;
|
|
6228
6870
|
}
|
|
6229
6871
|
|
|
6230
|
-
// src/
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
function
|
|
6236
|
-
return {
|
|
6237
|
-
detectRepo: detectGitRepo,
|
|
6238
|
-
ghAuthStatus,
|
|
6239
|
-
branchProtectionConfigured,
|
|
6240
|
-
hookDirFor: resolveHookDir,
|
|
6241
|
-
readHooksPath: readCoreHooksPath,
|
|
6242
|
-
nodeVersion: () => process.version,
|
|
6243
|
-
bunVersion: () => typeof Bun === "undefined" ? null : Bun.version,
|
|
6244
|
-
packageVersion: readVersion,
|
|
6245
|
-
meteringCapability: () => resolveMeteringCapability({
|
|
6246
|
-
env: process.env,
|
|
6247
|
-
cwd: process.cwd(),
|
|
6248
|
-
exists: existsSync11,
|
|
6249
|
-
readFile: (path) => readFileSync13(path, "utf8")
|
|
6250
|
-
})
|
|
6251
|
-
};
|
|
6872
|
+
// src/lib/registry-write.ts
|
|
6873
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync4 } from "node:fs";
|
|
6874
|
+
import { dirname as dirname6, join as join13 } from "node:path";
|
|
6875
|
+
var PROJECTS_REGISTRY_PATH = ".vinaya/projects.md";
|
|
6876
|
+
var CONFIG_FILE_PATH = "vinaya.config.json";
|
|
6877
|
+
function rowLine(name, path, specsPath) {
|
|
6878
|
+
return `| ${name} | \`${path}\` | \`${specsPath}\` | (state tracked globally) |`;
|
|
6252
6879
|
}
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6880
|
+
function freshProjectsRegistry(name, path, specsPath) {
|
|
6881
|
+
return `---
|
|
6882
|
+
sidebar_title: Projects
|
|
6883
|
+
---
|
|
6884
|
+
# Projects in this repo
|
|
6885
|
+
|
|
6886
|
+
**The project registry.** Declares the projects in this repo and where each
|
|
6887
|
+
one's specs and per-project state live. The \`Project\` field on a task (a
|
|
6888
|
+
forge Issue) resolves against this file.
|
|
6889
|
+
|
|
6890
|
+
**Presence of this file means this is a multi-project repo** — the
|
|
6891
|
+
\`Project\` field is required on task Issues/PRs that touch a registered
|
|
6892
|
+
project's path. A project is a \`(name, path)\` pair you declared — nothing
|
|
6893
|
+
is derived from the folder tree.
|
|
6894
|
+
|
|
6895
|
+
## Registry
|
|
6896
|
+
|
|
6897
|
+
| Project | Path | Specs | Per-project state |
|
|
6898
|
+
|---------|------|-------|---------------------|
|
|
6899
|
+
${rowLine(name, path, specsPath)}
|
|
6900
|
+
`;
|
|
6901
|
+
}
|
|
6902
|
+
var REGISTRY_HEADING_RE = /^##\s+Registry\b/i;
|
|
6903
|
+
function appendRegistryRow(content, name, path, specsPath) {
|
|
6904
|
+
const lines = content.split(/\r?\n/);
|
|
6905
|
+
const headingIdx = lines.findIndex((l) => REGISTRY_HEADING_RE.test(l));
|
|
6906
|
+
const newRow = rowLine(name, path, specsPath);
|
|
6907
|
+
if (headingIdx === -1) {
|
|
6908
|
+
const sep4 = content.endsWith(`
|
|
6909
|
+
`) ? "" : `
|
|
6910
|
+
`;
|
|
6911
|
+
return `${content}${sep4}
|
|
6912
|
+
## Registry
|
|
6913
|
+
|
|
6914
|
+
| Project | Path | Specs | Per-project state |
|
|
6915
|
+
|---------|------|-------|---------------------|
|
|
6916
|
+
${newRow}
|
|
6917
|
+
`;
|
|
6918
|
+
}
|
|
6919
|
+
let i = headingIdx + 1;
|
|
6920
|
+
for (;i < lines.length; i++) {
|
|
6921
|
+
if ((lines[i] ?? "").trim().startsWith("|"))
|
|
6922
|
+
break;
|
|
6923
|
+
}
|
|
6924
|
+
if (i >= lines.length) {
|
|
6925
|
+
lines.splice(headingIdx + 1, 0, "", "| Project | Path | Specs | Per-project state |", "|---------|------|-------|---------------------|", newRow);
|
|
6926
|
+
return lines.join(`
|
|
6927
|
+
`);
|
|
6928
|
+
}
|
|
6929
|
+
let last = i + 1;
|
|
6930
|
+
for (let j = i + 2;j < lines.length; j++) {
|
|
6931
|
+
if (!(lines[j] ?? "").trim().startsWith("|"))
|
|
6932
|
+
break;
|
|
6933
|
+
last = j;
|
|
6934
|
+
}
|
|
6935
|
+
lines.splice(last + 1, 0, newRow);
|
|
6936
|
+
return lines.join(`
|
|
6937
|
+
`);
|
|
6938
|
+
}
|
|
6939
|
+
function planRegistryRow(repoRoot, name, path, specsPath) {
|
|
6940
|
+
const abs2 = join13(repoRoot, PROJECTS_REGISTRY_PATH);
|
|
6941
|
+
const line = rowLine(name, path, specsPath);
|
|
6942
|
+
if (!existsSync11(abs2)) {
|
|
6943
|
+
return { action: "create-host", rowLine: line, path: PROJECTS_REGISTRY_PATH };
|
|
6944
|
+
}
|
|
6945
|
+
const existing = readFileSync14(abs2, "utf-8");
|
|
6946
|
+
const already = parseRegistry(existing).some((p) => p.name === name);
|
|
6947
|
+
return { action: already ? "skip-present" : "append-row", rowLine: line, path: PROJECTS_REGISTRY_PATH };
|
|
6948
|
+
}
|
|
6949
|
+
function applyRegistryRow(repoRoot, plan, name, path, specsPath) {
|
|
6950
|
+
const abs2 = join13(repoRoot, PROJECTS_REGISTRY_PATH);
|
|
6951
|
+
if (plan.action === "create-host") {
|
|
6952
|
+
mkdirSync3(dirname6(abs2), { recursive: true });
|
|
6953
|
+
writeFileSync4(abs2, freshProjectsRegistry(name, path, specsPath), "utf-8");
|
|
6954
|
+
} else if (plan.action === "append-row") {
|
|
6955
|
+
const existing = readFileSync14(abs2, "utf-8");
|
|
6956
|
+
writeFileSync4(abs2, appendRegistryRow(existing, name, path, specsPath), "utf-8");
|
|
6957
|
+
}
|
|
6958
|
+
}
|
|
6959
|
+
function renderRegistryRowDiffLine(plan) {
|
|
6960
|
+
if (plan.action === "skip-present")
|
|
6961
|
+
return ` = keep ${plan.path} (project already registered)`;
|
|
6962
|
+
if (plan.action === "append-row")
|
|
6963
|
+
return ` ~ append row to ${plan.path}
|
|
6964
|
+
${plan.rowLine}`;
|
|
6965
|
+
return ` + create ${plan.path} (with registry row)
|
|
6966
|
+
${plan.rowLine}`;
|
|
6967
|
+
}
|
|
6968
|
+
function readProjectEntries(repoRoot) {
|
|
6969
|
+
const abs2 = join13(repoRoot, CONFIG_FILE_PATH);
|
|
6970
|
+
if (!existsSync11(abs2))
|
|
6971
|
+
return [];
|
|
6972
|
+
try {
|
|
6973
|
+
return VinayaConfigSchema.parse(JSON.parse(readFileSync14(abs2, "utf-8"))).projects ?? [];
|
|
6974
|
+
} catch {
|
|
6975
|
+
return [];
|
|
6976
|
+
}
|
|
6977
|
+
}
|
|
6978
|
+
function planConfigProjectEntry(repoRoot, entry2) {
|
|
6979
|
+
const already = readProjectEntries(repoRoot).some((p) => p.name === entry2.name);
|
|
6980
|
+
return { action: already ? "skip-present" : "add-entry", entry: entry2 };
|
|
6981
|
+
}
|
|
6982
|
+
function applyConfigProjectEntry(repoRoot, plan) {
|
|
6983
|
+
if (plan.action === "skip-present")
|
|
6984
|
+
return;
|
|
6985
|
+
const abs2 = join13(repoRoot, CONFIG_FILE_PATH);
|
|
6986
|
+
const seed = JSON.parse(readFileSync14(abs2, "utf-8"));
|
|
6987
|
+
const projects = Array.isArray(seed.projects) ? seed.projects : [];
|
|
6988
|
+
writeFileSync4(abs2, `${JSON.stringify({ ...seed, projects: [...projects, plan.entry] }, null, 2)}
|
|
6989
|
+
`, "utf-8");
|
|
6990
|
+
}
|
|
6991
|
+
function renderConfigProjectEntryDiffLine(plan) {
|
|
6992
|
+
if (plan.action === "skip-present")
|
|
6993
|
+
return ` = keep ${CONFIG_FILE_PATH} (project already declared under "projects")`;
|
|
6994
|
+
const bits = [`"name": ${JSON.stringify(plan.entry.name)}`];
|
|
6995
|
+
if (plan.entry.path)
|
|
6996
|
+
bits.push(`"path": ${JSON.stringify(plan.entry.path)}`);
|
|
6997
|
+
if (plan.entry.description)
|
|
6998
|
+
bits.push(`"description": ${JSON.stringify(plan.entry.description)}`);
|
|
6999
|
+
return ` ~ append entry to ${CONFIG_FILE_PATH}'s "projects"
|
|
7000
|
+
{ ${bits.join(", ")} }`;
|
|
7001
|
+
}
|
|
7002
|
+
|
|
7003
|
+
// src/commands/doctor.ts
|
|
7004
|
+
function readVersion() {
|
|
7005
|
+
const pkg = JSON.parse(readFileSync15(join14(packageRoot(import.meta.url), "package.json"), "utf-8"));
|
|
7006
|
+
return pkg.version;
|
|
7007
|
+
}
|
|
7008
|
+
function realDeps4() {
|
|
7009
|
+
return {
|
|
7010
|
+
detectRepo: detectGitRepo,
|
|
7011
|
+
ghAuthStatus,
|
|
7012
|
+
branchProtectionConfigured,
|
|
7013
|
+
hookDirFor: resolveHookDir,
|
|
7014
|
+
readHooksPath: readCoreHooksPath,
|
|
7015
|
+
nodeVersion: () => process.version,
|
|
7016
|
+
bunVersion: () => typeof Bun === "undefined" ? null : Bun.version,
|
|
7017
|
+
packageVersion: readVersion,
|
|
7018
|
+
meteringCapability: () => resolveMeteringCapability(hardenedMeteringDeps())
|
|
7019
|
+
};
|
|
7020
|
+
}
|
|
7021
|
+
var ok = (check, message) => ({ check, severity: "ok", message });
|
|
7022
|
+
var info = (check, message) => ({ check, severity: "info", message });
|
|
7023
|
+
var warn = (check, message) => ({ check, severity: "warn", message });
|
|
7024
|
+
var error = (check, message) => ({ check, severity: "error", message });
|
|
7025
|
+
function readConfig(repoRoot) {
|
|
7026
|
+
const p = join14(repoRoot, CONFIG_PATH);
|
|
7027
|
+
if (!existsSync12(p))
|
|
7028
|
+
return { kind: "missing" };
|
|
7029
|
+
let raw;
|
|
7030
|
+
try {
|
|
7031
|
+
raw = JSON.parse(readFileSync15(p, "utf-8"));
|
|
7032
|
+
} catch (err) {
|
|
7033
|
+
return { kind: "invalid", error: `invalid JSON: ${err.message}` };
|
|
7034
|
+
}
|
|
7035
|
+
const parsed = VinayaConfigSchema.safeParse(raw);
|
|
7036
|
+
if (!parsed.success) {
|
|
7037
|
+
return {
|
|
7038
|
+
kind: "invalid",
|
|
7039
|
+
error: parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")
|
|
7040
|
+
};
|
|
6273
7041
|
}
|
|
6274
7042
|
return { kind: "ok", config: parsed.data };
|
|
6275
7043
|
}
|
|
@@ -6299,14 +7067,14 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
|
|
|
6299
7067
|
for (const op of buildInitOps(ctx)) {
|
|
6300
7068
|
if (op.kind === "create-file") {
|
|
6301
7069
|
const check = labelForPath(op.path);
|
|
6302
|
-
const abs2 =
|
|
6303
|
-
const exists =
|
|
7070
|
+
const abs2 = join14(repoRoot, op.path);
|
|
7071
|
+
const exists = existsSync12(abs2);
|
|
6304
7072
|
const owned = ownedFiles.has(op.path) || isDefaultedAgentVendorPath(op.path, manifest);
|
|
6305
7073
|
if (!exists) {
|
|
6306
7074
|
findings.push(owned ? error(check, `${op.path} is recorded as vinaya-managed but missing on disk — run \`vinaya upgrade\`.`) : error(check, `${op.path} is not installed — run \`vinaya init\`.`));
|
|
6307
7075
|
continue;
|
|
6308
7076
|
}
|
|
6309
|
-
const content =
|
|
7077
|
+
const content = readFileSync15(abs2, "utf-8");
|
|
6310
7078
|
if (!owned) {
|
|
6311
7079
|
findings.push(content === op.content ? warn(check, `${op.path} has vinaya's own content but isn't recorded in the manifest.`) : info(check, `${op.path} exists but is foreign content — not vinaya-managed, left untouched.`));
|
|
6312
7080
|
continue;
|
|
@@ -6325,11 +7093,11 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
|
|
|
6325
7093
|
const check = "hooks";
|
|
6326
7094
|
const abs2 = resolveManagedBlockPath(repoRoot, op.path);
|
|
6327
7095
|
const owned = ownedBlocks.has(blockKey(op.path, op.marker));
|
|
6328
|
-
if (!
|
|
7096
|
+
if (!existsSync12(abs2)) {
|
|
6329
7097
|
findings.push(owned ? error(check, `${op.path} is missing — likely a fresh clone (raw git hooks aren't tracked by git). Run \`vinaya upgrade\` to restore it.`) : info(check, `${op.path} is not installed.`));
|
|
6330
7098
|
continue;
|
|
6331
7099
|
}
|
|
6332
|
-
const content =
|
|
7100
|
+
const content = readFileSync15(abs2, "utf-8");
|
|
6333
7101
|
const { begin, end } = markerLines(op.marker, op.comment);
|
|
6334
7102
|
const hasMarkers = content.includes(begin) && content.includes(end);
|
|
6335
7103
|
if (!hasMarkers) {
|
|
@@ -6377,22 +7145,22 @@ async function diagnoseHookRouting(repoRoot, hookDir, readHooksPath) {
|
|
|
6377
7145
|
function diagnoseCustomChecks(repoRoot, config) {
|
|
6378
7146
|
const findings = [];
|
|
6379
7147
|
for (const [name, entry2] of Object.entries(config.checks ?? {})) {
|
|
6380
|
-
const scriptAbs =
|
|
6381
|
-
findings.push(
|
|
7148
|
+
const scriptAbs = join14(repoRoot, entry2.run);
|
|
7149
|
+
findings.push(existsSync12(scriptAbs) ? ok("checks", `custom check '${name}' → ${entry2.run}`) : error("checks", `custom check '${name}' points at a missing script: ${entry2.run}`));
|
|
6382
7150
|
}
|
|
6383
7151
|
return findings;
|
|
6384
7152
|
}
|
|
6385
7153
|
function listTrackedFiles(repoRoot) {
|
|
6386
7154
|
try {
|
|
6387
|
-
return
|
|
7155
|
+
return execFileSync9("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).split(`
|
|
6388
7156
|
`).map((s) => s.trim()).filter(Boolean);
|
|
6389
7157
|
} catch {
|
|
6390
7158
|
return [];
|
|
6391
7159
|
}
|
|
6392
7160
|
}
|
|
6393
7161
|
function diagnoseDocOwnersHealth(repoRoot) {
|
|
6394
|
-
const path =
|
|
6395
|
-
const content =
|
|
7162
|
+
const path = join14(repoRoot, DOC_OWNERS_PATH);
|
|
7163
|
+
const content = existsSync12(path) ? readFileSync15(path, "utf-8") : null;
|
|
6396
7164
|
const state = classifyDocOwnersManifest(content);
|
|
6397
7165
|
if (state === "absent" || state === "empty")
|
|
6398
7166
|
return [];
|
|
@@ -6409,8 +7177,8 @@ function diagnoseDocOwnersHealth(repoRoot) {
|
|
|
6409
7177
|
findings.push(warn("doc-owners", `${DOC_OWNERS_PATH}:${b.lineNum} binds glob '${b.glob}', which matches none of the ${codeFiles.length} ` + "tracked code file(s) in this repo — the code it names may have been deleted, renamed, or never " + "existed. Repoint the binding to the code's new location, or remove it."));
|
|
6410
7178
|
}
|
|
6411
7179
|
if (!isUrlPointer(b.pointer)) {
|
|
6412
|
-
const pointerPath =
|
|
6413
|
-
if (!
|
|
7180
|
+
const pointerPath = join14(repoRoot, pointerToPath(b.pointer));
|
|
7181
|
+
if (!existsSync12(pointerPath)) {
|
|
6414
7182
|
flagged = true;
|
|
6415
7183
|
findings.push(warn("doc-owners", `${DOC_OWNERS_PATH}:${b.lineNum} points to ${b.pointer}, which does not exist on disk. Repoint the ` + "binding, or add the missing doc."));
|
|
6416
7184
|
}
|
|
@@ -6425,7 +7193,7 @@ var LEGACY_AEG_PACKAGES_PATH = ".aeg/packages";
|
|
|
6425
7193
|
function readWorkspacesForDoctor(repoRoot) {
|
|
6426
7194
|
const fromPackageJson = () => {
|
|
6427
7195
|
try {
|
|
6428
|
-
const pkg = JSON.parse(
|
|
7196
|
+
const pkg = JSON.parse(readFileSync15(join14(repoRoot, "package.json"), "utf-8"));
|
|
6429
7197
|
return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
|
|
6430
7198
|
} catch {
|
|
6431
7199
|
return [];
|
|
@@ -6433,7 +7201,7 @@ function readWorkspacesForDoctor(repoRoot) {
|
|
|
6433
7201
|
};
|
|
6434
7202
|
const fromPnpmWorkspaceYaml = () => {
|
|
6435
7203
|
try {
|
|
6436
|
-
return parsePnpmWorkspaceYaml(
|
|
7204
|
+
return parsePnpmWorkspaceYaml(readFileSync15(join14(repoRoot, "pnpm-workspace.yaml"), "utf-8"));
|
|
6437
7205
|
} catch {
|
|
6438
7206
|
return [];
|
|
6439
7207
|
}
|
|
@@ -6442,23 +7210,23 @@ function readWorkspacesForDoctor(repoRoot) {
|
|
|
6442
7210
|
}
|
|
6443
7211
|
function listChildDirsForDoctor(dir, repoRoot) {
|
|
6444
7212
|
try {
|
|
6445
|
-
return readdirSync6(
|
|
7213
|
+
return readdirSync6(join14(repoRoot, dir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
6446
7214
|
} catch {
|
|
6447
7215
|
return [];
|
|
6448
7216
|
}
|
|
6449
7217
|
}
|
|
6450
7218
|
function diagnoseBlastRadiusDeprecation(repoRoot, config) {
|
|
6451
7219
|
const derived = deriveWorkspacePackageDomains(readWorkspacesForDoctor(repoRoot), (dir) => listChildDirsForDoctor(dir, repoRoot));
|
|
6452
|
-
const defaults = deriveBuiltinCrossCuttingDefaults((p) =>
|
|
7220
|
+
const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync12(join14(repoRoot, p)));
|
|
6453
7221
|
const covered = new Set([...derived, ...defaults]);
|
|
6454
7222
|
const configExtra = new Set(config?.blastRadius?.extraDomains ?? []);
|
|
6455
|
-
const legacyPath =
|
|
6456
|
-
if (!
|
|
7223
|
+
const legacyPath = join14(repoRoot, LEGACY_AEG_PACKAGES_PATH);
|
|
7224
|
+
if (!existsSync12(legacyPath)) {
|
|
6457
7225
|
return [
|
|
6458
7226
|
info("blast-radius", `checkBlastRadiusScope is active via live derivation (${derived.length} packages/* domain(s)) + built-in defaults (${defaults.length} present) — no ${LEGACY_AEG_PACKAGES_PATH}, and the check is not dormant.`)
|
|
6459
7227
|
];
|
|
6460
7228
|
}
|
|
6461
|
-
const legacyEntries =
|
|
7229
|
+
const legacyEntries = readFileSync15(legacyPath, "utf-8").split(`
|
|
6462
7230
|
`).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
6463
7231
|
const uncovered = legacyEntries.filter((e) => !covered.has(e) && !configExtra.has(e));
|
|
6464
7232
|
const migrationNote = uncovered.length === 0 ? "every entry is already covered by live derivation, the built-in defaults, or vinaya.config.json blastRadius.extraDomains — the file can be deleted." : `migrate ${uncovered.length} entr${uncovered.length === 1 ? "y" : "ies"} not yet covered before deleting it: ${uncovered.join(", ")}.`;
|
|
@@ -6493,7 +7261,7 @@ function diagnoseBriefSchemaDrift(config) {
|
|
|
6493
7261
|
}
|
|
6494
7262
|
function diagnoseEnvDeclarations(repoRoot, config) {
|
|
6495
7263
|
const resolved = resolveChecks(coreCheckRegistry(), config?.checks).resolved;
|
|
6496
|
-
const specs = resolved.map((entry2) => entry2.source === "config" ? { ...entry2.spec, run:
|
|
7264
|
+
const specs = resolved.map((entry2) => entry2.source === "config" ? { ...entry2.spec, run: join14(repoRoot, entry2.spec.run) } : entry2.spec);
|
|
6497
7265
|
const missing = checksMissingEnvDeclaration(specs);
|
|
6498
7266
|
const findings = missing.map((name) => info("env", envDeclarationWarning(name)));
|
|
6499
7267
|
for (const message of lintEnvDeclarations(config?.checks)) {
|
|
@@ -6514,11 +7282,11 @@ function diagnoseCheckClassification(config) {
|
|
|
6514
7282
|
return findings;
|
|
6515
7283
|
}
|
|
6516
7284
|
function diagnoseGlobalConfigChecks() {
|
|
6517
|
-
if (!
|
|
7285
|
+
if (!existsSync12(GLOBAL_CONFIG_PATH))
|
|
6518
7286
|
return [];
|
|
6519
7287
|
let raw;
|
|
6520
7288
|
try {
|
|
6521
|
-
raw = JSON.parse(
|
|
7289
|
+
raw = JSON.parse(readFileSync15(GLOBAL_CONFIG_PATH, "utf-8"));
|
|
6522
7290
|
} catch (err) {
|
|
6523
7291
|
return [info("checks", `${GLOBAL_CONFIG_PATH} is invalid JSON — ${err.message}`)];
|
|
6524
7292
|
}
|
|
@@ -6567,13 +7335,13 @@ function coversWorkflowsDir(pattern) {
|
|
|
6567
7335
|
return bare === ".github/workflows" || bare === "/.github/workflows";
|
|
6568
7336
|
}
|
|
6569
7337
|
function diagnoseCodeowners(repoRoot) {
|
|
6570
|
-
const path =
|
|
6571
|
-
if (!
|
|
7338
|
+
const path = join14(repoRoot, ".github", "CODEOWNERS");
|
|
7339
|
+
if (!existsSync12(path)) {
|
|
6572
7340
|
return info("codeowners", "no .github/CODEOWNERS — vinaya's workflow files have no required-review protection; see `vinaya init`'s printed recommendation.");
|
|
6573
7341
|
}
|
|
6574
7342
|
let body;
|
|
6575
7343
|
try {
|
|
6576
|
-
body =
|
|
7344
|
+
body = readFileSync15(path, "utf-8");
|
|
6577
7345
|
} catch (err) {
|
|
6578
7346
|
return warn("codeowners", `.github/CODEOWNERS exists but could not be read: ${err.message}`);
|
|
6579
7347
|
}
|
|
@@ -6586,7 +7354,7 @@ function diagnoseVinayaOnPath(agents) {
|
|
|
6586
7354
|
return [];
|
|
6587
7355
|
const names = process.platform === "win32" ? ["vinaya.cmd", "vinaya.exe", "vinaya.bat"] : ["vinaya"];
|
|
6588
7356
|
const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
6589
|
-
const onPath = dirs.some((dir) => names.some((name) =>
|
|
7357
|
+
const onPath = dirs.some((dir) => names.some((name) => existsSync12(join14(dir, name))));
|
|
6590
7358
|
return [
|
|
6591
7359
|
onPath ? ok("vinaya-on-path", "`vinaya` resolves on PATH — agent-native commands will work.") : info("vinaya-on-path", "`vinaya` is not resolvable on PATH — `/vinaya <role>` and the .agents/skills/vinaya-*/SKILL.md files " + 'installed here will fail "command not found" on first use. Run `npm install -g @attalabs/vinaya`.')
|
|
6592
7360
|
];
|
|
@@ -6597,21 +7365,38 @@ function diagnosePrincipals(config) {
|
|
|
6597
7365
|
}
|
|
6598
7366
|
return info("principals", 'vinaya.config.json has no "principals" — review-gate and the waiver-label actor check both fall back to a ' + "hardcoded placeholder allowlist that will not include anyone on this repo. Every reviewer's verdict is " + 'silently ignored (DANGLING) until you add `"principals": ["<your-github-login>", ...]` to vinaya.config.json.');
|
|
6599
7367
|
}
|
|
7368
|
+
function diagnoseProjectsCoherence(repoRoot, config) {
|
|
7369
|
+
const registryAbs = join14(repoRoot, PROJECTS_REGISTRY_PATH);
|
|
7370
|
+
const registryNames = existsSync12(registryAbs) ? new Set(parseRegistry(readFileSync15(registryAbs, "utf-8")).map((p) => p.name)) : new Set;
|
|
7371
|
+
const configNames = new Set((config?.projects ?? []).map((p) => p.name));
|
|
7372
|
+
if (registryNames.size === 0 && configNames.size === 0)
|
|
7373
|
+
return [];
|
|
7374
|
+
const registryOnly = [...registryNames].filter((n) => !configNames.has(n)).sort();
|
|
7375
|
+
const configOnly = [...configNames].filter((n) => !registryNames.has(n)).sort();
|
|
7376
|
+
const findings = [];
|
|
7377
|
+
if (registryOnly.length > 0) {
|
|
7378
|
+
findings.push(info("projects", `${PROJECTS_REGISTRY_PATH} declares ${registryOnly.join(", ")} with no matching vinaya.config.json "projects" entry.`));
|
|
7379
|
+
}
|
|
7380
|
+
if (configOnly.length > 0) {
|
|
7381
|
+
findings.push(info("projects", `vinaya.config.json "projects" declares ${configOnly.join(", ")} with no matching ${PROJECTS_REGISTRY_PATH} row.`));
|
|
7382
|
+
}
|
|
7383
|
+
return findings;
|
|
7384
|
+
}
|
|
6600
7385
|
var TEST_INVOCATION_SUBSTRINGS = ["npm test", "npm run test", "bun test", "bunx turbo test"];
|
|
6601
7386
|
function diagnoseTestCi(repoRoot) {
|
|
6602
7387
|
let pkg;
|
|
6603
7388
|
try {
|
|
6604
|
-
pkg = JSON.parse(
|
|
7389
|
+
pkg = JSON.parse(readFileSync15(join14(repoRoot, "package.json"), "utf-8"));
|
|
6605
7390
|
} catch {
|
|
6606
7391
|
return [];
|
|
6607
7392
|
}
|
|
6608
7393
|
const testScript = pkg?.scripts?.test;
|
|
6609
7394
|
if (typeof testScript !== "string" || testScript.trim() === "")
|
|
6610
7395
|
return [];
|
|
6611
|
-
const workflowsDir =
|
|
6612
|
-
const files =
|
|
7396
|
+
const workflowsDir = join14(repoRoot, ".github/workflows");
|
|
7397
|
+
const files = existsSync12(workflowsDir) ? readdirSync6(workflowsDir).filter((name) => statSync2(join14(workflowsDir, name)).isFile()) : [];
|
|
6613
7398
|
const invoked = files.some((name) => {
|
|
6614
|
-
const content =
|
|
7399
|
+
const content = readFileSync15(join14(workflowsDir, name), "utf-8");
|
|
6615
7400
|
return TEST_INVOCATION_SUBSTRINGS.some((s) => content.includes(s));
|
|
6616
7401
|
});
|
|
6617
7402
|
if (invoked)
|
|
@@ -6678,6 +7463,7 @@ async function runDoctor(args, deps) {
|
|
|
6678
7463
|
findings.push(diagnosePrincipals(configRead.config));
|
|
6679
7464
|
findings.push(...diagnoseVinayaOnPath(ctx.agents));
|
|
6680
7465
|
}
|
|
7466
|
+
findings.push(...diagnoseProjectsCoherence(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
|
|
6681
7467
|
findings.push(...diagnoseBlastRadiusDeprecation(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
|
|
6682
7468
|
findings.push(...diagnoseBriefSchemaDrift(configRead.kind === "ok" ? configRead.config : null));
|
|
6683
7469
|
findings.push(...diagnoseEnvDeclarations(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
|
|
@@ -6697,13 +7483,13 @@ async function runDoctor(args, deps) {
|
|
|
6697
7483
|
return healthy ? 0 : 1;
|
|
6698
7484
|
}
|
|
6699
7485
|
async function doctorCommand(args) {
|
|
6700
|
-
process.exit(await runDoctor(args,
|
|
7486
|
+
process.exit(await runDoctor(args, realDeps4()));
|
|
6701
7487
|
}
|
|
6702
7488
|
|
|
6703
7489
|
// src/commands/eject.ts
|
|
6704
|
-
import { existsSync as
|
|
6705
|
-
import { join as
|
|
6706
|
-
function
|
|
7490
|
+
import { existsSync as existsSync13, readFileSync as readFileSync16 } from "node:fs";
|
|
7491
|
+
import { join as join15 } from "node:path";
|
|
7492
|
+
function realDeps5() {
|
|
6707
7493
|
return {
|
|
6708
7494
|
detectRepo: detectGitRepo,
|
|
6709
7495
|
readHooksPath: readCoreHooksPath,
|
|
@@ -6719,12 +7505,12 @@ function parse(args) {
|
|
|
6719
7505
|
return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
|
|
6720
7506
|
}
|
|
6721
7507
|
function readManifest(repoRoot) {
|
|
6722
|
-
const p =
|
|
6723
|
-
if (!
|
|
7508
|
+
const p = join15(repoRoot, CONFIG_PATH);
|
|
7509
|
+
if (!existsSync13(p))
|
|
6724
7510
|
return { kind: "none" };
|
|
6725
7511
|
let raw;
|
|
6726
7512
|
try {
|
|
6727
|
-
raw = JSON.parse(
|
|
7513
|
+
raw = JSON.parse(readFileSync16(p, "utf-8"));
|
|
6728
7514
|
} catch (err) {
|
|
6729
7515
|
return { kind: "orphan", reason: `vinaya.config.json is not valid JSON: ${err.message}` };
|
|
6730
7516
|
}
|
|
@@ -6809,112 +7595,14 @@ Vinaya ejected.
|
|
|
6809
7595
|
return 0;
|
|
6810
7596
|
}
|
|
6811
7597
|
async function ejectCommand(args) {
|
|
6812
|
-
const code = await runEject(args,
|
|
7598
|
+
const code = await runEject(args, realDeps5());
|
|
6813
7599
|
process.exit(code);
|
|
6814
7600
|
}
|
|
6815
7601
|
|
|
6816
7602
|
// src/commands/init.ts
|
|
6817
|
-
import { existsSync as existsSync14, readFileSync as
|
|
7603
|
+
import { existsSync as existsSync14, readFileSync as readFileSync17, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6818
7604
|
import { join as join16 } from "node:path";
|
|
6819
|
-
|
|
6820
|
-
// src/lib/registry-write.ts
|
|
6821
|
-
import { existsSync as existsSync13, mkdirSync as mkdirSync3, readFileSync as readFileSync15, writeFileSync as writeFileSync4 } from "node:fs";
|
|
6822
|
-
import { dirname as dirname6, join as join15 } from "node:path";
|
|
6823
|
-
var PROJECTS_REGISTRY_PATH = ".vinaya/projects.md";
|
|
6824
|
-
function rowLine(name, path, specsPath) {
|
|
6825
|
-
return `| ${name} | \`${path}\` | \`${specsPath}\` | (state tracked globally) |`;
|
|
6826
|
-
}
|
|
6827
|
-
function freshProjectsRegistry(name, path, specsPath) {
|
|
6828
|
-
return `---
|
|
6829
|
-
sidebar_title: Projects
|
|
6830
|
-
---
|
|
6831
|
-
# Projects in this repo
|
|
6832
|
-
|
|
6833
|
-
**The project registry.** Declares the projects in this repo and where each
|
|
6834
|
-
one's specs and per-project state live. The \`Project\` field on a task (a
|
|
6835
|
-
forge Issue) resolves against this file.
|
|
6836
|
-
|
|
6837
|
-
**Presence of this file means this is a multi-project repo** — the
|
|
6838
|
-
\`Project\` field is required on task Issues/PRs that touch a registered
|
|
6839
|
-
project's path. A project is a \`(name, path)\` pair you declared — nothing
|
|
6840
|
-
is derived from the folder tree.
|
|
6841
|
-
|
|
6842
|
-
## Registry
|
|
6843
|
-
|
|
6844
|
-
| Project | Path | Specs | Per-project state |
|
|
6845
|
-
|---------|------|-------|---------------------|
|
|
6846
|
-
${rowLine(name, path, specsPath)}
|
|
6847
|
-
`;
|
|
6848
|
-
}
|
|
6849
|
-
var REGISTRY_HEADING_RE = /^##\s+Registry\b/i;
|
|
6850
|
-
function appendRegistryRow(content, name, path, specsPath) {
|
|
6851
|
-
const lines = content.split(/\r?\n/);
|
|
6852
|
-
const headingIdx = lines.findIndex((l) => REGISTRY_HEADING_RE.test(l));
|
|
6853
|
-
const newRow = rowLine(name, path, specsPath);
|
|
6854
|
-
if (headingIdx === -1) {
|
|
6855
|
-
const sep4 = content.endsWith(`
|
|
6856
|
-
`) ? "" : `
|
|
6857
|
-
`;
|
|
6858
|
-
return `${content}${sep4}
|
|
6859
|
-
## Registry
|
|
6860
|
-
|
|
6861
|
-
| Project | Path | Specs | Per-project state |
|
|
6862
|
-
|---------|------|-------|---------------------|
|
|
6863
|
-
${newRow}
|
|
6864
|
-
`;
|
|
6865
|
-
}
|
|
6866
|
-
let i = headingIdx + 1;
|
|
6867
|
-
for (;i < lines.length; i++) {
|
|
6868
|
-
if ((lines[i] ?? "").trim().startsWith("|"))
|
|
6869
|
-
break;
|
|
6870
|
-
}
|
|
6871
|
-
if (i >= lines.length) {
|
|
6872
|
-
lines.splice(headingIdx + 1, 0, "", "| Project | Path | Specs | Per-project state |", "|---------|------|-------|---------------------|", newRow);
|
|
6873
|
-
return lines.join(`
|
|
6874
|
-
`);
|
|
6875
|
-
}
|
|
6876
|
-
let last = i + 1;
|
|
6877
|
-
for (let j = i + 2;j < lines.length; j++) {
|
|
6878
|
-
if (!(lines[j] ?? "").trim().startsWith("|"))
|
|
6879
|
-
break;
|
|
6880
|
-
last = j;
|
|
6881
|
-
}
|
|
6882
|
-
lines.splice(last + 1, 0, newRow);
|
|
6883
|
-
return lines.join(`
|
|
6884
|
-
`);
|
|
6885
|
-
}
|
|
6886
|
-
function planRegistryRow(repoRoot, name, path, specsPath) {
|
|
6887
|
-
const abs2 = join15(repoRoot, PROJECTS_REGISTRY_PATH);
|
|
6888
|
-
const line = rowLine(name, path, specsPath);
|
|
6889
|
-
if (!existsSync13(abs2)) {
|
|
6890
|
-
return { action: "create-host", rowLine: line, path: PROJECTS_REGISTRY_PATH };
|
|
6891
|
-
}
|
|
6892
|
-
const existing = readFileSync15(abs2, "utf-8");
|
|
6893
|
-
const already = parseRegistry(existing).some((p) => p.name === name);
|
|
6894
|
-
return { action: already ? "skip-present" : "append-row", rowLine: line, path: PROJECTS_REGISTRY_PATH };
|
|
6895
|
-
}
|
|
6896
|
-
function applyRegistryRow(repoRoot, plan, name, path, specsPath) {
|
|
6897
|
-
const abs2 = join15(repoRoot, PROJECTS_REGISTRY_PATH);
|
|
6898
|
-
if (plan.action === "create-host") {
|
|
6899
|
-
mkdirSync3(dirname6(abs2), { recursive: true });
|
|
6900
|
-
writeFileSync4(abs2, freshProjectsRegistry(name, path, specsPath), "utf-8");
|
|
6901
|
-
} else if (plan.action === "append-row") {
|
|
6902
|
-
const existing = readFileSync15(abs2, "utf-8");
|
|
6903
|
-
writeFileSync4(abs2, appendRegistryRow(existing, name, path, specsPath), "utf-8");
|
|
6904
|
-
}
|
|
6905
|
-
}
|
|
6906
|
-
function renderRegistryRowDiffLine(plan) {
|
|
6907
|
-
if (plan.action === "skip-present")
|
|
6908
|
-
return ` = keep ${plan.path} (project already registered)`;
|
|
6909
|
-
if (plan.action === "append-row")
|
|
6910
|
-
return ` ~ append row to ${plan.path}
|
|
6911
|
-
${plan.rowLine}`;
|
|
6912
|
-
return ` + create ${plan.path} (with registry row)
|
|
6913
|
-
${plan.rowLine}`;
|
|
6914
|
-
}
|
|
6915
|
-
|
|
6916
|
-
// src/commands/init.ts
|
|
6917
|
-
function realDeps5() {
|
|
7605
|
+
function realDeps6() {
|
|
6918
7606
|
return {
|
|
6919
7607
|
detectRepo: detectGitRepo,
|
|
6920
7608
|
checkGhAuth,
|
|
@@ -6957,14 +7645,14 @@ function readManifest2(repoRoot) {
|
|
|
6957
7645
|
if (!existsSync14(p))
|
|
6958
7646
|
return null;
|
|
6959
7647
|
try {
|
|
6960
|
-
return VinayaConfigSchema.parse(JSON.parse(
|
|
7648
|
+
return VinayaConfigSchema.parse(JSON.parse(readFileSync17(p, "utf-8"))).managed ?? null;
|
|
6961
7649
|
} catch {
|
|
6962
7650
|
return null;
|
|
6963
7651
|
}
|
|
6964
7652
|
}
|
|
6965
7653
|
function writeManifest(repoRoot, manifest) {
|
|
6966
7654
|
const configAbs = join16(repoRoot, CONFIG_PATH);
|
|
6967
|
-
const seed = JSON.parse(
|
|
7655
|
+
const seed = JSON.parse(readFileSync17(configAbs, "utf-8"));
|
|
6968
7656
|
writeFileSync5(configAbs, `${JSON.stringify({ ...seed, managed: manifest }, null, 2)}
|
|
6969
7657
|
`, "utf-8");
|
|
6970
7658
|
}
|
|
@@ -7072,6 +7760,7 @@ async function runInitProduct(args, deps) {
|
|
|
7072
7760
|
return 1;
|
|
7073
7761
|
}
|
|
7074
7762
|
const registryPlan = planRegistryRow(repo.repoRoot, name, productPath, specsPath);
|
|
7763
|
+
const configPlan = planConfigProjectEntry(repo.repoRoot, { name, path: productPath });
|
|
7075
7764
|
process.stdout.write(`vinaya init product ${name} — the full diff:
|
|
7076
7765
|
|
|
7077
7766
|
`);
|
|
@@ -7079,6 +7768,11 @@ async function runInitProduct(args, deps) {
|
|
|
7079
7768
|
`);
|
|
7080
7769
|
process.stdout.write(`${renderRegistryRowDiffLine(registryPlan)}
|
|
7081
7770
|
|
|
7771
|
+
`);
|
|
7772
|
+
process.stdout.write(`── Config ────────────────────────────────────────
|
|
7773
|
+
`);
|
|
7774
|
+
process.stdout.write(`${renderConfigProjectEntryDiffLine(configPlan)}
|
|
7775
|
+
|
|
7082
7776
|
`);
|
|
7083
7777
|
if (dryRun) {
|
|
7084
7778
|
process.stdout.write(`--dry-run: nothing was written.
|
|
@@ -7094,25 +7788,26 @@ async function runInitProduct(args, deps) {
|
|
|
7094
7788
|
}
|
|
7095
7789
|
}
|
|
7096
7790
|
applyRegistryRow(repo.repoRoot, registryPlan, name, productPath, specsPath);
|
|
7791
|
+
applyConfigProjectEntry(repo.repoRoot, configPlan);
|
|
7097
7792
|
process.stdout.write(`
|
|
7098
7793
|
Governed product area '${name}' scaffolded.
|
|
7099
7794
|
`);
|
|
7100
7795
|
return 0;
|
|
7101
7796
|
}
|
|
7102
7797
|
async function initCommand(args) {
|
|
7103
|
-
process.exit(await runInit(args,
|
|
7798
|
+
process.exit(await runInit(args, realDeps6()));
|
|
7104
7799
|
}
|
|
7105
7800
|
async function initProductCommand(args) {
|
|
7106
|
-
process.exit(await runInitProduct(args,
|
|
7801
|
+
process.exit(await runInitProduct(args, realDeps6()));
|
|
7107
7802
|
}
|
|
7108
7803
|
|
|
7109
7804
|
// src/commands/issue.ts
|
|
7110
|
-
import { execFileSync as
|
|
7805
|
+
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
7111
7806
|
|
|
7112
7807
|
// src/lib/forge-write.ts
|
|
7113
|
-
import { execFileSync as
|
|
7808
|
+
import { execFileSync as execFileSync10 } from "node:child_process";
|
|
7114
7809
|
import { existsSync as existsSync15, mkdtempSync, readdirSync as readdirSync7, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
|
7115
|
-
import { readFileSync as
|
|
7810
|
+
import { readFileSync as readFileSync18 } from "node:fs";
|
|
7116
7811
|
import { tmpdir } from "node:os";
|
|
7117
7812
|
import { join as join17 } from "node:path";
|
|
7118
7813
|
class ForgeArgError extends Error {
|
|
@@ -7124,13 +7819,13 @@ function locateBody(args) {
|
|
|
7124
7819
|
const p = args[i + 1];
|
|
7125
7820
|
if (!p)
|
|
7126
7821
|
throw new ForgeArgError("`--body-file` was given with no path.");
|
|
7127
|
-
return { body:
|
|
7822
|
+
return { body: readFileSync18(p, "utf8"), source: { kind: "file", argIndex: i + 1, inlineForm: false } };
|
|
7128
7823
|
}
|
|
7129
7824
|
if (a.startsWith("--body-file=")) {
|
|
7130
7825
|
const p = a.slice("--body-file=".length);
|
|
7131
7826
|
if (!p)
|
|
7132
7827
|
throw new ForgeArgError("`--body-file=` was given with no path.");
|
|
7133
|
-
return { body:
|
|
7828
|
+
return { body: readFileSync18(p, "utf8"), source: { kind: "file", argIndex: i, inlineForm: true } };
|
|
7134
7829
|
}
|
|
7135
7830
|
if (a === "--body" || a === "-b") {
|
|
7136
7831
|
const v = args[i + 1];
|
|
@@ -7204,7 +7899,7 @@ function ensureTrancheLabelExists(slug) {
|
|
|
7204
7899
|
const name = trancheLabel(slug);
|
|
7205
7900
|
let existing;
|
|
7206
7901
|
try {
|
|
7207
|
-
const out =
|
|
7902
|
+
const out = execFileSync10("gh", ["label", "list", "--json", "name", "--limit", "200"], {
|
|
7208
7903
|
encoding: "utf8",
|
|
7209
7904
|
stdio: ["ignore", "pipe", "pipe"]
|
|
7210
7905
|
});
|
|
@@ -7217,7 +7912,7 @@ function ensureTrancheLabelExists(slug) {
|
|
|
7217
7912
|
if (existing.some((l) => l.name === name))
|
|
7218
7913
|
return;
|
|
7219
7914
|
try {
|
|
7220
|
-
|
|
7915
|
+
execFileSync10("gh", ["label", "create", name, "--color", TRANCHE_LABEL_COLOR, "--description", `Tranche: ${slug}`], {
|
|
7221
7916
|
encoding: "utf8",
|
|
7222
7917
|
stdio: ["ignore", "pipe", "pipe"]
|
|
7223
7918
|
});
|
|
@@ -7227,6 +7922,24 @@ function ensureTrancheLabelExists(slug) {
|
|
|
7227
7922
|
]);
|
|
7228
7923
|
}
|
|
7229
7924
|
}
|
|
7925
|
+
function resolveMilestoneAttachArgs(ghArgs, labels) {
|
|
7926
|
+
if (!isTaskIssueLabelSet(labels))
|
|
7927
|
+
return ghArgs;
|
|
7928
|
+
if (hasExplicitMilestoneFlag(ghArgs))
|
|
7929
|
+
return ghArgs;
|
|
7930
|
+
const slug = findTrancheSlug(labels);
|
|
7931
|
+
if (!slug)
|
|
7932
|
+
return ghArgs;
|
|
7933
|
+
let target;
|
|
7934
|
+
try {
|
|
7935
|
+
target = findMilestoneAttachTargetForSlug("{owner}", "{repo}", slug);
|
|
7936
|
+
} catch {
|
|
7937
|
+
process.stderr.write(`vinaya: milestone lookup for '${slug}' failed (\`gh api\`) — creating without --milestone.
|
|
7938
|
+
`);
|
|
7939
|
+
return ghArgs;
|
|
7940
|
+
}
|
|
7941
|
+
return target ? [...ghArgs, "--milestone", target.title] : ghArgs;
|
|
7942
|
+
}
|
|
7230
7943
|
function resolveSections(kind, retryCommand) {
|
|
7231
7944
|
const result = loadConfigChecked();
|
|
7232
7945
|
if (!result.ok) {
|
|
@@ -7351,7 +8064,7 @@ function validateForgeWrite(input) {
|
|
|
7351
8064
|
}
|
|
7352
8065
|
function git2(args) {
|
|
7353
8066
|
try {
|
|
7354
|
-
return
|
|
8067
|
+
return execFileSync10("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
7355
8068
|
} catch {
|
|
7356
8069
|
return "";
|
|
7357
8070
|
}
|
|
@@ -7362,7 +8075,7 @@ function repoRoot() {
|
|
|
7362
8075
|
function readWorkspaceGlobs(root) {
|
|
7363
8076
|
const fromPackageJson = () => {
|
|
7364
8077
|
try {
|
|
7365
|
-
const pkg = JSON.parse(
|
|
8078
|
+
const pkg = JSON.parse(readFileSync18(join17(root, "package.json"), "utf8"));
|
|
7366
8079
|
return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
|
|
7367
8080
|
} catch {
|
|
7368
8081
|
return [];
|
|
@@ -7370,7 +8083,7 @@ function readWorkspaceGlobs(root) {
|
|
|
7370
8083
|
};
|
|
7371
8084
|
const fromPnpmWorkspaceYaml = () => {
|
|
7372
8085
|
try {
|
|
7373
|
-
return parsePnpmWorkspaceYaml(
|
|
8086
|
+
return parsePnpmWorkspaceYaml(readFileSync18(join17(root, "pnpm-workspace.yaml"), "utf8"));
|
|
7374
8087
|
} catch {
|
|
7375
8088
|
return [];
|
|
7376
8089
|
}
|
|
@@ -7386,7 +8099,7 @@ function listWorkspaceChildDirs(dir, root) {
|
|
|
7386
8099
|
}
|
|
7387
8100
|
function readConfigExtraDomains(root) {
|
|
7388
8101
|
try {
|
|
7389
|
-
const parsed = VinayaConfigSchema.safeParse(JSON.parse(
|
|
8102
|
+
const parsed = VinayaConfigSchema.safeParse(JSON.parse(readFileSync18(join17(root, "vinaya.config.json"), "utf8")));
|
|
7390
8103
|
return parsed.success ? parsed.data.blastRadius?.extraDomains ?? [] : [];
|
|
7391
8104
|
} catch {
|
|
7392
8105
|
return [];
|
|
@@ -7404,7 +8117,7 @@ function readProjectPaths(root = repoRoot()) {
|
|
|
7404
8117
|
if (!root)
|
|
7405
8118
|
return [];
|
|
7406
8119
|
try {
|
|
7407
|
-
return parseRegistry(
|
|
8120
|
+
return parseRegistry(readFileSync18(join17(root, ".vinaya/projects.md"), "utf8"));
|
|
7408
8121
|
} catch {
|
|
7409
8122
|
return [];
|
|
7410
8123
|
}
|
|
@@ -7446,7 +8159,7 @@ function locateBodyOrRefuse(ghArgs, retryCommand) {
|
|
|
7446
8159
|
function fetchForgeLabels(issueRef) {
|
|
7447
8160
|
let out;
|
|
7448
8161
|
try {
|
|
7449
|
-
out =
|
|
8162
|
+
out = execFileSync11("gh", ["issue", "view", issueRef, "--json", "labels"], {
|
|
7450
8163
|
encoding: "utf8",
|
|
7451
8164
|
stdio: ["ignore", "pipe", "pipe"]
|
|
7452
8165
|
});
|
|
@@ -7474,7 +8187,7 @@ function reportPass(json, command) {
|
|
|
7474
8187
|
function runGhWrite(ghCmd, ghArgs, bodyResult, json) {
|
|
7475
8188
|
const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
|
|
7476
8189
|
try {
|
|
7477
|
-
const out =
|
|
8190
|
+
const out = execFileSync11("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
|
|
7478
8191
|
const url = out.trim();
|
|
7479
8192
|
if (json)
|
|
7480
8193
|
printJson({ validated: true, written: true, url });
|
|
@@ -7529,7 +8242,7 @@ function issueCreateCommand(args) {
|
|
|
7529
8242
|
const slugToEnsure = findTrancheSlug(labels);
|
|
7530
8243
|
if (slugToEnsure)
|
|
7531
8244
|
ensureTrancheLabelExists(slugToEnsure);
|
|
7532
|
-
runGhWrite(["issue", "create"], ghArgs, bodyResult, json);
|
|
8245
|
+
runGhWrite(["issue", "create"], resolveMilestoneAttachArgs(ghArgs, labels), bodyResult, json);
|
|
7533
8246
|
}
|
|
7534
8247
|
function issueEditCommand(args) {
|
|
7535
8248
|
const json = args.includes("--json");
|
|
@@ -7560,12 +8273,13 @@ function issueEditCommand(args) {
|
|
|
7560
8273
|
}
|
|
7561
8274
|
|
|
7562
8275
|
// src/commands/milestone.ts
|
|
7563
|
-
import { execFileSync as
|
|
8276
|
+
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
7564
8277
|
var RETRY_CREATE2 = "vinaya milestone create --title <title> --body-file <path>";
|
|
7565
8278
|
var RETRY_ADOPT = "vinaya milestone adopt --target <title> --slug <slug> [--slug <slug> ...]";
|
|
7566
8279
|
var RETRY_EDIT2 = "vinaya milestone edit <n> --body-file <path>";
|
|
8280
|
+
var RETRY_CLOSE = "vinaya milestone close --slug <slug>";
|
|
7567
8281
|
function sh3(args, input) {
|
|
7568
|
-
return
|
|
8282
|
+
return execFileSync12(args[0], args.slice(1), {
|
|
7569
8283
|
encoding: "utf8",
|
|
7570
8284
|
input,
|
|
7571
8285
|
env: process.env,
|
|
@@ -7723,18 +8437,18 @@ function extractSlugs(args) {
|
|
|
7723
8437
|
}
|
|
7724
8438
|
return slugs;
|
|
7725
8439
|
}
|
|
7726
|
-
function ghJsonOrRefuse(args, what) {
|
|
8440
|
+
function ghJsonOrRefuse(args, what, retryCommand) {
|
|
7727
8441
|
try {
|
|
7728
8442
|
return shJson2(args);
|
|
7729
8443
|
} catch (e) {
|
|
7730
8444
|
refuse([
|
|
7731
|
-
makeCheckError("forge-fetch", `could not fetch ${what} from the forge: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${
|
|
8445
|
+
makeCheckError("forge-fetch", `could not fetch ${what} from the forge: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${retryCommand}\`.`)
|
|
7732
8446
|
]);
|
|
7733
8447
|
}
|
|
7734
8448
|
}
|
|
7735
8449
|
function fetchAdoptFacts(repoFlag, target, slugs) {
|
|
7736
|
-
const labels = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/labels?per_page=100`], "this repo's labels");
|
|
7737
|
-
const milestones = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/milestones?state=all&per_page=100`], "this repo's Milestones");
|
|
8450
|
+
const labels = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/labels?per_page=100`], "this repo's labels", RETRY_ADOPT);
|
|
8451
|
+
const milestones = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/milestones?state=all&per_page=100`], "this repo's Milestones", RETRY_ADOPT);
|
|
7738
8452
|
const targetMilestone = milestones.find((m) => m.title === target);
|
|
7739
8453
|
const targetFacts = {
|
|
7740
8454
|
title: target,
|
|
@@ -7757,7 +8471,7 @@ function fetchAdoptFacts(repoFlag, target, slugs) {
|
|
|
7757
8471
|
"number,state,milestone",
|
|
7758
8472
|
"--limit",
|
|
7759
8473
|
"200"
|
|
7760
|
-
], `Issues for \`${trancheLabel(slug)}
|
|
8474
|
+
], `Issues for \`${trancheLabel(slug)}\``, RETRY_ADOPT) : [];
|
|
7761
8475
|
const elsewhere = new Set;
|
|
7762
8476
|
for (const issue of issues) {
|
|
7763
8477
|
const title = issue.milestone?.title;
|
|
@@ -7803,56 +8517,149 @@ async function milestoneAdoptCommand(args) {
|
|
|
7803
8517
|
}
|
|
7804
8518
|
if (validateOnly) {
|
|
7805
8519
|
if (json)
|
|
7806
|
-
printJson({ validated: true, written: false, command: "milestone adopt" });
|
|
7807
|
-
else
|
|
7808
|
-
process.stdout.write(`✓ adopt is safe — nothing written (--validate-only).
|
|
8520
|
+
printJson({ validated: true, written: false, command: "milestone adopt" });
|
|
8521
|
+
else
|
|
8522
|
+
process.stdout.write(`✓ adopt is safe — nothing written (--validate-only).
|
|
8523
|
+
`);
|
|
8524
|
+
return;
|
|
8525
|
+
}
|
|
8526
|
+
const adopted = [];
|
|
8527
|
+
for (const slugFact of facts.slugs) {
|
|
8528
|
+
for (const issueNumber of slugFact.issueNumbers) {
|
|
8529
|
+
try {
|
|
8530
|
+
sh3(["gh", "issue", "edit", String(issueNumber), "-R", repoFlag, "--milestone", target]);
|
|
8531
|
+
} catch (e) {
|
|
8532
|
+
failMidWrite(`failed to attach Issue #${issueNumber} (tranche \`${slugFact.slug}\`) to Milestone "${target}": ` + `${ghErrorDetail(e)}. Slugs already adopted this run: ${adopted.map((a) => a.slug).join(", ") || "(none)"}.`);
|
|
8533
|
+
}
|
|
8534
|
+
}
|
|
8535
|
+
const legacy = milestones.find((m) => m.title === slugFact.slug && m.title !== target);
|
|
8536
|
+
let closedMilestone = null;
|
|
8537
|
+
if (legacy) {
|
|
8538
|
+
closedMilestone = legacy.number;
|
|
8539
|
+
if (legacy.state !== "closed") {
|
|
8540
|
+
try {
|
|
8541
|
+
sh3(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${legacy.number}`, "-f", "state=closed"]);
|
|
8542
|
+
} catch (e) {
|
|
8543
|
+
failMidWrite(`attached tranche \`${slugFact.slug}\`'s Issue(s) to Milestone "${target}", but failed to close its old ` + `Milestone #${legacy.number}: ${ghErrorDetail(e)}. Slugs already adopted this run: ` + `${adopted.map((a) => a.slug).join(", ") || "(none)"}.`);
|
|
8544
|
+
}
|
|
8545
|
+
}
|
|
8546
|
+
}
|
|
8547
|
+
adopted.push({ slug: slugFact.slug, issueCount: slugFact.issueNumbers.length, closedMilestone });
|
|
8548
|
+
}
|
|
8549
|
+
if (json)
|
|
8550
|
+
printJson({ validated: true, written: true, target, adopted });
|
|
8551
|
+
else {
|
|
8552
|
+
for (const a of adopted) {
|
|
8553
|
+
const closedNote = a.closedMilestone ? ` (old Milestone #${a.closedMilestone} closed)` : "";
|
|
8554
|
+
process.stdout.write(`${a.slug}: ${a.issueCount} Issue(s) → "${target}"${closedNote}
|
|
8555
|
+
`);
|
|
8556
|
+
}
|
|
8557
|
+
}
|
|
8558
|
+
}
|
|
8559
|
+
function extractSlug(args) {
|
|
8560
|
+
for (let i = 0;i < args.length; i++) {
|
|
8561
|
+
const a = args[i];
|
|
8562
|
+
if (a === "--slug")
|
|
8563
|
+
return args[i + 1] ?? null;
|
|
8564
|
+
if (a.startsWith("--slug="))
|
|
8565
|
+
return a.slice("--slug=".length);
|
|
8566
|
+
}
|
|
8567
|
+
return null;
|
|
8568
|
+
}
|
|
8569
|
+
async function milestoneCloseCommand(args) {
|
|
8570
|
+
const json = args.includes("--json");
|
|
8571
|
+
const validateOnly = args.includes("--validate-only");
|
|
8572
|
+
const rest = args.filter((a) => a !== "--json" && a !== "--validate-only");
|
|
8573
|
+
const slug = extractSlug(rest);
|
|
8574
|
+
if (!slug) {
|
|
8575
|
+
refuse([
|
|
8576
|
+
makeCheckError("forge-args", "`vinaya milestone close` requires a `--slug <slug>` — the tranche whose Milestone is being closed.", `Add \`--slug <slug>\`, then re-run \`${RETRY_CLOSE}\`.`)
|
|
8577
|
+
]);
|
|
8578
|
+
}
|
|
8579
|
+
const repoFlag = await resolveRepoFlagOrRefuse(RETRY_CLOSE);
|
|
8580
|
+
const milestones = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/milestones?state=all&per_page=100`], "this repo's Milestones", RETRY_CLOSE);
|
|
8581
|
+
const target = resolveMilestoneAttachTarget(milestones, slug);
|
|
8582
|
+
if (!target) {
|
|
8583
|
+
refuse([
|
|
8584
|
+
makeCheckError("milestone-close", `No OPEN Milestone resolves for tranche \`${slug}\` — it may already be closed, or never had one attached.`, `Confirm with \`gh issue list --label ${trancheLabel(slug)} --json milestone\`, or run \`vinaya milestone ` + `create\`/\`${RETRY_ADOPT}\` if a Milestone is genuinely still owed, then re-run \`${RETRY_CLOSE}\`.`)
|
|
8585
|
+
]);
|
|
8586
|
+
}
|
|
8587
|
+
const labeledIssues = ghJsonOrRefuse([
|
|
8588
|
+
"gh",
|
|
8589
|
+
"issue",
|
|
8590
|
+
"list",
|
|
8591
|
+
"-R",
|
|
8592
|
+
repoFlag,
|
|
8593
|
+
"--label",
|
|
8594
|
+
trancheLabel(slug),
|
|
8595
|
+
"--state",
|
|
8596
|
+
"all",
|
|
8597
|
+
"--json",
|
|
8598
|
+
"number,state,milestone",
|
|
8599
|
+
"--limit",
|
|
8600
|
+
"200"
|
|
8601
|
+
], `Issues for \`${trancheLabel(slug)}\``, RETRY_CLOSE);
|
|
8602
|
+
const attachedIssues = ghJsonOrRefuse([
|
|
8603
|
+
"gh",
|
|
8604
|
+
"issue",
|
|
8605
|
+
"list",
|
|
8606
|
+
"-R",
|
|
8607
|
+
repoFlag,
|
|
8608
|
+
"--milestone",
|
|
8609
|
+
target.title,
|
|
8610
|
+
"--state",
|
|
8611
|
+
"all",
|
|
8612
|
+
"--json",
|
|
8613
|
+
"number,state,milestone",
|
|
8614
|
+
"--limit",
|
|
8615
|
+
"200"
|
|
8616
|
+
], `Issues attached to Milestone "${target.title}"`, RETRY_CLOSE);
|
|
8617
|
+
const report = checkMilestoneAttachment(labeledIssues.map((i) => i.number), attachedIssues.map((i) => i.number));
|
|
8618
|
+
if (report.status === "mismatch") {
|
|
8619
|
+
const errors = [];
|
|
8620
|
+
if (report.unattached.length > 0) {
|
|
8621
|
+
errors.push(makeCheckError("milestone-close", `Issue(s) ${report.unattached.map((n) => `#${n}`).join(", ")} carry \`${trancheLabel(slug)}\` but ` + `are not attached to Milestone "${target.title}".`, `Attach each with \`gh issue edit <n> --milestone "${target.title}"\`, then re-run \`${RETRY_CLOSE}\`.`));
|
|
8622
|
+
}
|
|
8623
|
+
if (report.foreign.length > 0) {
|
|
8624
|
+
errors.push(makeCheckError("milestone-close", `Issue(s) ${report.foreign.map((n) => `#${n}`).join(", ")} are attached to Milestone "${target.title}" ` + `but do not carry \`${trancheLabel(slug)}\` — closing would end tracking for whatever they belong to as well.`, `Move them with \`${RETRY_ADOPT}\` if they belong to a different tranche, or \`gh issue edit <n> ` + `--milestone <title>\` to relocate them, then re-run \`${RETRY_CLOSE}\`.`));
|
|
8625
|
+
}
|
|
8626
|
+
refuse(errors);
|
|
8627
|
+
}
|
|
8628
|
+
if (validateOnly) {
|
|
8629
|
+
if (json)
|
|
8630
|
+
printJson({ validated: true, written: false, command: "milestone close", number: target.number });
|
|
8631
|
+
else {
|
|
8632
|
+
process.stdout.write(`✓ Milestone "${target.title}" (#${target.number}) attachment verified — nothing written (--validate-only).
|
|
7809
8633
|
`);
|
|
8634
|
+
}
|
|
7810
8635
|
return;
|
|
7811
8636
|
}
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
}
|
|
7818
|
-
|
|
7819
|
-
}
|
|
7820
|
-
}
|
|
7821
|
-
const legacy = milestones.find((m) => m.title === slugFact.slug && m.title !== target);
|
|
7822
|
-
let closedMilestone = null;
|
|
7823
|
-
if (legacy) {
|
|
7824
|
-
closedMilestone = legacy.number;
|
|
7825
|
-
if (legacy.state !== "closed") {
|
|
7826
|
-
try {
|
|
7827
|
-
sh3(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${legacy.number}`, "-f", "state=closed"]);
|
|
7828
|
-
} catch (e) {
|
|
7829
|
-
failMidWrite(`attached tranche \`${slugFact.slug}\`'s Issue(s) to Milestone "${target}", but failed to close its old ` + `Milestone #${legacy.number}: ${ghErrorDetail(e)}. Slugs already adopted this run: ` + `${adopted.map((a) => a.slug).join(", ") || "(none)"}.`);
|
|
7830
|
-
}
|
|
7831
|
-
}
|
|
7832
|
-
}
|
|
7833
|
-
adopted.push({ slug: slugFact.slug, issueCount: slugFact.issueNumbers.length, closedMilestone });
|
|
8637
|
+
let out;
|
|
8638
|
+
try {
|
|
8639
|
+
out = sh3(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${target.number}`, "-f", "state=closed"]);
|
|
8640
|
+
} catch (e) {
|
|
8641
|
+
refuse([
|
|
8642
|
+
makeCheckError("forge-fetch", `\`gh api repos/${repoFlag}/milestones/${target.number}\` failed: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_CLOSE}\`.`)
|
|
8643
|
+
]);
|
|
7834
8644
|
}
|
|
8645
|
+
const closed = JSON.parse(out);
|
|
7835
8646
|
if (json)
|
|
7836
|
-
printJson({ validated: true, written: true,
|
|
7837
|
-
else
|
|
7838
|
-
|
|
7839
|
-
const closedNote = a.closedMilestone ? ` (old Milestone #${a.closedMilestone} closed)` : "";
|
|
7840
|
-
process.stdout.write(`${a.slug}: ${a.issueCount} Issue(s) → "${target}"${closedNote}
|
|
8647
|
+
printJson({ validated: true, written: true, number: closed.number, url: closed.html_url });
|
|
8648
|
+
else
|
|
8649
|
+
process.stdout.write(`${closed.html_url}
|
|
7841
8650
|
`);
|
|
7842
|
-
}
|
|
7843
|
-
}
|
|
7844
8651
|
}
|
|
7845
8652
|
|
|
7846
8653
|
// src/commands/new-check.ts
|
|
7847
|
-
import { chmodSync as chmodSync2, existsSync as existsSync16, mkdirSync as mkdirSync4, readFileSync as
|
|
8654
|
+
import { chmodSync as chmodSync2, existsSync as existsSync16, mkdirSync as mkdirSync4, readFileSync as readFileSync19, writeFileSync as writeFileSync7 } from "node:fs";
|
|
7848
8655
|
import { join as join18 } from "node:path";
|
|
7849
8656
|
var TEMPLATE_PATH = join18(packageRoot(import.meta.url), "templates", "custom-check.template.ts");
|
|
7850
8657
|
var CHECKS_DIR = join18("scripts", "vinaya-checks");
|
|
7851
|
-
var
|
|
8658
|
+
var USAGE2 = "Usage: vinaya new check <yourname>/<id> (both segments: lowercase letters, digits, hyphens; e.g. myteam/vocab-check)";
|
|
7852
8659
|
function newCheckCommand(args) {
|
|
7853
8660
|
const name = args[0];
|
|
7854
8661
|
if (!name) {
|
|
7855
|
-
console.error(
|
|
8662
|
+
console.error(USAGE2);
|
|
7856
8663
|
process.exit(2);
|
|
7857
8664
|
}
|
|
7858
8665
|
if (coreCheckRegistry().some((spec) => spec.name === name)) {
|
|
@@ -7860,7 +8667,7 @@ function newCheckCommand(args) {
|
|
|
7860
8667
|
process.exit(2);
|
|
7861
8668
|
}
|
|
7862
8669
|
if (!isValidNamespacedKey(name)) {
|
|
7863
|
-
console.error(`Refusing: "${name}" is not a valid check id. ${
|
|
8670
|
+
console.error(`Refusing: "${name}" is not a valid check id. ${USAGE2.slice("Usage: ".length)}
|
|
7864
8671
|
\`vinaya check\` refuses the entire run over a key it cannot resolve, so a bare, un-namespaced name would brick every check invocation in this repo.`);
|
|
7865
8672
|
process.exit(2);
|
|
7866
8673
|
}
|
|
@@ -7873,7 +8680,7 @@ function newCheckCommand(args) {
|
|
|
7873
8680
|
console.error(`Error: ${targetPath} already exists.`);
|
|
7874
8681
|
process.exit(1);
|
|
7875
8682
|
}
|
|
7876
|
-
const template =
|
|
8683
|
+
const template = readFileSync19(TEMPLATE_PATH, "utf-8");
|
|
7877
8684
|
const contents = template.split("{{CHECK_NAME}}").join(name);
|
|
7878
8685
|
writeFileSync7(targetPath, contents, "utf-8");
|
|
7879
8686
|
chmodSync2(targetPath, 493);
|
|
@@ -7887,20 +8694,20 @@ ${registration}
|
|
|
7887
8694
|
}
|
|
7888
8695
|
|
|
7889
8696
|
// src/commands/new-noop-check.ts
|
|
7890
|
-
import { chmodSync as chmodSync3, existsSync as existsSync17, mkdirSync as mkdirSync5, readFileSync as
|
|
8697
|
+
import { chmodSync as chmodSync3, existsSync as existsSync17, mkdirSync as mkdirSync5, readFileSync as readFileSync20, writeFileSync as writeFileSync8 } from "node:fs";
|
|
7891
8698
|
import { join as join19 } from "node:path";
|
|
7892
8699
|
var TEMPLATE_PATH2 = join19(packageRoot(import.meta.url), "templates", "noop-check.template.ts");
|
|
7893
8700
|
var CHECKS_DIR2 = join19("vinaya", "checks");
|
|
7894
|
-
var
|
|
8701
|
+
var USAGE3 = "Usage: vinaya new noop-check <core-check-id> (the exact id of a core check — run `vinaya check --plan` to list them)";
|
|
7895
8702
|
function newNoopCheckCommand(args) {
|
|
7896
8703
|
const name = args[0];
|
|
7897
8704
|
if (!name) {
|
|
7898
|
-
console.error(
|
|
8705
|
+
console.error(USAGE3);
|
|
7899
8706
|
process.exit(2);
|
|
7900
8707
|
}
|
|
7901
8708
|
const coreSpec = coreCheckRegistry().find((spec) => spec.name === name);
|
|
7902
8709
|
if (!coreSpec) {
|
|
7903
|
-
console.error(`Refusing: "${name}" is not a core check id. ${
|
|
8710
|
+
console.error(`Refusing: "${name}" is not a core check id. ${USAGE3}
|
|
7904
8711
|
\`new noop-check\` only silences a CORE check — a \`checks\` key that exactly matches a core check id REPLACES it. A namespaced or unknown name has nothing to replace; use \`vinaya new check\` to scaffold a new, additive check instead.`);
|
|
7905
8712
|
process.exit(2);
|
|
7906
8713
|
}
|
|
@@ -7912,7 +8719,7 @@ function newNoopCheckCommand(args) {
|
|
|
7912
8719
|
console.error(`Error: ${targetPath} already exists.`);
|
|
7913
8720
|
process.exit(1);
|
|
7914
8721
|
}
|
|
7915
|
-
const template =
|
|
8722
|
+
const template = readFileSync20(TEMPLATE_PATH2, "utf-8");
|
|
7916
8723
|
const contents = template.split("{{CHECK_NAME}}").join(name);
|
|
7917
8724
|
writeFileSync8(targetPath, contents, "utf-8");
|
|
7918
8725
|
chmodSync3(targetPath, 493);
|
|
@@ -7934,19 +8741,19 @@ ${registration}
|
|
|
7934
8741
|
}
|
|
7935
8742
|
|
|
7936
8743
|
// src/commands/new-role.ts
|
|
7937
|
-
import { existsSync as existsSync18, mkdirSync as mkdirSync6, readFileSync as
|
|
8744
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync6, readFileSync as readFileSync21, writeFileSync as writeFileSync9 } from "node:fs";
|
|
7938
8745
|
import { join as join20 } from "node:path";
|
|
7939
8746
|
var TEMPLATE_PATH3 = join20(packageRoot(import.meta.url), "templates", "role.template.md");
|
|
7940
8747
|
var ROLES_DIR = join20("vinaya", "roles");
|
|
7941
|
-
var
|
|
8748
|
+
var USAGE4 = "Usage: vinaya new role <yourname>/<id> (both segments: lowercase letters, digits, hyphens; e.g. acme/qa-lead)";
|
|
7942
8749
|
function newRoleCommand(args) {
|
|
7943
8750
|
const name = args[0];
|
|
7944
8751
|
if (!name) {
|
|
7945
|
-
console.error(
|
|
8752
|
+
console.error(USAGE4);
|
|
7946
8753
|
process.exit(2);
|
|
7947
8754
|
}
|
|
7948
8755
|
if (!isValidNamespacedKey(name)) {
|
|
7949
|
-
console.error(`Refusing: "${name}" is not a valid role key. ${
|
|
8756
|
+
console.error(`Refusing: "${name}" is not a valid role key. ${USAGE4.slice("Usage: ".length)}
|
|
7950
8757
|
A bare, un-namespaced key resolves as an OVERRIDE of a core role — a complete replacement of that role's contract, and a real governance decision this scaffolder does not make for you. Pick a namespaced name, or write the override entry by hand if you really mean to replace a core role's contract.`);
|
|
7951
8758
|
process.exit(2);
|
|
7952
8759
|
}
|
|
@@ -7959,7 +8766,7 @@ A bare, un-namespaced key resolves as an OVERRIDE of a core role — a complete
|
|
|
7959
8766
|
console.error(`Error: ${targetPath} already exists.`);
|
|
7960
8767
|
process.exit(1);
|
|
7961
8768
|
}
|
|
7962
|
-
const template =
|
|
8769
|
+
const template = readFileSync21(TEMPLATE_PATH3, "utf-8");
|
|
7963
8770
|
const contents = template.split("{{ROLE_ID}}").join(roleId);
|
|
7964
8771
|
writeFileSync9(targetPath, contents, "utf-8");
|
|
7965
8772
|
const relPath = join20(ROLES_DIR, `${roleId}.md`);
|
|
@@ -7972,7 +8779,7 @@ ${registration}
|
|
|
7972
8779
|
}
|
|
7973
8780
|
|
|
7974
8781
|
// src/commands/pr.ts
|
|
7975
|
-
import { execFileSync as
|
|
8782
|
+
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
7976
8783
|
|
|
7977
8784
|
// src/lib/numstat.ts
|
|
7978
8785
|
function summariseNumstat(numstat) {
|
|
@@ -8033,6 +8840,16 @@ class ScanContext {
|
|
|
8033
8840
|
return rawHasPair === (anchoredRegionBounds(this.masked, field) !== null);
|
|
8034
8841
|
}
|
|
8035
8842
|
}
|
|
8843
|
+
function resolveAnchoredRegion(ctx, field) {
|
|
8844
|
+
const bounds = anchoredRegionBounds(ctx.masked, field);
|
|
8845
|
+
if (bounds === null) {
|
|
8846
|
+
return anchoredRegionBounds(ctx.normalised, field) !== null ? "hidden" : null;
|
|
8847
|
+
}
|
|
8848
|
+
return {
|
|
8849
|
+
region: ctx.normalised.slice(bounds.innerStart, bounds.innerEnd),
|
|
8850
|
+
maskedRegion: ctx.masked.slice(bounds.innerStart, bounds.innerEnd)
|
|
8851
|
+
};
|
|
8852
|
+
}
|
|
8036
8853
|
|
|
8037
8854
|
// src/checks/body-bare-digits-logic.ts
|
|
8038
8855
|
function blankRange(text, start, end) {
|
|
@@ -8254,7 +9071,7 @@ var RETRY_CREATE3 = "vinaya pr create --validate-only …";
|
|
|
8254
9071
|
var RETRY_EDIT3 = "vinaya pr edit <n> --validate-only …";
|
|
8255
9072
|
function git3(args) {
|
|
8256
9073
|
try {
|
|
8257
|
-
return
|
|
9074
|
+
return execFileSync13("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
8258
9075
|
} catch {
|
|
8259
9076
|
return "";
|
|
8260
9077
|
}
|
|
@@ -8288,7 +9105,7 @@ function reportPass2(json, command) {
|
|
|
8288
9105
|
function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
|
|
8289
9106
|
const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
|
|
8290
9107
|
try {
|
|
8291
|
-
const out =
|
|
9108
|
+
const out = execFileSync13("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
|
|
8292
9109
|
const url = out.trim();
|
|
8293
9110
|
if (json)
|
|
8294
9111
|
printJson({ validated: true, written: true, url });
|
|
@@ -8302,7 +9119,7 @@ function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
|
|
|
8302
9119
|
function fetchPrForgeContext(prRef) {
|
|
8303
9120
|
let viewOut;
|
|
8304
9121
|
try {
|
|
8305
|
-
viewOut =
|
|
9122
|
+
viewOut = execFileSync13("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
|
|
8306
9123
|
encoding: "utf8",
|
|
8307
9124
|
stdio: ["ignore", "pipe", "pipe"]
|
|
8308
9125
|
});
|
|
@@ -8413,14 +9230,14 @@ function prEditCommand(args) {
|
|
|
8413
9230
|
}
|
|
8414
9231
|
|
|
8415
9232
|
// src/commands/pr-report.ts
|
|
8416
|
-
import { execFileSync as
|
|
8417
|
-
import { existsSync as existsSync19, readFileSync as
|
|
9233
|
+
import { execFileSync as execFileSync14, spawnSync as spawnSync2 } from "node:child_process";
|
|
9234
|
+
import { existsSync as existsSync19, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
|
|
8418
9235
|
import { join as join21 } from "node:path";
|
|
8419
9236
|
var EVIDENCE_START = "<!-- AEG:EVIDENCE:START -->";
|
|
8420
9237
|
var EVIDENCE_END = "<!-- AEG:EVIDENCE:END -->";
|
|
8421
9238
|
function git4(args) {
|
|
8422
9239
|
try {
|
|
8423
|
-
return
|
|
9240
|
+
return execFileSync14("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
8424
9241
|
} catch {
|
|
8425
9242
|
return "";
|
|
8426
9243
|
}
|
|
@@ -8438,7 +9255,7 @@ class GitCommandError extends Error {
|
|
|
8438
9255
|
}
|
|
8439
9256
|
function gitStrict(args) {
|
|
8440
9257
|
try {
|
|
8441
|
-
return
|
|
9258
|
+
return execFileSync14("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
8442
9259
|
} catch (err) {
|
|
8443
9260
|
const stderr = err.stderr;
|
|
8444
9261
|
throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
|
|
@@ -8567,6 +9384,84 @@ ${EVIDENCE_END}`;
|
|
|
8567
9384
|
${full}
|
|
8568
9385
|
`;
|
|
8569
9386
|
}
|
|
9387
|
+
var TOKENS_START = "<!-- AEG:TOKENS:START -->";
|
|
9388
|
+
var TOKENS_END = "<!-- AEG:TOKENS:END -->";
|
|
9389
|
+
var TOKEN_REPORT_HEADING = /^#{1,6}\s*token report\s*$/i;
|
|
9390
|
+
var HEADING_LINE = /^#{1,6}\s/;
|
|
9391
|
+
var TOKEN_TABLE_HEADER = "| Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date |";
|
|
9392
|
+
var TOKEN_TABLE_SEPARATOR = "|---|---|---|---|---|---|---|";
|
|
9393
|
+
function tokensBlockLineBounds(maskedLines) {
|
|
9394
|
+
const startIdx = maskedLines.findIndex((l) => l.trim() === TOKENS_START);
|
|
9395
|
+
if (startIdx === -1)
|
|
9396
|
+
return null;
|
|
9397
|
+
for (let i = startIdx + 1;i < maskedLines.length; i++) {
|
|
9398
|
+
if (maskedLines[i].trim() === TOKENS_END)
|
|
9399
|
+
return { startIdx, endIdx: i };
|
|
9400
|
+
}
|
|
9401
|
+
return null;
|
|
9402
|
+
}
|
|
9403
|
+
function writeTokensBlock(body, addition) {
|
|
9404
|
+
const rawLines = body.split(`
|
|
9405
|
+
`);
|
|
9406
|
+
const maskedLines = maskCode(body).split(`
|
|
9407
|
+
`);
|
|
9408
|
+
const additionLines = addition.split(`
|
|
9409
|
+
`);
|
|
9410
|
+
const existing = tokensBlockLineBounds(maskedLines);
|
|
9411
|
+
if (existing) {
|
|
9412
|
+
const before = rawLines.slice(0, existing.endIdx);
|
|
9413
|
+
const after = rawLines.slice(existing.endIdx);
|
|
9414
|
+
return [...before, ...additionLines, ...after].join(`
|
|
9415
|
+
`);
|
|
9416
|
+
}
|
|
9417
|
+
const freshBlock = [TOKENS_START, TOKEN_TABLE_HEADER, TOKEN_TABLE_SEPARATOR, ...additionLines, TOKENS_END];
|
|
9418
|
+
const headingIdx = rawLines.findIndex((l) => TOKEN_REPORT_HEADING.test(l));
|
|
9419
|
+
if (headingIdx !== -1) {
|
|
9420
|
+
let sectionEnd = rawLines.length;
|
|
9421
|
+
for (let i = headingIdx + 1;i < rawLines.length; i++) {
|
|
9422
|
+
if (HEADING_LINE.test(rawLines[i])) {
|
|
9423
|
+
sectionEnd = i;
|
|
9424
|
+
break;
|
|
9425
|
+
}
|
|
9426
|
+
}
|
|
9427
|
+
const before = rawLines.slice(0, headingIdx + 1);
|
|
9428
|
+
const after = rawLines.slice(sectionEnd);
|
|
9429
|
+
return [...before, "", ...freshBlock, "", ...after].join(`
|
|
9430
|
+
`);
|
|
9431
|
+
}
|
|
9432
|
+
const sep4 = body.length === 0 || body.endsWith(`
|
|
9433
|
+
`) ? [] : [""];
|
|
9434
|
+
return [...rawLines, ...sep4, "", "## Token report", "", ...freshBlock, ""].join(`
|
|
9435
|
+
`);
|
|
9436
|
+
}
|
|
9437
|
+
function derivePhase() {
|
|
9438
|
+
const branch = git4(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
9439
|
+
const m = /^task\/[^/]+\/(.+)$/.exec(branch);
|
|
9440
|
+
const taskId = m ? m[1] : branch || "unknown";
|
|
9441
|
+
return `${taskId}: develop`;
|
|
9442
|
+
}
|
|
9443
|
+
function isoToday() {
|
|
9444
|
+
return new Date().toISOString().slice(0, 10);
|
|
9445
|
+
}
|
|
9446
|
+
function collectTokensAddition(opts) {
|
|
9447
|
+
const capability = resolveMeteringCapability(realDeps(), opts.transcriptPath);
|
|
9448
|
+
if (!capability.capable) {
|
|
9449
|
+
return formatTokenReportRow({
|
|
9450
|
+
phase: opts.phase,
|
|
9451
|
+
role: opts.role,
|
|
9452
|
+
summary: null,
|
|
9453
|
+
modelOverride: `— (${capability.reason})`,
|
|
9454
|
+
date: opts.date
|
|
9455
|
+
});
|
|
9456
|
+
}
|
|
9457
|
+
return formatTokenReportRow({
|
|
9458
|
+
phase: opts.phase,
|
|
9459
|
+
role: opts.role,
|
|
9460
|
+
summary: capability.summary,
|
|
9461
|
+
modelOverride: opts.modelOverride,
|
|
9462
|
+
date: opts.date
|
|
9463
|
+
});
|
|
9464
|
+
}
|
|
8570
9465
|
async function buildReport(opts = {}) {
|
|
8571
9466
|
const groupA = opts.groupA ?? computeGroupA();
|
|
8572
9467
|
const gateRunner = opts.gateRunner ?? runRealGates;
|
|
@@ -8580,8 +9475,16 @@ ${EVIDENCE_END}`;
|
|
|
8580
9475
|
async function prReportCommand(args) {
|
|
8581
9476
|
const writeIdx = args.indexOf("--write");
|
|
8582
9477
|
const writePath = writeIdx !== -1 ? args[writeIdx + 1] : undefined;
|
|
9478
|
+
const transcriptIdx = args.indexOf("--transcript");
|
|
9479
|
+
const transcriptPath = transcriptIdx !== -1 ? args[transcriptIdx + 1] : undefined;
|
|
9480
|
+
const phaseIdx = args.indexOf("--phase");
|
|
9481
|
+
const phaseOverride = phaseIdx !== -1 ? args[phaseIdx + 1] : undefined;
|
|
9482
|
+
const roleIdx = args.indexOf("--role");
|
|
9483
|
+
const roleOverride = roleIdx !== -1 ? args[roleIdx + 1] : undefined;
|
|
9484
|
+
const modelIdx = args.indexOf("--model");
|
|
9485
|
+
const modelOverride = modelIdx !== -1 ? args[modelIdx + 1] : undefined;
|
|
8583
9486
|
if (writeIdx !== -1 && !writePath) {
|
|
8584
|
-
console.error("Usage: vinaya pr report [--write <body-file>]");
|
|
9487
|
+
console.error("Usage: vinaya pr report [--write <body-file>] [--phase <phase>] [--role <role>] " + "[--model <id>] [--transcript <path>]");
|
|
8585
9488
|
process.exit(2);
|
|
8586
9489
|
}
|
|
8587
9490
|
let result;
|
|
@@ -8595,10 +9498,18 @@ async function prReportCommand(args) {
|
|
|
8595
9498
|
throw err;
|
|
8596
9499
|
}
|
|
8597
9500
|
if (writePath) {
|
|
8598
|
-
const existing = existsSync19(writePath) ?
|
|
8599
|
-
const
|
|
9501
|
+
const existing = existsSync19(writePath) ? readFileSync22(writePath, "utf8") : "";
|
|
9502
|
+
const withEvidence = replaceEvidenceBlock(existing, result.blockInner);
|
|
9503
|
+
const tokensAddition = collectTokensAddition({
|
|
9504
|
+
phase: phaseOverride ?? derivePhase(),
|
|
9505
|
+
role: roleOverride ?? "Developer",
|
|
9506
|
+
date: isoToday(),
|
|
9507
|
+
transcriptPath,
|
|
9508
|
+
modelOverride
|
|
9509
|
+
});
|
|
9510
|
+
const updated = writeTokensBlock(withEvidence, tokensAddition);
|
|
8600
9511
|
writeFileSync10(writePath, updated);
|
|
8601
|
-
process.stdout.write(`Wrote AEG:EVIDENCE
|
|
9512
|
+
process.stdout.write(`Wrote AEG:EVIDENCE and AEG:TOKENS blocks to ${writePath}
|
|
8602
9513
|
`);
|
|
8603
9514
|
} else {
|
|
8604
9515
|
process.stdout.write(`${result.block}
|
|
@@ -8607,13 +9518,187 @@ async function prReportCommand(args) {
|
|
|
8607
9518
|
process.exit(result.gatesFailed ? 1 : 0);
|
|
8608
9519
|
}
|
|
8609
9520
|
|
|
9521
|
+
// src/commands/pr-verify-evidence.ts
|
|
9522
|
+
import { execFileSync as execFileSync15 } from "node:child_process";
|
|
9523
|
+
|
|
9524
|
+
// src/commands/pr-verify-evidence-logic.ts
|
|
9525
|
+
function publishedMergeBase(region) {
|
|
9526
|
+
const m = region.match(/^[ \t]*`git diff ([0-9a-f]{7,40})\.\.\.[0-9a-f]{7,40} --numstat`[ \t]*$/m);
|
|
9527
|
+
return m ? m[1] : null;
|
|
9528
|
+
}
|
|
9529
|
+
function normaliseLines(region, repoRoot2) {
|
|
9530
|
+
const root = repoRoot2.replace(/\/+$/, "");
|
|
9531
|
+
const out = [];
|
|
9532
|
+
for (const raw of region.split(`
|
|
9533
|
+
`)) {
|
|
9534
|
+
const localStripped = root ? raw.split(`${root}/`).join("").split(root).join("") : raw;
|
|
9535
|
+
const stripped = localStripped.replace(/(^|\s)\/\S*?\/(?=(?:apps|packages|aeg-root|scripts|\.changeset|\.vinaya|\.github)\/)/g, "$1");
|
|
9536
|
+
const line = stripped.replace(/[\u0000-\u001F\u007F-\u009F\u2028\u2029\u202A-\u202E\u2066-\u2069]/g, " ").trim();
|
|
9537
|
+
if (line)
|
|
9538
|
+
out.push(line);
|
|
9539
|
+
}
|
|
9540
|
+
return out;
|
|
9541
|
+
}
|
|
9542
|
+
function compareEvidence(published, freshRegion, repoRoot2) {
|
|
9543
|
+
if (published === null)
|
|
9544
|
+
return { status: "no-block" };
|
|
9545
|
+
if (published === "hidden")
|
|
9546
|
+
return { status: "hidden" };
|
|
9547
|
+
const publishedLines = normaliseLines(published.region, repoRoot2);
|
|
9548
|
+
const freshLines = normaliseLines(freshRegion, repoRoot2);
|
|
9549
|
+
const missing = subtractOnce(freshLines, publishedLines);
|
|
9550
|
+
const unexpected = subtractOnce(publishedLines, freshLines);
|
|
9551
|
+
if (missing.length === 0 && unexpected.length === 0)
|
|
9552
|
+
return { status: "match" };
|
|
9553
|
+
const publishedBase = publishedMergeBase(published.region);
|
|
9554
|
+
const currentBase = publishedMergeBase(freshRegion);
|
|
9555
|
+
const baseDrift = publishedBase && currentBase && publishedBase !== currentBase ? { publishedBase, currentBase } : undefined;
|
|
9556
|
+
return baseDrift ? { status: "differs", missing, unexpected, baseDrift } : { status: "differs", missing, unexpected };
|
|
9557
|
+
}
|
|
9558
|
+
function subtractOnce(a, b) {
|
|
9559
|
+
const counts = new Map;
|
|
9560
|
+
for (const line of b)
|
|
9561
|
+
counts.set(line, (counts.get(line) ?? 0) + 1);
|
|
9562
|
+
const out = [];
|
|
9563
|
+
for (const line of a) {
|
|
9564
|
+
const n = counts.get(line) ?? 0;
|
|
9565
|
+
if (n > 0)
|
|
9566
|
+
counts.set(line, n - 1);
|
|
9567
|
+
else
|
|
9568
|
+
out.push(line);
|
|
9569
|
+
}
|
|
9570
|
+
return out;
|
|
9571
|
+
}
|
|
9572
|
+
function renderVerdict(verdict) {
|
|
9573
|
+
if (verdict.status === "match") {
|
|
9574
|
+
return "pr verify-evidence: MATCH — every line of the published AEG:EVIDENCE region is reproduced by a fresh run at this head, and vice versa (compared as a multiset of repo-relative, control-stripped lines: order and whitespace are not compared).";
|
|
9575
|
+
}
|
|
9576
|
+
if (verdict.status === "no-block") {
|
|
9577
|
+
return "pr verify-evidence: NO BLOCK — this pull request body carries no AEG:EVIDENCE anchors. Run `vinaya pr report --write <body-file>` to generate one.";
|
|
9578
|
+
}
|
|
9579
|
+
if (verdict.status === "hidden") {
|
|
9580
|
+
return [
|
|
9581
|
+
"pr verify-evidence: HIDDEN — the AEG:EVIDENCE pair sits inside a collapsed `<details>` block.",
|
|
9582
|
+
"`body-bare-digits` blanks every digit in there, so nothing can verify what it claims. Move the block out of `<details>` and regenerate it with `vinaya pr report --write <body-file>`."
|
|
9583
|
+
].join(`
|
|
9584
|
+
`);
|
|
9585
|
+
}
|
|
9586
|
+
const lines = ["pr verify-evidence: DIFFERS — the published AEG:EVIDENCE region does not reproduce at this head."];
|
|
9587
|
+
if (verdict.baseDrift) {
|
|
9588
|
+
lines.push("", `The published block also records a different merge-base (${verdict.baseDrift.publishedBase}) than the one resolving now (${verdict.baseDrift.currentBase}).`, "That explains SOME differences — the gates ran over a different diff — but it is not on its own evidence the content was honest, and the differing lines are still listed below. Rebase or regenerate, then re-run this command to compare at a shared base.");
|
|
9589
|
+
} else {
|
|
9590
|
+
lines.push("No merge-base difference was detected — either the base is unchanged, or one side carries no readable Group A command line to compare. Drift therefore does not account for this difference.");
|
|
9591
|
+
}
|
|
9592
|
+
lines.push("", "Regenerate with `vinaya pr report --write <body-file>` and push.");
|
|
9593
|
+
if (verdict.missing.length > 0) {
|
|
9594
|
+
lines.push("", `Present in a fresh run, absent from the published block (${verdict.missing.length}):`);
|
|
9595
|
+
for (const line of verdict.missing)
|
|
9596
|
+
lines.push(` + ${line}`);
|
|
9597
|
+
}
|
|
9598
|
+
if (verdict.unexpected.length > 0) {
|
|
9599
|
+
lines.push("", `Published but not reproducible (${verdict.unexpected.length}):`);
|
|
9600
|
+
for (const line of verdict.unexpected)
|
|
9601
|
+
lines.push(` - ${line}`);
|
|
9602
|
+
}
|
|
9603
|
+
return lines.join(`
|
|
9604
|
+
`);
|
|
9605
|
+
}
|
|
9606
|
+
|
|
9607
|
+
// src/commands/pr-verify-evidence.ts
|
|
9608
|
+
function gh(args) {
|
|
9609
|
+
return execFileSync15("gh", args, { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
|
|
9610
|
+
}
|
|
9611
|
+
function assertCleanWorktree() {
|
|
9612
|
+
const dirty = execFileSync15("git", ["status", "--porcelain"], {
|
|
9613
|
+
encoding: "utf8",
|
|
9614
|
+
cwd: repoRoot2()
|
|
9615
|
+
}).trim();
|
|
9616
|
+
if (!dirty)
|
|
9617
|
+
return;
|
|
9618
|
+
process.stderr.write(`pr verify-evidence: REFUSED — the worktree has uncommitted or untracked changes.
|
|
9619
|
+
` + `Group B is regenerated by diff-scoped checks, so local changes alter which files are scanned and can
|
|
9620
|
+
` + `produce a MATCH that a clean checkout would not. Verify from a clean checkout at this head instead.
|
|
9621
|
+
|
|
9622
|
+
` + `${dirty}
|
|
9623
|
+
`);
|
|
9624
|
+
process.exit(2);
|
|
9625
|
+
}
|
|
9626
|
+
function assertNoBaseOverride() {
|
|
9627
|
+
const override = process.env.BASE_SHA;
|
|
9628
|
+
if (!override)
|
|
9629
|
+
return;
|
|
9630
|
+
process.stderr.write(`pr verify-evidence: REFUSED — BASE_SHA is set (${override}).
|
|
9631
|
+
` + `The regeneration resolves its merge-base from that variable, so an override changes what a "fresh run"
|
|
9632
|
+
` + `means and can turn a drifted comparison into an apparently shared-base one. Unset it and re-run.
|
|
9633
|
+
`);
|
|
9634
|
+
process.exit(2);
|
|
9635
|
+
}
|
|
9636
|
+
function assertRepoRootCwd() {
|
|
9637
|
+
const root = repoRoot2();
|
|
9638
|
+
const cwd = process.cwd();
|
|
9639
|
+
if (cwd === root)
|
|
9640
|
+
return;
|
|
9641
|
+
process.stderr.write(`pr verify-evidence: REFUSED — run this from the repository root (${root}), not ${cwd}.
|
|
9642
|
+
` + `The regeneration inherits this working directory, and several gates resolve their scan root from it,
|
|
9643
|
+
` + `so a subdirectory silently narrows what the fresh run inspects and can report MATCH on a block that
|
|
9644
|
+
` + `omits real findings.
|
|
9645
|
+
`);
|
|
9646
|
+
process.exit(2);
|
|
9647
|
+
}
|
|
9648
|
+
function repoRoot2() {
|
|
9649
|
+
return execFileSync15("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
9650
|
+
}
|
|
9651
|
+
async function prVerifyEvidenceCommand(args) {
|
|
9652
|
+
const prRef = args[0] && !args[0].startsWith("-") ? args[0] : undefined;
|
|
9653
|
+
if (!prRef) {
|
|
9654
|
+
process.stderr.write(`Usage: vinaya pr verify-evidence <pr-number>
|
|
9655
|
+
`);
|
|
9656
|
+
process.exit(2);
|
|
9657
|
+
}
|
|
9658
|
+
assertRepoRootCwd();
|
|
9659
|
+
assertCleanWorktree();
|
|
9660
|
+
assertNoBaseOverride();
|
|
9661
|
+
let body;
|
|
9662
|
+
let forgeHead;
|
|
9663
|
+
try {
|
|
9664
|
+
const view = JSON.parse(gh(["pr", "view", prRef, "--json", "body,headRefOid"]));
|
|
9665
|
+
body = view.body ?? "";
|
|
9666
|
+
forgeHead = view.headRefOid ?? "";
|
|
9667
|
+
} catch (err) {
|
|
9668
|
+
process.stderr.write(`pr verify-evidence: could not read pull request #${prRef} — ${err.message}
|
|
9669
|
+
`);
|
|
9670
|
+
process.exit(1);
|
|
9671
|
+
return;
|
|
9672
|
+
}
|
|
9673
|
+
const localHead = execFileSync15("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim();
|
|
9674
|
+
if (!forgeHead) {
|
|
9675
|
+
process.stderr.write(`pr verify-evidence: REFUSED — could not read #${prRef}'s head from the forge, so the verdict cannot be bound to a commit.
|
|
9676
|
+
`);
|
|
9677
|
+
process.exit(2);
|
|
9678
|
+
}
|
|
9679
|
+
if (forgeHead !== localHead) {
|
|
9680
|
+
process.stderr.write(`pr verify-evidence: REFUSED — this checkout is at ${localHead}, but #${prRef}'s head is ${forgeHead}.
|
|
9681
|
+
` + `Comparing against a different tree produces a difference that is real and uninteresting. Check out the head first.
|
|
9682
|
+
`);
|
|
9683
|
+
process.exit(2);
|
|
9684
|
+
}
|
|
9685
|
+
const fresh = await buildReport();
|
|
9686
|
+
const resolved = resolveAnchoredRegion(ScanContext.from(body), "EVIDENCE");
|
|
9687
|
+
const verdict = compareEvidence(resolved === null || resolved === "hidden" ? resolved : { region: resolved.region }, fresh.blockInner, repoRoot2());
|
|
9688
|
+
process.stdout.write(`${renderVerdict(verdict)}
|
|
9689
|
+
`);
|
|
9690
|
+
process.stdout.write(` compared at head ${localHead}
|
|
9691
|
+
`);
|
|
9692
|
+
process.exit(verdict.status === "match" ? 0 : 1);
|
|
9693
|
+
}
|
|
9694
|
+
|
|
8610
9695
|
// src/commands/quickstart.ts
|
|
8611
|
-
import { execFileSync as
|
|
8612
|
-
import { existsSync as existsSync21, readFileSync as
|
|
9696
|
+
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
9697
|
+
import { existsSync as existsSync21, readFileSync as readFileSync24 } from "node:fs";
|
|
8613
9698
|
import { join as join23 } from "node:path";
|
|
8614
9699
|
|
|
8615
9700
|
// src/lib/doc-owners-write.ts
|
|
8616
|
-
import { existsSync as existsSync20, mkdirSync as mkdirSync7, readFileSync as
|
|
9701
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync7, readFileSync as readFileSync23, writeFileSync as writeFileSync11 } from "node:fs";
|
|
8617
9702
|
import { dirname as dirname7, join as join22 } from "node:path";
|
|
8618
9703
|
function bindingLine(glob, pointer) {
|
|
8619
9704
|
return `${glob} ${pointer}`;
|
|
@@ -8629,23 +9714,23 @@ function appendDocOwnersBinding(content, glob, pointer) {
|
|
|
8629
9714
|
return `${content}${sep4}${bindingLine(glob, pointer)}
|
|
8630
9715
|
`;
|
|
8631
9716
|
}
|
|
8632
|
-
function planDocOwnersBinding(
|
|
8633
|
-
const abs2 = join22(
|
|
9717
|
+
function planDocOwnersBinding(repoRoot3, glob, pointer) {
|
|
9718
|
+
const abs2 = join22(repoRoot3, DOC_OWNERS_PATH);
|
|
8634
9719
|
const line = bindingLine(glob, pointer);
|
|
8635
9720
|
if (!existsSync20(abs2)) {
|
|
8636
9721
|
return { action: "create-host", line, path: DOC_OWNERS_PATH };
|
|
8637
9722
|
}
|
|
8638
|
-
const existing =
|
|
9723
|
+
const existing = readFileSync23(abs2, "utf-8");
|
|
8639
9724
|
const already = parseDocOwners(existing).bindings.some((b) => b.glob === glob);
|
|
8640
9725
|
return { action: already ? "skip-present" : "append-row", line, path: DOC_OWNERS_PATH };
|
|
8641
9726
|
}
|
|
8642
|
-
function applyDocOwnersBinding(
|
|
8643
|
-
const abs2 = join22(
|
|
9727
|
+
function applyDocOwnersBinding(repoRoot3, plan, glob, pointer) {
|
|
9728
|
+
const abs2 = join22(repoRoot3, DOC_OWNERS_PATH);
|
|
8644
9729
|
if (plan.action === "create-host") {
|
|
8645
9730
|
mkdirSync7(dirname7(abs2), { recursive: true });
|
|
8646
9731
|
writeFileSync11(abs2, freshDocOwners(glob, pointer), "utf-8");
|
|
8647
9732
|
} else if (plan.action === "append-row") {
|
|
8648
|
-
const existing =
|
|
9733
|
+
const existing = readFileSync23(abs2, "utf-8");
|
|
8649
9734
|
writeFileSync11(abs2, appendDocOwnersBinding(existing, glob, pointer), "utf-8");
|
|
8650
9735
|
}
|
|
8651
9736
|
}
|
|
@@ -8705,13 +9790,13 @@ var GLOB_INJECTION_RE = /[\s\r\n]/;
|
|
|
8705
9790
|
var POINTER_INJECTION_RE = /[\r\n]/;
|
|
8706
9791
|
var NAME_INJECTION_RE = /[\s|\r\n]/;
|
|
8707
9792
|
var PATH_INJECTION_RE2 = /[|\r\n]/;
|
|
8708
|
-
function pointerIsMissingLocalFile(
|
|
9793
|
+
function pointerIsMissingLocalFile(repoRoot3, pointer) {
|
|
8709
9794
|
if (/^https?:\/\//i.test(pointer))
|
|
8710
9795
|
return false;
|
|
8711
9796
|
const path = pointer.split("#")[0] ?? pointer;
|
|
8712
|
-
return !existsSync21(join23(
|
|
9797
|
+
return !existsSync21(join23(repoRoot3, path));
|
|
8713
9798
|
}
|
|
8714
|
-
async function bindDocOwnerLoop(deps,
|
|
9799
|
+
async function bindDocOwnerLoop(deps, repoRoot3) {
|
|
8715
9800
|
let any = false;
|
|
8716
9801
|
for (;; ) {
|
|
8717
9802
|
if (any && !await deps.confirm("Bind another doc→code pair?", false))
|
|
@@ -8726,11 +9811,11 @@ async function bindDocOwnerLoop(deps, repoRoot2) {
|
|
|
8726
9811
|
}
|
|
8727
9812
|
if (GLOB_INJECTION_RE.test(glob) || POINTER_INJECTION_RE.test(pointer)) {
|
|
8728
9813
|
console.error(red("Error: glob must not contain whitespace, and the pointer must not contain a newline."));
|
|
8729
|
-
} else if (pointerIsMissingLocalFile(
|
|
9814
|
+
} else if (pointerIsMissingLocalFile(repoRoot3, pointer)) {
|
|
8730
9815
|
console.error(red(`✗ "${pointer}" does not exist in this repo (and isn't a URL).`));
|
|
8731
9816
|
} else {
|
|
8732
|
-
const plan = planDocOwnersBinding(
|
|
8733
|
-
applyDocOwnersBinding(
|
|
9817
|
+
const plan = planDocOwnersBinding(repoRoot3, glob, pointer);
|
|
9818
|
+
applyDocOwnersBinding(repoRoot3, plan, glob, pointer);
|
|
8734
9819
|
process.stdout.write(`${green(renderDocOwnersBindingDiffLine(plan))}
|
|
8735
9820
|
`);
|
|
8736
9821
|
any = true;
|
|
@@ -8776,10 +9861,10 @@ async function registerProjectLoop(deps) {
|
|
|
8776
9861
|
}
|
|
8777
9862
|
}
|
|
8778
9863
|
function readPackageVersion() {
|
|
8779
|
-
const pkg = JSON.parse(
|
|
9864
|
+
const pkg = JSON.parse(readFileSync24(join23(packageRoot(import.meta.url), "package.json"), "utf-8"));
|
|
8780
9865
|
return pkg.version;
|
|
8781
9866
|
}
|
|
8782
|
-
function
|
|
9867
|
+
function realDeps7() {
|
|
8783
9868
|
return {
|
|
8784
9869
|
detectRepo: detectGitRepo,
|
|
8785
9870
|
initDeps: {
|
|
@@ -8800,20 +9885,15 @@ function realDeps6() {
|
|
|
8800
9885
|
nodeVersion: () => process.version,
|
|
8801
9886
|
bunVersion: () => typeof Bun === "undefined" ? null : Bun.version,
|
|
8802
9887
|
packageVersion: readPackageVersion,
|
|
8803
|
-
meteringCapability: () => resolveMeteringCapability(
|
|
8804
|
-
env: process.env,
|
|
8805
|
-
cwd: process.cwd(),
|
|
8806
|
-
exists: existsSync21,
|
|
8807
|
-
readFile: (path) => readFileSync23(path, "utf8")
|
|
8808
|
-
})
|
|
9888
|
+
meteringCapability: () => resolveMeteringCapability(hardenedMeteringDeps())
|
|
8809
9889
|
},
|
|
8810
9890
|
confirm: async (q, defaultYes) => promptYesNo(q, defaultYes),
|
|
8811
9891
|
ask: async (q) => prompt(q),
|
|
8812
9892
|
closeStdin
|
|
8813
9893
|
};
|
|
8814
9894
|
}
|
|
8815
|
-
function execGit(
|
|
8816
|
-
return
|
|
9895
|
+
function execGit(repoRoot3, args) {
|
|
9896
|
+
return execFileSync16("git", args, { cwd: repoRoot3, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
8817
9897
|
}
|
|
8818
9898
|
function errorDetail(err) {
|
|
8819
9899
|
const stderr = err.stderr;
|
|
@@ -8821,37 +9901,37 @@ function errorDetail(err) {
|
|
|
8821
9901
|
return stderr.toString().trim();
|
|
8822
9902
|
return err instanceof Error ? err.message : String(err);
|
|
8823
9903
|
}
|
|
8824
|
-
function gitStatusShort(
|
|
8825
|
-
return execGit(
|
|
9904
|
+
function gitStatusShort(repoRoot3) {
|
|
9905
|
+
return execGit(repoRoot3, ["status", "--short"]);
|
|
8826
9906
|
}
|
|
8827
|
-
function commitInstall(
|
|
8828
|
-
execGit(
|
|
8829
|
-
const staged = execGit(
|
|
9907
|
+
function commitInstall(repoRoot3) {
|
|
9908
|
+
execGit(repoRoot3, ["add", "-A"]);
|
|
9909
|
+
const staged = execGit(repoRoot3, ["diff", "--cached", "--stat"]);
|
|
8830
9910
|
if (!staged)
|
|
8831
9911
|
return { committed: false, message: "Nothing to commit — working tree already clean." };
|
|
8832
9912
|
try {
|
|
8833
|
-
execGit(
|
|
9913
|
+
execGit(repoRoot3, ["commit", "-m", "Chore: install Vinaya"]);
|
|
8834
9914
|
return { committed: true, message: "✓ Committed `Chore: install Vinaya`." };
|
|
8835
9915
|
} catch (err) {
|
|
8836
9916
|
return { committed: false, message: `git commit failed: ${errorDetail(err)}` };
|
|
8837
9917
|
}
|
|
8838
9918
|
}
|
|
8839
|
-
function pushInstall(
|
|
9919
|
+
function pushInstall(repoRoot3) {
|
|
8840
9920
|
try {
|
|
8841
|
-
const branch = execGit(
|
|
9921
|
+
const branch = execGit(repoRoot3, ["symbolic-ref", "--short", "HEAD"]);
|
|
8842
9922
|
let hasUpstream = true;
|
|
8843
9923
|
try {
|
|
8844
|
-
execGit(
|
|
9924
|
+
execGit(repoRoot3, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
|
|
8845
9925
|
} catch {
|
|
8846
9926
|
hasUpstream = false;
|
|
8847
9927
|
}
|
|
8848
9928
|
if (hasUpstream) {
|
|
8849
|
-
const ahead = execGit(
|
|
9929
|
+
const ahead = execGit(repoRoot3, ["rev-list", "--count", "@{u}..HEAD"]);
|
|
8850
9930
|
if (ahead === "0") {
|
|
8851
9931
|
return { status: "up-to-date", message: "Nothing to push — already up to date with the remote." };
|
|
8852
9932
|
}
|
|
8853
9933
|
}
|
|
8854
|
-
execGit(
|
|
9934
|
+
execGit(repoRoot3, hasUpstream ? ["push"] : ["push", "-u", "origin", branch]);
|
|
8855
9935
|
return { status: "pushed", message: "✓ Pushed." };
|
|
8856
9936
|
} catch (err) {
|
|
8857
9937
|
return { status: "failed", message: `git push failed: ${errorDetail(err)}` };
|
|
@@ -8943,12 +10023,12 @@ ${bold("Next steps:")}
|
|
|
8943
10023
|
}
|
|
8944
10024
|
}
|
|
8945
10025
|
async function quickstartCommand(args) {
|
|
8946
|
-
process.exit(await runQuickstart(args,
|
|
10026
|
+
process.exit(await runQuickstart(args, realDeps7()));
|
|
8947
10027
|
}
|
|
8948
10028
|
|
|
8949
10029
|
// src/commands/review-post.ts
|
|
8950
|
-
import { execFileSync as
|
|
8951
|
-
import { readFileSync as
|
|
10030
|
+
import { execFileSync as execFileSync17 } from "node:child_process";
|
|
10031
|
+
import { readFileSync as readFileSync25, rmSync as rmSync4, writeFileSync as writeFileSync12 } from "node:fs";
|
|
8952
10032
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8953
10033
|
import { join as join24 } from "node:path";
|
|
8954
10034
|
class FindingsParseError extends Error {
|
|
@@ -9130,7 +10210,7 @@ function readFindingsFile(path, allowedSeverities) {
|
|
|
9130
10210
|
return [];
|
|
9131
10211
|
let content;
|
|
9132
10212
|
try {
|
|
9133
|
-
content =
|
|
10213
|
+
content = readFileSync25(path, "utf8");
|
|
9134
10214
|
} catch {
|
|
9135
10215
|
refuseCmd(`Could not read findings file at ${path}.`, "Check the path and re-run.");
|
|
9136
10216
|
}
|
|
@@ -9159,9 +10239,9 @@ function normalizeSecurityVerdict(raw) {
|
|
|
9159
10239
|
return "FAIL";
|
|
9160
10240
|
refuseCmd(`\`--verdict ${raw}\` is not PASS or FAIL.`, "Pass `--verdict PASS` or `--verdict FAIL`.");
|
|
9161
10241
|
}
|
|
9162
|
-
function
|
|
10242
|
+
function gh2(args) {
|
|
9163
10243
|
try {
|
|
9164
|
-
return
|
|
10244
|
+
return execFileSync17("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
9165
10245
|
} catch (err) {
|
|
9166
10246
|
const stderr = err.stderr;
|
|
9167
10247
|
throw new Error(String(stderr ?? err.message).trim() || "gh command failed");
|
|
@@ -9170,7 +10250,7 @@ function gh(args) {
|
|
|
9170
10250
|
function resolveHeadSha(pr) {
|
|
9171
10251
|
let out;
|
|
9172
10252
|
try {
|
|
9173
|
-
out =
|
|
10253
|
+
out = gh2(["pr", "view", pr, "--json", "headRefOid", "-q", ".headRefOid"]);
|
|
9174
10254
|
} catch (err) {
|
|
9175
10255
|
refuseCmd(`Could not resolve PR ${pr}'s head via \`gh pr view --json headRefOid\`: ${err instanceof Error ? err.message : String(err)}`, "Confirm `gh auth status` passes and the PR number is correct, then re-run.");
|
|
9176
10256
|
}
|
|
@@ -9183,7 +10263,7 @@ function postComment(pr, body) {
|
|
|
9183
10263
|
const tmp = join24(tmpdir2(), `vinaya-review-post-${process.pid}-${Date.now()}.md`);
|
|
9184
10264
|
writeFileSync12(tmp, body);
|
|
9185
10265
|
try {
|
|
9186
|
-
return
|
|
10266
|
+
return gh2(["pr", "comment", pr, "--body-file", tmp]).trim();
|
|
9187
10267
|
} catch (err) {
|
|
9188
10268
|
refuseCmd(`Rendered comment failed to post via \`gh pr comment\`: ${err instanceof Error ? err.message : String(err)}`, "Check `gh auth status`/network, then re-run — nothing was posted.");
|
|
9189
10269
|
} finally {
|
|
@@ -9191,7 +10271,7 @@ function postComment(pr, body) {
|
|
|
9191
10271
|
}
|
|
9192
10272
|
}
|
|
9193
10273
|
function fetchComments(pr) {
|
|
9194
|
-
const out =
|
|
10274
|
+
const out = gh2(["pr", "view", pr, "--json", "comments"]);
|
|
9195
10275
|
const parsed = JSON.parse(out);
|
|
9196
10276
|
return parsed.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null }));
|
|
9197
10277
|
}
|
|
@@ -9299,7 +10379,7 @@ async function reviewPostCommand(args) {
|
|
|
9299
10379
|
let secretsEvidence = null;
|
|
9300
10380
|
if (secretsEvidenceFile) {
|
|
9301
10381
|
try {
|
|
9302
|
-
secretsEvidence =
|
|
10382
|
+
secretsEvidence = readFileSync25(secretsEvidenceFile, "utf8");
|
|
9303
10383
|
} catch {
|
|
9304
10384
|
refuseCmd(`Could not read secrets evidence file at ${secretsEvidenceFile}.`, "Check the path and re-run.");
|
|
9305
10385
|
}
|
|
@@ -9340,7 +10420,7 @@ Self-verification: clean — re-parsed VERDICT is bound to head ${headSha}.
|
|
|
9340
10420
|
|
|
9341
10421
|
// src/commands/studio.ts
|
|
9342
10422
|
import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
|
|
9343
|
-
import { existsSync as existsSync22, readFileSync as
|
|
10423
|
+
import { existsSync as existsSync22, readFileSync as readFileSync26, renameSync } from "node:fs";
|
|
9344
10424
|
import net from "node:net";
|
|
9345
10425
|
import { dirname as dirname8, join as join25 } from "node:path";
|
|
9346
10426
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -9399,7 +10479,7 @@ function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
|
|
|
9399
10479
|
const pkgPath = join25(webDir, "package.json");
|
|
9400
10480
|
if (existsSync22(pkgPath)) {
|
|
9401
10481
|
try {
|
|
9402
|
-
const pkg = JSON.parse(
|
|
10482
|
+
const pkg = JSON.parse(readFileSync26(pkgPath, "utf-8"));
|
|
9403
10483
|
if (pkg.name === "@atta/vinaya-studio-web") {
|
|
9404
10484
|
return { kind: "workspace", webDir };
|
|
9405
10485
|
}
|
|
@@ -9419,16 +10499,16 @@ function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
|
|
|
9419
10499
|
return { kind: "missing" };
|
|
9420
10500
|
}
|
|
9421
10501
|
function spawnDev(webDir, args) {
|
|
9422
|
-
return new Promise((
|
|
10502
|
+
return new Promise((resolve3) => {
|
|
9423
10503
|
const child = spawn2("bun", ["run", "dev", ...args], { cwd: webDir, stdio: "inherit" });
|
|
9424
|
-
child.on("exit", (code) =>
|
|
10504
|
+
child.on("exit", (code) => resolve3(code ?? 0));
|
|
9425
10505
|
});
|
|
9426
10506
|
}
|
|
9427
10507
|
function isPortFree(port) {
|
|
9428
|
-
return new Promise((
|
|
10508
|
+
return new Promise((resolve3) => {
|
|
9429
10509
|
const tester = net.createServer();
|
|
9430
|
-
tester.once("error", () =>
|
|
9431
|
-
tester.once("listening", () => tester.close(() =>
|
|
10510
|
+
tester.once("error", () => resolve3(false));
|
|
10511
|
+
tester.once("listening", () => tester.close(() => resolve3(true)));
|
|
9432
10512
|
tester.listen(port, "127.0.0.1");
|
|
9433
10513
|
});
|
|
9434
10514
|
}
|
|
@@ -9459,9 +10539,9 @@ async function spawnStandalone(cwd, serverPath, bundleRoot, explicitPort) {
|
|
|
9459
10539
|
const env = { HOSTNAME: "127.0.0.1", ...process.env, PORT: String(port), VINAYA_REPO_ROOT: cwd };
|
|
9460
10540
|
if (repo)
|
|
9461
10541
|
env.AEG_REPO = `${repo.owner}/${repo.repo}`;
|
|
9462
|
-
return new Promise((
|
|
10542
|
+
return new Promise((resolve3) => {
|
|
9463
10543
|
const child = spawn2("node", [serverPath], { cwd, stdio: "inherit", env });
|
|
9464
|
-
child.on("exit", (code) =>
|
|
10544
|
+
child.on("exit", (code) => resolve3(code ?? 0));
|
|
9465
10545
|
});
|
|
9466
10546
|
}
|
|
9467
10547
|
async function runStudio(cwd, args, moduleUrl = import.meta.url) {
|
|
@@ -9493,126 +10573,8 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
|
|
|
9493
10573
|
}
|
|
9494
10574
|
}
|
|
9495
10575
|
|
|
9496
|
-
// src/commands/tokens.ts
|
|
9497
|
-
import { existsSync as existsSync23, readFileSync as readFileSync26 } from "node:fs";
|
|
9498
|
-
var USAGE4 = [
|
|
9499
|
-
'Usage: vinaya tokens --phase "<task-id>: develop" --role Developer',
|
|
9500
|
-
" [--model <id>] [--transcript <path>] [--in <tokens-in> --out <tokens-out>]",
|
|
9501
|
-
"",
|
|
9502
|
-
" --transcript Read this transcript directly. Supported primary route — use it whenever you know",
|
|
9503
|
-
" which transcript is yours, and always in a repo with no track-transcript.sh hook.",
|
|
9504
|
-
" (omitted) Resolve via the Stop-hook pointer file, if this repo installs that hook.",
|
|
9505
|
-
" --in/--out Manual entry: the exact token figures, for a host whose usage arrives by some other",
|
|
9506
|
-
" means than a Claude Code transcript. Both required together; skips transcript",
|
|
9507
|
-
" resolution entirely."
|
|
9508
|
-
].join(`
|
|
9509
|
-
`);
|
|
9510
|
-
function parseArgs(argv) {
|
|
9511
|
-
let phase;
|
|
9512
|
-
let role;
|
|
9513
|
-
let model;
|
|
9514
|
-
let transcriptPath;
|
|
9515
|
-
let tokensInRaw;
|
|
9516
|
-
let tokensOutRaw;
|
|
9517
|
-
for (let i = 0;i < argv.length; i++) {
|
|
9518
|
-
const arg = argv[i];
|
|
9519
|
-
if (arg === "--phase")
|
|
9520
|
-
phase = argv[++i];
|
|
9521
|
-
else if (arg === "--role")
|
|
9522
|
-
role = argv[++i];
|
|
9523
|
-
else if (arg === "--model")
|
|
9524
|
-
model = argv[++i];
|
|
9525
|
-
else if (arg === "--transcript")
|
|
9526
|
-
transcriptPath = argv[++i];
|
|
9527
|
-
else if (arg === "--in")
|
|
9528
|
-
tokensInRaw = argv[++i];
|
|
9529
|
-
else if (arg === "--out")
|
|
9530
|
-
tokensOutRaw = argv[++i];
|
|
9531
|
-
}
|
|
9532
|
-
if (!phase || !role) {
|
|
9533
|
-
throw new Error(USAGE4);
|
|
9534
|
-
}
|
|
9535
|
-
if (tokensInRaw === undefined !== (tokensOutRaw === undefined)) {
|
|
9536
|
-
throw new Error("vinaya tokens: --in and --out must be given together, or not at all.");
|
|
9537
|
-
}
|
|
9538
|
-
const tokensIn = tokensInRaw === undefined ? undefined : Number(tokensInRaw);
|
|
9539
|
-
const tokensOut = tokensOutRaw === undefined ? undefined : Number(tokensOutRaw);
|
|
9540
|
-
if (tokensIn !== undefined && (!Number.isFinite(tokensIn) || tokensIn < 0)) {
|
|
9541
|
-
throw new Error(`vinaya tokens: --in must be a non-negative integer, got "${tokensInRaw}".`);
|
|
9542
|
-
}
|
|
9543
|
-
if (tokensOut !== undefined && (!Number.isFinite(tokensOut) || tokensOut < 0)) {
|
|
9544
|
-
throw new Error(`vinaya tokens: --out must be a non-negative integer, got "${tokensOutRaw}".`);
|
|
9545
|
-
}
|
|
9546
|
-
return { phase, role, model, transcriptPath, tokensIn, tokensOut };
|
|
9547
|
-
}
|
|
9548
|
-
function manualSummary(tokensIn, tokensOut) {
|
|
9549
|
-
return {
|
|
9550
|
-
components: {
|
|
9551
|
-
inputTokens: tokensIn,
|
|
9552
|
-
outputTokens: tokensOut,
|
|
9553
|
-
cacheCreationInputTokens: 0,
|
|
9554
|
-
cacheReadInputTokens: 0
|
|
9555
|
-
},
|
|
9556
|
-
model: null,
|
|
9557
|
-
messageCount: 1
|
|
9558
|
-
};
|
|
9559
|
-
}
|
|
9560
|
-
function realDeps7() {
|
|
9561
|
-
return {
|
|
9562
|
-
env: process.env,
|
|
9563
|
-
cwd: process.cwd(),
|
|
9564
|
-
exists: existsSync23,
|
|
9565
|
-
readFile: (path) => readFileSync26(path, "utf8")
|
|
9566
|
-
};
|
|
9567
|
-
}
|
|
9568
|
-
function buildTokensResult(parsed, deps) {
|
|
9569
|
-
if (parsed.tokensIn !== undefined && parsed.tokensOut !== undefined) {
|
|
9570
|
-
const summary = manualSummary(parsed.tokensIn, parsed.tokensOut);
|
|
9571
|
-
return {
|
|
9572
|
-
line: formatTokensLine({ phase: parsed.phase, role: parsed.role, summary, modelOverride: parsed.model }),
|
|
9573
|
-
breakdown: undefined
|
|
9574
|
-
};
|
|
9575
|
-
}
|
|
9576
|
-
const capability = resolveMeteringCapability(deps, parsed.transcriptPath);
|
|
9577
|
-
if (!capability.capable) {
|
|
9578
|
-
throw new Error(`vinaya tokens: refused — could not resolve real usage figures (${capability.reason}).
|
|
9579
|
-
${capability.detail}
|
|
9580
|
-
|
|
9581
|
-
${USAGE4}`);
|
|
9582
|
-
}
|
|
9583
|
-
return {
|
|
9584
|
-
line: formatTokensLine({
|
|
9585
|
-
phase: parsed.phase,
|
|
9586
|
-
role: parsed.role,
|
|
9587
|
-
summary: capability.summary,
|
|
9588
|
-
modelOverride: parsed.model
|
|
9589
|
-
}),
|
|
9590
|
-
breakdown: formatBreakdown(capability.summary)
|
|
9591
|
-
};
|
|
9592
|
-
}
|
|
9593
|
-
function tokensCommand(argv) {
|
|
9594
|
-
let parsed;
|
|
9595
|
-
try {
|
|
9596
|
-
parsed = parseArgs(argv);
|
|
9597
|
-
} catch (err) {
|
|
9598
|
-
console.error(err.message);
|
|
9599
|
-
process.exit(2);
|
|
9600
|
-
}
|
|
9601
|
-
let result;
|
|
9602
|
-
try {
|
|
9603
|
-
result = buildTokensResult(parsed, realDeps7());
|
|
9604
|
-
} catch (err) {
|
|
9605
|
-
console.error(err.message);
|
|
9606
|
-
process.exit(1);
|
|
9607
|
-
}
|
|
9608
|
-
if (result.breakdown)
|
|
9609
|
-
console.error(result.breakdown);
|
|
9610
|
-
process.stdout.write(`${result.line}
|
|
9611
|
-
`);
|
|
9612
|
-
}
|
|
9613
|
-
|
|
9614
10576
|
// src/commands/upgrade.ts
|
|
9615
|
-
import { existsSync as
|
|
10577
|
+
import { existsSync as existsSync23, readFileSync as readFileSync27, rmSync as rmSync5, writeFileSync as writeFileSync13 } from "node:fs";
|
|
9616
10578
|
import { join as join26 } from "node:path";
|
|
9617
10579
|
function realDeps8() {
|
|
9618
10580
|
return {
|
|
@@ -9630,9 +10592,9 @@ function realDeps8() {
|
|
|
9630
10592
|
function flags2(args) {
|
|
9631
10593
|
return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
|
|
9632
10594
|
}
|
|
9633
|
-
function readManifest3(
|
|
9634
|
-
const p = join26(
|
|
9635
|
-
if (!
|
|
10595
|
+
function readManifest3(repoRoot3) {
|
|
10596
|
+
const p = join26(repoRoot3, CONFIG_PATH);
|
|
10597
|
+
if (!existsSync23(p))
|
|
9636
10598
|
return { kind: "missing" };
|
|
9637
10599
|
let raw;
|
|
9638
10600
|
try {
|
|
@@ -9651,31 +10613,31 @@ function readManifest3(repoRoot2) {
|
|
|
9651
10613
|
return { kind: "not-initialized" };
|
|
9652
10614
|
return { kind: "ok", manifest: parsed.data.managed };
|
|
9653
10615
|
}
|
|
9654
|
-
function writeManifestVersion(
|
|
9655
|
-
const configAbs = join26(
|
|
10616
|
+
function writeManifestVersion(repoRoot3, manifest) {
|
|
10617
|
+
const configAbs = join26(repoRoot3, CONFIG_PATH);
|
|
9656
10618
|
const seed = JSON.parse(readFileSync27(configAbs, "utf-8"));
|
|
9657
10619
|
const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
|
|
9658
10620
|
writeFileSync13(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
|
|
9659
10621
|
`, "utf-8");
|
|
9660
10622
|
}
|
|
9661
|
-
function stripFor(
|
|
9662
|
-
const abs2 = resolveManagedBlockPath(
|
|
9663
|
-
if (!
|
|
10623
|
+
function stripFor(repoRoot3, path, marker, comment) {
|
|
10624
|
+
const abs2 = resolveManagedBlockPath(repoRoot3, path);
|
|
10625
|
+
if (!existsSync23(abs2))
|
|
9664
10626
|
return { path, marker, comment, present: false, removesHost: false };
|
|
9665
10627
|
const stripped = stripBlockFromContent(readFileSync27(abs2, "utf-8"), marker, comment);
|
|
9666
10628
|
if (stripped === null)
|
|
9667
10629
|
return { path, marker, comment, present: false, removesHost: false };
|
|
9668
10630
|
return { path, marker, comment, present: true, removesHost: blockStripLeavesEmpty(stripped) };
|
|
9669
10631
|
}
|
|
9670
|
-
function planHookRouting(
|
|
10632
|
+
function planHookRouting(repoRoot3, manifest, recorded, hooksPathValue) {
|
|
9671
10633
|
const none = { target: recorded, strips: [], arm: false, migratesManifest: false, blockedReason: null };
|
|
9672
10634
|
if (recorded === ".husky")
|
|
9673
10635
|
return none;
|
|
9674
10636
|
if (recorded === TRACKED_HOOK_DIR) {
|
|
9675
|
-
const strips = manifest.blocks.filter((b) => b.path.startsWith(`${TRACKED_HOOK_DIR}/`)).map((b) => stripFor(
|
|
10637
|
+
const strips = manifest.blocks.filter((b) => b.path.startsWith(`${TRACKED_HOOK_DIR}/`)).map((b) => stripFor(repoRoot3, `.git/hooks/${b.marker}`, b.marker, b.comment)).filter((s) => s.present);
|
|
9676
10638
|
if (hooksPathValue === TRACKED_HOOK_DIR)
|
|
9677
10639
|
return { ...none, strips };
|
|
9678
|
-
const foreign = foreignRawHooks(
|
|
10640
|
+
const foreign = foreignRawHooks(repoRoot3);
|
|
9679
10641
|
if (foreign.length > 0) {
|
|
9680
10642
|
return {
|
|
9681
10643
|
...none,
|
|
@@ -9688,8 +10650,8 @@ function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
|
|
|
9688
10650
|
const legacy = manifest.blocks.filter((b) => b.path.startsWith(".git/hooks/"));
|
|
9689
10651
|
const blocked = [];
|
|
9690
10652
|
for (const b of legacy) {
|
|
9691
|
-
const abs2 = resolveManagedBlockPath(
|
|
9692
|
-
if (!
|
|
10653
|
+
const abs2 = resolveManagedBlockPath(repoRoot3, b.path);
|
|
10654
|
+
if (!existsSync23(abs2))
|
|
9693
10655
|
continue;
|
|
9694
10656
|
const stripped = stripBlockFromContent(readFileSync27(abs2, "utf-8"), b.marker, b.comment);
|
|
9695
10657
|
if (stripped === null)
|
|
@@ -9698,7 +10660,7 @@ function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
|
|
|
9698
10660
|
blocked.push(`${b.path} carries your own lines alongside the managed block`);
|
|
9699
10661
|
}
|
|
9700
10662
|
const legacyNames = new Set(legacy.map((b) => b.path.slice(".git/hooks/".length)));
|
|
9701
|
-
for (const f of activeRawHooks(
|
|
10663
|
+
for (const f of activeRawHooks(repoRoot3)) {
|
|
9702
10664
|
if (!legacyNames.has(f))
|
|
9703
10665
|
blocked.push(`.git/hooks/${f} is an active raw hook vinaya does not manage`);
|
|
9704
10666
|
}
|
|
@@ -9706,7 +10668,7 @@ function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
|
|
|
9706
10668
|
return { ...none, blockedReason: blocked.join("; ") };
|
|
9707
10669
|
return {
|
|
9708
10670
|
target: TRACKED_HOOK_DIR,
|
|
9709
|
-
strips: legacy.map((b) => stripFor(
|
|
10671
|
+
strips: legacy.map((b) => stripFor(repoRoot3, b.path, b.marker, b.comment)).filter((s) => s.present),
|
|
9710
10672
|
arm: hooksPathValue !== TRACKED_HOOK_DIR,
|
|
9711
10673
|
migratesManifest: true,
|
|
9712
10674
|
blockedReason: null
|
|
@@ -9722,7 +10684,7 @@ function extractWorkflowTrigger(content) {
|
|
|
9722
10684
|
const match = content.match(/^on:\r?\n {2}([A-Za-z0-9_]+):/m);
|
|
9723
10685
|
return match?.[1] ?? null;
|
|
9724
10686
|
}
|
|
9725
|
-
function planUpgrade(ops,
|
|
10687
|
+
function planUpgrade(ops, repoRoot3, manifest, routing) {
|
|
9726
10688
|
const entries = [];
|
|
9727
10689
|
let hasChanges = routing.arm || routing.migratesManifest || routing.strips.some((s) => s.present);
|
|
9728
10690
|
const ownedFiles = new Set(manifest.files);
|
|
@@ -9730,8 +10692,8 @@ function planUpgrade(ops, repoRoot2, manifest, routing) {
|
|
|
9730
10692
|
const ownedBlocks = new Set(manifest.blocks.map((b) => blockKey(b.path, b.marker)));
|
|
9731
10693
|
for (const op of ops) {
|
|
9732
10694
|
if (op.kind === "create-file") {
|
|
9733
|
-
const abs2 = join26(
|
|
9734
|
-
const exists =
|
|
10695
|
+
const abs2 = join26(repoRoot3, op.path);
|
|
10696
|
+
const exists = existsSync23(abs2);
|
|
9735
10697
|
const owned = ownedFiles.has(op.path) || isDefaultedAgentVendorPath(op.path, manifest);
|
|
9736
10698
|
let action;
|
|
9737
10699
|
let triggerChange;
|
|
@@ -9759,12 +10721,12 @@ function planUpgrade(ops, repoRoot2, manifest, routing) {
|
|
|
9759
10721
|
}
|
|
9760
10722
|
entries.push({ kind: "create-file", op, action, ...triggerChange ? { triggerChange } : {} });
|
|
9761
10723
|
} else if (op.kind === "managed-block") {
|
|
9762
|
-
const abs2 = resolveManagedBlockPath(
|
|
10724
|
+
const abs2 = resolveManagedBlockPath(repoRoot3, op.path);
|
|
9763
10725
|
const owned = ownedBlocks.has(blockKey(op.path, op.marker));
|
|
9764
10726
|
let action;
|
|
9765
10727
|
if (!owned) {
|
|
9766
10728
|
action = "not-installed";
|
|
9767
|
-
} else if (!
|
|
10729
|
+
} else if (!existsSync23(abs2)) {
|
|
9768
10730
|
action = "recreate-host";
|
|
9769
10731
|
hasChanges = true;
|
|
9770
10732
|
} else {
|
|
@@ -9872,8 +10834,8 @@ function renderUpgradeDiff(plan) {
|
|
|
9872
10834
|
return lines.join(`
|
|
9873
10835
|
`);
|
|
9874
10836
|
}
|
|
9875
|
-
function regenerateBlock(
|
|
9876
|
-
const abs2 = resolveManagedBlockPath(
|
|
10837
|
+
function regenerateBlock(repoRoot3, op) {
|
|
10838
|
+
const abs2 = resolveManagedBlockPath(repoRoot3, op.path);
|
|
9877
10839
|
const content = readFileSync27(abs2, "utf-8");
|
|
9878
10840
|
const stripped = stripBlockFromContent(content, op.marker, op.comment);
|
|
9879
10841
|
if (stripped !== null) {
|
|
@@ -9881,28 +10843,28 @@ function regenerateBlock(repoRoot2, op) {
|
|
|
9881
10843
|
`) ? stripped : `${stripped}
|
|
9882
10844
|
`, "utf-8");
|
|
9883
10845
|
}
|
|
9884
|
-
appendBlock(
|
|
10846
|
+
appendBlock(repoRoot3, op);
|
|
9885
10847
|
}
|
|
9886
|
-
function applyUpgrade(plan,
|
|
10848
|
+
function applyUpgrade(plan, repoRoot3) {
|
|
9887
10849
|
for (const e of plan.entries) {
|
|
9888
10850
|
if (e.kind === "create-file") {
|
|
9889
10851
|
if (e.action === "regenerate" || e.action === "recreate") {
|
|
9890
|
-
writeFileWithDirs(join26(
|
|
10852
|
+
writeFileWithDirs(join26(repoRoot3, e.op.path), e.op.content, e.op.mode);
|
|
9891
10853
|
}
|
|
9892
10854
|
} else {
|
|
9893
10855
|
if (e.action === "recreate-host")
|
|
9894
|
-
createHost(
|
|
10856
|
+
createHost(repoRoot3, e.op);
|
|
9895
10857
|
else if (e.action === "recreate-append")
|
|
9896
|
-
appendBlock(
|
|
10858
|
+
appendBlock(repoRoot3, e.op);
|
|
9897
10859
|
else if (e.action === "regenerate-block")
|
|
9898
|
-
regenerateBlock(
|
|
10860
|
+
regenerateBlock(repoRoot3, e.op);
|
|
9899
10861
|
}
|
|
9900
10862
|
}
|
|
9901
10863
|
for (const s of plan.routing.strips) {
|
|
9902
10864
|
if (!s.present)
|
|
9903
10865
|
continue;
|
|
9904
|
-
const abs2 = resolveManagedBlockPath(
|
|
9905
|
-
if (!
|
|
10866
|
+
const abs2 = resolveManagedBlockPath(repoRoot3, s.path);
|
|
10867
|
+
if (!existsSync23(abs2))
|
|
9906
10868
|
continue;
|
|
9907
10869
|
const stripped = stripBlockFromContent(readFileSync27(abs2, "utf-8"), s.marker, s.comment);
|
|
9908
10870
|
if (stripped === null)
|
|
@@ -9993,16 +10955,16 @@ async function upgradeCommand(args) {
|
|
|
9993
10955
|
}
|
|
9994
10956
|
|
|
9995
10957
|
// src/commands/waiver.ts
|
|
9996
|
-
import { execFileSync as
|
|
10958
|
+
import { execFileSync as execFileSync18 } from "node:child_process";
|
|
9997
10959
|
function labelFor(kind) {
|
|
9998
10960
|
return kind === "docs" ? WAIVER_LABEL : WAIVER_LABEL_REVIEW;
|
|
9999
10961
|
}
|
|
10000
|
-
function
|
|
10001
|
-
return
|
|
10962
|
+
function gh3(args) {
|
|
10963
|
+
return execFileSync18("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
|
|
10002
10964
|
}
|
|
10003
10965
|
function detectPrNumber() {
|
|
10004
10966
|
try {
|
|
10005
|
-
const out =
|
|
10967
|
+
const out = gh3(["pr", "view", "--json", "number", "-q", ".number"]);
|
|
10006
10968
|
return /^\d+$/.test(out) ? out : null;
|
|
10007
10969
|
} catch {
|
|
10008
10970
|
return null;
|
|
@@ -10098,8 +11060,8 @@ Reason: ${reason}`];
|
|
|
10098
11060
|
}
|
|
10099
11061
|
process.stdout.write(`Applying \`${label2}\` to PR #${prNumber} under your authenticated \`gh\` identity — vinaya never fabricates an actor.
|
|
10100
11062
|
`);
|
|
10101
|
-
|
|
10102
|
-
|
|
11063
|
+
execFileSync18("gh", editArgs, { stdio: "inherit", env: process.env });
|
|
11064
|
+
execFileSync18("gh", commentArgs, { stdio: "inherit", env: process.env });
|
|
10103
11065
|
process.stdout.write(`
|
|
10104
11066
|
✓ Label applied and reason posted as a PR comment.
|
|
10105
11067
|
`);
|
|
@@ -10223,8 +11185,10 @@ try {
|
|
|
10223
11185
|
prEditCommand(rest);
|
|
10224
11186
|
} else if (subcommand === "report") {
|
|
10225
11187
|
await prReportCommand(rest);
|
|
11188
|
+
} else if (subcommand === "verify-evidence") {
|
|
11189
|
+
await prVerifyEvidenceCommand(rest);
|
|
10226
11190
|
} else {
|
|
10227
|
-
console.error(`Unknown 'pr' subcommand: ${subcommand ?? "(none)"} (expected 'create', 'edit', or '
|
|
11191
|
+
console.error(`Unknown 'pr' subcommand: ${subcommand ?? "(none)"} (expected 'create', 'edit', 'report', or 'verify-evidence')`);
|
|
10228
11192
|
process.exit(2);
|
|
10229
11193
|
}
|
|
10230
11194
|
break;
|
|
@@ -10249,8 +11213,10 @@ try {
|
|
|
10249
11213
|
await milestoneAdoptCommand(rest);
|
|
10250
11214
|
} else if (subcommand === "edit") {
|
|
10251
11215
|
await milestoneEditCommand(rest);
|
|
11216
|
+
} else if (subcommand === "close") {
|
|
11217
|
+
await milestoneCloseCommand(rest);
|
|
10252
11218
|
} else {
|
|
10253
|
-
console.error(`Unknown 'milestone' subcommand: ${subcommand ?? "(none)"} (expected create/adopt/edit)`);
|
|
11219
|
+
console.error(`Unknown 'milestone' subcommand: ${subcommand ?? "(none)"} (expected create/adopt/edit/close)`);
|
|
10254
11220
|
process.exit(2);
|
|
10255
11221
|
}
|
|
10256
11222
|
break;
|