@attalabs/vinaya 0.20.1 → 0.21.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 +74 -0
- package/aeg-root/enforcement.md +13 -6
- package/aeg-root/milestone-model.md +3 -1
- package/aeg-root/roles/architect.md +2 -2
- package/aeg-root/roles/archivist.md +1 -1
- package/aeg-root/roles/developer.md +3 -2
- package/aeg-root/roles/planner.md +1 -1
- 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/tranche-model.md +1 -1
- package/dist/checks/bin/check-body-bare-digits.js +281 -5
- package/dist/checks/bin/check-branch-topology.js +274 -0
- package/dist/checks/bin/check-brief-shape.js +249 -0
- package/dist/checks/bin/check-changeset-coverage.js +4156 -0
- package/dist/checks/bin/check-closes-n.js +274 -0
- package/dist/checks/bin/check-coherence.js +305 -3
- package/dist/checks/bin/check-dead-branch-push.js +249 -0
- package/dist/checks/bin/check-dispatch-readiness.js +381 -20
- package/dist/checks/bin/check-doc-coverage-push.js +280 -3
- package/dist/checks/bin/check-doc-coverage.js +280 -3
- package/dist/checks/bin/check-doctrine-portability.js +280 -3
- package/dist/checks/bin/check-evidence-fresh.js +249 -0
- package/dist/checks/bin/check-first-push-dispatch.js +372 -14
- package/dist/checks/bin/check-issue-assignment.js +274 -0
- package/dist/checks/bin/check-main-branch-refusal.js +249 -0
- package/dist/checks/bin/check-no-disk-state.js +249 -0
- package/dist/checks/bin/check-quoted-command.js +4405 -0
- package/dist/checks/bin/check-reader-resolvable-prose.js +282 -5
- package/dist/checks/bin/check-registry-gates.js +282 -0
- package/dist/checks/bin/check-retired-vocabulary.js +282 -5
- package/dist/checks/bin/check-review-gate.js +280 -3
- package/dist/checks/bin/check-single-plan-pr.js +249 -0
- package/dist/checks/bin/check-test-plan.js +249 -0
- package/dist/checks/bin/check-token-collection-wired.js +3975 -0
- package/dist/checks/bin/check-workspace-escape.js +249 -0
- package/dist/index.js +1523 -630
- 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]__040smoa._.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]__16cuydh._.js → [root-of-the-server]__0ozbaq0._.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/_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/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/0r4i_ks9z02bw.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0ea8-8nopz29h.js → 1hfhxjy577l5d.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1k916ih3p4nsg.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{29-foak16tjfu.js → 1t2kft7bxzuxn.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1ybrihl5_aocy.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{30ojnsx3p4lp4.js → 1yqs2zgvwa4p1.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{0msr2a1i-x-br.js → 25kbt5k-xb2a5.js} +1 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/{2twnwe77d4n-m.js → 2_k6j1o9q5nn4.js} +4 -4
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/2hmannd4bjhzy.js +1 -0
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/3mqu_uhluy19i.css +1 -0
- 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/0la0hhmjprdxm.css +0 -1
- package/studio-standalone/apps/vinaya-studio/web/.next/static/chunks/1lk7h-0buf4ab.js +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/_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 → crNCHQ9uAgKOYC6DKkZma}/_buildManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{b2VfZK2AItZwlghK_pUN0 → crNCHQ9uAgKOYC6DKkZma}/_clientMiddlewareManifest.js +0 -0
- /package/studio-standalone/apps/vinaya-studio/web/.next/static/{b2VfZK2AItZwlghK_pUN0 → crNCHQ9uAgKOYC6DKkZma}/_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,60 @@ 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
|
+
function sanitizeForTokensLine(value) {
|
|
805
|
+
const stripped = stripNewlines(value);
|
|
806
|
+
let result = "";
|
|
807
|
+
for (let i = 0;i < stripped.length; i++) {
|
|
808
|
+
const ch = stripped[i];
|
|
809
|
+
if (DASH_CHARS.has(ch)) {
|
|
810
|
+
const leftReachable = i === 0 || /\s/.test(stripped[i - 1]);
|
|
811
|
+
const rightReachable = i === stripped.length - 1 || /\s/.test(stripped[i + 1]);
|
|
812
|
+
if (leftReachable && rightReachable) {
|
|
813
|
+
result += DASH_LOOKALIKES[ch];
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
result += ch;
|
|
739
818
|
}
|
|
819
|
+
return result;
|
|
820
|
+
}
|
|
821
|
+
function renderCells(input) {
|
|
822
|
+
const model = input.modelOverride ?? input.summary?.model ?? null;
|
|
823
|
+
if (!input.summary)
|
|
824
|
+
return { model, tokensIn: "—", tokensOut: "—" };
|
|
740
825
|
const { inputTokens, cacheCreationInputTokens, cacheReadInputTokens, outputTokens } = input.summary.components;
|
|
741
826
|
const tokensIn = inputTokens + cacheCreationInputTokens + cacheReadInputTokens;
|
|
742
|
-
return
|
|
827
|
+
return { model, tokensIn: String(tokensIn), tokensOut: String(outputTokens) };
|
|
828
|
+
}
|
|
829
|
+
function formatTokensLine(input) {
|
|
830
|
+
const { model, tokensIn, tokensOut } = renderCells(input);
|
|
831
|
+
const phase = sanitizeForTokensLine(input.phase);
|
|
832
|
+
const role = sanitizeForTokensLine(input.role);
|
|
833
|
+
const safeModel = model === null ? "—" : sanitizeForTokensLine(model);
|
|
834
|
+
if (!input.summary) {
|
|
835
|
+
return `Tokens: ${phase} — ${role} — ${safeModel} — —`;
|
|
836
|
+
}
|
|
837
|
+
return `Tokens: ${phase} — ${role} — ${safeModel} — ${tokensIn}/${tokensOut}/—`;
|
|
838
|
+
}
|
|
839
|
+
function formatTokenReportRow(input) {
|
|
840
|
+
const { model, tokensIn, tokensOut } = renderCells(input);
|
|
841
|
+
const phase = sanitizeForTableCell(input.phase);
|
|
842
|
+
const role = sanitizeForTableCell(input.role);
|
|
843
|
+
const safeModel = model === null ? "—" : sanitizeForTableCell(model);
|
|
844
|
+
const date = sanitizeForTableCell(input.date);
|
|
845
|
+
return `| ${phase} | ${role} | ${safeModel} | ${tokensIn} | ${tokensOut} | — | ${date} |`;
|
|
743
846
|
}
|
|
744
847
|
function formatBreakdown(summary) {
|
|
745
848
|
const { inputTokens, cacheCreationInputTokens, cacheReadInputTokens, outputTokens } = summary.components;
|
|
@@ -807,43 +910,65 @@ function transcriptPointerPath(projectDir, tmpDir) {
|
|
|
807
910
|
}
|
|
808
911
|
function resolvePointer(explicitTranscriptPath, deps) {
|
|
809
912
|
if (explicitTranscriptPath)
|
|
810
|
-
return { path: explicitTranscriptPath };
|
|
913
|
+
return { path: explicitTranscriptPath, corroborated: true };
|
|
811
914
|
const projectDir = deps.env.CLAUDE_PROJECT_DIR ?? deps.cwd;
|
|
812
915
|
const tmpDir = deps.env.TMPDIR ?? "/tmp";
|
|
813
916
|
const pointerPath = transcriptPointerPath(projectDir, tmpDir);
|
|
917
|
+
const currentSessionId = deps.env.CLAUDE_CODE_SESSION_ID;
|
|
814
918
|
if (!deps.exists(pointerPath)) {
|
|
815
919
|
return {
|
|
920
|
+
pointerExisted: false,
|
|
921
|
+
corroborated: false,
|
|
816
922
|
error: `No transcript pointer at ${pointerPath} 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
923
|
};
|
|
818
924
|
}
|
|
819
925
|
let contents;
|
|
820
926
|
try {
|
|
821
|
-
contents = deps.readFile(pointerPath).
|
|
927
|
+
contents = deps.readFile(pointerPath).replace(/\r?\n+$/, "");
|
|
822
928
|
} catch (err) {
|
|
823
|
-
return {
|
|
929
|
+
return {
|
|
930
|
+
pointerExisted: true,
|
|
931
|
+
oursByLocation: true,
|
|
932
|
+
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."
|
|
933
|
+
};
|
|
824
934
|
}
|
|
825
935
|
const [pointerSessionId, transcriptPath] = contents.split("\t");
|
|
826
936
|
if (!transcriptPath) {
|
|
827
|
-
return {
|
|
937
|
+
return {
|
|
938
|
+
pointerExisted: true,
|
|
939
|
+
oursByLocation: true,
|
|
940
|
+
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."
|
|
941
|
+
};
|
|
828
942
|
}
|
|
829
|
-
const currentSessionId = deps.env.CLAUDE_CODE_SESSION_ID;
|
|
830
943
|
if (currentSessionId && pointerSessionId && currentSessionId !== pointerSessionId) {
|
|
831
944
|
return {
|
|
945
|
+
pointerExisted: true,
|
|
946
|
+
corroborated: false,
|
|
832
947
|
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
948
|
};
|
|
834
949
|
}
|
|
835
|
-
return { path: transcriptPath };
|
|
950
|
+
return { path: transcriptPath, corroborated: Boolean(currentSessionId && pointerSessionId) };
|
|
836
951
|
}
|
|
837
952
|
function resolveMeteringCapability(deps, explicitTranscriptPath) {
|
|
838
953
|
const resolved = resolvePointer(explicitTranscriptPath, deps);
|
|
839
954
|
if ("error" in resolved) {
|
|
840
|
-
|
|
955
|
+
const ours = resolved.pointerExisted && (resolved.corroborated || resolved.oursByLocation);
|
|
956
|
+
if (ours)
|
|
957
|
+
return { capable: false, reason: "pointer-unusable", detail: resolved.error };
|
|
958
|
+
return {
|
|
959
|
+
capable: false,
|
|
960
|
+
reason: "no-transcript-resolved",
|
|
961
|
+
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
|
|
962
|
+
};
|
|
841
963
|
}
|
|
964
|
+
const downstream = (r, detail) => resolved.corroborated ? { reason: r, detail } : {
|
|
965
|
+
reason: "no-transcript-resolved",
|
|
966
|
+
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})`
|
|
967
|
+
};
|
|
842
968
|
if (!deps.exists(resolved.path)) {
|
|
843
969
|
return {
|
|
844
970
|
capable: false,
|
|
845
|
-
|
|
846
|
-
detail: `Resolved transcript path ${resolved.path} does not exist.`
|
|
971
|
+
...downstream("transcript-unreadable", `Resolved transcript path ${resolved.path} is not readable as a regular file owned by this user.`)
|
|
847
972
|
};
|
|
848
973
|
}
|
|
849
974
|
let jsonl;
|
|
@@ -852,16 +977,14 @@ function resolveMeteringCapability(deps, explicitTranscriptPath) {
|
|
|
852
977
|
} catch (err) {
|
|
853
978
|
return {
|
|
854
979
|
capable: false,
|
|
855
|
-
|
|
856
|
-
detail: `Transcript at ${resolved.path} could not be read: ${err.message}`
|
|
980
|
+
...downstream("transcript-unreadable", `Transcript at ${resolved.path} could not be read: ${err.message}`)
|
|
857
981
|
};
|
|
858
982
|
}
|
|
859
983
|
const summary = summarizeTranscript(jsonl);
|
|
860
984
|
if (summary.messageCount === 0) {
|
|
861
985
|
return {
|
|
862
986
|
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."
|
|
987
|
+
...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
988
|
};
|
|
866
989
|
}
|
|
867
990
|
return { capable: true, transcriptPath: resolved.path, summary };
|
|
@@ -1453,6 +1576,8 @@ var EXEMPT_LITERALS = new Set([
|
|
|
1453
1576
|
var NON_PATH_TOP_SEGMENTS = new Set(["origin", "refs", "HEAD", "vinaya", "fix"]);
|
|
1454
1577
|
// ../../packages/aeg-core/src/reader-resolvable-prose.ts
|
|
1455
1578
|
var SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
|
|
1579
|
+
// ../../packages/aeg-core/src/quoted-command.ts
|
|
1580
|
+
var QUOTED_COMMAND_SWEPT_CLASSES = new Set(["ships", "reader-facing"]);
|
|
1456
1581
|
// ../../packages/aeg-core/src/retired-vocabulary.ts
|
|
1457
1582
|
var RETIRED_PATTERNS = [
|
|
1458
1583
|
String.raw`\bD-[0-9]{3}\b`,
|
|
@@ -1545,15 +1670,15 @@ function releaseFieldFromText(text) {
|
|
|
1545
1670
|
const raw = unwrapValue2(m[1] ?? "");
|
|
1546
1671
|
return { declared: true, value: RELEASE_VALUE.test(raw) ? raw : null };
|
|
1547
1672
|
}
|
|
1548
|
-
var
|
|
1549
|
-
var
|
|
1550
|
-
var
|
|
1673
|
+
var INTENTS_HEADING2 = /^#{1,6}\s*Tranche intents\s*$/im;
|
|
1674
|
+
var NEXT_HEADING2 = /^#{1,6}\s+\S/m;
|
|
1675
|
+
var INTENT_BULLET2 = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
|
|
1551
1676
|
function intentsBlock(text) {
|
|
1552
|
-
const start = text.match(
|
|
1677
|
+
const start = text.match(INTENTS_HEADING2);
|
|
1553
1678
|
if (!start || start.index === undefined)
|
|
1554
1679
|
return null;
|
|
1555
1680
|
const rest = text.slice(start.index + start[0].length);
|
|
1556
|
-
const next = rest.match(
|
|
1681
|
+
const next = rest.match(NEXT_HEADING2);
|
|
1557
1682
|
const sectionEnd = next && next.index !== undefined ? next.index : rest.length;
|
|
1558
1683
|
return { start: start.index, end: start.index + start[0].length + sectionEnd, section: rest.slice(0, sectionEnd) };
|
|
1559
1684
|
}
|
|
@@ -1570,7 +1695,7 @@ function parseIntents(text) {
|
|
|
1570
1695
|
const trimmed = line.trim();
|
|
1571
1696
|
if (trimmed.length === 0)
|
|
1572
1697
|
continue;
|
|
1573
|
-
const m = trimmed.match(
|
|
1698
|
+
const m = trimmed.match(INTENT_BULLET2);
|
|
1574
1699
|
if (!m)
|
|
1575
1700
|
return { intents: [], malformed: true };
|
|
1576
1701
|
intents.push({ slug: (m[1] ?? "").toLowerCase(), goal: (m[2] ?? "").trim() });
|
|
@@ -2838,13 +2963,14 @@ function buildInitOps(ctx) {
|
|
|
2838
2963
|
// src/lib/ops.ts
|
|
2839
2964
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
2840
2965
|
import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2841
|
-
import { dirname as dirname4, isAbsolute, join as join6, resolve, sep as sep2 } from "node:path";
|
|
2966
|
+
import { dirname as dirname4, isAbsolute, join as join6, resolve as resolve2, sep as sep2 } from "node:path";
|
|
2842
2967
|
|
|
2843
2968
|
// src/lib/config.ts
|
|
2844
2969
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2845
|
-
import {
|
|
2970
|
+
import { createHash } from "node:crypto";
|
|
2971
|
+
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, realpathSync, writeFileSync } from "node:fs";
|
|
2846
2972
|
import { homedir } from "node:os";
|
|
2847
|
-
import { dirname as dirname3, join as join5 } from "node:path";
|
|
2973
|
+
import { dirname as dirname3, join as join5, resolve } from "node:path";
|
|
2848
2974
|
import { z } from "zod";
|
|
2849
2975
|
|
|
2850
2976
|
// src/lib/agent-vendors.ts
|
|
@@ -2995,6 +3121,20 @@ function resolveAgentVendors(manifest) {
|
|
|
2995
3121
|
return new Set(AGENT_VENDORS);
|
|
2996
3122
|
return new Set(manifest.agents);
|
|
2997
3123
|
}
|
|
3124
|
+
var ProjectEntrySchema = z.object({
|
|
3125
|
+
name: z.string().min(1),
|
|
3126
|
+
description: z.string().min(1).optional(),
|
|
3127
|
+
path: z.string().min(1).optional()
|
|
3128
|
+
});
|
|
3129
|
+
function parseTokensCollectDeclaration(value) {
|
|
3130
|
+
const m = value.trim().match(/^(\S+)\s+(\S+)$/);
|
|
3131
|
+
if (!m)
|
|
3132
|
+
return null;
|
|
3133
|
+
const [, interpreter, script] = m;
|
|
3134
|
+
if (!isSafeRepoRelPath(script))
|
|
3135
|
+
return null;
|
|
3136
|
+
return { interpreter, script };
|
|
3137
|
+
}
|
|
2998
3138
|
var VinayaConfigSchema = z.object({
|
|
2999
3139
|
rings: z.object({
|
|
3000
3140
|
ring1_forgeWriteInterception: z.boolean(),
|
|
@@ -3007,13 +3147,19 @@ var VinayaConfigSchema = z.object({
|
|
|
3007
3147
|
principals: z.array(z.string()).min(1).optional(),
|
|
3008
3148
|
releaseActor: z.string().min(1).optional(),
|
|
3009
3149
|
ci: z.object({ setup: z.string().min(1) }).optional(),
|
|
3150
|
+
tokens: z.object({
|
|
3151
|
+
collect: z.string().min(1).refine((v) => parseTokensCollectDeclaration(v) !== null, {
|
|
3152
|
+
message: 'tokens.collect must be exactly "<interpreter> <repo-relative-script-path>" — two whitespace-separated tokens, no flags, no shell syntax, no quoting'
|
|
3153
|
+
})
|
|
3154
|
+
}).optional(),
|
|
3010
3155
|
blastRadius: z.object({ extraDomains: z.array(z.string()).optional() }).optional(),
|
|
3011
3156
|
proseGates: z.object({
|
|
3012
3157
|
doctrineRoot: z.string().min(1).optional(),
|
|
3013
3158
|
readerFacingPrefix: z.string().min(1).optional(),
|
|
3014
3159
|
readerFacingSuffix: z.string().min(1).optional(),
|
|
3015
3160
|
legacySlugDir: z.string().min(1).optional()
|
|
3016
|
-
}).optional()
|
|
3161
|
+
}).optional(),
|
|
3162
|
+
projects: z.array(ProjectEntrySchema).optional()
|
|
3017
3163
|
});
|
|
3018
3164
|
var GLOBAL_VINAYA_HOME = join5(homedir(), ".vinaya");
|
|
3019
3165
|
var GLOBAL_CONFIG_PATH = join5(GLOBAL_VINAYA_HOME, "config.json");
|
|
@@ -3052,6 +3198,9 @@ function globalRolesIgnoredWarning(path) {
|
|
|
3052
3198
|
function globalReleaseActorIgnoredWarning(path) {
|
|
3053
3199
|
return `${path}: "releaseActor" in the global config is ignored — releaseActor may only be declared from a repo-local vinaya.config.json.`;
|
|
3054
3200
|
}
|
|
3201
|
+
function globalTokensCollectIgnoredWarning(path) {
|
|
3202
|
+
return `${path}: "tokens" in the global config is ignored — tokens.collect may only be declared from a repo-local vinaya.config.json.`;
|
|
3203
|
+
}
|
|
3055
3204
|
function stripGlobalOnlyKeys(config, path) {
|
|
3056
3205
|
if (path !== GLOBAL_CONFIG_PATH)
|
|
3057
3206
|
return config;
|
|
@@ -3072,6 +3221,10 @@ function stripGlobalOnlyKeys(config, path) {
|
|
|
3072
3221
|
console.error(`⚠ ${globalReleaseActorIgnoredWarning(path)}`);
|
|
3073
3222
|
result = { ...result, releaseActor: undefined };
|
|
3074
3223
|
}
|
|
3224
|
+
if (result.tokens) {
|
|
3225
|
+
console.error(`⚠ ${globalTokensCollectIgnoredWarning(path)}`);
|
|
3226
|
+
result = { ...result, tokens: undefined };
|
|
3227
|
+
}
|
|
3075
3228
|
return result;
|
|
3076
3229
|
}
|
|
3077
3230
|
function resolvePrincipalAllowlist(config) {
|
|
@@ -3181,6 +3334,69 @@ function loadConfigChecked() {
|
|
|
3181
3334
|
}
|
|
3182
3335
|
return { ok: true, config: stripGlobalOnlyKeys(parsed.data, path) };
|
|
3183
3336
|
}
|
|
3337
|
+
var TOKENS_COLLECT_TRUST_PATH = join5(GLOBAL_VINAYA_HOME, "tokens-collect-trust.json");
|
|
3338
|
+
function readTokensCollectTrustStore(storePath) {
|
|
3339
|
+
try {
|
|
3340
|
+
if (!existsSync4(storePath))
|
|
3341
|
+
return {};
|
|
3342
|
+
const raw = JSON.parse(readFileSync4(storePath, "utf-8"));
|
|
3343
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
3344
|
+
} catch {
|
|
3345
|
+
return {};
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
function writeTokensCollectTrustStore(storePath, store) {
|
|
3349
|
+
const dir = dirname3(storePath);
|
|
3350
|
+
if (!existsSync4(dir))
|
|
3351
|
+
mkdirSync(dir, { recursive: true });
|
|
3352
|
+
writeFileSync(storePath, JSON.stringify(store, null, 2), "utf-8");
|
|
3353
|
+
}
|
|
3354
|
+
var GIT_IDENTITY_TIMEOUT_MS = 5000;
|
|
3355
|
+
function gitCommonDir(cwd = process.cwd()) {
|
|
3356
|
+
try {
|
|
3357
|
+
const raw = execFileSync2("git", ["rev-parse", "--git-common-dir"], {
|
|
3358
|
+
cwd,
|
|
3359
|
+
encoding: "utf-8",
|
|
3360
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3361
|
+
timeout: GIT_IDENTITY_TIMEOUT_MS
|
|
3362
|
+
}).trim();
|
|
3363
|
+
if (!raw)
|
|
3364
|
+
return null;
|
|
3365
|
+
return realpathSync(resolve(cwd, raw));
|
|
3366
|
+
} catch {
|
|
3367
|
+
return null;
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
function repoLocalConfigDir() {
|
|
3371
|
+
const local = findLocalConfig();
|
|
3372
|
+
return local ? dirname3(local) : null;
|
|
3373
|
+
}
|
|
3374
|
+
function gitBlobHash(absolutePath, cwd) {
|
|
3375
|
+
try {
|
|
3376
|
+
const raw = execFileSync2("git", ["hash-object", absolutePath], {
|
|
3377
|
+
cwd,
|
|
3378
|
+
encoding: "utf-8",
|
|
3379
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3380
|
+
timeout: GIT_IDENTITY_TIMEOUT_MS
|
|
3381
|
+
}).trim();
|
|
3382
|
+
return raw || null;
|
|
3383
|
+
} catch {
|
|
3384
|
+
return null;
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
function tokensCollectTrustKey(repoGitCommonDir, interpreter, script) {
|
|
3388
|
+
return createHash("sha256").update(JSON.stringify([repoGitCommonDir, interpreter, script])).digest("hex");
|
|
3389
|
+
}
|
|
3390
|
+
function getTokensCollectTrust(repoGitCommonDir, interpreter, script, storePath = TOKENS_COLLECT_TRUST_PATH) {
|
|
3391
|
+
const key = tokensCollectTrustKey(repoGitCommonDir, interpreter, script);
|
|
3392
|
+
return readTokensCollectTrustStore(storePath)[key] ?? null;
|
|
3393
|
+
}
|
|
3394
|
+
function trustTokensCollectCommand(repoGitCommonDir, interpreter, script, scriptBlobHash, storePath = TOKENS_COLLECT_TRUST_PATH) {
|
|
3395
|
+
const store = readTokensCollectTrustStore(storePath);
|
|
3396
|
+
const key = tokensCollectTrustKey(repoGitCommonDir, interpreter, script);
|
|
3397
|
+
store[key] = { interpreter, script, scriptBlobHash, trustedAt: new Date().toISOString() };
|
|
3398
|
+
writeTokensCollectTrustStore(storePath, store);
|
|
3399
|
+
}
|
|
3184
3400
|
|
|
3185
3401
|
// src/lib/ops.ts
|
|
3186
3402
|
var MARKER_NS = "vinaya:managed";
|
|
@@ -3204,9 +3420,9 @@ var GIT_DIR_PREFIX = ".git/";
|
|
|
3204
3420
|
function resolveManagedBlockPath(repoRoot, opPath) {
|
|
3205
3421
|
if (!opPath.startsWith(GIT_DIR_PREFIX))
|
|
3206
3422
|
return join6(repoRoot, opPath);
|
|
3207
|
-
return join6(
|
|
3423
|
+
return join6(gitCommonDir2(repoRoot), opPath.slice(GIT_DIR_PREFIX.length));
|
|
3208
3424
|
}
|
|
3209
|
-
function
|
|
3425
|
+
function gitCommonDir2(repoRoot) {
|
|
3210
3426
|
try {
|
|
3211
3427
|
const commonDir = execFileSync3("git", ["-C", repoRoot, "rev-parse", "--git-common-dir"], {
|
|
3212
3428
|
encoding: "utf8"
|
|
@@ -3219,8 +3435,8 @@ function gitCommonDir(repoRoot) {
|
|
|
3219
3435
|
}
|
|
3220
3436
|
}
|
|
3221
3437
|
function containedAbs(repoRoot, relPath) {
|
|
3222
|
-
const root =
|
|
3223
|
-
const target =
|
|
3438
|
+
const root = resolve2(repoRoot);
|
|
3439
|
+
const target = resolve2(root, relPath);
|
|
3224
3440
|
if (target === root)
|
|
3225
3441
|
return null;
|
|
3226
3442
|
return target === root || target.startsWith(root + sep2) ? target : null;
|
|
@@ -3228,9 +3444,9 @@ function containedAbs(repoRoot, relPath) {
|
|
|
3228
3444
|
function containedManagedBlockAbs(repoRoot, relPath) {
|
|
3229
3445
|
if (!relPath.startsWith(GIT_DIR_PREFIX))
|
|
3230
3446
|
return containedAbs(repoRoot, relPath);
|
|
3231
|
-
const commonDir =
|
|
3232
|
-
const hooksRoot =
|
|
3233
|
-
const target =
|
|
3447
|
+
const commonDir = gitCommonDir2(repoRoot);
|
|
3448
|
+
const hooksRoot = resolve2(join6(commonDir, "hooks"));
|
|
3449
|
+
const target = resolve2(join6(commonDir, relPath.slice(GIT_DIR_PREFIX.length)));
|
|
3234
3450
|
if (target === hooksRoot)
|
|
3235
3451
|
return null;
|
|
3236
3452
|
return target.startsWith(hooksRoot + sep2) ? target : null;
|
|
@@ -3766,8 +3982,8 @@ function prompt(question) {
|
|
|
3766
3982
|
setupStdinReader();
|
|
3767
3983
|
if (stdinEnded && pendingResolvers.length === 0)
|
|
3768
3984
|
return Promise.resolve("");
|
|
3769
|
-
return new Promise((
|
|
3770
|
-
pendingResolvers.push(
|
|
3985
|
+
return new Promise((resolve3) => {
|
|
3986
|
+
pendingResolvers.push(resolve3);
|
|
3771
3987
|
if (stdinEnded)
|
|
3772
3988
|
flushLines();
|
|
3773
3989
|
});
|
|
@@ -3786,112 +4002,397 @@ function closeStdin() {
|
|
|
3786
4002
|
}
|
|
3787
4003
|
}
|
|
3788
4004
|
|
|
3789
|
-
// src/commands/
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
}
|
|
3793
|
-
|
|
3794
|
-
|
|
4005
|
+
// src/commands/tokens.ts
|
|
4006
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
4007
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
|
|
4008
|
+
import { resolve as resolvePath } from "node:path";
|
|
4009
|
+
var USAGE = [
|
|
4010
|
+
'Usage: vinaya tokens --phase "<task-id>: develop" --role Developer',
|
|
4011
|
+
" [--model <id>] [--transcript <path>] [--in <tokens-in> --out <tokens-out>]",
|
|
4012
|
+
"",
|
|
4013
|
+
" --transcript Read this transcript directly. Supported primary route — use it whenever you know",
|
|
4014
|
+
" which transcript is yours, and always in a repo with no track-transcript.sh hook.",
|
|
4015
|
+
" (omitted) Resolve via the Stop-hook pointer file, if this repo installs that hook.",
|
|
4016
|
+
" --in/--out Manual entry: the exact token figures, for a host whose usage arrives by some other",
|
|
4017
|
+
" means than a Claude Code transcript. Both required together; skips transcript",
|
|
4018
|
+
" resolution entirely.",
|
|
4019
|
+
"",
|
|
4020
|
+
"Usage: vinaya tokens --trust-collect",
|
|
4021
|
+
" Approves this repo's declared tokens.collect command, pinned to the script's exact current",
|
|
4022
|
+
" content, on this machine. Run this once before tokens.collect will ever execute — a changed",
|
|
4023
|
+
" interpreter, script path, or script CONTENT needs its own fresh approval. Does not take",
|
|
4024
|
+
" --phase/--role and never emits a Tokens: line."
|
|
4025
|
+
].join(`
|
|
4026
|
+
`);
|
|
4027
|
+
function parseArgs(argv) {
|
|
4028
|
+
let phase;
|
|
4029
|
+
let role;
|
|
4030
|
+
let model;
|
|
4031
|
+
let transcriptPath;
|
|
4032
|
+
let tokensInRaw;
|
|
4033
|
+
let tokensOutRaw;
|
|
4034
|
+
for (let i = 0;i < argv.length; i++) {
|
|
4035
|
+
const arg = argv[i];
|
|
4036
|
+
if (arg === "--phase")
|
|
4037
|
+
phase = argv[++i];
|
|
4038
|
+
else if (arg === "--role")
|
|
4039
|
+
role = argv[++i];
|
|
4040
|
+
else if (arg === "--model")
|
|
4041
|
+
model = argv[++i];
|
|
4042
|
+
else if (arg === "--transcript")
|
|
4043
|
+
transcriptPath = argv[++i];
|
|
4044
|
+
else if (arg === "--in")
|
|
4045
|
+
tokensInRaw = argv[++i];
|
|
4046
|
+
else if (arg === "--out")
|
|
4047
|
+
tokensOutRaw = argv[++i];
|
|
4048
|
+
}
|
|
4049
|
+
if (!phase || !role) {
|
|
4050
|
+
throw new Error(USAGE);
|
|
4051
|
+
}
|
|
4052
|
+
if (tokensInRaw === undefined !== (tokensOutRaw === undefined)) {
|
|
4053
|
+
throw new Error("vinaya tokens: --in and --out must be given together, or not at all.");
|
|
4054
|
+
}
|
|
4055
|
+
const tokensIn = tokensInRaw === undefined ? undefined : Number(tokensInRaw);
|
|
4056
|
+
const tokensOut = tokensOutRaw === undefined ? undefined : Number(tokensOutRaw);
|
|
4057
|
+
if (tokensIn !== undefined && (!Number.isFinite(tokensIn) || tokensIn < 0)) {
|
|
4058
|
+
throw new Error(`vinaya tokens: --in must be a non-negative integer, got "${tokensInRaw}".`);
|
|
4059
|
+
}
|
|
4060
|
+
if (tokensOut !== undefined && (!Number.isFinite(tokensOut) || tokensOut < 0)) {
|
|
4061
|
+
throw new Error(`vinaya tokens: --out must be a non-negative integer, got "${tokensOutRaw}".`);
|
|
4062
|
+
}
|
|
4063
|
+
return { phase, role, model, transcriptPath, tokensIn, tokensOut };
|
|
3795
4064
|
}
|
|
3796
|
-
function
|
|
3797
|
-
return
|
|
4065
|
+
function manualSummary(tokensIn, tokensOut) {
|
|
4066
|
+
return {
|
|
4067
|
+
components: {
|
|
4068
|
+
inputTokens: tokensIn,
|
|
4069
|
+
outputTokens: tokensOut,
|
|
4070
|
+
cacheCreationInputTokens: 0,
|
|
4071
|
+
cacheReadInputTokens: 0
|
|
4072
|
+
},
|
|
4073
|
+
model: null,
|
|
4074
|
+
messageCount: 1
|
|
4075
|
+
};
|
|
3798
4076
|
}
|
|
3799
|
-
function
|
|
3800
|
-
return
|
|
4077
|
+
function declaredSummary(components, model) {
|
|
4078
|
+
return { components, model, messageCount: 1 };
|
|
3801
4079
|
}
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
4080
|
+
var COLLECT_FIELDS = ["inputTokens", "outputTokens", "cacheCreationInputTokens", "cacheReadInputTokens"];
|
|
4081
|
+
function parseDeclaredCollectOutput(raw, command) {
|
|
4082
|
+
const trimmed = raw.trim();
|
|
4083
|
+
const fail = (why) => {
|
|
4084
|
+
throw new Error(`vinaya tokens: declared tokens.collect command "${command}" ${why}
|
|
4085
|
+
` + "Expected stdout to be a JSON object: " + `{"inputTokens":N,"outputTokens":N,"cacheCreationInputTokens":N,"cacheReadInputTokens":N,"model":"…"|null}
|
|
4086
|
+
` + `Got: ${trimmed.slice(0, 500)}`);
|
|
4087
|
+
};
|
|
4088
|
+
let parsed;
|
|
4089
|
+
try {
|
|
4090
|
+
parsed = JSON.parse(trimmed);
|
|
4091
|
+
} catch (err) {
|
|
4092
|
+
fail(`did not print valid JSON on stdout (${err.message}).`);
|
|
3807
4093
|
}
|
|
3808
|
-
|
|
4094
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4095
|
+
fail("printed JSON that is not an object.");
|
|
4096
|
+
}
|
|
4097
|
+
const obj = parsed;
|
|
4098
|
+
const components = {};
|
|
4099
|
+
for (const field of COLLECT_FIELDS) {
|
|
4100
|
+
const value = obj[field];
|
|
4101
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
4102
|
+
fail(`printed an invalid "${field}" (must be a non-negative number), got ${JSON.stringify(value)}.`);
|
|
4103
|
+
}
|
|
4104
|
+
components[field] = value;
|
|
4105
|
+
}
|
|
4106
|
+
const model = obj.model;
|
|
4107
|
+
if (model !== undefined && model !== null && typeof model !== "string") {
|
|
4108
|
+
fail(`printed a non-string "model" field: ${JSON.stringify(model)}.`);
|
|
4109
|
+
}
|
|
4110
|
+
return declaredSummary(components, typeof model === "string" ? model : null);
|
|
3809
4111
|
}
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
4112
|
+
function resolveDeclaredCollect(command, deps) {
|
|
4113
|
+
const parsed = parseTokensCollectDeclaration(command);
|
|
4114
|
+
if (!parsed) {
|
|
4115
|
+
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.");
|
|
3814
4116
|
}
|
|
3815
|
-
const
|
|
3816
|
-
if (
|
|
3817
|
-
|
|
3818
|
-
return 1;
|
|
4117
|
+
const repoConfigDir = deps.repoConfigDir();
|
|
4118
|
+
if (repoConfigDir === null) {
|
|
4119
|
+
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.");
|
|
3819
4120
|
}
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
4121
|
+
const repoGitCommonDir = deps.gitCommonDir();
|
|
4122
|
+
if (repoGitCommonDir === null) {
|
|
4123
|
+
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.");
|
|
3823
4124
|
}
|
|
3824
|
-
const
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
if (associated.length === 0) {
|
|
3828
|
-
process.stdout.write(`[vinaya archive] no associated PR for merge ${mergeSha} — skip.
|
|
3829
|
-
`);
|
|
3830
|
-
return 0;
|
|
4125
|
+
const scriptAbsolutePath = resolvePath(repoConfigDir, parsed.script);
|
|
4126
|
+
if (!deps.exists(scriptAbsolutePath)) {
|
|
4127
|
+
throw new Error(`vinaya tokens: declared tokens.collect script "${parsed.script}" does not exist at ${scriptAbsolutePath}.`);
|
|
3831
4128
|
}
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
"
|
|
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
|
-
}
|
|
4129
|
+
return { interpreter: parsed.interpreter, script: parsed.script, scriptAbsolutePath, repoGitCommonDir };
|
|
4130
|
+
}
|
|
4131
|
+
function runDeclaredCollect(command, deps) {
|
|
4132
|
+
const { interpreter, script, scriptAbsolutePath, repoGitCommonDir } = resolveDeclaredCollect(command, deps);
|
|
4133
|
+
const currentHash = deps.scriptContentHash(scriptAbsolutePath, deps.cwd);
|
|
4134
|
+
if (currentHash === null) {
|
|
4135
|
+
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.");
|
|
3859
4136
|
}
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
`
|
|
3863
|
-
|
|
4137
|
+
const trust = deps.getTrust(repoGitCommonDir, interpreter, script);
|
|
4138
|
+
if (trust === null) {
|
|
4139
|
+
throw new Error(`vinaya tokens: declared tokens.collect is not yet trusted on this machine:
|
|
4140
|
+
` + ` ${interpreter} ${script}
|
|
4141
|
+
` + "Run `vinaya tokens --trust-collect` once to approve it. Refusing — no `Tokens:` line emitted, no zeros.");
|
|
3864
4142
|
}
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
`);
|
|
3869
|
-
return 0;
|
|
4143
|
+
if (trust.scriptBlobHash !== currentHash) {
|
|
4144
|
+
throw new Error(`vinaya tokens: declared tokens.collect script "${script}" no longer matches its trusted content ` + `(approved at ${trust.scriptBlobHash}, now ${currentHash}).
|
|
4145
|
+
` + "Run `vinaya tokens --trust-collect` again to approve the new content. Refusing — no `Tokens:` line emitted, no zeros.");
|
|
3870
4146
|
}
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
comments
|
|
3878
|
-
};
|
|
3879
|
-
const { block, issue, dangling } = buildProvenanceBlock(facts);
|
|
3880
|
-
process.stdout.write(`[vinaya archive] posting provenance block to PR #${pr.number}...
|
|
3881
|
-
`);
|
|
3882
|
-
sh(["gh", "pr", "comment", String(pr.number), "-R", repoFlag, "--body-file", "-"], block);
|
|
3883
|
-
process.stdout.write(`[vinaya archive] provenance block posted to PR #${pr.number}.
|
|
3884
|
-
`);
|
|
3885
|
-
if (dangling.length > 0) {
|
|
3886
|
-
process.stdout.write(`[vinaya archive] DANGLING (${dangling.length}): ${dangling.join("; ")}
|
|
3887
|
-
`);
|
|
4147
|
+
deps.warn(`⚠ vinaya tokens: running declared tokens.collect: ${interpreter} ${script}`);
|
|
4148
|
+
let raw;
|
|
4149
|
+
try {
|
|
4150
|
+
raw = deps.runScript(interpreter, scriptAbsolutePath, deps.cwd);
|
|
4151
|
+
} catch (err) {
|
|
4152
|
+
throw new Error(`vinaya tokens: declared tokens.collect "${interpreter} ${script}" failed: ${err.message}`);
|
|
3888
4153
|
}
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
4154
|
+
return parseDeclaredCollectOutput(raw, command);
|
|
4155
|
+
}
|
|
4156
|
+
var COLLECT_COMMAND_TIMEOUT_MS = 30000;
|
|
4157
|
+
function realDeps() {
|
|
4158
|
+
return {
|
|
4159
|
+
env: process.env,
|
|
4160
|
+
cwd: process.cwd(),
|
|
4161
|
+
exists: existsSync7,
|
|
4162
|
+
readFile: (path) => readFileSync7(path, "utf8"),
|
|
4163
|
+
loadConfig,
|
|
4164
|
+
repoConfigDir: repoLocalConfigDir,
|
|
4165
|
+
runScript: (interpreter, scriptAbsolutePath, cwd) => execFileSync4(interpreter, [scriptAbsolutePath], { cwd, encoding: "utf-8", timeout: COLLECT_COMMAND_TIMEOUT_MS }),
|
|
4166
|
+
warn: (message) => console.error(message),
|
|
4167
|
+
gitCommonDir,
|
|
4168
|
+
scriptContentHash: gitBlobHash,
|
|
4169
|
+
getTrust: getTokensCollectTrust,
|
|
4170
|
+
trustCollect: trustTokensCollectCommand
|
|
4171
|
+
};
|
|
4172
|
+
}
|
|
4173
|
+
function buildTokensResult(parsed, deps) {
|
|
4174
|
+
if (parsed.tokensIn !== undefined && parsed.tokensOut !== undefined) {
|
|
4175
|
+
const summary = manualSummary(parsed.tokensIn, parsed.tokensOut);
|
|
4176
|
+
return {
|
|
4177
|
+
line: formatTokensLine({ phase: parsed.phase, role: parsed.role, summary, modelOverride: parsed.model }),
|
|
4178
|
+
breakdown: undefined
|
|
4179
|
+
};
|
|
4180
|
+
}
|
|
4181
|
+
const declaredCommand = deps.loadConfig()?.tokens?.collect;
|
|
4182
|
+
if (declaredCommand) {
|
|
4183
|
+
const summary = runDeclaredCollect(declaredCommand, deps);
|
|
4184
|
+
return {
|
|
4185
|
+
line: formatTokensLine({ phase: parsed.phase, role: parsed.role, summary, modelOverride: parsed.model }),
|
|
4186
|
+
breakdown: formatBreakdown(summary)
|
|
4187
|
+
};
|
|
4188
|
+
}
|
|
4189
|
+
const capability = resolveMeteringCapability(deps, parsed.transcriptPath);
|
|
4190
|
+
if (!capability.capable) {
|
|
4191
|
+
throw new Error(`vinaya tokens: refused — could not resolve real usage figures (${capability.reason}).
|
|
4192
|
+
${capability.detail}
|
|
4193
|
+
|
|
4194
|
+
${USAGE}`);
|
|
4195
|
+
}
|
|
4196
|
+
return {
|
|
4197
|
+
line: formatTokensLine({
|
|
4198
|
+
phase: parsed.phase,
|
|
4199
|
+
role: parsed.role,
|
|
4200
|
+
summary: capability.summary,
|
|
4201
|
+
modelOverride: parsed.model
|
|
4202
|
+
}),
|
|
4203
|
+
breakdown: formatBreakdown(capability.summary)
|
|
4204
|
+
};
|
|
4205
|
+
}
|
|
4206
|
+
function runTrustCollect(deps) {
|
|
4207
|
+
const declaredCommand = deps.loadConfig()?.tokens?.collect;
|
|
4208
|
+
if (!declaredCommand) {
|
|
4209
|
+
return {
|
|
4210
|
+
ok: false,
|
|
4211
|
+
message: "vinaya tokens --trust-collect: no tokens.collect declared in this repo's vinaya.config.json — nothing to trust."
|
|
4212
|
+
};
|
|
4213
|
+
}
|
|
4214
|
+
let resolved;
|
|
4215
|
+
try {
|
|
4216
|
+
resolved = resolveDeclaredCollect(declaredCommand, deps);
|
|
4217
|
+
} catch (err) {
|
|
4218
|
+
return { ok: false, message: err.message };
|
|
4219
|
+
}
|
|
4220
|
+
const { interpreter, script, scriptAbsolutePath, repoGitCommonDir } = resolved;
|
|
4221
|
+
const scriptBlobHash = deps.scriptContentHash(scriptAbsolutePath, deps.cwd);
|
|
4222
|
+
if (scriptBlobHash === null) {
|
|
4223
|
+
return {
|
|
4224
|
+
ok: false,
|
|
4225
|
+
message: `vinaya tokens --trust-collect: script "${script}" could not be hashed (git unavailable, or the file could not be read) — refusing to trust blindly.`
|
|
4226
|
+
};
|
|
4227
|
+
}
|
|
4228
|
+
deps.trustCollect(repoGitCommonDir, interpreter, script, scriptBlobHash);
|
|
4229
|
+
return { ok: true, interpreter, script, scriptBlobHash };
|
|
4230
|
+
}
|
|
4231
|
+
function tokensCommand(argv) {
|
|
4232
|
+
if (argv.includes("--trust-collect")) {
|
|
4233
|
+
const outcome = runTrustCollect(realDeps());
|
|
4234
|
+
if (!outcome.ok) {
|
|
4235
|
+
console.error(outcome.message);
|
|
4236
|
+
process.exit(1);
|
|
4237
|
+
}
|
|
4238
|
+
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.
|
|
4239
|
+
${outcome.interpreter} ${outcome.script}
|
|
4240
|
+
` + ` content hash: ${outcome.scriptBlobHash}
|
|
4241
|
+
`);
|
|
4242
|
+
return;
|
|
4243
|
+
}
|
|
4244
|
+
let parsed;
|
|
4245
|
+
try {
|
|
4246
|
+
parsed = parseArgs(argv);
|
|
4247
|
+
} catch (err) {
|
|
4248
|
+
console.error(err.message);
|
|
4249
|
+
process.exit(2);
|
|
4250
|
+
}
|
|
4251
|
+
let result;
|
|
4252
|
+
try {
|
|
4253
|
+
result = buildTokensResult(parsed, realDeps());
|
|
4254
|
+
} catch (err) {
|
|
4255
|
+
console.error(err.message);
|
|
4256
|
+
process.exit(1);
|
|
4257
|
+
}
|
|
4258
|
+
if (result.breakdown)
|
|
4259
|
+
console.error(result.breakdown);
|
|
4260
|
+
process.stdout.write(`${result.line}
|
|
4261
|
+
`);
|
|
4262
|
+
}
|
|
4263
|
+
|
|
4264
|
+
// src/commands/archive.ts
|
|
4265
|
+
function ring2Accelerated() {
|
|
4266
|
+
return loadConfig()?.rings?.ring2_asyncAudits === true;
|
|
4267
|
+
}
|
|
4268
|
+
function realDeps2() {
|
|
4269
|
+
return { detectRepo: detectGitRepo, meteringCapability: () => resolveMeteringCapability(realDeps()) };
|
|
4270
|
+
}
|
|
4271
|
+
function isEmptySummary(summary) {
|
|
4272
|
+
const { inputTokens, outputTokens, cacheCreationInputTokens, cacheReadInputTokens } = summary.components;
|
|
4273
|
+
return inputTokens === 0 && outputTokens === 0 && cacheCreationInputTokens === 0 && cacheReadInputTokens === 0;
|
|
4274
|
+
}
|
|
4275
|
+
function renderArchiveTokensLine(capability, phase, role) {
|
|
4276
|
+
if (!capability.capable) {
|
|
4277
|
+
return { line: formatTokensLine({ phase, role, summary: null }), dangling: null };
|
|
4278
|
+
}
|
|
4279
|
+
if (isEmptySummary(capability.summary)) {
|
|
4280
|
+
return {
|
|
4281
|
+
line: null,
|
|
4282
|
+
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."
|
|
4283
|
+
};
|
|
4284
|
+
}
|
|
4285
|
+
return { line: formatTokensLine({ phase, role, summary: capability.summary }), dangling: null };
|
|
4286
|
+
}
|
|
4287
|
+
function sh(args, input) {
|
|
4288
|
+
return execFileSync5(args[0], args.slice(1), { encoding: "utf8", input, env: process.env }).trim();
|
|
4289
|
+
}
|
|
4290
|
+
function shJson(args) {
|
|
4291
|
+
return JSON.parse(sh(args));
|
|
4292
|
+
}
|
|
4293
|
+
function parseMergeSha(args) {
|
|
4294
|
+
for (const a of args) {
|
|
4295
|
+
const m = a.match(/^--merge-sha=(.+)$/);
|
|
4296
|
+
if (m)
|
|
4297
|
+
return m[1];
|
|
4298
|
+
}
|
|
4299
|
+
return null;
|
|
4300
|
+
}
|
|
4301
|
+
async function runArchive(args, deps) {
|
|
4302
|
+
if (ring2Accelerated()) {
|
|
4303
|
+
process.stdout.write("[vinaya archive] rings.ring2_asyncAudits is `true` (opt-in accelerator) — skipping, nothing changed.\n");
|
|
4304
|
+
return 0;
|
|
4305
|
+
}
|
|
4306
|
+
const repo = await deps.detectRepo();
|
|
4307
|
+
if (!repo) {
|
|
4308
|
+
console.error("Error: not a git repository. Run `vinaya archive` from inside your repo.");
|
|
4309
|
+
return 1;
|
|
4310
|
+
}
|
|
4311
|
+
if (!repo.owner || !repo.repo) {
|
|
4312
|
+
console.error("Error: could not resolve a GitHub owner/repo from the `origin` remote.");
|
|
4313
|
+
return 1;
|
|
4314
|
+
}
|
|
4315
|
+
const repoFlag = `${repo.owner}/${repo.repo}`;
|
|
4316
|
+
const mergeSha = parseMergeSha(args) ?? sh(["git", "rev-parse", "HEAD"]);
|
|
4317
|
+
const associated = shJson(["gh", "api", `repos/${repoFlag}/commits/${mergeSha}/pulls`]);
|
|
4318
|
+
if (associated.length === 0) {
|
|
4319
|
+
process.stdout.write(`[vinaya archive] no associated PR for merge ${mergeSha} — skip.
|
|
4320
|
+
`);
|
|
4321
|
+
return 0;
|
|
4322
|
+
}
|
|
4323
|
+
const prNumber = associated[0].number;
|
|
4324
|
+
const pr = shJson([
|
|
4325
|
+
"gh",
|
|
4326
|
+
"pr",
|
|
4327
|
+
"view",
|
|
4328
|
+
String(prNumber),
|
|
4329
|
+
"-R",
|
|
4330
|
+
repoFlag,
|
|
4331
|
+
"--json",
|
|
4332
|
+
"number,headRefName,body,mergedAt,comments"
|
|
4333
|
+
]);
|
|
4334
|
+
const ref = taskRefFromBranch(pr.headRefName);
|
|
4335
|
+
let issueLabels = [];
|
|
4336
|
+
if (ref === null) {
|
|
4337
|
+
const { issue: candidateIssue } = extractIssue(pr.body);
|
|
4338
|
+
if (candidateIssue !== null) {
|
|
4339
|
+
issueLabels = shJson([
|
|
4340
|
+
"gh",
|
|
4341
|
+
"issue",
|
|
4342
|
+
"view",
|
|
4343
|
+
String(candidateIssue),
|
|
4344
|
+
"-R",
|
|
4345
|
+
repoFlag,
|
|
4346
|
+
"--json",
|
|
4347
|
+
"labels"
|
|
4348
|
+
]).labels.map((l) => l.name);
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
if (!isEligibleForProvenance(ref, issueLabels)) {
|
|
4352
|
+
process.stdout.write(`[vinaya archive] non-task branch (${pr.headRefName}) and closing Issue carries no vinaya/tranche:* label — skip.
|
|
4353
|
+
`);
|
|
4354
|
+
return 0;
|
|
4355
|
+
}
|
|
4356
|
+
const comments = pr.comments.map((c) => c.body);
|
|
4357
|
+
if (hasProvenance(comments)) {
|
|
4358
|
+
process.stdout.write(`[vinaya archive] provenance already present on PR #${pr.number} — skip (idempotent).
|
|
4359
|
+
`);
|
|
4360
|
+
return 0;
|
|
4361
|
+
}
|
|
4362
|
+
const facts = {
|
|
4363
|
+
number: pr.number,
|
|
4364
|
+
headRefName: pr.headRefName,
|
|
4365
|
+
body: pr.body,
|
|
4366
|
+
mergedAt: pr.mergedAt,
|
|
4367
|
+
mergeSha,
|
|
4368
|
+
comments
|
|
4369
|
+
};
|
|
4370
|
+
const { block, issue, dangling } = buildProvenanceBlock(facts);
|
|
4371
|
+
const phase = `${ref ? ref.taskId : pr.headRefName}: archive`;
|
|
4372
|
+
const tokensResult = renderArchiveTokensLine(deps.meteringCapability(), phase, "Archivist");
|
|
4373
|
+
const tokensAddition = tokensResult.line !== null ? tokensResult.line : `DANGLING (tokens): ${tokensResult.dangling}`;
|
|
4374
|
+
const blockWithTokens = `${block}
|
|
4375
|
+
|
|
4376
|
+
${tokensAddition}`;
|
|
4377
|
+
process.stdout.write(`[vinaya archive] posting provenance block to PR #${pr.number}...
|
|
4378
|
+
`);
|
|
4379
|
+
sh(["gh", "pr", "comment", String(pr.number), "-R", repoFlag, "--body-file", "-"], blockWithTokens);
|
|
4380
|
+
process.stdout.write(`[vinaya archive] provenance block posted to PR #${pr.number}.
|
|
4381
|
+
`);
|
|
4382
|
+
if (dangling.length > 0) {
|
|
4383
|
+
process.stdout.write(`[vinaya archive] DANGLING (${dangling.length}): ${dangling.join("; ")}
|
|
4384
|
+
`);
|
|
4385
|
+
}
|
|
4386
|
+
if (tokensResult.dangling !== null) {
|
|
4387
|
+
process.stdout.write(`[vinaya archive] DANGLING (tokens): ${tokensResult.dangling}
|
|
4388
|
+
`);
|
|
4389
|
+
}
|
|
4390
|
+
if (issue !== null) {
|
|
4391
|
+
process.stdout.write(`[vinaya archive] closing Issue #${issue}...
|
|
4392
|
+
`);
|
|
4393
|
+
sh(["gh", "issue", "close", String(issue), "-R", repoFlag]);
|
|
4394
|
+
const state = shJson([
|
|
4395
|
+
"gh",
|
|
3895
4396
|
"issue",
|
|
3896
4397
|
"view",
|
|
3897
4398
|
String(issue),
|
|
@@ -3915,7 +4416,7 @@ async function runArchive(args, deps) {
|
|
|
3915
4416
|
return 0;
|
|
3916
4417
|
}
|
|
3917
4418
|
async function archiveCommand(args) {
|
|
3918
|
-
process.exit(await runArchive(args,
|
|
4419
|
+
process.exit(await runArchive(args, realDeps2()));
|
|
3919
4420
|
}
|
|
3920
4421
|
function parseTrancheArgs(args) {
|
|
3921
4422
|
const yes = args.includes("--yes");
|
|
@@ -3994,19 +4495,19 @@ async function runArchiveTranche(args, deps) {
|
|
|
3994
4495
|
return 0;
|
|
3995
4496
|
}
|
|
3996
4497
|
async function archiveTrancheCommand(args) {
|
|
3997
|
-
process.exit(await runArchiveTranche(args,
|
|
4498
|
+
process.exit(await runArchiveTranche(args, realDeps2()));
|
|
3998
4499
|
}
|
|
3999
4500
|
|
|
4000
4501
|
// src/commands/audit.ts
|
|
4001
|
-
import { execFileSync as
|
|
4502
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
4002
4503
|
function ring2Accelerated2() {
|
|
4003
4504
|
return loadConfig()?.rings?.ring2_asyncAudits === true;
|
|
4004
4505
|
}
|
|
4005
|
-
function
|
|
4506
|
+
function realDeps3() {
|
|
4006
4507
|
return { detectRepo: detectGitRepo };
|
|
4007
4508
|
}
|
|
4008
4509
|
function defaultSleep(ms) {
|
|
4009
|
-
return new Promise((
|
|
4510
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
4010
4511
|
}
|
|
4011
4512
|
var COMMAND_TIMEOUT_MS = 20000;
|
|
4012
4513
|
var DEAD_BRANCH_LABEL = label("dead-branch-push");
|
|
@@ -4017,7 +4518,7 @@ var DIRECT_PUSH_MARKER = `<!-- ${DIRECT_PUSH_LABEL} -->`;
|
|
|
4017
4518
|
var DIRECT_PUSH_LABEL_DESCRIPTION = "A commit landed on main with no associated merged PR (ring-2 detection)";
|
|
4018
4519
|
function shSoft(args, input) {
|
|
4019
4520
|
try {
|
|
4020
|
-
return
|
|
4521
|
+
return execFileSync6(args[0], args.slice(1), {
|
|
4021
4522
|
encoding: "utf8",
|
|
4022
4523
|
input,
|
|
4023
4524
|
timeout: COMMAND_TIMEOUT_MS
|
|
@@ -4041,7 +4542,7 @@ function shSoftJson(args) {
|
|
|
4041
4542
|
}
|
|
4042
4543
|
}
|
|
4043
4544
|
function sh2(args, input) {
|
|
4044
|
-
return
|
|
4545
|
+
return execFileSync6(args[0], args.slice(1), { encoding: "utf8", input }).trim();
|
|
4045
4546
|
}
|
|
4046
4547
|
function listTaskBranches() {
|
|
4047
4548
|
const out = shSoft(["git", "ls-remote", "--heads", "origin", "task/*"]);
|
|
@@ -4297,11 +4798,11 @@ async function runAudit(args, deps) {
|
|
|
4297
4798
|
return failed ? 1 : 0;
|
|
4298
4799
|
}
|
|
4299
4800
|
async function auditCommand(args) {
|
|
4300
|
-
process.exit(await runAudit(args,
|
|
4801
|
+
process.exit(await runAudit(args, realDeps3()));
|
|
4301
4802
|
}
|
|
4302
4803
|
|
|
4303
4804
|
// src/commands/check.ts
|
|
4304
|
-
import { execFileSync as
|
|
4805
|
+
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
4305
4806
|
import { dirname as dirname5 } from "node:path";
|
|
4306
4807
|
|
|
4307
4808
|
// src/checks/contract.ts
|
|
@@ -4312,11 +4813,11 @@ function emitCheckError(error) {
|
|
|
4312
4813
|
}
|
|
4313
4814
|
|
|
4314
4815
|
// src/checks/registry.ts
|
|
4315
|
-
import { existsSync as
|
|
4816
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
4316
4817
|
import { join as join8 } from "node:path";
|
|
4317
4818
|
var DIST_BIN_DIR = join8(packageRoot(import.meta.url), "dist", "checks", "bin");
|
|
4318
4819
|
var SRC_BIN_DIR = join8(packageRoot(import.meta.url), "src", "checks", "bin");
|
|
4319
|
-
var BIN_DIR =
|
|
4820
|
+
var BIN_DIR = existsSync8(DIST_BIN_DIR) ? DIST_BIN_DIR : SRC_BIN_DIR;
|
|
4320
4821
|
var BIN_EXT = BIN_DIR === DIST_BIN_DIR ? ".js" : ".ts";
|
|
4321
4822
|
function bin(name) {
|
|
4322
4823
|
return join8(BIN_DIR, `${name}${BIN_EXT}`);
|
|
@@ -4627,6 +5128,26 @@ var REGISTRY = [
|
|
|
4627
5128
|
},
|
|
4628
5129
|
0
|
|
4629
5130
|
],
|
|
5131
|
+
[
|
|
5132
|
+
{
|
|
5133
|
+
name: "changeset-coverage",
|
|
5134
|
+
run: bin("check-changeset-coverage"),
|
|
5135
|
+
scope: "diff",
|
|
5136
|
+
timeoutMs: 15000,
|
|
5137
|
+
env: {}
|
|
5138
|
+
},
|
|
5139
|
+
0
|
|
5140
|
+
],
|
|
5141
|
+
[
|
|
5142
|
+
{
|
|
5143
|
+
name: "quoted-command",
|
|
5144
|
+
run: bin("check-quoted-command"),
|
|
5145
|
+
scope: "diff",
|
|
5146
|
+
timeoutMs: 15000,
|
|
5147
|
+
env: {}
|
|
5148
|
+
},
|
|
5149
|
+
0
|
|
5150
|
+
],
|
|
4630
5151
|
[
|
|
4631
5152
|
{
|
|
4632
5153
|
name: "main-branch-refusal",
|
|
@@ -4636,6 +5157,19 @@ var REGISTRY = [
|
|
|
4636
5157
|
env: {}
|
|
4637
5158
|
},
|
|
4638
5159
|
0
|
|
5160
|
+
],
|
|
5161
|
+
[
|
|
5162
|
+
{
|
|
5163
|
+
name: "token-collection-wired",
|
|
5164
|
+
run: bin("check-token-collection-wired"),
|
|
5165
|
+
scope: "full",
|
|
5166
|
+
timeoutMs: 15000,
|
|
5167
|
+
env: {
|
|
5168
|
+
CLAUDE_PROJECT_DIR: { optional: true },
|
|
5169
|
+
CLAUDE_CODE_SESSION_ID: { optional: true }
|
|
5170
|
+
}
|
|
5171
|
+
},
|
|
5172
|
+
0
|
|
4639
5173
|
]
|
|
4640
5174
|
];
|
|
4641
5175
|
function coreCheckRegistry() {
|
|
@@ -4843,16 +5377,16 @@ async function runOne(spec, timeoutMs, callerEnv) {
|
|
|
4843
5377
|
const timer = setTimeout(() => {
|
|
4844
5378
|
timedOut = true;
|
|
4845
5379
|
killTree("SIGTERM");
|
|
4846
|
-
escalationDone = new Promise((
|
|
5380
|
+
escalationDone = new Promise((resolve3) => {
|
|
4847
5381
|
const graceTimer = setTimeout(() => {
|
|
4848
5382
|
const pid = proc.pid;
|
|
4849
5383
|
if (pid !== undefined && groupStillAlive(pid))
|
|
4850
5384
|
killTree("SIGKILL");
|
|
4851
|
-
|
|
5385
|
+
resolve3();
|
|
4852
5386
|
}, KILL_GRACE_MS);
|
|
4853
5387
|
cancelEscalation = () => {
|
|
4854
5388
|
clearTimeout(graceTimer);
|
|
4855
|
-
|
|
5389
|
+
resolve3();
|
|
4856
5390
|
};
|
|
4857
5391
|
});
|
|
4858
5392
|
}, timeoutMs);
|
|
@@ -4863,11 +5397,11 @@ async function runOne(spec, timeoutMs, callerEnv) {
|
|
|
4863
5397
|
});
|
|
4864
5398
|
proc.stdout.resume();
|
|
4865
5399
|
let spawnError;
|
|
4866
|
-
const exitCode = await new Promise((
|
|
4867
|
-
proc.on("close", (code) =>
|
|
5400
|
+
const exitCode = await new Promise((resolve3) => {
|
|
5401
|
+
proc.on("close", (code) => resolve3(code));
|
|
4868
5402
|
proc.on("error", (err) => {
|
|
4869
5403
|
spawnError = err;
|
|
4870
|
-
|
|
5404
|
+
resolve3(null);
|
|
4871
5405
|
});
|
|
4872
5406
|
});
|
|
4873
5407
|
clearTimeout(timer);
|
|
@@ -4955,7 +5489,7 @@ async function runChecks(specs, opts) {
|
|
|
4955
5489
|
}
|
|
4956
5490
|
|
|
4957
5491
|
// src/roles/plan.ts
|
|
4958
|
-
import { existsSync as
|
|
5492
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "node:fs";
|
|
4959
5493
|
import { join as join10 } from "node:path";
|
|
4960
5494
|
|
|
4961
5495
|
// ../../packages/sources/src/commands.ts
|
|
@@ -5110,6 +5644,18 @@ var COMMANDS = [
|
|
|
5110
5644
|
],
|
|
5111
5645
|
status: "shipped"
|
|
5112
5646
|
},
|
|
5647
|
+
{
|
|
5648
|
+
name: "pr verify-evidence",
|
|
5649
|
+
description: "Prove a pull request's AEG:EVIDENCE region was machine-generated — regenerate it and compare",
|
|
5650
|
+
flags: [],
|
|
5651
|
+
details: [
|
|
5652
|
+
"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.",
|
|
5653
|
+
"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.",
|
|
5654
|
+
"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.",
|
|
5655
|
+
"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."
|
|
5656
|
+
],
|
|
5657
|
+
status: "shipped"
|
|
5658
|
+
},
|
|
5113
5659
|
{
|
|
5114
5660
|
name: "issue create",
|
|
5115
5661
|
description: "Open an issue after full brief-schema validation",
|
|
@@ -5186,6 +5732,19 @@ var COMMANDS = [
|
|
|
5186
5732
|
],
|
|
5187
5733
|
status: "shipped"
|
|
5188
5734
|
},
|
|
5735
|
+
{
|
|
5736
|
+
name: "milestone close",
|
|
5737
|
+
description: "Close a tranche's Milestone after verifying every labeled Issue is actually attached",
|
|
5738
|
+
flags: [
|
|
5739
|
+
{ flag: "--slug", description: "The tranche whose Milestone is being closed" },
|
|
5740
|
+
{ flag: "--validate-only", description: "Run every gate and report PASS without closing the Milestone" },
|
|
5741
|
+
{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }
|
|
5742
|
+
],
|
|
5743
|
+
details: [
|
|
5744
|
+
"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."
|
|
5745
|
+
],
|
|
5746
|
+
status: "shipped"
|
|
5747
|
+
},
|
|
5189
5748
|
{
|
|
5190
5749
|
name: "review post",
|
|
5191
5750
|
description: "Render, post, and self-verify a code-reviewer or security-review verdict comment on a PR",
|
|
@@ -5373,7 +5932,7 @@ var COMMANDS = [
|
|
|
5373
5932
|
}
|
|
5374
5933
|
];
|
|
5375
5934
|
// ../../packages/sources/src/doctrine-file-adapter.ts
|
|
5376
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
5935
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync8 } from "node:fs";
|
|
5377
5936
|
import { join as join9 } from "node:path";
|
|
5378
5937
|
|
|
5379
5938
|
// ../../packages/sources/src/file-adapter.ts
|
|
@@ -5384,14 +5943,14 @@ function readMarkdownDir(root, dir) {
|
|
|
5384
5943
|
const dirPath = join9(root, dir);
|
|
5385
5944
|
return readdirSync4(dirPath).filter((f) => f.endsWith(".md")).sort().map((file) => {
|
|
5386
5945
|
const path = join9(dir, file);
|
|
5387
|
-
return { path, content:
|
|
5946
|
+
return { path, content: readFileSync8(join9(root, path), "utf-8") };
|
|
5388
5947
|
});
|
|
5389
5948
|
}
|
|
5390
5949
|
function createFileDoctrineSource(config = {}) {
|
|
5391
5950
|
const root = config.root ?? DEFAULT_GOVERNANCE_ROOT;
|
|
5392
5951
|
return {
|
|
5393
5952
|
async getDoctrine() {
|
|
5394
|
-
const enforcement =
|
|
5953
|
+
const enforcement = readFileSync8(join9(root, "enforcement.md"), "utf-8");
|
|
5395
5954
|
const roles = readMarkdownDir(root, "roles");
|
|
5396
5955
|
const contracts = readMarkdownDir(root, "contracts");
|
|
5397
5956
|
return { enforcement, roles, contracts };
|
|
@@ -5566,11 +6125,11 @@ function resolveRoles(core, configRoles) {
|
|
|
5566
6125
|
var NO_DOCTRINE_REASON = "no bundled doctrine found next to this CLI install — the doctrine ships inside the @attalabs/vinaya npm package. Reinstall it, or, in a repo that vendors the CLI, run the package's bundle-doctrine script first.";
|
|
5567
6126
|
function readContract(configDir, contractPath) {
|
|
5568
6127
|
const absolute = join10(configDir, contractPath);
|
|
5569
|
-
if (!
|
|
6128
|
+
if (!existsSync9(absolute)) {
|
|
5570
6129
|
return { ok: false, errors: [`contract file "${contractPath}" does not exist (resolved to "${absolute}")`] };
|
|
5571
6130
|
}
|
|
5572
6131
|
try {
|
|
5573
|
-
return validateRoleContract(
|
|
6132
|
+
return validateRoleContract(readFileSync9(absolute, "utf-8"));
|
|
5574
6133
|
} catch (err) {
|
|
5575
6134
|
return { ok: false, errors: [`could not read "${contractPath}": ${err.message}`] };
|
|
5576
6135
|
}
|
|
@@ -5603,7 +6162,7 @@ async function buildRolePlan(configDir, configRoles) {
|
|
|
5603
6162
|
// src/commands/check.ts
|
|
5604
6163
|
function git(args) {
|
|
5605
6164
|
try {
|
|
5606
|
-
return
|
|
6165
|
+
return execFileSync7("git", args, { encoding: "utf8" }).trim();
|
|
5607
6166
|
} catch {
|
|
5608
6167
|
return "";
|
|
5609
6168
|
}
|
|
@@ -5841,7 +6400,7 @@ ${renderRolesTable(rolePlan)}
|
|
|
5841
6400
|
}
|
|
5842
6401
|
|
|
5843
6402
|
// src/commands/commit-msg.ts
|
|
5844
|
-
import { readFileSync as
|
|
6403
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
5845
6404
|
function commitMsgCommand(args) {
|
|
5846
6405
|
const [messageFile, source] = args;
|
|
5847
6406
|
if (!messageFile) {
|
|
@@ -5852,7 +6411,7 @@ function commitMsgCommand(args) {
|
|
|
5852
6411
|
}
|
|
5853
6412
|
if (source === "merge")
|
|
5854
6413
|
return;
|
|
5855
|
-
const firstLine2 =
|
|
6414
|
+
const firstLine2 = readFileSync10(messageFile, "utf-8").split(`
|
|
5856
6415
|
`, 1)[0] ?? "";
|
|
5857
6416
|
if (COMMIT_TYPE_STYLE.test(firstLine2))
|
|
5858
6417
|
return;
|
|
@@ -5864,14 +6423,14 @@ function commitMsgCommand(args) {
|
|
|
5864
6423
|
}
|
|
5865
6424
|
|
|
5866
6425
|
// src/commands/demo.ts
|
|
5867
|
-
import { execFileSync as
|
|
5868
|
-
import { existsSync as
|
|
6426
|
+
import { execFileSync as execFileSync8, spawnSync } from "node:child_process";
|
|
6427
|
+
import { existsSync as existsSync10, readFileSync as readFileSync11, rmSync as rmSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5869
6428
|
import { isAbsolute as isAbsolute2, join as join11 } from "node:path";
|
|
5870
6429
|
var DEMO_BRANCH_PREFIX = "vinaya/demo-break-";
|
|
5871
6430
|
var FIXTURE_PATH = ".vinaya-demo-brief.md";
|
|
5872
6431
|
var STATE_FILENAME = "vinaya-demo-state.json";
|
|
5873
6432
|
function gitCapture(repoRoot, args) {
|
|
5874
|
-
return
|
|
6433
|
+
return execFileSync8("git", args, { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
5875
6434
|
}
|
|
5876
6435
|
function currentBranchName(repoRoot) {
|
|
5877
6436
|
try {
|
|
@@ -5882,7 +6441,7 @@ function currentBranchName(repoRoot) {
|
|
|
5882
6441
|
}
|
|
5883
6442
|
function branchExists(repoRoot, name) {
|
|
5884
6443
|
try {
|
|
5885
|
-
|
|
6444
|
+
execFileSync8("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], {
|
|
5886
6445
|
cwd: repoRoot,
|
|
5887
6446
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5888
6447
|
});
|
|
@@ -5908,9 +6467,9 @@ function gitDirAbs(repoRoot) {
|
|
|
5908
6467
|
function recoverFromCrash(repoRoot, statePath) {
|
|
5909
6468
|
const currentBranch = currentBranchName(repoRoot);
|
|
5910
6469
|
let state = null;
|
|
5911
|
-
if (
|
|
6470
|
+
if (existsSync10(statePath)) {
|
|
5912
6471
|
try {
|
|
5913
|
-
state = JSON.parse(
|
|
6472
|
+
state = JSON.parse(readFileSync11(statePath, "utf-8"));
|
|
5914
6473
|
} catch {
|
|
5915
6474
|
state = null;
|
|
5916
6475
|
}
|
|
@@ -5980,14 +6539,14 @@ function cleanup(repoRoot, originalBranch, demoBranch, statePath) {
|
|
|
5980
6539
|
gitCapture(repoRoot, ["clean", "-fd"]);
|
|
5981
6540
|
gitCapture(repoRoot, ["checkout", originalBranch]);
|
|
5982
6541
|
gitCapture(repoRoot, ["branch", "-D", demoBranch]);
|
|
5983
|
-
if (
|
|
6542
|
+
if (existsSync10(statePath))
|
|
5984
6543
|
rmSync2(statePath, { force: true });
|
|
5985
6544
|
}
|
|
5986
6545
|
async function runDemoBreak(repoRoot, args) {
|
|
5987
6546
|
const keep = args.includes("--keep");
|
|
5988
6547
|
const hookDir = resolveHookDir(repoRoot);
|
|
5989
6548
|
const hookPath = resolveManagedBlockPath(repoRoot, `${hookDir}/pre-commit`);
|
|
5990
|
-
if (!
|
|
6549
|
+
if (!existsSync10(hookPath)) {
|
|
5991
6550
|
console.error("Vinaya hooks are not installed in this repo. Run `vinaya init` first.");
|
|
5992
6551
|
return 1;
|
|
5993
6552
|
}
|
|
@@ -6057,7 +6616,7 @@ Attempting to commit…
|
|
|
6057
6616
|
process.stdout.write(`✓ Commit passed — the fix worked.
|
|
6058
6617
|
`);
|
|
6059
6618
|
if (keep) {
|
|
6060
|
-
if (
|
|
6619
|
+
if (existsSync10(statePath))
|
|
6061
6620
|
unlinkSync(statePath);
|
|
6062
6621
|
process.stdout.write(`
|
|
6063
6622
|
--keep: leaving \`${demoBranch}\` checked out for you to inspect. Clean up with:
|
|
@@ -6084,12 +6643,12 @@ async function demoBreakCommand(args) {
|
|
|
6084
6643
|
}
|
|
6085
6644
|
|
|
6086
6645
|
// src/commands/doctor.ts
|
|
6087
|
-
import { execFileSync as
|
|
6088
|
-
import { existsSync as
|
|
6089
|
-
import { delimiter, join as
|
|
6646
|
+
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
6647
|
+
import { existsSync as existsSync13, readdirSync as readdirSync6, readFileSync as readFileSync15, statSync as statSync2 } from "node:fs";
|
|
6648
|
+
import { delimiter, join as join14 } from "node:path";
|
|
6090
6649
|
|
|
6091
6650
|
// src/lib/self-host.ts
|
|
6092
|
-
import { readdirSync as readdirSync5, readFileSync as
|
|
6651
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync12, realpathSync as realpathSync2 } from "node:fs";
|
|
6093
6652
|
import { join as join12, sep as sep3 } from "node:path";
|
|
6094
6653
|
var VINAYA_PACKAGE_NAME = "@attalabs/vinaya";
|
|
6095
6654
|
var DEFAULT_BIN = "dist/index.js";
|
|
@@ -6108,7 +6667,7 @@ function isSafeRelPath(p) {
|
|
|
6108
6667
|
}
|
|
6109
6668
|
function readPackageJson(path) {
|
|
6110
6669
|
try {
|
|
6111
|
-
const parsed = JSON.parse(
|
|
6670
|
+
const parsed = JSON.parse(readFileSync12(path, "utf-8"));
|
|
6112
6671
|
return parsed && typeof parsed === "object" ? parsed : null;
|
|
6113
6672
|
} catch {
|
|
6114
6673
|
return null;
|
|
@@ -6162,8 +6721,8 @@ function expandPattern(repoRoot, pattern) {
|
|
|
6162
6721
|
}
|
|
6163
6722
|
function resolvesInsideRepo(repoRoot, rel) {
|
|
6164
6723
|
try {
|
|
6165
|
-
const root =
|
|
6166
|
-
const target =
|
|
6724
|
+
const root = realpathSync2(repoRoot);
|
|
6725
|
+
const target = realpathSync2(join12(repoRoot, rel));
|
|
6167
6726
|
return target === root || target.startsWith(`${root}${sep3}`);
|
|
6168
6727
|
} catch {
|
|
6169
6728
|
return false;
|
|
@@ -6203,18 +6762,18 @@ function detectVendoredVinaya(repoRoot) {
|
|
|
6203
6762
|
}
|
|
6204
6763
|
|
|
6205
6764
|
// src/lib/env-lint.ts
|
|
6206
|
-
import { existsSync as
|
|
6765
|
+
import { existsSync as existsSync11, readFileSync as readFileSync13 } from "node:fs";
|
|
6207
6766
|
var ENV_READ_PATTERN = /\b(?:process\.env|Bun\.env|Deno\.env)\b/;
|
|
6208
6767
|
function checksMissingEnvDeclaration(specs) {
|
|
6209
6768
|
const names = [];
|
|
6210
6769
|
for (const spec of specs) {
|
|
6211
6770
|
if (spec.env)
|
|
6212
6771
|
continue;
|
|
6213
|
-
if (!
|
|
6772
|
+
if (!existsSync11(spec.run))
|
|
6214
6773
|
continue;
|
|
6215
6774
|
let source;
|
|
6216
6775
|
try {
|
|
6217
|
-
source =
|
|
6776
|
+
source = readFileSync13(spec.run, "utf8");
|
|
6218
6777
|
} catch {
|
|
6219
6778
|
continue;
|
|
6220
6779
|
}
|
|
@@ -6227,40 +6786,171 @@ function envDeclarationWarning(name) {
|
|
|
6227
6786
|
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
6787
|
}
|
|
6229
6788
|
|
|
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
|
-
};
|
|
6789
|
+
// src/lib/registry-write.ts
|
|
6790
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync4 } from "node:fs";
|
|
6791
|
+
import { dirname as dirname6, join as join13 } from "node:path";
|
|
6792
|
+
var PROJECTS_REGISTRY_PATH = ".vinaya/projects.md";
|
|
6793
|
+
var CONFIG_FILE_PATH = "vinaya.config.json";
|
|
6794
|
+
function rowLine(name, path, specsPath) {
|
|
6795
|
+
return `| ${name} | \`${path}\` | \`${specsPath}\` | (state tracked globally) |`;
|
|
6252
6796
|
}
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6797
|
+
function freshProjectsRegistry(name, path, specsPath) {
|
|
6798
|
+
return `---
|
|
6799
|
+
sidebar_title: Projects
|
|
6800
|
+
---
|
|
6801
|
+
# Projects in this repo
|
|
6802
|
+
|
|
6803
|
+
**The project registry.** Declares the projects in this repo and where each
|
|
6804
|
+
one's specs and per-project state live. The \`Project\` field on a task (a
|
|
6805
|
+
forge Issue) resolves against this file.
|
|
6806
|
+
|
|
6807
|
+
**Presence of this file means this is a multi-project repo** — the
|
|
6808
|
+
\`Project\` field is required on task Issues/PRs that touch a registered
|
|
6809
|
+
project's path. A project is a \`(name, path)\` pair you declared — nothing
|
|
6810
|
+
is derived from the folder tree.
|
|
6811
|
+
|
|
6812
|
+
## Registry
|
|
6813
|
+
|
|
6814
|
+
| Project | Path | Specs | Per-project state |
|
|
6815
|
+
|---------|------|-------|---------------------|
|
|
6816
|
+
${rowLine(name, path, specsPath)}
|
|
6817
|
+
`;
|
|
6818
|
+
}
|
|
6819
|
+
var REGISTRY_HEADING_RE = /^##\s+Registry\b/i;
|
|
6820
|
+
function appendRegistryRow(content, name, path, specsPath) {
|
|
6821
|
+
const lines = content.split(/\r?\n/);
|
|
6822
|
+
const headingIdx = lines.findIndex((l) => REGISTRY_HEADING_RE.test(l));
|
|
6823
|
+
const newRow = rowLine(name, path, specsPath);
|
|
6824
|
+
if (headingIdx === -1) {
|
|
6825
|
+
const sep4 = content.endsWith(`
|
|
6826
|
+
`) ? "" : `
|
|
6827
|
+
`;
|
|
6828
|
+
return `${content}${sep4}
|
|
6829
|
+
## Registry
|
|
6830
|
+
|
|
6831
|
+
| Project | Path | Specs | Per-project state |
|
|
6832
|
+
|---------|------|-------|---------------------|
|
|
6833
|
+
${newRow}
|
|
6834
|
+
`;
|
|
6835
|
+
}
|
|
6836
|
+
let i = headingIdx + 1;
|
|
6837
|
+
for (;i < lines.length; i++) {
|
|
6838
|
+
if ((lines[i] ?? "").trim().startsWith("|"))
|
|
6839
|
+
break;
|
|
6840
|
+
}
|
|
6841
|
+
if (i >= lines.length) {
|
|
6842
|
+
lines.splice(headingIdx + 1, 0, "", "| Project | Path | Specs | Per-project state |", "|---------|------|-------|---------------------|", newRow);
|
|
6843
|
+
return lines.join(`
|
|
6844
|
+
`);
|
|
6845
|
+
}
|
|
6846
|
+
let last = i + 1;
|
|
6847
|
+
for (let j = i + 2;j < lines.length; j++) {
|
|
6848
|
+
if (!(lines[j] ?? "").trim().startsWith("|"))
|
|
6849
|
+
break;
|
|
6850
|
+
last = j;
|
|
6851
|
+
}
|
|
6852
|
+
lines.splice(last + 1, 0, newRow);
|
|
6853
|
+
return lines.join(`
|
|
6854
|
+
`);
|
|
6855
|
+
}
|
|
6856
|
+
function planRegistryRow(repoRoot, name, path, specsPath) {
|
|
6857
|
+
const abs2 = join13(repoRoot, PROJECTS_REGISTRY_PATH);
|
|
6858
|
+
const line = rowLine(name, path, specsPath);
|
|
6859
|
+
if (!existsSync12(abs2)) {
|
|
6860
|
+
return { action: "create-host", rowLine: line, path: PROJECTS_REGISTRY_PATH };
|
|
6861
|
+
}
|
|
6862
|
+
const existing = readFileSync14(abs2, "utf-8");
|
|
6863
|
+
const already = parseRegistry(existing).some((p) => p.name === name);
|
|
6864
|
+
return { action: already ? "skip-present" : "append-row", rowLine: line, path: PROJECTS_REGISTRY_PATH };
|
|
6865
|
+
}
|
|
6866
|
+
function applyRegistryRow(repoRoot, plan, name, path, specsPath) {
|
|
6867
|
+
const abs2 = join13(repoRoot, PROJECTS_REGISTRY_PATH);
|
|
6868
|
+
if (plan.action === "create-host") {
|
|
6869
|
+
mkdirSync3(dirname6(abs2), { recursive: true });
|
|
6870
|
+
writeFileSync4(abs2, freshProjectsRegistry(name, path, specsPath), "utf-8");
|
|
6871
|
+
} else if (plan.action === "append-row") {
|
|
6872
|
+
const existing = readFileSync14(abs2, "utf-8");
|
|
6873
|
+
writeFileSync4(abs2, appendRegistryRow(existing, name, path, specsPath), "utf-8");
|
|
6874
|
+
}
|
|
6875
|
+
}
|
|
6876
|
+
function renderRegistryRowDiffLine(plan) {
|
|
6877
|
+
if (plan.action === "skip-present")
|
|
6878
|
+
return ` = keep ${plan.path} (project already registered)`;
|
|
6879
|
+
if (plan.action === "append-row")
|
|
6880
|
+
return ` ~ append row to ${plan.path}
|
|
6881
|
+
${plan.rowLine}`;
|
|
6882
|
+
return ` + create ${plan.path} (with registry row)
|
|
6883
|
+
${plan.rowLine}`;
|
|
6884
|
+
}
|
|
6885
|
+
function readProjectEntries(repoRoot) {
|
|
6886
|
+
const abs2 = join13(repoRoot, CONFIG_FILE_PATH);
|
|
6887
|
+
if (!existsSync12(abs2))
|
|
6888
|
+
return [];
|
|
6889
|
+
try {
|
|
6890
|
+
return VinayaConfigSchema.parse(JSON.parse(readFileSync14(abs2, "utf-8"))).projects ?? [];
|
|
6891
|
+
} catch {
|
|
6892
|
+
return [];
|
|
6893
|
+
}
|
|
6894
|
+
}
|
|
6895
|
+
function planConfigProjectEntry(repoRoot, entry2) {
|
|
6896
|
+
const already = readProjectEntries(repoRoot).some((p) => p.name === entry2.name);
|
|
6897
|
+
return { action: already ? "skip-present" : "add-entry", entry: entry2 };
|
|
6898
|
+
}
|
|
6899
|
+
function applyConfigProjectEntry(repoRoot, plan) {
|
|
6900
|
+
if (plan.action === "skip-present")
|
|
6901
|
+
return;
|
|
6902
|
+
const abs2 = join13(repoRoot, CONFIG_FILE_PATH);
|
|
6903
|
+
const seed = JSON.parse(readFileSync14(abs2, "utf-8"));
|
|
6904
|
+
const projects = Array.isArray(seed.projects) ? seed.projects : [];
|
|
6905
|
+
writeFileSync4(abs2, `${JSON.stringify({ ...seed, projects: [...projects, plan.entry] }, null, 2)}
|
|
6906
|
+
`, "utf-8");
|
|
6907
|
+
}
|
|
6908
|
+
function renderConfigProjectEntryDiffLine(plan) {
|
|
6909
|
+
if (plan.action === "skip-present")
|
|
6910
|
+
return ` = keep ${CONFIG_FILE_PATH} (project already declared under "projects")`;
|
|
6911
|
+
const bits = [`"name": ${JSON.stringify(plan.entry.name)}`];
|
|
6912
|
+
if (plan.entry.path)
|
|
6913
|
+
bits.push(`"path": ${JSON.stringify(plan.entry.path)}`);
|
|
6914
|
+
if (plan.entry.description)
|
|
6915
|
+
bits.push(`"description": ${JSON.stringify(plan.entry.description)}`);
|
|
6916
|
+
return ` ~ append entry to ${CONFIG_FILE_PATH}'s "projects"
|
|
6917
|
+
{ ${bits.join(", ")} }`;
|
|
6918
|
+
}
|
|
6919
|
+
|
|
6920
|
+
// src/commands/doctor.ts
|
|
6921
|
+
function readVersion() {
|
|
6922
|
+
const pkg = JSON.parse(readFileSync15(join14(packageRoot(import.meta.url), "package.json"), "utf-8"));
|
|
6923
|
+
return pkg.version;
|
|
6924
|
+
}
|
|
6925
|
+
function realDeps4() {
|
|
6926
|
+
return {
|
|
6927
|
+
detectRepo: detectGitRepo,
|
|
6928
|
+
ghAuthStatus,
|
|
6929
|
+
branchProtectionConfigured,
|
|
6930
|
+
hookDirFor: resolveHookDir,
|
|
6931
|
+
readHooksPath: readCoreHooksPath,
|
|
6932
|
+
nodeVersion: () => process.version,
|
|
6933
|
+
bunVersion: () => typeof Bun === "undefined" ? null : Bun.version,
|
|
6934
|
+
packageVersion: readVersion,
|
|
6935
|
+
meteringCapability: () => resolveMeteringCapability({
|
|
6936
|
+
env: process.env,
|
|
6937
|
+
cwd: process.cwd(),
|
|
6938
|
+
exists: existsSync13,
|
|
6939
|
+
readFile: (path) => readFileSync15(path, "utf8")
|
|
6940
|
+
})
|
|
6941
|
+
};
|
|
6942
|
+
}
|
|
6943
|
+
var ok = (check, message) => ({ check, severity: "ok", message });
|
|
6944
|
+
var info = (check, message) => ({ check, severity: "info", message });
|
|
6945
|
+
var warn = (check, message) => ({ check, severity: "warn", message });
|
|
6946
|
+
var error = (check, message) => ({ check, severity: "error", message });
|
|
6257
6947
|
function readConfig(repoRoot) {
|
|
6258
|
-
const p =
|
|
6259
|
-
if (!
|
|
6948
|
+
const p = join14(repoRoot, CONFIG_PATH);
|
|
6949
|
+
if (!existsSync13(p))
|
|
6260
6950
|
return { kind: "missing" };
|
|
6261
6951
|
let raw;
|
|
6262
6952
|
try {
|
|
6263
|
-
raw = JSON.parse(
|
|
6953
|
+
raw = JSON.parse(readFileSync15(p, "utf-8"));
|
|
6264
6954
|
} catch (err) {
|
|
6265
6955
|
return { kind: "invalid", error: `invalid JSON: ${err.message}` };
|
|
6266
6956
|
}
|
|
@@ -6299,14 +6989,14 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
|
|
|
6299
6989
|
for (const op of buildInitOps(ctx)) {
|
|
6300
6990
|
if (op.kind === "create-file") {
|
|
6301
6991
|
const check = labelForPath(op.path);
|
|
6302
|
-
const abs2 =
|
|
6303
|
-
const exists =
|
|
6992
|
+
const abs2 = join14(repoRoot, op.path);
|
|
6993
|
+
const exists = existsSync13(abs2);
|
|
6304
6994
|
const owned = ownedFiles.has(op.path) || isDefaultedAgentVendorPath(op.path, manifest);
|
|
6305
6995
|
if (!exists) {
|
|
6306
6996
|
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
6997
|
continue;
|
|
6308
6998
|
}
|
|
6309
|
-
const content =
|
|
6999
|
+
const content = readFileSync15(abs2, "utf-8");
|
|
6310
7000
|
if (!owned) {
|
|
6311
7001
|
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
7002
|
continue;
|
|
@@ -6325,11 +7015,11 @@ function diagnoseInstall(repoRoot, ctx, manifest) {
|
|
|
6325
7015
|
const check = "hooks";
|
|
6326
7016
|
const abs2 = resolveManagedBlockPath(repoRoot, op.path);
|
|
6327
7017
|
const owned = ownedBlocks.has(blockKey(op.path, op.marker));
|
|
6328
|
-
if (!
|
|
7018
|
+
if (!existsSync13(abs2)) {
|
|
6329
7019
|
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
7020
|
continue;
|
|
6331
7021
|
}
|
|
6332
|
-
const content =
|
|
7022
|
+
const content = readFileSync15(abs2, "utf-8");
|
|
6333
7023
|
const { begin, end } = markerLines(op.marker, op.comment);
|
|
6334
7024
|
const hasMarkers = content.includes(begin) && content.includes(end);
|
|
6335
7025
|
if (!hasMarkers) {
|
|
@@ -6377,22 +7067,22 @@ async function diagnoseHookRouting(repoRoot, hookDir, readHooksPath) {
|
|
|
6377
7067
|
function diagnoseCustomChecks(repoRoot, config) {
|
|
6378
7068
|
const findings = [];
|
|
6379
7069
|
for (const [name, entry2] of Object.entries(config.checks ?? {})) {
|
|
6380
|
-
const scriptAbs =
|
|
6381
|
-
findings.push(
|
|
7070
|
+
const scriptAbs = join14(repoRoot, entry2.run);
|
|
7071
|
+
findings.push(existsSync13(scriptAbs) ? ok("checks", `custom check '${name}' → ${entry2.run}`) : error("checks", `custom check '${name}' points at a missing script: ${entry2.run}`));
|
|
6382
7072
|
}
|
|
6383
7073
|
return findings;
|
|
6384
7074
|
}
|
|
6385
7075
|
function listTrackedFiles(repoRoot) {
|
|
6386
7076
|
try {
|
|
6387
|
-
return
|
|
7077
|
+
return execFileSync9("git", ["ls-files"], { cwd: repoRoot, encoding: "utf-8" }).split(`
|
|
6388
7078
|
`).map((s) => s.trim()).filter(Boolean);
|
|
6389
7079
|
} catch {
|
|
6390
7080
|
return [];
|
|
6391
7081
|
}
|
|
6392
7082
|
}
|
|
6393
7083
|
function diagnoseDocOwnersHealth(repoRoot) {
|
|
6394
|
-
const path =
|
|
6395
|
-
const content =
|
|
7084
|
+
const path = join14(repoRoot, DOC_OWNERS_PATH);
|
|
7085
|
+
const content = existsSync13(path) ? readFileSync15(path, "utf-8") : null;
|
|
6396
7086
|
const state = classifyDocOwnersManifest(content);
|
|
6397
7087
|
if (state === "absent" || state === "empty")
|
|
6398
7088
|
return [];
|
|
@@ -6409,8 +7099,8 @@ function diagnoseDocOwnersHealth(repoRoot) {
|
|
|
6409
7099
|
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
7100
|
}
|
|
6411
7101
|
if (!isUrlPointer(b.pointer)) {
|
|
6412
|
-
const pointerPath =
|
|
6413
|
-
if (!
|
|
7102
|
+
const pointerPath = join14(repoRoot, pointerToPath(b.pointer));
|
|
7103
|
+
if (!existsSync13(pointerPath)) {
|
|
6414
7104
|
flagged = true;
|
|
6415
7105
|
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
7106
|
}
|
|
@@ -6425,7 +7115,7 @@ var LEGACY_AEG_PACKAGES_PATH = ".aeg/packages";
|
|
|
6425
7115
|
function readWorkspacesForDoctor(repoRoot) {
|
|
6426
7116
|
const fromPackageJson = () => {
|
|
6427
7117
|
try {
|
|
6428
|
-
const pkg = JSON.parse(
|
|
7118
|
+
const pkg = JSON.parse(readFileSync15(join14(repoRoot, "package.json"), "utf-8"));
|
|
6429
7119
|
return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
|
|
6430
7120
|
} catch {
|
|
6431
7121
|
return [];
|
|
@@ -6433,7 +7123,7 @@ function readWorkspacesForDoctor(repoRoot) {
|
|
|
6433
7123
|
};
|
|
6434
7124
|
const fromPnpmWorkspaceYaml = () => {
|
|
6435
7125
|
try {
|
|
6436
|
-
return parsePnpmWorkspaceYaml(
|
|
7126
|
+
return parsePnpmWorkspaceYaml(readFileSync15(join14(repoRoot, "pnpm-workspace.yaml"), "utf-8"));
|
|
6437
7127
|
} catch {
|
|
6438
7128
|
return [];
|
|
6439
7129
|
}
|
|
@@ -6442,23 +7132,23 @@ function readWorkspacesForDoctor(repoRoot) {
|
|
|
6442
7132
|
}
|
|
6443
7133
|
function listChildDirsForDoctor(dir, repoRoot) {
|
|
6444
7134
|
try {
|
|
6445
|
-
return readdirSync6(
|
|
7135
|
+
return readdirSync6(join14(repoRoot, dir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
6446
7136
|
} catch {
|
|
6447
7137
|
return [];
|
|
6448
7138
|
}
|
|
6449
7139
|
}
|
|
6450
7140
|
function diagnoseBlastRadiusDeprecation(repoRoot, config) {
|
|
6451
7141
|
const derived = deriveWorkspacePackageDomains(readWorkspacesForDoctor(repoRoot), (dir) => listChildDirsForDoctor(dir, repoRoot));
|
|
6452
|
-
const defaults = deriveBuiltinCrossCuttingDefaults((p) =>
|
|
7142
|
+
const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync13(join14(repoRoot, p)));
|
|
6453
7143
|
const covered = new Set([...derived, ...defaults]);
|
|
6454
7144
|
const configExtra = new Set(config?.blastRadius?.extraDomains ?? []);
|
|
6455
|
-
const legacyPath =
|
|
6456
|
-
if (!
|
|
7145
|
+
const legacyPath = join14(repoRoot, LEGACY_AEG_PACKAGES_PATH);
|
|
7146
|
+
if (!existsSync13(legacyPath)) {
|
|
6457
7147
|
return [
|
|
6458
7148
|
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
7149
|
];
|
|
6460
7150
|
}
|
|
6461
|
-
const legacyEntries =
|
|
7151
|
+
const legacyEntries = readFileSync15(legacyPath, "utf-8").split(`
|
|
6462
7152
|
`).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
6463
7153
|
const uncovered = legacyEntries.filter((e) => !covered.has(e) && !configExtra.has(e));
|
|
6464
7154
|
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 +7183,7 @@ function diagnoseBriefSchemaDrift(config) {
|
|
|
6493
7183
|
}
|
|
6494
7184
|
function diagnoseEnvDeclarations(repoRoot, config) {
|
|
6495
7185
|
const resolved = resolveChecks(coreCheckRegistry(), config?.checks).resolved;
|
|
6496
|
-
const specs = resolved.map((entry2) => entry2.source === "config" ? { ...entry2.spec, run:
|
|
7186
|
+
const specs = resolved.map((entry2) => entry2.source === "config" ? { ...entry2.spec, run: join14(repoRoot, entry2.spec.run) } : entry2.spec);
|
|
6497
7187
|
const missing = checksMissingEnvDeclaration(specs);
|
|
6498
7188
|
const findings = missing.map((name) => info("env", envDeclarationWarning(name)));
|
|
6499
7189
|
for (const message of lintEnvDeclarations(config?.checks)) {
|
|
@@ -6514,11 +7204,11 @@ function diagnoseCheckClassification(config) {
|
|
|
6514
7204
|
return findings;
|
|
6515
7205
|
}
|
|
6516
7206
|
function diagnoseGlobalConfigChecks() {
|
|
6517
|
-
if (!
|
|
7207
|
+
if (!existsSync13(GLOBAL_CONFIG_PATH))
|
|
6518
7208
|
return [];
|
|
6519
7209
|
let raw;
|
|
6520
7210
|
try {
|
|
6521
|
-
raw = JSON.parse(
|
|
7211
|
+
raw = JSON.parse(readFileSync15(GLOBAL_CONFIG_PATH, "utf-8"));
|
|
6522
7212
|
} catch (err) {
|
|
6523
7213
|
return [info("checks", `${GLOBAL_CONFIG_PATH} is invalid JSON — ${err.message}`)];
|
|
6524
7214
|
}
|
|
@@ -6567,13 +7257,13 @@ function coversWorkflowsDir(pattern) {
|
|
|
6567
7257
|
return bare === ".github/workflows" || bare === "/.github/workflows";
|
|
6568
7258
|
}
|
|
6569
7259
|
function diagnoseCodeowners(repoRoot) {
|
|
6570
|
-
const path =
|
|
6571
|
-
if (!
|
|
7260
|
+
const path = join14(repoRoot, ".github", "CODEOWNERS");
|
|
7261
|
+
if (!existsSync13(path)) {
|
|
6572
7262
|
return info("codeowners", "no .github/CODEOWNERS — vinaya's workflow files have no required-review protection; see `vinaya init`'s printed recommendation.");
|
|
6573
7263
|
}
|
|
6574
7264
|
let body;
|
|
6575
7265
|
try {
|
|
6576
|
-
body =
|
|
7266
|
+
body = readFileSync15(path, "utf-8");
|
|
6577
7267
|
} catch (err) {
|
|
6578
7268
|
return warn("codeowners", `.github/CODEOWNERS exists but could not be read: ${err.message}`);
|
|
6579
7269
|
}
|
|
@@ -6586,7 +7276,7 @@ function diagnoseVinayaOnPath(agents) {
|
|
|
6586
7276
|
return [];
|
|
6587
7277
|
const names = process.platform === "win32" ? ["vinaya.cmd", "vinaya.exe", "vinaya.bat"] : ["vinaya"];
|
|
6588
7278
|
const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
6589
|
-
const onPath = dirs.some((dir) => names.some((name) =>
|
|
7279
|
+
const onPath = dirs.some((dir) => names.some((name) => existsSync13(join14(dir, name))));
|
|
6590
7280
|
return [
|
|
6591
7281
|
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
7282
|
];
|
|
@@ -6597,21 +7287,38 @@ function diagnosePrincipals(config) {
|
|
|
6597
7287
|
}
|
|
6598
7288
|
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
7289
|
}
|
|
7290
|
+
function diagnoseProjectsCoherence(repoRoot, config) {
|
|
7291
|
+
const registryAbs = join14(repoRoot, PROJECTS_REGISTRY_PATH);
|
|
7292
|
+
const registryNames = existsSync13(registryAbs) ? new Set(parseRegistry(readFileSync15(registryAbs, "utf-8")).map((p) => p.name)) : new Set;
|
|
7293
|
+
const configNames = new Set((config?.projects ?? []).map((p) => p.name));
|
|
7294
|
+
if (registryNames.size === 0 && configNames.size === 0)
|
|
7295
|
+
return [];
|
|
7296
|
+
const registryOnly = [...registryNames].filter((n) => !configNames.has(n)).sort();
|
|
7297
|
+
const configOnly = [...configNames].filter((n) => !registryNames.has(n)).sort();
|
|
7298
|
+
const findings = [];
|
|
7299
|
+
if (registryOnly.length > 0) {
|
|
7300
|
+
findings.push(info("projects", `${PROJECTS_REGISTRY_PATH} declares ${registryOnly.join(", ")} with no matching vinaya.config.json "projects" entry.`));
|
|
7301
|
+
}
|
|
7302
|
+
if (configOnly.length > 0) {
|
|
7303
|
+
findings.push(info("projects", `vinaya.config.json "projects" declares ${configOnly.join(", ")} with no matching ${PROJECTS_REGISTRY_PATH} row.`));
|
|
7304
|
+
}
|
|
7305
|
+
return findings;
|
|
7306
|
+
}
|
|
6600
7307
|
var TEST_INVOCATION_SUBSTRINGS = ["npm test", "npm run test", "bun test", "bunx turbo test"];
|
|
6601
7308
|
function diagnoseTestCi(repoRoot) {
|
|
6602
7309
|
let pkg;
|
|
6603
7310
|
try {
|
|
6604
|
-
pkg = JSON.parse(
|
|
7311
|
+
pkg = JSON.parse(readFileSync15(join14(repoRoot, "package.json"), "utf-8"));
|
|
6605
7312
|
} catch {
|
|
6606
7313
|
return [];
|
|
6607
7314
|
}
|
|
6608
7315
|
const testScript = pkg?.scripts?.test;
|
|
6609
7316
|
if (typeof testScript !== "string" || testScript.trim() === "")
|
|
6610
7317
|
return [];
|
|
6611
|
-
const workflowsDir =
|
|
6612
|
-
const files =
|
|
7318
|
+
const workflowsDir = join14(repoRoot, ".github/workflows");
|
|
7319
|
+
const files = existsSync13(workflowsDir) ? readdirSync6(workflowsDir).filter((name) => statSync2(join14(workflowsDir, name)).isFile()) : [];
|
|
6613
7320
|
const invoked = files.some((name) => {
|
|
6614
|
-
const content =
|
|
7321
|
+
const content = readFileSync15(join14(workflowsDir, name), "utf-8");
|
|
6615
7322
|
return TEST_INVOCATION_SUBSTRINGS.some((s) => content.includes(s));
|
|
6616
7323
|
});
|
|
6617
7324
|
if (invoked)
|
|
@@ -6678,6 +7385,7 @@ async function runDoctor(args, deps) {
|
|
|
6678
7385
|
findings.push(diagnosePrincipals(configRead.config));
|
|
6679
7386
|
findings.push(...diagnoseVinayaOnPath(ctx.agents));
|
|
6680
7387
|
}
|
|
7388
|
+
findings.push(...diagnoseProjectsCoherence(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
|
|
6681
7389
|
findings.push(...diagnoseBlastRadiusDeprecation(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
|
|
6682
7390
|
findings.push(...diagnoseBriefSchemaDrift(configRead.kind === "ok" ? configRead.config : null));
|
|
6683
7391
|
findings.push(...diagnoseEnvDeclarations(repo.repoRoot, configRead.kind === "ok" ? configRead.config : null));
|
|
@@ -6697,13 +7405,13 @@ async function runDoctor(args, deps) {
|
|
|
6697
7405
|
return healthy ? 0 : 1;
|
|
6698
7406
|
}
|
|
6699
7407
|
async function doctorCommand(args) {
|
|
6700
|
-
process.exit(await runDoctor(args,
|
|
7408
|
+
process.exit(await runDoctor(args, realDeps4()));
|
|
6701
7409
|
}
|
|
6702
7410
|
|
|
6703
7411
|
// src/commands/eject.ts
|
|
6704
|
-
import { existsSync as
|
|
6705
|
-
import { join as
|
|
6706
|
-
function
|
|
7412
|
+
import { existsSync as existsSync14, readFileSync as readFileSync16 } from "node:fs";
|
|
7413
|
+
import { join as join15 } from "node:path";
|
|
7414
|
+
function realDeps5() {
|
|
6707
7415
|
return {
|
|
6708
7416
|
detectRepo: detectGitRepo,
|
|
6709
7417
|
readHooksPath: readCoreHooksPath,
|
|
@@ -6719,12 +7427,12 @@ function parse(args) {
|
|
|
6719
7427
|
return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
|
|
6720
7428
|
}
|
|
6721
7429
|
function readManifest(repoRoot) {
|
|
6722
|
-
const p =
|
|
6723
|
-
if (!
|
|
7430
|
+
const p = join15(repoRoot, CONFIG_PATH);
|
|
7431
|
+
if (!existsSync14(p))
|
|
6724
7432
|
return { kind: "none" };
|
|
6725
7433
|
let raw;
|
|
6726
7434
|
try {
|
|
6727
|
-
raw = JSON.parse(
|
|
7435
|
+
raw = JSON.parse(readFileSync16(p, "utf-8"));
|
|
6728
7436
|
} catch (err) {
|
|
6729
7437
|
return { kind: "orphan", reason: `vinaya.config.json is not valid JSON: ${err.message}` };
|
|
6730
7438
|
}
|
|
@@ -6809,112 +7517,14 @@ Vinaya ejected.
|
|
|
6809
7517
|
return 0;
|
|
6810
7518
|
}
|
|
6811
7519
|
async function ejectCommand(args) {
|
|
6812
|
-
const code = await runEject(args,
|
|
7520
|
+
const code = await runEject(args, realDeps5());
|
|
6813
7521
|
process.exit(code);
|
|
6814
7522
|
}
|
|
6815
7523
|
|
|
6816
7524
|
// src/commands/init.ts
|
|
6817
|
-
import { existsSync as
|
|
7525
|
+
import { existsSync as existsSync15, readFileSync as readFileSync17, writeFileSync as writeFileSync5 } from "node:fs";
|
|
6818
7526
|
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() {
|
|
7527
|
+
function realDeps6() {
|
|
6918
7528
|
return {
|
|
6919
7529
|
detectRepo: detectGitRepo,
|
|
6920
7530
|
checkGhAuth,
|
|
@@ -6954,17 +7564,17 @@ function parseAgentsFlag(args) {
|
|
|
6954
7564
|
var PRODUCT_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
6955
7565
|
function readManifest2(repoRoot) {
|
|
6956
7566
|
const p = join16(repoRoot, CONFIG_PATH);
|
|
6957
|
-
if (!
|
|
7567
|
+
if (!existsSync15(p))
|
|
6958
7568
|
return null;
|
|
6959
7569
|
try {
|
|
6960
|
-
return VinayaConfigSchema.parse(JSON.parse(
|
|
7570
|
+
return VinayaConfigSchema.parse(JSON.parse(readFileSync17(p, "utf-8"))).managed ?? null;
|
|
6961
7571
|
} catch {
|
|
6962
7572
|
return null;
|
|
6963
7573
|
}
|
|
6964
7574
|
}
|
|
6965
7575
|
function writeManifest(repoRoot, manifest) {
|
|
6966
7576
|
const configAbs = join16(repoRoot, CONFIG_PATH);
|
|
6967
|
-
const seed = JSON.parse(
|
|
7577
|
+
const seed = JSON.parse(readFileSync17(configAbs, "utf-8"));
|
|
6968
7578
|
writeFileSync5(configAbs, `${JSON.stringify({ ...seed, managed: manifest }, null, 2)}
|
|
6969
7579
|
`, "utf-8");
|
|
6970
7580
|
}
|
|
@@ -7072,6 +7682,7 @@ async function runInitProduct(args, deps) {
|
|
|
7072
7682
|
return 1;
|
|
7073
7683
|
}
|
|
7074
7684
|
const registryPlan = planRegistryRow(repo.repoRoot, name, productPath, specsPath);
|
|
7685
|
+
const configPlan = planConfigProjectEntry(repo.repoRoot, { name, path: productPath });
|
|
7075
7686
|
process.stdout.write(`vinaya init product ${name} — the full diff:
|
|
7076
7687
|
|
|
7077
7688
|
`);
|
|
@@ -7079,6 +7690,11 @@ async function runInitProduct(args, deps) {
|
|
|
7079
7690
|
`);
|
|
7080
7691
|
process.stdout.write(`${renderRegistryRowDiffLine(registryPlan)}
|
|
7081
7692
|
|
|
7693
|
+
`);
|
|
7694
|
+
process.stdout.write(`── Config ────────────────────────────────────────
|
|
7695
|
+
`);
|
|
7696
|
+
process.stdout.write(`${renderConfigProjectEntryDiffLine(configPlan)}
|
|
7697
|
+
|
|
7082
7698
|
`);
|
|
7083
7699
|
if (dryRun) {
|
|
7084
7700
|
process.stdout.write(`--dry-run: nothing was written.
|
|
@@ -7094,25 +7710,26 @@ async function runInitProduct(args, deps) {
|
|
|
7094
7710
|
}
|
|
7095
7711
|
}
|
|
7096
7712
|
applyRegistryRow(repo.repoRoot, registryPlan, name, productPath, specsPath);
|
|
7713
|
+
applyConfigProjectEntry(repo.repoRoot, configPlan);
|
|
7097
7714
|
process.stdout.write(`
|
|
7098
7715
|
Governed product area '${name}' scaffolded.
|
|
7099
7716
|
`);
|
|
7100
7717
|
return 0;
|
|
7101
7718
|
}
|
|
7102
7719
|
async function initCommand(args) {
|
|
7103
|
-
process.exit(await runInit(args,
|
|
7720
|
+
process.exit(await runInit(args, realDeps6()));
|
|
7104
7721
|
}
|
|
7105
7722
|
async function initProductCommand(args) {
|
|
7106
|
-
process.exit(await runInitProduct(args,
|
|
7723
|
+
process.exit(await runInitProduct(args, realDeps6()));
|
|
7107
7724
|
}
|
|
7108
7725
|
|
|
7109
7726
|
// src/commands/issue.ts
|
|
7110
|
-
import { execFileSync as
|
|
7727
|
+
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
7111
7728
|
|
|
7112
7729
|
// src/lib/forge-write.ts
|
|
7113
|
-
import { execFileSync as
|
|
7114
|
-
import { existsSync as
|
|
7115
|
-
import { readFileSync as
|
|
7730
|
+
import { execFileSync as execFileSync10 } from "node:child_process";
|
|
7731
|
+
import { existsSync as existsSync16, mkdtempSync, readdirSync as readdirSync7, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
|
7732
|
+
import { readFileSync as readFileSync18 } from "node:fs";
|
|
7116
7733
|
import { tmpdir } from "node:os";
|
|
7117
7734
|
import { join as join17 } from "node:path";
|
|
7118
7735
|
class ForgeArgError extends Error {
|
|
@@ -7124,13 +7741,13 @@ function locateBody(args) {
|
|
|
7124
7741
|
const p = args[i + 1];
|
|
7125
7742
|
if (!p)
|
|
7126
7743
|
throw new ForgeArgError("`--body-file` was given with no path.");
|
|
7127
|
-
return { body:
|
|
7744
|
+
return { body: readFileSync18(p, "utf8"), source: { kind: "file", argIndex: i + 1, inlineForm: false } };
|
|
7128
7745
|
}
|
|
7129
7746
|
if (a.startsWith("--body-file=")) {
|
|
7130
7747
|
const p = a.slice("--body-file=".length);
|
|
7131
7748
|
if (!p)
|
|
7132
7749
|
throw new ForgeArgError("`--body-file=` was given with no path.");
|
|
7133
|
-
return { body:
|
|
7750
|
+
return { body: readFileSync18(p, "utf8"), source: { kind: "file", argIndex: i, inlineForm: true } };
|
|
7134
7751
|
}
|
|
7135
7752
|
if (a === "--body" || a === "-b") {
|
|
7136
7753
|
const v = args[i + 1];
|
|
@@ -7204,7 +7821,7 @@ function ensureTrancheLabelExists(slug) {
|
|
|
7204
7821
|
const name = trancheLabel(slug);
|
|
7205
7822
|
let existing;
|
|
7206
7823
|
try {
|
|
7207
|
-
const out =
|
|
7824
|
+
const out = execFileSync10("gh", ["label", "list", "--json", "name", "--limit", "200"], {
|
|
7208
7825
|
encoding: "utf8",
|
|
7209
7826
|
stdio: ["ignore", "pipe", "pipe"]
|
|
7210
7827
|
});
|
|
@@ -7217,7 +7834,7 @@ function ensureTrancheLabelExists(slug) {
|
|
|
7217
7834
|
if (existing.some((l) => l.name === name))
|
|
7218
7835
|
return;
|
|
7219
7836
|
try {
|
|
7220
|
-
|
|
7837
|
+
execFileSync10("gh", ["label", "create", name, "--color", TRANCHE_LABEL_COLOR, "--description", `Tranche: ${slug}`], {
|
|
7221
7838
|
encoding: "utf8",
|
|
7222
7839
|
stdio: ["ignore", "pipe", "pipe"]
|
|
7223
7840
|
});
|
|
@@ -7227,6 +7844,24 @@ function ensureTrancheLabelExists(slug) {
|
|
|
7227
7844
|
]);
|
|
7228
7845
|
}
|
|
7229
7846
|
}
|
|
7847
|
+
function resolveMilestoneAttachArgs(ghArgs, labels) {
|
|
7848
|
+
if (!isTaskIssueLabelSet(labels))
|
|
7849
|
+
return ghArgs;
|
|
7850
|
+
if (hasExplicitMilestoneFlag(ghArgs))
|
|
7851
|
+
return ghArgs;
|
|
7852
|
+
const slug = findTrancheSlug(labels);
|
|
7853
|
+
if (!slug)
|
|
7854
|
+
return ghArgs;
|
|
7855
|
+
let target;
|
|
7856
|
+
try {
|
|
7857
|
+
target = findMilestoneAttachTargetForSlug("{owner}", "{repo}", slug);
|
|
7858
|
+
} catch {
|
|
7859
|
+
process.stderr.write(`vinaya: milestone lookup for '${slug}' failed (\`gh api\`) — creating without --milestone.
|
|
7860
|
+
`);
|
|
7861
|
+
return ghArgs;
|
|
7862
|
+
}
|
|
7863
|
+
return target ? [...ghArgs, "--milestone", target.title] : ghArgs;
|
|
7864
|
+
}
|
|
7230
7865
|
function resolveSections(kind, retryCommand) {
|
|
7231
7866
|
const result = loadConfigChecked();
|
|
7232
7867
|
if (!result.ok) {
|
|
@@ -7351,7 +7986,7 @@ function validateForgeWrite(input) {
|
|
|
7351
7986
|
}
|
|
7352
7987
|
function git2(args) {
|
|
7353
7988
|
try {
|
|
7354
|
-
return
|
|
7989
|
+
return execFileSync10("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
7355
7990
|
} catch {
|
|
7356
7991
|
return "";
|
|
7357
7992
|
}
|
|
@@ -7362,7 +7997,7 @@ function repoRoot() {
|
|
|
7362
7997
|
function readWorkspaceGlobs(root) {
|
|
7363
7998
|
const fromPackageJson = () => {
|
|
7364
7999
|
try {
|
|
7365
|
-
const pkg = JSON.parse(
|
|
8000
|
+
const pkg = JSON.parse(readFileSync18(join17(root, "package.json"), "utf8"));
|
|
7366
8001
|
return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
|
|
7367
8002
|
} catch {
|
|
7368
8003
|
return [];
|
|
@@ -7370,7 +8005,7 @@ function readWorkspaceGlobs(root) {
|
|
|
7370
8005
|
};
|
|
7371
8006
|
const fromPnpmWorkspaceYaml = () => {
|
|
7372
8007
|
try {
|
|
7373
|
-
return parsePnpmWorkspaceYaml(
|
|
8008
|
+
return parsePnpmWorkspaceYaml(readFileSync18(join17(root, "pnpm-workspace.yaml"), "utf8"));
|
|
7374
8009
|
} catch {
|
|
7375
8010
|
return [];
|
|
7376
8011
|
}
|
|
@@ -7386,7 +8021,7 @@ function listWorkspaceChildDirs(dir, root) {
|
|
|
7386
8021
|
}
|
|
7387
8022
|
function readConfigExtraDomains(root) {
|
|
7388
8023
|
try {
|
|
7389
|
-
const parsed = VinayaConfigSchema.safeParse(JSON.parse(
|
|
8024
|
+
const parsed = VinayaConfigSchema.safeParse(JSON.parse(readFileSync18(join17(root, "vinaya.config.json"), "utf8")));
|
|
7390
8025
|
return parsed.success ? parsed.data.blastRadius?.extraDomains ?? [] : [];
|
|
7391
8026
|
} catch {
|
|
7392
8027
|
return [];
|
|
@@ -7396,7 +8031,7 @@ function readSharedPackages(root = repoRoot()) {
|
|
|
7396
8031
|
if (!root)
|
|
7397
8032
|
return [];
|
|
7398
8033
|
const derived = deriveWorkspacePackageDomains(readWorkspaceGlobs(root), (dir) => listWorkspaceChildDirs(dir, root));
|
|
7399
|
-
const defaults = deriveBuiltinCrossCuttingDefaults((p) =>
|
|
8034
|
+
const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync16(join17(root, p)));
|
|
7400
8035
|
const configExtra = readConfigExtraDomains(root);
|
|
7401
8036
|
return [...new Set([...derived, ...defaults, ...configExtra])];
|
|
7402
8037
|
}
|
|
@@ -7404,7 +8039,7 @@ function readProjectPaths(root = repoRoot()) {
|
|
|
7404
8039
|
if (!root)
|
|
7405
8040
|
return [];
|
|
7406
8041
|
try {
|
|
7407
|
-
return parseRegistry(
|
|
8042
|
+
return parseRegistry(readFileSync18(join17(root, ".vinaya/projects.md"), "utf8"));
|
|
7408
8043
|
} catch {
|
|
7409
8044
|
return [];
|
|
7410
8045
|
}
|
|
@@ -7446,7 +8081,7 @@ function locateBodyOrRefuse(ghArgs, retryCommand) {
|
|
|
7446
8081
|
function fetchForgeLabels(issueRef) {
|
|
7447
8082
|
let out;
|
|
7448
8083
|
try {
|
|
7449
|
-
out =
|
|
8084
|
+
out = execFileSync11("gh", ["issue", "view", issueRef, "--json", "labels"], {
|
|
7450
8085
|
encoding: "utf8",
|
|
7451
8086
|
stdio: ["ignore", "pipe", "pipe"]
|
|
7452
8087
|
});
|
|
@@ -7474,7 +8109,7 @@ function reportPass(json, command) {
|
|
|
7474
8109
|
function runGhWrite(ghCmd, ghArgs, bodyResult, json) {
|
|
7475
8110
|
const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
|
|
7476
8111
|
try {
|
|
7477
|
-
const out =
|
|
8112
|
+
const out = execFileSync11("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
|
|
7478
8113
|
const url = out.trim();
|
|
7479
8114
|
if (json)
|
|
7480
8115
|
printJson({ validated: true, written: true, url });
|
|
@@ -7529,7 +8164,7 @@ function issueCreateCommand(args) {
|
|
|
7529
8164
|
const slugToEnsure = findTrancheSlug(labels);
|
|
7530
8165
|
if (slugToEnsure)
|
|
7531
8166
|
ensureTrancheLabelExists(slugToEnsure);
|
|
7532
|
-
runGhWrite(["issue", "create"], ghArgs, bodyResult, json);
|
|
8167
|
+
runGhWrite(["issue", "create"], resolveMilestoneAttachArgs(ghArgs, labels), bodyResult, json);
|
|
7533
8168
|
}
|
|
7534
8169
|
function issueEditCommand(args) {
|
|
7535
8170
|
const json = args.includes("--json");
|
|
@@ -7560,12 +8195,13 @@ function issueEditCommand(args) {
|
|
|
7560
8195
|
}
|
|
7561
8196
|
|
|
7562
8197
|
// src/commands/milestone.ts
|
|
7563
|
-
import { execFileSync as
|
|
8198
|
+
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
7564
8199
|
var RETRY_CREATE2 = "vinaya milestone create --title <title> --body-file <path>";
|
|
7565
8200
|
var RETRY_ADOPT = "vinaya milestone adopt --target <title> --slug <slug> [--slug <slug> ...]";
|
|
7566
8201
|
var RETRY_EDIT2 = "vinaya milestone edit <n> --body-file <path>";
|
|
8202
|
+
var RETRY_CLOSE = "vinaya milestone close --slug <slug>";
|
|
7567
8203
|
function sh3(args, input) {
|
|
7568
|
-
return
|
|
8204
|
+
return execFileSync12(args[0], args.slice(1), {
|
|
7569
8205
|
encoding: "utf8",
|
|
7570
8206
|
input,
|
|
7571
8207
|
env: process.env,
|
|
@@ -7723,18 +8359,18 @@ function extractSlugs(args) {
|
|
|
7723
8359
|
}
|
|
7724
8360
|
return slugs;
|
|
7725
8361
|
}
|
|
7726
|
-
function ghJsonOrRefuse(args, what) {
|
|
8362
|
+
function ghJsonOrRefuse(args, what, retryCommand) {
|
|
7727
8363
|
try {
|
|
7728
8364
|
return shJson2(args);
|
|
7729
8365
|
} catch (e) {
|
|
7730
8366
|
refuse([
|
|
7731
|
-
makeCheckError("forge-fetch", `could not fetch ${what} from the forge: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${
|
|
8367
|
+
makeCheckError("forge-fetch", `could not fetch ${what} from the forge: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${retryCommand}\`.`)
|
|
7732
8368
|
]);
|
|
7733
8369
|
}
|
|
7734
8370
|
}
|
|
7735
8371
|
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");
|
|
8372
|
+
const labels = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/labels?per_page=100`], "this repo's labels", RETRY_ADOPT);
|
|
8373
|
+
const milestones = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/milestones?state=all&per_page=100`], "this repo's Milestones", RETRY_ADOPT);
|
|
7738
8374
|
const targetMilestone = milestones.find((m) => m.title === target);
|
|
7739
8375
|
const targetFacts = {
|
|
7740
8376
|
title: target,
|
|
@@ -7757,7 +8393,7 @@ function fetchAdoptFacts(repoFlag, target, slugs) {
|
|
|
7757
8393
|
"number,state,milestone",
|
|
7758
8394
|
"--limit",
|
|
7759
8395
|
"200"
|
|
7760
|
-
], `Issues for \`${trancheLabel(slug)}
|
|
8396
|
+
], `Issues for \`${trancheLabel(slug)}\``, RETRY_ADOPT) : [];
|
|
7761
8397
|
const elsewhere = new Set;
|
|
7762
8398
|
for (const issue of issues) {
|
|
7763
8399
|
const title = issue.milestone?.title;
|
|
@@ -7818,41 +8454,134 @@ async function milestoneAdoptCommand(args) {
|
|
|
7818
8454
|
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)"}.`);
|
|
7819
8455
|
}
|
|
7820
8456
|
}
|
|
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
|
-
}
|
|
8457
|
+
const legacy = milestones.find((m) => m.title === slugFact.slug && m.title !== target);
|
|
8458
|
+
let closedMilestone = null;
|
|
8459
|
+
if (legacy) {
|
|
8460
|
+
closedMilestone = legacy.number;
|
|
8461
|
+
if (legacy.state !== "closed") {
|
|
8462
|
+
try {
|
|
8463
|
+
sh3(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${legacy.number}`, "-f", "state=closed"]);
|
|
8464
|
+
} catch (e) {
|
|
8465
|
+
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)"}.`);
|
|
8466
|
+
}
|
|
8467
|
+
}
|
|
8468
|
+
}
|
|
8469
|
+
adopted.push({ slug: slugFact.slug, issueCount: slugFact.issueNumbers.length, closedMilestone });
|
|
8470
|
+
}
|
|
8471
|
+
if (json)
|
|
8472
|
+
printJson({ validated: true, written: true, target, adopted });
|
|
8473
|
+
else {
|
|
8474
|
+
for (const a of adopted) {
|
|
8475
|
+
const closedNote = a.closedMilestone ? ` (old Milestone #${a.closedMilestone} closed)` : "";
|
|
8476
|
+
process.stdout.write(`${a.slug}: ${a.issueCount} Issue(s) → "${target}"${closedNote}
|
|
8477
|
+
`);
|
|
8478
|
+
}
|
|
8479
|
+
}
|
|
8480
|
+
}
|
|
8481
|
+
function extractSlug(args) {
|
|
8482
|
+
for (let i = 0;i < args.length; i++) {
|
|
8483
|
+
const a = args[i];
|
|
8484
|
+
if (a === "--slug")
|
|
8485
|
+
return args[i + 1] ?? null;
|
|
8486
|
+
if (a.startsWith("--slug="))
|
|
8487
|
+
return a.slice("--slug=".length);
|
|
8488
|
+
}
|
|
8489
|
+
return null;
|
|
8490
|
+
}
|
|
8491
|
+
async function milestoneCloseCommand(args) {
|
|
8492
|
+
const json = args.includes("--json");
|
|
8493
|
+
const validateOnly = args.includes("--validate-only");
|
|
8494
|
+
const rest = args.filter((a) => a !== "--json" && a !== "--validate-only");
|
|
8495
|
+
const slug = extractSlug(rest);
|
|
8496
|
+
if (!slug) {
|
|
8497
|
+
refuse([
|
|
8498
|
+
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}\`.`)
|
|
8499
|
+
]);
|
|
8500
|
+
}
|
|
8501
|
+
const repoFlag = await resolveRepoFlagOrRefuse(RETRY_CLOSE);
|
|
8502
|
+
const milestones = ghJsonOrRefuse(["gh", "api", `repos/${repoFlag}/milestones?state=all&per_page=100`], "this repo's Milestones", RETRY_CLOSE);
|
|
8503
|
+
const target = resolveMilestoneAttachTarget(milestones, slug);
|
|
8504
|
+
if (!target) {
|
|
8505
|
+
refuse([
|
|
8506
|
+
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}\`.`)
|
|
8507
|
+
]);
|
|
8508
|
+
}
|
|
8509
|
+
const labeledIssues = ghJsonOrRefuse([
|
|
8510
|
+
"gh",
|
|
8511
|
+
"issue",
|
|
8512
|
+
"list",
|
|
8513
|
+
"-R",
|
|
8514
|
+
repoFlag,
|
|
8515
|
+
"--label",
|
|
8516
|
+
trancheLabel(slug),
|
|
8517
|
+
"--state",
|
|
8518
|
+
"all",
|
|
8519
|
+
"--json",
|
|
8520
|
+
"number,state,milestone",
|
|
8521
|
+
"--limit",
|
|
8522
|
+
"200"
|
|
8523
|
+
], `Issues for \`${trancheLabel(slug)}\``, RETRY_CLOSE);
|
|
8524
|
+
const attachedIssues = ghJsonOrRefuse([
|
|
8525
|
+
"gh",
|
|
8526
|
+
"issue",
|
|
8527
|
+
"list",
|
|
8528
|
+
"-R",
|
|
8529
|
+
repoFlag,
|
|
8530
|
+
"--milestone",
|
|
8531
|
+
target.title,
|
|
8532
|
+
"--state",
|
|
8533
|
+
"all",
|
|
8534
|
+
"--json",
|
|
8535
|
+
"number,state,milestone",
|
|
8536
|
+
"--limit",
|
|
8537
|
+
"200"
|
|
8538
|
+
], `Issues attached to Milestone "${target.title}"`, RETRY_CLOSE);
|
|
8539
|
+
const report = checkMilestoneAttachment(labeledIssues.map((i) => i.number), attachedIssues.map((i) => i.number));
|
|
8540
|
+
if (report.status === "mismatch") {
|
|
8541
|
+
const errors = [];
|
|
8542
|
+
if (report.unattached.length > 0) {
|
|
8543
|
+
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}\`.`));
|
|
8544
|
+
}
|
|
8545
|
+
if (report.foreign.length > 0) {
|
|
8546
|
+
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}\`.`));
|
|
7832
8547
|
}
|
|
7833
|
-
|
|
8548
|
+
refuse(errors);
|
|
7834
8549
|
}
|
|
7835
|
-
if (
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
process.stdout.write(`${a.slug}: ${a.issueCount} Issue(s) → "${target}"${closedNote}
|
|
8550
|
+
if (validateOnly) {
|
|
8551
|
+
if (json)
|
|
8552
|
+
printJson({ validated: true, written: false, command: "milestone close", number: target.number });
|
|
8553
|
+
else {
|
|
8554
|
+
process.stdout.write(`✓ Milestone "${target.title}" (#${target.number}) attachment verified — nothing written (--validate-only).
|
|
7841
8555
|
`);
|
|
7842
8556
|
}
|
|
8557
|
+
return;
|
|
8558
|
+
}
|
|
8559
|
+
let out;
|
|
8560
|
+
try {
|
|
8561
|
+
out = sh3(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${target.number}`, "-f", "state=closed"]);
|
|
8562
|
+
} catch (e) {
|
|
8563
|
+
refuse([
|
|
8564
|
+
makeCheckError("forge-fetch", `\`gh api repos/${repoFlag}/milestones/${target.number}\` failed: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_CLOSE}\`.`)
|
|
8565
|
+
]);
|
|
7843
8566
|
}
|
|
8567
|
+
const closed = JSON.parse(out);
|
|
8568
|
+
if (json)
|
|
8569
|
+
printJson({ validated: true, written: true, number: closed.number, url: closed.html_url });
|
|
8570
|
+
else
|
|
8571
|
+
process.stdout.write(`${closed.html_url}
|
|
8572
|
+
`);
|
|
7844
8573
|
}
|
|
7845
8574
|
|
|
7846
8575
|
// src/commands/new-check.ts
|
|
7847
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
8576
|
+
import { chmodSync as chmodSync2, existsSync as existsSync17, mkdirSync as mkdirSync4, readFileSync as readFileSync19, writeFileSync as writeFileSync7 } from "node:fs";
|
|
7848
8577
|
import { join as join18 } from "node:path";
|
|
7849
8578
|
var TEMPLATE_PATH = join18(packageRoot(import.meta.url), "templates", "custom-check.template.ts");
|
|
7850
8579
|
var CHECKS_DIR = join18("scripts", "vinaya-checks");
|
|
7851
|
-
var
|
|
8580
|
+
var USAGE2 = "Usage: vinaya new check <yourname>/<id> (both segments: lowercase letters, digits, hyphens; e.g. myteam/vocab-check)";
|
|
7852
8581
|
function newCheckCommand(args) {
|
|
7853
8582
|
const name = args[0];
|
|
7854
8583
|
if (!name) {
|
|
7855
|
-
console.error(
|
|
8584
|
+
console.error(USAGE2);
|
|
7856
8585
|
process.exit(2);
|
|
7857
8586
|
}
|
|
7858
8587
|
if (coreCheckRegistry().some((spec) => spec.name === name)) {
|
|
@@ -7860,20 +8589,20 @@ function newCheckCommand(args) {
|
|
|
7860
8589
|
process.exit(2);
|
|
7861
8590
|
}
|
|
7862
8591
|
if (!isValidNamespacedKey(name)) {
|
|
7863
|
-
console.error(`Refusing: "${name}" is not a valid check id. ${
|
|
8592
|
+
console.error(`Refusing: "${name}" is not a valid check id. ${USAGE2.slice("Usage: ".length)}
|
|
7864
8593
|
\`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
8594
|
process.exit(2);
|
|
7866
8595
|
}
|
|
7867
8596
|
const scriptsDir = join18(process.cwd(), CHECKS_DIR);
|
|
7868
|
-
if (!
|
|
8597
|
+
if (!existsSync17(scriptsDir))
|
|
7869
8598
|
mkdirSync4(scriptsDir, { recursive: true });
|
|
7870
8599
|
const fileStem = name.slice(name.indexOf("/") + 1);
|
|
7871
8600
|
const targetPath = join18(scriptsDir, `${fileStem}.ts`);
|
|
7872
|
-
if (
|
|
8601
|
+
if (existsSync17(targetPath)) {
|
|
7873
8602
|
console.error(`Error: ${targetPath} already exists.`);
|
|
7874
8603
|
process.exit(1);
|
|
7875
8604
|
}
|
|
7876
|
-
const template =
|
|
8605
|
+
const template = readFileSync19(TEMPLATE_PATH, "utf-8");
|
|
7877
8606
|
const contents = template.split("{{CHECK_NAME}}").join(name);
|
|
7878
8607
|
writeFileSync7(targetPath, contents, "utf-8");
|
|
7879
8608
|
chmodSync2(targetPath, 493);
|
|
@@ -7887,32 +8616,32 @@ ${registration}
|
|
|
7887
8616
|
}
|
|
7888
8617
|
|
|
7889
8618
|
// src/commands/new-noop-check.ts
|
|
7890
|
-
import { chmodSync as chmodSync3, existsSync as
|
|
8619
|
+
import { chmodSync as chmodSync3, existsSync as existsSync18, mkdirSync as mkdirSync5, readFileSync as readFileSync20, writeFileSync as writeFileSync8 } from "node:fs";
|
|
7891
8620
|
import { join as join19 } from "node:path";
|
|
7892
8621
|
var TEMPLATE_PATH2 = join19(packageRoot(import.meta.url), "templates", "noop-check.template.ts");
|
|
7893
8622
|
var CHECKS_DIR2 = join19("vinaya", "checks");
|
|
7894
|
-
var
|
|
8623
|
+
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
8624
|
function newNoopCheckCommand(args) {
|
|
7896
8625
|
const name = args[0];
|
|
7897
8626
|
if (!name) {
|
|
7898
|
-
console.error(
|
|
8627
|
+
console.error(USAGE3);
|
|
7899
8628
|
process.exit(2);
|
|
7900
8629
|
}
|
|
7901
8630
|
const coreSpec = coreCheckRegistry().find((spec) => spec.name === name);
|
|
7902
8631
|
if (!coreSpec) {
|
|
7903
|
-
console.error(`Refusing: "${name}" is not a core check id. ${
|
|
8632
|
+
console.error(`Refusing: "${name}" is not a core check id. ${USAGE3}
|
|
7904
8633
|
\`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
8634
|
process.exit(2);
|
|
7906
8635
|
}
|
|
7907
8636
|
const checksDir = join19(process.cwd(), CHECKS_DIR2);
|
|
7908
|
-
if (!
|
|
8637
|
+
if (!existsSync18(checksDir))
|
|
7909
8638
|
mkdirSync5(checksDir, { recursive: true });
|
|
7910
8639
|
const targetPath = join19(checksDir, `${name}.ts`);
|
|
7911
|
-
if (
|
|
8640
|
+
if (existsSync18(targetPath)) {
|
|
7912
8641
|
console.error(`Error: ${targetPath} already exists.`);
|
|
7913
8642
|
process.exit(1);
|
|
7914
8643
|
}
|
|
7915
|
-
const template =
|
|
8644
|
+
const template = readFileSync20(TEMPLATE_PATH2, "utf-8");
|
|
7916
8645
|
const contents = template.split("{{CHECK_NAME}}").join(name);
|
|
7917
8646
|
writeFileSync8(targetPath, contents, "utf-8");
|
|
7918
8647
|
chmodSync3(targetPath, 493);
|
|
@@ -7934,32 +8663,32 @@ ${registration}
|
|
|
7934
8663
|
}
|
|
7935
8664
|
|
|
7936
8665
|
// src/commands/new-role.ts
|
|
7937
|
-
import { existsSync as
|
|
8666
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync6, readFileSync as readFileSync21, writeFileSync as writeFileSync9 } from "node:fs";
|
|
7938
8667
|
import { join as join20 } from "node:path";
|
|
7939
8668
|
var TEMPLATE_PATH3 = join20(packageRoot(import.meta.url), "templates", "role.template.md");
|
|
7940
8669
|
var ROLES_DIR = join20("vinaya", "roles");
|
|
7941
|
-
var
|
|
8670
|
+
var USAGE4 = "Usage: vinaya new role <yourname>/<id> (both segments: lowercase letters, digits, hyphens; e.g. acme/qa-lead)";
|
|
7942
8671
|
function newRoleCommand(args) {
|
|
7943
8672
|
const name = args[0];
|
|
7944
8673
|
if (!name) {
|
|
7945
|
-
console.error(
|
|
8674
|
+
console.error(USAGE4);
|
|
7946
8675
|
process.exit(2);
|
|
7947
8676
|
}
|
|
7948
8677
|
if (!isValidNamespacedKey(name)) {
|
|
7949
|
-
console.error(`Refusing: "${name}" is not a valid role key. ${
|
|
8678
|
+
console.error(`Refusing: "${name}" is not a valid role key. ${USAGE4.slice("Usage: ".length)}
|
|
7950
8679
|
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
8680
|
process.exit(2);
|
|
7952
8681
|
}
|
|
7953
8682
|
const rolesDir = join20(process.cwd(), ROLES_DIR);
|
|
7954
|
-
if (!
|
|
8683
|
+
if (!existsSync19(rolesDir))
|
|
7955
8684
|
mkdirSync6(rolesDir, { recursive: true });
|
|
7956
8685
|
const roleId = name.slice(name.indexOf("/") + 1);
|
|
7957
8686
|
const targetPath = join20(rolesDir, `${roleId}.md`);
|
|
7958
|
-
if (
|
|
8687
|
+
if (existsSync19(targetPath)) {
|
|
7959
8688
|
console.error(`Error: ${targetPath} already exists.`);
|
|
7960
8689
|
process.exit(1);
|
|
7961
8690
|
}
|
|
7962
|
-
const template =
|
|
8691
|
+
const template = readFileSync21(TEMPLATE_PATH3, "utf-8");
|
|
7963
8692
|
const contents = template.split("{{ROLE_ID}}").join(roleId);
|
|
7964
8693
|
writeFileSync9(targetPath, contents, "utf-8");
|
|
7965
8694
|
const relPath = join20(ROLES_DIR, `${roleId}.md`);
|
|
@@ -7972,7 +8701,7 @@ ${registration}
|
|
|
7972
8701
|
}
|
|
7973
8702
|
|
|
7974
8703
|
// src/commands/pr.ts
|
|
7975
|
-
import { execFileSync as
|
|
8704
|
+
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
7976
8705
|
|
|
7977
8706
|
// src/lib/numstat.ts
|
|
7978
8707
|
function summariseNumstat(numstat) {
|
|
@@ -8033,6 +8762,16 @@ class ScanContext {
|
|
|
8033
8762
|
return rawHasPair === (anchoredRegionBounds(this.masked, field) !== null);
|
|
8034
8763
|
}
|
|
8035
8764
|
}
|
|
8765
|
+
function resolveAnchoredRegion(ctx, field) {
|
|
8766
|
+
const bounds = anchoredRegionBounds(ctx.masked, field);
|
|
8767
|
+
if (bounds === null) {
|
|
8768
|
+
return anchoredRegionBounds(ctx.normalised, field) !== null ? "hidden" : null;
|
|
8769
|
+
}
|
|
8770
|
+
return {
|
|
8771
|
+
region: ctx.normalised.slice(bounds.innerStart, bounds.innerEnd),
|
|
8772
|
+
maskedRegion: ctx.masked.slice(bounds.innerStart, bounds.innerEnd)
|
|
8773
|
+
};
|
|
8774
|
+
}
|
|
8036
8775
|
|
|
8037
8776
|
// src/checks/body-bare-digits-logic.ts
|
|
8038
8777
|
function blankRange(text, start, end) {
|
|
@@ -8254,7 +8993,7 @@ var RETRY_CREATE3 = "vinaya pr create --validate-only …";
|
|
|
8254
8993
|
var RETRY_EDIT3 = "vinaya pr edit <n> --validate-only …";
|
|
8255
8994
|
function git3(args) {
|
|
8256
8995
|
try {
|
|
8257
|
-
return
|
|
8996
|
+
return execFileSync13("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
8258
8997
|
} catch {
|
|
8259
8998
|
return "";
|
|
8260
8999
|
}
|
|
@@ -8288,7 +9027,7 @@ function reportPass2(json, command) {
|
|
|
8288
9027
|
function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
|
|
8289
9028
|
const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
|
|
8290
9029
|
try {
|
|
8291
|
-
const out =
|
|
9030
|
+
const out = execFileSync13("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
|
|
8292
9031
|
const url = out.trim();
|
|
8293
9032
|
if (json)
|
|
8294
9033
|
printJson({ validated: true, written: true, url });
|
|
@@ -8302,7 +9041,7 @@ function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
|
|
|
8302
9041
|
function fetchPrForgeContext(prRef) {
|
|
8303
9042
|
let viewOut;
|
|
8304
9043
|
try {
|
|
8305
|
-
viewOut =
|
|
9044
|
+
viewOut = execFileSync13("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
|
|
8306
9045
|
encoding: "utf8",
|
|
8307
9046
|
stdio: ["ignore", "pipe", "pipe"]
|
|
8308
9047
|
});
|
|
@@ -8413,14 +9152,14 @@ function prEditCommand(args) {
|
|
|
8413
9152
|
}
|
|
8414
9153
|
|
|
8415
9154
|
// src/commands/pr-report.ts
|
|
8416
|
-
import { execFileSync as
|
|
8417
|
-
import { existsSync as
|
|
9155
|
+
import { execFileSync as execFileSync14, spawnSync as spawnSync2 } from "node:child_process";
|
|
9156
|
+
import { existsSync as existsSync20, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
|
|
8418
9157
|
import { join as join21 } from "node:path";
|
|
8419
9158
|
var EVIDENCE_START = "<!-- AEG:EVIDENCE:START -->";
|
|
8420
9159
|
var EVIDENCE_END = "<!-- AEG:EVIDENCE:END -->";
|
|
8421
9160
|
function git4(args) {
|
|
8422
9161
|
try {
|
|
8423
|
-
return
|
|
9162
|
+
return execFileSync14("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
8424
9163
|
} catch {
|
|
8425
9164
|
return "";
|
|
8426
9165
|
}
|
|
@@ -8438,7 +9177,7 @@ class GitCommandError extends Error {
|
|
|
8438
9177
|
}
|
|
8439
9178
|
function gitStrict(args) {
|
|
8440
9179
|
try {
|
|
8441
|
-
return
|
|
9180
|
+
return execFileSync14("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
8442
9181
|
} catch (err) {
|
|
8443
9182
|
const stderr = err.stderr;
|
|
8444
9183
|
throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
|
|
@@ -8474,7 +9213,7 @@ function computeGroupA() {
|
|
|
8474
9213
|
function resolveSelfEntry() {
|
|
8475
9214
|
const root = packageRoot(import.meta.url);
|
|
8476
9215
|
const dist = join21(root, "dist", "index.js");
|
|
8477
|
-
return
|
|
9216
|
+
return existsSync20(dist) ? dist : join21(root, "src", "index.ts");
|
|
8478
9217
|
}
|
|
8479
9218
|
var FAILING_STATUSES = new Set(["fail", "error", "timeout"]);
|
|
8480
9219
|
function anyGateFailed(outcomes) {
|
|
@@ -8567,6 +9306,84 @@ ${EVIDENCE_END}`;
|
|
|
8567
9306
|
${full}
|
|
8568
9307
|
`;
|
|
8569
9308
|
}
|
|
9309
|
+
var TOKENS_START = "<!-- AEG:TOKENS:START -->";
|
|
9310
|
+
var TOKENS_END = "<!-- AEG:TOKENS:END -->";
|
|
9311
|
+
var TOKEN_REPORT_HEADING = /^#{1,6}\s*token report\s*$/i;
|
|
9312
|
+
var HEADING_LINE = /^#{1,6}\s/;
|
|
9313
|
+
var TOKEN_TABLE_HEADER = "| Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date |";
|
|
9314
|
+
var TOKEN_TABLE_SEPARATOR = "|---|---|---|---|---|---|---|";
|
|
9315
|
+
function tokensBlockLineBounds(maskedLines) {
|
|
9316
|
+
const startIdx = maskedLines.findIndex((l) => l.trim() === TOKENS_START);
|
|
9317
|
+
if (startIdx === -1)
|
|
9318
|
+
return null;
|
|
9319
|
+
for (let i = startIdx + 1;i < maskedLines.length; i++) {
|
|
9320
|
+
if (maskedLines[i].trim() === TOKENS_END)
|
|
9321
|
+
return { startIdx, endIdx: i };
|
|
9322
|
+
}
|
|
9323
|
+
return null;
|
|
9324
|
+
}
|
|
9325
|
+
function writeTokensBlock(body, addition) {
|
|
9326
|
+
const rawLines = body.split(`
|
|
9327
|
+
`);
|
|
9328
|
+
const maskedLines = maskCode(body).split(`
|
|
9329
|
+
`);
|
|
9330
|
+
const additionLines = addition.split(`
|
|
9331
|
+
`);
|
|
9332
|
+
const existing = tokensBlockLineBounds(maskedLines);
|
|
9333
|
+
if (existing) {
|
|
9334
|
+
const before = rawLines.slice(0, existing.endIdx);
|
|
9335
|
+
const after = rawLines.slice(existing.endIdx);
|
|
9336
|
+
return [...before, ...additionLines, ...after].join(`
|
|
9337
|
+
`);
|
|
9338
|
+
}
|
|
9339
|
+
const freshBlock = [TOKENS_START, TOKEN_TABLE_HEADER, TOKEN_TABLE_SEPARATOR, ...additionLines, TOKENS_END];
|
|
9340
|
+
const headingIdx = rawLines.findIndex((l) => TOKEN_REPORT_HEADING.test(l));
|
|
9341
|
+
if (headingIdx !== -1) {
|
|
9342
|
+
let sectionEnd = rawLines.length;
|
|
9343
|
+
for (let i = headingIdx + 1;i < rawLines.length; i++) {
|
|
9344
|
+
if (HEADING_LINE.test(rawLines[i])) {
|
|
9345
|
+
sectionEnd = i;
|
|
9346
|
+
break;
|
|
9347
|
+
}
|
|
9348
|
+
}
|
|
9349
|
+
const before = rawLines.slice(0, headingIdx + 1);
|
|
9350
|
+
const after = rawLines.slice(sectionEnd);
|
|
9351
|
+
return [...before, "", ...freshBlock, "", ...after].join(`
|
|
9352
|
+
`);
|
|
9353
|
+
}
|
|
9354
|
+
const sep4 = body.length === 0 || body.endsWith(`
|
|
9355
|
+
`) ? [] : [""];
|
|
9356
|
+
return [...rawLines, ...sep4, "", "## Token report", "", ...freshBlock, ""].join(`
|
|
9357
|
+
`);
|
|
9358
|
+
}
|
|
9359
|
+
function derivePhase() {
|
|
9360
|
+
const branch = git4(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
9361
|
+
const m = /^task\/[^/]+\/(.+)$/.exec(branch);
|
|
9362
|
+
const taskId = m ? m[1] : branch || "unknown";
|
|
9363
|
+
return `${taskId}: develop`;
|
|
9364
|
+
}
|
|
9365
|
+
function isoToday() {
|
|
9366
|
+
return new Date().toISOString().slice(0, 10);
|
|
9367
|
+
}
|
|
9368
|
+
function collectTokensAddition(opts) {
|
|
9369
|
+
const capability = resolveMeteringCapability(realDeps(), opts.transcriptPath);
|
|
9370
|
+
if (!capability.capable) {
|
|
9371
|
+
return formatTokenReportRow({
|
|
9372
|
+
phase: opts.phase,
|
|
9373
|
+
role: opts.role,
|
|
9374
|
+
summary: null,
|
|
9375
|
+
modelOverride: `— (${capability.reason})`,
|
|
9376
|
+
date: opts.date
|
|
9377
|
+
});
|
|
9378
|
+
}
|
|
9379
|
+
return formatTokenReportRow({
|
|
9380
|
+
phase: opts.phase,
|
|
9381
|
+
role: opts.role,
|
|
9382
|
+
summary: capability.summary,
|
|
9383
|
+
modelOverride: opts.modelOverride,
|
|
9384
|
+
date: opts.date
|
|
9385
|
+
});
|
|
9386
|
+
}
|
|
8570
9387
|
async function buildReport(opts = {}) {
|
|
8571
9388
|
const groupA = opts.groupA ?? computeGroupA();
|
|
8572
9389
|
const gateRunner = opts.gateRunner ?? runRealGates;
|
|
@@ -8580,8 +9397,16 @@ ${EVIDENCE_END}`;
|
|
|
8580
9397
|
async function prReportCommand(args) {
|
|
8581
9398
|
const writeIdx = args.indexOf("--write");
|
|
8582
9399
|
const writePath = writeIdx !== -1 ? args[writeIdx + 1] : undefined;
|
|
9400
|
+
const transcriptIdx = args.indexOf("--transcript");
|
|
9401
|
+
const transcriptPath = transcriptIdx !== -1 ? args[transcriptIdx + 1] : undefined;
|
|
9402
|
+
const phaseIdx = args.indexOf("--phase");
|
|
9403
|
+
const phaseOverride = phaseIdx !== -1 ? args[phaseIdx + 1] : undefined;
|
|
9404
|
+
const roleIdx = args.indexOf("--role");
|
|
9405
|
+
const roleOverride = roleIdx !== -1 ? args[roleIdx + 1] : undefined;
|
|
9406
|
+
const modelIdx = args.indexOf("--model");
|
|
9407
|
+
const modelOverride = modelIdx !== -1 ? args[modelIdx + 1] : undefined;
|
|
8583
9408
|
if (writeIdx !== -1 && !writePath) {
|
|
8584
|
-
console.error("Usage: vinaya pr report [--write <body-file>]");
|
|
9409
|
+
console.error("Usage: vinaya pr report [--write <body-file>] [--phase <phase>] [--role <role>] " + "[--model <id>] [--transcript <path>]");
|
|
8585
9410
|
process.exit(2);
|
|
8586
9411
|
}
|
|
8587
9412
|
let result;
|
|
@@ -8595,10 +9420,18 @@ async function prReportCommand(args) {
|
|
|
8595
9420
|
throw err;
|
|
8596
9421
|
}
|
|
8597
9422
|
if (writePath) {
|
|
8598
|
-
const existing =
|
|
8599
|
-
const
|
|
9423
|
+
const existing = existsSync20(writePath) ? readFileSync22(writePath, "utf8") : "";
|
|
9424
|
+
const withEvidence = replaceEvidenceBlock(existing, result.blockInner);
|
|
9425
|
+
const tokensAddition = collectTokensAddition({
|
|
9426
|
+
phase: phaseOverride ?? derivePhase(),
|
|
9427
|
+
role: roleOverride ?? "Developer",
|
|
9428
|
+
date: isoToday(),
|
|
9429
|
+
transcriptPath,
|
|
9430
|
+
modelOverride
|
|
9431
|
+
});
|
|
9432
|
+
const updated = writeTokensBlock(withEvidence, tokensAddition);
|
|
8600
9433
|
writeFileSync10(writePath, updated);
|
|
8601
|
-
process.stdout.write(`Wrote AEG:EVIDENCE
|
|
9434
|
+
process.stdout.write(`Wrote AEG:EVIDENCE and AEG:TOKENS blocks to ${writePath}
|
|
8602
9435
|
`);
|
|
8603
9436
|
} else {
|
|
8604
9437
|
process.stdout.write(`${result.block}
|
|
@@ -8607,13 +9440,187 @@ async function prReportCommand(args) {
|
|
|
8607
9440
|
process.exit(result.gatesFailed ? 1 : 0);
|
|
8608
9441
|
}
|
|
8609
9442
|
|
|
9443
|
+
// src/commands/pr-verify-evidence.ts
|
|
9444
|
+
import { execFileSync as execFileSync15 } from "node:child_process";
|
|
9445
|
+
|
|
9446
|
+
// src/commands/pr-verify-evidence-logic.ts
|
|
9447
|
+
function publishedMergeBase(region) {
|
|
9448
|
+
const m = region.match(/^[ \t]*`git diff ([0-9a-f]{7,40})\.\.\.[0-9a-f]{7,40} --numstat`[ \t]*$/m);
|
|
9449
|
+
return m ? m[1] : null;
|
|
9450
|
+
}
|
|
9451
|
+
function normaliseLines(region, repoRoot2) {
|
|
9452
|
+
const root = repoRoot2.replace(/\/+$/, "");
|
|
9453
|
+
const out = [];
|
|
9454
|
+
for (const raw of region.split(`
|
|
9455
|
+
`)) {
|
|
9456
|
+
const localStripped = root ? raw.split(`${root}/`).join("").split(root).join("") : raw;
|
|
9457
|
+
const stripped = localStripped.replace(/(^|\s)\/\S*?\/(?=(?:apps|packages|aeg-root|scripts|\.changeset|\.vinaya|\.github)\/)/g, "$1");
|
|
9458
|
+
const line = stripped.replace(/[\u0000-\u001F\u007F-\u009F\u2028\u2029\u202A-\u202E\u2066-\u2069]/g, " ").trim();
|
|
9459
|
+
if (line)
|
|
9460
|
+
out.push(line);
|
|
9461
|
+
}
|
|
9462
|
+
return out;
|
|
9463
|
+
}
|
|
9464
|
+
function compareEvidence(published, freshRegion, repoRoot2) {
|
|
9465
|
+
if (published === null)
|
|
9466
|
+
return { status: "no-block" };
|
|
9467
|
+
if (published === "hidden")
|
|
9468
|
+
return { status: "hidden" };
|
|
9469
|
+
const publishedLines = normaliseLines(published.region, repoRoot2);
|
|
9470
|
+
const freshLines = normaliseLines(freshRegion, repoRoot2);
|
|
9471
|
+
const missing = subtractOnce(freshLines, publishedLines);
|
|
9472
|
+
const unexpected = subtractOnce(publishedLines, freshLines);
|
|
9473
|
+
if (missing.length === 0 && unexpected.length === 0)
|
|
9474
|
+
return { status: "match" };
|
|
9475
|
+
const publishedBase = publishedMergeBase(published.region);
|
|
9476
|
+
const currentBase = publishedMergeBase(freshRegion);
|
|
9477
|
+
const baseDrift = publishedBase && currentBase && publishedBase !== currentBase ? { publishedBase, currentBase } : undefined;
|
|
9478
|
+
return baseDrift ? { status: "differs", missing, unexpected, baseDrift } : { status: "differs", missing, unexpected };
|
|
9479
|
+
}
|
|
9480
|
+
function subtractOnce(a, b) {
|
|
9481
|
+
const counts = new Map;
|
|
9482
|
+
for (const line of b)
|
|
9483
|
+
counts.set(line, (counts.get(line) ?? 0) + 1);
|
|
9484
|
+
const out = [];
|
|
9485
|
+
for (const line of a) {
|
|
9486
|
+
const n = counts.get(line) ?? 0;
|
|
9487
|
+
if (n > 0)
|
|
9488
|
+
counts.set(line, n - 1);
|
|
9489
|
+
else
|
|
9490
|
+
out.push(line);
|
|
9491
|
+
}
|
|
9492
|
+
return out;
|
|
9493
|
+
}
|
|
9494
|
+
function renderVerdict(verdict) {
|
|
9495
|
+
if (verdict.status === "match") {
|
|
9496
|
+
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).";
|
|
9497
|
+
}
|
|
9498
|
+
if (verdict.status === "no-block") {
|
|
9499
|
+
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.";
|
|
9500
|
+
}
|
|
9501
|
+
if (verdict.status === "hidden") {
|
|
9502
|
+
return [
|
|
9503
|
+
"pr verify-evidence: HIDDEN — the AEG:EVIDENCE pair sits inside a collapsed `<details>` block.",
|
|
9504
|
+
"`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>`."
|
|
9505
|
+
].join(`
|
|
9506
|
+
`);
|
|
9507
|
+
}
|
|
9508
|
+
const lines = ["pr verify-evidence: DIFFERS — the published AEG:EVIDENCE region does not reproduce at this head."];
|
|
9509
|
+
if (verdict.baseDrift) {
|
|
9510
|
+
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.");
|
|
9511
|
+
} else {
|
|
9512
|
+
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.");
|
|
9513
|
+
}
|
|
9514
|
+
lines.push("", "Regenerate with `vinaya pr report --write <body-file>` and push.");
|
|
9515
|
+
if (verdict.missing.length > 0) {
|
|
9516
|
+
lines.push("", `Present in a fresh run, absent from the published block (${verdict.missing.length}):`);
|
|
9517
|
+
for (const line of verdict.missing)
|
|
9518
|
+
lines.push(` + ${line}`);
|
|
9519
|
+
}
|
|
9520
|
+
if (verdict.unexpected.length > 0) {
|
|
9521
|
+
lines.push("", `Published but not reproducible (${verdict.unexpected.length}):`);
|
|
9522
|
+
for (const line of verdict.unexpected)
|
|
9523
|
+
lines.push(` - ${line}`);
|
|
9524
|
+
}
|
|
9525
|
+
return lines.join(`
|
|
9526
|
+
`);
|
|
9527
|
+
}
|
|
9528
|
+
|
|
9529
|
+
// src/commands/pr-verify-evidence.ts
|
|
9530
|
+
function gh(args) {
|
|
9531
|
+
return execFileSync15("gh", args, { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
|
|
9532
|
+
}
|
|
9533
|
+
function assertCleanWorktree() {
|
|
9534
|
+
const dirty = execFileSync15("git", ["status", "--porcelain"], {
|
|
9535
|
+
encoding: "utf8",
|
|
9536
|
+
cwd: repoRoot2()
|
|
9537
|
+
}).trim();
|
|
9538
|
+
if (!dirty)
|
|
9539
|
+
return;
|
|
9540
|
+
process.stderr.write(`pr verify-evidence: REFUSED — the worktree has uncommitted or untracked changes.
|
|
9541
|
+
` + `Group B is regenerated by diff-scoped checks, so local changes alter which files are scanned and can
|
|
9542
|
+
` + `produce a MATCH that a clean checkout would not. Verify from a clean checkout at this head instead.
|
|
9543
|
+
|
|
9544
|
+
` + `${dirty}
|
|
9545
|
+
`);
|
|
9546
|
+
process.exit(2);
|
|
9547
|
+
}
|
|
9548
|
+
function assertNoBaseOverride() {
|
|
9549
|
+
const override = process.env.BASE_SHA;
|
|
9550
|
+
if (!override)
|
|
9551
|
+
return;
|
|
9552
|
+
process.stderr.write(`pr verify-evidence: REFUSED — BASE_SHA is set (${override}).
|
|
9553
|
+
` + `The regeneration resolves its merge-base from that variable, so an override changes what a "fresh run"
|
|
9554
|
+
` + `means and can turn a drifted comparison into an apparently shared-base one. Unset it and re-run.
|
|
9555
|
+
`);
|
|
9556
|
+
process.exit(2);
|
|
9557
|
+
}
|
|
9558
|
+
function assertRepoRootCwd() {
|
|
9559
|
+
const root = repoRoot2();
|
|
9560
|
+
const cwd = process.cwd();
|
|
9561
|
+
if (cwd === root)
|
|
9562
|
+
return;
|
|
9563
|
+
process.stderr.write(`pr verify-evidence: REFUSED — run this from the repository root (${root}), not ${cwd}.
|
|
9564
|
+
` + `The regeneration inherits this working directory, and several gates resolve their scan root from it,
|
|
9565
|
+
` + `so a subdirectory silently narrows what the fresh run inspects and can report MATCH on a block that
|
|
9566
|
+
` + `omits real findings.
|
|
9567
|
+
`);
|
|
9568
|
+
process.exit(2);
|
|
9569
|
+
}
|
|
9570
|
+
function repoRoot2() {
|
|
9571
|
+
return execFileSync15("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
9572
|
+
}
|
|
9573
|
+
async function prVerifyEvidenceCommand(args) {
|
|
9574
|
+
const prRef = args[0] && !args[0].startsWith("-") ? args[0] : undefined;
|
|
9575
|
+
if (!prRef) {
|
|
9576
|
+
process.stderr.write(`Usage: vinaya pr verify-evidence <pr-number>
|
|
9577
|
+
`);
|
|
9578
|
+
process.exit(2);
|
|
9579
|
+
}
|
|
9580
|
+
assertRepoRootCwd();
|
|
9581
|
+
assertCleanWorktree();
|
|
9582
|
+
assertNoBaseOverride();
|
|
9583
|
+
let body;
|
|
9584
|
+
let forgeHead;
|
|
9585
|
+
try {
|
|
9586
|
+
const view = JSON.parse(gh(["pr", "view", prRef, "--json", "body,headRefOid"]));
|
|
9587
|
+
body = view.body ?? "";
|
|
9588
|
+
forgeHead = view.headRefOid ?? "";
|
|
9589
|
+
} catch (err) {
|
|
9590
|
+
process.stderr.write(`pr verify-evidence: could not read pull request #${prRef} — ${err.message}
|
|
9591
|
+
`);
|
|
9592
|
+
process.exit(1);
|
|
9593
|
+
return;
|
|
9594
|
+
}
|
|
9595
|
+
const localHead = execFileSync15("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim();
|
|
9596
|
+
if (!forgeHead) {
|
|
9597
|
+
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.
|
|
9598
|
+
`);
|
|
9599
|
+
process.exit(2);
|
|
9600
|
+
}
|
|
9601
|
+
if (forgeHead !== localHead) {
|
|
9602
|
+
process.stderr.write(`pr verify-evidence: REFUSED — this checkout is at ${localHead}, but #${prRef}'s head is ${forgeHead}.
|
|
9603
|
+
` + `Comparing against a different tree produces a difference that is real and uninteresting. Check out the head first.
|
|
9604
|
+
`);
|
|
9605
|
+
process.exit(2);
|
|
9606
|
+
}
|
|
9607
|
+
const fresh = await buildReport();
|
|
9608
|
+
const resolved = resolveAnchoredRegion(ScanContext.from(body), "EVIDENCE");
|
|
9609
|
+
const verdict = compareEvidence(resolved === null || resolved === "hidden" ? resolved : { region: resolved.region }, fresh.blockInner, repoRoot2());
|
|
9610
|
+
process.stdout.write(`${renderVerdict(verdict)}
|
|
9611
|
+
`);
|
|
9612
|
+
process.stdout.write(` compared at head ${localHead}
|
|
9613
|
+
`);
|
|
9614
|
+
process.exit(verdict.status === "match" ? 0 : 1);
|
|
9615
|
+
}
|
|
9616
|
+
|
|
8610
9617
|
// src/commands/quickstart.ts
|
|
8611
|
-
import { execFileSync as
|
|
8612
|
-
import { existsSync as
|
|
9618
|
+
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
9619
|
+
import { existsSync as existsSync22, readFileSync as readFileSync24 } from "node:fs";
|
|
8613
9620
|
import { join as join23 } from "node:path";
|
|
8614
9621
|
|
|
8615
9622
|
// src/lib/doc-owners-write.ts
|
|
8616
|
-
import { existsSync as
|
|
9623
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync7, readFileSync as readFileSync23, writeFileSync as writeFileSync11 } from "node:fs";
|
|
8617
9624
|
import { dirname as dirname7, join as join22 } from "node:path";
|
|
8618
9625
|
function bindingLine(glob, pointer) {
|
|
8619
9626
|
return `${glob} ${pointer}`;
|
|
@@ -8629,23 +9636,23 @@ function appendDocOwnersBinding(content, glob, pointer) {
|
|
|
8629
9636
|
return `${content}${sep4}${bindingLine(glob, pointer)}
|
|
8630
9637
|
`;
|
|
8631
9638
|
}
|
|
8632
|
-
function planDocOwnersBinding(
|
|
8633
|
-
const abs2 = join22(
|
|
9639
|
+
function planDocOwnersBinding(repoRoot3, glob, pointer) {
|
|
9640
|
+
const abs2 = join22(repoRoot3, DOC_OWNERS_PATH);
|
|
8634
9641
|
const line = bindingLine(glob, pointer);
|
|
8635
|
-
if (!
|
|
9642
|
+
if (!existsSync21(abs2)) {
|
|
8636
9643
|
return { action: "create-host", line, path: DOC_OWNERS_PATH };
|
|
8637
9644
|
}
|
|
8638
|
-
const existing =
|
|
9645
|
+
const existing = readFileSync23(abs2, "utf-8");
|
|
8639
9646
|
const already = parseDocOwners(existing).bindings.some((b) => b.glob === glob);
|
|
8640
9647
|
return { action: already ? "skip-present" : "append-row", line, path: DOC_OWNERS_PATH };
|
|
8641
9648
|
}
|
|
8642
|
-
function applyDocOwnersBinding(
|
|
8643
|
-
const abs2 = join22(
|
|
9649
|
+
function applyDocOwnersBinding(repoRoot3, plan, glob, pointer) {
|
|
9650
|
+
const abs2 = join22(repoRoot3, DOC_OWNERS_PATH);
|
|
8644
9651
|
if (plan.action === "create-host") {
|
|
8645
9652
|
mkdirSync7(dirname7(abs2), { recursive: true });
|
|
8646
9653
|
writeFileSync11(abs2, freshDocOwners(glob, pointer), "utf-8");
|
|
8647
9654
|
} else if (plan.action === "append-row") {
|
|
8648
|
-
const existing =
|
|
9655
|
+
const existing = readFileSync23(abs2, "utf-8");
|
|
8649
9656
|
writeFileSync11(abs2, appendDocOwnersBinding(existing, glob, pointer), "utf-8");
|
|
8650
9657
|
}
|
|
8651
9658
|
}
|
|
@@ -8705,13 +9712,13 @@ var GLOB_INJECTION_RE = /[\s\r\n]/;
|
|
|
8705
9712
|
var POINTER_INJECTION_RE = /[\r\n]/;
|
|
8706
9713
|
var NAME_INJECTION_RE = /[\s|\r\n]/;
|
|
8707
9714
|
var PATH_INJECTION_RE2 = /[|\r\n]/;
|
|
8708
|
-
function pointerIsMissingLocalFile(
|
|
9715
|
+
function pointerIsMissingLocalFile(repoRoot3, pointer) {
|
|
8709
9716
|
if (/^https?:\/\//i.test(pointer))
|
|
8710
9717
|
return false;
|
|
8711
9718
|
const path = pointer.split("#")[0] ?? pointer;
|
|
8712
|
-
return !
|
|
9719
|
+
return !existsSync22(join23(repoRoot3, path));
|
|
8713
9720
|
}
|
|
8714
|
-
async function bindDocOwnerLoop(deps,
|
|
9721
|
+
async function bindDocOwnerLoop(deps, repoRoot3) {
|
|
8715
9722
|
let any = false;
|
|
8716
9723
|
for (;; ) {
|
|
8717
9724
|
if (any && !await deps.confirm("Bind another doc→code pair?", false))
|
|
@@ -8726,11 +9733,11 @@ async function bindDocOwnerLoop(deps, repoRoot2) {
|
|
|
8726
9733
|
}
|
|
8727
9734
|
if (GLOB_INJECTION_RE.test(glob) || POINTER_INJECTION_RE.test(pointer)) {
|
|
8728
9735
|
console.error(red("Error: glob must not contain whitespace, and the pointer must not contain a newline."));
|
|
8729
|
-
} else if (pointerIsMissingLocalFile(
|
|
9736
|
+
} else if (pointerIsMissingLocalFile(repoRoot3, pointer)) {
|
|
8730
9737
|
console.error(red(`✗ "${pointer}" does not exist in this repo (and isn't a URL).`));
|
|
8731
9738
|
} else {
|
|
8732
|
-
const plan = planDocOwnersBinding(
|
|
8733
|
-
applyDocOwnersBinding(
|
|
9739
|
+
const plan = planDocOwnersBinding(repoRoot3, glob, pointer);
|
|
9740
|
+
applyDocOwnersBinding(repoRoot3, plan, glob, pointer);
|
|
8734
9741
|
process.stdout.write(`${green(renderDocOwnersBindingDiffLine(plan))}
|
|
8735
9742
|
`);
|
|
8736
9743
|
any = true;
|
|
@@ -8776,10 +9783,10 @@ async function registerProjectLoop(deps) {
|
|
|
8776
9783
|
}
|
|
8777
9784
|
}
|
|
8778
9785
|
function readPackageVersion() {
|
|
8779
|
-
const pkg = JSON.parse(
|
|
9786
|
+
const pkg = JSON.parse(readFileSync24(join23(packageRoot(import.meta.url), "package.json"), "utf-8"));
|
|
8780
9787
|
return pkg.version;
|
|
8781
9788
|
}
|
|
8782
|
-
function
|
|
9789
|
+
function realDeps7() {
|
|
8783
9790
|
return {
|
|
8784
9791
|
detectRepo: detectGitRepo,
|
|
8785
9792
|
initDeps: {
|
|
@@ -8803,8 +9810,8 @@ function realDeps6() {
|
|
|
8803
9810
|
meteringCapability: () => resolveMeteringCapability({
|
|
8804
9811
|
env: process.env,
|
|
8805
9812
|
cwd: process.cwd(),
|
|
8806
|
-
exists:
|
|
8807
|
-
readFile: (path) =>
|
|
9813
|
+
exists: existsSync22,
|
|
9814
|
+
readFile: (path) => readFileSync24(path, "utf8")
|
|
8808
9815
|
})
|
|
8809
9816
|
},
|
|
8810
9817
|
confirm: async (q, defaultYes) => promptYesNo(q, defaultYes),
|
|
@@ -8812,8 +9819,8 @@ function realDeps6() {
|
|
|
8812
9819
|
closeStdin
|
|
8813
9820
|
};
|
|
8814
9821
|
}
|
|
8815
|
-
function execGit(
|
|
8816
|
-
return
|
|
9822
|
+
function execGit(repoRoot3, args) {
|
|
9823
|
+
return execFileSync16("git", args, { cwd: repoRoot3, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
8817
9824
|
}
|
|
8818
9825
|
function errorDetail(err) {
|
|
8819
9826
|
const stderr = err.stderr;
|
|
@@ -8821,37 +9828,37 @@ function errorDetail(err) {
|
|
|
8821
9828
|
return stderr.toString().trim();
|
|
8822
9829
|
return err instanceof Error ? err.message : String(err);
|
|
8823
9830
|
}
|
|
8824
|
-
function gitStatusShort(
|
|
8825
|
-
return execGit(
|
|
9831
|
+
function gitStatusShort(repoRoot3) {
|
|
9832
|
+
return execGit(repoRoot3, ["status", "--short"]);
|
|
8826
9833
|
}
|
|
8827
|
-
function commitInstall(
|
|
8828
|
-
execGit(
|
|
8829
|
-
const staged = execGit(
|
|
9834
|
+
function commitInstall(repoRoot3) {
|
|
9835
|
+
execGit(repoRoot3, ["add", "-A"]);
|
|
9836
|
+
const staged = execGit(repoRoot3, ["diff", "--cached", "--stat"]);
|
|
8830
9837
|
if (!staged)
|
|
8831
9838
|
return { committed: false, message: "Nothing to commit — working tree already clean." };
|
|
8832
9839
|
try {
|
|
8833
|
-
execGit(
|
|
9840
|
+
execGit(repoRoot3, ["commit", "-m", "Chore: install Vinaya"]);
|
|
8834
9841
|
return { committed: true, message: "✓ Committed `Chore: install Vinaya`." };
|
|
8835
9842
|
} catch (err) {
|
|
8836
9843
|
return { committed: false, message: `git commit failed: ${errorDetail(err)}` };
|
|
8837
9844
|
}
|
|
8838
9845
|
}
|
|
8839
|
-
function pushInstall(
|
|
9846
|
+
function pushInstall(repoRoot3) {
|
|
8840
9847
|
try {
|
|
8841
|
-
const branch = execGit(
|
|
9848
|
+
const branch = execGit(repoRoot3, ["symbolic-ref", "--short", "HEAD"]);
|
|
8842
9849
|
let hasUpstream = true;
|
|
8843
9850
|
try {
|
|
8844
|
-
execGit(
|
|
9851
|
+
execGit(repoRoot3, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"]);
|
|
8845
9852
|
} catch {
|
|
8846
9853
|
hasUpstream = false;
|
|
8847
9854
|
}
|
|
8848
9855
|
if (hasUpstream) {
|
|
8849
|
-
const ahead = execGit(
|
|
9856
|
+
const ahead = execGit(repoRoot3, ["rev-list", "--count", "@{u}..HEAD"]);
|
|
8850
9857
|
if (ahead === "0") {
|
|
8851
9858
|
return { status: "up-to-date", message: "Nothing to push — already up to date with the remote." };
|
|
8852
9859
|
}
|
|
8853
9860
|
}
|
|
8854
|
-
execGit(
|
|
9861
|
+
execGit(repoRoot3, hasUpstream ? ["push"] : ["push", "-u", "origin", branch]);
|
|
8855
9862
|
return { status: "pushed", message: "✓ Pushed." };
|
|
8856
9863
|
} catch (err) {
|
|
8857
9864
|
return { status: "failed", message: `git push failed: ${errorDetail(err)}` };
|
|
@@ -8943,12 +9950,12 @@ ${bold("Next steps:")}
|
|
|
8943
9950
|
}
|
|
8944
9951
|
}
|
|
8945
9952
|
async function quickstartCommand(args) {
|
|
8946
|
-
process.exit(await runQuickstart(args,
|
|
9953
|
+
process.exit(await runQuickstart(args, realDeps7()));
|
|
8947
9954
|
}
|
|
8948
9955
|
|
|
8949
9956
|
// src/commands/review-post.ts
|
|
8950
|
-
import { execFileSync as
|
|
8951
|
-
import { readFileSync as
|
|
9957
|
+
import { execFileSync as execFileSync17 } from "node:child_process";
|
|
9958
|
+
import { readFileSync as readFileSync25, rmSync as rmSync4, writeFileSync as writeFileSync12 } from "node:fs";
|
|
8952
9959
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8953
9960
|
import { join as join24 } from "node:path";
|
|
8954
9961
|
class FindingsParseError extends Error {
|
|
@@ -9130,7 +10137,7 @@ function readFindingsFile(path, allowedSeverities) {
|
|
|
9130
10137
|
return [];
|
|
9131
10138
|
let content;
|
|
9132
10139
|
try {
|
|
9133
|
-
content =
|
|
10140
|
+
content = readFileSync25(path, "utf8");
|
|
9134
10141
|
} catch {
|
|
9135
10142
|
refuseCmd(`Could not read findings file at ${path}.`, "Check the path and re-run.");
|
|
9136
10143
|
}
|
|
@@ -9159,9 +10166,9 @@ function normalizeSecurityVerdict(raw) {
|
|
|
9159
10166
|
return "FAIL";
|
|
9160
10167
|
refuseCmd(`\`--verdict ${raw}\` is not PASS or FAIL.`, "Pass `--verdict PASS` or `--verdict FAIL`.");
|
|
9161
10168
|
}
|
|
9162
|
-
function
|
|
10169
|
+
function gh2(args) {
|
|
9163
10170
|
try {
|
|
9164
|
-
return
|
|
10171
|
+
return execFileSync17("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
9165
10172
|
} catch (err) {
|
|
9166
10173
|
const stderr = err.stderr;
|
|
9167
10174
|
throw new Error(String(stderr ?? err.message).trim() || "gh command failed");
|
|
@@ -9170,7 +10177,7 @@ function gh(args) {
|
|
|
9170
10177
|
function resolveHeadSha(pr) {
|
|
9171
10178
|
let out;
|
|
9172
10179
|
try {
|
|
9173
|
-
out =
|
|
10180
|
+
out = gh2(["pr", "view", pr, "--json", "headRefOid", "-q", ".headRefOid"]);
|
|
9174
10181
|
} catch (err) {
|
|
9175
10182
|
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
10183
|
}
|
|
@@ -9183,7 +10190,7 @@ function postComment(pr, body) {
|
|
|
9183
10190
|
const tmp = join24(tmpdir2(), `vinaya-review-post-${process.pid}-${Date.now()}.md`);
|
|
9184
10191
|
writeFileSync12(tmp, body);
|
|
9185
10192
|
try {
|
|
9186
|
-
return
|
|
10193
|
+
return gh2(["pr", "comment", pr, "--body-file", tmp]).trim();
|
|
9187
10194
|
} catch (err) {
|
|
9188
10195
|
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
10196
|
} finally {
|
|
@@ -9191,7 +10198,7 @@ function postComment(pr, body) {
|
|
|
9191
10198
|
}
|
|
9192
10199
|
}
|
|
9193
10200
|
function fetchComments(pr) {
|
|
9194
|
-
const out =
|
|
10201
|
+
const out = gh2(["pr", "view", pr, "--json", "comments"]);
|
|
9195
10202
|
const parsed = JSON.parse(out);
|
|
9196
10203
|
return parsed.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null }));
|
|
9197
10204
|
}
|
|
@@ -9299,7 +10306,7 @@ async function reviewPostCommand(args) {
|
|
|
9299
10306
|
let secretsEvidence = null;
|
|
9300
10307
|
if (secretsEvidenceFile) {
|
|
9301
10308
|
try {
|
|
9302
|
-
secretsEvidence =
|
|
10309
|
+
secretsEvidence = readFileSync25(secretsEvidenceFile, "utf8");
|
|
9303
10310
|
} catch {
|
|
9304
10311
|
refuseCmd(`Could not read secrets evidence file at ${secretsEvidenceFile}.`, "Check the path and re-run.");
|
|
9305
10312
|
}
|
|
@@ -9340,7 +10347,7 @@ Self-verification: clean — re-parsed VERDICT is bound to head ${headSha}.
|
|
|
9340
10347
|
|
|
9341
10348
|
// src/commands/studio.ts
|
|
9342
10349
|
import { execFile as execFile5, spawn as spawn2 } from "node:child_process";
|
|
9343
|
-
import { existsSync as
|
|
10350
|
+
import { existsSync as existsSync23, readFileSync as readFileSync26, renameSync } from "node:fs";
|
|
9344
10351
|
import net from "node:net";
|
|
9345
10352
|
import { dirname as dirname8, join as join25 } from "node:path";
|
|
9346
10353
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -9397,15 +10404,15 @@ function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
|
|
|
9397
10404
|
for (;; ) {
|
|
9398
10405
|
const webDir = join25(dir, "apps", "vinaya-studio", "web");
|
|
9399
10406
|
const pkgPath = join25(webDir, "package.json");
|
|
9400
|
-
if (
|
|
10407
|
+
if (existsSync23(pkgPath)) {
|
|
9401
10408
|
try {
|
|
9402
|
-
const pkg = JSON.parse(
|
|
10409
|
+
const pkg = JSON.parse(readFileSync26(pkgPath, "utf-8"));
|
|
9403
10410
|
if (pkg.name === "@atta/vinaya-studio-web") {
|
|
9404
10411
|
return { kind: "workspace", webDir };
|
|
9405
10412
|
}
|
|
9406
10413
|
} catch {}
|
|
9407
10414
|
}
|
|
9408
|
-
if (
|
|
10415
|
+
if (existsSync23(join25(dir, ".git")))
|
|
9409
10416
|
break;
|
|
9410
10417
|
const parent = dirname8(dir);
|
|
9411
10418
|
if (parent === dir)
|
|
@@ -9413,29 +10420,29 @@ function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
|
|
|
9413
10420
|
dir = parent;
|
|
9414
10421
|
}
|
|
9415
10422
|
const standaloneWebDir = join25(packageRoot(moduleUrl), "studio-standalone", "apps", "vinaya-studio", "web");
|
|
9416
|
-
if (
|
|
10423
|
+
if (existsSync23(join25(standaloneWebDir, "server.js"))) {
|
|
9417
10424
|
return { kind: "package", packageDir: standaloneWebDir };
|
|
9418
10425
|
}
|
|
9419
10426
|
return { kind: "missing" };
|
|
9420
10427
|
}
|
|
9421
10428
|
function spawnDev(webDir, args) {
|
|
9422
|
-
return new Promise((
|
|
10429
|
+
return new Promise((resolve3) => {
|
|
9423
10430
|
const child = spawn2("bun", ["run", "dev", ...args], { cwd: webDir, stdio: "inherit" });
|
|
9424
|
-
child.on("exit", (code) =>
|
|
10431
|
+
child.on("exit", (code) => resolve3(code ?? 0));
|
|
9425
10432
|
});
|
|
9426
10433
|
}
|
|
9427
10434
|
function isPortFree(port) {
|
|
9428
|
-
return new Promise((
|
|
10435
|
+
return new Promise((resolve3) => {
|
|
9429
10436
|
const tester = net.createServer();
|
|
9430
|
-
tester.once("error", () =>
|
|
9431
|
-
tester.once("listening", () => tester.close(() =>
|
|
10437
|
+
tester.once("error", () => resolve3(false));
|
|
10438
|
+
tester.once("listening", () => tester.close(() => resolve3(true)));
|
|
9432
10439
|
tester.listen(port, "127.0.0.1");
|
|
9433
10440
|
});
|
|
9434
10441
|
}
|
|
9435
10442
|
function ensureStudioNodeModules(bundleRoot) {
|
|
9436
10443
|
const real = join25(bundleRoot, "node_modules");
|
|
9437
10444
|
const packed = join25(bundleRoot, STUDIO_NODE_MODULES_PACKED_DIRNAME);
|
|
9438
|
-
if (!
|
|
10445
|
+
if (!existsSync23(real) && existsSync23(packed)) {
|
|
9439
10446
|
renameSync(packed, real);
|
|
9440
10447
|
}
|
|
9441
10448
|
}
|
|
@@ -9459,9 +10466,9 @@ async function spawnStandalone(cwd, serverPath, bundleRoot, explicitPort) {
|
|
|
9459
10466
|
const env = { HOSTNAME: "127.0.0.1", ...process.env, PORT: String(port), VINAYA_REPO_ROOT: cwd };
|
|
9460
10467
|
if (repo)
|
|
9461
10468
|
env.AEG_REPO = `${repo.owner}/${repo.repo}`;
|
|
9462
|
-
return new Promise((
|
|
10469
|
+
return new Promise((resolve3) => {
|
|
9463
10470
|
const child = spawn2("node", [serverPath], { cwd, stdio: "inherit", env });
|
|
9464
|
-
child.on("exit", (code) =>
|
|
10471
|
+
child.on("exit", (code) => resolve3(code ?? 0));
|
|
9465
10472
|
});
|
|
9466
10473
|
}
|
|
9467
10474
|
async function runStudio(cwd, args, moduleUrl = import.meta.url) {
|
|
@@ -9493,124 +10500,6 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
|
|
|
9493
10500
|
}
|
|
9494
10501
|
}
|
|
9495
10502
|
|
|
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
10503
|
// src/commands/upgrade.ts
|
|
9615
10504
|
import { existsSync as existsSync24, readFileSync as readFileSync27, rmSync as rmSync5, writeFileSync as writeFileSync13 } from "node:fs";
|
|
9616
10505
|
import { join as join26 } from "node:path";
|
|
@@ -9630,8 +10519,8 @@ function realDeps8() {
|
|
|
9630
10519
|
function flags2(args) {
|
|
9631
10520
|
return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
|
|
9632
10521
|
}
|
|
9633
|
-
function readManifest3(
|
|
9634
|
-
const p = join26(
|
|
10522
|
+
function readManifest3(repoRoot3) {
|
|
10523
|
+
const p = join26(repoRoot3, CONFIG_PATH);
|
|
9635
10524
|
if (!existsSync24(p))
|
|
9636
10525
|
return { kind: "missing" };
|
|
9637
10526
|
let raw;
|
|
@@ -9651,15 +10540,15 @@ function readManifest3(repoRoot2) {
|
|
|
9651
10540
|
return { kind: "not-initialized" };
|
|
9652
10541
|
return { kind: "ok", manifest: parsed.data.managed };
|
|
9653
10542
|
}
|
|
9654
|
-
function writeManifestVersion(
|
|
9655
|
-
const configAbs = join26(
|
|
10543
|
+
function writeManifestVersion(repoRoot3, manifest) {
|
|
10544
|
+
const configAbs = join26(repoRoot3, CONFIG_PATH);
|
|
9656
10545
|
const seed = JSON.parse(readFileSync27(configAbs, "utf-8"));
|
|
9657
10546
|
const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
|
|
9658
10547
|
writeFileSync13(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
|
|
9659
10548
|
`, "utf-8");
|
|
9660
10549
|
}
|
|
9661
|
-
function stripFor(
|
|
9662
|
-
const abs2 = resolveManagedBlockPath(
|
|
10550
|
+
function stripFor(repoRoot3, path, marker, comment) {
|
|
10551
|
+
const abs2 = resolveManagedBlockPath(repoRoot3, path);
|
|
9663
10552
|
if (!existsSync24(abs2))
|
|
9664
10553
|
return { path, marker, comment, present: false, removesHost: false };
|
|
9665
10554
|
const stripped = stripBlockFromContent(readFileSync27(abs2, "utf-8"), marker, comment);
|
|
@@ -9667,15 +10556,15 @@ function stripFor(repoRoot2, path, marker, comment) {
|
|
|
9667
10556
|
return { path, marker, comment, present: false, removesHost: false };
|
|
9668
10557
|
return { path, marker, comment, present: true, removesHost: blockStripLeavesEmpty(stripped) };
|
|
9669
10558
|
}
|
|
9670
|
-
function planHookRouting(
|
|
10559
|
+
function planHookRouting(repoRoot3, manifest, recorded, hooksPathValue) {
|
|
9671
10560
|
const none = { target: recorded, strips: [], arm: false, migratesManifest: false, blockedReason: null };
|
|
9672
10561
|
if (recorded === ".husky")
|
|
9673
10562
|
return none;
|
|
9674
10563
|
if (recorded === TRACKED_HOOK_DIR) {
|
|
9675
|
-
const strips = manifest.blocks.filter((b) => b.path.startsWith(`${TRACKED_HOOK_DIR}/`)).map((b) => stripFor(
|
|
10564
|
+
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
10565
|
if (hooksPathValue === TRACKED_HOOK_DIR)
|
|
9677
10566
|
return { ...none, strips };
|
|
9678
|
-
const foreign = foreignRawHooks(
|
|
10567
|
+
const foreign = foreignRawHooks(repoRoot3);
|
|
9679
10568
|
if (foreign.length > 0) {
|
|
9680
10569
|
return {
|
|
9681
10570
|
...none,
|
|
@@ -9688,7 +10577,7 @@ function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
|
|
|
9688
10577
|
const legacy = manifest.blocks.filter((b) => b.path.startsWith(".git/hooks/"));
|
|
9689
10578
|
const blocked = [];
|
|
9690
10579
|
for (const b of legacy) {
|
|
9691
|
-
const abs2 = resolveManagedBlockPath(
|
|
10580
|
+
const abs2 = resolveManagedBlockPath(repoRoot3, b.path);
|
|
9692
10581
|
if (!existsSync24(abs2))
|
|
9693
10582
|
continue;
|
|
9694
10583
|
const stripped = stripBlockFromContent(readFileSync27(abs2, "utf-8"), b.marker, b.comment);
|
|
@@ -9698,7 +10587,7 @@ function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
|
|
|
9698
10587
|
blocked.push(`${b.path} carries your own lines alongside the managed block`);
|
|
9699
10588
|
}
|
|
9700
10589
|
const legacyNames = new Set(legacy.map((b) => b.path.slice(".git/hooks/".length)));
|
|
9701
|
-
for (const f of activeRawHooks(
|
|
10590
|
+
for (const f of activeRawHooks(repoRoot3)) {
|
|
9702
10591
|
if (!legacyNames.has(f))
|
|
9703
10592
|
blocked.push(`.git/hooks/${f} is an active raw hook vinaya does not manage`);
|
|
9704
10593
|
}
|
|
@@ -9706,7 +10595,7 @@ function planHookRouting(repoRoot2, manifest, recorded, hooksPathValue) {
|
|
|
9706
10595
|
return { ...none, blockedReason: blocked.join("; ") };
|
|
9707
10596
|
return {
|
|
9708
10597
|
target: TRACKED_HOOK_DIR,
|
|
9709
|
-
strips: legacy.map((b) => stripFor(
|
|
10598
|
+
strips: legacy.map((b) => stripFor(repoRoot3, b.path, b.marker, b.comment)).filter((s) => s.present),
|
|
9710
10599
|
arm: hooksPathValue !== TRACKED_HOOK_DIR,
|
|
9711
10600
|
migratesManifest: true,
|
|
9712
10601
|
blockedReason: null
|
|
@@ -9722,7 +10611,7 @@ function extractWorkflowTrigger(content) {
|
|
|
9722
10611
|
const match = content.match(/^on:\r?\n {2}([A-Za-z0-9_]+):/m);
|
|
9723
10612
|
return match?.[1] ?? null;
|
|
9724
10613
|
}
|
|
9725
|
-
function planUpgrade(ops,
|
|
10614
|
+
function planUpgrade(ops, repoRoot3, manifest, routing) {
|
|
9726
10615
|
const entries = [];
|
|
9727
10616
|
let hasChanges = routing.arm || routing.migratesManifest || routing.strips.some((s) => s.present);
|
|
9728
10617
|
const ownedFiles = new Set(manifest.files);
|
|
@@ -9730,7 +10619,7 @@ function planUpgrade(ops, repoRoot2, manifest, routing) {
|
|
|
9730
10619
|
const ownedBlocks = new Set(manifest.blocks.map((b) => blockKey(b.path, b.marker)));
|
|
9731
10620
|
for (const op of ops) {
|
|
9732
10621
|
if (op.kind === "create-file") {
|
|
9733
|
-
const abs2 = join26(
|
|
10622
|
+
const abs2 = join26(repoRoot3, op.path);
|
|
9734
10623
|
const exists = existsSync24(abs2);
|
|
9735
10624
|
const owned = ownedFiles.has(op.path) || isDefaultedAgentVendorPath(op.path, manifest);
|
|
9736
10625
|
let action;
|
|
@@ -9759,7 +10648,7 @@ function planUpgrade(ops, repoRoot2, manifest, routing) {
|
|
|
9759
10648
|
}
|
|
9760
10649
|
entries.push({ kind: "create-file", op, action, ...triggerChange ? { triggerChange } : {} });
|
|
9761
10650
|
} else if (op.kind === "managed-block") {
|
|
9762
|
-
const abs2 = resolveManagedBlockPath(
|
|
10651
|
+
const abs2 = resolveManagedBlockPath(repoRoot3, op.path);
|
|
9763
10652
|
const owned = ownedBlocks.has(blockKey(op.path, op.marker));
|
|
9764
10653
|
let action;
|
|
9765
10654
|
if (!owned) {
|
|
@@ -9872,8 +10761,8 @@ function renderUpgradeDiff(plan) {
|
|
|
9872
10761
|
return lines.join(`
|
|
9873
10762
|
`);
|
|
9874
10763
|
}
|
|
9875
|
-
function regenerateBlock(
|
|
9876
|
-
const abs2 = resolveManagedBlockPath(
|
|
10764
|
+
function regenerateBlock(repoRoot3, op) {
|
|
10765
|
+
const abs2 = resolveManagedBlockPath(repoRoot3, op.path);
|
|
9877
10766
|
const content = readFileSync27(abs2, "utf-8");
|
|
9878
10767
|
const stripped = stripBlockFromContent(content, op.marker, op.comment);
|
|
9879
10768
|
if (stripped !== null) {
|
|
@@ -9881,27 +10770,27 @@ function regenerateBlock(repoRoot2, op) {
|
|
|
9881
10770
|
`) ? stripped : `${stripped}
|
|
9882
10771
|
`, "utf-8");
|
|
9883
10772
|
}
|
|
9884
|
-
appendBlock(
|
|
10773
|
+
appendBlock(repoRoot3, op);
|
|
9885
10774
|
}
|
|
9886
|
-
function applyUpgrade(plan,
|
|
10775
|
+
function applyUpgrade(plan, repoRoot3) {
|
|
9887
10776
|
for (const e of plan.entries) {
|
|
9888
10777
|
if (e.kind === "create-file") {
|
|
9889
10778
|
if (e.action === "regenerate" || e.action === "recreate") {
|
|
9890
|
-
writeFileWithDirs(join26(
|
|
10779
|
+
writeFileWithDirs(join26(repoRoot3, e.op.path), e.op.content, e.op.mode);
|
|
9891
10780
|
}
|
|
9892
10781
|
} else {
|
|
9893
10782
|
if (e.action === "recreate-host")
|
|
9894
|
-
createHost(
|
|
10783
|
+
createHost(repoRoot3, e.op);
|
|
9895
10784
|
else if (e.action === "recreate-append")
|
|
9896
|
-
appendBlock(
|
|
10785
|
+
appendBlock(repoRoot3, e.op);
|
|
9897
10786
|
else if (e.action === "regenerate-block")
|
|
9898
|
-
regenerateBlock(
|
|
10787
|
+
regenerateBlock(repoRoot3, e.op);
|
|
9899
10788
|
}
|
|
9900
10789
|
}
|
|
9901
10790
|
for (const s of plan.routing.strips) {
|
|
9902
10791
|
if (!s.present)
|
|
9903
10792
|
continue;
|
|
9904
|
-
const abs2 = resolveManagedBlockPath(
|
|
10793
|
+
const abs2 = resolveManagedBlockPath(repoRoot3, s.path);
|
|
9905
10794
|
if (!existsSync24(abs2))
|
|
9906
10795
|
continue;
|
|
9907
10796
|
const stripped = stripBlockFromContent(readFileSync27(abs2, "utf-8"), s.marker, s.comment);
|
|
@@ -9993,16 +10882,16 @@ async function upgradeCommand(args) {
|
|
|
9993
10882
|
}
|
|
9994
10883
|
|
|
9995
10884
|
// src/commands/waiver.ts
|
|
9996
|
-
import { execFileSync as
|
|
10885
|
+
import { execFileSync as execFileSync18 } from "node:child_process";
|
|
9997
10886
|
function labelFor(kind) {
|
|
9998
10887
|
return kind === "docs" ? WAIVER_LABEL : WAIVER_LABEL_REVIEW;
|
|
9999
10888
|
}
|
|
10000
|
-
function
|
|
10001
|
-
return
|
|
10889
|
+
function gh3(args) {
|
|
10890
|
+
return execFileSync18("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: process.env }).trim();
|
|
10002
10891
|
}
|
|
10003
10892
|
function detectPrNumber() {
|
|
10004
10893
|
try {
|
|
10005
|
-
const out =
|
|
10894
|
+
const out = gh3(["pr", "view", "--json", "number", "-q", ".number"]);
|
|
10006
10895
|
return /^\d+$/.test(out) ? out : null;
|
|
10007
10896
|
} catch {
|
|
10008
10897
|
return null;
|
|
@@ -10098,8 +10987,8 @@ Reason: ${reason}`];
|
|
|
10098
10987
|
}
|
|
10099
10988
|
process.stdout.write(`Applying \`${label2}\` to PR #${prNumber} under your authenticated \`gh\` identity — vinaya never fabricates an actor.
|
|
10100
10989
|
`);
|
|
10101
|
-
|
|
10102
|
-
|
|
10990
|
+
execFileSync18("gh", editArgs, { stdio: "inherit", env: process.env });
|
|
10991
|
+
execFileSync18("gh", commentArgs, { stdio: "inherit", env: process.env });
|
|
10103
10992
|
process.stdout.write(`
|
|
10104
10993
|
✓ Label applied and reason posted as a PR comment.
|
|
10105
10994
|
`);
|
|
@@ -10223,8 +11112,10 @@ try {
|
|
|
10223
11112
|
prEditCommand(rest);
|
|
10224
11113
|
} else if (subcommand === "report") {
|
|
10225
11114
|
await prReportCommand(rest);
|
|
11115
|
+
} else if (subcommand === "verify-evidence") {
|
|
11116
|
+
await prVerifyEvidenceCommand(rest);
|
|
10226
11117
|
} else {
|
|
10227
|
-
console.error(`Unknown 'pr' subcommand: ${subcommand ?? "(none)"} (expected 'create', 'edit', or '
|
|
11118
|
+
console.error(`Unknown 'pr' subcommand: ${subcommand ?? "(none)"} (expected 'create', 'edit', 'report', or 'verify-evidence')`);
|
|
10228
11119
|
process.exit(2);
|
|
10229
11120
|
}
|
|
10230
11121
|
break;
|
|
@@ -10249,8 +11140,10 @@ try {
|
|
|
10249
11140
|
await milestoneAdoptCommand(rest);
|
|
10250
11141
|
} else if (subcommand === "edit") {
|
|
10251
11142
|
await milestoneEditCommand(rest);
|
|
11143
|
+
} else if (subcommand === "close") {
|
|
11144
|
+
await milestoneCloseCommand(rest);
|
|
10252
11145
|
} else {
|
|
10253
|
-
console.error(`Unknown 'milestone' subcommand: ${subcommand ?? "(none)"} (expected create/adopt/edit)`);
|
|
11146
|
+
console.error(`Unknown 'milestone' subcommand: ${subcommand ?? "(none)"} (expected create/adopt/edit/close)`);
|
|
10254
11147
|
process.exit(2);
|
|
10255
11148
|
}
|
|
10256
11149
|
break;
|