@attalabs/vinaya 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/aeg-root/contracts/architect-planner.md +5 -5
- package/aeg-root/contracts/developer-reviewer.md +7 -7
- package/aeg-root/contracts/planner-developer.md +142 -0
- package/aeg-root/contracts/reviewer-archivist.md +2 -2
- package/aeg-root/contracts/tranche-archivist-planner.md +12 -11
- package/aeg-root/enforcement.md +15 -5
- package/aeg-root/process.md +53 -75
- package/aeg-root/roles/archivist.md +3 -3
- package/aeg-root/roles/developer.md +10 -10
- package/aeg-root/roles/planner.md +88 -23
- package/aeg-root/roles/principal.md +11 -11
- package/aeg-root/roles/reviewer.md +10 -10
- package/aeg-root/roles/security.md +7 -7
- package/aeg-root/roles/tranche-archivist.md +2 -2
- package/aeg-root/skills/aeg/SKILL.md +5 -5
- package/aeg-root/skills/aeg-roles/SKILL.md +7 -7
- package/aeg-root/state-machine.md +35 -34
- package/aeg-root/task-model.md +3 -3
- package/aeg-root/templates/brief-template.md +2 -2
- package/aeg-root/templates/issue-rationale-template.md +3 -3
- package/aeg-root/tranche-model.md +21 -21
- package/dist/checks/bin/check-body-bare-digits.js +116 -19
- package/dist/checks/bin/check-branch-topology.js +116 -19
- package/dist/checks/bin/check-brief-shape.js +121 -1101
- package/dist/checks/bin/check-changeset-coverage.js +877 -26
- package/dist/checks/bin/check-closes-n.js +116 -19
- package/dist/checks/bin/check-coherence.js +116 -19
- package/dist/checks/bin/check-dead-branch-push.js +116 -19
- package/dist/checks/bin/check-dispatch-readiness.js +119 -22
- package/dist/checks/bin/check-doc-coverage-push.js +877 -26
- package/dist/checks/bin/check-doc-coverage.js +879 -28
- package/dist/checks/bin/check-doctrine-no-procedures.js +116 -19
- package/dist/checks/bin/check-doctrine-portability.js +877 -26
- package/dist/checks/bin/check-evidence-fresh.js +116 -19
- package/dist/checks/bin/check-exec-bits.js +877 -26
- package/dist/checks/bin/check-first-push-dispatch.js +116 -19
- package/dist/checks/bin/check-issue-assignment.js +116 -19
- package/dist/checks/bin/check-main-branch-refusal.js +117 -20
- package/dist/checks/bin/check-no-disk-state.js +116 -19
- package/dist/checks/bin/check-pr-premise-reassert.js +5401 -0
- package/dist/checks/bin/check-pr-report-density.js +116 -19
- package/dist/checks/bin/check-quoted-command.js +875 -24
- package/dist/checks/bin/check-reader-resolvable-prose.js +875 -24
- package/dist/checks/bin/check-registry-gates.js +147 -21
- package/dist/checks/bin/check-retired-vocabulary.js +875 -24
- package/dist/checks/bin/check-review-gate.js +116 -19
- package/dist/checks/bin/check-single-plan-pr.js +116 -19
- package/dist/checks/bin/check-surface-scope.js +5722 -0
- package/dist/checks/bin/check-test-plan.js +116 -19
- package/dist/checks/bin/check-token-collection-wired.js +116 -19
- package/dist/checks/bin/check-token-report.js +128 -26
- package/dist/checks/bin/check-workspace-escape.js +875 -24
- package/dist/index.js +1002 -214
- package/package.json +1 -1
- package/aeg-root/contracts/brief-developer.md +0 -141
- package/aeg-root/contracts/planner-brief.md +0 -143
- package/aeg-root/roles/brief-author.md +0 -116
- package/aeg-root/skills/brief-authoring/SKILL.md +0 -509
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { readFileSync as readFileSync35 } from "node:fs";
|
|
5
|
-
import { dirname as
|
|
5
|
+
import { dirname as dirname12, join as join34 } from "node:path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
7
7
|
|
|
8
8
|
// src/commands/archive.ts
|
|
@@ -374,7 +374,7 @@ var LABELS = [
|
|
|
374
374
|
id: "vinaya/needs:brief-correction",
|
|
375
375
|
category: "needs",
|
|
376
376
|
form: "literal",
|
|
377
|
-
carries: "Waiting on the
|
|
377
|
+
carries: "Waiting on the Planner — the brief contradicts the surface it describes."
|
|
378
378
|
},
|
|
379
379
|
{
|
|
380
380
|
key: "waiver-docs",
|
|
@@ -2460,6 +2460,11 @@ function readTierFromPrBody(prBody) {
|
|
|
2460
2460
|
const t = Number(m[4]);
|
|
2461
2461
|
return t === 0 || t === 1 || t === 3 ? t : null;
|
|
2462
2462
|
}
|
|
2463
|
+
function applyTierFloor(declared, derivedFloor) {
|
|
2464
|
+
if (declared === null)
|
|
2465
|
+
return derivedFloor;
|
|
2466
|
+
return declared >= derivedFloor ? declared : derivedFloor;
|
|
2467
|
+
}
|
|
2463
2468
|
var OVERRIDE_BODY_TOKEN = `[${label("override-docs")}]`;
|
|
2464
2469
|
// ../../packages/aeg-core/src/objectives.ts
|
|
2465
2470
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -2523,7 +2528,7 @@ function objectivesOf(body) {
|
|
|
2523
2528
|
continue;
|
|
2524
2529
|
}
|
|
2525
2530
|
if (hasBacktickedPath(text) && wordCount(stripObjectiveBackticks(text)) < MIN_WORDS_OUTSIDE_BACKTICKS) {
|
|
2526
|
-
errors.push(`O${n} is little more than a file path — an objective states an observable outcome, never a bare path (the
|
|
2531
|
+
errors.push(`O${n} is little more than a file path — an objective states an observable outcome, never a bare path (the Planner maps it to files).`);
|
|
2527
2532
|
continue;
|
|
2528
2533
|
}
|
|
2529
2534
|
objectives.push({ id: `O${n}`, text });
|
|
@@ -2691,18 +2696,14 @@ function checkTestPlanExclusivity(prBody) {
|
|
|
2691
2696
|
function checkPrincipalPlaceholder(prBody) {
|
|
2692
2697
|
const region = testPlanRegion(prBody);
|
|
2693
2698
|
const lineRe = /^-\s*\[[ xX]\]\s*\*{2}\[principal\]\*{2}(.*)$/gim;
|
|
2699
|
+
const errors = [];
|
|
2694
2700
|
for (const m of region.matchAll(lineRe)) {
|
|
2695
2701
|
const content = m[1] ?? "";
|
|
2696
2702
|
if (/^\s*None\b/i.test(content)) {
|
|
2697
|
-
|
|
2698
|
-
status: "fail",
|
|
2699
|
-
errors: [
|
|
2700
|
-
'brief-validation Test Plan shape: a `**[principal]**` checkbox item is a "None" placeholder — if there is no principal-runnable surface, omit the item entirely; an untickable placeholder box blocks the merge gate forever.'
|
|
2701
|
-
]
|
|
2702
|
-
};
|
|
2703
|
+
errors.push(`brief-validation Test Plan shape: a \`**[principal]**\` checkbox item is a "None" placeholder ("${content.trim()}") — if there is no principal-runnable surface, omit the item entirely; an untickable placeholder box blocks the merge gate forever.`);
|
|
2703
2704
|
}
|
|
2704
2705
|
}
|
|
2705
|
-
return { status: "pass", errors: [] };
|
|
2706
|
+
return errors.length > 0 ? { status: "fail", errors } : { status: "pass", errors: [] };
|
|
2706
2707
|
}
|
|
2707
2708
|
function checkPremiseCoverage(prBody, surfaceFiles) {
|
|
2708
2709
|
if (surfaceFiles.length === 0)
|
|
@@ -2828,6 +2829,18 @@ function extractFencedBlocks(text) {
|
|
|
2828
2829
|
}
|
|
2829
2830
|
return blocks;
|
|
2830
2831
|
}
|
|
2832
|
+
var AEG_BRIEF_V1_MARKER = "<!-- aeg:brief:v1 -->";
|
|
2833
|
+
function contentAfterTwoLines(body) {
|
|
2834
|
+
const first = body.indexOf(`
|
|
2835
|
+
`);
|
|
2836
|
+
if (first === -1)
|
|
2837
|
+
return "";
|
|
2838
|
+
const second = body.indexOf(`
|
|
2839
|
+
`, first + 1);
|
|
2840
|
+
if (second === -1)
|
|
2841
|
+
return "";
|
|
2842
|
+
return body.slice(second + 1);
|
|
2843
|
+
}
|
|
2831
2844
|
// ../../packages/aeg-core/src/doctrine-portability.ts
|
|
2832
2845
|
var EXEMPT_LITERALS = new Set([
|
|
2833
2846
|
"path/inside/the/shipped/diff.ts",
|
|
@@ -2909,7 +2922,9 @@ function partWordCount(text) {
|
|
|
2909
2922
|
}
|
|
2910
2923
|
function looksLikeFilePath(entry2) {
|
|
2911
2924
|
const stripped = entry2.replace(/\/\*\*?$/, "");
|
|
2912
|
-
|
|
2925
|
+
const lastSegment = stripped.split("/").pop() ?? stripped;
|
|
2926
|
+
const extMatch = /\.[A-Za-z0-9]{1,6}$/.exec(lastSegment);
|
|
2927
|
+
return extMatch !== null && extMatch.index > 0;
|
|
2913
2928
|
}
|
|
2914
2929
|
function splitGlobList(raw) {
|
|
2915
2930
|
return raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
@@ -2940,6 +2955,23 @@ function parseIssueSurface(body) {
|
|
|
2940
2955
|
return { ok: false, errors };
|
|
2941
2956
|
return { ok: true, value: { in: inGlobs, out: outGlobs } };
|
|
2942
2957
|
}
|
|
2958
|
+
function globCoversPath(glob, path) {
|
|
2959
|
+
const g = glob.replace(/\/\*\*?$/, "").replace(/\/+$/, "");
|
|
2960
|
+
const p = path.replace(/\/+$/, "");
|
|
2961
|
+
return g === p || g.startsWith(`${p}/`) || p.startsWith(`${g}/`);
|
|
2962
|
+
}
|
|
2963
|
+
function checkSurfaceGlobsResolve(body, resolvesToFile) {
|
|
2964
|
+
const surface = parseIssueSurface(body);
|
|
2965
|
+
if (!surface.ok)
|
|
2966
|
+
return { status: "pass", errors: [] };
|
|
2967
|
+
const errors = [];
|
|
2968
|
+
for (const glob of surface.value.in) {
|
|
2969
|
+
if (!resolvesToFile(glob)) {
|
|
2970
|
+
errors.push(`issue-validation Surface: \`${glob}\` in \`## Surface\`'s \`in:\` list matches no tracked file — a Surface that cannot resolve cannot render a brief's file list.`);
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
return { status: errors.length > 0 ? "fail" : "pass", errors };
|
|
2974
|
+
}
|
|
2943
2975
|
var ISSUE_PART_LINE_RE = /^Part\s+(\d+)\s*\(([^)]*)\)\s*[-—–]\s*(.*)$/i;
|
|
2944
2976
|
function parseIssueParts(body) {
|
|
2945
2977
|
const section = topLevelSectionText(body, "Parts");
|
|
@@ -2975,6 +3007,22 @@ function parseIssueParts(body) {
|
|
|
2975
3007
|
return { ok: false, errors };
|
|
2976
3008
|
return { ok: true, value: parts };
|
|
2977
3009
|
}
|
|
3010
|
+
function checkPartsCiteDefinedObjectives(body) {
|
|
3011
|
+
const parts = parseIssueParts(body);
|
|
3012
|
+
const objectives = objectivesOf(body);
|
|
3013
|
+
if (!parts.ok || !objectives.ok)
|
|
3014
|
+
return { status: "pass", errors: [] };
|
|
3015
|
+
const definedIds = new Set(objectives.objectives.map((o) => Number.parseInt(o.id.slice(1), 10)));
|
|
3016
|
+
const errors = [];
|
|
3017
|
+
for (const part of parts.value) {
|
|
3018
|
+
for (const objectiveId of part.objectiveIds) {
|
|
3019
|
+
if (!definedIds.has(objectiveId)) {
|
|
3020
|
+
errors.push(`issue-validation Parts: Part ${part.n} cites O${objectiveId}, which the Issue's own \`## Objectives\` section does not define.`);
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
return { status: errors.length > 0 ? "fail" : "pass", errors };
|
|
3025
|
+
}
|
|
2978
3026
|
var ISSUE_PRINCIPAL_LINE_RE = /^-\s*\[[ xX]\]\s*\*{2}\[principal\]\*{2}(.*)$/gim;
|
|
2979
3027
|
var UNIT_TESTS_ONLY_SENTINEL_RE = /(?:\*\*)?Test plan(?:\*\*)?\s*:\s*(?:\*\*)?\s*unit-tests-only/i;
|
|
2980
3028
|
function parseIssueTestPlan(body) {
|
|
@@ -3055,15 +3103,23 @@ function namesPath(text, path) {
|
|
|
3055
3103
|
return false;
|
|
3056
3104
|
}
|
|
3057
3105
|
var BLAST_RADIUS_ACK_RE = /(?:\*\*)?blast-radius-ack(?:\*\*)?\s*[:—–-]/i;
|
|
3058
|
-
function checkBlastRadiusScope(body, _labels, sharedPackages, projectPaths) {
|
|
3106
|
+
function checkBlastRadiusScope(body, _labels, sharedPackages, projectPaths, issueNumber = null) {
|
|
3059
3107
|
if (sharedPackages.length === 0)
|
|
3060
3108
|
return { status: "pass", errors: [] };
|
|
3061
|
-
const
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3109
|
+
const decideFromSurface = issueNumber !== null && issueNumber >= BRIEF_SECTIONS_SINCE_ISSUE;
|
|
3110
|
+
let named;
|
|
3111
|
+
if (decideFromSurface) {
|
|
3112
|
+
const surface = parseIssueSurface(body);
|
|
3113
|
+
const surfaceIn = surface.ok ? surface.value.in : [];
|
|
3114
|
+
named = sharedPackages.filter((d) => surfaceIn.some((glob) => globCoversPath(glob, d)));
|
|
3115
|
+
} else {
|
|
3116
|
+
const text = [
|
|
3117
|
+
rationaleFieldText(PATH_TEXT(body), "Boundary"),
|
|
3118
|
+
rationaleFieldText(PATH_TEXT(body), "Project\\(s\\)|Project(?:s)?\\s*\\+|blast radius")
|
|
3119
|
+
].join(`
|
|
3065
3120
|
`);
|
|
3066
|
-
|
|
3121
|
+
named = sharedPackages.filter((d) => namesPath(text, d));
|
|
3122
|
+
}
|
|
3067
3123
|
if (named.length === 0)
|
|
3068
3124
|
return { status: "pass", errors: [] };
|
|
3069
3125
|
const registered = new Set(projectPaths.map((p) => p.name.trim()));
|
|
@@ -3103,6 +3159,7 @@ function rationaleFieldText(text, labelPattern) {
|
|
|
3103
3159
|
return m ? m[0] : "";
|
|
3104
3160
|
}
|
|
3105
3161
|
var DOC_PATH_RE = /(?:(?:aeg-root|apps|packages|specs|docs|tools|\.claude|\.github)\/[\w./@-]*\.(?:md|mdx)|\.claude\/(?:skills|rules)\/[\w./-]+|\b(?:docs-index|decisions|projects|state-machine|enforcement|process|README|CLAUDE)\.md\b|\b[\w-]+-(?:spec|decisions|backlog)\.md\b)/i;
|
|
3162
|
+
var DOC_PATH_RE_GLOBAL = new RegExp(DOC_PATH_RE.source, "gi");
|
|
3106
3163
|
var NO_DOC_SURFACE_RE = /(?:\*\*)?\s*no-doc-surface/i;
|
|
3107
3164
|
function checkRationaleNamesDocs(body) {
|
|
3108
3165
|
const text = PATH_TEXT(body);
|
|
@@ -3119,6 +3176,26 @@ function checkRationaleNamesDocs(body) {
|
|
|
3119
3176
|
]
|
|
3120
3177
|
};
|
|
3121
3178
|
}
|
|
3179
|
+
function checkDocsWithinSurface(body, issueNumber) {
|
|
3180
|
+
if (issueNumber !== null && issueNumber < BRIEF_SECTIONS_SINCE_ISSUE)
|
|
3181
|
+
return { status: "pass", errors: [] };
|
|
3182
|
+
const surface = parseIssueSurface(body);
|
|
3183
|
+
if (!surface.ok)
|
|
3184
|
+
return { status: "pass", errors: [] };
|
|
3185
|
+
const text = PATH_TEXT(body);
|
|
3186
|
+
const scope = rationaleFieldText(text, "Docs to keep coherent|§7");
|
|
3187
|
+
if (NO_DOC_SURFACE_RE.test(scope))
|
|
3188
|
+
return { status: "pass", errors: [] };
|
|
3189
|
+
const pointers = [...new Set([...scope.matchAll(DOC_PATH_RE_GLOBAL)].map((m) => m[0]))];
|
|
3190
|
+
const errors = [];
|
|
3191
|
+
for (const pointer of pointers) {
|
|
3192
|
+
const excludingGlob = surface.value.out.find((glob) => globCoversPath(glob, pointer));
|
|
3193
|
+
if (excludingGlob) {
|
|
3194
|
+
errors.push(`issue-validation Docs to keep coherent: \`${pointer}\` falls inside \`## Surface\`'s \`out:\` glob \`${excludingGlob}\` — this task's own declared surface explicitly excludes the doc it claims to keep coherent.`);
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
return { status: errors.length > 0 ? "fail" : "pass", errors };
|
|
3198
|
+
}
|
|
3122
3199
|
|
|
3123
3200
|
// ../../packages/aeg-core/src/coherence-checks.ts
|
|
3124
3201
|
var R1_GRANDFATHERED_ISSUES = new Set([279, 280, 281, 282]);
|
|
@@ -3478,8 +3555,31 @@ function parseRationaleFields(body) {
|
|
|
3478
3555
|
if (text)
|
|
3479
3556
|
out[key] = text;
|
|
3480
3557
|
}
|
|
3558
|
+
return { ...out, declaredTier: readTierFromPrBody(body) };
|
|
3559
|
+
}
|
|
3560
|
+
function extractBoundaryFilePaths(boundaryText) {
|
|
3561
|
+
const out = [];
|
|
3562
|
+
const seen = new Set;
|
|
3563
|
+
for (const match of boundaryText.matchAll(/`([^`\n]+)`/g)) {
|
|
3564
|
+
const token = match[1].trim();
|
|
3565
|
+
if (token.length === 0 || token.includes(" ") || token.includes("*"))
|
|
3566
|
+
continue;
|
|
3567
|
+
if (!/\.[A-Za-z0-9]{1,6}$/.test(token))
|
|
3568
|
+
continue;
|
|
3569
|
+
if (seen.has(token))
|
|
3570
|
+
continue;
|
|
3571
|
+
seen.add(token);
|
|
3572
|
+
out.push(token);
|
|
3573
|
+
}
|
|
3481
3574
|
return out;
|
|
3482
3575
|
}
|
|
3576
|
+
function admittedSurfaceFiles(surfaceFiles, surface) {
|
|
3577
|
+
return surfaceFiles.filter((f) => {
|
|
3578
|
+
const admitted = surface.in.some((g) => globCoversPath(g, f.path));
|
|
3579
|
+
const excluded = surface.out.some((g) => globCoversPath(g, f.path));
|
|
3580
|
+
return admitted && !excluded;
|
|
3581
|
+
});
|
|
3582
|
+
}
|
|
3483
3583
|
function bulletList(items) {
|
|
3484
3584
|
return items.map((i) => `- ${i}`).join(`
|
|
3485
3585
|
`);
|
|
@@ -3491,7 +3591,8 @@ function renderHeader(facts, template) {
|
|
|
3491
3591
|
const introMatch = template.match(/^You are the AEG Developer\.[^\n]*$/m);
|
|
3492
3592
|
const intro = introMatch ? introMatch[0].replace(/\s*\[[^\]]*\]/g, "").replace(/\bBoth mandatory\.$/, "Mandatory.").trim() : "You are the AEG Developer. Read `aeg-root/roles/developer.md` first. Mandatory.";
|
|
3493
3593
|
const reason = facts.rationale.suggestedAgentClass ?? "";
|
|
3494
|
-
const
|
|
3594
|
+
const derivedFloor = deriveTierFromDiff(facts.surfaceFiles.map((f) => f.path));
|
|
3595
|
+
const tier = applyTierFloor(facts.rationale.declaredTier ?? null, derivedFloor);
|
|
3495
3596
|
const lines = [
|
|
3496
3597
|
"**For:** [model] (coding-agent CLI on a dev machine, dispatched locally, unattended)",
|
|
3497
3598
|
`**Reason:** ${reason}`,
|
|
@@ -3575,7 +3676,7 @@ function renderSection5(facts) {
|
|
|
3575
3676
|
"**Step 0 (mandatory, verbatim):**",
|
|
3576
3677
|
"",
|
|
3577
3678
|
"```",
|
|
3578
|
-
`git worktree add .worktrees/task/${facts.trancheSlug}/${facts.taskId} -b task/${facts.trancheSlug}/${facts.taskId} origin/main && cd .worktrees/task/${facts.trancheSlug}/${facts.taskId} && bun install --frozen-lockfile --silent`,
|
|
3679
|
+
`git worktree add .worktrees/task/${facts.trancheSlug}/${facts.taskId} -b task/${facts.trancheSlug}/${facts.taskId} --no-track origin/main && cd .worktrees/task/${facts.trancheSlug}/${facts.taskId} && git config push.autoSetupRemote true && bun install --frozen-lockfile --silent`,
|
|
3579
3680
|
"```",
|
|
3580
3681
|
"",
|
|
3581
3682
|
"1. Clean status; parent `origin/main`; branch suffix literal-matches the task id.",
|
|
@@ -3746,22 +3847,27 @@ function renderBrief(facts, template) {
|
|
|
3746
3847
|
if (!facts.rationale[key])
|
|
3747
3848
|
missing.push(RATIONALE_FIELD_NAMES[key]);
|
|
3748
3849
|
}
|
|
3850
|
+
const filteredSurfaceFiles = admittedSurfaceFiles(facts.surfaceFiles, facts.surface);
|
|
3851
|
+
if (facts.surface.in.length > 0 && facts.surfaceFiles.length > 0 && filteredSurfaceFiles.length === 0) {
|
|
3852
|
+
missing.push("Surface map (every file the Boundary named is excluded by the Issue's own `## Surface` `in:`/`out:` globs — Boundary and Surface disagree; fix the Issue rather than render an empty surface map)");
|
|
3853
|
+
}
|
|
3749
3854
|
if (missing.length > 0)
|
|
3750
3855
|
return { ok: false, missing };
|
|
3751
|
-
const
|
|
3856
|
+
const scopedFacts = { ...facts, surfaceFiles: filteredSurfaceFiles };
|
|
3857
|
+
const section7Pointers = facts.docOwnersContent ? deriveSection7Pointers(filteredSurfaceFiles.map((f) => f.path), facts.docOwnersContent) : [];
|
|
3752
3858
|
const brief = [
|
|
3753
|
-
renderHeader(
|
|
3859
|
+
renderHeader(scopedFacts, template),
|
|
3754
3860
|
"",
|
|
3755
3861
|
...facts.objectives.length > 0 ? [renderObjectives(facts.objectives), ""] : [],
|
|
3756
3862
|
renderSection2(facts),
|
|
3757
3863
|
"",
|
|
3758
3864
|
renderSection3(facts),
|
|
3759
3865
|
"",
|
|
3760
|
-
renderSection4(
|
|
3866
|
+
renderSection4(scopedFacts),
|
|
3761
3867
|
"",
|
|
3762
3868
|
renderSection5(facts),
|
|
3763
3869
|
"",
|
|
3764
|
-
renderSection6(
|
|
3870
|
+
renderSection6(scopedFacts),
|
|
3765
3871
|
"",
|
|
3766
3872
|
renderSection7(section7Pointers),
|
|
3767
3873
|
"",
|
|
@@ -4112,6 +4218,25 @@ function assessRound(state, observations) {
|
|
|
4112
4218
|
return assessGate(state, observations);
|
|
4113
4219
|
return assessVerdicts(state, observations);
|
|
4114
4220
|
}
|
|
4221
|
+
// ../../packages/aeg-core/src/dev-review-loop/render-summary.ts
|
|
4222
|
+
function confidenceCell(confidence) {
|
|
4223
|
+
if (confidence === null)
|
|
4224
|
+
return "—";
|
|
4225
|
+
if (confidence === "absent")
|
|
4226
|
+
return "absent";
|
|
4227
|
+
return `${confidence.value}%`;
|
|
4228
|
+
}
|
|
4229
|
+
function row(record) {
|
|
4230
|
+
const counts = SEVERITY_COLUMNS.map((key) => String(record.countsBySeverity[key] ?? 0));
|
|
4231
|
+
return `| ${record.round} | ${counts.join(" | ")} | ${confidenceCell(record.confidence)} | ${record.outcome} |`;
|
|
4232
|
+
}
|
|
4233
|
+
function renderSummary(journal) {
|
|
4234
|
+
const header = `| round | ${SEVERITY_COLUMNS.join(" | ")} | confidence | outcome |`;
|
|
4235
|
+
const divider = `| --- | ${SEVERITY_COLUMNS.map(() => "---").join(" | ")} | --- | --- |`;
|
|
4236
|
+
const rows = journal.rounds.map(row);
|
|
4237
|
+
return [header, divider, ...rows].join(`
|
|
4238
|
+
`);
|
|
4239
|
+
}
|
|
4115
4240
|
// ../../packages/aeg-core/src/log/envelope.ts
|
|
4116
4241
|
import { createHash as createHash3 } from "node:crypto";
|
|
4117
4242
|
|
|
@@ -4119,7 +4244,6 @@ import { createHash as createHash3 } from "node:crypto";
|
|
|
4119
4244
|
import { z } from "zod";
|
|
4120
4245
|
var ROLE_VALUES = [
|
|
4121
4246
|
"planner",
|
|
4122
|
-
"brief-author",
|
|
4123
4247
|
"developer",
|
|
4124
4248
|
"code-reviewer",
|
|
4125
4249
|
"security",
|
|
@@ -4193,18 +4317,20 @@ var dispatchShared = {
|
|
|
4193
4317
|
round: z.number().int().optional(),
|
|
4194
4318
|
effect_id: z.string()
|
|
4195
4319
|
};
|
|
4320
|
+
var dispatchUsageField = z.object({ input: z.number().nonnegative(), output: z.number().nonnegative() }).strict().nullable();
|
|
4196
4321
|
var DispatchEventSchema = z.discriminatedUnion("event", [
|
|
4197
4322
|
z.object({ ...dispatchShared, event: z.literal("dispatched"), prompt_hash: z.string() }).strict(),
|
|
4198
4323
|
z.object({
|
|
4199
4324
|
...dispatchShared,
|
|
4200
4325
|
event: z.literal("outcome_received"),
|
|
4201
4326
|
outcome: DispatchOutcomeSchema,
|
|
4202
|
-
usage:
|
|
4327
|
+
usage: dispatchUsageField
|
|
4203
4328
|
}).strict(),
|
|
4204
4329
|
z.object({
|
|
4205
4330
|
...dispatchShared,
|
|
4206
4331
|
event: z.literal("dispatch_failed"),
|
|
4207
|
-
reason: z.enum(["timeout", "crash", "refused", "unattributed_write"])
|
|
4332
|
+
reason: z.enum(["timeout", "crash", "refused", "unattributed_write"]),
|
|
4333
|
+
usage: dispatchUsageField
|
|
4208
4334
|
}).strict()
|
|
4209
4335
|
]);
|
|
4210
4336
|
var loopShared = {
|
|
@@ -4479,6 +4605,7 @@ function resolveDoctrineRootInfo(pkg = packageRoot(import.meta.url), cwd = proce
|
|
|
4479
4605
|
return hasDoctrineEntry(bundleRoot) ? { root: bundleRoot, source: "bundle" } : null;
|
|
4480
4606
|
}
|
|
4481
4607
|
var ROLE_ALIASES = { "code-reviewer": "reviewer" };
|
|
4608
|
+
var RETIRED_ROLES = { "brief-author": "planner" };
|
|
4482
4609
|
function hasDoctrineEntry(root) {
|
|
4483
4610
|
return existsSync2(join2(root, ...ENTRY_SEGMENTS));
|
|
4484
4611
|
}
|
|
@@ -4502,6 +4629,11 @@ function doctrineCommand(args) {
|
|
|
4502
4629
|
if (roleFlagIndex !== -1) {
|
|
4503
4630
|
const requested = args[roleFlagIndex + 1];
|
|
4504
4631
|
const roleName = requested !== undefined ? Object.hasOwn(ROLE_ALIASES, requested) ? ROLE_ALIASES[requested] : requested : undefined;
|
|
4632
|
+
if (roleName !== undefined && Object.hasOwn(RETIRED_ROLES, roleName)) {
|
|
4633
|
+
process.stderr.write(`vinaya doctrine --role: '${roleName}' has been retired — use --role ${RETIRED_ROLES[roleName]}.
|
|
4634
|
+
`);
|
|
4635
|
+
process.exit(1);
|
|
4636
|
+
}
|
|
4505
4637
|
const validRoleNames = listRoleNames(root);
|
|
4506
4638
|
if (roleName === undefined || roleName.startsWith("--") || !validRoleNames.includes(roleName)) {
|
|
4507
4639
|
process.stderr.write(`vinaya doctrine --role: '${requested ?? ""}' is not a known role. ` + `Valid role names: ${validRoleNames.join(", ")}
|
|
@@ -4534,11 +4666,12 @@ var AGENTS_SKILLS_GROUP = "Agent skills (.agents/skills/)";
|
|
|
4534
4666
|
function formatRoleTitle(roleName) {
|
|
4535
4667
|
return roleName.split("-").filter((segment) => segment.length > 0).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
4536
4668
|
}
|
|
4669
|
+
var RETIRED_ROLE_NAMES = new Set(["brief-author"]);
|
|
4537
4670
|
function discoverRoleNames(doctrineRoot) {
|
|
4538
4671
|
const rolesDir = join3(doctrineRoot, "roles");
|
|
4539
4672
|
if (!existsSync3(rolesDir))
|
|
4540
4673
|
return [];
|
|
4541
|
-
return readdirSync2(rolesDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -".md".length)).filter((roleName) => {
|
|
4674
|
+
return readdirSync2(rolesDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -".md".length)).filter((roleName) => !RETIRED_ROLE_NAMES.has(roleName)).filter((roleName) => {
|
|
4542
4675
|
const { data } = matter3(readFileSync3(join3(rolesDir, `${roleName}.md`), "utf8"));
|
|
4543
4676
|
return data.actor !== "human";
|
|
4544
4677
|
}).sort();
|
|
@@ -4567,6 +4700,14 @@ function buildAgentsSkillsOps(doctrineRoot, selfHost = null) {
|
|
|
4567
4700
|
group: AGENTS_SKILLS_GROUP
|
|
4568
4701
|
}));
|
|
4569
4702
|
}
|
|
4703
|
+
var AGENT_SKILL_PATH_PATTERN = /^\.agents\/skills\/vinaya-([^/]+)\/SKILL\.md$/;
|
|
4704
|
+
function staleAgentSkillPaths(doctrineRoot, manifestFiles) {
|
|
4705
|
+
const liveRoles = new Set(discoverRoleNames(doctrineRoot));
|
|
4706
|
+
return manifestFiles.filter((path) => {
|
|
4707
|
+
const match = AGENT_SKILL_PATH_PATTERN.exec(path);
|
|
4708
|
+
return match !== null && !liveRoles.has(match[1]);
|
|
4709
|
+
});
|
|
4710
|
+
}
|
|
4570
4711
|
|
|
4571
4712
|
// src/lib/claude-command-emitter.ts
|
|
4572
4713
|
var CLAUDE_COMMAND_GROUP = "Claude Code command (.claude/commands/)";
|
|
@@ -5415,9 +5556,21 @@ fi
|
|
|
5415
5556
|
node ${selfHost.bin} ${args} || exit 1`;
|
|
5416
5557
|
}
|
|
5417
5558
|
function preCommitBody(selfHost) {
|
|
5418
|
-
|
|
5559
|
+
const doctrineGate = `# Vinaya commit-time gate. Runs the deterministic checks over your staged
|
|
5419
5560
|
# diff before the commit lands.
|
|
5420
5561
|
${hookRun(selfHost, "check --all --diff-only --local --skip-full")}`;
|
|
5562
|
+
if (!selfHost)
|
|
5563
|
+
return doctrineGate;
|
|
5564
|
+
return `# Ring 0: format + lint your staged files, applying every safe fix, then
|
|
5565
|
+
# type-check only the packages your staged diff actually touches (O9) —
|
|
5566
|
+
# scoped so this hook costs seconds, not the whole repo's worth of work.
|
|
5567
|
+
VINAYA_STAGED_FILES="$(git diff --cached --name-only --diff-filter=ACMR)"
|
|
5568
|
+
bunx biome check --write --staged . || exit 1
|
|
5569
|
+
if [ -n "$VINAYA_STAGED_FILES" ]; then
|
|
5570
|
+
echo "$VINAYA_STAGED_FILES" | xargs git add --
|
|
5571
|
+
fi
|
|
5572
|
+
bunx turbo typecheck --affected || exit 1
|
|
5573
|
+
${doctrineGate}`;
|
|
5421
5574
|
}
|
|
5422
5575
|
function prePushBody(selfHost) {
|
|
5423
5576
|
const base = `# Vinaya pre-push gate. Runs branch/dispatch checks before the push leaves.
|
|
@@ -5489,7 +5642,7 @@ anything substantive:
|
|
|
5489
5642
|
|
|
5490
5643
|
prints the absolute path to that role's doctrine — \`architect\`,
|
|
5491
5644
|
\`developer\`, \`reviewer\`, \`planner\`, \`security\`, \`archivist\`,
|
|
5492
|
-
\`tranche-archivist\`,
|
|
5645
|
+
\`tranche-archivist\`, or \`principal\`. If your agent tool
|
|
5493
5646
|
supports slash-style commands, the same doctrine is likely exposed as
|
|
5494
5647
|
\`/vinaya <role>\` — check your tool's command list before falling back to
|
|
5495
5648
|
the raw CLI form.
|
|
@@ -5630,7 +5783,7 @@ var FIXED_LABEL_METADATA = {
|
|
|
5630
5783
|
"needs-principal-input": { color: "b60205", description: "Blocked on a Principal decision" },
|
|
5631
5784
|
"needs-brief-correction": {
|
|
5632
5785
|
color: "fbca04",
|
|
5633
|
-
description: "Blocked on the
|
|
5786
|
+
description: "Blocked on the Planner — brief contradicts the surface"
|
|
5634
5787
|
},
|
|
5635
5788
|
"waiver-docs": { color: "0e8a16", description: "Doc-coverage gate excused for this PR (principal-applied)" },
|
|
5636
5789
|
"waiver-review": { color: "0e8a16", description: "Review gate excused for this PR (principal-applied)" },
|
|
@@ -8511,6 +8664,21 @@ function expandGlob(glob) {
|
|
|
8511
8664
|
function sha256OfFile(path) {
|
|
8512
8665
|
return createHash5("sha256").update(readFileSync9(path)).digest("hex");
|
|
8513
8666
|
}
|
|
8667
|
+
function resolveBoundaryPaths(tokens, allTrackedFiles) {
|
|
8668
|
+
const trackedSet = new Set(allTrackedFiles);
|
|
8669
|
+
const resolved = new Set;
|
|
8670
|
+
for (const token of tokens) {
|
|
8671
|
+
if (trackedSet.has(token)) {
|
|
8672
|
+
resolved.add(token);
|
|
8673
|
+
continue;
|
|
8674
|
+
}
|
|
8675
|
+
const suffix = `/${token}`;
|
|
8676
|
+
const suffixMatches = allTrackedFiles.filter((f) => f.endsWith(suffix));
|
|
8677
|
+
if (suffixMatches.length === 1)
|
|
8678
|
+
resolved.add(suffixMatches[0]);
|
|
8679
|
+
}
|
|
8680
|
+
return [...resolved];
|
|
8681
|
+
}
|
|
8514
8682
|
function listDirs(dir) {
|
|
8515
8683
|
try {
|
|
8516
8684
|
return readdirSync5(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
@@ -8618,17 +8786,17 @@ async function assembleAndRenderBrief(trancheSlug, taskId, surfaceGlobsOverride)
|
|
|
8618
8786
|
const gate = checkDispatchReadiness(gateInput);
|
|
8619
8787
|
const surfaceResult = parseIssueSurface(issueBody);
|
|
8620
8788
|
const surface = surfaceResult.ok ? surfaceResult.value : { in: [], out: [] };
|
|
8789
|
+
const rationale = parseRationaleFields(issueBody);
|
|
8621
8790
|
const globs = surfaceGlobsOverride ?? surface.in;
|
|
8622
|
-
const surfaceFiles = [];
|
|
8623
8791
|
for (const glob of globs) {
|
|
8624
|
-
|
|
8625
|
-
if (matches.length === 0) {
|
|
8792
|
+
if (expandGlob(glob).length === 0) {
|
|
8626
8793
|
return { ok: false, missing: [`--surfaces glob "${glob}" matched no tracked file.`] };
|
|
8627
8794
|
}
|
|
8628
|
-
for (const path of matches) {
|
|
8629
|
-
surfaceFiles.push({ path, sha256: sha256OfFile(path), packageName: packageNameForPath(path) });
|
|
8630
|
-
}
|
|
8631
8795
|
}
|
|
8796
|
+
const allTrackedFiles = git(["ls-files"]).split(`
|
|
8797
|
+
`).map((s) => s.trim()).filter(Boolean);
|
|
8798
|
+
const boundaryTokens = extractBoundaryFilePaths(rationale.boundary ?? "");
|
|
8799
|
+
const surfaceFiles = resolveBoundaryPaths(boundaryTokens, allTrackedFiles).sort().map((path) => ({ path, sha256: sha256OfFile(path), packageName: packageNameForPath(path) }));
|
|
8632
8800
|
const workspaces = workspaceGlobs();
|
|
8633
8801
|
const consumersOf = buildConsumersOf(workspaces, listDirs, readManifest);
|
|
8634
8802
|
const partsResult = parseIssueParts(issueBody);
|
|
@@ -8645,7 +8813,7 @@ async function assembleAndRenderBrief(trancheSlug, taskId, surfaceGlobsOverride)
|
|
|
8645
8813
|
projects: task.projects,
|
|
8646
8814
|
dependsOn: task.dependsOn,
|
|
8647
8815
|
conflictsWith: task.conflictsWith,
|
|
8648
|
-
rationale
|
|
8816
|
+
rationale,
|
|
8649
8817
|
objectives: (() => {
|
|
8650
8818
|
const parsed = objectivesOf(issueBody);
|
|
8651
8819
|
return parsed.ok ? parsed.objectives : [];
|
|
@@ -8662,7 +8830,7 @@ async function assembleAndRenderBrief(trancheSlug, taskId, surfaceGlobsOverride)
|
|
|
8662
8830
|
};
|
|
8663
8831
|
const template = readFileSync9(TEMPLATE_PATH, "utf8");
|
|
8664
8832
|
const result = renderBrief(facts, template);
|
|
8665
|
-
return result.ok ? { ok: true, brief: result.brief } : { ok: false, missing: result.missing };
|
|
8833
|
+
return result.ok ? { ok: true, brief: result.brief, issue: task.issue } : { ok: false, missing: result.missing };
|
|
8666
8834
|
}
|
|
8667
8835
|
|
|
8668
8836
|
// src/commands/brief.ts
|
|
@@ -8674,16 +8842,19 @@ async function briefRenderCommand(args) {
|
|
|
8674
8842
|
const trancheSlug = args[0];
|
|
8675
8843
|
const taskId = args[1];
|
|
8676
8844
|
if (!trancheSlug || !taskId || trancheSlug.startsWith("--")) {
|
|
8677
|
-
console.error("Usage: vinaya brief render <tranche> <n> --surfaces <glob1,glob2,...> [--out <path>]");
|
|
8845
|
+
console.error("Usage: vinaya brief render <tranche> <n> [--surfaces <glob1,glob2,...>] [--out <path>]");
|
|
8678
8846
|
process.exit(2);
|
|
8679
8847
|
}
|
|
8680
8848
|
const surfacesIdx = args.indexOf("--surfaces");
|
|
8681
8849
|
const surfacesArg = surfacesIdx !== -1 ? args[surfacesIdx + 1] : undefined;
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8850
|
+
let globs;
|
|
8851
|
+
if (surfacesIdx !== -1) {
|
|
8852
|
+
if (!surfacesArg)
|
|
8853
|
+
refuse("--surfaces <glob1,glob2,...> was passed with no value.");
|
|
8854
|
+
globs = surfacesArg.split(",").map((s) => s.trim()).filter(Boolean);
|
|
8855
|
+
if (globs.length === 0)
|
|
8856
|
+
refuse("--surfaces resolved to zero globs.");
|
|
8857
|
+
}
|
|
8687
8858
|
const outIdx = args.indexOf("--out");
|
|
8688
8859
|
const outPath = outIdx !== -1 ? args[outIdx + 1] : undefined;
|
|
8689
8860
|
const result = await assembleAndRenderBrief(trancheSlug, taskId, globs);
|
|
@@ -8790,6 +8961,18 @@ var REGISTRY = [
|
|
|
8790
8961
|
},
|
|
8791
8962
|
0
|
|
8792
8963
|
],
|
|
8964
|
+
[
|
|
8965
|
+
{
|
|
8966
|
+
name: "pr-premise-reassert",
|
|
8967
|
+
run: bin("check-pr-premise-reassert"),
|
|
8968
|
+
scope: "diff",
|
|
8969
|
+
timeoutMs: 15000,
|
|
8970
|
+
env: {
|
|
8971
|
+
PR_BODY: { optional: true }
|
|
8972
|
+
}
|
|
8973
|
+
},
|
|
8974
|
+
0
|
|
8975
|
+
],
|
|
8793
8976
|
[
|
|
8794
8977
|
{
|
|
8795
8978
|
name: "dispatch-readiness",
|
|
@@ -8838,6 +9021,22 @@ var REGISTRY = [
|
|
|
8838
9021
|
},
|
|
8839
9022
|
0
|
|
8840
9023
|
],
|
|
9024
|
+
[
|
|
9025
|
+
{
|
|
9026
|
+
name: "surface-scope",
|
|
9027
|
+
run: bin("check-surface-scope"),
|
|
9028
|
+
scope: "diff",
|
|
9029
|
+
timeoutMs: 15000,
|
|
9030
|
+
env: {
|
|
9031
|
+
AEG_REPO: { optional: true },
|
|
9032
|
+
BASE_SHA: { optional: true },
|
|
9033
|
+
BRANCH: { optional: true },
|
|
9034
|
+
GITHUB_TOKEN: { optional: true },
|
|
9035
|
+
GH_TOKEN: { optional: true }
|
|
9036
|
+
}
|
|
9037
|
+
},
|
|
9038
|
+
0
|
|
9039
|
+
],
|
|
8841
9040
|
[
|
|
8842
9041
|
{
|
|
8843
9042
|
name: "test-plan",
|
|
@@ -8879,6 +9078,7 @@ var REGISTRY = [
|
|
|
8879
9078
|
requiresOpenPr: true,
|
|
8880
9079
|
env: {
|
|
8881
9080
|
PR_BODY: { optional: true },
|
|
9081
|
+
BRANCH: { optional: true },
|
|
8882
9082
|
CLAUDE_PROJECT_DIR: { optional: true },
|
|
8883
9083
|
CLAUDE_CODE_SESSION_ID: { optional: true }
|
|
8884
9084
|
}
|
|
@@ -9751,8 +9951,8 @@ function renderPlanTable(result) {
|
|
|
9751
9951
|
const rows = result.resolved.map((entry2) => [entry2.name, entry2.state, entry2.source, envCellFor(entry2.spec)]);
|
|
9752
9952
|
const failureRows = result.failures.map((f) => [f.key, "FAILED", "—", f.reason]);
|
|
9753
9953
|
const table = [header, ...rows, ...failureRows];
|
|
9754
|
-
const widths = header.map((_, col) => Math.max(...table.map((
|
|
9755
|
-
return table.map((
|
|
9954
|
+
const widths = header.map((_, col) => Math.max(...table.map((row2) => row2[col]?.length ?? 0)));
|
|
9955
|
+
return table.map((row2) => row2.map((cell, col) => (cell ?? "").padEnd(widths[col] ?? 0)).join(" ")).join(`
|
|
9756
9956
|
`);
|
|
9757
9957
|
}
|
|
9758
9958
|
function renderRolesTable(rolePlan) {
|
|
@@ -9762,8 +9962,8 @@ function renderRolesTable(rolePlan) {
|
|
|
9762
9962
|
const rows = rolePlan.resolved.map((r) => [r.name, r.state, r.source, r.renderId, r.inertToGating ? "inert" : "core"]);
|
|
9763
9963
|
const failureRows = rolePlan.failures.map((f) => [f.key, "FAILED", "—", "—", f.reason]);
|
|
9764
9964
|
const table = [header, ...rows, ...failureRows];
|
|
9765
|
-
const widths = header.map((_, col) => Math.max(...table.map((
|
|
9766
|
-
return table.map((
|
|
9965
|
+
const widths = header.map((_, col) => Math.max(...table.map((row2) => row2[col]?.length ?? 0)));
|
|
9966
|
+
return table.map((row2) => row2.map((cell, col) => (cell ?? "").padEnd(widths[col] ?? 0)).join(" ")).join(`
|
|
9767
9967
|
`);
|
|
9768
9968
|
}
|
|
9769
9969
|
function duplicateIdFailures(resolved) {
|
|
@@ -10031,6 +10231,12 @@ None — fixture content only.
|
|
|
10031
10231
|
## Test Plan
|
|
10032
10232
|
Test Plan: unit-tests-only
|
|
10033
10233
|
|
|
10234
|
+
## Token report
|
|
10235
|
+
|
|
10236
|
+
| Phase | Role | Agent/Model | Tokens in | Tokens out | Cost | Date |
|
|
10237
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
10238
|
+
| demo: develop | Developer | — | — | — | — | — |
|
|
10239
|
+
|
|
10034
10240
|
## Stop Conditions
|
|
10035
10241
|
Autonomy: Do not stop to ask clarifying questions — this is a scripted demo; proceed and clean up automatically.
|
|
10036
10242
|
|
|
@@ -10152,8 +10358,10 @@ async function demoBreakCommand(args) {
|
|
|
10152
10358
|
|
|
10153
10359
|
// src/lib/dispatch.ts
|
|
10154
10360
|
import { randomUUID as randomUUID2, createHash as createHash6 } from "node:crypto";
|
|
10155
|
-
import { accessSync, constants as fsConstants3, readFileSync as readFileSync14 } from "node:fs";
|
|
10361
|
+
import { accessSync, constants as fsConstants3, mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync5 } from "node:fs";
|
|
10362
|
+
import { chmodSync as chmodSync2, createWriteStream } from "node:fs";
|
|
10156
10363
|
import { execFileSync as execFileSync13, spawn as spawn2 } from "node:child_process";
|
|
10364
|
+
import { homedir as homedir3 } from "node:os";
|
|
10157
10365
|
|
|
10158
10366
|
// src/lib/log-sink.ts
|
|
10159
10367
|
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
@@ -10354,21 +10562,112 @@ function log(e) {
|
|
|
10354
10562
|
}
|
|
10355
10563
|
|
|
10356
10564
|
// src/lib/dispatch.ts
|
|
10357
|
-
import { join as join14 } from "node:path";
|
|
10565
|
+
import { dirname as dirname7, join as join14 } from "node:path";
|
|
10358
10566
|
var AGENT_VENDOR_NAMES = ["claude", "codex", "gemini"];
|
|
10359
10567
|
function isAgentVendor2(value) {
|
|
10360
10568
|
return AGENT_VENDOR_NAMES.includes(value);
|
|
10361
10569
|
}
|
|
10362
|
-
var
|
|
10570
|
+
var AGENT_CLASS_VALUES = ["high", "mid", "fast"];
|
|
10571
|
+
function isAgentClass(value) {
|
|
10572
|
+
return AGENT_CLASS_VALUES.includes(value);
|
|
10573
|
+
}
|
|
10574
|
+
var DEFAULT_TIMEOUT_MS = 14400000;
|
|
10363
10575
|
var SIGKILL_GRACE_MS = 5000;
|
|
10364
|
-
|
|
10576
|
+
var HEARTBEAT_INTERVAL_MS = 60000;
|
|
10577
|
+
function timeoutWarningLeadMs(timeoutMs) {
|
|
10578
|
+
return Math.min(300000, Math.floor(timeoutMs / 2));
|
|
10579
|
+
}
|
|
10580
|
+
var MAX_TEE_BYTES = 8 * 1024 * 1024;
|
|
10581
|
+
function openOutputTee(effectId) {
|
|
10582
|
+
const inert = { write: () => {}, end: () => {}, path: null };
|
|
10583
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(effectId))
|
|
10584
|
+
return inert;
|
|
10365
10585
|
try {
|
|
10366
|
-
const
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
}
|
|
10371
|
-
|
|
10586
|
+
const dir = join14(GLOBAL_VINAYA_HOME, "dispatch-output");
|
|
10587
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
10588
|
+
chmodSync2(dir, 448);
|
|
10589
|
+
const path = join14(dir, `${effectId}.log`);
|
|
10590
|
+
const stream = createWriteStream(path, { flags: "a", mode: 384 });
|
|
10591
|
+
let broken = false;
|
|
10592
|
+
let written = 0;
|
|
10593
|
+
let carry = "";
|
|
10594
|
+
stream.on("error", () => {
|
|
10595
|
+
broken = true;
|
|
10596
|
+
});
|
|
10597
|
+
const home = homedir3();
|
|
10598
|
+
return {
|
|
10599
|
+
write: (chunk) => {
|
|
10600
|
+
if (broken || written >= MAX_TEE_BYTES)
|
|
10601
|
+
return;
|
|
10602
|
+
try {
|
|
10603
|
+
const text = carry + chunk.toString("utf8");
|
|
10604
|
+
const cut = Math.max(0, text.length - 256);
|
|
10605
|
+
const emit = redact(text.slice(0, cut), home);
|
|
10606
|
+
carry = text.slice(cut);
|
|
10607
|
+
if (emit.length === 0)
|
|
10608
|
+
return;
|
|
10609
|
+
written += Buffer.byteLength(emit);
|
|
10610
|
+
stream.write(emit);
|
|
10611
|
+
} catch {
|
|
10612
|
+
broken = true;
|
|
10613
|
+
}
|
|
10614
|
+
},
|
|
10615
|
+
end: () => {
|
|
10616
|
+
try {
|
|
10617
|
+
if (!broken && carry.length > 0)
|
|
10618
|
+
stream.write(redact(carry, home));
|
|
10619
|
+
stream.end();
|
|
10620
|
+
} catch {
|
|
10621
|
+
broken = true;
|
|
10622
|
+
}
|
|
10623
|
+
},
|
|
10624
|
+
path
|
|
10625
|
+
};
|
|
10626
|
+
} catch {
|
|
10627
|
+
return inert;
|
|
10628
|
+
}
|
|
10629
|
+
}
|
|
10630
|
+
function parseClaudeModel(stdout) {
|
|
10631
|
+
const lines = stdout.split(`
|
|
10632
|
+
`).filter((l) => l.trim().length > 0);
|
|
10633
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
10634
|
+
try {
|
|
10635
|
+
const obj = JSON.parse(lines[i]);
|
|
10636
|
+
const keys = obj.modelUsage ? Object.keys(obj.modelUsage) : [];
|
|
10637
|
+
if (keys.length > 0)
|
|
10638
|
+
return keys.join(",");
|
|
10639
|
+
} catch {}
|
|
10640
|
+
}
|
|
10641
|
+
return null;
|
|
10642
|
+
}
|
|
10643
|
+
function parseCodexModel(_stdout) {
|
|
10644
|
+
return null;
|
|
10645
|
+
}
|
|
10646
|
+
function parseGeminiModel(stdout) {
|
|
10647
|
+
const lines = stdout.split(`
|
|
10648
|
+
`).filter((l) => l.trim().length > 0);
|
|
10649
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
10650
|
+
try {
|
|
10651
|
+
const obj = JSON.parse(lines[i]);
|
|
10652
|
+
const keys = obj.stats?.models ? Object.keys(obj.stats.models) : [];
|
|
10653
|
+
if (keys.length > 0)
|
|
10654
|
+
return keys.join(",");
|
|
10655
|
+
} catch {}
|
|
10656
|
+
}
|
|
10657
|
+
return null;
|
|
10658
|
+
}
|
|
10659
|
+
function parseClaudeUsage(stdout) {
|
|
10660
|
+
const lines = stdout.split(`
|
|
10661
|
+
`).filter((l) => l.trim().length > 0);
|
|
10662
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
10663
|
+
try {
|
|
10664
|
+
const obj = JSON.parse(lines[i]);
|
|
10665
|
+
const u = obj.usage;
|
|
10666
|
+
if (u && typeof u.input_tokens === "number" && typeof u.output_tokens === "number") {
|
|
10667
|
+
return { input: u.input_tokens, output: u.output_tokens };
|
|
10668
|
+
}
|
|
10669
|
+
} catch {}
|
|
10670
|
+
}
|
|
10372
10671
|
return null;
|
|
10373
10672
|
}
|
|
10374
10673
|
function parseCodexUsage(stdout) {
|
|
@@ -10390,16 +10689,31 @@ function parseCodexUsage(stdout) {
|
|
|
10390
10689
|
}
|
|
10391
10690
|
return null;
|
|
10392
10691
|
}
|
|
10393
|
-
function parseGeminiUsage(
|
|
10692
|
+
function parseGeminiUsage(stdout) {
|
|
10693
|
+
const lines = stdout.split(`
|
|
10694
|
+
`).filter((l) => l.trim().length > 0);
|
|
10695
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
10696
|
+
try {
|
|
10697
|
+
const obj = JSON.parse(lines[i]);
|
|
10698
|
+
const st = obj.stats;
|
|
10699
|
+
if (st && typeof st.input_tokens === "number" && typeof st.output_tokens === "number") {
|
|
10700
|
+
return { input: st.input_tokens, output: st.output_tokens };
|
|
10701
|
+
}
|
|
10702
|
+
} catch {}
|
|
10703
|
+
}
|
|
10394
10704
|
return null;
|
|
10395
10705
|
}
|
|
10396
10706
|
function parseClaudeResumeId(stdout) {
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
|
|
10401
|
-
|
|
10707
|
+
const lines = stdout.split(`
|
|
10708
|
+
`).filter((l) => l.trim().length > 0);
|
|
10709
|
+
for (let i = lines.length - 1;i >= 0; i--) {
|
|
10710
|
+
try {
|
|
10711
|
+
const obj = JSON.parse(lines[i]);
|
|
10712
|
+
if (typeof obj.session_id === "string")
|
|
10713
|
+
return obj.session_id;
|
|
10714
|
+
} catch {}
|
|
10402
10715
|
}
|
|
10716
|
+
return null;
|
|
10403
10717
|
}
|
|
10404
10718
|
function parseCodexResumeId(stdout) {
|
|
10405
10719
|
for (const raw of stdout.split(`
|
|
@@ -10423,29 +10737,132 @@ function parseGeminiResumeId(stdout) {
|
|
|
10423
10737
|
return null;
|
|
10424
10738
|
}
|
|
10425
10739
|
}
|
|
10740
|
+
var SAFE_REPO_SEGMENT = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
10741
|
+
function isSafeRepoSegment2(segment) {
|
|
10742
|
+
return SAFE_REPO_SEGMENT.test(segment) && !segment.includes("..");
|
|
10743
|
+
}
|
|
10744
|
+
function resumeRecordPathFor(role, agent, repo, task, pr) {
|
|
10745
|
+
const repoSegment = repo && isSafeRepoSegment2(repo.owner) && isSafeRepoSegment2(repo.repo) ? `${repo.owner}-${repo.repo}` : "unresolved";
|
|
10746
|
+
const scope = task !== undefined ? `issue${task}` : pr !== undefined ? `pr${pr}` : "unscoped";
|
|
10747
|
+
return join14(GLOBAL_VINAYA_HOME, "dispatch-resume", repoSegment, `${role}-${agent}-${scope}.json`);
|
|
10748
|
+
}
|
|
10749
|
+
function recordResumeState(record) {
|
|
10750
|
+
try {
|
|
10751
|
+
const path = resumeRecordPathFor(record.role, record.agent, record.repo, record.task ?? undefined, record.pr ?? undefined);
|
|
10752
|
+
mkdirSync4(dirname7(path), { recursive: true, mode: 448 });
|
|
10753
|
+
chmodSync2(dirname7(path), 448);
|
|
10754
|
+
writeFileSync5(path, JSON.stringify(record, null, 2), { mode: 384 });
|
|
10755
|
+
return path;
|
|
10756
|
+
} catch {
|
|
10757
|
+
return null;
|
|
10758
|
+
}
|
|
10759
|
+
}
|
|
10760
|
+
function renderClaudeEvent(obj) {
|
|
10761
|
+
const type = obj.type;
|
|
10762
|
+
if (type === "assistant" || type === "user") {
|
|
10763
|
+
const msg = obj.message;
|
|
10764
|
+
const content = Array.isArray(msg?.content) ? msg?.content : [];
|
|
10765
|
+
const out = [];
|
|
10766
|
+
for (const block of content) {
|
|
10767
|
+
if (block.type === "text" && typeof block.text === "string" && block.text.trim().length > 0) {
|
|
10768
|
+
out.push(block.text.trim());
|
|
10769
|
+
} else if (block.type === "tool_use" && typeof block.name === "string") {
|
|
10770
|
+
const input = block.input;
|
|
10771
|
+
const subject = typeof input?.file_path === "string" && input.file_path || typeof input?.command === "string" && input.command || typeof input?.pattern === "string" && input.pattern || typeof input?.path === "string" && input.path || "";
|
|
10772
|
+
const trimmed = subject.length > 120 ? `${subject.slice(0, 117)}...` : subject;
|
|
10773
|
+
out.push(trimmed ? `⚙ ${block.name}: ${trimmed}` : `⚙ ${block.name}`);
|
|
10774
|
+
}
|
|
10775
|
+
}
|
|
10776
|
+
return out.length > 0 ? out.join(`
|
|
10777
|
+
`) : null;
|
|
10778
|
+
}
|
|
10779
|
+
if (type === "system" && obj.subtype === "init")
|
|
10780
|
+
return "⏵ session started";
|
|
10781
|
+
if (typeof obj.stop_reason === "string")
|
|
10782
|
+
return `⏹ ${obj.stop_reason}`;
|
|
10783
|
+
return null;
|
|
10784
|
+
}
|
|
10785
|
+
function renderCodexEvent(obj) {
|
|
10786
|
+
const type = typeof obj.type === "string" ? obj.type : "";
|
|
10787
|
+
if (type.endsWith(".completed") || type.endsWith(".started"))
|
|
10788
|
+
return `⏵ ${type}`;
|
|
10789
|
+
const item = obj.item;
|
|
10790
|
+
return typeof item?.text === "string" && item.text.trim().length > 0 ? item.text.trim() : null;
|
|
10791
|
+
}
|
|
10792
|
+
function renderGeminiEvent(obj) {
|
|
10793
|
+
if (obj.type === "init")
|
|
10794
|
+
return "⏵ session started";
|
|
10795
|
+
if (obj.type === "message" && obj.role === "assistant") {
|
|
10796
|
+
const text = typeof obj.content === "string" ? obj.content.trim() : "";
|
|
10797
|
+
return text.length > 0 ? text : null;
|
|
10798
|
+
}
|
|
10799
|
+
if (obj.type === "result") {
|
|
10800
|
+
const status = typeof obj.status === "string" ? obj.status : "finished";
|
|
10801
|
+
return `⏹ ${status}`;
|
|
10802
|
+
}
|
|
10803
|
+
return null;
|
|
10804
|
+
}
|
|
10426
10805
|
var VENDOR_TABLE = {
|
|
10427
10806
|
claude: {
|
|
10428
10807
|
binary: "claude",
|
|
10429
|
-
args: ["-p", "--output-format", "json"],
|
|
10430
|
-
resumeArgs: (id) => [
|
|
10808
|
+
args: (model) => ["-p", "--verbose", "--output-format", "stream-json", ...model ? ["--model", model] : []],
|
|
10809
|
+
resumeArgs: (id, model) => [
|
|
10810
|
+
"-p",
|
|
10811
|
+
"-r",
|
|
10812
|
+
id,
|
|
10813
|
+
"--verbose",
|
|
10814
|
+
"--output-format",
|
|
10815
|
+
"stream-json",
|
|
10816
|
+
...model ? ["--model", model] : []
|
|
10817
|
+
],
|
|
10431
10818
|
parseUsage: parseClaudeUsage,
|
|
10432
|
-
|
|
10819
|
+
parseModel: parseClaudeModel,
|
|
10820
|
+
parseResumeId: parseClaudeResumeId,
|
|
10821
|
+
renderEvent: renderClaudeEvent,
|
|
10822
|
+
classModels: { high: "opus", mid: "sonnet", fast: "haiku" }
|
|
10433
10823
|
},
|
|
10434
10824
|
codex: {
|
|
10435
10825
|
binary: "codex",
|
|
10436
|
-
args: ["exec", "--json", "-"],
|
|
10437
|
-
resumeArgs: (id) => ["exec", "resume", id, "--json", "-"],
|
|
10826
|
+
args: (model) => ["exec", ...model ? ["--model", model] : [], "--json", "-"],
|
|
10827
|
+
resumeArgs: (id, model) => ["exec", "resume", id, ...model ? ["--model", model] : [], "--json", "-"],
|
|
10438
10828
|
parseUsage: parseCodexUsage,
|
|
10439
|
-
|
|
10829
|
+
parseModel: parseCodexModel,
|
|
10830
|
+
parseResumeId: parseCodexResumeId,
|
|
10831
|
+
renderEvent: renderCodexEvent,
|
|
10832
|
+
classModels: {}
|
|
10440
10833
|
},
|
|
10441
10834
|
gemini: {
|
|
10442
10835
|
binary: "gemini",
|
|
10443
|
-
args: ["-p", "", "--output-format", "json", "--skip-trust"],
|
|
10444
|
-
resumeArgs: (id) => [
|
|
10836
|
+
args: (model) => ["-p", "", ...model ? ["--model", model] : [], "--output-format", "stream-json", "--skip-trust"],
|
|
10837
|
+
resumeArgs: (id, model) => [
|
|
10838
|
+
"-p",
|
|
10839
|
+
"",
|
|
10840
|
+
"--resume",
|
|
10841
|
+
id,
|
|
10842
|
+
...model ? ["--model", model] : [],
|
|
10843
|
+
"--output-format",
|
|
10844
|
+
"stream-json",
|
|
10845
|
+
"--skip-trust"
|
|
10846
|
+
],
|
|
10445
10847
|
parseUsage: parseGeminiUsage,
|
|
10446
|
-
|
|
10848
|
+
parseModel: parseGeminiModel,
|
|
10849
|
+
parseResumeId: parseGeminiResumeId,
|
|
10850
|
+
renderEvent: renderGeminiEvent,
|
|
10851
|
+
classModels: {}
|
|
10447
10852
|
}
|
|
10448
10853
|
};
|
|
10854
|
+
var CLAUDE_MODEL_ALIASES = new Set(["fable", "opus", "sonnet", "haiku"]);
|
|
10855
|
+
function identifyVendorFromModelShape(model) {
|
|
10856
|
+
const lower = model.toLowerCase();
|
|
10857
|
+
if (CLAUDE_MODEL_ALIASES.has(lower) || lower.startsWith("claude-"))
|
|
10858
|
+
return "claude";
|
|
10859
|
+
if (lower.startsWith("gemini-") || lower.startsWith("gemma-"))
|
|
10860
|
+
return "gemini";
|
|
10861
|
+
return null;
|
|
10862
|
+
}
|
|
10863
|
+
function resolveClassModel(agent, agentClass) {
|
|
10864
|
+
return VENDOR_TABLE[agent].classModels[agentClass] ?? null;
|
|
10865
|
+
}
|
|
10449
10866
|
function resolveExecutable(binary) {
|
|
10450
10867
|
try {
|
|
10451
10868
|
const path = execFileSync13("which", [binary], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
@@ -10513,6 +10930,30 @@ async function dispatchRole(role, agent, prompt2, opts) {
|
|
|
10513
10930
|
const vendor = VENDOR_TABLE[agent];
|
|
10514
10931
|
const start = Date.now();
|
|
10515
10932
|
const roundField = opts.round !== undefined ? { round: opts.round } : {};
|
|
10933
|
+
const resolvedModel = opts.model !== undefined ? `requested:${opts.model}` : "default";
|
|
10934
|
+
if (opts.model !== undefined) {
|
|
10935
|
+
const foreignVendor = identifyVendorFromModelShape(opts.model);
|
|
10936
|
+
if (foreignVendor !== null && foreignVendor !== agent) {
|
|
10937
|
+
const durationMs = Date.now() - start;
|
|
10938
|
+
const priorSize = sizeOfSafe(outboxPath);
|
|
10939
|
+
log2({
|
|
10940
|
+
kind: "dispatch",
|
|
10941
|
+
event: "dispatch_failed",
|
|
10942
|
+
payload: {},
|
|
10943
|
+
target_role: role,
|
|
10944
|
+
model: resolvedModel,
|
|
10945
|
+
...roundField,
|
|
10946
|
+
effect_id: effectId,
|
|
10947
|
+
reason: "refused",
|
|
10948
|
+
usage: null,
|
|
10949
|
+
duration_ms: durationMs
|
|
10950
|
+
});
|
|
10951
|
+
process.stderr.write(`[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).
|
|
10952
|
+
`);
|
|
10953
|
+
await waitForDispatchLine(outboxPath, priorSize, runId, effectId, "dispatch_failed");
|
|
10954
|
+
return { exitCode: null, durationMs, usage: null, resumeId: null, timedOut: false, failureReason: "refused" };
|
|
10955
|
+
}
|
|
10956
|
+
}
|
|
10516
10957
|
const binaryPath = resolveExecutable(vendor.binary);
|
|
10517
10958
|
if (binaryPath === null) {
|
|
10518
10959
|
const durationMs = Date.now() - start;
|
|
@@ -10522,10 +10963,11 @@ async function dispatchRole(role, agent, prompt2, opts) {
|
|
|
10522
10963
|
event: "dispatch_failed",
|
|
10523
10964
|
payload: {},
|
|
10524
10965
|
target_role: role,
|
|
10525
|
-
model:
|
|
10966
|
+
model: resolvedModel,
|
|
10526
10967
|
...roundField,
|
|
10527
10968
|
effect_id: effectId,
|
|
10528
10969
|
reason: "refused",
|
|
10970
|
+
usage: null,
|
|
10529
10971
|
duration_ms: durationMs
|
|
10530
10972
|
});
|
|
10531
10973
|
await waitForDispatchLine(outboxPath, priorSize, runId, effectId, "dispatch_failed");
|
|
@@ -10538,7 +10980,7 @@ async function dispatchRole(role, agent, prompt2, opts) {
|
|
|
10538
10980
|
event: "dispatched",
|
|
10539
10981
|
payload: {},
|
|
10540
10982
|
target_role: role,
|
|
10541
|
-
model:
|
|
10983
|
+
model: resolvedModel,
|
|
10542
10984
|
...roundField,
|
|
10543
10985
|
effect_id: effectId,
|
|
10544
10986
|
prompt_hash: promptHashOf(prompt2)
|
|
@@ -10547,7 +10989,7 @@ async function dispatchRole(role, agent, prompt2, opts) {
|
|
|
10547
10989
|
}
|
|
10548
10990
|
const timeoutMs = loadConfig()?.dispatch?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
10549
10991
|
return new Promise((resolve3) => {
|
|
10550
|
-
const child = spawn2(binaryPath, opts.resumeId ? vendor.resumeArgs(opts.resumeId) : vendor.args, {
|
|
10992
|
+
const child = spawn2(binaryPath, opts.resumeId ? vendor.resumeArgs(opts.resumeId, opts.model) : vendor.args(opts.model), {
|
|
10551
10993
|
stdio: ["pipe", "pipe", "pipe"],
|
|
10552
10994
|
env: {
|
|
10553
10995
|
...process.env,
|
|
@@ -10562,15 +11004,52 @@ async function dispatchRole(role, agent, prompt2, opts) {
|
|
|
10562
11004
|
let killTimer;
|
|
10563
11005
|
let stdoutBuf = "";
|
|
10564
11006
|
const MAX_STDOUT_BYTES = 1e6;
|
|
11007
|
+
const outputTee = openOutputTee(effectId);
|
|
11008
|
+
if (outputTee.path !== null) {
|
|
11009
|
+
process.stderr.write(`[vinaya dispatch ${effectId}] ${role} via ${agent}: output teed to ${outputTee.path}
|
|
11010
|
+
`);
|
|
11011
|
+
}
|
|
11012
|
+
let renderCarry = "";
|
|
10565
11013
|
child.stdout.on("data", (chunk) => {
|
|
11014
|
+
outputTee.write(chunk);
|
|
10566
11015
|
if (stdoutBuf.length < MAX_STDOUT_BYTES)
|
|
10567
11016
|
stdoutBuf += chunk.toString("utf8");
|
|
11017
|
+
renderCarry += chunk.toString("utf8");
|
|
11018
|
+
const lines = renderCarry.split(`
|
|
11019
|
+
`);
|
|
11020
|
+
renderCarry = lines.pop() ?? "";
|
|
11021
|
+
for (const line of lines) {
|
|
11022
|
+
if (line.trim().length === 0)
|
|
11023
|
+
continue;
|
|
11024
|
+
try {
|
|
11025
|
+
const rendered = vendor.renderEvent(JSON.parse(line));
|
|
11026
|
+
if (rendered)
|
|
11027
|
+
process.stderr.write(`${rendered}
|
|
11028
|
+
`);
|
|
11029
|
+
} catch {}
|
|
11030
|
+
}
|
|
10568
11031
|
});
|
|
10569
|
-
child.stderr.on("data", () => {
|
|
11032
|
+
child.stderr.on("data", (chunk) => {
|
|
11033
|
+
outputTee.write(chunk);
|
|
11034
|
+
});
|
|
11035
|
+
const heartbeatTimer = setInterval(() => {
|
|
11036
|
+
const elapsedS = Math.round((Date.now() - start) / 1000);
|
|
11037
|
+
process.stderr.write(`[vinaya dispatch ${effectId}] ${role} via ${agent}: still running — ${elapsedS}s elapsed (ceiling ${Math.round(timeoutMs / 1000)}s)
|
|
11038
|
+
`);
|
|
11039
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
11040
|
+
const warnLeadMs = timeoutWarningLeadMs(timeoutMs);
|
|
11041
|
+
const warnTimer = setTimeout(() => {
|
|
11042
|
+
process.stderr.write(`[vinaya dispatch ${effectId}] ${role} via ${agent}: approaching timeout — SIGTERM in ~${Math.round(warnLeadMs / 1000)}s unless it finishes first
|
|
11043
|
+
`);
|
|
11044
|
+
}, Math.max(timeoutMs - warnLeadMs, 0));
|
|
10570
11045
|
const timeoutTimer = setTimeout(() => {
|
|
10571
11046
|
timedOut = true;
|
|
11047
|
+
process.stderr.write(`[vinaya dispatch ${effectId}] ${role} via ${agent}: ceiling reached — sending SIGTERM
|
|
11048
|
+
`);
|
|
10572
11049
|
child.kill("SIGTERM");
|
|
10573
11050
|
killTimer = setTimeout(() => {
|
|
11051
|
+
process.stderr.write(`[vinaya dispatch ${effectId}] ${role} via ${agent}: still alive after SIGTERM — sending SIGKILL
|
|
11052
|
+
`);
|
|
10574
11053
|
child.kill("SIGKILL");
|
|
10575
11054
|
}, SIGKILL_GRACE_MS);
|
|
10576
11055
|
}, timeoutMs);
|
|
@@ -10581,6 +11060,9 @@ async function dispatchRole(role, agent, prompt2, opts) {
|
|
|
10581
11060
|
clearTimeout(timeoutTimer);
|
|
10582
11061
|
if (killTimer)
|
|
10583
11062
|
clearTimeout(killTimer);
|
|
11063
|
+
clearInterval(heartbeatTimer);
|
|
11064
|
+
clearTimeout(warnTimer);
|
|
11065
|
+
outputTee.end();
|
|
10584
11066
|
await waitForDispatchLine(outboxPath, priorSize, runId, effectId, event);
|
|
10585
11067
|
resolve3(handle);
|
|
10586
11068
|
}
|
|
@@ -10592,16 +11074,18 @@ async function dispatchRole(role, agent, prompt2, opts) {
|
|
|
10592
11074
|
event: "dispatch_failed",
|
|
10593
11075
|
payload: {},
|
|
10594
11076
|
target_role: role,
|
|
10595
|
-
model:
|
|
11077
|
+
model: resolvedModel,
|
|
10596
11078
|
...roundField,
|
|
10597
11079
|
effect_id: effectId,
|
|
10598
11080
|
reason: "crash",
|
|
11081
|
+
usage: null,
|
|
10599
11082
|
duration_ms: durationMs
|
|
10600
11083
|
});
|
|
10601
11084
|
finish({ exitCode: null, durationMs, usage: null, resumeId: null, timedOut: false, failureReason: "crash" }, "dispatch_failed", priorSize);
|
|
10602
11085
|
});
|
|
10603
11086
|
child.on("exit", (code) => {
|
|
10604
11087
|
const durationMs = Date.now() - start;
|
|
11088
|
+
const usage = vendor.parseUsage(stdoutBuf);
|
|
10605
11089
|
if (timedOut) {
|
|
10606
11090
|
const priorSize2 = sizeOfSafe(outboxPath);
|
|
10607
11091
|
log2({
|
|
@@ -10609,13 +11093,14 @@ async function dispatchRole(role, agent, prompt2, opts) {
|
|
|
10609
11093
|
event: "dispatch_failed",
|
|
10610
11094
|
payload: {},
|
|
10611
11095
|
target_role: role,
|
|
10612
|
-
model:
|
|
11096
|
+
model: resolvedModel,
|
|
10613
11097
|
...roundField,
|
|
10614
11098
|
effect_id: effectId,
|
|
10615
11099
|
reason: "timeout",
|
|
11100
|
+
usage,
|
|
10616
11101
|
duration_ms: durationMs
|
|
10617
11102
|
});
|
|
10618
|
-
finish({ exitCode: code, durationMs, usage
|
|
11103
|
+
finish({ exitCode: code, durationMs, usage, resumeId: null, timedOut: true, failureReason: "timeout" }, "dispatch_failed", priorSize2);
|
|
10619
11104
|
return;
|
|
10620
11105
|
}
|
|
10621
11106
|
if (code !== 0) {
|
|
@@ -10625,24 +11110,42 @@ async function dispatchRole(role, agent, prompt2, opts) {
|
|
|
10625
11110
|
event: "dispatch_failed",
|
|
10626
11111
|
payload: {},
|
|
10627
11112
|
target_role: role,
|
|
10628
|
-
model:
|
|
11113
|
+
model: resolvedModel,
|
|
10629
11114
|
...roundField,
|
|
10630
11115
|
effect_id: effectId,
|
|
10631
11116
|
reason: "crash",
|
|
11117
|
+
usage,
|
|
10632
11118
|
duration_ms: durationMs
|
|
10633
11119
|
});
|
|
10634
|
-
finish({ exitCode: code, durationMs, usage
|
|
11120
|
+
finish({ exitCode: code, durationMs, usage, resumeId: null, timedOut: false, failureReason: "crash" }, "dispatch_failed", priorSize2);
|
|
10635
11121
|
return;
|
|
10636
11122
|
}
|
|
10637
|
-
const usage = vendor.parseUsage(stdoutBuf);
|
|
10638
11123
|
const resumeId = vendor.parseResumeId(stdoutBuf);
|
|
11124
|
+
if (resumeId !== null) {
|
|
11125
|
+
const resumeRecordPath = recordResumeState({
|
|
11126
|
+
resumeId,
|
|
11127
|
+
role,
|
|
11128
|
+
agent,
|
|
11129
|
+
repo,
|
|
11130
|
+
task: opts.task ?? null,
|
|
11131
|
+
pr: opts.pr ?? null,
|
|
11132
|
+
round: opts.round ?? null,
|
|
11133
|
+
effectId,
|
|
11134
|
+
capturedAt: new Date().toISOString()
|
|
11135
|
+
});
|
|
11136
|
+
if (resumeRecordPath !== null) {
|
|
11137
|
+
process.stderr.write(`[vinaya dispatch ${effectId}] ${role} via ${agent}: resumable — session recorded at ${resumeRecordPath}
|
|
11138
|
+
`);
|
|
11139
|
+
}
|
|
11140
|
+
}
|
|
11141
|
+
const reportedModel = vendor.parseModel(stdoutBuf);
|
|
10639
11142
|
const priorSize = sizeOfSafe(outboxPath);
|
|
10640
11143
|
log2({
|
|
10641
11144
|
kind: "dispatch",
|
|
10642
11145
|
event: "outcome_received",
|
|
10643
11146
|
payload: {},
|
|
10644
11147
|
target_role: role,
|
|
10645
|
-
model:
|
|
11148
|
+
model: reportedModel ?? resolvedModel,
|
|
10646
11149
|
...roundField,
|
|
10647
11150
|
effect_id: effectId,
|
|
10648
11151
|
outcome: { type: "plan", issues: [] },
|
|
@@ -10659,19 +11162,19 @@ async function dispatchRole(role, agent, prompt2, opts) {
|
|
|
10659
11162
|
// src/lib/dev-review-loop.ts
|
|
10660
11163
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
10661
11164
|
import { execFileSync as execFileSync17 } from "node:child_process";
|
|
10662
|
-
import { mkdirSync as
|
|
11165
|
+
import { mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync3, readFileSync as readFileSync17, rmSync as rmSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync9 } from "node:fs";
|
|
10663
11166
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10664
|
-
import { join as join18 } from "node:path";
|
|
11167
|
+
import { dirname as dirname8, join as join18 } from "node:path";
|
|
10665
11168
|
|
|
10666
11169
|
// src/commands/review-post.ts
|
|
10667
11170
|
import { execFileSync as execFileSync15 } from "node:child_process";
|
|
10668
|
-
import { readFileSync as readFileSync16, rmSync as rmSync4, writeFileSync as
|
|
11171
|
+
import { readFileSync as readFileSync16, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "node:fs";
|
|
10669
11172
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
10670
11173
|
import { join as join16 } from "node:path";
|
|
10671
11174
|
|
|
10672
11175
|
// src/lib/forge-write.ts
|
|
10673
11176
|
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
10674
|
-
import { existsSync as existsSync12, mkdtempSync, readdirSync as readdirSync6, rmSync as rmSync3, writeFileSync as
|
|
11177
|
+
import { existsSync as existsSync12, mkdtempSync, readdirSync as readdirSync6, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
|
10675
11178
|
import { readFileSync as readFileSync15 } from "node:fs";
|
|
10676
11179
|
import { tmpdir } from "node:os";
|
|
10677
11180
|
import { join as join15 } from "node:path";
|
|
@@ -10709,7 +11212,7 @@ function resolveShippableArgs(args, bodyResult) {
|
|
|
10709
11212
|
}
|
|
10710
11213
|
const dir = mkdtempSync(join15(tmpdir(), "vinaya-forge-body-"));
|
|
10711
11214
|
const tempPath = join15(dir, "body.md");
|
|
10712
|
-
|
|
11215
|
+
writeFileSync6(tempPath, bodyResult.body, "utf8");
|
|
10713
11216
|
const finalArgs = [...args];
|
|
10714
11217
|
const { argIndex, inlineForm } = bodyResult.source;
|
|
10715
11218
|
finalArgs[argIndex] = inlineForm ? `--body-file=${tempPath}` : tempPath;
|
|
@@ -10995,13 +11498,22 @@ var CHECK_ISSUE_CONTENT = "issue-content";
|
|
|
10995
11498
|
var ISSUE_CONTENT_RECOVERY = {
|
|
10996
11499
|
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}`.",
|
|
10997
11500
|
noBriefContent: "Move the brief-shaped section named above out of the Issue body and into the brief — the Issue carries the Planner's durable rationale, not the brief's just-in-time surface — then re-run `{cmd}`.",
|
|
10998
|
-
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}`.'
|
|
11501
|
+
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
|
+
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
|
+
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}`.'
|
|
10999
11505
|
};
|
|
11000
11506
|
function validateIssueContent(input) {
|
|
11001
11507
|
const findings = [
|
|
11002
|
-
[
|
|
11508
|
+
[
|
|
11509
|
+
checkBlastRadiusScope(input.body, input.labels, input.sharedPackages, input.projectPaths, input.issueNumber).errors,
|
|
11510
|
+
"blastRadius"
|
|
11511
|
+
],
|
|
11003
11512
|
[checkNoBriefContent(input.body).errors, "noBriefContent"],
|
|
11004
|
-
[checkRationaleNamesDocs(input.body).errors, "rationaleNamesDocs"]
|
|
11513
|
+
[checkRationaleNamesDocs(input.body).errors, "rationaleNamesDocs"],
|
|
11514
|
+
[checkSurfaceGlobsResolve(input.body, input.resolvesToFile).errors, "surfaceGlobsResolve"],
|
|
11515
|
+
[checkPartsCiteDefinedObjectives(input.body).errors, "partsCiteObjectives"],
|
|
11516
|
+
[checkDocsWithinSurface(input.body, input.issueNumber).errors, "docsWithinSurface"]
|
|
11005
11517
|
];
|
|
11006
11518
|
const errors = [];
|
|
11007
11519
|
for (const [messages, kind] of findings) {
|
|
@@ -11073,7 +11585,9 @@ function validateTaskIssue(body, title, labels, retryCommand, issueNumber) {
|
|
|
11073
11585
|
labels,
|
|
11074
11586
|
sharedPackages: readSharedPackages(),
|
|
11075
11587
|
projectPaths: readProjectPaths(),
|
|
11076
|
-
retryCommand
|
|
11588
|
+
retryCommand,
|
|
11589
|
+
issueNumber,
|
|
11590
|
+
resolvesToFile: (glob) => expandGlob(glob).length > 0
|
|
11077
11591
|
});
|
|
11078
11592
|
if (contentErrors.length > 0)
|
|
11079
11593
|
refuse2(contentErrors);
|
|
@@ -11120,7 +11634,7 @@ ${body}
|
|
|
11120
11634
|
`;
|
|
11121
11635
|
const dir = mkdtempSync(join15(tmpdir(), "vinaya-marked-comment-"));
|
|
11122
11636
|
const tmp = join15(dir, "comment.md");
|
|
11123
|
-
|
|
11637
|
+
writeFileSync6(tmp, commentBody, "utf8");
|
|
11124
11638
|
try {
|
|
11125
11639
|
const out = execFileSync14("gh", [kind, "comment", ref, "--body-file", tmp], {
|
|
11126
11640
|
encoding: "utf8",
|
|
@@ -11769,7 +12283,7 @@ function printOnlyResult(json, role, headSha, body) {
|
|
|
11769
12283
|
}
|
|
11770
12284
|
function postComment(pr, body) {
|
|
11771
12285
|
const tmp = join16(tmpdir2(), `vinaya-review-post-${process.pid}-${Date.now()}.md`);
|
|
11772
|
-
|
|
12286
|
+
writeFileSync7(tmp, body);
|
|
11773
12287
|
try {
|
|
11774
12288
|
return gh(["pr", "comment", pr, "--body-file", tmp]).trim();
|
|
11775
12289
|
} catch (err) {
|
|
@@ -12114,10 +12628,9 @@ Self-verification: clean — re-parsed VERDICT is bound to head ${headSha}.
|
|
|
12114
12628
|
// src/lib/dispatch-task.ts
|
|
12115
12629
|
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
12116
12630
|
import { createHash as createHash7 } from "node:crypto";
|
|
12117
|
-
import { mkdtempSync as mkdtempSync2, rmSync as rmSync5, writeFileSync as
|
|
12631
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
12118
12632
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
12119
12633
|
import { join as join17 } from "node:path";
|
|
12120
|
-
var AEG_BRIEF_V1_MARKER = "<!-- aeg:brief:v1 -->";
|
|
12121
12634
|
var DISPATCH_AGENTS = ["claude", "codex", "gemini"];
|
|
12122
12635
|
|
|
12123
12636
|
class DispatchTaskError extends Error {
|
|
@@ -12125,17 +12638,6 @@ class DispatchTaskError extends Error {
|
|
|
12125
12638
|
function sh3(cmd, args) {
|
|
12126
12639
|
return execFileSync16(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
12127
12640
|
}
|
|
12128
|
-
function contentAfterTwoLines(body) {
|
|
12129
|
-
const first = body.indexOf(`
|
|
12130
|
-
`);
|
|
12131
|
-
if (first === -1)
|
|
12132
|
-
return "";
|
|
12133
|
-
const second = body.indexOf(`
|
|
12134
|
-
`, first + 1);
|
|
12135
|
-
if (second === -1)
|
|
12136
|
-
return "";
|
|
12137
|
-
return body.slice(second + 1);
|
|
12138
|
-
}
|
|
12139
12641
|
function briefHash(brief) {
|
|
12140
12642
|
return createHash7("sha256").update(`${brief}
|
|
12141
12643
|
`).digest("hex");
|
|
@@ -12153,6 +12655,32 @@ function fetchIssueComments(n) {
|
|
|
12153
12655
|
throw new DispatchTaskError(`could not parse \`gh issue view ${n} --json comments\` output.`);
|
|
12154
12656
|
}
|
|
12155
12657
|
}
|
|
12658
|
+
function fetchIssueBody(n) {
|
|
12659
|
+
try {
|
|
12660
|
+
return JSON.parse(sh3("gh", ["issue", "view", String(n), "--json", "body"])).body;
|
|
12661
|
+
} catch (err) {
|
|
12662
|
+
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)}`);
|
|
12663
|
+
}
|
|
12664
|
+
}
|
|
12665
|
+
function extractAgentClass(rawRationaleField) {
|
|
12666
|
+
const m = /agent-class\**\s*[—–-]\s*(\w+)/i.exec(rawRationaleField);
|
|
12667
|
+
const word = m?.[1]?.toLowerCase();
|
|
12668
|
+
return word !== undefined && isAgentClass(word) ? word : null;
|
|
12669
|
+
}
|
|
12670
|
+
function resolveModelFromRationale(agent, rawRationaleField, explicitModel) {
|
|
12671
|
+
if (explicitModel !== undefined)
|
|
12672
|
+
return explicitModel;
|
|
12673
|
+
const agentClass = rawRationaleField !== undefined ? extractAgentClass(rawRationaleField) : null;
|
|
12674
|
+
if (agentClass === null)
|
|
12675
|
+
return;
|
|
12676
|
+
return resolveClassModel(agent, agentClass) ?? undefined;
|
|
12677
|
+
}
|
|
12678
|
+
function resolveModelForDispatch(agent, issue, explicitModel) {
|
|
12679
|
+
if (explicitModel !== undefined)
|
|
12680
|
+
return explicitModel;
|
|
12681
|
+
const raw = parseRationaleFields(fetchIssueBody(issue)).suggestedAgentClass;
|
|
12682
|
+
return resolveModelFromRationale(agent, raw, undefined);
|
|
12683
|
+
}
|
|
12156
12684
|
function findExistingV1Comment(n) {
|
|
12157
12685
|
return fetchIssueComments(n).find((c) => c.body.split(`
|
|
12158
12686
|
`)[0] === AEG_BRIEF_V1_MARKER) ?? null;
|
|
@@ -12178,7 +12706,7 @@ Once \`dispatchRole\` ships, the same \`--agent\` flag on \`task dispatch\` will
|
|
|
12178
12706
|
async function withPromptFile(prompt2, fn) {
|
|
12179
12707
|
const dir = mkdtempSync2(join17(tmpdir3(), "vinaya-dispatch-prompt-"));
|
|
12180
12708
|
const promptFile = join17(dir, "prompt.md");
|
|
12181
|
-
|
|
12709
|
+
writeFileSync8(promptFile, prompt2, "utf8");
|
|
12182
12710
|
try {
|
|
12183
12711
|
return await fn(promptFile);
|
|
12184
12712
|
} finally {
|
|
@@ -12195,10 +12723,11 @@ var defaultDeps2 = {
|
|
|
12195
12723
|
findExistingV1Comment,
|
|
12196
12724
|
postMarkedComment,
|
|
12197
12725
|
resolveDispatchRole,
|
|
12198
|
-
resolveDispatchAuthorization
|
|
12726
|
+
resolveDispatchAuthorization,
|
|
12727
|
+
resolveModelForDispatch
|
|
12199
12728
|
};
|
|
12200
12729
|
async function dispatchTask(input, deps = defaultDeps2) {
|
|
12201
|
-
const { tranche, n, agent } = input;
|
|
12730
|
+
const { tranche, n, agent, model } = input;
|
|
12202
12731
|
{
|
|
12203
12732
|
const { authorized, login } = deps.resolveDispatchAuthorization();
|
|
12204
12733
|
if (!authorized) {
|
|
@@ -12211,18 +12740,30 @@ async function dispatchTask(input, deps = defaultDeps2) {
|
|
|
12211
12740
|
${result.missing.map((m) => ` - ${m}`).join(`
|
|
12212
12741
|
`)}`);
|
|
12213
12742
|
}
|
|
12214
|
-
const
|
|
12743
|
+
const issue = result.issue;
|
|
12744
|
+
const existing = deps.findExistingV1Comment(issue);
|
|
12215
12745
|
if (existing) {
|
|
12216
12746
|
throw new DispatchTaskError(`Task ${n} in tranche \`${tranche}\` is already dispatched — see ${existing.url}`);
|
|
12217
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
|
+
}
|
|
12218
12756
|
const hash = briefHash(result.brief);
|
|
12219
12757
|
const commentBody = `Brief hash: ${hash}
|
|
12220
12758
|
${result.brief}`;
|
|
12221
|
-
const url = deps.postMarkedComment("issue", String(
|
|
12759
|
+
const url = deps.postMarkedComment("issue", String(issue), AEG_BRIEF_V1_MARKER, commentBody);
|
|
12222
12760
|
if (agent) {
|
|
12223
|
-
const dispatchRole2 = await deps.resolveDispatchRole();
|
|
12224
12761
|
if (dispatchRole2) {
|
|
12225
|
-
await withPromptFile(result.brief, (promptFile) => dispatchRole2("developer", agent, result.brief, {
|
|
12762
|
+
await withPromptFile(result.brief, (promptFile) => dispatchRole2("developer", agent, result.brief, {
|
|
12763
|
+
task: issue,
|
|
12764
|
+
promptFile,
|
|
12765
|
+
model: resolvedModel
|
|
12766
|
+
}));
|
|
12226
12767
|
} else {
|
|
12227
12768
|
printManualDispatchInstruction(tranche, n, agent);
|
|
12228
12769
|
}
|
|
@@ -12385,14 +12926,123 @@ function renderReviewerPrompt(facts) {
|
|
|
12385
12926
|
function outboxRoot() {
|
|
12386
12927
|
return join18(GLOBAL_VINAYA_HOME, "outbox");
|
|
12387
12928
|
}
|
|
12929
|
+
function heldVerdictPath(root, task, round, role) {
|
|
12930
|
+
return join18(root, "dev-review-loop", String(task), `round-${round}-${role}.md`);
|
|
12931
|
+
}
|
|
12388
12932
|
function writeHeldVerdict(root, task, round, role, renderedComment) {
|
|
12389
12933
|
const dir = join18(root, "dev-review-loop", String(task));
|
|
12390
|
-
|
|
12391
|
-
|
|
12934
|
+
mkdirSync5(dir, { recursive: true });
|
|
12935
|
+
writeFileSync9(heldVerdictPath(root, task, round, role), renderedComment, "utf8");
|
|
12392
12936
|
}
|
|
12393
12937
|
function reviewerWorkDir(root, task, round, role) {
|
|
12394
12938
|
return join18(root, "dev-review-loop", String(task), `round-${round}-${role}-work`);
|
|
12395
12939
|
}
|
|
12940
|
+
function forgeEffectPath(root, task, key) {
|
|
12941
|
+
return join18(root, "dev-review-loop", String(task), `effect-${key}.json`);
|
|
12942
|
+
}
|
|
12943
|
+
function readForgeEffect(path) {
|
|
12944
|
+
const raw = readIfExists(path);
|
|
12945
|
+
if (!raw)
|
|
12946
|
+
return null;
|
|
12947
|
+
try {
|
|
12948
|
+
return JSON.parse(raw);
|
|
12949
|
+
} catch {
|
|
12950
|
+
return null;
|
|
12951
|
+
}
|
|
12952
|
+
}
|
|
12953
|
+
function writeForgeEffect(path, record) {
|
|
12954
|
+
mkdirSync5(dirname8(path), { recursive: true });
|
|
12955
|
+
writeFileSync9(path, JSON.stringify(record), "utf8");
|
|
12956
|
+
}
|
|
12957
|
+
function postForgeEffectOnce(root, task, key, poster) {
|
|
12958
|
+
const path = forgeEffectPath(root, task, key);
|
|
12959
|
+
const existing = readForgeEffect(path);
|
|
12960
|
+
if (existing?.status === "posted" && existing.url)
|
|
12961
|
+
return existing.url;
|
|
12962
|
+
const effectId = existing?.effectId ?? randomUUID3();
|
|
12963
|
+
writeForgeEffect(path, { effectId, status: "started" });
|
|
12964
|
+
const url = poster();
|
|
12965
|
+
writeForgeEffect(path, { effectId, status: "posted", url });
|
|
12966
|
+
return url;
|
|
12967
|
+
}
|
|
12968
|
+
function postPrComment(pr, body) {
|
|
12969
|
+
const dir = mkdtempSync3(join18(tmpdir4(), "vinaya-dev-review-loop-comment-"));
|
|
12970
|
+
const tmp = join18(dir, "comment.md");
|
|
12971
|
+
writeFileSync9(tmp, body, "utf8");
|
|
12972
|
+
try {
|
|
12973
|
+
return execFileSync17("gh", ["pr", "comment", String(pr), "--body-file", tmp], {
|
|
12974
|
+
encoding: "utf8",
|
|
12975
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
12976
|
+
}).trim();
|
|
12977
|
+
} finally {
|
|
12978
|
+
rmSync6(dir, { recursive: true, force: true });
|
|
12979
|
+
}
|
|
12980
|
+
}
|
|
12981
|
+
function fetchAllPrCommentBodies(pr) {
|
|
12982
|
+
const out = sh4("gh", ["pr", "view", String(pr), "--json", "comments"]);
|
|
12983
|
+
return principalBodies(markerComments(out), principalAllowlist());
|
|
12984
|
+
}
|
|
12985
|
+
function publishRound(root, input) {
|
|
12986
|
+
const { task, round, prNumber, expectedHead } = input;
|
|
12987
|
+
const reviewerBody = readIfExists(heldVerdictPath(root, task, round, "reviewer"));
|
|
12988
|
+
const securityBody = readIfExists(heldVerdictPath(root, task, round, "security"));
|
|
12989
|
+
if (!reviewerBody || !securityBody) {
|
|
12990
|
+
throw new Error(`publishRound: missing held verdict file(s) for task ${task} round ${round} — writeHeldVerdict should have written both before assessRound ever returned 'publish'.`);
|
|
12991
|
+
}
|
|
12992
|
+
postForgeEffectOnce(root, task, `${round}-reviewer-verdict`, () => postPrComment(prNumber, reviewerBody));
|
|
12993
|
+
const postedReviewer = extractCodeReviewVerdict(fetchAllPrCommentBodies(prNumber));
|
|
12994
|
+
if (postedReviewer.danglingNote || postedReviewer.headSha !== expectedHead) {
|
|
12995
|
+
throw new Error(`publishRound: posted reviewer verdict does not re-parse clean through extractCodeReviewVerdict bound to ${expectedHead}: ${postedReviewer.danglingNote ?? `headSha read back as ${String(postedReviewer.headSha)}`}`);
|
|
12996
|
+
}
|
|
12997
|
+
postForgeEffectOnce(root, task, `${round}-security-verdict`, () => postPrComment(prNumber, securityBody));
|
|
12998
|
+
const postedSecurity = extractSecurityReviewVerdict(fetchAllPrCommentBodies(prNumber));
|
|
12999
|
+
if (postedSecurity.danglingNote || postedSecurity.headSha !== expectedHead) {
|
|
13000
|
+
throw new Error(`publishRound: posted security verdict does not re-parse clean through extractSecurityReviewVerdict bound to ${expectedHead}: ${postedSecurity.danglingNote ?? `headSha read back as ${String(postedSecurity.headSha)}`}`);
|
|
13001
|
+
}
|
|
13002
|
+
const summary = renderSummary(input.journal);
|
|
13003
|
+
const summaryAsCodeReview = extractCodeReviewVerdict([summary]);
|
|
13004
|
+
const summaryAsSecurity = extractSecurityReviewVerdict([summary]);
|
|
13005
|
+
if (summaryAsCodeReview.danglingNote === null || summaryAsSecurity.danglingNote === null) {
|
|
13006
|
+
throw new Error("publishRound: the rendered summary re-parses as a real verdict through the gate's own extractors — refusing to post it (a summary mistaken for a verdict decides a merge).");
|
|
13007
|
+
}
|
|
13008
|
+
postForgeEffectOnce(root, task, `${round}-summary`, () => postPrComment(prNumber, summary));
|
|
13009
|
+
}
|
|
13010
|
+
function pauseMarker(reason) {
|
|
13011
|
+
return `<!-- aeg:loop:paused:${reason} -->`;
|
|
13012
|
+
}
|
|
13013
|
+
function renderPauseComment(prNumber, reason) {
|
|
13014
|
+
return [
|
|
13015
|
+
`The dev-review-loop paused: ${reason}.`,
|
|
13016
|
+
"",
|
|
13017
|
+
"A Principal ruling is needed before this can continue. Once one is posted on this PR, resume with:",
|
|
13018
|
+
"",
|
|
13019
|
+
"```",
|
|
13020
|
+
`vinaya dev-review-loop --resume ${prNumber}`,
|
|
13021
|
+
"```"
|
|
13022
|
+
].join(`
|
|
13023
|
+
`);
|
|
13024
|
+
}
|
|
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)));
|
|
13027
|
+
}
|
|
13028
|
+
function pauseStatePath(root, task) {
|
|
13029
|
+
return join18(root, "dev-review-loop", String(task), "pause-state.json");
|
|
13030
|
+
}
|
|
13031
|
+
function writePauseState(root, state) {
|
|
13032
|
+
const path = pauseStatePath(root, state.task);
|
|
13033
|
+
mkdirSync5(dirname8(path), { recursive: true });
|
|
13034
|
+
writeFileSync9(path, JSON.stringify(state), "utf8");
|
|
13035
|
+
}
|
|
13036
|
+
function readPauseState(root, task) {
|
|
13037
|
+
const raw = readIfExists(pauseStatePath(root, task));
|
|
13038
|
+
if (!raw)
|
|
13039
|
+
return null;
|
|
13040
|
+
try {
|
|
13041
|
+
return JSON.parse(raw);
|
|
13042
|
+
} catch {
|
|
13043
|
+
return null;
|
|
13044
|
+
}
|
|
13045
|
+
}
|
|
12396
13046
|
var CONFIDENCE_PROMPT_LINE = "Before ending this turn, write your confidence in this round's changes to a file named `.vinaya-confidence` at the root of your worktree, containing exactly one line: `CONFIDENCE: <0-100> — <one-sentence reason>` (a whole number from 0 to 100, an em dash, then your reason in one sentence). This is read by the review loop before it decides the next step — do not skip it.";
|
|
12397
13047
|
var CONFIDENCE_LINE = /^CONFIDENCE:\s*(\d{1,3})\s*(?:—|-)\s*(.+)$/m;
|
|
12398
13048
|
function parseConfidenceReply(replyText) {
|
|
@@ -12430,7 +13080,7 @@ function readIfExists(path) {
|
|
|
12430
13080
|
function withPromptFile2(prompt2, fn) {
|
|
12431
13081
|
const dir = mkdtempSync3(join18(tmpdir4(), "vinaya-dev-review-loop-prompt-"));
|
|
12432
13082
|
const promptFile = join18(dir, "prompt.md");
|
|
12433
|
-
|
|
13083
|
+
writeFileSync9(promptFile, prompt2, "utf8");
|
|
12434
13084
|
try {
|
|
12435
13085
|
return fn(promptFile);
|
|
12436
13086
|
} finally {
|
|
@@ -12675,18 +13325,66 @@ function renderReviewerDispatchPrompt(role, facts, workDir) {
|
|
|
12675
13325
|
|
|
12676
13326
|
${instructions}`;
|
|
12677
13327
|
}
|
|
13328
|
+
function taskFromPrBody(body) {
|
|
13329
|
+
const m = /Closes #(\d+)/i.exec(body);
|
|
13330
|
+
return m ? Number(m[1]) : null;
|
|
13331
|
+
}
|
|
13332
|
+
function fetchPrBody2(pr) {
|
|
13333
|
+
const out = sh4("gh", ["pr", "view", String(pr), "--json", "body"]);
|
|
13334
|
+
return JSON.parse(out).body;
|
|
13335
|
+
}
|
|
13336
|
+
function routeCompletionEvents(events, decisionType) {
|
|
13337
|
+
if (decisionType !== "publish")
|
|
13338
|
+
return { toLogNow: [...events], toDeferUntilPublish: [] };
|
|
13339
|
+
return {
|
|
13340
|
+
toLogNow: events.filter((e) => e.event !== "journal_finalized"),
|
|
13341
|
+
toDeferUntilPublish: events.filter((e) => e.event === "journal_finalized")
|
|
13342
|
+
};
|
|
13343
|
+
}
|
|
12678
13344
|
async function devReviewLoop(input, deps = {}) {
|
|
12679
13345
|
const d = { ...defaultDeps3(), ...deps };
|
|
13346
|
+
const root = d.outboxRoot();
|
|
13347
|
+
let task;
|
|
13348
|
+
let branch;
|
|
13349
|
+
let prNumber = -1;
|
|
13350
|
+
let resumeFrom = null;
|
|
13351
|
+
if ("resumePr" in input) {
|
|
13352
|
+
const resumePr = input.resumePr;
|
|
13353
|
+
const closesTask = taskFromPrBody(fetchPrBody2(resumePr));
|
|
13354
|
+
if (closesTask === null) {
|
|
13355
|
+
throw new Error(`devReviewLoop --resume: PR #${resumePr}'s body carries no \`Closes #N\` reference — cannot derive its task.`);
|
|
13356
|
+
}
|
|
13357
|
+
const held = readPauseState(root, closesTask);
|
|
13358
|
+
if (!held) {
|
|
13359
|
+
throw new Error(`devReviewLoop --resume: no held pause state found for task ${closesTask} (PR #${resumePr}) — nothing to resume.`);
|
|
13360
|
+
}
|
|
13361
|
+
if (held.prNumber !== resumePr) {
|
|
13362
|
+
throw new Error(`devReviewLoop --resume: task ${closesTask}'s held pause state names PR #${held.prNumber}, not PR #${resumePr}.`);
|
|
13363
|
+
}
|
|
13364
|
+
const rulings = d.fetchRulings(resumePr);
|
|
13365
|
+
if (rulings.length === 0) {
|
|
13366
|
+
throw new Error(`devReviewLoop --resume: PR #${resumePr} carries no Principal ruling comment yet — nothing to resume from.`);
|
|
13367
|
+
}
|
|
13368
|
+
const currentHead = d.resolveHead(held.branch);
|
|
13369
|
+
if (currentHead !== held.head) {
|
|
13370
|
+
throw new Error(`devReviewLoop --resume: PR #${resumePr}'s head has moved since it paused (paused at ${held.head}, now ${currentHead}) — restart from round ${held.round} against the new head; resume never silently replays from round 1.`);
|
|
13371
|
+
}
|
|
13372
|
+
task = held.task;
|
|
13373
|
+
branch = held.branch;
|
|
13374
|
+
prNumber = held.prNumber;
|
|
13375
|
+
resumeFrom = held;
|
|
13376
|
+
} else {
|
|
13377
|
+
task = input.task;
|
|
13378
|
+
branch = d.developerBranchFor(task);
|
|
13379
|
+
}
|
|
12680
13380
|
const { log: log2, runId } = createLogSink();
|
|
12681
13381
|
if (!process.env.VINAYA_RUN_ID)
|
|
12682
13382
|
process.env.VINAYA_RUN_ID = runId;
|
|
12683
|
-
process.env.VINAYA_TASK = String(
|
|
13383
|
+
process.env.VINAYA_TASK = String(task);
|
|
12684
13384
|
const repo = await resolveRepo().catch(() => null);
|
|
12685
|
-
const branch = d.developerBranchFor(input.task);
|
|
12686
|
-
const root = d.outboxRoot();
|
|
12687
13385
|
const repoRoot2 = d.repoRoot();
|
|
12688
13386
|
const confidenceFilePath = join18(repoRoot2, ".worktrees", branch, CONFIDENCE_FILE_NAME);
|
|
12689
|
-
const loopOutboxPath = outboxPathFor({ outboxRoot: () => root }, repo,
|
|
13387
|
+
const loopOutboxPath = outboxPathFor({ outboxRoot: () => root }, repo, task);
|
|
12690
13388
|
async function logEvents(events) {
|
|
12691
13389
|
for (const e of events) {
|
|
12692
13390
|
const priorSize = sizeOfSafe2(loopOutboxPath);
|
|
@@ -12696,20 +13394,20 @@ async function devReviewLoop(input, deps = {}) {
|
|
|
12696
13394
|
}
|
|
12697
13395
|
const config = {
|
|
12698
13396
|
loopId: randomUUID3(),
|
|
12699
|
-
task
|
|
13397
|
+
task,
|
|
12700
13398
|
reviewers: ["code-reviewer", "security"],
|
|
12701
13399
|
models: { developer: input.agent, "code-reviewer": input.agent, security: input.agent }
|
|
12702
13400
|
};
|
|
12703
13401
|
let state = initialLoopState(config);
|
|
12704
|
-
let round = 1;
|
|
13402
|
+
let round = resumeFrom ? resumeFrom.round : 1;
|
|
12705
13403
|
let devResumeId = null;
|
|
12706
13404
|
let devDispatchSucceededBefore = false;
|
|
12707
|
-
let prNumber = -1;
|
|
12708
13405
|
let lastReviewContext = null;
|
|
13406
|
+
let resumedDispatch = resumeFrom !== null;
|
|
12709
13407
|
async function dispatchDeveloper(prompt2, roundNum) {
|
|
12710
13408
|
const isResume = devResumeId !== null;
|
|
12711
13409
|
const handle = await withPromptFile2(prompt2, (promptFile) => d.dispatchRole("developer", input.agent, prompt2, {
|
|
12712
|
-
task
|
|
13410
|
+
task,
|
|
12713
13411
|
round: roundNum,
|
|
12714
13412
|
resumeId: devResumeId ?? undefined,
|
|
12715
13413
|
promptFile
|
|
@@ -12723,14 +13421,14 @@ async function devReviewLoop(input, deps = {}) {
|
|
|
12723
13421
|
return handle;
|
|
12724
13422
|
}
|
|
12725
13423
|
async function dispatchReviewer(role, roundNum, facts) {
|
|
12726
|
-
const workDir = reviewerWorkDir(root,
|
|
12727
|
-
|
|
13424
|
+
const workDir = reviewerWorkDir(root, task, roundNum, role);
|
|
13425
|
+
mkdirSync5(workDir, { recursive: true });
|
|
12728
13426
|
const dispatchRoleName = role === "reviewer" ? "code-reviewer" : "security";
|
|
12729
13427
|
const prompt2 = renderReviewerDispatchPrompt(role, facts, workDir);
|
|
12730
|
-
const handle = await withPromptFile2(prompt2, (promptFile) => d.dispatchRole(dispatchRoleName, input.agent, prompt2, { task
|
|
13428
|
+
const handle = await withPromptFile2(prompt2, (promptFile) => d.dispatchRole(dispatchRoleName, input.agent, prompt2, { task, round: roundNum, promptFile }));
|
|
12731
13429
|
await assertDispatchOrEscalate(handle, input.agent, false, false);
|
|
12732
|
-
const parsed = buildVerdictFromReport(role, workDir, facts.head, input.agent,
|
|
12733
|
-
writeHeldVerdict(root,
|
|
13430
|
+
const parsed = buildVerdictFromReport(role, workDir, facts.head, input.agent, task, handle);
|
|
13431
|
+
writeHeldVerdict(root, task, roundNum, role, parsed.rendered);
|
|
12734
13432
|
return parsed;
|
|
12735
13433
|
}
|
|
12736
13434
|
function computeStats(head, roundStartMs2) {
|
|
@@ -12754,18 +13452,27 @@ async function devReviewLoop(input, deps = {}) {
|
|
|
12754
13452
|
} catch {}
|
|
12755
13453
|
return content ? parseConfidenceReply(content) : "absent";
|
|
12756
13454
|
}
|
|
12757
|
-
|
|
12758
|
-
|
|
12759
|
-
|
|
12760
|
-
|
|
13455
|
+
let roundStartMs = d.now();
|
|
13456
|
+
if (resumeFrom) {
|
|
13457
|
+
const rulings = d.fetchRulings(prNumber);
|
|
13458
|
+
lastReviewContext = rulings.map((r, i) => `${i + 1}. ${r}`).join(`
|
|
13459
|
+
`);
|
|
13460
|
+
} else {
|
|
13461
|
+
const brief = d.fetchFrozenBrief(task);
|
|
13462
|
+
await dispatchDeveloper(brief, round);
|
|
13463
|
+
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);
|
|
13464
|
+
}
|
|
12761
13465
|
let decision = { type: "dispatch_developer" };
|
|
12762
|
-
let
|
|
12763
|
-
let
|
|
13466
|
+
let firstPass = !resumeFrom;
|
|
13467
|
+
let pendingCompletionEvents = [];
|
|
12764
13468
|
while (true) {
|
|
12765
13469
|
if (decision.type === "dispatch_developer") {
|
|
12766
13470
|
if (!firstPass) {
|
|
12767
13471
|
const prompt2 = [
|
|
12768
|
-
|
|
13472
|
+
resumedDispatch ? `Principal ruling on this pause:
|
|
13473
|
+
|
|
13474
|
+
${lastReviewContext}
|
|
13475
|
+
` : lastReviewContext ? `Round ${round} review findings:
|
|
12769
13476
|
|
|
12770
13477
|
${lastReviewContext}
|
|
12771
13478
|
` : "CI was red on the last head — fix and push.",
|
|
@@ -12776,6 +13483,7 @@ ${lastReviewContext}
|
|
|
12776
13483
|
`);
|
|
12777
13484
|
roundStartMs = d.now();
|
|
12778
13485
|
await dispatchDeveloper(prompt2, round);
|
|
13486
|
+
resumedDispatch = false;
|
|
12779
13487
|
}
|
|
12780
13488
|
firstPass = false;
|
|
12781
13489
|
const gate = await waitForGreenGate(roundStartMs);
|
|
@@ -12785,7 +13493,7 @@ ${lastReviewContext}
|
|
|
12785
13493
|
state = result.state;
|
|
12786
13494
|
decision = result.decision;
|
|
12787
13495
|
await logEvents(result.events);
|
|
12788
|
-
d.flushOutbox(
|
|
13496
|
+
d.flushOutbox(task);
|
|
12789
13497
|
} else if (decision.type === "ask_confidence") {
|
|
12790
13498
|
const reaskPrompt = `Your last reply did not include a valid confidence line.
|
|
12791
13499
|
|
|
@@ -12799,11 +13507,11 @@ ${CONFIDENCE_PROMPT_LINE}`;
|
|
|
12799
13507
|
state = result.state;
|
|
12800
13508
|
decision = result.decision;
|
|
12801
13509
|
await logEvents(result.events);
|
|
12802
|
-
d.flushOutbox(
|
|
13510
|
+
d.flushOutbox(task);
|
|
12803
13511
|
} else if (decision.type === "dispatch_reviewers") {
|
|
12804
13512
|
const head = d.resolveHead(branch);
|
|
12805
13513
|
const ciConclusion = d.fetchCiConclusion(head);
|
|
12806
|
-
const objectives = d.fetchIssueObjectives(
|
|
13514
|
+
const objectives = d.fetchIssueObjectives(task);
|
|
12807
13515
|
const rulings = d.fetchRulings(prNumber);
|
|
12808
13516
|
const facts = { objectives, rulings, head, ciConclusion };
|
|
12809
13517
|
const [reviewer, security] = await Promise.all([
|
|
@@ -12819,14 +13527,39 @@ ${security.rendered}`;
|
|
|
12819
13527
|
const result = assessRound(state, obs);
|
|
12820
13528
|
state = result.state;
|
|
12821
13529
|
decision = result.decision;
|
|
12822
|
-
|
|
12823
|
-
|
|
13530
|
+
const routed = routeCompletionEvents(result.events, decision.type);
|
|
13531
|
+
pendingCompletionEvents = routed.toDeferUntilPublish;
|
|
13532
|
+
await logEvents(routed.toLogNow);
|
|
13533
|
+
d.flushOutbox(task);
|
|
12824
13534
|
if (decision.type === "dispatch_developer")
|
|
12825
13535
|
round += 1;
|
|
12826
13536
|
}
|
|
12827
|
-
if (decision.type === "publish"
|
|
12828
|
-
|
|
12829
|
-
|
|
13537
|
+
if (decision.type === "publish") {
|
|
13538
|
+
publishRound(root, {
|
|
13539
|
+
task,
|
|
13540
|
+
round,
|
|
13541
|
+
prNumber,
|
|
13542
|
+
expectedHead: d.resolveHead(branch),
|
|
13543
|
+
journal: { rounds: state.rounds }
|
|
13544
|
+
});
|
|
13545
|
+
await logEvents(pendingCompletionEvents);
|
|
13546
|
+
d.flushOutbox(task);
|
|
13547
|
+
return { finalDecision: decision, prNumber, task };
|
|
13548
|
+
}
|
|
13549
|
+
if (decision.type === "pause") {
|
|
13550
|
+
const pauseHead = d.resolveHead(branch);
|
|
13551
|
+
writePauseState(root, {
|
|
13552
|
+
task,
|
|
13553
|
+
round,
|
|
13554
|
+
head: pauseHead,
|
|
13555
|
+
branch,
|
|
13556
|
+
prNumber,
|
|
13557
|
+
reason: decision.reason,
|
|
13558
|
+
pausedAt: new Date().toISOString()
|
|
13559
|
+
});
|
|
13560
|
+
postPauseComment(root, task, round, pauseHead, prNumber, decision.reason);
|
|
13561
|
+
d.flushOutbox(task);
|
|
13562
|
+
return { finalDecision: decision, prNumber, task };
|
|
12830
13563
|
}
|
|
12831
13564
|
}
|
|
12832
13565
|
}
|
|
@@ -12834,27 +13567,24 @@ ${security.rendered}`;
|
|
|
12834
13567
|
// src/commands/dev-review-loop.ts
|
|
12835
13568
|
function parseArgs2(args) {
|
|
12836
13569
|
let task;
|
|
13570
|
+
let resumePr;
|
|
12837
13571
|
let agent;
|
|
12838
13572
|
let json = false;
|
|
12839
13573
|
for (let i = 0;i < args.length; i++) {
|
|
12840
13574
|
const a = args[i];
|
|
12841
13575
|
if (a === "--task")
|
|
12842
13576
|
task = Number(args[++i]);
|
|
13577
|
+
else if (a === "--resume")
|
|
13578
|
+
resumePr = Number(args[++i]);
|
|
12843
13579
|
else if (a === "--agent")
|
|
12844
13580
|
agent = args[++i];
|
|
12845
13581
|
else if (a === "--json")
|
|
12846
13582
|
json = true;
|
|
12847
13583
|
}
|
|
12848
|
-
return { task, agent, json };
|
|
13584
|
+
return { task, resumePr, agent, json };
|
|
12849
13585
|
}
|
|
12850
13586
|
async function devReviewLoopCommand(args) {
|
|
12851
13587
|
const parsed = parseArgs2(args);
|
|
12852
|
-
if (parsed.task === undefined || !Number.isInteger(parsed.task) || parsed.task <= 0) {
|
|
12853
|
-
process.stderr.write(`vinaya dev-review-loop: --task <n> is required (a positive integer Issue number)
|
|
12854
|
-
`);
|
|
12855
|
-
process.exit(1);
|
|
12856
|
-
}
|
|
12857
|
-
const task = parsed.task;
|
|
12858
13588
|
const agentRaw = parsed.agent ?? loadConfig()?.dispatch?.agent;
|
|
12859
13589
|
if (!agentRaw) {
|
|
12860
13590
|
process.stderr.write(`vinaya dev-review-loop: --agent <claude|codex|gemini> is required (or set dispatch.agent in vinaya.config.json)
|
|
@@ -12867,16 +13597,34 @@ async function devReviewLoopCommand(args) {
|
|
|
12867
13597
|
process.exit(1);
|
|
12868
13598
|
}
|
|
12869
13599
|
const agent = agentRaw;
|
|
12870
|
-
|
|
13600
|
+
let input;
|
|
13601
|
+
if (parsed.resumePr !== undefined) {
|
|
13602
|
+
if (!Number.isInteger(parsed.resumePr) || parsed.resumePr <= 0) {
|
|
13603
|
+
process.stderr.write(`vinaya dev-review-loop: --resume <pr> requires a positive integer PR number
|
|
13604
|
+
`);
|
|
13605
|
+
process.exit(1);
|
|
13606
|
+
}
|
|
13607
|
+
input = { resumePr: parsed.resumePr, agent };
|
|
13608
|
+
} else {
|
|
13609
|
+
if (parsed.task === undefined || !Number.isInteger(parsed.task) || parsed.task <= 0) {
|
|
13610
|
+
process.stderr.write(`vinaya dev-review-loop: --task <n> is required (a positive integer Issue number)
|
|
13611
|
+
`);
|
|
13612
|
+
process.exit(1);
|
|
13613
|
+
}
|
|
13614
|
+
input = { task: parsed.task, agent };
|
|
13615
|
+
}
|
|
13616
|
+
const result = await devReviewLoop(input);
|
|
12871
13617
|
if (parsed.json) {
|
|
12872
|
-
printJson({ finalDecision: result.finalDecision, prNumber: result.prNumber });
|
|
13618
|
+
printJson({ finalDecision: result.finalDecision, prNumber: result.prNumber, task: result.task });
|
|
12873
13619
|
} else if (result.finalDecision.type === "publish") {
|
|
12874
|
-
process.stdout.write(`vinaya dev-review-loop: task ${task}, PR #${result.prNumber} — publish
|
|
13620
|
+
process.stdout.write(`vinaya dev-review-loop: task ${result.task}, PR #${result.prNumber} — publish
|
|
12875
13621
|
`);
|
|
12876
13622
|
} else if (result.finalDecision.type === "pause") {
|
|
12877
|
-
process.stdout.write(`vinaya dev-review-loop: task ${task}, PR #${result.prNumber} — paused (${result.finalDecision.reason})
|
|
13623
|
+
process.stdout.write(`vinaya dev-review-loop: task ${result.task}, PR #${result.prNumber} — paused (${result.finalDecision.reason})
|
|
12878
13624
|
`);
|
|
12879
13625
|
}
|
|
13626
|
+
if (result.finalDecision.type === "pause")
|
|
13627
|
+
process.exit(1);
|
|
12880
13628
|
}
|
|
12881
13629
|
|
|
12882
13630
|
// src/commands/dispatch.ts
|
|
@@ -12884,8 +13632,8 @@ import { readFileSync as readFileSync19 } from "node:fs";
|
|
|
12884
13632
|
|
|
12885
13633
|
// src/commands/log.ts
|
|
12886
13634
|
import { execFileSync as execFileSync18 } from "node:child_process";
|
|
12887
|
-
import { lstatSync, readFileSync as readFileSync18, statSync as statSync2, writeFileSync as
|
|
12888
|
-
import { homedir as
|
|
13635
|
+
import { lstatSync, readFileSync as readFileSync18, statSync as statSync2, writeFileSync as writeFileSync10, rmSync as rmSync7 } from "node:fs";
|
|
13636
|
+
import { homedir as homedir4, tmpdir as tmpdir5 } from "node:os";
|
|
12889
13637
|
import { join as join19 } from "node:path";
|
|
12890
13638
|
var FORGE_COMMENT_MAX_CHARS = 65536;
|
|
12891
13639
|
function isEnoent2(err) {
|
|
@@ -12956,7 +13704,7 @@ function refuse22(error) {
|
|
|
12956
13704
|
process.exit(2);
|
|
12957
13705
|
}
|
|
12958
13706
|
var SAFE_PATH_SEGMENT2 = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
12959
|
-
function
|
|
13707
|
+
function isSafeRepoSegment3(segment) {
|
|
12960
13708
|
return SAFE_PATH_SEGMENT2.test(segment) && !segment.includes("..");
|
|
12961
13709
|
}
|
|
12962
13710
|
function outboxPathFor2(deps, target) {
|
|
@@ -12989,7 +13737,7 @@ function parseOutboxLine(raw, index) {
|
|
|
12989
13737
|
if (!result.success) {
|
|
12990
13738
|
throw new CorruptOutboxLineError(index, result.error.issues[0]?.message ?? "schema violation");
|
|
12991
13739
|
}
|
|
12992
|
-
const redacted = redact(result.data,
|
|
13740
|
+
const redacted = redact(result.data, homedir4());
|
|
12993
13741
|
return { postLine: JSON.stringify(redacted), runId: redacted.meta.run_id, seq: redacted.meta.seq };
|
|
12994
13742
|
}
|
|
12995
13743
|
function renderChunk(runId, seqFrom, seqTo, postLines) {
|
|
@@ -13062,7 +13810,7 @@ function issueFromPr(prNumber) {
|
|
|
13062
13810
|
}
|
|
13063
13811
|
function postChunk(op, targetId, body, index) {
|
|
13064
13812
|
const tmp = join19(tmpdir5(), `vinaya-log-flush-${process.pid}-${Date.now()}-${index}.md`);
|
|
13065
|
-
|
|
13813
|
+
writeFileSync10(tmp, body, { flag: "wx" });
|
|
13066
13814
|
try {
|
|
13067
13815
|
const out = op === "pr.comment" ? gh2(["pr", "comment", targetId, "--body-file", tmp]) : gh2(["issue", "comment", targetId, "--body-file", tmp]);
|
|
13068
13816
|
const match = /#issuecomment-(\d+)/.exec(out);
|
|
@@ -13104,7 +13852,7 @@ async function logFlushCommand(args) {
|
|
|
13104
13852
|
const target = parsed.pr !== undefined ? { pr: parsed.pr } : { issue: issueNumber };
|
|
13105
13853
|
const forgeTargetId = parsed.pr !== undefined ? String(parsed.pr) : String(issueNumber);
|
|
13106
13854
|
const resolved = await resolveRepo();
|
|
13107
|
-
const repo = resolved &&
|
|
13855
|
+
const repo = resolved && isSafeRepoSegment3(resolved.owner) && isSafeRepoSegment3(resolved.repo) ? resolved : null;
|
|
13108
13856
|
const outboxRoot2 = () => join19(GLOBAL_VINAYA_HOME, "outbox");
|
|
13109
13857
|
const path = outboxPathFor2({ outboxRoot: outboxRoot2, repo }, { issue: issueNumber });
|
|
13110
13858
|
let lstat;
|
|
@@ -13185,7 +13933,7 @@ async function logFlushCommand(args) {
|
|
|
13185
13933
|
`).join("");
|
|
13186
13934
|
const liveNow = readFileSync18(path);
|
|
13187
13935
|
const tail = liveNow.subarray(Math.min(startOffset, liveNow.byteLength));
|
|
13188
|
-
|
|
13936
|
+
writeFileSync10(path, Buffer.concat([Buffer.from(unposted, "utf8"), tail]));
|
|
13189
13937
|
if (!finalLanded) {
|
|
13190
13938
|
emitCheckError({
|
|
13191
13939
|
schema: CHECK_SCHEMA_VERSION,
|
|
@@ -13210,6 +13958,7 @@ async function logFlushCommand(args) {
|
|
|
13210
13958
|
function parseArgs4(args) {
|
|
13211
13959
|
const role = args[0];
|
|
13212
13960
|
let agent;
|
|
13961
|
+
let model;
|
|
13213
13962
|
let promptFile;
|
|
13214
13963
|
let task;
|
|
13215
13964
|
let pr;
|
|
@@ -13220,6 +13969,8 @@ function parseArgs4(args) {
|
|
|
13220
13969
|
const a = args[i];
|
|
13221
13970
|
if (a === "--agent")
|
|
13222
13971
|
agent = args[++i];
|
|
13972
|
+
else if (a === "--model")
|
|
13973
|
+
model = args[++i];
|
|
13223
13974
|
else if (a === "--prompt-file")
|
|
13224
13975
|
promptFile = args[++i];
|
|
13225
13976
|
else if (a === "--task")
|
|
@@ -13233,7 +13984,7 @@ function parseArgs4(args) {
|
|
|
13233
13984
|
else if (a === "--json")
|
|
13234
13985
|
json = true;
|
|
13235
13986
|
}
|
|
13236
|
-
return { role, agent, promptFile, task, pr, round, resume, json };
|
|
13987
|
+
return { role, agent, model, promptFile, task, pr, round, resume, json };
|
|
13237
13988
|
}
|
|
13238
13989
|
async function dispatchCommand(args) {
|
|
13239
13990
|
const parsed = parseArgs4(args);
|
|
@@ -13280,6 +14031,7 @@ async function dispatchCommand(args) {
|
|
|
13280
14031
|
pr: parsed.pr,
|
|
13281
14032
|
round: parsed.round,
|
|
13282
14033
|
resumeId: parsed.resume,
|
|
14034
|
+
model: parsed.model,
|
|
13283
14035
|
promptFile
|
|
13284
14036
|
});
|
|
13285
14037
|
if (parsed.json) {
|
|
@@ -13452,8 +14204,8 @@ function envDeclarationWarning(name) {
|
|
|
13452
14204
|
}
|
|
13453
14205
|
|
|
13454
14206
|
// src/lib/registry-write.ts
|
|
13455
|
-
import { existsSync as existsSync14, mkdirSync as
|
|
13456
|
-
import { dirname as
|
|
14207
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync22, writeFileSync as writeFileSync11 } from "node:fs";
|
|
14208
|
+
import { dirname as dirname9, join as join21 } from "node:path";
|
|
13457
14209
|
var PROJECTS_REGISTRY_PATH = ".vinaya/projects.md";
|
|
13458
14210
|
var CONFIG_FILE_PATH = "vinaya.config.json";
|
|
13459
14211
|
function rowLine(name, path, specsPath) {
|
|
@@ -13531,11 +14283,11 @@ function planRegistryRow(repoRoot2, name, path, specsPath) {
|
|
|
13531
14283
|
function applyRegistryRow(repoRoot2, plan, name, path, specsPath) {
|
|
13532
14284
|
const abs2 = join21(repoRoot2, PROJECTS_REGISTRY_PATH);
|
|
13533
14285
|
if (plan.action === "create-host") {
|
|
13534
|
-
|
|
13535
|
-
|
|
14286
|
+
mkdirSync6(dirname9(abs2), { recursive: true });
|
|
14287
|
+
writeFileSync11(abs2, freshProjectsRegistry(name, path, specsPath), "utf-8");
|
|
13536
14288
|
} else if (plan.action === "append-row") {
|
|
13537
14289
|
const existing = readFileSync22(abs2, "utf-8");
|
|
13538
|
-
|
|
14290
|
+
writeFileSync11(abs2, appendRegistryRow(existing, name, path, specsPath), "utf-8");
|
|
13539
14291
|
}
|
|
13540
14292
|
}
|
|
13541
14293
|
function renderRegistryRowDiffLine(plan) {
|
|
@@ -13567,7 +14319,7 @@ function applyConfigProjectEntry(repoRoot2, plan) {
|
|
|
13567
14319
|
const abs2 = join21(repoRoot2, CONFIG_FILE_PATH);
|
|
13568
14320
|
const seed = JSON.parse(readFileSync22(abs2, "utf-8"));
|
|
13569
14321
|
const projects = Array.isArray(seed.projects) ? seed.projects : [];
|
|
13570
|
-
|
|
14322
|
+
writeFileSync11(abs2, `${JSON.stringify({ ...seed, projects: [...projects, plan.entry] }, null, 2)}
|
|
13571
14323
|
`, "utf-8");
|
|
13572
14324
|
}
|
|
13573
14325
|
function renderConfigProjectEntryDiffLine(plan) {
|
|
@@ -14188,7 +14940,7 @@ async function ejectCommand(args) {
|
|
|
14188
14940
|
}
|
|
14189
14941
|
|
|
14190
14942
|
// src/commands/init.ts
|
|
14191
|
-
import { existsSync as existsSync17, readFileSync as readFileSync25, writeFileSync as
|
|
14943
|
+
import { existsSync as existsSync17, readFileSync as readFileSync25, writeFileSync as writeFileSync12 } from "node:fs";
|
|
14192
14944
|
import { join as join24 } from "node:path";
|
|
14193
14945
|
function realDeps6() {
|
|
14194
14946
|
return {
|
|
@@ -14241,7 +14993,7 @@ function readManifest3(repoRoot2) {
|
|
|
14241
14993
|
function writeManifest(repoRoot2, manifest) {
|
|
14242
14994
|
const configAbs = join24(repoRoot2, CONFIG_PATH);
|
|
14243
14995
|
const seed = JSON.parse(readFileSync25(configAbs, "utf-8"));
|
|
14244
|
-
|
|
14996
|
+
writeFileSync12(configAbs, `${JSON.stringify({ ...seed, managed: manifest }, null, 2)}
|
|
14245
14997
|
`, "utf-8");
|
|
14246
14998
|
}
|
|
14247
14999
|
async function runInit(args, deps) {
|
|
@@ -14457,7 +15209,7 @@ function issueEditCommand(args) {
|
|
|
14457
15209
|
|
|
14458
15210
|
// src/commands/issue-objectives.ts
|
|
14459
15211
|
import { execFileSync as execFileSync20 } from "node:child_process";
|
|
14460
|
-
import { mkdtempSync as mkdtempSync4, rmSync as rmSync8, writeFileSync as
|
|
15212
|
+
import { mkdtempSync as mkdtempSync4, rmSync as rmSync8, writeFileSync as writeFileSync13 } from "node:fs";
|
|
14461
15213
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
14462
15214
|
import { join as join25 } from "node:path";
|
|
14463
15215
|
var RETRY = 'vinaya issue objectives edit <n> --add "<sentence>" | --drop O<k> | --replace O<k> "<sentence>" --reason "<text>"';
|
|
@@ -14592,7 +15344,7 @@ function issueObjectivesEditCommand(args) {
|
|
|
14592
15344
|
const newBody = spliceObjectivesSection(body, renderObjectives(updated));
|
|
14593
15345
|
const dir = mkdtempSync4(join25(tmpdir6(), "vinaya-objectives-edit-"));
|
|
14594
15346
|
const tmp = join25(dir, "body.md");
|
|
14595
|
-
|
|
15347
|
+
writeFileSync13(tmp, newBody, "utf8");
|
|
14596
15348
|
try {
|
|
14597
15349
|
const ghArgs = ["--body-file", tmp];
|
|
14598
15350
|
writeValidatedIssueEdit({
|
|
@@ -15007,7 +15759,7 @@ async function milestoneCloseCommand(args) {
|
|
|
15007
15759
|
}
|
|
15008
15760
|
|
|
15009
15761
|
// src/commands/new-check.ts
|
|
15010
|
-
import { chmodSync as
|
|
15762
|
+
import { chmodSync as chmodSync3, existsSync as existsSync18, mkdirSync as mkdirSync7, readFileSync as readFileSync26, writeFileSync as writeFileSync14 } from "node:fs";
|
|
15011
15763
|
import { join as join26 } from "node:path";
|
|
15012
15764
|
var TEMPLATE_PATH2 = join26(packageRoot(import.meta.url), "templates", "custom-check.template.ts");
|
|
15013
15765
|
var CHECKS_DIR = join26("scripts", "vinaya-checks");
|
|
@@ -15029,7 +15781,7 @@ function newCheckCommand(args) {
|
|
|
15029
15781
|
}
|
|
15030
15782
|
const scriptsDir = join26(process.cwd(), CHECKS_DIR);
|
|
15031
15783
|
if (!existsSync18(scriptsDir))
|
|
15032
|
-
|
|
15784
|
+
mkdirSync7(scriptsDir, { recursive: true });
|
|
15033
15785
|
const fileStem = name.slice(name.indexOf("/") + 1);
|
|
15034
15786
|
const targetPath = join26(scriptsDir, `${fileStem}.ts`);
|
|
15035
15787
|
if (existsSync18(targetPath)) {
|
|
@@ -15038,8 +15790,8 @@ function newCheckCommand(args) {
|
|
|
15038
15790
|
}
|
|
15039
15791
|
const template = readFileSync26(TEMPLATE_PATH2, "utf-8");
|
|
15040
15792
|
const contents = template.split("{{CHECK_NAME}}").join(name);
|
|
15041
|
-
|
|
15042
|
-
|
|
15793
|
+
writeFileSync14(targetPath, contents, "utf-8");
|
|
15794
|
+
chmodSync3(targetPath, 493);
|
|
15043
15795
|
const relPath = join26(CHECKS_DIR, `${fileStem}.ts`);
|
|
15044
15796
|
const registration = JSON.stringify({ checks: { [name]: { run: `./${relPath}`, scope: "diff" } } }, null, 2);
|
|
15045
15797
|
process.stdout.write(`Created ${relPath}
|
|
@@ -15050,7 +15802,7 @@ ${registration}
|
|
|
15050
15802
|
}
|
|
15051
15803
|
|
|
15052
15804
|
// src/commands/new-noop-check.ts
|
|
15053
|
-
import { chmodSync as
|
|
15805
|
+
import { chmodSync as chmodSync4, existsSync as existsSync19, mkdirSync as mkdirSync8, readFileSync as readFileSync27, writeFileSync as writeFileSync15 } from "node:fs";
|
|
15054
15806
|
import { join as join27 } from "node:path";
|
|
15055
15807
|
var TEMPLATE_PATH3 = join27(packageRoot(import.meta.url), "templates", "noop-check.template.ts");
|
|
15056
15808
|
var CHECKS_DIR2 = join27("vinaya", "checks");
|
|
@@ -15069,7 +15821,7 @@ function newNoopCheckCommand(args) {
|
|
|
15069
15821
|
}
|
|
15070
15822
|
const checksDir = join27(process.cwd(), CHECKS_DIR2);
|
|
15071
15823
|
if (!existsSync19(checksDir))
|
|
15072
|
-
|
|
15824
|
+
mkdirSync8(checksDir, { recursive: true });
|
|
15073
15825
|
const targetPath = join27(checksDir, `${name}.ts`);
|
|
15074
15826
|
if (existsSync19(targetPath)) {
|
|
15075
15827
|
console.error(`Error: ${targetPath} already exists.`);
|
|
@@ -15077,8 +15829,8 @@ function newNoopCheckCommand(args) {
|
|
|
15077
15829
|
}
|
|
15078
15830
|
const template = readFileSync27(TEMPLATE_PATH3, "utf-8");
|
|
15079
15831
|
const contents = template.split("{{CHECK_NAME}}").join(name);
|
|
15080
|
-
|
|
15081
|
-
|
|
15832
|
+
writeFileSync15(targetPath, contents, "utf-8");
|
|
15833
|
+
chmodSync4(targetPath, 493);
|
|
15082
15834
|
const relPath = join27(CHECKS_DIR2, `${name}.ts`);
|
|
15083
15835
|
const entry2 = {
|
|
15084
15836
|
run: `./${relPath}`,
|
|
@@ -15097,7 +15849,7 @@ ${registration}
|
|
|
15097
15849
|
}
|
|
15098
15850
|
|
|
15099
15851
|
// src/commands/new-role.ts
|
|
15100
|
-
import { existsSync as existsSync20, mkdirSync as
|
|
15852
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync9, readFileSync as readFileSync28, writeFileSync as writeFileSync16 } from "node:fs";
|
|
15101
15853
|
import { join as join28 } from "node:path";
|
|
15102
15854
|
var TEMPLATE_PATH4 = join28(packageRoot(import.meta.url), "templates", "role.template.md");
|
|
15103
15855
|
var ROLES_DIR = join28("vinaya", "roles");
|
|
@@ -15115,7 +15867,7 @@ A bare, un-namespaced key resolves as an OVERRIDE of a core role — a complete
|
|
|
15115
15867
|
}
|
|
15116
15868
|
const rolesDir = join28(process.cwd(), ROLES_DIR);
|
|
15117
15869
|
if (!existsSync20(rolesDir))
|
|
15118
|
-
|
|
15870
|
+
mkdirSync9(rolesDir, { recursive: true });
|
|
15119
15871
|
const roleId = name.slice(name.indexOf("/") + 1);
|
|
15120
15872
|
const targetPath = join28(rolesDir, `${roleId}.md`);
|
|
15121
15873
|
if (existsSync20(targetPath)) {
|
|
@@ -15124,7 +15876,7 @@ A bare, un-namespaced key resolves as an OVERRIDE of a core role — a complete
|
|
|
15124
15876
|
}
|
|
15125
15877
|
const template = readFileSync28(TEMPLATE_PATH4, "utf-8");
|
|
15126
15878
|
const contents = template.split("{{ROLE_ID}}").join(roleId);
|
|
15127
|
-
|
|
15879
|
+
writeFileSync16(targetPath, contents, "utf-8");
|
|
15128
15880
|
const relPath = join28(ROLES_DIR, `${roleId}.md`);
|
|
15129
15881
|
const registration = JSON.stringify({ roles: { [name]: { contract: `./${relPath}` } } }, null, 2);
|
|
15130
15882
|
process.stdout.write(`Created ${relPath}
|
|
@@ -15144,8 +15896,8 @@ function summariseNumstat(numstat) {
|
|
|
15144
15896
|
let added = 0;
|
|
15145
15897
|
let deleted = 0;
|
|
15146
15898
|
let binary = 0;
|
|
15147
|
-
for (const
|
|
15148
|
-
const [a, d] =
|
|
15899
|
+
for (const row2 of rows) {
|
|
15900
|
+
const [a, d] = row2.split("\t");
|
|
15149
15901
|
if (a === "-" || d === "-") {
|
|
15150
15902
|
binary++;
|
|
15151
15903
|
continue;
|
|
@@ -15709,7 +16461,7 @@ function prRuleCommand(args) {
|
|
|
15709
16461
|
|
|
15710
16462
|
// src/commands/pr-report.ts
|
|
15711
16463
|
import { execFileSync as execFileSync24, spawnSync as spawnSync2 } from "node:child_process";
|
|
15712
|
-
import { existsSync as existsSync21, mkdtempSync as mkdtempSync5, readFileSync as readFileSync30, rmSync as rmSync9, writeFileSync as
|
|
16464
|
+
import { existsSync as existsSync21, mkdtempSync as mkdtempSync5, readFileSync as readFileSync30, rmSync as rmSync9, writeFileSync as writeFileSync17 } from "node:fs";
|
|
15713
16465
|
import { tmpdir as tmpdir7 } from "node:os";
|
|
15714
16466
|
import { join as join29 } from "node:path";
|
|
15715
16467
|
var EVIDENCE_START = "<!-- AEG:EVIDENCE:START -->";
|
|
@@ -16112,7 +16864,7 @@ var USAGE5 = "Usage: vinaya pr report [--write <body-file> | --push <pr>] [--pha
|
|
|
16112
16864
|
function ghEditBody(pr, body) {
|
|
16113
16865
|
const dir = mkdtempSync5(join29(tmpdir7(), "vinaya-pr-report-push-"));
|
|
16114
16866
|
const tmp = join29(dir, "body.md");
|
|
16115
|
-
|
|
16867
|
+
writeFileSync17(tmp, body);
|
|
16116
16868
|
try {
|
|
16117
16869
|
gh3(["pr", "edit", pr, "--body-file", tmp]);
|
|
16118
16870
|
} finally {
|
|
@@ -16208,7 +16960,7 @@ ${USAGE5}`);
|
|
|
16208
16960
|
} catch (err) {
|
|
16209
16961
|
const dir = mkdtempSync5(join29(tmpdir7(), "vinaya-pr-report-push-restore-failed-"));
|
|
16210
16962
|
const savePath = join29(dir, "pre-edit-body.md");
|
|
16211
|
-
|
|
16963
|
+
writeFileSync17(savePath, preEditBody);
|
|
16212
16964
|
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}\`.`);
|
|
16213
16965
|
process.exit(1);
|
|
16214
16966
|
}
|
|
@@ -16241,7 +16993,7 @@ The AEG:EVIDENCE block was still pushed to PR ${pushPr}.`);
|
|
|
16241
16993
|
transcriptPath,
|
|
16242
16994
|
modelOverride
|
|
16243
16995
|
});
|
|
16244
|
-
|
|
16996
|
+
writeFileSync17(writePath, composeWrittenBody(existing, result.blockInner, tokens));
|
|
16245
16997
|
if (tokens.collected) {
|
|
16246
16998
|
process.stdout.write(`Wrote AEG:EVIDENCE and AEG:TOKENS blocks to ${writePath}
|
|
16247
16999
|
`);
|
|
@@ -16440,8 +17192,8 @@ import { existsSync as existsSync23, readFileSync as readFileSync32 } from "node
|
|
|
16440
17192
|
import { join as join31 } from "node:path";
|
|
16441
17193
|
|
|
16442
17194
|
// src/lib/doc-owners-write.ts
|
|
16443
|
-
import { existsSync as existsSync22, mkdirSync as
|
|
16444
|
-
import { dirname as
|
|
17195
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync10, readFileSync as readFileSync31, writeFileSync as writeFileSync18 } from "node:fs";
|
|
17196
|
+
import { dirname as dirname10, join as join30 } from "node:path";
|
|
16445
17197
|
function bindingLine(glob, pointer) {
|
|
16446
17198
|
return `${glob} ${pointer}`;
|
|
16447
17199
|
}
|
|
@@ -16469,11 +17221,11 @@ function planDocOwnersBinding(repoRoot3, glob, pointer) {
|
|
|
16469
17221
|
function applyDocOwnersBinding(repoRoot3, plan, glob, pointer) {
|
|
16470
17222
|
const abs2 = join30(repoRoot3, DOC_OWNERS_PATH);
|
|
16471
17223
|
if (plan.action === "create-host") {
|
|
16472
|
-
|
|
16473
|
-
|
|
17224
|
+
mkdirSync10(dirname10(abs2), { recursive: true });
|
|
17225
|
+
writeFileSync18(abs2, freshDocOwners(glob, pointer), "utf-8");
|
|
16474
17226
|
} else if (plan.action === "append-row") {
|
|
16475
17227
|
const existing = readFileSync31(abs2, "utf-8");
|
|
16476
|
-
|
|
17228
|
+
writeFileSync18(abs2, appendDocOwnersBinding(existing, glob, pointer), "utf-8");
|
|
16477
17229
|
}
|
|
16478
17230
|
}
|
|
16479
17231
|
function renderDocOwnersBindingDiffLine(plan) {
|
|
@@ -17033,7 +17785,7 @@ async function reviewStatusCommand(args) {
|
|
|
17033
17785
|
import { execFile as execFile5, spawn as spawn3 } from "node:child_process";
|
|
17034
17786
|
import { existsSync as existsSync24, readFileSync as readFileSync33, renameSync as renameSync2 } from "node:fs";
|
|
17035
17787
|
import net from "node:net";
|
|
17036
|
-
import { dirname as
|
|
17788
|
+
import { dirname as dirname11, join as join32 } from "node:path";
|
|
17037
17789
|
import { promisify as promisify5 } from "node:util";
|
|
17038
17790
|
|
|
17039
17791
|
// src/lib/studio-bundle.ts
|
|
@@ -17098,7 +17850,7 @@ function resolveStudioTarget(cwd, moduleUrl = import.meta.url) {
|
|
|
17098
17850
|
}
|
|
17099
17851
|
if (existsSync24(join32(dir, ".git")))
|
|
17100
17852
|
break;
|
|
17101
|
-
const parent =
|
|
17853
|
+
const parent = dirname11(dir);
|
|
17102
17854
|
if (parent === dir)
|
|
17103
17855
|
break;
|
|
17104
17856
|
dir = parent;
|
|
@@ -17189,7 +17941,7 @@ async function taskDispatchCommand(args) {
|
|
|
17189
17941
|
const trancheSlug = args[0];
|
|
17190
17942
|
const taskIdArg = args[1];
|
|
17191
17943
|
if (!trancheSlug || !taskIdArg || trancheSlug.startsWith("--")) {
|
|
17192
|
-
console.error(`Usage: vinaya task dispatch <tranche> <n> [--agent ${DISPATCH_AGENTS.join(" | ")}]`);
|
|
17944
|
+
console.error(`Usage: vinaya task dispatch <tranche> <n> [--agent ${DISPATCH_AGENTS.join(" | ")}] [--model <name>]`);
|
|
17193
17945
|
process.exit(2);
|
|
17194
17946
|
}
|
|
17195
17947
|
const n = Number.parseInt(taskIdArg, 10);
|
|
@@ -17208,7 +17960,17 @@ async function taskDispatchCommand(args) {
|
|
|
17208
17960
|
}
|
|
17209
17961
|
agent = value;
|
|
17210
17962
|
}
|
|
17211
|
-
const
|
|
17963
|
+
const modelIdx = rest.indexOf("--model");
|
|
17964
|
+
let model;
|
|
17965
|
+
if (modelIdx !== -1) {
|
|
17966
|
+
const value = rest[modelIdx + 1];
|
|
17967
|
+
if (!value) {
|
|
17968
|
+
console.error("--model requires a value");
|
|
17969
|
+
process.exit(2);
|
|
17970
|
+
}
|
|
17971
|
+
model = value;
|
|
17972
|
+
}
|
|
17973
|
+
const result = await dispatchTask({ tranche: trancheSlug, n, agent, model });
|
|
17212
17974
|
process.stdout.write(`${result.brief}
|
|
17213
17975
|
`);
|
|
17214
17976
|
if (result.commentUrl)
|
|
@@ -17218,7 +17980,7 @@ Posted: ${result.commentUrl}
|
|
|
17218
17980
|
}
|
|
17219
17981
|
|
|
17220
17982
|
// src/commands/upgrade.ts
|
|
17221
|
-
import { existsSync as existsSync25, readFileSync as readFileSync34, rmSync as rmSync10, writeFileSync as
|
|
17983
|
+
import { existsSync as existsSync25, readFileSync as readFileSync34, rmSync as rmSync10, writeFileSync as writeFileSync19 } from "node:fs";
|
|
17222
17984
|
import { join as join33 } from "node:path";
|
|
17223
17985
|
function realDeps9() {
|
|
17224
17986
|
return {
|
|
@@ -17261,7 +18023,7 @@ function writeManifestVersion(repoRoot3, manifest) {
|
|
|
17261
18023
|
const configAbs = join33(repoRoot3, CONFIG_PATH);
|
|
17262
18024
|
const seed = JSON.parse(readFileSync34(configAbs, "utf-8"));
|
|
17263
18025
|
const updated = { ...manifest, version: MANAGED_MANIFEST_VERSION };
|
|
17264
|
-
|
|
18026
|
+
writeFileSync19(configAbs, `${JSON.stringify({ ...seed, managed: updated }, null, 2)}
|
|
17265
18027
|
`, "utf-8");
|
|
17266
18028
|
}
|
|
17267
18029
|
function stripFor(repoRoot3, path, marker, comment) {
|
|
@@ -17328,9 +18090,13 @@ function extractWorkflowTrigger(content) {
|
|
|
17328
18090
|
const match = content.match(/^on:\r?\n {2}([A-Za-z0-9_]+):/m);
|
|
17329
18091
|
return match?.[1] ?? null;
|
|
17330
18092
|
}
|
|
17331
|
-
function planUpgrade(ops, repoRoot3, manifest, routing) {
|
|
18093
|
+
function planUpgrade(ops, repoRoot3, manifest, routing, staleSkillPaths = []) {
|
|
17332
18094
|
const entries = [];
|
|
17333
|
-
|
|
18095
|
+
const staleFiles = staleSkillPaths.map((path) => ({
|
|
18096
|
+
path,
|
|
18097
|
+
present: existsSync25(join33(repoRoot3, path))
|
|
18098
|
+
}));
|
|
18099
|
+
let hasChanges = routing.arm || routing.migratesManifest || routing.strips.some((s) => s.present) || staleFiles.some((f) => f.present);
|
|
17334
18100
|
const ownedFiles = new Set(manifest.files);
|
|
17335
18101
|
const blockKey = (path, marker) => `${path}::${marker}`;
|
|
17336
18102
|
const ownedBlocks = new Set(manifest.blocks.map((b) => blockKey(b.path, b.marker)));
|
|
@@ -17394,7 +18160,13 @@ function planUpgrade(ops, repoRoot3, manifest, routing) {
|
|
|
17394
18160
|
}
|
|
17395
18161
|
}
|
|
17396
18162
|
const versionMigration = manifest.version === MANAGED_MANIFEST_VERSION ? null : { from: manifest.version, to: MANAGED_MANIFEST_VERSION };
|
|
17397
|
-
return { entries, routing, versionMigration, hasChanges: hasChanges || versionMigration !== null };
|
|
18163
|
+
return { entries, routing, staleFiles, versionMigration, hasChanges: hasChanges || versionMigration !== null };
|
|
18164
|
+
}
|
|
18165
|
+
function withoutStaleFiles(manifest, plan) {
|
|
18166
|
+
if (plan.staleFiles.length === 0)
|
|
18167
|
+
return manifest;
|
|
18168
|
+
const stale = new Set(plan.staleFiles.map((f) => f.path));
|
|
18169
|
+
return { ...manifest, files: manifest.files.filter((f) => !stale.has(f)) };
|
|
17398
18170
|
}
|
|
17399
18171
|
function withClaudeStopHookRecorded(manifest, plan) {
|
|
17400
18172
|
let files = manifest.files;
|
|
@@ -17419,6 +18191,13 @@ function renderUpgradeDiff(plan) {
|
|
|
17419
18191
|
lines.push(` ~ migrate manifest version ${plan.versionMigration.from} → ${plan.versionMigration.to}`);
|
|
17420
18192
|
lines.push("");
|
|
17421
18193
|
}
|
|
18194
|
+
if (plan.staleFiles.length > 0) {
|
|
18195
|
+
lines.push("── Retired role skills ─────────────────────────────");
|
|
18196
|
+
for (const f of plan.staleFiles) {
|
|
18197
|
+
lines.push(f.present ? ` - remove ${f.path} (role retired — its doctrine now refuses)` : ` · gone ${f.path} (already removed)`);
|
|
18198
|
+
}
|
|
18199
|
+
lines.push("");
|
|
18200
|
+
}
|
|
17422
18201
|
const r = plan.routing;
|
|
17423
18202
|
if (r.blockedReason || r.arm || r.migratesManifest || r.strips.length > 0) {
|
|
17424
18203
|
lines.push("── Hook location ─────────────────────────────");
|
|
@@ -17503,7 +18282,7 @@ function regenerateBlock(repoRoot3, op) {
|
|
|
17503
18282
|
const content = readFileSync34(abs2, "utf-8");
|
|
17504
18283
|
const stripped = stripBlockFromContent(content, op.marker, op.comment);
|
|
17505
18284
|
if (stripped !== null) {
|
|
17506
|
-
|
|
18285
|
+
writeFileSync19(abs2, stripped.endsWith(`
|
|
17507
18286
|
`) ? stripped : `${stripped}
|
|
17508
18287
|
`, "utf-8");
|
|
17509
18288
|
}
|
|
@@ -17524,6 +18303,13 @@ function applyUpgrade(plan, repoRoot3) {
|
|
|
17524
18303
|
regenerateBlock(repoRoot3, e.op);
|
|
17525
18304
|
}
|
|
17526
18305
|
}
|
|
18306
|
+
for (const f of plan.staleFiles) {
|
|
18307
|
+
if (!f.present)
|
|
18308
|
+
continue;
|
|
18309
|
+
const abs2 = containedAbs(repoRoot3, f.path);
|
|
18310
|
+
if (abs2 !== null)
|
|
18311
|
+
rmSync10(abs2, { force: true });
|
|
18312
|
+
}
|
|
17527
18313
|
for (const s of plan.routing.strips) {
|
|
17528
18314
|
if (!s.present)
|
|
17529
18315
|
continue;
|
|
@@ -17536,7 +18322,7 @@ function applyUpgrade(plan, repoRoot3) {
|
|
|
17536
18322
|
if (blockStripLeavesEmpty(stripped))
|
|
17537
18323
|
rmSync10(abs2, { force: true });
|
|
17538
18324
|
else
|
|
17539
|
-
|
|
18325
|
+
writeFileSync19(abs2, stripped.endsWith(`
|
|
17540
18326
|
`) ? stripped : `${stripped}
|
|
17541
18327
|
`, "utf-8");
|
|
17542
18328
|
}
|
|
@@ -17575,7 +18361,9 @@ async function runUpgrade(args, deps) {
|
|
|
17575
18361
|
agents: resolveAgentVendors(planManifest)
|
|
17576
18362
|
};
|
|
17577
18363
|
const ops = buildInitOps(ctx);
|
|
17578
|
-
const
|
|
18364
|
+
const doctrineRootForStaleSkills = ctx.agents.has("skills") ? resolveDoctrineRoot() : null;
|
|
18365
|
+
const staleSkillPaths = doctrineRootForStaleSkills ? staleAgentSkillPaths(doctrineRootForStaleSkills, planManifest.files) : [];
|
|
18366
|
+
const plan = planUpgrade(ops, repo.repoRoot, planManifest, routing, staleSkillPaths);
|
|
17579
18367
|
if (!plan.hasChanges) {
|
|
17580
18368
|
if (routing.blockedReason) {
|
|
17581
18369
|
process.stdout.write(routing.target === TRACKED_HOOK_DIR ? `Note: core.hooksPath NOT armed — ${routing.blockedReason}.
|
|
@@ -17608,7 +18396,7 @@ async function runUpgrade(args, deps) {
|
|
|
17608
18396
|
applyUpgrade(plan, repo.repoRoot);
|
|
17609
18397
|
if (routing.arm)
|
|
17610
18398
|
await deps.setHooksPath(repo.repoRoot, TRACKED_HOOK_DIR);
|
|
17611
|
-
writeManifestVersion(repo.repoRoot, withClaudeStopHookRecorded(planManifest, plan));
|
|
18399
|
+
writeManifestVersion(repo.repoRoot, withClaudeStopHookRecorded(withoutStaleFiles(planManifest, plan), plan));
|
|
17612
18400
|
process.stdout.write(`
|
|
17613
18401
|
Vinaya upgraded.
|
|
17614
18402
|
`);
|
|
@@ -17734,7 +18522,7 @@ Reason: ${reason}`];
|
|
|
17734
18522
|
// src/lib/output.ts
|
|
17735
18523
|
var NAME_COLUMN_WIDTH = 28;
|
|
17736
18524
|
var PLANNED_MARKER = "[planned — not yet implemented] ";
|
|
17737
|
-
function
|
|
18525
|
+
function row2(indent2, name, description) {
|
|
17738
18526
|
const padded = name.length >= NAME_COLUMN_WIDTH ? `${name} ` : name.padEnd(NAME_COLUMN_WIDTH);
|
|
17739
18527
|
return `${indent2}${padded}${description}`;
|
|
17740
18528
|
}
|
|
@@ -17742,9 +18530,9 @@ function printHelp() {
|
|
|
17742
18530
|
const lines = ["vinaya — Vinaya CLI", "", "USAGE", " vinaya <command> [options]", "", "COMMANDS"];
|
|
17743
18531
|
for (const command of COMMANDS) {
|
|
17744
18532
|
const marker = command.status === "planned" ? PLANNED_MARKER : "";
|
|
17745
|
-
lines.push(
|
|
18533
|
+
lines.push(row2(" ", command.name, `${marker}${command.description}`));
|
|
17746
18534
|
for (const flag of command.flags ?? []) {
|
|
17747
|
-
lines.push(
|
|
18535
|
+
lines.push(row2(" ", flag.flag, `${marker}${flag.description}`));
|
|
17748
18536
|
}
|
|
17749
18537
|
}
|
|
17750
18538
|
lines.push("", "Run 'vinaya version' to check what's installed.");
|
|
@@ -17754,7 +18542,7 @@ function printHelp() {
|
|
|
17754
18542
|
}
|
|
17755
18543
|
|
|
17756
18544
|
// src/index.ts
|
|
17757
|
-
var PACKAGE_ROOT = join34(
|
|
18545
|
+
var PACKAGE_ROOT = join34(dirname12(fileURLToPath2(import.meta.url)), "..");
|
|
17758
18546
|
function readVersion2() {
|
|
17759
18547
|
const pkg = JSON.parse(readFileSync35(join34(PACKAGE_ROOT, "package.json"), "utf-8"));
|
|
17760
18548
|
return pkg.version;
|