@attalabs/vinaya 0.26.0 → 0.27.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 +4 -2
- package/aeg-root/enforcement.md +2 -2
- package/aeg-root/milestone-model.md +2 -0
- package/aeg-root/process.md +1 -1
- package/aeg-root/roles/planner.md +2 -0
- package/aeg-root/roles/principal.md +4 -2
- package/aeg-root/roles/reviewer.md +6 -2
- package/aeg-root/roles/security.md +6 -2
- package/dist/checks/bin/check-body-bare-digits.js +128 -22
- package/dist/checks/bin/check-branch-topology.js +148 -24
- package/dist/checks/bin/check-brief-shape.js +430 -32
- package/dist/checks/bin/check-changeset-coverage.js +148 -24
- package/dist/checks/bin/check-closes-n.js +148 -24
- package/dist/checks/bin/check-coherence.js +148 -24
- package/dist/checks/bin/check-dead-branch-push.js +110 -22
- package/dist/checks/bin/check-dispatch-readiness.js +148 -24
- package/dist/checks/bin/check-doc-coverage-push.js +148 -24
- package/dist/checks/bin/check-doc-coverage.js +148 -24
- package/dist/checks/bin/check-doctrine-no-procedures.js +110 -22
- package/dist/checks/bin/check-doctrine-portability.js +148 -24
- package/dist/checks/bin/check-evidence-fresh.js +650 -63
- package/dist/checks/bin/check-exec-bits.js +148 -24
- package/dist/checks/bin/check-first-push-dispatch.js +148 -24
- package/dist/checks/bin/check-issue-assignment.js +148 -24
- package/dist/checks/bin/check-main-branch-refusal.js +110 -22
- package/dist/checks/bin/check-no-disk-state.js +110 -22
- package/dist/checks/bin/check-pr-premise-reassert.js +110 -22
- package/dist/checks/bin/check-pr-report-density.js +110 -22
- package/dist/checks/bin/check-quoted-command.js +148 -24
- package/dist/checks/bin/check-reader-resolvable-prose.js +148 -24
- package/dist/checks/bin/check-registry-gates.js +110 -22
- package/dist/checks/bin/check-retired-vocabulary.js +148 -24
- package/dist/checks/bin/check-review-gate.js +169 -77
- package/dist/checks/bin/check-single-plan-pr.js +110 -22
- package/dist/checks/bin/check-surface-scope.js +148 -24
- package/dist/checks/bin/check-test-plan.js +110 -22
- package/dist/checks/bin/check-token-collection-wired.js +110 -22
- package/dist/checks/bin/check-token-report.js +110 -22
- package/dist/checks/bin/check-workspace-escape.js +148 -24
- package/dist/index.js +1584 -572
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
4
5
|
import { readFileSync as readFileSync35 } from "node:fs";
|
|
5
|
-
import { dirname as
|
|
6
|
+
import { dirname as dirname13, join as join34 } from "node:path";
|
|
6
7
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7
8
|
|
|
8
9
|
// src/commands/archive.ts
|
|
@@ -281,7 +282,7 @@ function issueListByLabelArgs(owner, repo, label) {
|
|
|
281
282
|
"--state",
|
|
282
283
|
"all",
|
|
283
284
|
"--json",
|
|
284
|
-
"number,title,body,state,labels,milestone",
|
|
285
|
+
"number,title,body,state,labels,milestone,stateReason",
|
|
285
286
|
"--limit",
|
|
286
287
|
"200"
|
|
287
288
|
];
|
|
@@ -615,14 +616,19 @@ async function fetchTrancheIssuesAsync(owner, repo, slug) {
|
|
|
615
616
|
var INTENTS_HEADING = /^#{1,6}\s*Tranche intents\s*$/im;
|
|
616
617
|
var NEXT_HEADING = /^#{1,6}\s+\S/m;
|
|
617
618
|
var INTENT_BULLET = /^-\s+([a-z0-9][a-z0-9-]*)\s*:\s*(.+)$/i;
|
|
618
|
-
function
|
|
619
|
-
const text = stripCode(description, { inlineSpans: "keep" });
|
|
619
|
+
function intentsSection(text) {
|
|
620
620
|
const start = text.match(INTENTS_HEADING);
|
|
621
621
|
if (!start || start.index === undefined)
|
|
622
|
-
return
|
|
622
|
+
return null;
|
|
623
623
|
const rest = text.slice(start.index + start[0].length);
|
|
624
624
|
const next = rest.match(NEXT_HEADING);
|
|
625
|
-
|
|
625
|
+
return rest.slice(0, next && next.index !== undefined ? next.index : rest.length);
|
|
626
|
+
}
|
|
627
|
+
function intentGoalForSlug(description, slug) {
|
|
628
|
+
const text = stripCode(description, { inlineSpans: "keep" });
|
|
629
|
+
const section = intentsSection(text);
|
|
630
|
+
if (section === null)
|
|
631
|
+
return "";
|
|
626
632
|
for (const line of section.split(`
|
|
627
633
|
`)) {
|
|
628
634
|
const trimmed = line.trim();
|
|
@@ -634,6 +640,23 @@ function intentGoalForSlug(description, slug) {
|
|
|
634
640
|
}
|
|
635
641
|
return "";
|
|
636
642
|
}
|
|
643
|
+
function intentLines(description) {
|
|
644
|
+
const text = stripCode(description, { inlineSpans: "keep" });
|
|
645
|
+
const section = intentsSection(text);
|
|
646
|
+
if (section === null)
|
|
647
|
+
return [];
|
|
648
|
+
const lines = [];
|
|
649
|
+
for (const line of section.split(`
|
|
650
|
+
`)) {
|
|
651
|
+
const trimmed = line.trim();
|
|
652
|
+
if (trimmed.length === 0)
|
|
653
|
+
continue;
|
|
654
|
+
const m = trimmed.match(INTENT_BULLET);
|
|
655
|
+
if (m)
|
|
656
|
+
lines.push({ slug: (m[1] ?? "").toLowerCase(), goal: (m[2] ?? "").trim() });
|
|
657
|
+
}
|
|
658
|
+
return lines;
|
|
659
|
+
}
|
|
637
660
|
function matchesLegacyMilestone(milestones, slug) {
|
|
638
661
|
return milestones.find((m) => m.title === slug) ?? null;
|
|
639
662
|
}
|
|
@@ -1904,6 +1927,7 @@ function isPrincipal(login, principalAllowlist) {
|
|
|
1904
1927
|
// ../../packages/aeg-core/src/verdict-extraction.ts
|
|
1905
1928
|
var HEAD_SHA_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Judged head:\s*([0-9a-f]{7,40})(?![A-Za-z0-9])/im;
|
|
1906
1929
|
var OBJECTIVES_VERSION_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Objectives version:\s*([0-9a-f]{64})(?![A-Za-z0-9])/im;
|
|
1930
|
+
var RULING_ORDINAL_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?Ruling ordinal:\s*(\d+)(?!\d)/im;
|
|
1907
1931
|
function firstFiveLines(comment) {
|
|
1908
1932
|
return comment.split(`
|
|
1909
1933
|
`).slice(0, 5).join(`
|
|
@@ -1917,6 +1941,15 @@ function extractObjectivesVersion(comment) {
|
|
|
1917
1941
|
const m = firstFiveLines(comment).match(OBJECTIVES_VERSION_PATTERN);
|
|
1918
1942
|
return m ? m[1].toLowerCase() : null;
|
|
1919
1943
|
}
|
|
1944
|
+
function firstSevenLines(comment) {
|
|
1945
|
+
return comment.split(`
|
|
1946
|
+
`).slice(0, 7).join(`
|
|
1947
|
+
`);
|
|
1948
|
+
}
|
|
1949
|
+
function extractRulingOrdinal(comment) {
|
|
1950
|
+
const m = firstSevenLines(comment).match(RULING_ORDINAL_PATTERN);
|
|
1951
|
+
return m ? Number.parseInt(m[1], 10) : null;
|
|
1952
|
+
}
|
|
1920
1953
|
function extractVerdict(comments, valuePattern, missingLabel) {
|
|
1921
1954
|
const candidates = comments.filter((c) => valuePattern.test(c));
|
|
1922
1955
|
if (candidates.length === 0) {
|
|
@@ -1924,6 +1957,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
|
|
|
1924
1957
|
value: `no ${missingLabel} pass was run before merge — DANGLING, see below`,
|
|
1925
1958
|
headSha: null,
|
|
1926
1959
|
objectivesVersion: null,
|
|
1960
|
+
rulingOrdinal: null,
|
|
1927
1961
|
danglingNote: `no ${missingLabel} verdict comment found on this PR`
|
|
1928
1962
|
};
|
|
1929
1963
|
}
|
|
@@ -1934,6 +1968,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
|
|
|
1934
1968
|
value: `the most recent ${missingLabel} comment's VERDICT line is not within its first five lines — DANGLING, see below`,
|
|
1935
1969
|
headSha: null,
|
|
1936
1970
|
objectivesVersion: null,
|
|
1971
|
+
rulingOrdinal: null,
|
|
1937
1972
|
danglingNote: `the most recent ${missingLabel} verdict comment carries a VERDICT-shaped line outside the first-five-line read window`
|
|
1938
1973
|
};
|
|
1939
1974
|
}
|
|
@@ -1941,6 +1976,7 @@ function extractVerdict(comments, valuePattern, missingLabel) {
|
|
|
1941
1976
|
value: m[1].toUpperCase().replace(/[_-]/g, " "),
|
|
1942
1977
|
headSha: extractHeadSha(latest),
|
|
1943
1978
|
objectivesVersion: extractObjectivesVersion(latest),
|
|
1979
|
+
rulingOrdinal: extractRulingOrdinal(latest),
|
|
1944
1980
|
danglingNote: null
|
|
1945
1981
|
};
|
|
1946
1982
|
}
|
|
@@ -2471,6 +2507,9 @@ import { createHash as createHash2 } from "node:crypto";
|
|
|
2471
2507
|
var HEADING_RE = /^##[ \t]*Objectives[ \t]*$/im;
|
|
2472
2508
|
var NEXT_HEADING_RE = /^##[ \t]/m;
|
|
2473
2509
|
var OBJECTIVE_LINE_RE = /^O(\d+)\.[ \t]*(.*)$/;
|
|
2510
|
+
function maskedForHeadingSearch(body) {
|
|
2511
|
+
return maskDetailsBlocks(maskCode(body));
|
|
2512
|
+
}
|
|
2474
2513
|
function hasBacktickedPath(text) {
|
|
2475
2514
|
let i = 0;
|
|
2476
2515
|
while (i < text.length) {
|
|
@@ -2493,16 +2532,22 @@ function stripObjectiveBackticks(text) {
|
|
|
2493
2532
|
function wordCount(text) {
|
|
2494
2533
|
return text.split(/\s+/).filter((w) => /[a-z]/i.test(w)).length;
|
|
2495
2534
|
}
|
|
2496
|
-
function
|
|
2497
|
-
const
|
|
2535
|
+
function objectivesSectionBounds(body) {
|
|
2536
|
+
const masked = maskedForHeadingSearch(body);
|
|
2537
|
+
const heading = HEADING_RE.exec(masked);
|
|
2498
2538
|
if (!heading)
|
|
2499
2539
|
return null;
|
|
2500
|
-
const
|
|
2540
|
+
const start = heading.index + heading[0].length;
|
|
2541
|
+
const afterHeading = masked.slice(start);
|
|
2501
2542
|
const next = NEXT_HEADING_RE.exec(afterHeading);
|
|
2502
|
-
return next ?
|
|
2543
|
+
return { start, end: next ? start + next.index : body.length };
|
|
2544
|
+
}
|
|
2545
|
+
function objectivesSectionText(body) {
|
|
2546
|
+
const bounds = objectivesSectionBounds(body);
|
|
2547
|
+
return bounds === null ? null : body.slice(bounds.start, bounds.end);
|
|
2503
2548
|
}
|
|
2504
2549
|
function hasObjectivesHeading(body) {
|
|
2505
|
-
return HEADING_RE.test(body);
|
|
2550
|
+
return HEADING_RE.test(maskedForHeadingSearch(body));
|
|
2506
2551
|
}
|
|
2507
2552
|
function objectivesOf(body) {
|
|
2508
2553
|
const section = objectivesSectionText(body);
|
|
@@ -2566,6 +2611,15 @@ function isIssueNotFoundError(err) {
|
|
|
2566
2611
|
`);
|
|
2567
2612
|
return /could not resolve to an (?:issue|pull request)|\b404\b|not found/i.test(haystack);
|
|
2568
2613
|
}
|
|
2614
|
+
function resolveObjectivesSource(prBody, issue, cutoverIssue) {
|
|
2615
|
+
if (issue !== null && issue < cutoverIssue)
|
|
2616
|
+
return { kind: "none" };
|
|
2617
|
+
if (issue !== null)
|
|
2618
|
+
return { kind: "issue", issue };
|
|
2619
|
+
if (hasObjectivesHeading(prBody))
|
|
2620
|
+
return { kind: "body" };
|
|
2621
|
+
return { kind: "none" };
|
|
2622
|
+
}
|
|
2569
2623
|
|
|
2570
2624
|
// ../../packages/aeg-core/src/premise-check.ts
|
|
2571
2625
|
var ASSERTION_KINDS = new Set(["contains", "absent", "sha256"]);
|
|
@@ -2830,16 +2884,43 @@ function extractFencedBlocks(text) {
|
|
|
2830
2884
|
return blocks;
|
|
2831
2885
|
}
|
|
2832
2886
|
var AEG_BRIEF_V1_MARKER = "<!-- aeg:brief:v1 -->";
|
|
2833
|
-
function
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2887
|
+
function contentAfterNLines(body, n) {
|
|
2888
|
+
let idx = -1;
|
|
2889
|
+
for (let i = 0;i < n; i++) {
|
|
2890
|
+
idx = body.indexOf(`
|
|
2891
|
+
`, idx + 1);
|
|
2892
|
+
if (idx === -1)
|
|
2893
|
+
return "";
|
|
2894
|
+
}
|
|
2895
|
+
return body.slice(idx + 1);
|
|
2896
|
+
}
|
|
2897
|
+
function briefMarkerFor(version) {
|
|
2898
|
+
return `<!-- aeg:brief:v${version} -->`;
|
|
2899
|
+
}
|
|
2900
|
+
var BRIEF_MARKER_LINE_RE = /^<!-- aeg:brief:v(\d+) -->$/;
|
|
2901
|
+
function parseBriefMarkerVersion(firstLine) {
|
|
2902
|
+
const m = BRIEF_MARKER_LINE_RE.exec(firstLine.trim());
|
|
2903
|
+
if (!m)
|
|
2904
|
+
return null;
|
|
2905
|
+
const version = Number.parseInt(m[1], 10);
|
|
2906
|
+
return Number.isInteger(version) && version >= 1 ? version : null;
|
|
2907
|
+
}
|
|
2908
|
+
function frozenBriefContent(body, version) {
|
|
2909
|
+
return contentAfterNLines(body, version === 1 ? 2 : 3);
|
|
2910
|
+
}
|
|
2911
|
+
function resolveNewestFrozenBrief(comments, allowlist) {
|
|
2912
|
+
let best = null;
|
|
2913
|
+
for (const c of comments) {
|
|
2914
|
+
if (!isPrincipal(c.author, allowlist))
|
|
2915
|
+
continue;
|
|
2916
|
+
const version = parseBriefMarkerVersion(c.body.split(`
|
|
2917
|
+
`)[0] ?? "");
|
|
2918
|
+
if (version === null)
|
|
2919
|
+
continue;
|
|
2920
|
+
if (best === null || version > best.version)
|
|
2921
|
+
best = { ...c, version };
|
|
2922
|
+
}
|
|
2923
|
+
return best === null ? null : { ...best, content: frozenBriefContent(best.body, best.version) };
|
|
2843
2924
|
}
|
|
2844
2925
|
// ../../packages/aeg-core/src/doctrine-portability.ts
|
|
2845
2926
|
var EXEMPT_LITERALS = new Set([
|
|
@@ -3196,6 +3277,30 @@ function checkDocsWithinSurface(body, issueNumber) {
|
|
|
3196
3277
|
}
|
|
3197
3278
|
return { status: errors.length > 0 ? "fail" : "pass", errors };
|
|
3198
3279
|
}
|
|
3280
|
+
function checkSurfaceExcludesBoundDoc(body, docOwnersContent) {
|
|
3281
|
+
if (docOwnersContent === null)
|
|
3282
|
+
return { status: "pass", errors: [] };
|
|
3283
|
+
const surface = parseIssueSurface(body);
|
|
3284
|
+
if (!surface.ok)
|
|
3285
|
+
return { status: "pass", errors: [] };
|
|
3286
|
+
const { bindings } = parseDocOwners(docOwnersContent);
|
|
3287
|
+
if (bindings.length === 0)
|
|
3288
|
+
return { status: "pass", errors: [] };
|
|
3289
|
+
const errors = [];
|
|
3290
|
+
for (const binding of bindings) {
|
|
3291
|
+
if (isUrlPointer(binding.pointer))
|
|
3292
|
+
continue;
|
|
3293
|
+
const inGlob = surface.value.in.find((g) => globsOverlap(binding.glob, g));
|
|
3294
|
+
if (!inGlob)
|
|
3295
|
+
continue;
|
|
3296
|
+
const pointerPath = pointerToPath(binding.pointer);
|
|
3297
|
+
const outGlob = surface.value.out.find((g) => globCoversPath(g, pointerPath));
|
|
3298
|
+
if (!outGlob)
|
|
3299
|
+
continue;
|
|
3300
|
+
errors.push(`issue-validation Surface: \`${inGlob}\` in \`## Surface\`'s \`in:\` list matches ${DOC_OWNERS_PATH}:${binding.lineNum} (glob \`${binding.glob}\` → ${binding.pointer}), but \`${outGlob}\` in \`## Surface\`'s \`out:\` list excludes ${binding.pointer} — this task's own surface cannot satisfy doc-coverage and surface-scope at once.`);
|
|
3301
|
+
}
|
|
3302
|
+
return { status: errors.length > 0 ? "fail" : "pass", errors };
|
|
3303
|
+
}
|
|
3199
3304
|
|
|
3200
3305
|
// ../../packages/aeg-core/src/coherence-checks.ts
|
|
3201
3306
|
var R1_GRANDFATHERED_ISSUES = new Set([279, 280, 281, 282]);
|
|
@@ -3297,6 +3402,24 @@ function buildProvenanceBlock(facts) {
|
|
|
3297
3402
|
return { block: lines.join(`
|
|
3298
3403
|
`), issue, dangling };
|
|
3299
3404
|
}
|
|
3405
|
+
// ../../packages/aeg-core/src/ruling-ordinal.ts
|
|
3406
|
+
var RULING_MARKER_ORDINAL = /^<!-- aeg:principal:ruling:\d+-(\d+) -->$/;
|
|
3407
|
+
function newestPrincipalRulingOrdinal(comments, allowlist) {
|
|
3408
|
+
let best = 0;
|
|
3409
|
+
for (const c of comments) {
|
|
3410
|
+
if (!isPrincipal(c.author, allowlist))
|
|
3411
|
+
continue;
|
|
3412
|
+
const firstLine = (c.body.split(`
|
|
3413
|
+
`)[0] ?? "").trim();
|
|
3414
|
+
const m = RULING_MARKER_ORDINAL.exec(firstLine);
|
|
3415
|
+
if (!m)
|
|
3416
|
+
continue;
|
|
3417
|
+
const k = Number.parseInt(m[1], 10);
|
|
3418
|
+
if (k > best)
|
|
3419
|
+
best = k;
|
|
3420
|
+
}
|
|
3421
|
+
return best;
|
|
3422
|
+
}
|
|
3300
3423
|
// ../../packages/aeg-core/src/diagram-model.ts
|
|
3301
3424
|
import matter from "gray-matter";
|
|
3302
3425
|
// ../../packages/aeg-core/src/reader-resolvable-prose.ts
|
|
@@ -3616,12 +3739,26 @@ function renderSection2(facts) {
|
|
|
3616
3739
|
"",
|
|
3617
3740
|
`- **Tranche:** \`${facts.trancheSlug}\`, task ${facts.taskId}, Issue #${facts.issue}. Branch \`task/${facts.trancheSlug}/${facts.taskId}\`. \`Depends-on: ${depends}\`, \`Conflicts-with: ${conflicts}\`. Confirm \`READY TO DISPATCH\` at your own Step 0.`,
|
|
3618
3741
|
`- **Read Issue #${facts.issue} in full** for the complete rationale — do not re-derive it.`,
|
|
3742
|
+
`- **Revision:** rendered at \`${facts.sourceRevision}\` — the checkout's HEAD equaled the remote default branch, and no pinned file below carried an uncommitted change, when these facts were read.`,
|
|
3619
3743
|
`- ${facts.rationale.boundary}`,
|
|
3620
3744
|
`- ${facts.rationale.trapsToAvoid}`
|
|
3621
3745
|
];
|
|
3622
3746
|
return lines.join(`
|
|
3623
3747
|
`);
|
|
3624
3748
|
}
|
|
3749
|
+
function extractSourceRevision(briefText) {
|
|
3750
|
+
const lines = briefText.split(`
|
|
3751
|
+
`);
|
|
3752
|
+
const start = lines.findIndex((l) => /^##\s*2\.\s*Context\b/.test(l.trim()));
|
|
3753
|
+
if (start === -1)
|
|
3754
|
+
return null;
|
|
3755
|
+
const rest = lines.slice(start + 1);
|
|
3756
|
+
const end = rest.findIndex((l) => /^##\s/.test(l));
|
|
3757
|
+
const section2 = (end === -1 ? rest : rest.slice(0, end)).join(`
|
|
3758
|
+
`);
|
|
3759
|
+
const m = /^- \*\*Revision:\*\* rendered at `([0-9a-fA-F]{7,40})`/m.exec(section2);
|
|
3760
|
+
return m ? m[1] : null;
|
|
3761
|
+
}
|
|
3625
3762
|
function renderSection3(facts) {
|
|
3626
3763
|
return ["## 3. Technical dependencies", "", `${facts.rationale.dependencyRationale}`].join(`
|
|
3627
3764
|
`);
|
|
@@ -3825,6 +3962,9 @@ function renderBrief(facts, template) {
|
|
|
3825
3962
|
const missing = [];
|
|
3826
3963
|
if (facts.projects.length === 0)
|
|
3827
3964
|
missing.push("Project (task has no Project(s) declared)");
|
|
3965
|
+
if (!facts.sourceRevision) {
|
|
3966
|
+
missing.push("Revision (no source revision resolved — the caller must refuse before render, never render with an empty one)");
|
|
3967
|
+
}
|
|
3828
3968
|
if (facts.objectives.length === 0 && facts.issue >= OBJECTIVES_SINCE_ISSUE) {
|
|
3829
3969
|
missing.push("Objectives (Issue has no `## Objectives` section)");
|
|
3830
3970
|
}
|
|
@@ -7959,14 +8099,41 @@ var COMMANDS = [
|
|
|
7959
8099
|
},
|
|
7960
8100
|
{
|
|
7961
8101
|
name: "task dispatch",
|
|
7962
|
-
description: "
|
|
8102
|
+
description: "Deprecated — render, pin, and post the brief on the Issue as the frozen original; start the developer",
|
|
7963
8103
|
flags: [
|
|
7964
8104
|
{ flag: "--agent <claude|codex|gemini>", description: "Start the developer through dispatchRole once posted" }
|
|
7965
8105
|
],
|
|
7966
8106
|
details: [
|
|
7967
8107
|
"Renders the brief from the Issue and the tree (the same assembly `brief render` uses) and posts it once as an Issue comment whose first line is `<!-- aeg:brief:v1 -->` and whose second line is `Brief hash: <sha256>` — the hash covers only the body below those two lines, so any reader recomputes it. Refuses outright, naming the existing comment's URL, when a `v1` comment already exists on the Issue — the brief is frozen by design, never overwritten or silently reissued.",
|
|
7968
8108
|
"With `--agent`, starts the Developer through `dispatchRole` when `apps/cli/src/lib/dispatch.ts` exports it; otherwise prints the rendered brief and the manual dispatch instruction and exits `0` — a soft dependency, never a hard block.",
|
|
7969
|
-
"Principal-only, with or without `--agent`: refuses before any render, forge read, or post when the authenticated `gh` identity is not on the Principal allowlist (or cannot be resolved at all) — dispatching is the `todo → in-flight` transition, not a general-purpose comment poster."
|
|
8109
|
+
"Principal-only, with or without `--agent`: refuses before any render, forge read, or post when the authenticated `gh` identity is not on the Principal allowlist (or cannot be resolved at all) — dispatching is the `todo → in-flight` transition, not a general-purpose comment poster.",
|
|
8110
|
+
"Deprecated in favor of `task brief` (preparation only) and `task run` (the full unattended loop) — kept for a documented compatibility window while callers migrate."
|
|
8111
|
+
],
|
|
8112
|
+
status: "shipped"
|
|
8113
|
+
},
|
|
8114
|
+
{
|
|
8115
|
+
name: "task brief",
|
|
8116
|
+
description: "Render and freeze the brief as the Issue's original comment — preparation only, starts nobody",
|
|
8117
|
+
details: [
|
|
8118
|
+
"The preparation half of `task dispatch`, extracted so it is callable on its own: resolves the task's Issue, renders the brief, refuses on any gap, refuses when a frozen brief already exists, and posts it once as the same `aeg:brief:v1` Issue comment `task dispatch` posts. Starts no agent under any circumstances — there is no `--agent` flag here at all.",
|
|
8119
|
+
"Successor to `task dispatch` for the preparation step; the full unattended run (preparation, then the developer, then the review loop) is `task run`."
|
|
8120
|
+
],
|
|
8121
|
+
status: "shipped"
|
|
8122
|
+
},
|
|
8123
|
+
{
|
|
8124
|
+
name: "task run",
|
|
8125
|
+
description: "One command from a planned Issue to a reviewed pull request — exactly one developer started",
|
|
8126
|
+
flags: [
|
|
8127
|
+
{
|
|
8128
|
+
flag: "--agent <claude|codex|gemini>",
|
|
8129
|
+
description: "Vendor for the developer and both reviewers this run dispatches"
|
|
8130
|
+
}
|
|
8131
|
+
],
|
|
8132
|
+
details: [
|
|
8133
|
+
"Composes `task brief`'s own preparation (`prepareTask`) with `dev-review-loop` (`devReviewLoop`) — nothing else. Preparation starts no agent; the loop's own round 1 reads the frozen brief off the Issue and is the only place a developer is ever dispatched from a fresh task, so exactly one developer is started by construction.",
|
|
8134
|
+
"A brief already frozen on the Issue is reused, never re-posted — the second `task dispatch`/`task brief` call this composes around does not fail the whole run, it just skips straight to running the loop. A task whose Issue refuses preparation (a missing brief section, an unmet dispatch gate) is refused before any agent starts, with nothing posted.",
|
|
8135
|
+
"Refuses when the frozen brief's developer branch already has an open pull request — the old `task dispatch` followed by `task run` cannot start two developers this way.",
|
|
8136
|
+
"Exit and printed summary distinguish a published, reviewed pull request (exit `0`, the PR URL) from a pause (exit `1`, with the exact `vinaya dev-review-loop --resume <pr>` command to continue), a usage/argv error (exit `2`), and any other failure (exit `3`) — never sharing `1` with a pause, so an unattended host tells them apart from the exit code alone. A run that pauses is resumed with the loop's own existing `--resume <pr>` flag, never a flag on this command."
|
|
7970
8137
|
],
|
|
7971
8138
|
status: "shipped"
|
|
7972
8139
|
},
|
|
@@ -8174,6 +8341,15 @@ var COMMANDS = [
|
|
|
8174
8341
|
],
|
|
8175
8342
|
status: "shipped"
|
|
8176
8343
|
},
|
|
8344
|
+
{
|
|
8345
|
+
name: "milestone status",
|
|
8346
|
+
description: "Print each of a Milestone's declared tranche intents with its derived lifecycle and issue counts",
|
|
8347
|
+
flags: [{ flag: "--json", description: "Enveloped JSON output (schema: 1)" }],
|
|
8348
|
+
details: [
|
|
8349
|
+
"Read-only — writes nothing. For each `- <slug>: …` line in the Milestone's `### Tranche intents` section, prints the tranche's lifecycle (`planned`/`active`/`complete`) and its labeled Issues' counts (merged, open, not planned), all derived from the forge. Refuses if `<n>` isn't a real Milestone in this repo, or if the forge is unreachable."
|
|
8350
|
+
],
|
|
8351
|
+
status: "shipped"
|
|
8352
|
+
},
|
|
8177
8353
|
{
|
|
8178
8354
|
name: "review status",
|
|
8179
8355
|
description: "Print the review loop's own state for a PR, and its branch's distance from the base",
|
|
@@ -8664,6 +8840,62 @@ function expandGlob(glob) {
|
|
|
8664
8840
|
function sha256OfFile(path) {
|
|
8665
8841
|
return createHash5("sha256").update(readFileSync9(path)).digest("hex");
|
|
8666
8842
|
}
|
|
8843
|
+
function resolveRemoteDefaultBranch(cwd) {
|
|
8844
|
+
let out;
|
|
8845
|
+
try {
|
|
8846
|
+
out = execFileSync9("git", ["ls-remote", "--symref", "origin", "HEAD"], {
|
|
8847
|
+
cwd,
|
|
8848
|
+
encoding: "utf8",
|
|
8849
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
8850
|
+
}).trim();
|
|
8851
|
+
} catch {
|
|
8852
|
+
return null;
|
|
8853
|
+
}
|
|
8854
|
+
const lines = out.split(`
|
|
8855
|
+
`);
|
|
8856
|
+
const symrefLine = lines.find((l) => l.startsWith("ref:"));
|
|
8857
|
+
const shaLine = lines.find((l) => !l.startsWith("ref:") && /\tHEAD$/.test(l));
|
|
8858
|
+
const branch = symrefLine ? /^ref:\s*refs\/heads\/(\S+)/.exec(symrefLine)?.[1] : undefined;
|
|
8859
|
+
const sha = shaLine?.split("\t")[0];
|
|
8860
|
+
return branch && sha ? { branch, sha } : null;
|
|
8861
|
+
}
|
|
8862
|
+
function checkStaleAgainstRemote(headSha, resolveRemote = resolveRemoteDefaultBranch) {
|
|
8863
|
+
const remote = resolveRemote();
|
|
8864
|
+
if (!remote) {
|
|
8865
|
+
return [
|
|
8866
|
+
"the remote default branch could not be resolved (`git ls-remote origin HEAD` failed — offline?) — refusing rather than rendering from a checkout of unknown freshness."
|
|
8867
|
+
];
|
|
8868
|
+
}
|
|
8869
|
+
if (headSha !== remote.sha) {
|
|
8870
|
+
return [
|
|
8871
|
+
`checkout HEAD \`${headSha}\` is behind the remote default branch \`${remote.branch}\` at \`${remote.sha}\` — fetch and update before preparing a brief.`
|
|
8872
|
+
];
|
|
8873
|
+
}
|
|
8874
|
+
return [];
|
|
8875
|
+
}
|
|
8876
|
+
function checkDirtyPinnedFiles(pinnedPaths, cwd) {
|
|
8877
|
+
if (pinnedPaths.length === 0)
|
|
8878
|
+
return [];
|
|
8879
|
+
let out;
|
|
8880
|
+
try {
|
|
8881
|
+
out = execFileSync9("git", ["status", "--porcelain", "--", ...pinnedPaths], {
|
|
8882
|
+
cwd,
|
|
8883
|
+
encoding: "utf8",
|
|
8884
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
8885
|
+
});
|
|
8886
|
+
} catch (err) {
|
|
8887
|
+
return [
|
|
8888
|
+
`could not check working-tree status for the brief's pinned files: ${err instanceof Error ? err.message : String(err)}`
|
|
8889
|
+
];
|
|
8890
|
+
}
|
|
8891
|
+
if (!out.trim())
|
|
8892
|
+
return [];
|
|
8893
|
+
const dirty = out.split(`
|
|
8894
|
+
`).filter((l) => l.length > 0).map((l) => l.slice(3).trim()).filter(Boolean);
|
|
8895
|
+
return [
|
|
8896
|
+
`checkout carries uncommitted changes to pinned file(s): ${dirty.join(", ")} — commit or discard them before preparing a brief.`
|
|
8897
|
+
];
|
|
8898
|
+
}
|
|
8667
8899
|
function resolveBoundaryPaths(tokens, allTrackedFiles) {
|
|
8668
8900
|
const trackedSet = new Set(allTrackedFiles);
|
|
8669
8901
|
const resolved = new Set;
|
|
@@ -8716,6 +8948,10 @@ async function assembleAndRenderBrief(trancheSlug, taskId, surfaceGlobsOverride)
|
|
|
8716
8948
|
missing: ["could not resolve owner/repo (set AEG_REPO=owner/repo, or confirm `git remote get-url origin`)."]
|
|
8717
8949
|
};
|
|
8718
8950
|
}
|
|
8951
|
+
const headSha = git(["rev-parse", "HEAD"]);
|
|
8952
|
+
const staleness = checkStaleAgainstRemote(headSha);
|
|
8953
|
+
if (staleness.length > 0)
|
|
8954
|
+
return { ok: false, missing: staleness };
|
|
8719
8955
|
const source = createForgeSource({ owner: repo.owner, repo: repo.repo });
|
|
8720
8956
|
let tranche;
|
|
8721
8957
|
try {
|
|
@@ -8797,6 +9033,9 @@ async function assembleAndRenderBrief(trancheSlug, taskId, surfaceGlobsOverride)
|
|
|
8797
9033
|
`).map((s) => s.trim()).filter(Boolean);
|
|
8798
9034
|
const boundaryTokens = extractBoundaryFilePaths(rationale.boundary ?? "");
|
|
8799
9035
|
const surfaceFiles = resolveBoundaryPaths(boundaryTokens, allTrackedFiles).sort().map((path) => ({ path, sha256: sha256OfFile(path), packageName: packageNameForPath(path) }));
|
|
9036
|
+
const dirtiness = checkDirtyPinnedFiles(surfaceFiles.map((f) => f.path));
|
|
9037
|
+
if (dirtiness.length > 0)
|
|
9038
|
+
return { ok: false, missing: dirtiness };
|
|
8800
9039
|
const workspaces = workspaceGlobs();
|
|
8801
9040
|
const consumersOf = buildConsumersOf(workspaces, listDirs, readManifest);
|
|
8802
9041
|
const partsResult = parseIssueParts(issueBody);
|
|
@@ -8826,7 +9065,8 @@ async function assembleAndRenderBrief(trancheSlug, taskId, surfaceGlobsOverride)
|
|
|
8826
9065
|
dispatchBlockers: gate.blockers,
|
|
8827
9066
|
surfaceFiles,
|
|
8828
9067
|
consumersOf,
|
|
8829
|
-
docOwnersContent: existsSync7(DOC_OWNERS_PATH2) ? readFileSync9(DOC_OWNERS_PATH2, "utf8") : null
|
|
9068
|
+
docOwnersContent: existsSync7(DOC_OWNERS_PATH2) ? readFileSync9(DOC_OWNERS_PATH2, "utf8") : null,
|
|
9069
|
+
sourceRevision: headSha
|
|
8830
9070
|
};
|
|
8831
9071
|
const template = readFileSync9(TEMPLATE_PATH, "utf8");
|
|
8832
9072
|
const result = renderBrief(facts, template);
|
|
@@ -10563,6 +10803,27 @@ function log(e) {
|
|
|
10563
10803
|
|
|
10564
10804
|
// src/lib/dispatch.ts
|
|
10565
10805
|
import { dirname as dirname7, join as join14 } from "node:path";
|
|
10806
|
+
var ANSI_RESET = "\x1B[0m";
|
|
10807
|
+
var ROLE_ANSI = {
|
|
10808
|
+
planner: "\x1B[34m",
|
|
10809
|
+
developer: "\x1B[36m",
|
|
10810
|
+
"code-reviewer": "\x1B[35m",
|
|
10811
|
+
security: "\x1B[31m",
|
|
10812
|
+
principal: "\x1B[33m",
|
|
10813
|
+
archivist: "\x1B[32m",
|
|
10814
|
+
architect: "\x1B[93m"
|
|
10815
|
+
};
|
|
10816
|
+
var LOOP_ANSI = "\x1B[90m";
|
|
10817
|
+
function colourEnabled(stream) {
|
|
10818
|
+
return Boolean(stream.isTTY) && process.env.NO_COLOR === undefined;
|
|
10819
|
+
}
|
|
10820
|
+
function colourAgentLine(role, line, stream) {
|
|
10821
|
+
const prefixed = `[${role}] ${line}`;
|
|
10822
|
+
return colourEnabled(stream) ? `${ROLE_ANSI[role]}${prefixed}${ANSI_RESET}` : prefixed;
|
|
10823
|
+
}
|
|
10824
|
+
function colourLoopLine(line, stream) {
|
|
10825
|
+
return colourEnabled(stream) ? `${LOOP_ANSI}${line}${ANSI_RESET}` : line;
|
|
10826
|
+
}
|
|
10566
10827
|
var AGENT_VENDOR_NAMES = ["claude", "codex", "gemini"];
|
|
10567
10828
|
function isAgentVendor2(value) {
|
|
10568
10829
|
return AGENT_VENDOR_NAMES.includes(value);
|
|
@@ -10757,6 +11018,14 @@ function recordResumeState(record) {
|
|
|
10757
11018
|
return null;
|
|
10758
11019
|
}
|
|
10759
11020
|
}
|
|
11021
|
+
function readResumeRecord(role, agent, repo, task, pr) {
|
|
11022
|
+
try {
|
|
11023
|
+
const path = resumeRecordPathFor(role, agent, repo, task, pr);
|
|
11024
|
+
return JSON.parse(readFileSync14(path, "utf8"));
|
|
11025
|
+
} catch {
|
|
11026
|
+
return null;
|
|
11027
|
+
}
|
|
11028
|
+
}
|
|
10760
11029
|
function renderClaudeEvent(obj) {
|
|
10761
11030
|
const type = obj.type;
|
|
10762
11031
|
if (type === "assistant" || type === "user") {
|
|
@@ -10929,6 +11198,10 @@ async function dispatchRole(role, agent, prompt2, opts) {
|
|
|
10929
11198
|
const effectId = randomUUID2();
|
|
10930
11199
|
const vendor = VENDOR_TABLE[agent];
|
|
10931
11200
|
const start = Date.now();
|
|
11201
|
+
const writeLifecycle = (msg) => {
|
|
11202
|
+
process.stderr.write(`${colourLoopLine(msg, process.stderr)}
|
|
11203
|
+
`);
|
|
11204
|
+
};
|
|
10932
11205
|
const roundField = opts.round !== undefined ? { round: opts.round } : {};
|
|
10933
11206
|
const resolvedModel = opts.model !== undefined ? `requested:${opts.model}` : "default";
|
|
10934
11207
|
if (opts.model !== undefined) {
|
|
@@ -10948,8 +11221,7 @@ async function dispatchRole(role, agent, prompt2, opts) {
|
|
|
10948
11221
|
usage: null,
|
|
10949
11222
|
duration_ms: durationMs
|
|
10950
11223
|
});
|
|
10951
|
-
|
|
10952
|
-
`);
|
|
11224
|
+
writeLifecycle(`[vinaya dispatch ${effectId}] ${role} via ${agent}: refused — model '${opts.model}' is a ${foreignVendor} model; ` + `${agent} does not accept it. ${agent} accepts its own model names (never a ${foreignVendor} alias or a ` + `'${foreignVendor}-'/'gemma-' full name).`);
|
|
10953
11225
|
await waitForDispatchLine(outboxPath, priorSize, runId, effectId, "dispatch_failed");
|
|
10954
11226
|
return { exitCode: null, durationMs, usage: null, resumeId: null, timedOut: false, failureReason: "refused" };
|
|
10955
11227
|
}
|
|
@@ -11006,8 +11278,7 @@ async function dispatchRole(role, agent, prompt2, opts) {
|
|
|
11006
11278
|
const MAX_STDOUT_BYTES = 1e6;
|
|
11007
11279
|
const outputTee = openOutputTee(effectId);
|
|
11008
11280
|
if (outputTee.path !== null) {
|
|
11009
|
-
|
|
11010
|
-
`);
|
|
11281
|
+
writeLifecycle(`[vinaya dispatch ${effectId}] ${role} via ${agent}: output teed to ${outputTee.path}`);
|
|
11011
11282
|
}
|
|
11012
11283
|
let renderCarry = "";
|
|
11013
11284
|
child.stdout.on("data", (chunk) => {
|
|
@@ -11023,9 +11294,13 @@ async function dispatchRole(role, agent, prompt2, opts) {
|
|
|
11023
11294
|
continue;
|
|
11024
11295
|
try {
|
|
11025
11296
|
const rendered = vendor.renderEvent(JSON.parse(line));
|
|
11026
|
-
if (rendered)
|
|
11027
|
-
|
|
11297
|
+
if (rendered) {
|
|
11298
|
+
const out = rendered.split(`
|
|
11299
|
+
`).map((l) => colourAgentLine(role, l, process.stderr)).join(`
|
|
11300
|
+
`);
|
|
11301
|
+
process.stderr.write(`${out}
|
|
11028
11302
|
`);
|
|
11303
|
+
}
|
|
11029
11304
|
} catch {}
|
|
11030
11305
|
}
|
|
11031
11306
|
});
|
|
@@ -11034,22 +11309,18 @@ async function dispatchRole(role, agent, prompt2, opts) {
|
|
|
11034
11309
|
});
|
|
11035
11310
|
const heartbeatTimer = setInterval(() => {
|
|
11036
11311
|
const elapsedS = Math.round((Date.now() - start) / 1000);
|
|
11037
|
-
|
|
11038
|
-
`);
|
|
11312
|
+
writeLifecycle(`[vinaya dispatch ${effectId}] ${role} via ${agent}: still running — ${elapsedS}s elapsed (ceiling ${Math.round(timeoutMs / 1000)}s)`);
|
|
11039
11313
|
}, HEARTBEAT_INTERVAL_MS);
|
|
11040
11314
|
const warnLeadMs = timeoutWarningLeadMs(timeoutMs);
|
|
11041
11315
|
const warnTimer = setTimeout(() => {
|
|
11042
|
-
|
|
11043
|
-
`);
|
|
11316
|
+
writeLifecycle(`[vinaya dispatch ${effectId}] ${role} via ${agent}: approaching timeout — SIGTERM in ~${Math.round(warnLeadMs / 1000)}s unless it finishes first`);
|
|
11044
11317
|
}, Math.max(timeoutMs - warnLeadMs, 0));
|
|
11045
11318
|
const timeoutTimer = setTimeout(() => {
|
|
11046
11319
|
timedOut = true;
|
|
11047
|
-
|
|
11048
|
-
`);
|
|
11320
|
+
writeLifecycle(`[vinaya dispatch ${effectId}] ${role} via ${agent}: ceiling reached — sending SIGTERM`);
|
|
11049
11321
|
child.kill("SIGTERM");
|
|
11050
11322
|
killTimer = setTimeout(() => {
|
|
11051
|
-
|
|
11052
|
-
`);
|
|
11323
|
+
writeLifecycle(`[vinaya dispatch ${effectId}] ${role} via ${agent}: still alive after SIGTERM — sending SIGKILL`);
|
|
11053
11324
|
child.kill("SIGKILL");
|
|
11054
11325
|
}, SIGKILL_GRACE_MS);
|
|
11055
11326
|
}, timeoutMs);
|
|
@@ -11134,8 +11405,7 @@ async function dispatchRole(role, agent, prompt2, opts) {
|
|
|
11134
11405
|
capturedAt: new Date().toISOString()
|
|
11135
11406
|
});
|
|
11136
11407
|
if (resumeRecordPath !== null) {
|
|
11137
|
-
|
|
11138
|
-
`);
|
|
11408
|
+
writeLifecycle(`[vinaya dispatch ${effectId}] ${role} via ${agent}: resumable — session recorded at ${resumeRecordPath}`);
|
|
11139
11409
|
}
|
|
11140
11410
|
}
|
|
11141
11411
|
const reportedModel = vendor.parseModel(stdoutBuf);
|
|
@@ -11161,10 +11431,10 @@ async function dispatchRole(role, agent, prompt2, opts) {
|
|
|
11161
11431
|
|
|
11162
11432
|
// src/lib/dev-review-loop.ts
|
|
11163
11433
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
11164
|
-
import { execFileSync as
|
|
11165
|
-
import { mkdirSync as mkdirSync5, mkdtempSync as
|
|
11166
|
-
import { tmpdir as
|
|
11167
|
-
import { dirname as dirname8, join as
|
|
11434
|
+
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
11435
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync2, readFileSync as readFileSync17, rmSync as rmSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync8 } from "node:fs";
|
|
11436
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
11437
|
+
import { dirname as dirname8, join as join17 } from "node:path";
|
|
11168
11438
|
|
|
11169
11439
|
// src/commands/review-post.ts
|
|
11170
11440
|
import { execFileSync as execFileSync15 } from "node:child_process";
|
|
@@ -11494,6 +11764,15 @@ function readProjectPaths(root = repoRoot()) {
|
|
|
11494
11764
|
return [];
|
|
11495
11765
|
}
|
|
11496
11766
|
}
|
|
11767
|
+
function readDocOwnersContent(root = repoRoot()) {
|
|
11768
|
+
if (!root)
|
|
11769
|
+
return null;
|
|
11770
|
+
try {
|
|
11771
|
+
return readFileSync15(join15(root, DOC_OWNERS_PATH), "utf8");
|
|
11772
|
+
} catch {
|
|
11773
|
+
return null;
|
|
11774
|
+
}
|
|
11775
|
+
}
|
|
11497
11776
|
var CHECK_ISSUE_CONTENT = "issue-content";
|
|
11498
11777
|
var ISSUE_CONTENT_RECOVERY = {
|
|
11499
11778
|
blastRadius: "Add a second registered `Project(s)` this task also touches, or a `blast-radius-ack: <why one lens is enough>` line, then re-run `{cmd}`.",
|
|
@@ -11501,7 +11780,8 @@ var ISSUE_CONTENT_RECOVERY = {
|
|
|
11501
11780
|
rationaleNamesDocs: 'Name a concrete doc/skill path (e.g. `aeg-root/…`, `.claude/skills/…/SKILL.md`) in "Docs to keep coherent" or "Traps", or write the `no-doc-surface` sentinel if the surface genuinely has none, then re-run `{cmd}`.',
|
|
11502
11781
|
surfaceGlobsResolve: "Fix the named `## Surface` `in:` glob so it matches at least one real tracked file (a typo, or a directory that does not exist yet), then re-run `{cmd}`.",
|
|
11503
11782
|
partsCiteObjectives: "Fix the named Part to cite an objective id the `## Objectives` section actually defines, or add the missing objective, then re-run `{cmd}`.",
|
|
11504
|
-
docsWithinSurface: 'Move the named doc pointer to a path `## Surface`\'s `in:` globs actually cover (never widen the surface just to fit the pointer — that renders an unusable brief), or drop it from "Docs to keep coherent" if this task does not really keep it coherent, then re-run `{cmd}`.'
|
|
11783
|
+
docsWithinSurface: 'Move the named doc pointer to a path `## Surface`\'s `in:` globs actually cover (never widen the surface just to fit the pointer — that renders an unusable brief), or drop it from "Docs to keep coherent" if this task does not really keep it coherent, then re-run `{cmd}`.',
|
|
11784
|
+
surfaceExcludesBoundDoc: "Either move the named `out:` glob so it no longer covers the bound document, or narrow the `in:` glob so it no longer reaches the doc-owners binding — the Issue cannot declare both at once. Then re-run `{cmd}`."
|
|
11505
11785
|
};
|
|
11506
11786
|
function validateIssueContent(input) {
|
|
11507
11787
|
const findings = [
|
|
@@ -11513,7 +11793,8 @@ function validateIssueContent(input) {
|
|
|
11513
11793
|
[checkRationaleNamesDocs(input.body).errors, "rationaleNamesDocs"],
|
|
11514
11794
|
[checkSurfaceGlobsResolve(input.body, input.resolvesToFile).errors, "surfaceGlobsResolve"],
|
|
11515
11795
|
[checkPartsCiteDefinedObjectives(input.body).errors, "partsCiteObjectives"],
|
|
11516
|
-
[checkDocsWithinSurface(input.body, input.issueNumber).errors, "docsWithinSurface"]
|
|
11796
|
+
[checkDocsWithinSurface(input.body, input.issueNumber).errors, "docsWithinSurface"],
|
|
11797
|
+
[checkSurfaceExcludesBoundDoc(input.body, input.docOwnersContent).errors, "surfaceExcludesBoundDoc"]
|
|
11517
11798
|
];
|
|
11518
11799
|
const errors = [];
|
|
11519
11800
|
for (const [messages, kind] of findings) {
|
|
@@ -11587,7 +11868,8 @@ function validateTaskIssue(body, title, labels, retryCommand, issueNumber) {
|
|
|
11587
11868
|
projectPaths: readProjectPaths(),
|
|
11588
11869
|
retryCommand,
|
|
11589
11870
|
issueNumber,
|
|
11590
|
-
resolvesToFile: (glob) => expandGlob(glob).length > 0
|
|
11871
|
+
resolvesToFile: (glob) => expandGlob(glob).length > 0,
|
|
11872
|
+
docOwnersContent: readDocOwnersContent()
|
|
11591
11873
|
});
|
|
11592
11874
|
if (contentErrors.length > 0)
|
|
11593
11875
|
refuse2(contentErrors);
|
|
@@ -11707,14 +11989,14 @@ function renderFindingsSection(findings) {
|
|
|
11707
11989
|
class ObjectivesParseError extends Error {
|
|
11708
11990
|
}
|
|
11709
11991
|
var OBJECTIVE_ID_ONLY = /^O\d+$/;
|
|
11710
|
-
var STRUCTURAL_MARKER_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?(?:VERDICT|Judged head|Objectives version):/i;
|
|
11992
|
+
var STRUCTURAL_MARKER_PATTERN = /^[ \t]*(?:\*{1,3}|_{1,3})?(?:VERDICT|Judged head|Objectives version|Ruling ordinal):/i;
|
|
11711
11993
|
function invalidObjectiveEvidenceReason(evidence) {
|
|
11712
11994
|
if (evidence.includes(`
|
|
11713
11995
|
`)) {
|
|
11714
11996
|
return "evidence contains a newline — each objective renders as exactly one line";
|
|
11715
11997
|
}
|
|
11716
11998
|
if (STRUCTURAL_MARKER_PATTERN.test(evidence)) {
|
|
11717
|
-
return "evidence looks like a VERDICT:/Judged head:/Objectives version: line, which would corrupt the rendered comment's structural markers";
|
|
11999
|
+
return "evidence looks like a VERDICT:/Judged head:/Objectives version:/Ruling ordinal: line, which would corrupt the rendered comment's structural markers";
|
|
11718
12000
|
}
|
|
11719
12001
|
return null;
|
|
11720
12002
|
}
|
|
@@ -11797,6 +12079,7 @@ function renderCodeReviewComment(input) {
|
|
|
11797
12079
|
if (input.objectivesVersion !== null) {
|
|
11798
12080
|
lines.push(`Objectives version: ${input.objectivesVersion}`, "");
|
|
11799
12081
|
}
|
|
12082
|
+
lines.push(`Ruling ordinal: ${input.rulingOrdinal}`, "");
|
|
11800
12083
|
if (input.scopeEvidence !== null) {
|
|
11801
12084
|
lines.push("```", input.scopeEvidence, "```", "");
|
|
11802
12085
|
}
|
|
@@ -11820,6 +12103,7 @@ function renderSecurityComment(input) {
|
|
|
11820
12103
|
if (input.objectivesVersion !== null) {
|
|
11821
12104
|
lines.push(`Objectives version: ${input.objectivesVersion}`, "");
|
|
11822
12105
|
}
|
|
12106
|
+
lines.push(`Ruling ordinal: ${input.rulingOrdinal}`, "");
|
|
11823
12107
|
lines.push("FINDINGS (ordered by severity):", renderFindingsSection(sorted), "");
|
|
11824
12108
|
if (input.objectiveResults !== null) {
|
|
11825
12109
|
lines.push(renderObjectivesBlock(input.objectiveResults), "");
|
|
@@ -11900,6 +12184,7 @@ function renderEscalationComment(input) {
|
|
|
11900
12184
|
if (input.objectivesVersion !== null) {
|
|
11901
12185
|
lines.push(`Objectives version: ${input.objectivesVersion}`, "");
|
|
11902
12186
|
}
|
|
12187
|
+
lines.push(`Ruling ordinal: ${input.rulingOrdinal}`, "");
|
|
11903
12188
|
lines.push(input.summary, "", renderTokensLine(input.role, input.roleLabel, input), renderCastByLine(input.roleLabel, input.sessionId));
|
|
11904
12189
|
return lines.join(`
|
|
11905
12190
|
`);
|
|
@@ -11909,7 +12194,7 @@ function isBoundToHead(extraction, headSha) {
|
|
|
11909
12194
|
return false;
|
|
11910
12195
|
return headSha.toLowerCase().startsWith(extraction.headSha.toLowerCase());
|
|
11911
12196
|
}
|
|
11912
|
-
function checkExtraction(extraction, expectedValue, headSha, expectedObjectivesVersion) {
|
|
12197
|
+
function checkExtraction(extraction, expectedValue, headSha, expectedObjectivesVersion, expectedRulingOrdinal) {
|
|
11913
12198
|
if (extraction.danglingNote) {
|
|
11914
12199
|
return {
|
|
11915
12200
|
ok: false,
|
|
@@ -11937,6 +12222,12 @@ function checkExtraction(extraction, expectedValue, headSha, expectedObjectivesV
|
|
|
11937
12222
|
reason: `re-extraction found objectives version ${extraction.objectivesVersion ?? "none"}, expected ${expectedObjectivesVersion ?? "none"} — the posted comment's Objectives version: line does not match what this command rendered.`
|
|
11938
12223
|
};
|
|
11939
12224
|
}
|
|
12225
|
+
if (extraction.rulingOrdinal !== expectedRulingOrdinal) {
|
|
12226
|
+
return {
|
|
12227
|
+
ok: false,
|
|
12228
|
+
reason: `re-extraction found ruling ordinal ${extraction.rulingOrdinal ?? "none"}, expected ${expectedRulingOrdinal} — the posted comment's Ruling ordinal: line does not match what this command rendered.`
|
|
12229
|
+
};
|
|
12230
|
+
}
|
|
11940
12231
|
return { ok: true, reason: "clean" };
|
|
11941
12232
|
}
|
|
11942
12233
|
function principalBodies(comments, principalAllowlist) {
|
|
@@ -11952,14 +12243,14 @@ function checkNoCrossRoleVerdict(postedBody, crossExtract, crossRoleLabel) {
|
|
|
11952
12243
|
}
|
|
11953
12244
|
return null;
|
|
11954
12245
|
}
|
|
11955
|
-
function verifyPostedCodeReview(comments, verdict, headSha, principalAllowlist, postedBody, objectivesVersion2) {
|
|
11956
|
-
const own = checkExtraction(extractCodeReviewVerdict(principalBodies(comments, principalAllowlist)), CODE_REVIEW_VERDICT_TEXT[verdict], headSha, objectivesVersion2);
|
|
12246
|
+
function verifyPostedCodeReview(comments, verdict, headSha, principalAllowlist, postedBody, objectivesVersion2, rulingOrdinal) {
|
|
12247
|
+
const own = checkExtraction(extractCodeReviewVerdict(principalBodies(comments, principalAllowlist)), CODE_REVIEW_VERDICT_TEXT[verdict], headSha, objectivesVersion2, rulingOrdinal);
|
|
11957
12248
|
if (!own.ok)
|
|
11958
12249
|
return own;
|
|
11959
12250
|
return checkNoCrossRoleVerdict(postedBody, extractSecurityReviewVerdict, "security") ?? own;
|
|
11960
12251
|
}
|
|
11961
|
-
function verifyPostedSecurity(comments, verdict, headSha, principalAllowlist, postedBody, objectivesVersion2) {
|
|
11962
|
-
const own = checkExtraction(extractSecurityReviewVerdict(principalBodies(comments, principalAllowlist)), verdict, headSha, objectivesVersion2);
|
|
12252
|
+
function verifyPostedSecurity(comments, verdict, headSha, principalAllowlist, postedBody, objectivesVersion2, rulingOrdinal) {
|
|
12253
|
+
const own = checkExtraction(extractSecurityReviewVerdict(principalBodies(comments, principalAllowlist)), verdict, headSha, objectivesVersion2, rulingOrdinal);
|
|
11963
12254
|
if (!own.ok)
|
|
11964
12255
|
return own;
|
|
11965
12256
|
return checkNoCrossRoleVerdict(postedBody, extractCodeReviewVerdict, "code-review") ?? own;
|
|
@@ -12199,32 +12490,39 @@ function fetchIssueBodyForObjectives(issue) {
|
|
|
12199
12490
|
function resolveObjectivesForPr(pr) {
|
|
12200
12491
|
const prBody = fetchPrBody(pr);
|
|
12201
12492
|
const { issue } = extractIssue(prBody);
|
|
12202
|
-
|
|
12493
|
+
const source = resolveObjectivesSource(prBody, issue, OBJECTIVES_SINCE_ISSUE);
|
|
12494
|
+
if (source.kind === "none")
|
|
12203
12495
|
return { kind: "skip" };
|
|
12204
|
-
if (
|
|
12496
|
+
if (source.kind === "issue") {
|
|
12205
12497
|
let issueBody;
|
|
12206
12498
|
try {
|
|
12207
|
-
issueBody = fetchIssueBodyForObjectives(issue);
|
|
12499
|
+
issueBody = fetchIssueBodyForObjectives(source.issue);
|
|
12208
12500
|
} catch (err) {
|
|
12209
12501
|
if (isIssueNotFoundError(err)) {
|
|
12210
|
-
refuseCmd(`Issue #${issue} does not resolve via \`gh issue view\` — no objectives to judge against.`, "Confirm the Issue exists, or fix `Closes #N` in the PR body, then re-run.");
|
|
12502
|
+
refuseCmd(`Issue #${source.issue} does not resolve via \`gh issue view\` — no objectives to judge against.`, "Confirm the Issue exists, or fix `Closes #N` in the PR body, then re-run.");
|
|
12211
12503
|
}
|
|
12212
|
-
refuseCmd(`Could not fetch Issue #${issue}'s body via \`gh issue view\` to resolve its objectives — no objectives to judge against: ${err instanceof Error ? err.message : String(err)}`, "Confirm `gh auth status` passes, then re-run.");
|
|
12504
|
+
refuseCmd(`Could not fetch Issue #${source.issue}'s body via \`gh issue view\` to resolve its objectives — no objectives to judge against: ${err instanceof Error ? err.message : String(err)}`, "Confirm `gh auth status` passes, then re-run.");
|
|
12213
12505
|
}
|
|
12214
|
-
const
|
|
12215
|
-
if (!
|
|
12216
|
-
refuseCmd(`Issue #${issue}'s \`## Objectives\` section does not parse (${
|
|
12506
|
+
const parsed2 = objectivesOf(issueBody);
|
|
12507
|
+
if (!parsed2.ok) {
|
|
12508
|
+
refuseCmd(`Issue #${source.issue}'s \`## Objectives\` section does not parse (${parsed2.errors.join("; ")}) — no objectives to judge against.`, "Fix the Issue body, then re-run.");
|
|
12217
12509
|
}
|
|
12218
|
-
return { kind: "list", objectives:
|
|
12510
|
+
return { kind: "list", objectives: parsed2.objectives, version: objectivesVersion(parsed2.objectives) };
|
|
12219
12511
|
}
|
|
12220
|
-
|
|
12221
|
-
|
|
12222
|
-
|
|
12223
|
-
|
|
12224
|
-
|
|
12225
|
-
|
|
12512
|
+
const parsed = objectivesOf(prBody);
|
|
12513
|
+
if (!parsed.ok) {
|
|
12514
|
+
refuseCmd(`This PR body's own \`## Objectives\` section does not parse (${parsed.errors.join("; ")}) — no objectives to judge against.`, "Fix the PR body's Objectives section, then re-run.");
|
|
12515
|
+
}
|
|
12516
|
+
return { kind: "list", objectives: parsed.objectives, version: objectivesVersion(parsed.objectives) };
|
|
12517
|
+
}
|
|
12518
|
+
function resolveRulingOrdinalForPr(pr) {
|
|
12519
|
+
let comments;
|
|
12520
|
+
try {
|
|
12521
|
+
comments = fetchComments(pr);
|
|
12522
|
+
} catch (err) {
|
|
12523
|
+
refuseCmd(`Could not fetch PR ${pr}'s comments via \`gh pr view --json comments\` to resolve the newest ruling ordinal: ${err instanceof Error ? err.message : String(err)}`, "Confirm `gh auth status` passes, then re-run.");
|
|
12226
12524
|
}
|
|
12227
|
-
|
|
12525
|
+
return newestPrincipalRulingOrdinal(comments, resolvePrincipalAllowlist(loadTrustAnchorConfig()));
|
|
12228
12526
|
}
|
|
12229
12527
|
function readObjectivesFile(path) {
|
|
12230
12528
|
if (path.trim() === "") {
|
|
@@ -12248,7 +12546,7 @@ function readObjectivesFile(path) {
|
|
|
12248
12546
|
function resolveObjectiveResultsForCommand(resolution, objectivesFileRaw) {
|
|
12249
12547
|
if (resolution.kind === "skip") {
|
|
12250
12548
|
if (objectivesFileRaw !== undefined) {
|
|
12251
|
-
refuseCmd("`--objectives-file` was given, but no objectives to judge against exist for this PR (its Issue predates the objectives cutover).", "Drop `--objectives-file` for this PR,
|
|
12549
|
+
refuseCmd("`--objectives-file` was given, but no objectives to judge against exist for this PR (either its Issue predates the objectives cutover, or it closes no Issue and its body carries no `## Objectives` section).", "Drop `--objectives-file` for this PR, judge against a post-cutover Issue, or add a `## Objectives` section to the PR body.");
|
|
12252
12550
|
}
|
|
12253
12551
|
return { objectivesVersion: null, objectiveResults: null };
|
|
12254
12552
|
}
|
|
@@ -12437,6 +12735,7 @@ async function reviewPostCommand(args) {
|
|
|
12437
12735
|
const headSha2 = resolveHeadSha(pr);
|
|
12438
12736
|
const escalationObjectivesResolution = resolveObjectivesForPr(pr);
|
|
12439
12737
|
const escalationObjectivesVersion = escalationObjectivesResolution.kind === "list" ? escalationObjectivesResolution.version : null;
|
|
12738
|
+
const escalationRulingOrdinal = resolveRulingOrdinalForPr(pr);
|
|
12440
12739
|
const body2 = renderEscalationComment({
|
|
12441
12740
|
...tokens,
|
|
12442
12741
|
headSha: headSha2,
|
|
@@ -12444,7 +12743,8 @@ async function reviewPostCommand(args) {
|
|
|
12444
12743
|
summary,
|
|
12445
12744
|
role: tokensRole,
|
|
12446
12745
|
roleLabel,
|
|
12447
|
-
objectivesVersion: escalationObjectivesVersion
|
|
12746
|
+
objectivesVersion: escalationObjectivesVersion,
|
|
12747
|
+
rulingOrdinal: escalationRulingOrdinal
|
|
12448
12748
|
});
|
|
12449
12749
|
checkRenderedCommentOrRefuse(body2, { kind: "escalation" });
|
|
12450
12750
|
if (printOnly) {
|
|
@@ -12502,6 +12802,7 @@ Self-verification: clean — no verdict extracted.
|
|
|
12502
12802
|
const objectivesResolution2 = resolveObjectivesForPr(pr);
|
|
12503
12803
|
const { objectivesVersion: resolvedObjectivesVersion2, objectiveResults: objectiveResults2 } = resolveObjectiveResultsForCommand(objectivesResolution2, flags.get("--objectives-file"));
|
|
12504
12804
|
refuseIfCleanVerdictHasNotMetObjective(isCleanVerdict(verdict2), "APPROVE", objectiveResults2);
|
|
12805
|
+
const resolvedRulingOrdinal2 = resolveRulingOrdinalForPr(pr);
|
|
12505
12806
|
let comments2;
|
|
12506
12807
|
try {
|
|
12507
12808
|
comments2 = fetchComments(pr);
|
|
@@ -12521,7 +12822,8 @@ Self-verification: clean — no verdict extracted.
|
|
|
12521
12822
|
tests,
|
|
12522
12823
|
docs,
|
|
12523
12824
|
objectivesVersion: resolvedObjectivesVersion2,
|
|
12524
|
-
objectiveResults: objectiveResults2
|
|
12825
|
+
objectiveResults: objectiveResults2,
|
|
12826
|
+
rulingOrdinal: resolvedRulingOrdinal2
|
|
12525
12827
|
};
|
|
12526
12828
|
const body2 = renderCodeReviewComment(input2);
|
|
12527
12829
|
checkRenderedCommentOrRefuse(body2, { kind: "code-review", verdict: verdict2 });
|
|
@@ -12536,7 +12838,7 @@ Self-verification: clean — no verdict extracted.
|
|
|
12536
12838
|
} catch (err) {
|
|
12537
12839
|
refuseCmd(`Posted the comment (${url2}) but could not re-fetch PR ${pr}'s comments to self-verify: ${err instanceof Error ? err.message : String(err)}`, "Check `gh auth status`/network and manually confirm the posted comment parses cleanly — this command could not verify it.");
|
|
12538
12840
|
}
|
|
12539
|
-
const result2 = verifyPostedCodeReview(postComments2, verdict2, headSha2, principalAllowlist2, body2, resolvedObjectivesVersion2);
|
|
12841
|
+
const result2 = verifyPostedCodeReview(postComments2, verdict2, headSha2, principalAllowlist2, body2, resolvedObjectivesVersion2, resolvedRulingOrdinal2);
|
|
12540
12842
|
if (!result2.ok) {
|
|
12541
12843
|
refuseCmd(`Posted comment ${url2}, but self-verification FAILED on re-parse: ${result2.reason}`, "The posted comment does not re-parse clean through the same extractCodeReviewVerdict/extractSecurityReviewVerdict functions the merge gate calls. Do not treat the post as valid — inspect the comment and this command for drift, fix, and re-run.");
|
|
12542
12844
|
}
|
|
@@ -12579,6 +12881,7 @@ Self-verification: clean — re-parsed VERDICT is bound to head ${headSha2}.
|
|
|
12579
12881
|
const objectivesResolution = resolveObjectivesForPr(pr);
|
|
12580
12882
|
const { objectivesVersion: resolvedObjectivesVersion, objectiveResults } = resolveObjectiveResultsForCommand(objectivesResolution, flags.get("--objectives-file"));
|
|
12581
12883
|
refuseIfCleanVerdictHasNotMetObjective(isCleanVerdict(verdict), "PASS", objectiveResults);
|
|
12884
|
+
const resolvedRulingOrdinal = resolveRulingOrdinalForPr(pr);
|
|
12582
12885
|
let comments;
|
|
12583
12886
|
try {
|
|
12584
12887
|
comments = fetchComments(pr);
|
|
@@ -12595,7 +12898,8 @@ Self-verification: clean — re-parsed VERDICT is bound to head ${headSha2}.
|
|
|
12595
12898
|
secrets,
|
|
12596
12899
|
secretsEvidence,
|
|
12597
12900
|
objectivesVersion: resolvedObjectivesVersion,
|
|
12598
|
-
objectiveResults
|
|
12901
|
+
objectiveResults,
|
|
12902
|
+
rulingOrdinal: resolvedRulingOrdinal
|
|
12599
12903
|
};
|
|
12600
12904
|
const body = renderSecurityComment(input);
|
|
12601
12905
|
checkRenderedCommentOrRefuse(body, { kind: "security", verdict });
|
|
@@ -12610,7 +12914,7 @@ Self-verification: clean — re-parsed VERDICT is bound to head ${headSha2}.
|
|
|
12610
12914
|
} catch (err) {
|
|
12611
12915
|
refuseCmd(`Posted the comment (${url}) but could not re-fetch PR ${pr}'s comments to self-verify: ${err instanceof Error ? err.message : String(err)}`, "Check `gh auth status`/network and manually confirm the posted comment parses cleanly — this command could not verify it.");
|
|
12612
12916
|
}
|
|
12613
|
-
const result = verifyPostedSecurity(postComments, verdict, headSha, principalAllowlist, body, resolvedObjectivesVersion);
|
|
12917
|
+
const result = verifyPostedSecurity(postComments, verdict, headSha, principalAllowlist, body, resolvedObjectivesVersion, resolvedRulingOrdinal);
|
|
12614
12918
|
if (!result.ok) {
|
|
12615
12919
|
refuseCmd(`Posted comment ${url}, but self-verification FAILED on re-parse: ${result.reason}`, "The posted comment does not re-parse clean through the same extractCodeReviewVerdict/extractSecurityReviewVerdict functions the merge gate calls. Do not treat the post as valid — inspect the comment and this command for drift, fix, and re-run.");
|
|
12616
12920
|
}
|
|
@@ -12625,244 +12929,125 @@ Self-verification: clean — re-parsed VERDICT is bound to head ${headSha}.
|
|
|
12625
12929
|
}
|
|
12626
12930
|
}
|
|
12627
12931
|
|
|
12628
|
-
// src/lib/
|
|
12629
|
-
|
|
12630
|
-
import { createHash as createHash7 } from "node:crypto";
|
|
12631
|
-
import { mkdtempSync as mkdtempSync2, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
12632
|
-
import { tmpdir as tmpdir3 } from "node:os";
|
|
12633
|
-
import { join as join17 } from "node:path";
|
|
12634
|
-
var DISPATCH_AGENTS = ["claude", "codex", "gemini"];
|
|
12932
|
+
// src/lib/review-gate-check-name.ts
|
|
12933
|
+
var REVIEW_GATE_CHECK_RUN_NAME = "vinaya review gate";
|
|
12635
12934
|
|
|
12636
|
-
|
|
12637
|
-
}
|
|
12935
|
+
// src/lib/dev-review-loop.ts
|
|
12638
12936
|
function sh3(cmd, args) {
|
|
12639
12937
|
return execFileSync16(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
12640
12938
|
}
|
|
12641
|
-
function
|
|
12642
|
-
return createHash7("sha256").update(`${brief}
|
|
12643
|
-
`).digest("hex");
|
|
12644
|
-
}
|
|
12645
|
-
function fetchIssueComments(n) {
|
|
12939
|
+
function resolveHead(branch) {
|
|
12646
12940
|
let out;
|
|
12647
12941
|
try {
|
|
12648
|
-
out = sh3("
|
|
12942
|
+
out = sh3("git", ["ls-remote", "origin", `refs/heads/${branch}`]);
|
|
12649
12943
|
} catch (err) {
|
|
12650
|
-
throw new
|
|
12944
|
+
throw new Error(`resolveHead: \`git ls-remote origin refs/heads/${branch}\` failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
12651
12945
|
}
|
|
12946
|
+
const sha = out.split(/\s+/)[0] ?? "";
|
|
12947
|
+
if (!sha)
|
|
12948
|
+
throw new Error(`resolveHead: branch \`${branch}\` has no head on \`origin\` (empty ls-remote output).`);
|
|
12949
|
+
return sha;
|
|
12950
|
+
}
|
|
12951
|
+
function fetchMechanicalCheckRuns(headSha) {
|
|
12952
|
+
let out;
|
|
12652
12953
|
try {
|
|
12653
|
-
|
|
12954
|
+
out = sh3("gh", [
|
|
12955
|
+
"api",
|
|
12956
|
+
`repos/{owner}/{repo}/commits/${headSha}/check-runs`,
|
|
12957
|
+
"--paginate",
|
|
12958
|
+
"--jq",
|
|
12959
|
+
".check_runs[] | {id, name, status, conclusion}"
|
|
12960
|
+
]);
|
|
12654
12961
|
} catch {
|
|
12655
|
-
|
|
12962
|
+
return null;
|
|
12656
12963
|
}
|
|
12657
|
-
|
|
12658
|
-
|
|
12659
|
-
|
|
12660
|
-
|
|
12661
|
-
|
|
12662
|
-
|
|
12964
|
+
const runs = out.split(`
|
|
12965
|
+
`).filter((l) => l.trim().length > 0).map((l) => JSON.parse(l));
|
|
12966
|
+
const latestByName = new Map;
|
|
12967
|
+
for (const run2 of runs) {
|
|
12968
|
+
const seen = latestByName.get(run2.name);
|
|
12969
|
+
if (!seen || run2.id > seen.id)
|
|
12970
|
+
latestByName.set(run2.name, run2);
|
|
12663
12971
|
}
|
|
12972
|
+
return Array.from(latestByName.values()).filter((r) => r.name !== REVIEW_GATE_CHECK_RUN_NAME);
|
|
12664
12973
|
}
|
|
12665
|
-
function
|
|
12666
|
-
const
|
|
12667
|
-
|
|
12668
|
-
|
|
12974
|
+
function fetchCiConclusion(headSha) {
|
|
12975
|
+
const latest = fetchMechanicalCheckRuns(headSha);
|
|
12976
|
+
if (latest === null || latest.length === 0)
|
|
12977
|
+
return "pending";
|
|
12978
|
+
if (latest.some((r) => r.status !== "completed"))
|
|
12979
|
+
return "pending";
|
|
12980
|
+
if (latest.every((r) => r.conclusion === "success" || r.conclusion === "neutral" || r.conclusion === "skipped")) {
|
|
12981
|
+
return "green";
|
|
12982
|
+
}
|
|
12983
|
+
return "red";
|
|
12669
12984
|
}
|
|
12670
|
-
function
|
|
12671
|
-
|
|
12672
|
-
|
|
12673
|
-
|
|
12674
|
-
|
|
12675
|
-
return;
|
|
12676
|
-
return resolveClassModel(agent, agentClass) ?? undefined;
|
|
12985
|
+
function fetchFailingCheckNames(headSha) {
|
|
12986
|
+
const latest = fetchMechanicalCheckRuns(headSha);
|
|
12987
|
+
if (latest === null)
|
|
12988
|
+
return [];
|
|
12989
|
+
return latest.filter((r) => r.status === "completed").filter((r) => r.conclusion !== "success" && r.conclusion !== "neutral" && r.conclusion !== "skipped").map((r) => r.name);
|
|
12677
12990
|
}
|
|
12678
|
-
|
|
12679
|
-
|
|
12680
|
-
|
|
12681
|
-
|
|
12682
|
-
return
|
|
12991
|
+
var RULING_MARKER = /^<!-- aeg:principal:ruling:\d+-\d+ -->$/;
|
|
12992
|
+
function contentAfterOneLine(body) {
|
|
12993
|
+
const idx = body.indexOf(`
|
|
12994
|
+
`);
|
|
12995
|
+
return idx === -1 ? "" : body.slice(idx + 1);
|
|
12683
12996
|
}
|
|
12684
|
-
function
|
|
12685
|
-
|
|
12686
|
-
|
|
12997
|
+
function markerComments(raw) {
|
|
12998
|
+
const parsed = JSON.parse(raw);
|
|
12999
|
+
return parsed.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null }));
|
|
12687
13000
|
}
|
|
12688
|
-
|
|
12689
|
-
|
|
12690
|
-
try {
|
|
12691
|
-
const mod = await import(dispatchModulePath);
|
|
12692
|
-
return typeof mod.dispatchRole === "function" ? mod.dispatchRole : null;
|
|
12693
|
-
} catch {
|
|
12694
|
-
return null;
|
|
12695
|
-
}
|
|
12696
|
-
}
|
|
12697
|
-
function printManualDispatchInstruction(tranche, n, agent) {
|
|
12698
|
-
process.stdout.write(`
|
|
12699
|
-
vinaya task dispatch: \`dispatchRole\` is not available yet (apps/cli/src/lib/dispatch.ts has no such export) — the brief above is posted; start the developer yourself:
|
|
12700
|
-
|
|
12701
|
-
` + ` vinaya dispatch developer --agent ${agent} --tranche ${tranche} --task ${n}
|
|
12702
|
-
|
|
12703
|
-
Once \`dispatchRole\` ships, the same \`--agent\` flag on \`task dispatch\` will start it automatically.
|
|
12704
|
-
`);
|
|
12705
|
-
}
|
|
12706
|
-
async function withPromptFile(prompt2, fn) {
|
|
12707
|
-
const dir = mkdtempSync2(join17(tmpdir3(), "vinaya-dispatch-prompt-"));
|
|
12708
|
-
const promptFile = join17(dir, "prompt.md");
|
|
12709
|
-
writeFileSync8(promptFile, prompt2, "utf8");
|
|
12710
|
-
try {
|
|
12711
|
-
return await fn(promptFile);
|
|
12712
|
-
} finally {
|
|
12713
|
-
rmSync5(dir, { recursive: true, force: true });
|
|
12714
|
-
}
|
|
12715
|
-
}
|
|
12716
|
-
function resolveDispatchAuthorization() {
|
|
12717
|
-
const login = currentGhLogin();
|
|
12718
|
-
const allowlist = resolvePrincipalAllowlist(loadTrustAnchorConfig());
|
|
12719
|
-
return { authorized: login !== null && isPrincipal(login, allowlist), login };
|
|
12720
|
-
}
|
|
12721
|
-
var defaultDeps2 = {
|
|
12722
|
-
assembleAndRenderBrief,
|
|
12723
|
-
findExistingV1Comment,
|
|
12724
|
-
postMarkedComment,
|
|
12725
|
-
resolveDispatchRole,
|
|
12726
|
-
resolveDispatchAuthorization,
|
|
12727
|
-
resolveModelForDispatch
|
|
12728
|
-
};
|
|
12729
|
-
async function dispatchTask(input, deps = defaultDeps2) {
|
|
12730
|
-
const { tranche, n, agent, model } = input;
|
|
12731
|
-
{
|
|
12732
|
-
const { authorized, login } = deps.resolveDispatchAuthorization();
|
|
12733
|
-
if (!authorized) {
|
|
12734
|
-
throw new DispatchTaskError(login === null ? "could not resolve the identity `gh` is authenticated as — `task dispatch` is Principal-only and refuses rather than proceeding with an unverified actor." : `\`${login}\` is not on the Principal allowlist — \`task dispatch\` is Principal-only.`);
|
|
12735
|
-
}
|
|
12736
|
-
}
|
|
12737
|
-
const result = await deps.assembleAndRenderBrief(tranche, String(n));
|
|
12738
|
-
if (!result.ok) {
|
|
12739
|
-
throw new DispatchTaskError(`cannot dispatch — brief render refused:
|
|
12740
|
-
${result.missing.map((m) => ` - ${m}`).join(`
|
|
12741
|
-
`)}`);
|
|
12742
|
-
}
|
|
12743
|
-
const issue = result.issue;
|
|
12744
|
-
const existing = deps.findExistingV1Comment(issue);
|
|
12745
|
-
if (existing) {
|
|
12746
|
-
throw new DispatchTaskError(`Task ${n} in tranche \`${tranche}\` is already dispatched — see ${existing.url}`);
|
|
12747
|
-
}
|
|
12748
|
-
let dispatchRole2 = null;
|
|
12749
|
-
let resolvedModel;
|
|
12750
|
-
if (agent) {
|
|
12751
|
-
dispatchRole2 = await deps.resolveDispatchRole();
|
|
12752
|
-
if (dispatchRole2) {
|
|
12753
|
-
resolvedModel = deps.resolveModelForDispatch(agent, issue, model);
|
|
12754
|
-
}
|
|
12755
|
-
}
|
|
12756
|
-
const hash = briefHash(result.brief);
|
|
12757
|
-
const commentBody = `Brief hash: ${hash}
|
|
12758
|
-
${result.brief}`;
|
|
12759
|
-
const url = deps.postMarkedComment("issue", String(issue), AEG_BRIEF_V1_MARKER, commentBody);
|
|
12760
|
-
if (agent) {
|
|
12761
|
-
if (dispatchRole2) {
|
|
12762
|
-
await withPromptFile(result.brief, (promptFile) => dispatchRole2("developer", agent, result.brief, {
|
|
12763
|
-
task: issue,
|
|
12764
|
-
promptFile,
|
|
12765
|
-
model: resolvedModel
|
|
12766
|
-
}));
|
|
12767
|
-
} else {
|
|
12768
|
-
printManualDispatchInstruction(tranche, n, agent);
|
|
12769
|
-
}
|
|
12770
|
-
}
|
|
12771
|
-
return { posted: true, commentUrl: url, brief: result.brief };
|
|
12772
|
-
}
|
|
12773
|
-
|
|
12774
|
-
// src/lib/dev-review-loop.ts
|
|
12775
|
-
function sh4(cmd, args) {
|
|
12776
|
-
return execFileSync17(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
12777
|
-
}
|
|
12778
|
-
function resolveHead(branch) {
|
|
12779
|
-
let out;
|
|
12780
|
-
try {
|
|
12781
|
-
out = sh4("git", ["ls-remote", "origin", `refs/heads/${branch}`]);
|
|
12782
|
-
} catch (err) {
|
|
12783
|
-
throw new Error(`resolveHead: \`git ls-remote origin refs/heads/${branch}\` failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
12784
|
-
}
|
|
12785
|
-
const sha = out.split(/\s+/)[0] ?? "";
|
|
12786
|
-
if (!sha)
|
|
12787
|
-
throw new Error(`resolveHead: branch \`${branch}\` has no head on \`origin\` (empty ls-remote output).`);
|
|
12788
|
-
return sha;
|
|
12789
|
-
}
|
|
12790
|
-
function fetchCiConclusion(headSha) {
|
|
12791
|
-
let out;
|
|
12792
|
-
try {
|
|
12793
|
-
out = sh4("gh", [
|
|
12794
|
-
"api",
|
|
12795
|
-
`repos/{owner}/{repo}/commits/${headSha}/check-runs`,
|
|
12796
|
-
"--paginate",
|
|
12797
|
-
"--jq",
|
|
12798
|
-
".check_runs[] | {id, name, status, conclusion}"
|
|
12799
|
-
]);
|
|
12800
|
-
} catch {
|
|
12801
|
-
return "pending";
|
|
12802
|
-
}
|
|
12803
|
-
const runs = out.split(`
|
|
12804
|
-
`).filter((l) => l.trim().length > 0).map((l) => JSON.parse(l));
|
|
12805
|
-
if (runs.length === 0)
|
|
12806
|
-
return "pending";
|
|
12807
|
-
const latestByName = new Map;
|
|
12808
|
-
for (const run2 of runs) {
|
|
12809
|
-
const seen = latestByName.get(run2.name);
|
|
12810
|
-
if (!seen || run2.id > seen.id)
|
|
12811
|
-
latestByName.set(run2.name, run2);
|
|
12812
|
-
}
|
|
12813
|
-
const latest = Array.from(latestByName.values());
|
|
12814
|
-
if (latest.some((r) => r.status !== "completed"))
|
|
12815
|
-
return "pending";
|
|
12816
|
-
if (latest.every((r) => r.conclusion === "success" || r.conclusion === "neutral" || r.conclusion === "skipped")) {
|
|
12817
|
-
return "green";
|
|
12818
|
-
}
|
|
12819
|
-
return "red";
|
|
12820
|
-
}
|
|
12821
|
-
var RULING_MARKER = /^<!-- aeg:principal:ruling:\d+-\d+ -->$/;
|
|
12822
|
-
function contentAfterOneLine(body) {
|
|
12823
|
-
const idx = body.indexOf(`
|
|
12824
|
-
`);
|
|
12825
|
-
return idx === -1 ? "" : body.slice(idx + 1);
|
|
12826
|
-
}
|
|
12827
|
-
function markerComments(raw) {
|
|
12828
|
-
const parsed = JSON.parse(raw);
|
|
12829
|
-
return parsed.comments.map((c) => ({ body: c.body, author: c.author?.login ?? null }));
|
|
12830
|
-
}
|
|
12831
|
-
function principalAllowlist() {
|
|
12832
|
-
return resolvePrincipalAllowlist(loadTrustAnchorConfig());
|
|
13001
|
+
function principalAllowlist() {
|
|
13002
|
+
return resolvePrincipalAllowlist(loadTrustAnchorConfig());
|
|
12833
13003
|
}
|
|
12834
13004
|
function filterPrincipalRulings(comments, allowlist) {
|
|
12835
13005
|
return comments.filter((c) => isPrincipal(c.author, allowlist)).filter((c) => RULING_MARKER.test(c.body.split(`
|
|
12836
13006
|
`)[0] ?? "")).map((c) => contentAfterOneLine(c.body).trim());
|
|
12837
13007
|
}
|
|
12838
13008
|
function findPrincipalFrozenBrief(comments, allowlist) {
|
|
12839
|
-
return comments
|
|
12840
|
-
`)[0] === AEG_BRIEF_V1_MARKER && isPrincipal(c.author, allowlist)) ?? null;
|
|
13009
|
+
return resolveNewestFrozenBrief(comments, allowlist);
|
|
12841
13010
|
}
|
|
12842
13011
|
function fetchRulings(prNumber) {
|
|
12843
13012
|
let out;
|
|
12844
13013
|
try {
|
|
12845
|
-
out =
|
|
13014
|
+
out = sh3("gh", ["pr", "view", String(prNumber), "--json", "comments"]);
|
|
12846
13015
|
} catch (err) {
|
|
12847
13016
|
throw new Error(`fetchRulings: could not fetch PR #${prNumber}'s comments: ${err instanceof Error ? err.message : String(err)}`);
|
|
12848
13017
|
}
|
|
12849
13018
|
return filterPrincipalRulings(markerComments(out), principalAllowlist());
|
|
12850
13019
|
}
|
|
12851
|
-
function
|
|
13020
|
+
function fetchNewestRulingOrdinal(prNumber) {
|
|
13021
|
+
let out;
|
|
13022
|
+
try {
|
|
13023
|
+
out = sh3("gh", ["pr", "view", String(prNumber), "--json", "comments"]);
|
|
13024
|
+
} catch (err) {
|
|
13025
|
+
throw new Error(`fetchNewestRulingOrdinal: could not fetch PR #${prNumber}'s comments: ${err instanceof Error ? err.message : String(err)}`);
|
|
13026
|
+
}
|
|
13027
|
+
return newestPrincipalRulingOrdinal(markerComments(out), principalAllowlist());
|
|
13028
|
+
}
|
|
13029
|
+
function fetchIssueComments(issueNumber, caller) {
|
|
12852
13030
|
let out;
|
|
12853
13031
|
try {
|
|
12854
|
-
out =
|
|
13032
|
+
out = sh3("gh", ["issue", "view", String(issueNumber), "--json", "comments"]);
|
|
12855
13033
|
} catch (err) {
|
|
12856
|
-
throw new Error(
|
|
13034
|
+
throw new Error(`${caller}: could not fetch Issue #${issueNumber}'s comments: ${err instanceof Error ? err.message : String(err)}`);
|
|
12857
13035
|
}
|
|
12858
|
-
|
|
13036
|
+
return markerComments(out);
|
|
13037
|
+
}
|
|
13038
|
+
function fetchFrozenBrief(issueNumber) {
|
|
13039
|
+
const found = resolveNewestFrozenBrief(fetchIssueComments(issueNumber, "fetchFrozenBrief"), principalAllowlist());
|
|
12859
13040
|
if (!found) {
|
|
12860
|
-
throw new Error(`fetchFrozenBrief: Issue #${issueNumber} carries no principal-authored, frozen \`aeg:brief:
|
|
13041
|
+
throw new Error(`fetchFrozenBrief: Issue #${issueNumber} carries no principal-authored, frozen \`aeg:brief:v<k>\` comment — \`vinaya task brief\` must post the brief before this loop can start.`);
|
|
12861
13042
|
}
|
|
12862
|
-
return
|
|
13043
|
+
return found.content;
|
|
13044
|
+
}
|
|
13045
|
+
var NO_SOURCE_REVISION = "(none — pre-task-4 frozen brief)";
|
|
13046
|
+
function fetchSourceRevision(issueNumber) {
|
|
13047
|
+
return extractSourceRevision(fetchFrozenBrief(issueNumber)) ?? NO_SOURCE_REVISION;
|
|
12863
13048
|
}
|
|
12864
13049
|
function fetchIssueTitle(issueNumber) {
|
|
12865
|
-
const out =
|
|
13050
|
+
const out = sh3("gh", ["issue", "view", String(issueNumber), "--json", "title"]);
|
|
12866
13051
|
return JSON.parse(out).title;
|
|
12867
13052
|
}
|
|
12868
13053
|
function extractObjectivesSection(body) {
|
|
@@ -12876,8 +13061,106 @@ function extractObjectivesSection(body) {
|
|
|
12876
13061
|
return (end === -1 ? rest : rest.slice(0, end)).join(`
|
|
12877
13062
|
`).trim();
|
|
12878
13063
|
}
|
|
12879
|
-
|
|
12880
|
-
|
|
13064
|
+
var OBJECTIVES_EDIT_MARKER_RE = /^<!--\s*aeg:objectives:v(\d+)\s*-->$/;
|
|
13065
|
+
function findLatestPrincipalObjectivesEdit(comments, allowlist) {
|
|
13066
|
+
let best = null;
|
|
13067
|
+
for (const c of comments) {
|
|
13068
|
+
if (!isPrincipal(c.author, allowlist))
|
|
13069
|
+
continue;
|
|
13070
|
+
const m = OBJECTIVES_EDIT_MARKER_RE.exec((c.body.split(`
|
|
13071
|
+
`)[0] ?? "").trim());
|
|
13072
|
+
if (!m)
|
|
13073
|
+
continue;
|
|
13074
|
+
const k = Number.parseInt(m[1], 10);
|
|
13075
|
+
if (best === null || k > best.k)
|
|
13076
|
+
best = { k, comment: c };
|
|
13077
|
+
}
|
|
13078
|
+
return best?.comment ?? null;
|
|
13079
|
+
}
|
|
13080
|
+
function parseObjectiveLines(raw) {
|
|
13081
|
+
const parsed = objectivesOf(["## Objectives", "", raw].join(`
|
|
13082
|
+
`));
|
|
13083
|
+
return parsed.ok ? parsed.objectives : null;
|
|
13084
|
+
}
|
|
13085
|
+
function parseObjectivesEditComment(body) {
|
|
13086
|
+
const lines = body.split(`
|
|
13087
|
+
`);
|
|
13088
|
+
const previousIdx = lines.findIndex((l) => l.trim() === "Previous:");
|
|
13089
|
+
const nowIdx = lines.findIndex((l) => l.trim() === "Now:");
|
|
13090
|
+
const reasonLine = lines.find((l) => l.startsWith("Reason:"));
|
|
13091
|
+
const versionLine = lines.find((l) => l.startsWith("Version:"));
|
|
13092
|
+
if (previousIdx === -1 || nowIdx === -1 || nowIdx < previousIdx || !reasonLine || !versionLine)
|
|
13093
|
+
return null;
|
|
13094
|
+
const previousBlock = lines.slice(previousIdx + 1, nowIdx).join(`
|
|
13095
|
+
`).trim();
|
|
13096
|
+
const nowRest = lines.slice(nowIdx + 1);
|
|
13097
|
+
const nowEnd = nowRest.findIndex((l) => l.trim() === "");
|
|
13098
|
+
const nowBlock = (nowEnd === -1 ? nowRest : nowRest.slice(0, nowEnd)).join(`
|
|
13099
|
+
`).trim();
|
|
13100
|
+
const previous = parseObjectiveLines(previousBlock);
|
|
13101
|
+
const now = parseObjectiveLines(nowBlock);
|
|
13102
|
+
if (!previous || !now)
|
|
13103
|
+
return null;
|
|
13104
|
+
return {
|
|
13105
|
+
previous,
|
|
13106
|
+
now,
|
|
13107
|
+
reason: reasonLine.slice("Reason:".length).trim(),
|
|
13108
|
+
version: versionLine.slice("Version:".length).trim()
|
|
13109
|
+
};
|
|
13110
|
+
}
|
|
13111
|
+
function resolveIssueObjectives(issueNumber) {
|
|
13112
|
+
let out;
|
|
13113
|
+
try {
|
|
13114
|
+
out = sh3("gh", ["issue", "view", String(issueNumber), "--json", "comments"]);
|
|
13115
|
+
} catch (err) {
|
|
13116
|
+
throw new Error(`resolveIssueObjectives: could not fetch Issue #${issueNumber}'s comments: ${err instanceof Error ? err.message : String(err)}`);
|
|
13117
|
+
}
|
|
13118
|
+
const comments = markerComments(out);
|
|
13119
|
+
const allowlist = principalAllowlist();
|
|
13120
|
+
const latestEdit = findLatestPrincipalObjectivesEdit(comments, allowlist);
|
|
13121
|
+
if (latestEdit) {
|
|
13122
|
+
const parsed = parseObjectivesEditComment(latestEdit.body);
|
|
13123
|
+
if (parsed) {
|
|
13124
|
+
return {
|
|
13125
|
+
text: parsed.now.map((o) => `${o.id}. ${o.text}`).join(`
|
|
13126
|
+
`),
|
|
13127
|
+
version: parsed.version,
|
|
13128
|
+
edit: { previous: parsed.previous, now: parsed.now, reason: parsed.reason }
|
|
13129
|
+
};
|
|
13130
|
+
}
|
|
13131
|
+
}
|
|
13132
|
+
const brief = findPrincipalFrozenBrief(comments, allowlist);
|
|
13133
|
+
if (!brief) {
|
|
13134
|
+
throw new Error(`resolveIssueObjectives: Issue #${issueNumber} carries no principal-authored, frozen \`aeg:brief:v1\` comment — \`vinaya task dispatch\` must post the brief before this loop can start.`);
|
|
13135
|
+
}
|
|
13136
|
+
const text = extractObjectivesSection(brief.content);
|
|
13137
|
+
if (text.length === 0)
|
|
13138
|
+
return { text: "", version: null, edit: null };
|
|
13139
|
+
const parsedObjectives = objectivesOf(["## Objectives", "", text].join(`
|
|
13140
|
+
`));
|
|
13141
|
+
return { text, version: parsedObjectives.ok ? objectivesVersion(parsedObjectives.objectives) : null, edit: null };
|
|
13142
|
+
}
|
|
13143
|
+
function describeObjectivesEdit(issueNumber, edit) {
|
|
13144
|
+
const { previous, now, reason } = edit;
|
|
13145
|
+
const base = `vinaya issue objectives edit ${issueNumber}`;
|
|
13146
|
+
const sameThrough = (n) => previous.slice(0, n).every((o, i) => o.id === now[i]?.id && o.text === now[i]?.text);
|
|
13147
|
+
if (now.length === previous.length + 1 && sameThrough(previous.length)) {
|
|
13148
|
+
const added = now[now.length - 1];
|
|
13149
|
+
return `${base} --add "${added.text}" --reason "${reason}"`;
|
|
13150
|
+
}
|
|
13151
|
+
if (now.length === previous.length - 1) {
|
|
13152
|
+
const missing = previous.find((p) => !now.some((n) => n.id === p.id));
|
|
13153
|
+
if (missing && now.every((n, i) => n.id === previous.filter((p) => p.id !== missing.id)[i]?.id)) {
|
|
13154
|
+
return `${base} --drop ${missing.id} --reason "${reason}"`;
|
|
13155
|
+
}
|
|
13156
|
+
}
|
|
13157
|
+
if (now.length === previous.length) {
|
|
13158
|
+
const changed = now.find((n, i) => previous[i]?.id === n.id && previous[i]?.text !== n.text);
|
|
13159
|
+
if (changed && now.every((n, i) => n.id === previous[i]?.id)) {
|
|
13160
|
+
return `${base} --replace ${changed.id} "${changed.text}" --reason "${reason}"`;
|
|
13161
|
+
}
|
|
13162
|
+
}
|
|
13163
|
+
return `${base} — could not reconstruct the exact flags from the edit comment's Previous:/Now: diff; Reason: ${reason}`;
|
|
12881
13164
|
}
|
|
12882
13165
|
var ISSUE_TITLE_SHAPE = /^\[([^\]]+)\]\s+(\d+)\s+[—-]/;
|
|
12883
13166
|
function developerBranchFor(issueNumber, fetchTitle = fetchIssueTitle) {
|
|
@@ -12890,7 +13173,7 @@ function developerBranchFor(issueNumber, fetchTitle = fetchIssueTitle) {
|
|
|
12890
13173
|
}
|
|
12891
13174
|
function findOpenPrForBranch(branch) {
|
|
12892
13175
|
try {
|
|
12893
|
-
const out =
|
|
13176
|
+
const out = sh3("gh", ["pr", "list", "--head", branch, "--state", "open", "--json", "number,headRefName"]);
|
|
12894
13177
|
const list = JSON.parse(out);
|
|
12895
13178
|
const found = list.find((p) => p.headRefName === branch);
|
|
12896
13179
|
return found ? { number: found.number, branch } : null;
|
|
@@ -12919,26 +13202,50 @@ function renderReviewerPrompt(facts) {
|
|
|
12919
13202
|
`) : "(none)",
|
|
12920
13203
|
"",
|
|
12921
13204
|
`HEAD: ${facts.head}`,
|
|
12922
|
-
`CI: ${facts.ciConclusion}
|
|
13205
|
+
`CI: ${facts.ciConclusion}`,
|
|
13206
|
+
`BRIEF REVISION: ${facts.revision}`
|
|
12923
13207
|
].join(`
|
|
12924
13208
|
`);
|
|
12925
13209
|
}
|
|
12926
13210
|
function outboxRoot() {
|
|
12927
|
-
return
|
|
13211
|
+
return join17(GLOBAL_VINAYA_HOME, "outbox");
|
|
12928
13212
|
}
|
|
12929
13213
|
function heldVerdictPath(root, task, round, role) {
|
|
12930
|
-
return
|
|
13214
|
+
return join17(root, "dev-review-loop", String(task), `round-${round}-${role}.md`);
|
|
12931
13215
|
}
|
|
12932
13216
|
function writeHeldVerdict(root, task, round, role, renderedComment) {
|
|
12933
|
-
const dir =
|
|
13217
|
+
const dir = join17(root, "dev-review-loop", String(task));
|
|
12934
13218
|
mkdirSync5(dir, { recursive: true });
|
|
12935
|
-
|
|
13219
|
+
writeFileSync8(heldVerdictPath(root, task, round, role), renderedComment, "utf8");
|
|
13220
|
+
}
|
|
13221
|
+
function reviewerWorkDir(root, task, round, role, attempt = 1) {
|
|
13222
|
+
const suffix = attempt > 1 ? `-retry${attempt - 1}` : "";
|
|
13223
|
+
return join17(root, "dev-review-loop", String(task), `round-${round}-${role}-work${suffix}`);
|
|
12936
13224
|
}
|
|
12937
|
-
function
|
|
12938
|
-
|
|
13225
|
+
function missingReviewerArtifacts(workDir, hasObjectives) {
|
|
13226
|
+
const missing = [];
|
|
13227
|
+
const reportRaw = readIfExists(join17(workDir, "report.txt"));
|
|
13228
|
+
if (reportRaw === null)
|
|
13229
|
+
missing.push("report.txt");
|
|
13230
|
+
if (!existsSync13(join17(workDir, "findings.txt")))
|
|
13231
|
+
missing.push("findings.txt");
|
|
13232
|
+
const isEscalation = reportRaw !== null && parseReport(reportRaw).ESCALATE !== undefined;
|
|
13233
|
+
if (hasObjectives && !isEscalation && !existsSync13(join17(workDir, "objectives.txt")))
|
|
13234
|
+
missing.push("objectives.txt");
|
|
13235
|
+
return missing;
|
|
13236
|
+
}
|
|
13237
|
+
|
|
13238
|
+
class ReviewerInfrastructureFailure extends Error {
|
|
13239
|
+
role;
|
|
13240
|
+
missing;
|
|
13241
|
+
constructor(role, missing) {
|
|
13242
|
+
super(`${role}'s work directory carried no ${missing.join(" and no ")} after a fresh dispatch and one fresh retry.`);
|
|
13243
|
+
this.role = role;
|
|
13244
|
+
this.missing = missing;
|
|
13245
|
+
}
|
|
12939
13246
|
}
|
|
12940
13247
|
function forgeEffectPath(root, task, key) {
|
|
12941
|
-
return
|
|
13248
|
+
return join17(root, "dev-review-loop", String(task), `effect-${key}.json`);
|
|
12942
13249
|
}
|
|
12943
13250
|
function readForgeEffect(path) {
|
|
12944
13251
|
const raw = readIfExists(path);
|
|
@@ -12952,7 +13259,7 @@ function readForgeEffect(path) {
|
|
|
12952
13259
|
}
|
|
12953
13260
|
function writeForgeEffect(path, record) {
|
|
12954
13261
|
mkdirSync5(dirname8(path), { recursive: true });
|
|
12955
|
-
|
|
13262
|
+
writeFileSync8(path, JSON.stringify(record), "utf8");
|
|
12956
13263
|
}
|
|
12957
13264
|
function postForgeEffectOnce(root, task, key, poster) {
|
|
12958
13265
|
const path = forgeEffectPath(root, task, key);
|
|
@@ -12966,20 +13273,20 @@ function postForgeEffectOnce(root, task, key, poster) {
|
|
|
12966
13273
|
return url;
|
|
12967
13274
|
}
|
|
12968
13275
|
function postPrComment(pr, body) {
|
|
12969
|
-
const dir =
|
|
12970
|
-
const tmp =
|
|
12971
|
-
|
|
13276
|
+
const dir = mkdtempSync2(join17(tmpdir3(), "vinaya-dev-review-loop-comment-"));
|
|
13277
|
+
const tmp = join17(dir, "comment.md");
|
|
13278
|
+
writeFileSync8(tmp, body, "utf8");
|
|
12972
13279
|
try {
|
|
12973
|
-
return
|
|
13280
|
+
return execFileSync16("gh", ["pr", "comment", String(pr), "--body-file", tmp], {
|
|
12974
13281
|
encoding: "utf8",
|
|
12975
13282
|
stdio: ["ignore", "pipe", "pipe"]
|
|
12976
13283
|
}).trim();
|
|
12977
13284
|
} finally {
|
|
12978
|
-
|
|
13285
|
+
rmSync5(dir, { recursive: true, force: true });
|
|
12979
13286
|
}
|
|
12980
13287
|
}
|
|
12981
13288
|
function fetchAllPrCommentBodies(pr) {
|
|
12982
|
-
const out =
|
|
13289
|
+
const out = sh3("gh", ["pr", "view", String(pr), "--json", "comments"]);
|
|
12983
13290
|
return principalBodies(markerComments(out), principalAllowlist());
|
|
12984
13291
|
}
|
|
12985
13292
|
function publishRound(root, input) {
|
|
@@ -13010,9 +13317,9 @@ function publishRound(root, input) {
|
|
|
13010
13317
|
function pauseMarker(reason) {
|
|
13011
13318
|
return `<!-- aeg:loop:paused:${reason} -->`;
|
|
13012
13319
|
}
|
|
13013
|
-
function renderPauseComment(prNumber, reason) {
|
|
13320
|
+
function renderPauseComment(prNumber, reason, detail) {
|
|
13014
13321
|
return [
|
|
13015
|
-
`The dev-review-loop paused: ${reason}.`,
|
|
13322
|
+
`The dev-review-loop paused: ${reason}${detail ? ` — ${detail}` : ""}.`,
|
|
13016
13323
|
"",
|
|
13017
13324
|
"A Principal ruling is needed before this can continue. Once one is posted on this PR, resume with:",
|
|
13018
13325
|
"",
|
|
@@ -13022,16 +13329,16 @@ function renderPauseComment(prNumber, reason) {
|
|
|
13022
13329
|
].join(`
|
|
13023
13330
|
`);
|
|
13024
13331
|
}
|
|
13025
|
-
function postPauseComment(root, task, round, head, prNumber, reason) {
|
|
13026
|
-
postForgeEffectOnce(root, task, `pause-${round}-${head}`, () => postMarkedComment("pr", String(prNumber), pauseMarker(reason), renderPauseComment(prNumber, reason)));
|
|
13332
|
+
function postPauseComment(root, task, round, head, prNumber, reason, detail) {
|
|
13333
|
+
postForgeEffectOnce(root, task, `pause-${round}-${head}`, () => postMarkedComment("pr", String(prNumber), pauseMarker(reason), renderPauseComment(prNumber, reason, detail)));
|
|
13027
13334
|
}
|
|
13028
13335
|
function pauseStatePath(root, task) {
|
|
13029
|
-
return
|
|
13336
|
+
return join17(root, "dev-review-loop", String(task), "pause-state.json");
|
|
13030
13337
|
}
|
|
13031
13338
|
function writePauseState(root, state) {
|
|
13032
13339
|
const path = pauseStatePath(root, state.task);
|
|
13033
13340
|
mkdirSync5(dirname8(path), { recursive: true });
|
|
13034
|
-
|
|
13341
|
+
writeFileSync8(path, JSON.stringify(state), "utf8");
|
|
13035
13342
|
}
|
|
13036
13343
|
function readPauseState(root, task) {
|
|
13037
13344
|
const raw = readIfExists(pauseStatePath(root, task));
|
|
@@ -13077,41 +13384,41 @@ function readIfExists(path) {
|
|
|
13077
13384
|
return null;
|
|
13078
13385
|
}
|
|
13079
13386
|
}
|
|
13080
|
-
function
|
|
13081
|
-
const dir =
|
|
13082
|
-
const promptFile =
|
|
13083
|
-
|
|
13387
|
+
function withPromptFile(prompt2, fn) {
|
|
13388
|
+
const dir = mkdtempSync2(join17(tmpdir3(), "vinaya-dev-review-loop-prompt-"));
|
|
13389
|
+
const promptFile = join17(dir, "prompt.md");
|
|
13390
|
+
writeFileSync8(promptFile, prompt2, "utf8");
|
|
13084
13391
|
try {
|
|
13085
13392
|
return fn(promptFile);
|
|
13086
13393
|
} finally {
|
|
13087
|
-
|
|
13394
|
+
rmSync5(dir, { recursive: true, force: true });
|
|
13088
13395
|
}
|
|
13089
13396
|
}
|
|
13090
13397
|
|
|
13091
13398
|
class DevReviewLoopResumeError extends Error {
|
|
13092
13399
|
}
|
|
13093
13400
|
function defaultRepoRoot() {
|
|
13094
|
-
return
|
|
13401
|
+
return sh3("git", ["rev-parse", "--show-toplevel"]);
|
|
13095
13402
|
}
|
|
13096
13403
|
function defaultGitRevParseOriginMain() {
|
|
13097
|
-
return
|
|
13404
|
+
return sh3("git", ["rev-parse", "origin/main"]);
|
|
13098
13405
|
}
|
|
13099
13406
|
function defaultGitFetch(sha) {
|
|
13100
13407
|
try {
|
|
13101
|
-
|
|
13408
|
+
execFileSync16("git", ["fetch", "--quiet", "origin", sha], { stdio: ["ignore", "ignore", "ignore"] });
|
|
13102
13409
|
} catch {}
|
|
13103
13410
|
}
|
|
13104
13411
|
function defaultGitDiffShortstat(base, head) {
|
|
13105
13412
|
try {
|
|
13106
|
-
return
|
|
13413
|
+
return sh3("git", ["diff", `${base}...${head}`, "--shortstat"]);
|
|
13107
13414
|
} catch {
|
|
13108
13415
|
return "";
|
|
13109
13416
|
}
|
|
13110
13417
|
}
|
|
13111
13418
|
function defaultFlushOutbox(task) {
|
|
13112
13419
|
try {
|
|
13113
|
-
const cliEntry =
|
|
13114
|
-
|
|
13420
|
+
const cliEntry = join17(packageRoot(import.meta.url), "src", "index.ts");
|
|
13421
|
+
execFileSync16("bun", [cliEntry, "log", "flush", "--issue", String(task)], {
|
|
13115
13422
|
stdio: ["ignore", "pipe", "pipe"]
|
|
13116
13423
|
});
|
|
13117
13424
|
} catch (err) {
|
|
@@ -13122,16 +13429,27 @@ function defaultFlushOutbox(task) {
|
|
|
13122
13429
|
function defaultSleep2(ms) {
|
|
13123
13430
|
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
13124
13431
|
}
|
|
13125
|
-
function
|
|
13432
|
+
function gatePollEnvOverride(name, fallback) {
|
|
13433
|
+
const raw = process.env[name];
|
|
13434
|
+
if (!raw)
|
|
13435
|
+
return fallback;
|
|
13436
|
+
const n = Number(raw);
|
|
13437
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
13438
|
+
}
|
|
13439
|
+
function defaultDeps2() {
|
|
13126
13440
|
return {
|
|
13127
13441
|
dispatchRole,
|
|
13128
13442
|
resolveHead,
|
|
13129
13443
|
fetchCiConclusion,
|
|
13444
|
+
fetchFailingCheckNames,
|
|
13130
13445
|
fetchRulings,
|
|
13446
|
+
fetchNewestRulingOrdinal,
|
|
13131
13447
|
fetchFrozenBrief,
|
|
13132
|
-
|
|
13448
|
+
resolveIssueObjectives,
|
|
13449
|
+
fetchSourceRevision,
|
|
13133
13450
|
developerBranchFor: (n) => developerBranchFor(n),
|
|
13134
13451
|
findOpenPrForBranch,
|
|
13452
|
+
readResumeRecord: (task, agent, repo) => readResumeRecord("developer", agent, repo, task),
|
|
13135
13453
|
outboxRoot,
|
|
13136
13454
|
repoRoot: defaultRepoRoot,
|
|
13137
13455
|
gitRevParseOriginMain: defaultGitRevParseOriginMain,
|
|
@@ -13142,8 +13460,8 @@ function defaultDeps3() {
|
|
|
13142
13460
|
now: () => Date.now(),
|
|
13143
13461
|
prPollMaxAttempts: 120,
|
|
13144
13462
|
prPollIntervalMs: 15000,
|
|
13145
|
-
gatePollMaxAttempts: 120,
|
|
13146
|
-
gatePollIntervalMs: 15000
|
|
13463
|
+
gatePollMaxAttempts: gatePollEnvOverride("VINAYA_DEV_REVIEW_LOOP_GATE_POLL_MAX_ATTEMPTS", 120),
|
|
13464
|
+
gatePollIntervalMs: gatePollEnvOverride("VINAYA_DEV_REVIEW_LOOP_GATE_POLL_INTERVAL_MS", 15000)
|
|
13147
13465
|
};
|
|
13148
13466
|
}
|
|
13149
13467
|
function parseShortstat(stat) {
|
|
@@ -13219,8 +13537,8 @@ async function waitForOwnLoopLine(path, priorSize, runId, event, sleep, timeoutM
|
|
|
13219
13537
|
await sleep(5);
|
|
13220
13538
|
}
|
|
13221
13539
|
}
|
|
13222
|
-
function buildVerdictFromReport(role, workDir, headSha, agent, taskId, handle) {
|
|
13223
|
-
const reportRaw = readIfExists(
|
|
13540
|
+
function buildVerdictFromReport(role, workDir, headSha, agent, taskId, handle, objectivesVersionAtDispatch, rulingOrdinalAtDispatch) {
|
|
13541
|
+
const reportRaw = readIfExists(join17(workDir, "report.txt")) ?? "";
|
|
13224
13542
|
const report = parseReport(reportRaw);
|
|
13225
13543
|
const sessionId = handle.resumeId ?? "(unknown)";
|
|
13226
13544
|
const tokensIn = handle.usage ? String(handle.usage.input) : "—";
|
|
@@ -13237,7 +13555,8 @@ function buildVerdictFromReport(role, workDir, headSha, agent, taskId, handle) {
|
|
|
13237
13555
|
summary: report.SUMMARY ?? "(no summary given)",
|
|
13238
13556
|
role: role === "reviewer" ? "review" : "security",
|
|
13239
13557
|
roleLabel,
|
|
13240
|
-
objectivesVersion:
|
|
13558
|
+
objectivesVersion: objectivesVersionAtDispatch,
|
|
13559
|
+
rulingOrdinal: rulingOrdinalAtDispatch,
|
|
13241
13560
|
taskId: String(taskId),
|
|
13242
13561
|
model: agent,
|
|
13243
13562
|
tokensIn,
|
|
@@ -13247,12 +13566,13 @@ function buildVerdictFromReport(role, workDir, headSha, agent, taskId, handle) {
|
|
|
13247
13566
|
});
|
|
13248
13567
|
return { observation: { role, verdict: "ESCALATE", objectives: [], findings: [] }, rendered: rendered2 };
|
|
13249
13568
|
}
|
|
13250
|
-
const findingsRaw = readIfExists(
|
|
13569
|
+
const findingsRaw = readIfExists(join17(workDir, "findings.txt")) ?? "";
|
|
13251
13570
|
const allowedSeverities = role === "reviewer" ? ["BLOCKER", "MAJOR", "MINOR"] : ["CRITICAL", "HIGH", "MEDIUM", "LOW"];
|
|
13252
13571
|
const findings = findingsRaw.trim() ? parseFindingsFile(findingsRaw, allowedSeverities) : [];
|
|
13253
|
-
const objectivesRaw = readIfExists(
|
|
13572
|
+
const objectivesRaw = readIfExists(join17(workDir, "objectives.txt"));
|
|
13254
13573
|
const objectiveResults = objectivesRaw?.trim() ? parseObjectivesFile(objectivesRaw) : [];
|
|
13255
13574
|
const objectives = objectiveResults.map((o) => ({ id: o.id, met: o.status === "MET" }));
|
|
13575
|
+
const renderedObjectiveResults = objectivesVersionAtDispatch !== null ? objectiveResults : null;
|
|
13256
13576
|
const findingObservations = findings.map((f, i) => ({ id: `F${i + 1}`, severity: f.severity, state: null }));
|
|
13257
13577
|
if (role === "reviewer") {
|
|
13258
13578
|
const verdict2 = deriveCodeReviewVerdict(findings);
|
|
@@ -13266,8 +13586,9 @@ function buildVerdictFromReport(role, workDir, headSha, agent, taskId, handle) {
|
|
|
13266
13586
|
scopeEvidence: null,
|
|
13267
13587
|
tests: report.TESTS ?? "(not reported)",
|
|
13268
13588
|
docs: report.DOCS ?? "(not reported)",
|
|
13269
|
-
objectivesVersion:
|
|
13270
|
-
objectiveResults:
|
|
13589
|
+
objectivesVersion: objectivesVersionAtDispatch,
|
|
13590
|
+
objectiveResults: renderedObjectiveResults,
|
|
13591
|
+
rulingOrdinal: rulingOrdinalAtDispatch,
|
|
13271
13592
|
taskId: String(taskId),
|
|
13272
13593
|
model: agent,
|
|
13273
13594
|
tokensIn,
|
|
@@ -13293,8 +13614,9 @@ function buildVerdictFromReport(role, workDir, headSha, agent, taskId, handle) {
|
|
|
13293
13614
|
configScan: report.CONFIG_SCAN ?? "(not reported)",
|
|
13294
13615
|
secrets: report.SECRETS ?? "none found",
|
|
13295
13616
|
secretsEvidence: null,
|
|
13296
|
-
objectivesVersion:
|
|
13297
|
-
objectiveResults:
|
|
13617
|
+
objectivesVersion: objectivesVersionAtDispatch,
|
|
13618
|
+
objectiveResults: renderedObjectiveResults,
|
|
13619
|
+
rulingOrdinal: rulingOrdinalAtDispatch,
|
|
13298
13620
|
taskId: String(taskId),
|
|
13299
13621
|
model: agent,
|
|
13300
13622
|
tokensIn,
|
|
@@ -13304,6 +13626,9 @@ function buildVerdictFromReport(role, workDir, headSha, agent, taskId, handle) {
|
|
|
13304
13626
|
});
|
|
13305
13627
|
return { observation: { role, verdict, objectives, findings: findingObservations }, rendered };
|
|
13306
13628
|
}
|
|
13629
|
+
function hasObjectivesFacts(facts) {
|
|
13630
|
+
return facts.objectives.trim().length > 0;
|
|
13631
|
+
}
|
|
13307
13632
|
function renderReviewerDispatchPrompt(role, facts, workDir) {
|
|
13308
13633
|
const base = renderReviewerPrompt(facts);
|
|
13309
13634
|
const lint = lintReviewerPrompt(base);
|
|
@@ -13314,9 +13639,12 @@ function renderReviewerDispatchPrompt(role, facts, workDir) {
|
|
|
13314
13639
|
const instructions = [
|
|
13315
13640
|
roleLine,
|
|
13316
13641
|
"Review the PR at the HEAD above against the OBJECTIVES and RULINGS above.",
|
|
13317
|
-
`Write your findings to ${
|
|
13642
|
+
`Write your findings to ${join17(workDir, "findings.txt")}, one per line: SEVERITY|file:line|description`,
|
|
13318
13643
|
role === "reviewer" ? "(severities: BLOCKER, MAJOR, MINOR — leave the file empty if there are none)." : "(severities: CRITICAL, HIGH, MEDIUM, LOW — leave the file empty if there are none).",
|
|
13319
|
-
|
|
13644
|
+
...hasObjectivesFacts(facts) ? [
|
|
13645
|
+
`Write one line per objective listed above to ${join17(workDir, "objectives.txt")}: O<n>|MET|<evidence> or O<n>|NOT MET|<evidence>.`
|
|
13646
|
+
] : [],
|
|
13647
|
+
`Write a short report to ${join17(workDir, "report.txt")} as one \`KEY: value\` line per field:`,
|
|
13320
13648
|
role === "reviewer" ? " BRIEF_CONFORMANCE, SPEC_CONFORMANCE, SCOPE, TESTS, DOCS" : " CONFIG_SCAN, SECRETS",
|
|
13321
13649
|
"To escalate instead of casting a verdict, write only `ESCALATE: authority|strategy|product` and `SUMMARY: <text>` to report.txt."
|
|
13322
13650
|
].join(`
|
|
@@ -13330,7 +13658,7 @@ function taskFromPrBody(body) {
|
|
|
13330
13658
|
return m ? Number(m[1]) : null;
|
|
13331
13659
|
}
|
|
13332
13660
|
function fetchPrBody2(pr) {
|
|
13333
|
-
const out =
|
|
13661
|
+
const out = sh3("gh", ["pr", "view", String(pr), "--json", "body"]);
|
|
13334
13662
|
return JSON.parse(out).body;
|
|
13335
13663
|
}
|
|
13336
13664
|
function routeCompletionEvents(events, decisionType) {
|
|
@@ -13341,8 +13669,40 @@ function routeCompletionEvents(events, decisionType) {
|
|
|
13341
13669
|
toDeferUntilPublish: events.filter((e) => e.event === "journal_finalized")
|
|
13342
13670
|
};
|
|
13343
13671
|
}
|
|
13672
|
+
var MAX_GATE_STALLED_TURNS = 2;
|
|
13673
|
+
function driverDecidedPauseEvents(loopId, state, round, stats) {
|
|
13674
|
+
const envelope = { kind: "dev_review_loop", payload: {} };
|
|
13675
|
+
return [
|
|
13676
|
+
{ ...envelope, loop_id: loopId, event: "stop_condition_met", round, condition: "principal_stop" },
|
|
13677
|
+
{ ...envelope, loop_id: loopId, event: "paused", round, reason: "principal_item" },
|
|
13678
|
+
{
|
|
13679
|
+
...envelope,
|
|
13680
|
+
loop_id: loopId,
|
|
13681
|
+
event: "round_ended",
|
|
13682
|
+
round,
|
|
13683
|
+
base_head: stats.baseHead,
|
|
13684
|
+
head: stats.head,
|
|
13685
|
+
files_changed: stats.filesChanged,
|
|
13686
|
+
insertions: stats.insertions,
|
|
13687
|
+
deletions: stats.deletions,
|
|
13688
|
+
wall_ms: stats.wallMs,
|
|
13689
|
+
outcome: "changes_requested"
|
|
13690
|
+
},
|
|
13691
|
+
{
|
|
13692
|
+
...envelope,
|
|
13693
|
+
loop_id: loopId,
|
|
13694
|
+
event: "journal_finalized",
|
|
13695
|
+
rounds: state.rounds.length + 1,
|
|
13696
|
+
total_wall_ms: state.totalWallMs + stats.wallMs,
|
|
13697
|
+
time_to_green_ms: null,
|
|
13698
|
+
files_changed_total: state.totalFilesChanged + stats.filesChanged,
|
|
13699
|
+
final_head: stats.head,
|
|
13700
|
+
result: "stopped"
|
|
13701
|
+
}
|
|
13702
|
+
];
|
|
13703
|
+
}
|
|
13344
13704
|
async function devReviewLoop(input, deps = {}) {
|
|
13345
|
-
const d = { ...
|
|
13705
|
+
const d = { ...defaultDeps2(), ...deps };
|
|
13346
13706
|
const root = d.outboxRoot();
|
|
13347
13707
|
let task;
|
|
13348
13708
|
let branch;
|
|
@@ -13383,7 +13743,7 @@ async function devReviewLoop(input, deps = {}) {
|
|
|
13383
13743
|
process.env.VINAYA_TASK = String(task);
|
|
13384
13744
|
const repo = await resolveRepo().catch(() => null);
|
|
13385
13745
|
const repoRoot2 = d.repoRoot();
|
|
13386
|
-
const confidenceFilePath =
|
|
13746
|
+
const confidenceFilePath = join17(repoRoot2, ".worktrees", branch, CONFIDENCE_FILE_NAME);
|
|
13387
13747
|
const loopOutboxPath = outboxPathFor({ outboxRoot: () => root }, repo, task);
|
|
13388
13748
|
async function logEvents(events) {
|
|
13389
13749
|
for (const e of events) {
|
|
@@ -13404,9 +13764,12 @@ async function devReviewLoop(input, deps = {}) {
|
|
|
13404
13764
|
let devDispatchSucceededBefore = false;
|
|
13405
13765
|
let lastReviewContext = null;
|
|
13406
13766
|
let resumedDispatch = resumeFrom !== null;
|
|
13767
|
+
let lastFailingChecks = [];
|
|
13768
|
+
let pendingGateRedRetry = false;
|
|
13769
|
+
let gateStalledStreak = 0;
|
|
13407
13770
|
async function dispatchDeveloper(prompt2, roundNum) {
|
|
13408
13771
|
const isResume = devResumeId !== null;
|
|
13409
|
-
const handle = await
|
|
13772
|
+
const handle = await withPromptFile(prompt2, (promptFile) => d.dispatchRole("developer", input.agent, prompt2, {
|
|
13410
13773
|
task,
|
|
13411
13774
|
round: roundNum,
|
|
13412
13775
|
resumeId: devResumeId ?? undefined,
|
|
@@ -13421,15 +13784,23 @@ async function devReviewLoop(input, deps = {}) {
|
|
|
13421
13784
|
return handle;
|
|
13422
13785
|
}
|
|
13423
13786
|
async function dispatchReviewer(role, roundNum, facts) {
|
|
13424
|
-
const
|
|
13425
|
-
mkdirSync5(workDir, { recursive: true });
|
|
13787
|
+
const hasObjectives = hasObjectivesFacts(facts);
|
|
13426
13788
|
const dispatchRoleName = role === "reviewer" ? "code-reviewer" : "security";
|
|
13427
|
-
|
|
13428
|
-
|
|
13429
|
-
|
|
13430
|
-
|
|
13431
|
-
|
|
13432
|
-
|
|
13789
|
+
let lastMissing = [];
|
|
13790
|
+
for (let attempt = 1;attempt <= 2; attempt++) {
|
|
13791
|
+
const workDir = reviewerWorkDir(root, task, roundNum, role, attempt);
|
|
13792
|
+
mkdirSync5(workDir, { recursive: true });
|
|
13793
|
+
const prompt2 = renderReviewerDispatchPrompt(role, facts, workDir);
|
|
13794
|
+
const handle = await withPromptFile(prompt2, (promptFile) => d.dispatchRole(dispatchRoleName, input.agent, prompt2, { task, round: roundNum, promptFile }));
|
|
13795
|
+
await assertDispatchOrEscalate(handle, input.agent, false, false);
|
|
13796
|
+
const missing = missingReviewerArtifacts(workDir, hasObjectives);
|
|
13797
|
+
if (missing.length > 0) {
|
|
13798
|
+
lastMissing = missing;
|
|
13799
|
+
continue;
|
|
13800
|
+
}
|
|
13801
|
+
return buildVerdictFromReport(role, workDir, facts.head, input.agent, task, handle, facts.objectivesVersion, facts.rulingOrdinal);
|
|
13802
|
+
}
|
|
13803
|
+
throw new ReviewerInfrastructureFailure(role, lastMissing);
|
|
13433
13804
|
}
|
|
13434
13805
|
function computeStats(head, roundStartMs2) {
|
|
13435
13806
|
const baseHead = d.gitRevParseOriginMain();
|
|
@@ -13443,7 +13814,13 @@ async function devReviewLoop(input, deps = {}) {
|
|
|
13443
13814
|
const c = d.fetchCiConclusion(head);
|
|
13444
13815
|
return c === "pending" ? null : c;
|
|
13445
13816
|
}, d.gatePollMaxAttempts, d.gatePollIntervalMs, d.sleep, `devReviewLoop: CI never resolved off 'pending' for head ${head} within the poll budget.`).catch(() => "red");
|
|
13446
|
-
|
|
13817
|
+
const failingChecks = conclusion === "red" ? d.fetchFailingCheckNames(head) : [];
|
|
13818
|
+
return {
|
|
13819
|
+
green: conclusion === "green",
|
|
13820
|
+
stats: computeStats(head, roundStartMs2),
|
|
13821
|
+
ciConclusion: conclusion,
|
|
13822
|
+
failingChecks
|
|
13823
|
+
};
|
|
13447
13824
|
}
|
|
13448
13825
|
function readAndClearConfidence() {
|
|
13449
13826
|
const content = readIfExists(confidenceFilePath);
|
|
@@ -13458,35 +13835,88 @@ async function devReviewLoop(input, deps = {}) {
|
|
|
13458
13835
|
lastReviewContext = rulings.map((r, i) => `${i + 1}. ${r}`).join(`
|
|
13459
13836
|
`);
|
|
13460
13837
|
} else {
|
|
13461
|
-
const
|
|
13462
|
-
|
|
13463
|
-
|
|
13838
|
+
const existingPr = d.findOpenPrForBranch(branch);
|
|
13839
|
+
if (existingPr) {
|
|
13840
|
+
prNumber = existingPr.number;
|
|
13841
|
+
const rec = d.readResumeRecord(task, input.agent, repo);
|
|
13842
|
+
if (rec)
|
|
13843
|
+
devResumeId = rec.resumeId;
|
|
13844
|
+
} else {
|
|
13845
|
+
let branchExists2 = true;
|
|
13846
|
+
try {
|
|
13847
|
+
d.resolveHead(branch);
|
|
13848
|
+
} catch {
|
|
13849
|
+
branchExists2 = false;
|
|
13850
|
+
}
|
|
13851
|
+
if (branchExists2) {
|
|
13852
|
+
const rec = d.readResumeRecord(task, input.agent, repo);
|
|
13853
|
+
if (rec)
|
|
13854
|
+
devResumeId = rec.resumeId;
|
|
13855
|
+
const openPrPrompt = [
|
|
13856
|
+
"This branch already exists with no open pull request for it.",
|
|
13857
|
+
"Open the pull request through the validated path per aeg-root/roles/developer.md:",
|
|
13858
|
+
'`bun apps/cli/src/index.ts pr create --body-file <path> --title "<title>"`.'
|
|
13859
|
+
].join(`
|
|
13860
|
+
|
|
13861
|
+
`);
|
|
13862
|
+
await dispatchDeveloper(openPrPrompt, round);
|
|
13863
|
+
prNumber = await pollUntil(() => d.findOpenPrForBranch(branch), d.prPollMaxAttempts, d.prPollIntervalMs, d.sleep, `devReviewLoop: no open PR appeared for branch \`${branch}\` within the poll budget after resuming to open one.`).then((pr) => pr.number);
|
|
13864
|
+
} else {
|
|
13865
|
+
const brief = d.fetchFrozenBrief(task);
|
|
13866
|
+
await dispatchDeveloper(brief, round);
|
|
13867
|
+
prNumber = await pollUntil(() => d.findOpenPrForBranch(branch), d.prPollMaxAttempts, d.prPollIntervalMs, d.sleep, `devReviewLoop: no open PR appeared for branch \`${branch}\` within the poll budget.`).then((pr) => pr.number);
|
|
13868
|
+
}
|
|
13869
|
+
}
|
|
13464
13870
|
}
|
|
13465
13871
|
let decision = { type: "dispatch_developer" };
|
|
13466
13872
|
let firstPass = !resumeFrom;
|
|
13467
13873
|
let pendingCompletionEvents = [];
|
|
13468
13874
|
while (true) {
|
|
13469
13875
|
if (decision.type === "dispatch_developer") {
|
|
13876
|
+
const isGateRedRetry = pendingGateRedRetry;
|
|
13470
13877
|
if (!firstPass) {
|
|
13471
13878
|
const prompt2 = [
|
|
13472
13879
|
resumedDispatch ? `Principal ruling on this pause:
|
|
13473
13880
|
|
|
13474
13881
|
${lastReviewContext}
|
|
13475
|
-
` :
|
|
13882
|
+
` : isGateRedRetry ? `CI is red on the last head. Failing check-run(s): ${lastFailingChecks.length > 0 ? lastFailingChecks.join(", ") : "(unknown)"}. Fix and push.` : `Round ${round} review findings:
|
|
13476
13883
|
|
|
13477
13884
|
${lastReviewContext}
|
|
13478
|
-
|
|
13885
|
+
`,
|
|
13479
13886
|
"Address the findings above per aeg-root/roles/developer.md. Push fixes as new commits on the SAME branch; do not open a new PR.",
|
|
13480
13887
|
round >= 2 ? CONFIDENCE_PROMPT_LINE : ""
|
|
13481
13888
|
].filter(Boolean).join(`
|
|
13482
13889
|
|
|
13483
13890
|
`);
|
|
13891
|
+
const headBeforeDispatch = isGateRedRetry ? d.resolveHead(branch) : null;
|
|
13484
13892
|
roundStartMs = d.now();
|
|
13485
13893
|
await dispatchDeveloper(prompt2, round);
|
|
13486
13894
|
resumedDispatch = false;
|
|
13895
|
+
if (headBeforeDispatch !== null) {
|
|
13896
|
+
const changedHead = await pollUntil(() => {
|
|
13897
|
+
const h = d.resolveHead(branch);
|
|
13898
|
+
return h !== headBeforeDispatch ? h : null;
|
|
13899
|
+
}, d.gatePollMaxAttempts, d.gatePollIntervalMs, d.sleep, "devReviewLoop: head-change wait timed out").catch(() => null);
|
|
13900
|
+
if (changedHead === null) {
|
|
13901
|
+
gateStalledStreak += 1;
|
|
13902
|
+
const stats = computeStats(headBeforeDispatch, roundStartMs);
|
|
13903
|
+
const detail = `head ${headBeforeDispatch} unchanged after dispatch; failing check-run(s): ${lastFailingChecks.length > 0 ? lastFailingChecks.join(", ") : "(unknown)"}`;
|
|
13904
|
+
if (gateStalledStreak < MAX_GATE_STALLED_TURNS) {
|
|
13905
|
+
d.flushOutbox(task);
|
|
13906
|
+
continue;
|
|
13907
|
+
}
|
|
13908
|
+
await logEvents(driverDecidedPauseEvents(config.loopId, state, round, stats));
|
|
13909
|
+
decision = { type: "pause", reason: "infrastructure", detail };
|
|
13910
|
+
d.flushOutbox(task);
|
|
13911
|
+
continue;
|
|
13912
|
+
}
|
|
13913
|
+
}
|
|
13487
13914
|
}
|
|
13488
13915
|
firstPass = false;
|
|
13489
13916
|
const gate = await waitForGreenGate(roundStartMs);
|
|
13917
|
+
lastFailingChecks = gate.failingChecks;
|
|
13918
|
+
pendingGateRedRetry = !gate.green;
|
|
13919
|
+
gateStalledStreak = 0;
|
|
13490
13920
|
const confidence = round >= 2 && gate.green ? readAndClearConfidence() : undefined;
|
|
13491
13921
|
const obs = { kind: "gate", round, green: gate.green, confidence, stats: gate.stats };
|
|
13492
13922
|
const result = assessRound(state, obs);
|
|
@@ -13506,33 +13936,78 @@ ${CONFIDENCE_PROMPT_LINE}`;
|
|
|
13506
13936
|
const result = assessRound(state, obs);
|
|
13507
13937
|
state = result.state;
|
|
13508
13938
|
decision = result.decision;
|
|
13939
|
+
pendingGateRedRetry = false;
|
|
13940
|
+
gateStalledStreak = 0;
|
|
13509
13941
|
await logEvents(result.events);
|
|
13510
13942
|
d.flushOutbox(task);
|
|
13511
13943
|
} else if (decision.type === "dispatch_reviewers") {
|
|
13512
13944
|
const head = d.resolveHead(branch);
|
|
13513
13945
|
const ciConclusion = d.fetchCiConclusion(head);
|
|
13514
|
-
const
|
|
13946
|
+
const resolvedObjectives = d.resolveIssueObjectives(task);
|
|
13515
13947
|
const rulings = d.fetchRulings(prNumber);
|
|
13516
|
-
const
|
|
13517
|
-
const
|
|
13518
|
-
|
|
13519
|
-
|
|
13520
|
-
|
|
13521
|
-
|
|
13948
|
+
const rulingOrdinal = d.fetchNewestRulingOrdinal(prNumber);
|
|
13949
|
+
const revision = d.fetchSourceRevision(task);
|
|
13950
|
+
const facts = {
|
|
13951
|
+
objectives: resolvedObjectives.text,
|
|
13952
|
+
objectivesVersion: resolvedObjectives.version,
|
|
13953
|
+
rulings,
|
|
13954
|
+
rulingOrdinal,
|
|
13955
|
+
head,
|
|
13956
|
+
ciConclusion,
|
|
13957
|
+
revision
|
|
13958
|
+
};
|
|
13959
|
+
let verdicts = null;
|
|
13960
|
+
try {
|
|
13961
|
+
verdicts = await Promise.all([
|
|
13962
|
+
dispatchReviewer("reviewer", round, facts),
|
|
13963
|
+
dispatchReviewer("security", round, facts)
|
|
13964
|
+
]);
|
|
13965
|
+
} catch (err) {
|
|
13966
|
+
if (!(err instanceof ReviewerInfrastructureFailure))
|
|
13967
|
+
throw err;
|
|
13968
|
+
const stats = computeStats(head, roundStartMs);
|
|
13969
|
+
await logEvents(driverDecidedPauseEvents(config.loopId, state, round, stats));
|
|
13970
|
+
decision = { type: "pause", reason: "infrastructure", detail: err.message };
|
|
13971
|
+
}
|
|
13972
|
+
if (verdicts) {
|
|
13973
|
+
const reassessedObjectives = d.resolveIssueObjectives(task);
|
|
13974
|
+
const reassessedRulingOrdinal = d.fetchNewestRulingOrdinal(prNumber);
|
|
13975
|
+
if (reassessedObjectives.version !== facts.objectivesVersion) {
|
|
13976
|
+
const command = reassessedObjectives.edit ? describeObjectivesEdit(task, reassessedObjectives.edit) : `vinaya issue objectives edit ${task} ... (edit comment not found on re-read)`;
|
|
13977
|
+
const detail = `objectives moved from ${facts.objectivesVersion ?? "none"} to ${reassessedObjectives.version ?? "none"} between reviewer dispatch and assessment — superseded by \`${command}\``;
|
|
13978
|
+
const stats = computeStats(head, roundStartMs);
|
|
13979
|
+
await logEvents(driverDecidedPauseEvents(config.loopId, state, round, stats));
|
|
13980
|
+
decision = { type: "pause", reason: "objectives_changed", detail };
|
|
13981
|
+
d.flushOutbox(task);
|
|
13982
|
+
} else if (reassessedRulingOrdinal !== facts.rulingOrdinal) {
|
|
13983
|
+
const detail = `a new ruling landed between reviewer dispatch and assessment — ruling ordinal moved from ${facts.rulingOrdinal} to ${reassessedRulingOrdinal} — superseded by ruling ${prNumber}-${reassessedRulingOrdinal}`;
|
|
13984
|
+
const stats = computeStats(head, roundStartMs);
|
|
13985
|
+
await logEvents(driverDecidedPauseEvents(config.loopId, state, round, stats));
|
|
13986
|
+
decision = { type: "pause", reason: "ruling_posted", detail };
|
|
13987
|
+
d.flushOutbox(task);
|
|
13988
|
+
} else {
|
|
13989
|
+
const [reviewer, security] = verdicts;
|
|
13990
|
+
writeHeldVerdict(root, task, round, "reviewer", reviewer.rendered);
|
|
13991
|
+
writeHeldVerdict(root, task, round, "security", security.rendered);
|
|
13992
|
+
lastReviewContext = `${reviewer.rendered}
|
|
13522
13993
|
|
|
13523
13994
|
---
|
|
13524
13995
|
|
|
13525
13996
|
${security.rendered}`;
|
|
13526
|
-
|
|
13527
|
-
|
|
13528
|
-
|
|
13529
|
-
|
|
13530
|
-
|
|
13531
|
-
|
|
13532
|
-
|
|
13533
|
-
|
|
13534
|
-
|
|
13535
|
-
|
|
13997
|
+
const obs = { kind: "verdicts", round, verdicts: [reviewer.observation, security.observation] };
|
|
13998
|
+
const result = assessRound(state, obs);
|
|
13999
|
+
state = result.state;
|
|
14000
|
+
decision = result.decision;
|
|
14001
|
+
const routed = routeCompletionEvents(result.events, decision.type);
|
|
14002
|
+
pendingCompletionEvents = routed.toDeferUntilPublish;
|
|
14003
|
+
await logEvents(routed.toLogNow);
|
|
14004
|
+
d.flushOutbox(task);
|
|
14005
|
+
if (decision.type === "dispatch_developer")
|
|
14006
|
+
round += 1;
|
|
14007
|
+
}
|
|
14008
|
+
} else {
|
|
14009
|
+
d.flushOutbox(task);
|
|
14010
|
+
}
|
|
13536
14011
|
}
|
|
13537
14012
|
if (decision.type === "publish") {
|
|
13538
14013
|
publishRound(root, {
|
|
@@ -13555,9 +14030,10 @@ ${security.rendered}`;
|
|
|
13555
14030
|
branch,
|
|
13556
14031
|
prNumber,
|
|
13557
14032
|
reason: decision.reason,
|
|
14033
|
+
detail: decision.detail,
|
|
13558
14034
|
pausedAt: new Date().toISOString()
|
|
13559
14035
|
});
|
|
13560
|
-
postPauseComment(root, task, round, pauseHead, prNumber, decision.reason);
|
|
14036
|
+
postPauseComment(root, task, round, pauseHead, prNumber, decision.reason, decision.detail);
|
|
13561
14037
|
d.flushOutbox(task);
|
|
13562
14038
|
return { finalDecision: decision, prNumber, task };
|
|
13563
14039
|
}
|
|
@@ -13617,10 +14093,10 @@ async function devReviewLoopCommand(args) {
|
|
|
13617
14093
|
if (parsed.json) {
|
|
13618
14094
|
printJson({ finalDecision: result.finalDecision, prNumber: result.prNumber, task: result.task });
|
|
13619
14095
|
} else if (result.finalDecision.type === "publish") {
|
|
13620
|
-
process.stdout.write(`vinaya dev-review-loop: task ${result.task}, PR #${result.prNumber} — publish
|
|
14096
|
+
process.stdout.write(`${colourLoopLine(`vinaya dev-review-loop: task ${result.task}, PR #${result.prNumber} — publish`, process.stdout)}
|
|
13621
14097
|
`);
|
|
13622
14098
|
} else if (result.finalDecision.type === "pause") {
|
|
13623
|
-
process.stdout.write(`vinaya dev-review-loop: task ${result.task}, PR #${result.prNumber} — paused (${result.finalDecision.reason})
|
|
14099
|
+
process.stdout.write(`${colourLoopLine(`vinaya dev-review-loop: task ${result.task}, PR #${result.prNumber} — paused (${result.finalDecision.reason})`, process.stdout)}
|
|
13624
14100
|
`);
|
|
13625
14101
|
}
|
|
13626
14102
|
if (result.finalDecision.type === "pause")
|
|
@@ -13631,10 +14107,10 @@ async function devReviewLoopCommand(args) {
|
|
|
13631
14107
|
import { readFileSync as readFileSync19 } from "node:fs";
|
|
13632
14108
|
|
|
13633
14109
|
// src/commands/log.ts
|
|
13634
|
-
import { execFileSync as
|
|
13635
|
-
import { lstatSync, readFileSync as readFileSync18, statSync as statSync2, writeFileSync as
|
|
13636
|
-
import { homedir as homedir4, tmpdir as
|
|
13637
|
-
import { join as
|
|
14110
|
+
import { execFileSync as execFileSync17 } from "node:child_process";
|
|
14111
|
+
import { lstatSync, readFileSync as readFileSync18, statSync as statSync2, writeFileSync as writeFileSync9, rmSync as rmSync6 } from "node:fs";
|
|
14112
|
+
import { homedir as homedir4, tmpdir as tmpdir4 } from "node:os";
|
|
14113
|
+
import { join as join18 } from "node:path";
|
|
13638
14114
|
var FORGE_COMMENT_MAX_CHARS = 65536;
|
|
13639
14115
|
function isEnoent2(err) {
|
|
13640
14116
|
return typeof err === "object" && err !== null && err.code === "ENOENT";
|
|
@@ -13794,7 +14270,7 @@ function planFlush(lines, maxChars) {
|
|
|
13794
14270
|
}
|
|
13795
14271
|
function gh2(args) {
|
|
13796
14272
|
try {
|
|
13797
|
-
return
|
|
14273
|
+
return execFileSync17("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
13798
14274
|
} catch (err) {
|
|
13799
14275
|
const stderr = err.stderr;
|
|
13800
14276
|
throw new Error(String(stderr ?? err.message).trim() || "gh command failed");
|
|
@@ -13809,8 +14285,8 @@ function issueFromPr(prNumber) {
|
|
|
13809
14285
|
return issue;
|
|
13810
14286
|
}
|
|
13811
14287
|
function postChunk(op, targetId, body, index) {
|
|
13812
|
-
const tmp =
|
|
13813
|
-
|
|
14288
|
+
const tmp = join18(tmpdir4(), `vinaya-log-flush-${process.pid}-${Date.now()}-${index}.md`);
|
|
14289
|
+
writeFileSync9(tmp, body, { flag: "wx" });
|
|
13814
14290
|
try {
|
|
13815
14291
|
const out = op === "pr.comment" ? gh2(["pr", "comment", targetId, "--body-file", tmp]) : gh2(["issue", "comment", targetId, "--body-file", tmp]);
|
|
13816
14292
|
const match = /#issuecomment-(\d+)/.exec(out);
|
|
@@ -13820,7 +14296,7 @@ function postChunk(op, targetId, body, index) {
|
|
|
13820
14296
|
`);
|
|
13821
14297
|
return `unparsed:${out.slice(0, 200)}`;
|
|
13822
14298
|
} finally {
|
|
13823
|
-
|
|
14299
|
+
rmSync6(tmp, { force: true });
|
|
13824
14300
|
}
|
|
13825
14301
|
}
|
|
13826
14302
|
function parseArgs3(args) {
|
|
@@ -13853,7 +14329,7 @@ async function logFlushCommand(args) {
|
|
|
13853
14329
|
const forgeTargetId = parsed.pr !== undefined ? String(parsed.pr) : String(issueNumber);
|
|
13854
14330
|
const resolved = await resolveRepo();
|
|
13855
14331
|
const repo = resolved && isSafeRepoSegment3(resolved.owner) && isSafeRepoSegment3(resolved.repo) ? resolved : null;
|
|
13856
|
-
const outboxRoot2 = () =>
|
|
14332
|
+
const outboxRoot2 = () => join18(GLOBAL_VINAYA_HOME, "outbox");
|
|
13857
14333
|
const path = outboxPathFor2({ outboxRoot: outboxRoot2, repo }, { issue: issueNumber });
|
|
13858
14334
|
let lstat;
|
|
13859
14335
|
try {
|
|
@@ -13933,7 +14409,7 @@ async function logFlushCommand(args) {
|
|
|
13933
14409
|
`).join("");
|
|
13934
14410
|
const liveNow = readFileSync18(path);
|
|
13935
14411
|
const tail = liveNow.subarray(Math.min(startOffset, liveNow.byteLength));
|
|
13936
|
-
|
|
14412
|
+
writeFileSync9(path, Buffer.concat([Buffer.from(unposted, "utf8"), tail]));
|
|
13937
14413
|
if (!finalLanded) {
|
|
13938
14414
|
emitCheckError({
|
|
13939
14415
|
schema: CHECK_SCHEMA_VERSION,
|
|
@@ -13955,6 +14431,7 @@ async function logFlushCommand(args) {
|
|
|
13955
14431
|
}
|
|
13956
14432
|
|
|
13957
14433
|
// src/commands/dispatch.ts
|
|
14434
|
+
var KNOWN_FLAGS = ["--agent", "--model", "--prompt-file", "--task", "--pr", "--round", "--resume", "--json"];
|
|
13958
14435
|
function parseArgs4(args) {
|
|
13959
14436
|
const role = args[0];
|
|
13960
14437
|
let agent;
|
|
@@ -13965,6 +14442,7 @@ function parseArgs4(args) {
|
|
|
13965
14442
|
let round;
|
|
13966
14443
|
let resume;
|
|
13967
14444
|
let json = false;
|
|
14445
|
+
const unknown = [];
|
|
13968
14446
|
for (let i = 1;i < args.length; i++) {
|
|
13969
14447
|
const a = args[i];
|
|
13970
14448
|
if (a === "--agent")
|
|
@@ -13983,11 +14461,18 @@ function parseArgs4(args) {
|
|
|
13983
14461
|
resume = args[++i];
|
|
13984
14462
|
else if (a === "--json")
|
|
13985
14463
|
json = true;
|
|
14464
|
+
else if (a !== undefined)
|
|
14465
|
+
unknown.push(a);
|
|
13986
14466
|
}
|
|
13987
|
-
return { role, agent, model, promptFile, task, pr, round, resume, json };
|
|
14467
|
+
return { role, agent, model, promptFile, task, pr, round, resume, json, unknown };
|
|
13988
14468
|
}
|
|
13989
14469
|
async function dispatchCommand(args) {
|
|
13990
14470
|
const parsed = parseArgs4(args);
|
|
14471
|
+
if (parsed.unknown.length > 0) {
|
|
14472
|
+
process.stderr.write(`vinaya dispatch: unrecognized flag${parsed.unknown.length > 1 ? "s" : ""} ${parsed.unknown.map((f) => `'${f}'`).join(", ")} — expected one of ${KNOWN_FLAGS.join(", ")}
|
|
14473
|
+
`);
|
|
14474
|
+
process.exit(1);
|
|
14475
|
+
}
|
|
13991
14476
|
if (!parsed.role || !ROLE_VALUES.includes(parsed.role)) {
|
|
13992
14477
|
process.stderr.write(`vinaya dispatch: invalid role '${parsed.role ?? "(none)"}' — expected one of ${ROLE_VALUES.join(", ")}
|
|
13993
14478
|
`);
|
|
@@ -14060,13 +14545,13 @@ async function dispatchCommand(args) {
|
|
|
14060
14545
|
}
|
|
14061
14546
|
|
|
14062
14547
|
// src/commands/doctor.ts
|
|
14063
|
-
import { execFileSync as
|
|
14064
|
-
import { existsSync as
|
|
14065
|
-
import { delimiter, join as
|
|
14548
|
+
import { execFileSync as execFileSync18 } from "node:child_process";
|
|
14549
|
+
import { existsSync as existsSync17, readdirSync as readdirSync8, readFileSync as readFileSync23, statSync as statSync3 } from "node:fs";
|
|
14550
|
+
import { delimiter, join as join21 } from "node:path";
|
|
14066
14551
|
|
|
14067
14552
|
// src/lib/self-host.ts
|
|
14068
|
-
import { readdirSync as readdirSync7, readFileSync as readFileSync20, realpathSync as realpathSync2 } from "node:fs";
|
|
14069
|
-
import { join as
|
|
14553
|
+
import { existsSync as existsSync14, readdirSync as readdirSync7, readFileSync as readFileSync20, realpathSync as realpathSync2 } from "node:fs";
|
|
14554
|
+
import { dirname as dirname9, join as join19, sep as sep3 } from "node:path";
|
|
14070
14555
|
var VINAYA_PACKAGE_NAME = "@attalabs/vinaya";
|
|
14071
14556
|
var DEFAULT_BIN = "dist/index.js";
|
|
14072
14557
|
var MAX_CANDIDATE_DIRS = 2000;
|
|
@@ -14124,7 +14609,7 @@ function expandPattern(repoRoot2, pattern) {
|
|
|
14124
14609
|
for (const dir of dirs) {
|
|
14125
14610
|
if (segment.includes("*")) {
|
|
14126
14611
|
const matches = segmentMatcher(segment);
|
|
14127
|
-
for (const name of childDirs(
|
|
14612
|
+
for (const name of childDirs(join19(repoRoot2, dir))) {
|
|
14128
14613
|
if (matches(name))
|
|
14129
14614
|
next.push(dir ? `${dir}/${name}` : name);
|
|
14130
14615
|
}
|
|
@@ -14139,7 +14624,7 @@ function expandPattern(repoRoot2, pattern) {
|
|
|
14139
14624
|
function resolvesInsideRepo(repoRoot2, rel) {
|
|
14140
14625
|
try {
|
|
14141
14626
|
const root = realpathSync2(repoRoot2);
|
|
14142
|
-
const target = realpathSync2(
|
|
14627
|
+
const target = realpathSync2(join19(repoRoot2, rel));
|
|
14143
14628
|
return target === root || target.startsWith(`${root}${sep3}`);
|
|
14144
14629
|
} catch {
|
|
14145
14630
|
return false;
|
|
@@ -14157,14 +14642,14 @@ function binPath(pkg) {
|
|
|
14157
14642
|
return DEFAULT_BIN;
|
|
14158
14643
|
}
|
|
14159
14644
|
function detectVendoredVinaya(repoRoot2) {
|
|
14160
|
-
const patterns = workspacePatterns(readPackageJson(
|
|
14645
|
+
const patterns = workspacePatterns(readPackageJson(join19(repoRoot2, "package.json")));
|
|
14161
14646
|
const seen = new Set;
|
|
14162
14647
|
for (const pattern of patterns) {
|
|
14163
14648
|
for (const dir of expandPattern(repoRoot2, pattern)) {
|
|
14164
14649
|
if (seen.has(dir))
|
|
14165
14650
|
continue;
|
|
14166
14651
|
seen.add(dir);
|
|
14167
|
-
const member = readPackageJson(
|
|
14652
|
+
const member = readPackageJson(join19(repoRoot2, dir, "package.json"));
|
|
14168
14653
|
if (member?.name === VINAYA_PACKAGE_NAME) {
|
|
14169
14654
|
const bin2 = `${dir}/${binPath(member)}`;
|
|
14170
14655
|
if (!isSafeRelPath(dir) || !isSafeRelPath(bin2))
|
|
@@ -14177,16 +14662,29 @@ function detectVendoredVinaya(repoRoot2) {
|
|
|
14177
14662
|
}
|
|
14178
14663
|
return null;
|
|
14179
14664
|
}
|
|
14665
|
+
function resolveAuthorRepoSourceEntry(ownPackageRoot, cwd = process.cwd()) {
|
|
14666
|
+
if (!ownPackageRoot.split(sep3).includes("node_modules"))
|
|
14667
|
+
return null;
|
|
14668
|
+
const info = resolveDoctrineRootInfo(undefined, cwd);
|
|
14669
|
+
if (info?.source !== "tree")
|
|
14670
|
+
return null;
|
|
14671
|
+
const toplevel = dirname9(info.root);
|
|
14672
|
+
const cliPkg = readPackageJson(join19(toplevel, "apps", "cli", "package.json"));
|
|
14673
|
+
if (cliPkg?.name !== VINAYA_PACKAGE_NAME)
|
|
14674
|
+
return null;
|
|
14675
|
+
const sourceEntry = join19(toplevel, "apps", "cli", "src", "index.ts");
|
|
14676
|
+
return existsSync14(sourceEntry) ? sourceEntry : null;
|
|
14677
|
+
}
|
|
14180
14678
|
|
|
14181
14679
|
// src/lib/env-lint.ts
|
|
14182
|
-
import { existsSync as
|
|
14680
|
+
import { existsSync as existsSync15, readFileSync as readFileSync21 } from "node:fs";
|
|
14183
14681
|
var ENV_READ_PATTERN = /\b(?:process\.env|Bun\.env|Deno\.env)\b/;
|
|
14184
14682
|
function checksMissingEnvDeclaration(specs) {
|
|
14185
14683
|
const names = [];
|
|
14186
14684
|
for (const spec of specs) {
|
|
14187
14685
|
if (spec.env)
|
|
14188
14686
|
continue;
|
|
14189
|
-
if (!
|
|
14687
|
+
if (!existsSync15(spec.run))
|
|
14190
14688
|
continue;
|
|
14191
14689
|
let source;
|
|
14192
14690
|
try {
|
|
@@ -14204,8 +14702,8 @@ function envDeclarationWarning(name) {
|
|
|
14204
14702
|
}
|
|
14205
14703
|
|
|
14206
14704
|
// src/lib/registry-write.ts
|
|
14207
|
-
import { existsSync as
|
|
14208
|
-
import { dirname as
|
|
14705
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync6, readFileSync as readFileSync22, writeFileSync as writeFileSync10 } from "node:fs";
|
|
14706
|
+
import { dirname as dirname10, join as join20 } from "node:path";
|
|
14209
14707
|
var PROJECTS_REGISTRY_PATH = ".vinaya/projects.md";
|
|
14210
14708
|
var CONFIG_FILE_PATH = "vinaya.config.json";
|
|
14211
14709
|
function rowLine(name, path, specsPath) {
|
|
@@ -14271,9 +14769,9 @@ ${newRow}
|
|
|
14271
14769
|
`);
|
|
14272
14770
|
}
|
|
14273
14771
|
function planRegistryRow(repoRoot2, name, path, specsPath) {
|
|
14274
|
-
const abs2 =
|
|
14772
|
+
const abs2 = join20(repoRoot2, PROJECTS_REGISTRY_PATH);
|
|
14275
14773
|
const line = rowLine(name, path, specsPath);
|
|
14276
|
-
if (!
|
|
14774
|
+
if (!existsSync16(abs2)) {
|
|
14277
14775
|
return { action: "create-host", rowLine: line, path: PROJECTS_REGISTRY_PATH };
|
|
14278
14776
|
}
|
|
14279
14777
|
const existing = readFileSync22(abs2, "utf-8");
|
|
@@ -14281,13 +14779,13 @@ function planRegistryRow(repoRoot2, name, path, specsPath) {
|
|
|
14281
14779
|
return { action: already ? "skip-present" : "append-row", rowLine: line, path: PROJECTS_REGISTRY_PATH };
|
|
14282
14780
|
}
|
|
14283
14781
|
function applyRegistryRow(repoRoot2, plan, name, path, specsPath) {
|
|
14284
|
-
const abs2 =
|
|
14782
|
+
const abs2 = join20(repoRoot2, PROJECTS_REGISTRY_PATH);
|
|
14285
14783
|
if (plan.action === "create-host") {
|
|
14286
|
-
mkdirSync6(
|
|
14287
|
-
|
|
14784
|
+
mkdirSync6(dirname10(abs2), { recursive: true });
|
|
14785
|
+
writeFileSync10(abs2, freshProjectsRegistry(name, path, specsPath), "utf-8");
|
|
14288
14786
|
} else if (plan.action === "append-row") {
|
|
14289
14787
|
const existing = readFileSync22(abs2, "utf-8");
|
|
14290
|
-
|
|
14788
|
+
writeFileSync10(abs2, appendRegistryRow(existing, name, path, specsPath), "utf-8");
|
|
14291
14789
|
}
|
|
14292
14790
|
}
|
|
14293
14791
|
function renderRegistryRowDiffLine(plan) {
|
|
@@ -14300,8 +14798,8 @@ function renderRegistryRowDiffLine(plan) {
|
|
|
14300
14798
|
${plan.rowLine}`;
|
|
14301
14799
|
}
|
|
14302
14800
|
function readProjectEntries(repoRoot2) {
|
|
14303
|
-
const abs2 =
|
|
14304
|
-
if (!
|
|
14801
|
+
const abs2 = join20(repoRoot2, CONFIG_FILE_PATH);
|
|
14802
|
+
if (!existsSync16(abs2))
|
|
14305
14803
|
return [];
|
|
14306
14804
|
try {
|
|
14307
14805
|
return VinayaConfigSchema.parse(JSON.parse(readFileSync22(abs2, "utf-8"))).projects ?? [];
|
|
@@ -14316,10 +14814,10 @@ function planConfigProjectEntry(repoRoot2, entry2) {
|
|
|
14316
14814
|
function applyConfigProjectEntry(repoRoot2, plan) {
|
|
14317
14815
|
if (plan.action === "skip-present")
|
|
14318
14816
|
return;
|
|
14319
|
-
const abs2 =
|
|
14817
|
+
const abs2 = join20(repoRoot2, CONFIG_FILE_PATH);
|
|
14320
14818
|
const seed = JSON.parse(readFileSync22(abs2, "utf-8"));
|
|
14321
14819
|
const projects = Array.isArray(seed.projects) ? seed.projects : [];
|
|
14322
|
-
|
|
14820
|
+
writeFileSync10(abs2, `${JSON.stringify({ ...seed, projects: [...projects, plan.entry] }, null, 2)}
|
|
14323
14821
|
`, "utf-8");
|
|
14324
14822
|
}
|
|
14325
14823
|
function renderConfigProjectEntryDiffLine(plan) {
|
|
@@ -14336,7 +14834,7 @@ function renderConfigProjectEntryDiffLine(plan) {
|
|
|
14336
14834
|
|
|
14337
14835
|
// src/commands/doctor.ts
|
|
14338
14836
|
function readVersion() {
|
|
14339
|
-
const pkg = JSON.parse(readFileSync23(
|
|
14837
|
+
const pkg = JSON.parse(readFileSync23(join21(packageRoot(import.meta.url), "package.json"), "utf-8"));
|
|
14340
14838
|
return pkg.version;
|
|
14341
14839
|
}
|
|
14342
14840
|
function realDeps4() {
|
|
@@ -14357,8 +14855,8 @@ var info = (check, message) => ({ check, severity: "info", message });
|
|
|
14357
14855
|
var warn = (check, message) => ({ check, severity: "warn", message });
|
|
14358
14856
|
var error = (check, message) => ({ check, severity: "error", message });
|
|
14359
14857
|
function readConfig(repoRoot2) {
|
|
14360
|
-
const p =
|
|
14361
|
-
if (!
|
|
14858
|
+
const p = join21(repoRoot2, CONFIG_PATH);
|
|
14859
|
+
if (!existsSync17(p))
|
|
14362
14860
|
return { kind: "missing" };
|
|
14363
14861
|
let raw;
|
|
14364
14862
|
try {
|
|
@@ -14401,8 +14899,8 @@ function diagnoseInstall(repoRoot2, ctx, manifest) {
|
|
|
14401
14899
|
for (const op of buildInitOps(ctx)) {
|
|
14402
14900
|
if (op.kind === "create-file") {
|
|
14403
14901
|
const check = labelForPath(op.path);
|
|
14404
|
-
const abs2 =
|
|
14405
|
-
const exists =
|
|
14902
|
+
const abs2 = join21(repoRoot2, op.path);
|
|
14903
|
+
const exists = existsSync17(abs2);
|
|
14406
14904
|
const owned = ownedFiles.has(op.path) || isDefaultedAgentVendorPath(op.path, manifest);
|
|
14407
14905
|
if (!exists) {
|
|
14408
14906
|
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\`.`));
|
|
@@ -14427,7 +14925,7 @@ function diagnoseInstall(repoRoot2, ctx, manifest) {
|
|
|
14427
14925
|
const check = "hooks";
|
|
14428
14926
|
const abs2 = resolveManagedBlockPath(repoRoot2, op.path);
|
|
14429
14927
|
const owned = ownedBlocks.has(blockKey(op.path, op.marker));
|
|
14430
|
-
if (!
|
|
14928
|
+
if (!existsSync17(abs2)) {
|
|
14431
14929
|
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.`));
|
|
14432
14930
|
continue;
|
|
14433
14931
|
}
|
|
@@ -14479,22 +14977,22 @@ async function diagnoseHookRouting(repoRoot2, hookDir, readHooksPath) {
|
|
|
14479
14977
|
function diagnoseCustomChecks(repoRoot2, config) {
|
|
14480
14978
|
const findings = [];
|
|
14481
14979
|
for (const [name, entry2] of Object.entries(config.checks ?? {})) {
|
|
14482
|
-
const scriptAbs =
|
|
14483
|
-
findings.push(
|
|
14980
|
+
const scriptAbs = join21(repoRoot2, entry2.run);
|
|
14981
|
+
findings.push(existsSync17(scriptAbs) ? ok("checks", `custom check '${name}' → ${entry2.run}`) : error("checks", `custom check '${name}' points at a missing script: ${entry2.run}`));
|
|
14484
14982
|
}
|
|
14485
14983
|
return findings;
|
|
14486
14984
|
}
|
|
14487
14985
|
function listTrackedFiles(repoRoot2) {
|
|
14488
14986
|
try {
|
|
14489
|
-
return
|
|
14987
|
+
return execFileSync18("git", ["ls-files"], { cwd: repoRoot2, encoding: "utf-8" }).split(`
|
|
14490
14988
|
`).map((s) => s.trim()).filter(Boolean);
|
|
14491
14989
|
} catch {
|
|
14492
14990
|
return [];
|
|
14493
14991
|
}
|
|
14494
14992
|
}
|
|
14495
14993
|
function diagnoseDocOwnersHealth(repoRoot2) {
|
|
14496
|
-
const path =
|
|
14497
|
-
const content =
|
|
14994
|
+
const path = join21(repoRoot2, DOC_OWNERS_PATH);
|
|
14995
|
+
const content = existsSync17(path) ? readFileSync23(path, "utf-8") : null;
|
|
14498
14996
|
const state = classifyDocOwnersManifest(content);
|
|
14499
14997
|
if (state === "absent" || state === "empty")
|
|
14500
14998
|
return [];
|
|
@@ -14511,8 +15009,8 @@ function diagnoseDocOwnersHealth(repoRoot2) {
|
|
|
14511
15009
|
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."));
|
|
14512
15010
|
}
|
|
14513
15011
|
if (!isUrlPointer(b.pointer)) {
|
|
14514
|
-
const pointerPath =
|
|
14515
|
-
if (!
|
|
15012
|
+
const pointerPath = join21(repoRoot2, pointerToPath(b.pointer));
|
|
15013
|
+
if (!existsSync17(pointerPath)) {
|
|
14516
15014
|
flagged = true;
|
|
14517
15015
|
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."));
|
|
14518
15016
|
}
|
|
@@ -14527,7 +15025,7 @@ var LEGACY_AEG_PACKAGES_PATH = ".aeg/packages";
|
|
|
14527
15025
|
function readWorkspacesForDoctor(repoRoot2) {
|
|
14528
15026
|
const fromPackageJson = () => {
|
|
14529
15027
|
try {
|
|
14530
|
-
const pkg = JSON.parse(readFileSync23(
|
|
15028
|
+
const pkg = JSON.parse(readFileSync23(join21(repoRoot2, "package.json"), "utf-8"));
|
|
14531
15029
|
return Array.isArray(pkg.workspaces) ? pkg.workspaces.filter((w) => typeof w === "string") : [];
|
|
14532
15030
|
} catch {
|
|
14533
15031
|
return [];
|
|
@@ -14535,7 +15033,7 @@ function readWorkspacesForDoctor(repoRoot2) {
|
|
|
14535
15033
|
};
|
|
14536
15034
|
const fromPnpmWorkspaceYaml = () => {
|
|
14537
15035
|
try {
|
|
14538
|
-
return parsePnpmWorkspaceYaml(readFileSync23(
|
|
15036
|
+
return parsePnpmWorkspaceYaml(readFileSync23(join21(repoRoot2, "pnpm-workspace.yaml"), "utf-8"));
|
|
14539
15037
|
} catch {
|
|
14540
15038
|
return [];
|
|
14541
15039
|
}
|
|
@@ -14544,18 +15042,18 @@ function readWorkspacesForDoctor(repoRoot2) {
|
|
|
14544
15042
|
}
|
|
14545
15043
|
function listChildDirsForDoctor(dir, repoRoot2) {
|
|
14546
15044
|
try {
|
|
14547
|
-
return readdirSync8(
|
|
15045
|
+
return readdirSync8(join21(repoRoot2, dir), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
14548
15046
|
} catch {
|
|
14549
15047
|
return [];
|
|
14550
15048
|
}
|
|
14551
15049
|
}
|
|
14552
15050
|
function diagnoseBlastRadiusDeprecation(repoRoot2, config) {
|
|
14553
15051
|
const derived = deriveWorkspacePackageDomains(readWorkspacesForDoctor(repoRoot2), (dir) => listChildDirsForDoctor(dir, repoRoot2));
|
|
14554
|
-
const defaults = deriveBuiltinCrossCuttingDefaults((p) =>
|
|
15052
|
+
const defaults = deriveBuiltinCrossCuttingDefaults((p) => existsSync17(join21(repoRoot2, p)));
|
|
14555
15053
|
const covered = new Set([...derived, ...defaults]);
|
|
14556
15054
|
const configExtra = new Set(config?.blastRadius?.extraDomains ?? []);
|
|
14557
|
-
const legacyPath =
|
|
14558
|
-
if (!
|
|
15055
|
+
const legacyPath = join21(repoRoot2, LEGACY_AEG_PACKAGES_PATH);
|
|
15056
|
+
if (!existsSync17(legacyPath)) {
|
|
14559
15057
|
return [
|
|
14560
15058
|
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.`)
|
|
14561
15059
|
];
|
|
@@ -14595,7 +15093,7 @@ function diagnoseBriefSchemaDrift(config) {
|
|
|
14595
15093
|
}
|
|
14596
15094
|
function diagnoseEnvDeclarations(repoRoot2, config) {
|
|
14597
15095
|
const resolved = resolveChecks(coreCheckRegistry(), config?.checks).resolved;
|
|
14598
|
-
const specs = resolved.map((entry2) => entry2.source === "config" ? { ...entry2.spec, run:
|
|
15096
|
+
const specs = resolved.map((entry2) => entry2.source === "config" ? { ...entry2.spec, run: join21(repoRoot2, entry2.spec.run) } : entry2.spec);
|
|
14599
15097
|
const missing = checksMissingEnvDeclaration(specs);
|
|
14600
15098
|
const findings = missing.map((name) => info("env", envDeclarationWarning(name)));
|
|
14601
15099
|
for (const message of lintEnvDeclarations(config?.checks)) {
|
|
@@ -14616,7 +15114,7 @@ function diagnoseCheckClassification(config) {
|
|
|
14616
15114
|
return findings;
|
|
14617
15115
|
}
|
|
14618
15116
|
function diagnoseGlobalConfigChecks() {
|
|
14619
|
-
if (!
|
|
15117
|
+
if (!existsSync17(GLOBAL_CONFIG_PATH))
|
|
14620
15118
|
return [];
|
|
14621
15119
|
let raw;
|
|
14622
15120
|
try {
|
|
@@ -14669,8 +15167,8 @@ function coversWorkflowsDir(pattern) {
|
|
|
14669
15167
|
return bare === ".github/workflows" || bare === "/.github/workflows";
|
|
14670
15168
|
}
|
|
14671
15169
|
function diagnoseCodeowners(repoRoot2) {
|
|
14672
|
-
const path =
|
|
14673
|
-
if (!
|
|
15170
|
+
const path = join21(repoRoot2, ".github", "CODEOWNERS");
|
|
15171
|
+
if (!existsSync17(path)) {
|
|
14674
15172
|
return info("codeowners", "no .github/CODEOWNERS — vinaya's workflow files have no required-review protection; see `vinaya init`'s printed recommendation.");
|
|
14675
15173
|
}
|
|
14676
15174
|
let body;
|
|
@@ -14688,7 +15186,7 @@ function diagnoseVinayaOnPath(agents) {
|
|
|
14688
15186
|
return [];
|
|
14689
15187
|
const names = process.platform === "win32" ? ["vinaya.cmd", "vinaya.exe", "vinaya.bat"] : ["vinaya"];
|
|
14690
15188
|
const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
14691
|
-
const onPath = dirs.some((dir) => names.some((name) =>
|
|
15189
|
+
const onPath = dirs.some((dir) => names.some((name) => existsSync17(join21(dir, name))));
|
|
14692
15190
|
return [
|
|
14693
15191
|
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`.')
|
|
14694
15192
|
];
|
|
@@ -14700,8 +15198,8 @@ function diagnosePrincipals(config) {
|
|
|
14700
15198
|
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.');
|
|
14701
15199
|
}
|
|
14702
15200
|
function diagnoseProjectsCoherence(repoRoot2, config) {
|
|
14703
|
-
const registryAbs =
|
|
14704
|
-
const registryNames =
|
|
15201
|
+
const registryAbs = join21(repoRoot2, PROJECTS_REGISTRY_PATH);
|
|
15202
|
+
const registryNames = existsSync17(registryAbs) ? new Set(parseRegistry(readFileSync23(registryAbs, "utf-8")).map((p) => p.name)) : new Set;
|
|
14705
15203
|
const configNames = new Set((config?.projects ?? []).map((p) => p.name));
|
|
14706
15204
|
if (registryNames.size === 0 && configNames.size === 0)
|
|
14707
15205
|
return [];
|
|
@@ -14720,17 +15218,17 @@ var TEST_INVOCATION_SUBSTRINGS = ["npm test", "npm run test", "bun test", "bunx
|
|
|
14720
15218
|
function diagnoseTestCi(repoRoot2) {
|
|
14721
15219
|
let pkg;
|
|
14722
15220
|
try {
|
|
14723
|
-
pkg = JSON.parse(readFileSync23(
|
|
15221
|
+
pkg = JSON.parse(readFileSync23(join21(repoRoot2, "package.json"), "utf-8"));
|
|
14724
15222
|
} catch {
|
|
14725
15223
|
return [];
|
|
14726
15224
|
}
|
|
14727
15225
|
const testScript = pkg?.scripts?.test;
|
|
14728
15226
|
if (typeof testScript !== "string" || testScript.trim() === "")
|
|
14729
15227
|
return [];
|
|
14730
|
-
const workflowsDir =
|
|
14731
|
-
const files =
|
|
15228
|
+
const workflowsDir = join21(repoRoot2, ".github/workflows");
|
|
15229
|
+
const files = existsSync17(workflowsDir) ? readdirSync8(workflowsDir).filter((name) => statSync3(join21(workflowsDir, name)).isFile()) : [];
|
|
14732
15230
|
const invoked = files.some((name) => {
|
|
14733
|
-
const content = readFileSync23(
|
|
15231
|
+
const content = readFileSync23(join21(workflowsDir, name), "utf-8");
|
|
14734
15232
|
return TEST_INVOCATION_SUBSTRINGS.some((s) => content.includes(s));
|
|
14735
15233
|
});
|
|
14736
15234
|
if (invoked)
|
|
@@ -14827,8 +15325,8 @@ async function doctorCommand(args) {
|
|
|
14827
15325
|
}
|
|
14828
15326
|
|
|
14829
15327
|
// src/commands/eject.ts
|
|
14830
|
-
import { existsSync as
|
|
14831
|
-
import { join as
|
|
15328
|
+
import { existsSync as existsSync18, readFileSync as readFileSync24 } from "node:fs";
|
|
15329
|
+
import { join as join22 } from "node:path";
|
|
14832
15330
|
function realDeps5() {
|
|
14833
15331
|
return {
|
|
14834
15332
|
detectRepo: detectGitRepo,
|
|
@@ -14845,8 +15343,8 @@ function parse2(args) {
|
|
|
14845
15343
|
return { dryRun: args.includes("--dry-run"), yes: args.includes("--yes") };
|
|
14846
15344
|
}
|
|
14847
15345
|
function readManifest2(repoRoot2) {
|
|
14848
|
-
const p =
|
|
14849
|
-
if (!
|
|
15346
|
+
const p = join22(repoRoot2, CONFIG_PATH);
|
|
15347
|
+
if (!existsSync18(p))
|
|
14850
15348
|
return { kind: "none" };
|
|
14851
15349
|
let raw;
|
|
14852
15350
|
try {
|
|
@@ -14940,8 +15438,8 @@ async function ejectCommand(args) {
|
|
|
14940
15438
|
}
|
|
14941
15439
|
|
|
14942
15440
|
// src/commands/init.ts
|
|
14943
|
-
import { existsSync as
|
|
14944
|
-
import { join as
|
|
15441
|
+
import { existsSync as existsSync19, readFileSync as readFileSync25, writeFileSync as writeFileSync11 } from "node:fs";
|
|
15442
|
+
import { join as join23 } from "node:path";
|
|
14945
15443
|
function realDeps6() {
|
|
14946
15444
|
return {
|
|
14947
15445
|
detectRepo: detectGitRepo,
|
|
@@ -14981,8 +15479,8 @@ function parseAgentsFlag(args) {
|
|
|
14981
15479
|
}
|
|
14982
15480
|
var PRODUCT_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
14983
15481
|
function readManifest3(repoRoot2) {
|
|
14984
|
-
const p =
|
|
14985
|
-
if (!
|
|
15482
|
+
const p = join23(repoRoot2, CONFIG_PATH);
|
|
15483
|
+
if (!existsSync19(p))
|
|
14986
15484
|
return null;
|
|
14987
15485
|
try {
|
|
14988
15486
|
return VinayaConfigSchema.parse(JSON.parse(readFileSync25(p, "utf-8"))).managed ?? null;
|
|
@@ -14991,9 +15489,9 @@ function readManifest3(repoRoot2) {
|
|
|
14991
15489
|
}
|
|
14992
15490
|
}
|
|
14993
15491
|
function writeManifest(repoRoot2, manifest) {
|
|
14994
|
-
const configAbs =
|
|
15492
|
+
const configAbs = join23(repoRoot2, CONFIG_PATH);
|
|
14995
15493
|
const seed = JSON.parse(readFileSync25(configAbs, "utf-8"));
|
|
14996
|
-
|
|
15494
|
+
writeFileSync11(configAbs, `${JSON.stringify({ ...seed, managed: manifest }, null, 2)}
|
|
14997
15495
|
`, "utf-8");
|
|
14998
15496
|
}
|
|
14999
15497
|
async function runInit(args, deps) {
|
|
@@ -15208,10 +15706,10 @@ function issueEditCommand(args) {
|
|
|
15208
15706
|
}
|
|
15209
15707
|
|
|
15210
15708
|
// src/commands/issue-objectives.ts
|
|
15211
|
-
import { execFileSync as
|
|
15212
|
-
import { mkdtempSync as
|
|
15213
|
-
import { tmpdir as
|
|
15214
|
-
import { join as
|
|
15709
|
+
import { execFileSync as execFileSync19 } from "node:child_process";
|
|
15710
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "node:fs";
|
|
15711
|
+
import { tmpdir as tmpdir5 } from "node:os";
|
|
15712
|
+
import { join as join24 } from "node:path";
|
|
15215
15713
|
var RETRY = 'vinaya issue objectives edit <n> --add "<sentence>" | --drop O<k> | --replace O<k> "<sentence>" --reason "<text>"';
|
|
15216
15714
|
var MARKER_PREFIX = "<!-- aeg:objectives:v";
|
|
15217
15715
|
var HEADING_RE2 = /^##[ \t]*Objectives[ \t]*$/im;
|
|
@@ -15278,7 +15776,7 @@ function parseArgs5(args) {
|
|
|
15278
15776
|
function fetchIssueBodyAndComments(issueRef) {
|
|
15279
15777
|
let out;
|
|
15280
15778
|
try {
|
|
15281
|
-
out =
|
|
15779
|
+
out = execFileSync19("gh", ["issue", "view", issueRef, "--json", "body,comments"], {
|
|
15282
15780
|
encoding: "utf8",
|
|
15283
15781
|
stdio: ["ignore", "pipe", "pipe"]
|
|
15284
15782
|
});
|
|
@@ -15342,9 +15840,9 @@ function issueObjectivesEditCommand(args) {
|
|
|
15342
15840
|
const previous = parsed.objectives;
|
|
15343
15841
|
const updated = applyOp(previous, op);
|
|
15344
15842
|
const newBody = spliceObjectivesSection(body, renderObjectives(updated));
|
|
15345
|
-
const dir =
|
|
15346
|
-
const tmp =
|
|
15347
|
-
|
|
15843
|
+
const dir = mkdtempSync3(join24(tmpdir5(), "vinaya-objectives-edit-"));
|
|
15844
|
+
const tmp = join24(dir, "body.md");
|
|
15845
|
+
writeFileSync12(tmp, newBody, "utf8");
|
|
15348
15846
|
try {
|
|
15349
15847
|
const ghArgs = ["--body-file", tmp];
|
|
15350
15848
|
writeValidatedIssueEdit({
|
|
@@ -15356,7 +15854,7 @@ function issueObjectivesEditCommand(args) {
|
|
|
15356
15854
|
quiet: true
|
|
15357
15855
|
});
|
|
15358
15856
|
} finally {
|
|
15359
|
-
|
|
15857
|
+
rmSync7(dir, { recursive: true, force: true });
|
|
15360
15858
|
}
|
|
15361
15859
|
const newVersion = objectivesVersion(updated);
|
|
15362
15860
|
const k = countMarkerComments(comments, MARKER_PREFIX) + 1;
|
|
@@ -15381,13 +15879,14 @@ function issueObjectivesEditCommand(args) {
|
|
|
15381
15879
|
}
|
|
15382
15880
|
|
|
15383
15881
|
// src/commands/milestone.ts
|
|
15384
|
-
import { execFileSync as
|
|
15882
|
+
import { execFileSync as execFileSync20 } from "node:child_process";
|
|
15385
15883
|
var RETRY_CREATE2 = "vinaya milestone create --title <title> --body-file <path>";
|
|
15386
15884
|
var RETRY_ADOPT = "vinaya milestone adopt --target <title> --slug <slug> [--slug <slug> ...]";
|
|
15387
15885
|
var RETRY_EDIT2 = "vinaya milestone edit <n> --body-file <path>";
|
|
15388
15886
|
var RETRY_CLOSE = "vinaya milestone close --slug <slug>";
|
|
15389
|
-
|
|
15390
|
-
|
|
15887
|
+
var RETRY_STATUS = "vinaya milestone status <n>";
|
|
15888
|
+
function sh4(args, input) {
|
|
15889
|
+
return execFileSync20(args[0], args.slice(1), {
|
|
15391
15890
|
encoding: "utf8",
|
|
15392
15891
|
input,
|
|
15393
15892
|
env: process.env,
|
|
@@ -15395,13 +15894,19 @@ function sh5(args, input) {
|
|
|
15395
15894
|
}).trim();
|
|
15396
15895
|
}
|
|
15397
15896
|
function shJson2(args, input) {
|
|
15398
|
-
return JSON.parse(
|
|
15897
|
+
return JSON.parse(sh4(args, input));
|
|
15399
15898
|
}
|
|
15400
15899
|
function ghErrorDetail(e) {
|
|
15401
15900
|
const stderr = e?.stderr;
|
|
15402
15901
|
const text = typeof stderr === "string" ? stderr : stderr?.toString();
|
|
15403
15902
|
return (text && text.trim().length > 0 ? text : e?.message ?? "unknown error").trim();
|
|
15404
15903
|
}
|
|
15904
|
+
function is404(e) {
|
|
15905
|
+
const stderr = e?.stderr;
|
|
15906
|
+
const haystack = [e?.message ?? "", typeof stderr === "string" ? stderr : stderr?.toString() ?? ""].join(`
|
|
15907
|
+
`);
|
|
15908
|
+
return /\b404\b|not found/i.test(haystack);
|
|
15909
|
+
}
|
|
15405
15910
|
function locateBodyOrRefuse2(args, commandName, retryCommand) {
|
|
15406
15911
|
let result;
|
|
15407
15912
|
try {
|
|
@@ -15469,7 +15974,7 @@ async function milestoneCreateCommand(args) {
|
|
|
15469
15974
|
const repoFlag = await resolveRepoFlagOrRefuse(RETRY_CREATE2);
|
|
15470
15975
|
let out;
|
|
15471
15976
|
try {
|
|
15472
|
-
out =
|
|
15977
|
+
out = sh4(["gh", "api", `repos/${repoFlag}/milestones`, "--input", "-"], JSON.stringify({ title, description: body }));
|
|
15473
15978
|
} catch (e) {
|
|
15474
15979
|
refuse2([
|
|
15475
15980
|
makeCheckError("forge-fetch", `\`gh api repos/${repoFlag}/milestones\` failed: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_CREATE2}\`.`)
|
|
@@ -15508,7 +16013,7 @@ async function milestoneEditCommand(args) {
|
|
|
15508
16013
|
const repoFlag = await resolveRepoFlagOrRefuse(RETRY_EDIT2);
|
|
15509
16014
|
let out;
|
|
15510
16015
|
try {
|
|
15511
|
-
out =
|
|
16016
|
+
out = sh4(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${number}`, "--input", "-"], JSON.stringify({ description: body }));
|
|
15512
16017
|
} catch (e) {
|
|
15513
16018
|
refuse2([
|
|
15514
16019
|
makeCheckError("forge-fetch", `\`gh api repos/${repoFlag}/milestones/${number}\` failed: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_EDIT2}\`.`)
|
|
@@ -15635,7 +16140,7 @@ async function milestoneAdoptCommand(args) {
|
|
|
15635
16140
|
for (const slugFact of facts.slugs) {
|
|
15636
16141
|
for (const issueNumber of slugFact.issueNumbers) {
|
|
15637
16142
|
try {
|
|
15638
|
-
|
|
16143
|
+
sh4(["gh", "issue", "edit", String(issueNumber), "-R", repoFlag, "--milestone", target]);
|
|
15639
16144
|
} catch (e) {
|
|
15640
16145
|
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)"}.`);
|
|
15641
16146
|
}
|
|
@@ -15646,7 +16151,7 @@ async function milestoneAdoptCommand(args) {
|
|
|
15646
16151
|
closedMilestone = legacy.number;
|
|
15647
16152
|
if (legacy.state !== "closed") {
|
|
15648
16153
|
try {
|
|
15649
|
-
|
|
16154
|
+
sh4(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${legacy.number}`, "-f", "state=closed"]);
|
|
15650
16155
|
} catch (e) {
|
|
15651
16156
|
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)"}.`);
|
|
15652
16157
|
}
|
|
@@ -15744,7 +16249,7 @@ async function milestoneCloseCommand(args) {
|
|
|
15744
16249
|
}
|
|
15745
16250
|
let out;
|
|
15746
16251
|
try {
|
|
15747
|
-
out =
|
|
16252
|
+
out = sh4(["gh", "api", "-X", "PATCH", `repos/${repoFlag}/milestones/${target.number}`, "-f", "state=closed"]);
|
|
15748
16253
|
} catch (e) {
|
|
15749
16254
|
refuse2([
|
|
15750
16255
|
makeCheckError("forge-fetch", `\`gh api repos/${repoFlag}/milestones/${target.number}\` failed: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_CLOSE}\`.`)
|
|
@@ -15757,12 +16262,116 @@ async function milestoneCloseCommand(args) {
|
|
|
15757
16262
|
process.stdout.write(`${closed.html_url}
|
|
15758
16263
|
`);
|
|
15759
16264
|
}
|
|
16265
|
+
function tallyIssueCounts(issues) {
|
|
16266
|
+
const counts = { merged: 0, open: 0, notPlanned: 0 };
|
|
16267
|
+
for (const issue of issues) {
|
|
16268
|
+
if (issue.state === "OPEN")
|
|
16269
|
+
counts.open++;
|
|
16270
|
+
else if (issue.stateReason === "NOT_PLANNED")
|
|
16271
|
+
counts.notPlanned++;
|
|
16272
|
+
else
|
|
16273
|
+
counts.merged++;
|
|
16274
|
+
}
|
|
16275
|
+
return counts;
|
|
16276
|
+
}
|
|
16277
|
+
function formatIssueCounts(total, counts) {
|
|
16278
|
+
if (total === 0)
|
|
16279
|
+
return "0 issues";
|
|
16280
|
+
const parts = [];
|
|
16281
|
+
if (counts.merged > 0)
|
|
16282
|
+
parts.push(`${counts.merged} merged`);
|
|
16283
|
+
if (counts.open > 0)
|
|
16284
|
+
parts.push(`${counts.open} open`);
|
|
16285
|
+
if (counts.notPlanned > 0)
|
|
16286
|
+
parts.push(`${counts.notPlanned} not planned`);
|
|
16287
|
+
return parts.length > 0 ? `${total} issues · ${parts.join(" · ")}` : `${total} issues`;
|
|
16288
|
+
}
|
|
16289
|
+
function extractStatusNumber(rest) {
|
|
16290
|
+
const numberArg = rest[0];
|
|
16291
|
+
if (!numberArg || numberArg.startsWith("-") || !/^\d+$/.test(numberArg))
|
|
16292
|
+
return null;
|
|
16293
|
+
return numberArg;
|
|
16294
|
+
}
|
|
16295
|
+
async function milestoneStatusCommand(args) {
|
|
16296
|
+
const json = args.includes("--json");
|
|
16297
|
+
const rest = args.filter((a) => a !== "--json");
|
|
16298
|
+
const numberArg = extractStatusNumber(rest);
|
|
16299
|
+
if (!numberArg) {
|
|
16300
|
+
refuse2([
|
|
16301
|
+
makeCheckError("forge-args", "`vinaya milestone status` requires the target Milestone number (digits only) as the first argument.", `Pass the Milestone number, e.g. \`${RETRY_STATUS}\`.`)
|
|
16302
|
+
]);
|
|
16303
|
+
}
|
|
16304
|
+
const repoFlag = await resolveRepoFlagOrRefuse(RETRY_STATUS);
|
|
16305
|
+
const [owner, repo] = repoFlag.split("/");
|
|
16306
|
+
let milestone;
|
|
16307
|
+
try {
|
|
16308
|
+
milestone = shJson2(["gh", "api", `repos/${repoFlag}/milestones/${numberArg}`]);
|
|
16309
|
+
} catch (e) {
|
|
16310
|
+
if (is404(e)) {
|
|
16311
|
+
refuse2([
|
|
16312
|
+
makeCheckError("milestone-status", `Milestone #${numberArg} is not an open-or-closed Milestone in ${repoFlag}.`, `Confirm the number with \`gh api repos/${repoFlag}/milestones\`, then re-run \`${RETRY_STATUS}\`.`)
|
|
16313
|
+
]);
|
|
16314
|
+
}
|
|
16315
|
+
refuse2([
|
|
16316
|
+
makeCheckError("forge-fetch", `could not fetch Milestone #${numberArg} from the forge: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_STATUS}\`.`)
|
|
16317
|
+
]);
|
|
16318
|
+
}
|
|
16319
|
+
const header = { number: milestone.number, title: milestone.title, state: milestone.state };
|
|
16320
|
+
const intents = intentLines(milestone.description ?? "");
|
|
16321
|
+
if (intents.length === 0) {
|
|
16322
|
+
if (json)
|
|
16323
|
+
printJson({ milestone: header, tranches: [] });
|
|
16324
|
+
else {
|
|
16325
|
+
process.stdout.write(`${header.title} (#${header.number}, ${header.state})
|
|
16326
|
+
`);
|
|
16327
|
+
process.stdout.write(`no tranche intents declared
|
|
16328
|
+
`);
|
|
16329
|
+
}
|
|
16330
|
+
return;
|
|
16331
|
+
}
|
|
16332
|
+
const rows = [];
|
|
16333
|
+
for (const intent of intents) {
|
|
16334
|
+
let issues;
|
|
16335
|
+
try {
|
|
16336
|
+
issues = await fetchTrancheIssuesAsync(owner, repo, intent.slug);
|
|
16337
|
+
} catch (e) {
|
|
16338
|
+
refuse2([
|
|
16339
|
+
makeCheckError("forge-fetch", `could not fetch tranche \`${intent.slug}\`'s Issues from the forge: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_STATUS}\`.`)
|
|
16340
|
+
]);
|
|
16341
|
+
}
|
|
16342
|
+
let milestoneFacts;
|
|
16343
|
+
try {
|
|
16344
|
+
milestoneFacts = findMilestoneForSlug(owner, repo, intent.slug);
|
|
16345
|
+
} catch (e) {
|
|
16346
|
+
refuse2([
|
|
16347
|
+
makeCheckError("forge-fetch", `could not derive tranche \`${intent.slug}\`'s Milestone facts from the forge: ${ghErrorDetail(e)}`, `Check \`gh auth status\` and network, then re-run \`${RETRY_STATUS}\`.`)
|
|
16348
|
+
]);
|
|
16349
|
+
}
|
|
16350
|
+
const tranche = trancheFromIssues(intent.slug, issues, milestoneFacts);
|
|
16351
|
+
rows.push({
|
|
16352
|
+
slug: intent.slug,
|
|
16353
|
+
lifecycle: tranche.lifecycle,
|
|
16354
|
+
issues: issues.length,
|
|
16355
|
+
counts: tallyIssueCounts(issues)
|
|
16356
|
+
});
|
|
16357
|
+
}
|
|
16358
|
+
if (json) {
|
|
16359
|
+
printJson({ milestone: header, tranches: rows });
|
|
16360
|
+
} else {
|
|
16361
|
+
process.stdout.write(`${header.title} (#${header.number}, ${header.state})
|
|
16362
|
+
`);
|
|
16363
|
+
for (const row2 of rows) {
|
|
16364
|
+
process.stdout.write(`${row2.slug} ${row2.lifecycle} ${formatIssueCounts(row2.issues, row2.counts)}
|
|
16365
|
+
`);
|
|
16366
|
+
}
|
|
16367
|
+
}
|
|
16368
|
+
}
|
|
15760
16369
|
|
|
15761
16370
|
// src/commands/new-check.ts
|
|
15762
|
-
import { chmodSync as chmodSync3, existsSync as
|
|
15763
|
-
import { join as
|
|
15764
|
-
var TEMPLATE_PATH2 =
|
|
15765
|
-
var CHECKS_DIR =
|
|
16371
|
+
import { chmodSync as chmodSync3, existsSync as existsSync20, mkdirSync as mkdirSync7, readFileSync as readFileSync26, writeFileSync as writeFileSync13 } from "node:fs";
|
|
16372
|
+
import { join as join25 } from "node:path";
|
|
16373
|
+
var TEMPLATE_PATH2 = join25(packageRoot(import.meta.url), "templates", "custom-check.template.ts");
|
|
16374
|
+
var CHECKS_DIR = join25("scripts", "vinaya-checks");
|
|
15766
16375
|
var USAGE2 = "Usage: vinaya new check <yourname>/<id> (both segments: lowercase letters, digits, hyphens; e.g. myteam/vocab-check)";
|
|
15767
16376
|
function newCheckCommand(args) {
|
|
15768
16377
|
const name = args[0];
|
|
@@ -15779,20 +16388,20 @@ function newCheckCommand(args) {
|
|
|
15779
16388
|
\`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.`);
|
|
15780
16389
|
process.exit(2);
|
|
15781
16390
|
}
|
|
15782
|
-
const scriptsDir =
|
|
15783
|
-
if (!
|
|
16391
|
+
const scriptsDir = join25(process.cwd(), CHECKS_DIR);
|
|
16392
|
+
if (!existsSync20(scriptsDir))
|
|
15784
16393
|
mkdirSync7(scriptsDir, { recursive: true });
|
|
15785
16394
|
const fileStem = name.slice(name.indexOf("/") + 1);
|
|
15786
|
-
const targetPath =
|
|
15787
|
-
if (
|
|
16395
|
+
const targetPath = join25(scriptsDir, `${fileStem}.ts`);
|
|
16396
|
+
if (existsSync20(targetPath)) {
|
|
15788
16397
|
console.error(`Error: ${targetPath} already exists.`);
|
|
15789
16398
|
process.exit(1);
|
|
15790
16399
|
}
|
|
15791
16400
|
const template = readFileSync26(TEMPLATE_PATH2, "utf-8");
|
|
15792
16401
|
const contents = template.split("{{CHECK_NAME}}").join(name);
|
|
15793
|
-
|
|
16402
|
+
writeFileSync13(targetPath, contents, "utf-8");
|
|
15794
16403
|
chmodSync3(targetPath, 493);
|
|
15795
|
-
const relPath =
|
|
16404
|
+
const relPath = join25(CHECKS_DIR, `${fileStem}.ts`);
|
|
15796
16405
|
const registration = JSON.stringify({ checks: { [name]: { run: `./${relPath}`, scope: "diff" } } }, null, 2);
|
|
15797
16406
|
process.stdout.write(`Created ${relPath}
|
|
15798
16407
|
|
|
@@ -15802,10 +16411,10 @@ ${registration}
|
|
|
15802
16411
|
}
|
|
15803
16412
|
|
|
15804
16413
|
// src/commands/new-noop-check.ts
|
|
15805
|
-
import { chmodSync as chmodSync4, existsSync as
|
|
15806
|
-
import { join as
|
|
15807
|
-
var TEMPLATE_PATH3 =
|
|
15808
|
-
var CHECKS_DIR2 =
|
|
16414
|
+
import { chmodSync as chmodSync4, existsSync as existsSync21, mkdirSync as mkdirSync8, readFileSync as readFileSync27, writeFileSync as writeFileSync14 } from "node:fs";
|
|
16415
|
+
import { join as join26 } from "node:path";
|
|
16416
|
+
var TEMPLATE_PATH3 = join26(packageRoot(import.meta.url), "templates", "noop-check.template.ts");
|
|
16417
|
+
var CHECKS_DIR2 = join26("vinaya", "checks");
|
|
15809
16418
|
var USAGE3 = "Usage: vinaya new noop-check <core-check-id> (the exact id of a core check — run `vinaya check --plan` to list them)";
|
|
15810
16419
|
function newNoopCheckCommand(args) {
|
|
15811
16420
|
const name = args[0];
|
|
@@ -15819,19 +16428,19 @@ function newNoopCheckCommand(args) {
|
|
|
15819
16428
|
\`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.`);
|
|
15820
16429
|
process.exit(2);
|
|
15821
16430
|
}
|
|
15822
|
-
const checksDir =
|
|
15823
|
-
if (!
|
|
16431
|
+
const checksDir = join26(process.cwd(), CHECKS_DIR2);
|
|
16432
|
+
if (!existsSync21(checksDir))
|
|
15824
16433
|
mkdirSync8(checksDir, { recursive: true });
|
|
15825
|
-
const targetPath =
|
|
15826
|
-
if (
|
|
16434
|
+
const targetPath = join26(checksDir, `${name}.ts`);
|
|
16435
|
+
if (existsSync21(targetPath)) {
|
|
15827
16436
|
console.error(`Error: ${targetPath} already exists.`);
|
|
15828
16437
|
process.exit(1);
|
|
15829
16438
|
}
|
|
15830
16439
|
const template = readFileSync27(TEMPLATE_PATH3, "utf-8");
|
|
15831
16440
|
const contents = template.split("{{CHECK_NAME}}").join(name);
|
|
15832
|
-
|
|
16441
|
+
writeFileSync14(targetPath, contents, "utf-8");
|
|
15833
16442
|
chmodSync4(targetPath, 493);
|
|
15834
|
-
const relPath =
|
|
16443
|
+
const relPath = join26(CHECKS_DIR2, `${name}.ts`);
|
|
15835
16444
|
const entry2 = {
|
|
15836
16445
|
run: `./${relPath}`,
|
|
15837
16446
|
scope: coreSpec.scope
|
|
@@ -15849,10 +16458,10 @@ ${registration}
|
|
|
15849
16458
|
}
|
|
15850
16459
|
|
|
15851
16460
|
// src/commands/new-role.ts
|
|
15852
|
-
import { existsSync as
|
|
15853
|
-
import { join as
|
|
15854
|
-
var TEMPLATE_PATH4 =
|
|
15855
|
-
var ROLES_DIR =
|
|
16461
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync9, readFileSync as readFileSync28, writeFileSync as writeFileSync15 } from "node:fs";
|
|
16462
|
+
import { join as join27 } from "node:path";
|
|
16463
|
+
var TEMPLATE_PATH4 = join27(packageRoot(import.meta.url), "templates", "role.template.md");
|
|
16464
|
+
var ROLES_DIR = join27("vinaya", "roles");
|
|
15856
16465
|
var USAGE4 = "Usage: vinaya new role <yourname>/<id> (both segments: lowercase letters, digits, hyphens; e.g. acme/qa-lead)";
|
|
15857
16466
|
function newRoleCommand(args) {
|
|
15858
16467
|
const name = args[0];
|
|
@@ -15865,19 +16474,19 @@ function newRoleCommand(args) {
|
|
|
15865
16474
|
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.`);
|
|
15866
16475
|
process.exit(2);
|
|
15867
16476
|
}
|
|
15868
|
-
const rolesDir =
|
|
15869
|
-
if (!
|
|
16477
|
+
const rolesDir = join27(process.cwd(), ROLES_DIR);
|
|
16478
|
+
if (!existsSync22(rolesDir))
|
|
15870
16479
|
mkdirSync9(rolesDir, { recursive: true });
|
|
15871
16480
|
const roleId = name.slice(name.indexOf("/") + 1);
|
|
15872
|
-
const targetPath =
|
|
15873
|
-
if (
|
|
16481
|
+
const targetPath = join27(rolesDir, `${roleId}.md`);
|
|
16482
|
+
if (existsSync22(targetPath)) {
|
|
15874
16483
|
console.error(`Error: ${targetPath} already exists.`);
|
|
15875
16484
|
process.exit(1);
|
|
15876
16485
|
}
|
|
15877
16486
|
const template = readFileSync28(TEMPLATE_PATH4, "utf-8");
|
|
15878
16487
|
const contents = template.split("{{ROLE_ID}}").join(roleId);
|
|
15879
|
-
|
|
15880
|
-
const relPath =
|
|
16488
|
+
writeFileSync15(targetPath, contents, "utf-8");
|
|
16489
|
+
const relPath = join27(ROLES_DIR, `${roleId}.md`);
|
|
15881
16490
|
const registration = JSON.stringify({ roles: { [name]: { contract: `./${relPath}` } } }, null, 2);
|
|
15882
16491
|
process.stdout.write(`Created ${relPath}
|
|
15883
16492
|
|
|
@@ -15887,7 +16496,7 @@ ${registration}
|
|
|
15887
16496
|
}
|
|
15888
16497
|
|
|
15889
16498
|
// src/commands/pr.ts
|
|
15890
|
-
import { execFileSync as
|
|
16499
|
+
import { execFileSync as execFileSync21 } from "node:child_process";
|
|
15891
16500
|
|
|
15892
16501
|
// src/lib/numstat.ts
|
|
15893
16502
|
function summariseNumstat(numstat) {
|
|
@@ -16113,6 +16722,23 @@ function blankUnanchoredStructuralFields(body) {
|
|
|
16113
16722
|
`).map((l) => blankProjectField(blankTierField(l))).join(`
|
|
16114
16723
|
`);
|
|
16115
16724
|
}
|
|
16725
|
+
var OBJECTIVE_MARKER_LINE = /^ {0,3}O\d{1,9}\./;
|
|
16726
|
+
function blankObjectiveMarkerLine(line) {
|
|
16727
|
+
const m = OBJECTIVE_MARKER_LINE.exec(line);
|
|
16728
|
+
if (!m)
|
|
16729
|
+
return line;
|
|
16730
|
+
return " ".repeat(m[0].length) + line.slice(m[0].length);
|
|
16731
|
+
}
|
|
16732
|
+
function blankObjectiveMarkers(body) {
|
|
16733
|
+
const bounds = objectivesSectionBounds(body);
|
|
16734
|
+
if (!bounds)
|
|
16735
|
+
return body;
|
|
16736
|
+
const section = body.slice(bounds.start, bounds.end);
|
|
16737
|
+
const blanked = section.split(`
|
|
16738
|
+
`).map(blankObjectiveMarkerLine).join(`
|
|
16739
|
+
`);
|
|
16740
|
+
return body.slice(0, bounds.start) + blanked + body.slice(bounds.end);
|
|
16741
|
+
}
|
|
16116
16742
|
var CLOSES_REF = /Closes\s*#\d+/i;
|
|
16117
16743
|
var EVIDENCE_HEADING = /^#{1,6}\s/;
|
|
16118
16744
|
var EVIDENCE_HEAD_LINE = /^Head:\s*\S+$/i;
|
|
@@ -16140,6 +16766,7 @@ function buildScanMask(ctx) {
|
|
|
16140
16766
|
masked = blankPremiseValues(masked);
|
|
16141
16767
|
masked = blankTokenReportSection(masked);
|
|
16142
16768
|
masked = blankUnanchoredStructuralFields(masked);
|
|
16769
|
+
masked = blankObjectiveMarkers(masked);
|
|
16143
16770
|
return masked;
|
|
16144
16771
|
}
|
|
16145
16772
|
var TOKEN_WITH_DIGIT = /\S*\p{Nd}\S*/gu;
|
|
@@ -16179,7 +16806,7 @@ var RETRY_CREATE3 = "vinaya pr create --validate-only …";
|
|
|
16179
16806
|
var RETRY_EDIT3 = "vinaya pr edit <n> --validate-only …";
|
|
16180
16807
|
function git4(args) {
|
|
16181
16808
|
try {
|
|
16182
|
-
return
|
|
16809
|
+
return execFileSync21("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
16183
16810
|
} catch {
|
|
16184
16811
|
return "";
|
|
16185
16812
|
}
|
|
@@ -16213,7 +16840,7 @@ function reportPass2(json, command) {
|
|
|
16213
16840
|
function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
|
|
16214
16841
|
const { finalArgs, cleanup: cleanup2 } = resolveShippableArgs(ghArgs, bodyResult);
|
|
16215
16842
|
try {
|
|
16216
|
-
const out =
|
|
16843
|
+
const out = execFileSync21("gh", [...ghCmd, ...finalArgs], { encoding: "utf8", stdio: ["ignore", "pipe", "inherit"] });
|
|
16217
16844
|
const url = out.trim();
|
|
16218
16845
|
if (json)
|
|
16219
16846
|
printJson({ validated: true, written: true, url });
|
|
@@ -16228,7 +16855,7 @@ function runGhWrite2(ghCmd, ghArgs, bodyResult, json) {
|
|
|
16228
16855
|
function fetchPrForgeContext(prRef) {
|
|
16229
16856
|
let viewOut;
|
|
16230
16857
|
try {
|
|
16231
|
-
viewOut =
|
|
16858
|
+
viewOut = execFileSync21("gh", ["pr", "view", prRef, "--json", "headRefName,files"], {
|
|
16232
16859
|
encoding: "utf8",
|
|
16233
16860
|
stdio: ["ignore", "pipe", "pipe"]
|
|
16234
16861
|
});
|
|
@@ -16375,7 +17002,7 @@ function prEditCommand(args) {
|
|
|
16375
17002
|
}
|
|
16376
17003
|
|
|
16377
17004
|
// src/commands/pr-rule.ts
|
|
16378
|
-
import { execFileSync as
|
|
17005
|
+
import { execFileSync as execFileSync22 } from "node:child_process";
|
|
16379
17006
|
import { readFileSync as readFileSync29 } from "node:fs";
|
|
16380
17007
|
var RETRY2 = "vinaya pr rule <pr> --file <ruling.md>";
|
|
16381
17008
|
var NO_CODE_REVIEW_CANDIDATE = "no code-reviewer verdict comment found on this PR";
|
|
@@ -16400,7 +17027,7 @@ function refuseIfCastsAVerdict(body, filePath) {
|
|
|
16400
17027
|
function fetchPrCommentBodies(prRef) {
|
|
16401
17028
|
let out;
|
|
16402
17029
|
try {
|
|
16403
|
-
out =
|
|
17030
|
+
out = execFileSync22("gh", ["pr", "view", prRef, "--json", "comments"], {
|
|
16404
17031
|
encoding: "utf8",
|
|
16405
17032
|
stdio: ["ignore", "pipe", "pipe"]
|
|
16406
17033
|
});
|
|
@@ -16460,22 +17087,22 @@ function prRuleCommand(args) {
|
|
|
16460
17087
|
}
|
|
16461
17088
|
|
|
16462
17089
|
// src/commands/pr-report.ts
|
|
16463
|
-
import { execFileSync as
|
|
16464
|
-
import { existsSync as
|
|
16465
|
-
import { tmpdir as
|
|
16466
|
-
import { join as
|
|
17090
|
+
import { execFileSync as execFileSync23, spawnSync as spawnSync2 } from "node:child_process";
|
|
17091
|
+
import { existsSync as existsSync23, mkdtempSync as mkdtempSync4, readFileSync as readFileSync30, rmSync as rmSync8, writeFileSync as writeFileSync16 } from "node:fs";
|
|
17092
|
+
import { tmpdir as tmpdir6 } from "node:os";
|
|
17093
|
+
import { join as join28 } from "node:path";
|
|
16467
17094
|
var EVIDENCE_START = "<!-- AEG:EVIDENCE:START -->";
|
|
16468
17095
|
var EVIDENCE_END = "<!-- AEG:EVIDENCE:END -->";
|
|
16469
17096
|
function git5(args) {
|
|
16470
17097
|
try {
|
|
16471
|
-
return
|
|
17098
|
+
return execFileSync23("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
16472
17099
|
} catch {
|
|
16473
17100
|
return "";
|
|
16474
17101
|
}
|
|
16475
17102
|
}
|
|
16476
17103
|
function gh3(args) {
|
|
16477
17104
|
try {
|
|
16478
|
-
return
|
|
17105
|
+
return execFileSync23("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
16479
17106
|
} catch (err) {
|
|
16480
17107
|
const stderr = err.stderr;
|
|
16481
17108
|
throw new Error(String(stderr ?? err.message).trim() || "gh command failed");
|
|
@@ -16494,7 +17121,7 @@ class GitCommandError extends Error {
|
|
|
16494
17121
|
}
|
|
16495
17122
|
function gitStrict(args) {
|
|
16496
17123
|
try {
|
|
16497
|
-
return
|
|
17124
|
+
return execFileSync23("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
16498
17125
|
} catch (err) {
|
|
16499
17126
|
const stderr = err.stderr;
|
|
16500
17127
|
throw new GitCommandError(args, String(stderr ?? err.message).trim() || "non-zero exit");
|
|
@@ -16527,10 +17154,27 @@ function computeGroupA() {
|
|
|
16527
17154
|
const numstat = gitStrict(["diff", `${base}...${head}`, "--numstat"]);
|
|
16528
17155
|
return { head, base, numstat };
|
|
16529
17156
|
}
|
|
17157
|
+
function describeGradedBodySource(source) {
|
|
17158
|
+
switch (source) {
|
|
17159
|
+
case "write":
|
|
17160
|
+
return "the drafted body file (`--write`)";
|
|
17161
|
+
case "push":
|
|
17162
|
+
return "the live pull-request body (`--push`)";
|
|
17163
|
+
case "ambient":
|
|
17164
|
+
return "the ambient `PR_BODY` environment (no `--write`/`--push`)";
|
|
17165
|
+
}
|
|
17166
|
+
}
|
|
17167
|
+
var BODY_READING_CHECK_NAMES = new Set(coreCheckRegistry().filter((spec) => {
|
|
17168
|
+
const decl = spec.env?.PR_BODY;
|
|
17169
|
+
return decl === true || typeof decl === "object" && decl !== null && "optional" in decl;
|
|
17170
|
+
}).map((spec) => spec.name));
|
|
17171
|
+
function shouldRenderAsSkipped(outcome, gradedBody) {
|
|
17172
|
+
return gradedBody === "" && outcome.status === "pass" && outcome.errors.length === 0 && BODY_READING_CHECK_NAMES.has(outcome.name);
|
|
17173
|
+
}
|
|
16530
17174
|
function resolveSelfEntry() {
|
|
16531
17175
|
const root = packageRoot(import.meta.url);
|
|
16532
|
-
const dist =
|
|
16533
|
-
return
|
|
17176
|
+
const dist = join28(root, "dist", "index.js");
|
|
17177
|
+
return existsSync23(dist) ? dist : join28(root, "src", "index.ts");
|
|
16534
17178
|
}
|
|
16535
17179
|
var FAILING_STATUSES = new Set(["fail", "error", "timeout"]);
|
|
16536
17180
|
function anyGateFailed(outcomes) {
|
|
@@ -16577,7 +17221,7 @@ function renderGroupA(groupA) {
|
|
|
16577
17221
|
].join(`
|
|
16578
17222
|
`);
|
|
16579
17223
|
}
|
|
16580
|
-
function renderGroupB(outcomes) {
|
|
17224
|
+
function renderGroupB(outcomes, gradedBodySource) {
|
|
16581
17225
|
const sorted = [...outcomes].sort((a, b) => a.name.localeCompare(b.name));
|
|
16582
17226
|
const lines = sorted.flatMap((o) => {
|
|
16583
17227
|
const rows = [`${o.name}: ${o.status}`];
|
|
@@ -16585,8 +17229,18 @@ function renderGroupB(outcomes) {
|
|
|
16585
17229
|
rows.push(` ${e.severity}: ${e.message}`);
|
|
16586
17230
|
return rows;
|
|
16587
17231
|
});
|
|
16588
|
-
return [
|
|
16589
|
-
|
|
17232
|
+
return [
|
|
17233
|
+
"### Group B — attested",
|
|
17234
|
+
"",
|
|
17235
|
+
"`vinaya check --all --diff-only`",
|
|
17236
|
+
"",
|
|
17237
|
+
`Graded body: ${describeGradedBodySource(gradedBodySource)}`,
|
|
17238
|
+
"",
|
|
17239
|
+
"```",
|
|
17240
|
+
lines.join(`
|
|
17241
|
+
`),
|
|
17242
|
+
"```"
|
|
17243
|
+
].join(`
|
|
16590
17244
|
`);
|
|
16591
17245
|
}
|
|
16592
17246
|
var AGENT_COMMAND_TIMEOUT_MS = 30000;
|
|
@@ -16652,14 +17306,14 @@ function renderGroupC(groupC) {
|
|
|
16652
17306
|
`)].join(`
|
|
16653
17307
|
`);
|
|
16654
17308
|
}
|
|
16655
|
-
function buildBlockInner(groupA, gateOutcomes, groupC) {
|
|
17309
|
+
function buildBlockInner(groupA, gateOutcomes, groupC, gradedBodySource) {
|
|
16656
17310
|
return [
|
|
16657
17311
|
`Head: ${groupA.head}`,
|
|
16658
17312
|
`${EVIDENCE_SUMMARY_PREFIX}\`${summariseNumstat(groupA.numstat)}\``,
|
|
16659
17313
|
"",
|
|
16660
17314
|
renderGroupA(groupA),
|
|
16661
17315
|
"",
|
|
16662
|
-
renderGroupB(gateOutcomes),
|
|
17316
|
+
renderGroupB(gateOutcomes, gradedBodySource),
|
|
16663
17317
|
"",
|
|
16664
17318
|
renderGroupC(groupC)
|
|
16665
17319
|
].join(`
|
|
@@ -16846,9 +17500,11 @@ async function buildReport(opts = {}) {
|
|
|
16846
17500
|
const groupA = opts.groupA ?? computeGroupA();
|
|
16847
17501
|
const gateRunner = opts.gateRunner ?? runRealGates;
|
|
16848
17502
|
const gateResult = await gateRunner();
|
|
16849
|
-
const
|
|
16850
|
-
const
|
|
16851
|
-
const
|
|
17503
|
+
const gradedBody = opts.body ?? process.env.PR_BODY ?? "";
|
|
17504
|
+
const gradedBodySource = opts.gradedBodySource ?? "ambient";
|
|
17505
|
+
const outcomes = gateResult.outcomes.filter((o) => o.name !== "evidence-fresh").map((o) => shouldRenderAsSkipped(o, gradedBody) ? { ...o, status: "skipped" } : o);
|
|
17506
|
+
const groupC = opts.groupC ?? computeGroupC(gradedBody);
|
|
17507
|
+
const blockInner = buildBlockInner(groupA, outcomes, groupC, gradedBodySource);
|
|
16852
17508
|
const block = `${EVIDENCE_START}
|
|
16853
17509
|
${blockInner}
|
|
16854
17510
|
${EVIDENCE_END}`;
|
|
@@ -16862,16 +17518,16 @@ ${EVIDENCE_END}`;
|
|
|
16862
17518
|
}
|
|
16863
17519
|
var USAGE5 = "Usage: vinaya pr report [--write <body-file> | --push <pr>] [--phase <phase>] [--role <role>] " + "[--model <id>] [--transcript <path>]";
|
|
16864
17520
|
function ghEditBody(pr, body) {
|
|
16865
|
-
const dir =
|
|
16866
|
-
const tmp =
|
|
16867
|
-
|
|
17521
|
+
const dir = mkdtempSync4(join28(tmpdir6(), "vinaya-pr-report-push-"));
|
|
17522
|
+
const tmp = join28(dir, "body.md");
|
|
17523
|
+
writeFileSync16(tmp, body);
|
|
16868
17524
|
try {
|
|
16869
17525
|
gh3(["pr", "edit", pr, "--body-file", tmp]);
|
|
16870
17526
|
} finally {
|
|
16871
|
-
|
|
17527
|
+
rmSync8(dir, { recursive: true, force: true });
|
|
16872
17528
|
}
|
|
16873
17529
|
}
|
|
16874
|
-
async function prReportCommand(args) {
|
|
17530
|
+
async function prReportCommand(args, testOverrides) {
|
|
16875
17531
|
const writeIdx = args.indexOf("--write");
|
|
16876
17532
|
const writePath = writeIdx !== -1 ? args[writeIdx + 1] : undefined;
|
|
16877
17533
|
const pushIdx = args.indexOf("--push");
|
|
@@ -16914,10 +17570,19 @@ ${USAGE5}`);
|
|
|
16914
17570
|
process.env.PR_NUMBER = pushPr;
|
|
16915
17571
|
process.env.BRANCH = git5(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
16916
17572
|
}
|
|
16917
|
-
const existingForWrite = writePath === undefined ? undefined :
|
|
17573
|
+
const existingForWrite = writePath === undefined ? undefined : existsSync23(writePath) ? readFileSync30(writePath, "utf8") : "";
|
|
17574
|
+
if (writePath !== undefined) {
|
|
17575
|
+
process.env.PR_BODY = existingForWrite;
|
|
17576
|
+
process.env.BRANCH = git5(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
17577
|
+
}
|
|
17578
|
+
const gradedBodySource = pushPr ? "push" : writePath !== undefined ? "write" : "ambient";
|
|
16918
17579
|
let result;
|
|
16919
17580
|
try {
|
|
16920
|
-
result = await buildReport({
|
|
17581
|
+
result = await buildReport({
|
|
17582
|
+
body: preEditBody ?? existingForWrite,
|
|
17583
|
+
gradedBodySource,
|
|
17584
|
+
gateRunner: testOverrides?.gateRunner
|
|
17585
|
+
});
|
|
16921
17586
|
} catch (err) {
|
|
16922
17587
|
if (err instanceof UnresolvableMergeBaseError || err instanceof GitCommandError) {
|
|
16923
17588
|
console.error(`vinaya pr report: refused — ${err.message}`);
|
|
@@ -16958,9 +17623,9 @@ ${USAGE5}`);
|
|
|
16958
17623
|
try {
|
|
16959
17624
|
ghEditBody(pushPr, preEditBody);
|
|
16960
17625
|
} catch (err) {
|
|
16961
|
-
const dir =
|
|
16962
|
-
const savePath =
|
|
16963
|
-
|
|
17626
|
+
const dir = mkdtempSync4(join28(tmpdir6(), "vinaya-pr-report-push-restore-failed-"));
|
|
17627
|
+
const savePath = join28(dir, "pre-edit-body.md");
|
|
17628
|
+
writeFileSync16(savePath, preEditBody);
|
|
16964
17629
|
console.error(`vinaya pr report: self-verification FAILED on PR ${pushPr} AND the restore of its pre-edit body also failed: ${err instanceof Error ? err.message : String(err)}. PR ${pushPr}'s body may now be corrupted — the pre-edit body was saved to ${savePath}; restore it by hand with \`gh pr edit ${pushPr} --body-file ${savePath}\`.`);
|
|
16965
17630
|
process.exit(1);
|
|
16966
17631
|
}
|
|
@@ -16993,7 +17658,7 @@ The AEG:EVIDENCE block was still pushed to PR ${pushPr}.`);
|
|
|
16993
17658
|
transcriptPath,
|
|
16994
17659
|
modelOverride
|
|
16995
17660
|
});
|
|
16996
|
-
|
|
17661
|
+
writeFileSync16(writePath, composeWrittenBody(existing, result.blockInner, tokens));
|
|
16997
17662
|
if (tokens.collected) {
|
|
16998
17663
|
process.stdout.write(`Wrote AEG:EVIDENCE and AEG:TOKENS blocks to ${writePath}
|
|
16999
17664
|
`);
|
|
@@ -17013,7 +17678,7 @@ The AEG:EVIDENCE block was still written to ${writePath}.`);
|
|
|
17013
17678
|
}
|
|
17014
17679
|
|
|
17015
17680
|
// src/commands/pr-verify-evidence.ts
|
|
17016
|
-
import { execFileSync as
|
|
17681
|
+
import { execFileSync as execFileSync24 } from "node:child_process";
|
|
17017
17682
|
|
|
17018
17683
|
// src/commands/pr-verify-evidence-logic.ts
|
|
17019
17684
|
function publishedMergeBase(region) {
|
|
@@ -17100,10 +17765,10 @@ function renderVerdict(verdict) {
|
|
|
17100
17765
|
|
|
17101
17766
|
// src/commands/pr-verify-evidence.ts
|
|
17102
17767
|
function gh4(args) {
|
|
17103
|
-
return
|
|
17768
|
+
return execFileSync24("gh", args, { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
|
|
17104
17769
|
}
|
|
17105
17770
|
function assertCleanWorktree() {
|
|
17106
|
-
const dirty =
|
|
17771
|
+
const dirty = execFileSync24("git", ["status", "--porcelain"], {
|
|
17107
17772
|
encoding: "utf8",
|
|
17108
17773
|
cwd: repoRoot2()
|
|
17109
17774
|
}).trim();
|
|
@@ -17140,7 +17805,7 @@ function assertRepoRootCwd() {
|
|
|
17140
17805
|
process.exit(2);
|
|
17141
17806
|
}
|
|
17142
17807
|
function repoRoot2() {
|
|
17143
|
-
return
|
|
17808
|
+
return execFileSync24("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
17144
17809
|
}
|
|
17145
17810
|
async function prVerifyEvidenceCommand(args) {
|
|
17146
17811
|
const prRef = args[0] && !args[0].startsWith("-") ? args[0] : undefined;
|
|
@@ -17164,7 +17829,7 @@ async function prVerifyEvidenceCommand(args) {
|
|
|
17164
17829
|
process.exit(1);
|
|
17165
17830
|
return;
|
|
17166
17831
|
}
|
|
17167
|
-
const localHead =
|
|
17832
|
+
const localHead = execFileSync24("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim();
|
|
17168
17833
|
if (!forgeHead) {
|
|
17169
17834
|
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.
|
|
17170
17835
|
`);
|
|
@@ -17187,13 +17852,13 @@ async function prVerifyEvidenceCommand(args) {
|
|
|
17187
17852
|
}
|
|
17188
17853
|
|
|
17189
17854
|
// src/commands/quickstart.ts
|
|
17190
|
-
import { execFileSync as
|
|
17191
|
-
import { existsSync as
|
|
17192
|
-
import { join as
|
|
17855
|
+
import { execFileSync as execFileSync25 } from "node:child_process";
|
|
17856
|
+
import { existsSync as existsSync25, readFileSync as readFileSync32 } from "node:fs";
|
|
17857
|
+
import { join as join30 } from "node:path";
|
|
17193
17858
|
|
|
17194
17859
|
// src/lib/doc-owners-write.ts
|
|
17195
|
-
import { existsSync as
|
|
17196
|
-
import { dirname as
|
|
17860
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync10, readFileSync as readFileSync31, writeFileSync as writeFileSync17 } from "node:fs";
|
|
17861
|
+
import { dirname as dirname11, join as join29 } from "node:path";
|
|
17197
17862
|
function bindingLine(glob, pointer) {
|
|
17198
17863
|
return `${glob} ${pointer}`;
|
|
17199
17864
|
}
|
|
@@ -17209,9 +17874,9 @@ function appendDocOwnersBinding(content, glob, pointer) {
|
|
|
17209
17874
|
`;
|
|
17210
17875
|
}
|
|
17211
17876
|
function planDocOwnersBinding(repoRoot3, glob, pointer) {
|
|
17212
|
-
const abs2 =
|
|
17877
|
+
const abs2 = join29(repoRoot3, DOC_OWNERS_PATH);
|
|
17213
17878
|
const line = bindingLine(glob, pointer);
|
|
17214
|
-
if (!
|
|
17879
|
+
if (!existsSync24(abs2)) {
|
|
17215
17880
|
return { action: "create-host", line, path: DOC_OWNERS_PATH };
|
|
17216
17881
|
}
|
|
17217
17882
|
const existing = readFileSync31(abs2, "utf-8");
|
|
@@ -17219,13 +17884,13 @@ function planDocOwnersBinding(repoRoot3, glob, pointer) {
|
|
|
17219
17884
|
return { action: already ? "skip-present" : "append-row", line, path: DOC_OWNERS_PATH };
|
|
17220
17885
|
}
|
|
17221
17886
|
function applyDocOwnersBinding(repoRoot3, plan, glob, pointer) {
|
|
17222
|
-
const abs2 =
|
|
17887
|
+
const abs2 = join29(repoRoot3, DOC_OWNERS_PATH);
|
|
17223
17888
|
if (plan.action === "create-host") {
|
|
17224
|
-
mkdirSync10(
|
|
17225
|
-
|
|
17889
|
+
mkdirSync10(dirname11(abs2), { recursive: true });
|
|
17890
|
+
writeFileSync17(abs2, freshDocOwners(glob, pointer), "utf-8");
|
|
17226
17891
|
} else if (plan.action === "append-row") {
|
|
17227
17892
|
const existing = readFileSync31(abs2, "utf-8");
|
|
17228
|
-
|
|
17893
|
+
writeFileSync17(abs2, appendDocOwnersBinding(existing, glob, pointer), "utf-8");
|
|
17229
17894
|
}
|
|
17230
17895
|
}
|
|
17231
17896
|
function renderDocOwnersBindingDiffLine(plan) {
|
|
@@ -17288,7 +17953,7 @@ function pointerIsMissingLocalFile(repoRoot3, pointer) {
|
|
|
17288
17953
|
if (/^https?:\/\//i.test(pointer))
|
|
17289
17954
|
return false;
|
|
17290
17955
|
const path = pointer.split("#")[0] ?? pointer;
|
|
17291
|
-
return !
|
|
17956
|
+
return !existsSync25(join30(repoRoot3, path));
|
|
17292
17957
|
}
|
|
17293
17958
|
async function bindDocOwnerLoop(deps, repoRoot3) {
|
|
17294
17959
|
let any = false;
|
|
@@ -17355,7 +18020,7 @@ async function registerProjectLoop(deps) {
|
|
|
17355
18020
|
}
|
|
17356
18021
|
}
|
|
17357
18022
|
function readPackageVersion() {
|
|
17358
|
-
const pkg = JSON.parse(readFileSync32(
|
|
18023
|
+
const pkg = JSON.parse(readFileSync32(join30(packageRoot(import.meta.url), "package.json"), "utf-8"));
|
|
17359
18024
|
return pkg.version;
|
|
17360
18025
|
}
|
|
17361
18026
|
function realDeps7() {
|
|
@@ -17387,7 +18052,7 @@ function realDeps7() {
|
|
|
17387
18052
|
};
|
|
17388
18053
|
}
|
|
17389
18054
|
function execGit(repoRoot3, args) {
|
|
17390
|
-
return
|
|
18055
|
+
return execFileSync25("git", args, { cwd: repoRoot3, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
17391
18056
|
}
|
|
17392
18057
|
function errorDetail(err) {
|
|
17393
18058
|
const stderr = err.stderr;
|
|
@@ -17521,11 +18186,11 @@ async function quickstartCommand(args) {
|
|
|
17521
18186
|
}
|
|
17522
18187
|
|
|
17523
18188
|
// src/commands/release.ts
|
|
17524
|
-
import { execFileSync as
|
|
18189
|
+
import { execFileSync as execFileSync26 } from "node:child_process";
|
|
17525
18190
|
function realDeps8() {
|
|
17526
18191
|
const git6 = (args) => {
|
|
17527
18192
|
try {
|
|
17528
|
-
return
|
|
18193
|
+
return execFileSync26("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
17529
18194
|
} catch {
|
|
17530
18195
|
return null;
|
|
17531
18196
|
}
|
|
@@ -17544,7 +18209,7 @@ function realDeps8() {
|
|
|
17544
18209
|
},
|
|
17545
18210
|
fetchOrigin() {
|
|
17546
18211
|
try {
|
|
17547
|
-
|
|
18212
|
+
execFileSync26("git", ["fetch", "origin"], { stdio: "inherit" });
|
|
17548
18213
|
return true;
|
|
17549
18214
|
} catch {
|
|
17550
18215
|
return false;
|
|
@@ -17565,14 +18230,14 @@ function realDeps8() {
|
|
|
17565
18230
|
},
|
|
17566
18231
|
npmWhoami() {
|
|
17567
18232
|
try {
|
|
17568
|
-
|
|
18233
|
+
execFileSync26("npm", ["whoami"], { stdio: "ignore" });
|
|
17569
18234
|
return true;
|
|
17570
18235
|
} catch {
|
|
17571
18236
|
return false;
|
|
17572
18237
|
}
|
|
17573
18238
|
},
|
|
17574
18239
|
runStreamed(cmd, args) {
|
|
17575
|
-
|
|
18240
|
+
execFileSync26(cmd, args, { stdio: "inherit" });
|
|
17576
18241
|
},
|
|
17577
18242
|
tagsAtHead() {
|
|
17578
18243
|
const tags = git6(["tag", "--points-at", "HEAD"]);
|
|
@@ -17583,7 +18248,7 @@ function realDeps8() {
|
|
|
17583
18248
|
},
|
|
17584
18249
|
npmViewVersion(pkg) {
|
|
17585
18250
|
try {
|
|
17586
|
-
const out =
|
|
18251
|
+
const out = execFileSync26("npm", ["view", pkg, "version"], {
|
|
17587
18252
|
encoding: "utf8",
|
|
17588
18253
|
stdio: ["ignore", "pipe", "pipe"]
|
|
17589
18254
|
}).trim();
|
|
@@ -17725,11 +18390,11 @@ async function releaseCommand(args) {
|
|
|
17725
18390
|
}
|
|
17726
18391
|
|
|
17727
18392
|
// src/commands/review-status.ts
|
|
17728
|
-
import { execFileSync as
|
|
18393
|
+
import { execFileSync as execFileSync27 } from "node:child_process";
|
|
17729
18394
|
var MAX_ROUNDS2 = 3;
|
|
17730
18395
|
function fetchPr(prNumber) {
|
|
17731
18396
|
try {
|
|
17732
|
-
const out =
|
|
18397
|
+
const out = execFileSync27("gh", ["pr", "view", prNumber, "--json", "comments,headRefOid,baseRefName"], {
|
|
17733
18398
|
encoding: "utf8",
|
|
17734
18399
|
stdio: ["ignore", "pipe", "pipe"]
|
|
17735
18400
|
});
|
|
@@ -17740,7 +18405,7 @@ function fetchPr(prNumber) {
|
|
|
17740
18405
|
}
|
|
17741
18406
|
function behindBy(base) {
|
|
17742
18407
|
try {
|
|
17743
|
-
const out =
|
|
18408
|
+
const out = execFileSync27("git", ["rev-list", "--count", `HEAD..origin/${base}`], {
|
|
17744
18409
|
encoding: "utf8",
|
|
17745
18410
|
stdio: ["ignore", "pipe", "pipe"]
|
|
17746
18411
|
}).trim();
|
|
@@ -17783,9 +18448,9 @@ async function reviewStatusCommand(args) {
|
|
|
17783
18448
|
|
|
17784
18449
|
// src/commands/studio.ts
|
|
17785
18450
|
import { execFile as execFile5, spawn as spawn3 } from "node:child_process";
|
|
17786
|
-
import { existsSync as
|
|
18451
|
+
import { existsSync as existsSync26, readFileSync as readFileSync33, renameSync as renameSync2 } from "node:fs";
|
|
17787
18452
|
import net from "node:net";
|
|
17788
|
-
import { dirname as
|
|
18453
|
+
import { dirname as dirname12, join as join31 } from "node:path";
|
|
17789
18454
|
import { promisify as promisify5 } from "node:util";
|
|
17790
18455
|
|
|
17791
18456
|
// src/lib/studio-bundle.ts
|
|
@@ -17838,9 +18503,9 @@ class PortFlagError extends Error {
|
|
|
17838
18503
|
function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
|
|
17839
18504
|
let dir = cwd;
|
|
17840
18505
|
for (;; ) {
|
|
17841
|
-
const webDir =
|
|
17842
|
-
const pkgPath =
|
|
17843
|
-
if (
|
|
18506
|
+
const webDir = join31(dir, "apps", "vinaya-studio", "web");
|
|
18507
|
+
const pkgPath = join31(webDir, "package.json");
|
|
18508
|
+
if (existsSync26(pkgPath)) {
|
|
17844
18509
|
try {
|
|
17845
18510
|
const pkg = JSON.parse(readFileSync33(pkgPath, "utf-8"));
|
|
17846
18511
|
if (pkg.name === "@atta/vinaya-studio-web") {
|
|
@@ -17848,15 +18513,15 @@ function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
|
|
|
17848
18513
|
}
|
|
17849
18514
|
} catch {}
|
|
17850
18515
|
}
|
|
17851
|
-
if (
|
|
18516
|
+
if (existsSync26(join31(dir, ".git")))
|
|
17852
18517
|
break;
|
|
17853
|
-
const parent =
|
|
18518
|
+
const parent = dirname12(dir);
|
|
17854
18519
|
if (parent === dir)
|
|
17855
18520
|
break;
|
|
17856
18521
|
dir = parent;
|
|
17857
18522
|
}
|
|
17858
|
-
const standaloneWebDir =
|
|
17859
|
-
if (
|
|
18523
|
+
const standaloneWebDir = join31(packageRoot(moduleUrl), "studio-standalone", "apps", "vinaya-studio", "web");
|
|
18524
|
+
if (existsSync26(join31(standaloneWebDir, "server.js"))) {
|
|
17860
18525
|
return { kind: "package", packageDir: standaloneWebDir };
|
|
17861
18526
|
}
|
|
17862
18527
|
return { kind: "missing" };
|
|
@@ -17876,9 +18541,9 @@ function isPortFree(port) {
|
|
|
17876
18541
|
});
|
|
17877
18542
|
}
|
|
17878
18543
|
function ensureStudioNodeModules(bundleRoot) {
|
|
17879
|
-
const real =
|
|
17880
|
-
const packed =
|
|
17881
|
-
if (!
|
|
18544
|
+
const real = join31(bundleRoot, "node_modules");
|
|
18545
|
+
const packed = join31(bundleRoot, STUDIO_NODE_MODULES_PACKED_DIRNAME);
|
|
18546
|
+
if (!existsSync26(real) && existsSync26(packed)) {
|
|
17882
18547
|
renameSync2(packed, real);
|
|
17883
18548
|
}
|
|
17884
18549
|
}
|
|
@@ -17927,8 +18592,8 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
|
|
|
17927
18592
|
}
|
|
17928
18593
|
return spawnDev(target.webDir, args);
|
|
17929
18594
|
case "package": {
|
|
17930
|
-
const bundleRoot =
|
|
17931
|
-
return spawnStandalone(cwd,
|
|
18595
|
+
const bundleRoot = join31(target.packageDir, "..", "..", "..");
|
|
18596
|
+
return spawnStandalone(cwd, join31(target.packageDir, "server.js"), bundleRoot, explicitPort);
|
|
17932
18597
|
}
|
|
17933
18598
|
case "missing":
|
|
17934
18599
|
console.error("Vinaya Studio isn't available in this install — no published @attalabs/vinaya build bundles the Studio app yet. Inside a checkout that contains Studio's source (apps/vinaya-studio/web), `vinaya studio` runs it directly.");
|
|
@@ -17936,12 +18601,180 @@ async function runStudio(cwd, args, moduleUrl = import.meta.url) {
|
|
|
17936
18601
|
}
|
|
17937
18602
|
}
|
|
17938
18603
|
|
|
18604
|
+
// src/lib/dispatch-task.ts
|
|
18605
|
+
import { execFileSync as execFileSync28 } from "node:child_process";
|
|
18606
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
18607
|
+
import { mkdtempSync as mkdtempSync5, rmSync as rmSync9, writeFileSync as writeFileSync18 } from "node:fs";
|
|
18608
|
+
import { tmpdir as tmpdir7 } from "node:os";
|
|
18609
|
+
import { join as join32 } from "node:path";
|
|
18610
|
+
var DISPATCH_AGENTS = ["claude", "codex", "gemini"];
|
|
18611
|
+
|
|
18612
|
+
class DispatchTaskError extends Error {
|
|
18613
|
+
}
|
|
18614
|
+
function sh5(cmd, args) {
|
|
18615
|
+
return execFileSync28(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
18616
|
+
}
|
|
18617
|
+
function briefHash(brief) {
|
|
18618
|
+
return createHash7("sha256").update(`${brief}
|
|
18619
|
+
`).digest("hex");
|
|
18620
|
+
}
|
|
18621
|
+
function fetchIssueComments2(n) {
|
|
18622
|
+
let out;
|
|
18623
|
+
try {
|
|
18624
|
+
out = sh5("gh", ["issue", "view", String(n), "--json", "comments"]);
|
|
18625
|
+
} catch (err) {
|
|
18626
|
+
throw new DispatchTaskError(`could not fetch Issue #${n}'s comments (\`gh issue view\`) to check for an existing brief — refusing rather than risking a duplicate: ${err instanceof Error ? err.message : String(err)}`);
|
|
18627
|
+
}
|
|
18628
|
+
try {
|
|
18629
|
+
return JSON.parse(out).comments.map((c) => ({
|
|
18630
|
+
body: c.body,
|
|
18631
|
+
url: c.url,
|
|
18632
|
+
author: c.author?.login ?? null
|
|
18633
|
+
}));
|
|
18634
|
+
} catch {
|
|
18635
|
+
throw new DispatchTaskError(`could not parse \`gh issue view ${n} --json comments\` output.`);
|
|
18636
|
+
}
|
|
18637
|
+
}
|
|
18638
|
+
function principalAllowlist2() {
|
|
18639
|
+
return resolvePrincipalAllowlist(loadTrustAnchorConfig());
|
|
18640
|
+
}
|
|
18641
|
+
function fetchIssueBody(n) {
|
|
18642
|
+
try {
|
|
18643
|
+
return JSON.parse(sh5("gh", ["issue", "view", String(n), "--json", "body"])).body;
|
|
18644
|
+
} catch (err) {
|
|
18645
|
+
throw new DispatchTaskError(`could not fetch Issue #${n}'s body (\`gh issue view\`) to resolve its suggested agent-class: ${err instanceof Error ? err.message : String(err)}`);
|
|
18646
|
+
}
|
|
18647
|
+
}
|
|
18648
|
+
function extractAgentClass(rawRationaleField) {
|
|
18649
|
+
const m = /agent-class\**\s*[—–-]\s*(\w+)/i.exec(rawRationaleField);
|
|
18650
|
+
const word = m?.[1]?.toLowerCase();
|
|
18651
|
+
return word !== undefined && isAgentClass(word) ? word : null;
|
|
18652
|
+
}
|
|
18653
|
+
function resolveModelFromRationale(agent, rawRationaleField, explicitModel) {
|
|
18654
|
+
if (explicitModel !== undefined)
|
|
18655
|
+
return explicitModel;
|
|
18656
|
+
const agentClass = rawRationaleField !== undefined ? extractAgentClass(rawRationaleField) : null;
|
|
18657
|
+
if (agentClass === null)
|
|
18658
|
+
return;
|
|
18659
|
+
return resolveClassModel(agent, agentClass) ?? undefined;
|
|
18660
|
+
}
|
|
18661
|
+
function resolveModelForDispatch(agent, issue, explicitModel) {
|
|
18662
|
+
if (explicitModel !== undefined)
|
|
18663
|
+
return explicitModel;
|
|
18664
|
+
const raw = parseRationaleFields(fetchIssueBody(issue)).suggestedAgentClass;
|
|
18665
|
+
return resolveModelFromRationale(agent, raw, undefined);
|
|
18666
|
+
}
|
|
18667
|
+
function findExistingFrozenBrief(n) {
|
|
18668
|
+
return resolveNewestFrozenBrief(fetchIssueComments2(n), principalAllowlist2());
|
|
18669
|
+
}
|
|
18670
|
+
async function withPromptFile2(prompt2, fn) {
|
|
18671
|
+
const dir = mkdtempSync5(join32(tmpdir7(), "vinaya-dispatch-prompt-"));
|
|
18672
|
+
const promptFile = join32(dir, "prompt.md");
|
|
18673
|
+
writeFileSync18(promptFile, prompt2, "utf8");
|
|
18674
|
+
try {
|
|
18675
|
+
return await fn(promptFile);
|
|
18676
|
+
} finally {
|
|
18677
|
+
rmSync9(dir, { recursive: true, force: true });
|
|
18678
|
+
}
|
|
18679
|
+
}
|
|
18680
|
+
function resolveDispatchAuthorization() {
|
|
18681
|
+
const login = currentGhLogin();
|
|
18682
|
+
const allowlist = resolvePrincipalAllowlist(loadTrustAnchorConfig());
|
|
18683
|
+
return { authorized: login !== null && isPrincipal(login, allowlist), login };
|
|
18684
|
+
}
|
|
18685
|
+
var defaultPrepareTaskDeps = {
|
|
18686
|
+
assembleAndRenderBrief,
|
|
18687
|
+
findExistingFrozenBrief,
|
|
18688
|
+
postMarkedComment,
|
|
18689
|
+
resolveDispatchAuthorization
|
|
18690
|
+
};
|
|
18691
|
+
async function prepareTask(input, deps = defaultPrepareTaskDeps) {
|
|
18692
|
+
const { tranche, n, supersede } = input;
|
|
18693
|
+
{
|
|
18694
|
+
const { authorized, login } = deps.resolveDispatchAuthorization();
|
|
18695
|
+
if (!authorized) {
|
|
18696
|
+
throw new DispatchTaskError(login === null ? "could not resolve the identity `gh` is authenticated as — preparing a task is Principal-only and refuses rather than proceeding with an unverified actor." : `\`${login}\` is not on the Principal allowlist — preparing a task is Principal-only.`);
|
|
18697
|
+
}
|
|
18698
|
+
}
|
|
18699
|
+
if (supersede && supersede.reason.trim().length === 0) {
|
|
18700
|
+
throw new DispatchTaskError("--supersede requires --reason <text> — a superseding brief must name why the prior one was wrong, same authorization as a first freeze, never a silent rewrite.");
|
|
18701
|
+
}
|
|
18702
|
+
if (supersede && /[\r\n]/.test(supersede.reason)) {
|
|
18703
|
+
throw new DispatchTaskError("--reason must be a single line — it becomes one line of the frozen comment header, and a newline in it would corrupt every reader's header-line count for this version.");
|
|
18704
|
+
}
|
|
18705
|
+
const result = await deps.assembleAndRenderBrief(tranche, String(n));
|
|
18706
|
+
if (!result.ok) {
|
|
18707
|
+
throw new DispatchTaskError(`cannot dispatch — brief render refused:
|
|
18708
|
+
${result.missing.map((m) => ` - ${m}`).join(`
|
|
18709
|
+
`)}`);
|
|
18710
|
+
}
|
|
18711
|
+
const issue = result.issue;
|
|
18712
|
+
const existing = deps.findExistingFrozenBrief(issue);
|
|
18713
|
+
const hash = briefHash(result.brief);
|
|
18714
|
+
let marker;
|
|
18715
|
+
let commentBody;
|
|
18716
|
+
let version;
|
|
18717
|
+
if (supersede) {
|
|
18718
|
+
if (!existing) {
|
|
18719
|
+
throw new DispatchTaskError(`Task ${n} in tranche \`${tranche}\` has no frozen brief yet — nothing to supersede. Run \`vinaya task brief ${tranche} ${n}\` without --supersede first.`);
|
|
18720
|
+
}
|
|
18721
|
+
version = existing.version + 1;
|
|
18722
|
+
marker = briefMarkerFor(version);
|
|
18723
|
+
commentBody = `Brief hash: ${hash}
|
|
18724
|
+
Supersedes: ${existing.url} — ${supersede.reason}
|
|
18725
|
+
${result.brief}`;
|
|
18726
|
+
} else {
|
|
18727
|
+
if (existing) {
|
|
18728
|
+
throw new DispatchTaskError(`Task ${n} in tranche \`${tranche}\` is already dispatched — see ${existing.url}`);
|
|
18729
|
+
}
|
|
18730
|
+
version = 1;
|
|
18731
|
+
marker = AEG_BRIEF_V1_MARKER;
|
|
18732
|
+
commentBody = `Brief hash: ${hash}
|
|
18733
|
+
${result.brief}`;
|
|
18734
|
+
}
|
|
18735
|
+
if (deps.beforePost) {
|
|
18736
|
+
await deps.beforePost(issue);
|
|
18737
|
+
}
|
|
18738
|
+
const url = deps.postMarkedComment("issue", String(issue), marker, commentBody);
|
|
18739
|
+
return { issue, brief: result.brief, commentUrl: url, version };
|
|
18740
|
+
}
|
|
18741
|
+
var defaultDeps3 = {
|
|
18742
|
+
assembleAndRenderBrief,
|
|
18743
|
+
findExistingFrozenBrief,
|
|
18744
|
+
postMarkedComment,
|
|
18745
|
+
dispatchRole,
|
|
18746
|
+
resolveDispatchAuthorization,
|
|
18747
|
+
resolveModelForDispatch
|
|
18748
|
+
};
|
|
18749
|
+
async function dispatchTask(input, deps = defaultDeps3) {
|
|
18750
|
+
const { tranche, n, agent, model } = input;
|
|
18751
|
+
let resolvedModel;
|
|
18752
|
+
const prep = await prepareTask({ tranche, n }, {
|
|
18753
|
+
assembleAndRenderBrief: deps.assembleAndRenderBrief,
|
|
18754
|
+
findExistingFrozenBrief: deps.findExistingFrozenBrief,
|
|
18755
|
+
postMarkedComment: deps.postMarkedComment,
|
|
18756
|
+
resolveDispatchAuthorization: deps.resolveDispatchAuthorization,
|
|
18757
|
+
beforePost: agent ? async (issue) => {
|
|
18758
|
+
resolvedModel = deps.resolveModelForDispatch(agent, issue, model);
|
|
18759
|
+
} : undefined
|
|
18760
|
+
});
|
|
18761
|
+
if (agent) {
|
|
18762
|
+
await withPromptFile2(prep.brief, (promptFile) => deps.dispatchRole("developer", agent, prep.brief, {
|
|
18763
|
+
task: prep.issue,
|
|
18764
|
+
promptFile,
|
|
18765
|
+
model: resolvedModel
|
|
18766
|
+
}));
|
|
18767
|
+
}
|
|
18768
|
+
return { posted: true, commentUrl: prep.commentUrl, brief: prep.brief };
|
|
18769
|
+
}
|
|
18770
|
+
|
|
17939
18771
|
// src/commands/task.ts
|
|
17940
18772
|
async function taskDispatchCommand(args) {
|
|
17941
18773
|
const trancheSlug = args[0];
|
|
17942
18774
|
const taskIdArg = args[1];
|
|
17943
18775
|
if (!trancheSlug || !taskIdArg || trancheSlug.startsWith("--")) {
|
|
17944
|
-
console.error(`Usage: vinaya task dispatch <tranche> <n> [--agent ${DISPATCH_AGENTS.join(" | ")}] [--model <name>]
|
|
18776
|
+
console.error(`Usage: vinaya task dispatch <tranche> <n> [--agent ${DISPATCH_AGENTS.join(" | ")}] [--model <name>]
|
|
18777
|
+
` + "Deprecated: prefer `vinaya task brief` (preparation only) or `vinaya task run` (the full unattended loop).");
|
|
17945
18778
|
process.exit(2);
|
|
17946
18779
|
}
|
|
17947
18780
|
const n = Number.parseInt(taskIdArg, 10);
|
|
@@ -17978,9 +18811,155 @@ async function taskDispatchCommand(args) {
|
|
|
17978
18811
|
Posted: ${result.commentUrl}
|
|
17979
18812
|
`);
|
|
17980
18813
|
}
|
|
18814
|
+
async function taskBriefCommand(args) {
|
|
18815
|
+
const trancheSlug = args[0];
|
|
18816
|
+
const taskIdArg = args[1];
|
|
18817
|
+
if (!trancheSlug || !taskIdArg || trancheSlug.startsWith("--")) {
|
|
18818
|
+
console.error("Usage: vinaya task brief <tranche> <n> [--supersede --reason <text>]");
|
|
18819
|
+
process.exit(2);
|
|
18820
|
+
}
|
|
18821
|
+
const n = Number.parseInt(taskIdArg, 10);
|
|
18822
|
+
if (!Number.isInteger(n) || String(n) !== taskIdArg) {
|
|
18823
|
+
console.error(`vinaya task brief: task id must be numeric — got "${taskIdArg}".`);
|
|
18824
|
+
process.exit(2);
|
|
18825
|
+
}
|
|
18826
|
+
const rest = args.slice(2);
|
|
18827
|
+
const hasSupersede = rest.includes("--supersede");
|
|
18828
|
+
const reasonIdx = rest.indexOf("--reason");
|
|
18829
|
+
const reason = reasonIdx !== -1 ? rest[reasonIdx + 1] : undefined;
|
|
18830
|
+
if (hasSupersede && !reason) {
|
|
18831
|
+
console.error("vinaya task brief: --supersede requires --reason <text>.");
|
|
18832
|
+
process.exit(2);
|
|
18833
|
+
}
|
|
18834
|
+
if (!hasSupersede && reasonIdx !== -1) {
|
|
18835
|
+
console.error("vinaya task brief: --reason is only meaningful with --supersede.");
|
|
18836
|
+
process.exit(2);
|
|
18837
|
+
}
|
|
18838
|
+
const result = await prepareTask({
|
|
18839
|
+
tranche: trancheSlug,
|
|
18840
|
+
n,
|
|
18841
|
+
supersede: hasSupersede ? { reason } : undefined
|
|
18842
|
+
});
|
|
18843
|
+
process.stdout.write(`${result.brief}
|
|
18844
|
+
`);
|
|
18845
|
+
process.stdout.write(`
|
|
18846
|
+
Posted (v${result.version}): ${result.commentUrl}
|
|
18847
|
+
`);
|
|
18848
|
+
}
|
|
18849
|
+
|
|
18850
|
+
// src/lib/task-run.ts
|
|
18851
|
+
class RunTaskError extends Error {
|
|
18852
|
+
}
|
|
18853
|
+
var ALREADY_DISPATCHED_PATTERN = /is already dispatched/;
|
|
18854
|
+
function isAlreadyDispatchedError(err) {
|
|
18855
|
+
return err instanceof DispatchTaskError && ALREADY_DISPATCHED_PATTERN.test(err.message);
|
|
18856
|
+
}
|
|
18857
|
+
var defaultRunTaskDeps = {
|
|
18858
|
+
prepareTask,
|
|
18859
|
+
assembleAndRenderBrief,
|
|
18860
|
+
developerBranchFor,
|
|
18861
|
+
findOpenPrForBranch,
|
|
18862
|
+
devReviewLoop,
|
|
18863
|
+
resolveRepo: () => resolveRepo()
|
|
18864
|
+
};
|
|
18865
|
+
async function resolvePrUrl(resolveRepo3, prNumber) {
|
|
18866
|
+
const repo = await resolveRepo3().catch(() => null);
|
|
18867
|
+
return repo ? `https://github.com/${repo.owner}/${repo.repo}/pull/${prNumber}` : null;
|
|
18868
|
+
}
|
|
18869
|
+
async function runTask(input, deps = defaultRunTaskDeps) {
|
|
18870
|
+
const { tranche, n, agent } = input;
|
|
18871
|
+
let issue;
|
|
18872
|
+
try {
|
|
18873
|
+
const prep = await deps.prepareTask({ tranche, n });
|
|
18874
|
+
issue = prep.issue;
|
|
18875
|
+
} catch (err) {
|
|
18876
|
+
if (!isAlreadyDispatchedError(err))
|
|
18877
|
+
throw err;
|
|
18878
|
+
const rendered = await deps.assembleAndRenderBrief(tranche, String(n));
|
|
18879
|
+
if (!rendered.ok) {
|
|
18880
|
+
throw new RunTaskError(`runTask: task ${n} in tranche \`${tranche}\` was already dispatched, but re-resolving its Issue number failed:
|
|
18881
|
+
${rendered.missing.map((m) => ` - ${m}`).join(`
|
|
18882
|
+
`)}`);
|
|
18883
|
+
}
|
|
18884
|
+
issue = rendered.issue;
|
|
18885
|
+
}
|
|
18886
|
+
const branch = deps.developerBranchFor(issue);
|
|
18887
|
+
const existingPr = deps.findOpenPrForBranch(branch);
|
|
18888
|
+
if (existingPr) {
|
|
18889
|
+
throw new RunTaskError(`runTask: task ${n} in tranche \`${tranche}\`'s developer branch \`${branch}\` already has an open pull request (#${existingPr.number}) — refusing to start a second developer. Resume the review loop instead: \`vinaya dev-review-loop --resume ${existingPr.number}\`.`);
|
|
18890
|
+
}
|
|
18891
|
+
const loopResult = await deps.devReviewLoop({ task: issue, agent });
|
|
18892
|
+
const prUrl = await resolvePrUrl(deps.resolveRepo, loopResult.prNumber);
|
|
18893
|
+
return { ...loopResult, prUrl };
|
|
18894
|
+
}
|
|
18895
|
+
|
|
18896
|
+
// src/commands/task-run.ts
|
|
18897
|
+
var TASK_RUN_FAILURE_EXIT_CODE = 3;
|
|
18898
|
+
var KNOWN_FLAGS2 = ["--agent"];
|
|
18899
|
+
function parseFlags2(rest) {
|
|
18900
|
+
let agent;
|
|
18901
|
+
const unknown = [];
|
|
18902
|
+
for (let i = 0;i < rest.length; i++) {
|
|
18903
|
+
const a = rest[i];
|
|
18904
|
+
if (a === "--agent")
|
|
18905
|
+
agent = rest[++i];
|
|
18906
|
+
else if (a !== undefined)
|
|
18907
|
+
unknown.push(a);
|
|
18908
|
+
}
|
|
18909
|
+
return { agent, unknown };
|
|
18910
|
+
}
|
|
18911
|
+
async function taskRunCommand(args) {
|
|
18912
|
+
const trancheSlug = args[0];
|
|
18913
|
+
const taskIdArg = args[1];
|
|
18914
|
+
if (!trancheSlug || !taskIdArg || trancheSlug.startsWith("--")) {
|
|
18915
|
+
console.error(`Usage: vinaya task run <tranche> <n> --agent ${DISPATCH_AGENTS.join(" | ")}`);
|
|
18916
|
+
process.exit(2);
|
|
18917
|
+
}
|
|
18918
|
+
const n = Number.parseInt(taskIdArg, 10);
|
|
18919
|
+
if (!Number.isInteger(n) || String(n) !== taskIdArg) {
|
|
18920
|
+
console.error(`vinaya task run: task id must be numeric — got "${taskIdArg}".`);
|
|
18921
|
+
process.exit(2);
|
|
18922
|
+
}
|
|
18923
|
+
const parsed = parseFlags2(args.slice(2));
|
|
18924
|
+
if (parsed.unknown.length > 0) {
|
|
18925
|
+
console.error(`vinaya task run: unrecognized flag${parsed.unknown.length > 1 ? "s" : ""} ${parsed.unknown.map((f) => `'${f}'`).join(", ")} — expected one of ${KNOWN_FLAGS2.join(", ")}`);
|
|
18926
|
+
process.exit(2);
|
|
18927
|
+
}
|
|
18928
|
+
if (!parsed.agent || !DISPATCH_AGENTS.includes(parsed.agent)) {
|
|
18929
|
+
console.error(`vinaya task run: --agent <${DISPATCH_AGENTS.join("|")}> is required.`);
|
|
18930
|
+
process.exit(2);
|
|
18931
|
+
}
|
|
18932
|
+
const agent = parsed.agent;
|
|
18933
|
+
let result;
|
|
18934
|
+
try {
|
|
18935
|
+
result = await runTask({ tranche: trancheSlug, n, agent });
|
|
18936
|
+
} catch (err) {
|
|
18937
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
18938
|
+
process.stderr.write(`Error: ${message}
|
|
18939
|
+
`);
|
|
18940
|
+
process.exit(TASK_RUN_FAILURE_EXIT_CODE);
|
|
18941
|
+
}
|
|
18942
|
+
const prRef = result.prUrl ?? `PR #${result.prNumber}`;
|
|
18943
|
+
const decision = result.finalDecision;
|
|
18944
|
+
if (decision.type === "publish") {
|
|
18945
|
+
process.stdout.write(`${colourLoopLine(`vinaya task run: task ${result.task}, ${prRef} — published`, process.stdout)}
|
|
18946
|
+
`);
|
|
18947
|
+
return;
|
|
18948
|
+
}
|
|
18949
|
+
if (decision.type !== "pause") {
|
|
18950
|
+
process.stderr.write(`Error: vinaya task run: devReviewLoop returned an unexpected final decision type \`${decision.type}\`.
|
|
18951
|
+
`);
|
|
18952
|
+
process.exit(TASK_RUN_FAILURE_EXIT_CODE);
|
|
18953
|
+
}
|
|
18954
|
+
process.stdout.write(`${colourLoopLine(`vinaya task run: task ${result.task}, ${prRef} — paused (${decision.reason})`, process.stdout)}
|
|
18955
|
+
`);
|
|
18956
|
+
process.stdout.write(`Resume with: vinaya dev-review-loop --resume ${result.prNumber}
|
|
18957
|
+
`);
|
|
18958
|
+
process.exit(1);
|
|
18959
|
+
}
|
|
17981
18960
|
|
|
17982
18961
|
// src/commands/upgrade.ts
|
|
17983
|
-
import { existsSync as
|
|
18962
|
+
import { existsSync as existsSync27, readFileSync as readFileSync34, rmSync as rmSync10, writeFileSync as writeFileSync19 } from "node:fs";
|
|
17984
18963
|
import { join as join33 } from "node:path";
|
|
17985
18964
|
function realDeps9() {
|
|
17986
18965
|
return {
|
|
@@ -18000,7 +18979,7 @@ function flags2(args) {
|
|
|
18000
18979
|
}
|
|
18001
18980
|
function readManifest4(repoRoot3) {
|
|
18002
18981
|
const p = join33(repoRoot3, CONFIG_PATH);
|
|
18003
|
-
if (!
|
|
18982
|
+
if (!existsSync27(p))
|
|
18004
18983
|
return { kind: "missing" };
|
|
18005
18984
|
let raw;
|
|
18006
18985
|
try {
|
|
@@ -18028,7 +19007,7 @@ function writeManifestVersion(repoRoot3, manifest) {
|
|
|
18028
19007
|
}
|
|
18029
19008
|
function stripFor(repoRoot3, path, marker, comment) {
|
|
18030
19009
|
const abs2 = resolveManagedBlockPath(repoRoot3, path);
|
|
18031
|
-
if (!
|
|
19010
|
+
if (!existsSync27(abs2))
|
|
18032
19011
|
return { path, marker, comment, present: false, removesHost: false };
|
|
18033
19012
|
const stripped = stripBlockFromContent(readFileSync34(abs2, "utf-8"), marker, comment);
|
|
18034
19013
|
if (stripped === null)
|
|
@@ -18057,7 +19036,7 @@ function planHookRouting(repoRoot3, manifest, recorded, hooksPathValue) {
|
|
|
18057
19036
|
const blocked = [];
|
|
18058
19037
|
for (const b of legacy) {
|
|
18059
19038
|
const abs2 = resolveManagedBlockPath(repoRoot3, b.path);
|
|
18060
|
-
if (!
|
|
19039
|
+
if (!existsSync27(abs2))
|
|
18061
19040
|
continue;
|
|
18062
19041
|
const stripped = stripBlockFromContent(readFileSync34(abs2, "utf-8"), b.marker, b.comment);
|
|
18063
19042
|
if (stripped === null)
|
|
@@ -18094,7 +19073,7 @@ function planUpgrade(ops, repoRoot3, manifest, routing, staleSkillPaths = []) {
|
|
|
18094
19073
|
const entries = [];
|
|
18095
19074
|
const staleFiles = staleSkillPaths.map((path) => ({
|
|
18096
19075
|
path,
|
|
18097
|
-
present:
|
|
19076
|
+
present: existsSync27(join33(repoRoot3, path))
|
|
18098
19077
|
}));
|
|
18099
19078
|
let hasChanges = routing.arm || routing.migratesManifest || routing.strips.some((s) => s.present) || staleFiles.some((f) => f.present);
|
|
18100
19079
|
const ownedFiles = new Set(manifest.files);
|
|
@@ -18103,7 +19082,7 @@ function planUpgrade(ops, repoRoot3, manifest, routing, staleSkillPaths = []) {
|
|
|
18103
19082
|
for (const op of ops) {
|
|
18104
19083
|
if (op.kind === "create-file") {
|
|
18105
19084
|
const abs2 = join33(repoRoot3, op.path);
|
|
18106
|
-
const exists =
|
|
19085
|
+
const exists = existsSync27(abs2);
|
|
18107
19086
|
const owned = ownedFiles.has(op.path) || isDefaultedAgentVendorPath(op.path, manifest);
|
|
18108
19087
|
let action;
|
|
18109
19088
|
let triggerChange;
|
|
@@ -18140,7 +19119,7 @@ function planUpgrade(ops, repoRoot3, manifest, routing, staleSkillPaths = []) {
|
|
|
18140
19119
|
let action;
|
|
18141
19120
|
if (!owned) {
|
|
18142
19121
|
action = "not-installed";
|
|
18143
|
-
} else if (!
|
|
19122
|
+
} else if (!existsSync27(abs2)) {
|
|
18144
19123
|
action = "recreate-host";
|
|
18145
19124
|
hasChanges = true;
|
|
18146
19125
|
} else {
|
|
@@ -18314,7 +19293,7 @@ function applyUpgrade(plan, repoRoot3) {
|
|
|
18314
19293
|
if (!s.present)
|
|
18315
19294
|
continue;
|
|
18316
19295
|
const abs2 = resolveManagedBlockPath(repoRoot3, s.path);
|
|
18317
|
-
if (!
|
|
19296
|
+
if (!existsSync27(abs2))
|
|
18318
19297
|
continue;
|
|
18319
19298
|
const stripped = stripBlockFromContent(readFileSync34(abs2, "utf-8"), s.marker, s.comment);
|
|
18320
19299
|
if (stripped === null)
|
|
@@ -18542,7 +19521,34 @@ function printHelp() {
|
|
|
18542
19521
|
}
|
|
18543
19522
|
|
|
18544
19523
|
// src/index.ts
|
|
18545
|
-
var PACKAGE_ROOT = join34(
|
|
19524
|
+
var PACKAGE_ROOT = join34(dirname13(fileURLToPath2(import.meta.url)), "..");
|
|
19525
|
+
function maybeDeferToAuthorRepoSource() {
|
|
19526
|
+
if (process.env.VINAYA_NO_DEFER === "1")
|
|
19527
|
+
return;
|
|
19528
|
+
if (process.env.GITHUB_ACTIONS)
|
|
19529
|
+
return;
|
|
19530
|
+
const sourceEntry = resolveAuthorRepoSourceEntry(packageRoot(import.meta.url));
|
|
19531
|
+
if (!sourceEntry)
|
|
19532
|
+
return;
|
|
19533
|
+
const bunCheck = spawnSync3("bun", ["--version"], { stdio: "ignore" });
|
|
19534
|
+
if (bunCheck.error || bunCheck.status !== 0) {
|
|
19535
|
+
process.stderr.write(`vinaya: author repo detected, but 'bun' is not on PATH — running the installed build instead of ${sourceEntry}
|
|
19536
|
+
`);
|
|
19537
|
+
return;
|
|
19538
|
+
}
|
|
19539
|
+
process.stderr.write(`vinaya: deferring to source at ${sourceEntry}
|
|
19540
|
+
`);
|
|
19541
|
+
const result = spawnSync3("bun", [sourceEntry, ...process.argv.slice(2)], {
|
|
19542
|
+
stdio: "inherit",
|
|
19543
|
+
env: process.env
|
|
19544
|
+
});
|
|
19545
|
+
if (result.signal) {
|
|
19546
|
+
process.kill(process.pid, result.signal);
|
|
19547
|
+
return;
|
|
19548
|
+
}
|
|
19549
|
+
process.exit(result.status ?? 1);
|
|
19550
|
+
}
|
|
19551
|
+
maybeDeferToAuthorRepoSource();
|
|
18546
19552
|
function readVersion2() {
|
|
18547
19553
|
const pkg = JSON.parse(readFileSync35(join34(PACKAGE_ROOT, "package.json"), "utf-8"));
|
|
18548
19554
|
return pkg.version;
|
|
@@ -18677,8 +19683,12 @@ try {
|
|
|
18677
19683
|
const [subcommand, ...rest] = args;
|
|
18678
19684
|
if (subcommand === "dispatch") {
|
|
18679
19685
|
await taskDispatchCommand(rest);
|
|
19686
|
+
} else if (subcommand === "brief") {
|
|
19687
|
+
await taskBriefCommand(rest);
|
|
19688
|
+
} else if (subcommand === "run") {
|
|
19689
|
+
await taskRunCommand(rest);
|
|
18680
19690
|
} else {
|
|
18681
|
-
console.error(`Unknown 'task' subcommand: ${subcommand ?? "(none)"} (expected 'dispatch')`);
|
|
19691
|
+
console.error(`Unknown 'task' subcommand: ${subcommand ?? "(none)"} (expected 'dispatch', 'brief', or 'run')`);
|
|
18682
19692
|
process.exit(2);
|
|
18683
19693
|
}
|
|
18684
19694
|
break;
|
|
@@ -18703,8 +19713,10 @@ try {
|
|
|
18703
19713
|
await milestoneEditCommand(rest);
|
|
18704
19714
|
} else if (subcommand === "close") {
|
|
18705
19715
|
await milestoneCloseCommand(rest);
|
|
19716
|
+
} else if (subcommand === "status") {
|
|
19717
|
+
await milestoneStatusCommand(rest);
|
|
18706
19718
|
} else {
|
|
18707
|
-
console.error(`Unknown 'milestone' subcommand: ${subcommand ?? "(none)"} (expected create/adopt/edit/close)`);
|
|
19719
|
+
console.error(`Unknown 'milestone' subcommand: ${subcommand ?? "(none)"} (expected create/adopt/edit/close/status)`);
|
|
18708
19720
|
process.exit(2);
|
|
18709
19721
|
}
|
|
18710
19722
|
break;
|