strikethroo 3.14.3 → 3.16.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 +52 -31
- package/dist/__tests__/fixtures/review-gate.d.ts +59 -0
- package/dist/__tests__/fixtures/review-gate.d.ts.map +1 -0
- package/dist/__tests__/fixtures/review-gate.js +150 -0
- package/dist/__tests__/fixtures/review-gate.js.map +1 -0
- package/dist/cli.js +19 -0
- package/dist/cli.js.map +1 -1
- package/dist/export-profile.d.ts +50 -0
- package/dist/export-profile.d.ts.map +1 -0
- package/dist/export-profile.js +321 -0
- package/dist/export-profile.js.map +1 -0
- package/dist/harnesses/registry.d.ts +4 -2
- package/dist/harnesses/registry.d.ts.map +1 -1
- package/dist/harnesses/registry.js +4 -2
- package/dist/harnesses/registry.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +67 -3
- package/dist/index.js.map +1 -1
- package/dist/profiles.d.ts +163 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +561 -0
- package/dist/profiles.js.map +1 -0
- package/dist/types.d.ts +37 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +15 -1
- package/dist/types.js.map +1 -1
- package/dist-web/assets/{arc-CsLKjSmD.js → arc-DMHUv2NY.js} +1 -1
- package/dist-web/assets/{architectureDiagram-3BPJPVTR-CUdI2ZME.js → architectureDiagram-3BPJPVTR-CeAAqVS4.js} +1 -1
- package/dist-web/assets/{blockDiagram-GPEHLZMM-BzGl3sJ5.js → blockDiagram-GPEHLZMM-Hns_u_jz.js} +1 -1
- package/dist-web/assets/{c4Diagram-AAUBKEIU-B9BcD268.js → c4Diagram-AAUBKEIU-BH3RLVAi.js} +1 -1
- package/dist-web/assets/channel-Dk0dEQIT.js +1 -0
- package/dist-web/assets/{chunk-2J33WTMH-jrfdvIhS.js → chunk-2J33WTMH-h6Ws9VXu.js} +1 -1
- package/dist-web/assets/{chunk-4BX2VUAB-BppnV9t3.js → chunk-4BX2VUAB-BSBeNNnS.js} +1 -1
- package/dist-web/assets/{chunk-55IACEB6-Dgig1qV7.js → chunk-55IACEB6-BmUY060b.js} +1 -1
- package/dist-web/assets/{chunk-727SXJPM-CssKzpPg.js → chunk-727SXJPM-CmemMVvQ.js} +1 -1
- package/dist-web/assets/{chunk-AQP2D5EJ-DWYbdfaL.js → chunk-AQP2D5EJ-LQpUb_FL.js} +1 -1
- package/dist-web/assets/{chunk-FMBD7UC4-Cbt7Dqxf.js → chunk-FMBD7UC4-wk7OYcdf.js} +1 -1
- package/dist-web/assets/{chunk-ND2GUHAM-2W-YdHxx.js → chunk-ND2GUHAM-O3K3rkHW.js} +1 -1
- package/dist-web/assets/{chunk-QZHKN3VN-BSLdTa36.js → chunk-QZHKN3VN-CvppUbp9.js} +1 -1
- package/dist-web/assets/classDiagram-4FO5ZUOK-gqDJqcDV.js +1 -0
- package/dist-web/assets/classDiagram-v2-Q7XG4LA2-gqDJqcDV.js +1 -0
- package/dist-web/assets/{cose-bilkent-S5V4N54A-D0_MX2M6.js → cose-bilkent-S5V4N54A-Cf1nQ4V_.js} +1 -1
- package/dist-web/assets/{dagre-BM42HDAG-DGXkicI6.js → dagre-BM42HDAG-YyP7mwPP.js} +1 -1
- package/dist-web/assets/{diagram-2AECGRRQ-DMfWZxh-.js → diagram-2AECGRRQ-BmAtTKkQ.js} +1 -1
- package/dist-web/assets/{diagram-5GNKFQAL-IChrzosS.js → diagram-5GNKFQAL-Bem8_DUK.js} +1 -1
- package/dist-web/assets/{diagram-KO2AKTUF-C7IVA_Py.js → diagram-KO2AKTUF-0gNyY9-f.js} +1 -1
- package/dist-web/assets/{diagram-LMA3HP47-UQXc1x2n.js → diagram-LMA3HP47-CJJh_IUU.js} +1 -1
- package/dist-web/assets/{diagram-OG6HWLK6-Z69O_NXy.js → diagram-OG6HWLK6-DJcCho7F.js} +1 -1
- package/dist-web/assets/{erDiagram-TEJ5UH35-xrgfPZU-.js → erDiagram-TEJ5UH35-D-TI_EbX.js} +1 -1
- package/dist-web/assets/{flowDiagram-I6XJVG4X-Co-y5e2I.js → flowDiagram-I6XJVG4X-BUrB4EYN.js} +1 -1
- package/dist-web/assets/{ganttDiagram-6RSMTGT7-C9TOh3M1.js → ganttDiagram-6RSMTGT7-C9_ItheP.js} +1 -1
- package/dist-web/assets/{gitGraphDiagram-PVQCEYII-iBpK83U-.js → gitGraphDiagram-PVQCEYII-DYQEmll0.js} +1 -1
- package/dist-web/assets/{index-DaaOiRou.js → index-DULKNhw0.js} +4 -4
- package/dist-web/assets/{index-6RY653Ds.js → index-X-a-o-AT.js} +1 -1
- package/dist-web/assets/{index-D4y2EDE-.js → index-mEUl0sqe.js} +1 -1
- package/dist-web/assets/{infoDiagram-5YYISTIA-C-_CUdyF.js → infoDiagram-5YYISTIA-B06Tp7cb.js} +1 -1
- package/dist-web/assets/{ishikawaDiagram-YF4QCWOH-zPkWZ6FX.js → ishikawaDiagram-YF4QCWOH-DY9iP3P6.js} +1 -1
- package/dist-web/assets/{journeyDiagram-JHISSGLW-CsTWcFHo.js → journeyDiagram-JHISSGLW-Bk4MV49m.js} +1 -1
- package/dist-web/assets/{kanban-definition-UN3LZRKU-CCsrhCu2.js → kanban-definition-UN3LZRKU-B9-wsNt9.js} +1 -1
- package/dist-web/assets/{linear-DlkSJnhW.js → linear-DEY9HwQB.js} +1 -1
- package/dist-web/assets/{mermaid.core-BM7nlTu6.js → mermaid.core-Cn0e4l6V.js} +4 -4
- package/dist-web/assets/{mindmap-definition-RKZ34NQL-C5tQ7zJH.js → mindmap-definition-RKZ34NQL-pN4Q2WNG.js} +1 -1
- package/dist-web/assets/{pieDiagram-4H26LBE5-CsK9eMOa.js → pieDiagram-4H26LBE5-DTkPv6S2.js} +1 -1
- package/dist-web/assets/{quadrantDiagram-W4KKPZXB-CsUwvpcD.js → quadrantDiagram-W4KKPZXB-BS9LhTv0.js} +1 -1
- package/dist-web/assets/{requirementDiagram-4Y6WPE33-CzQDuhAP.js → requirementDiagram-4Y6WPE33-BlSOLsQe.js} +1 -1
- package/dist-web/assets/{sankeyDiagram-5OEKKPKP-BTa3-BBs.js → sankeyDiagram-5OEKKPKP-B1xl-B1U.js} +1 -1
- package/dist-web/assets/{sequenceDiagram-3UESZ5HK-CPPduAn5.js → sequenceDiagram-3UESZ5HK-BoiYrW_0.js} +1 -1
- package/dist-web/assets/{stateDiagram-AJRCARHV-T5j3NpOr.js → stateDiagram-AJRCARHV-BxvfZYDK.js} +1 -1
- package/dist-web/assets/stateDiagram-v2-BHNVJYJU-XGZY1tGg.js +1 -0
- package/dist-web/assets/{timeline-definition-PNZ67QCA-BG2gG3_T.js → timeline-definition-PNZ67QCA-CUEbrRkc.js} +1 -1
- package/dist-web/assets/{vennDiagram-CIIHVFJN-B3LyjiD_.js → vennDiagram-CIIHVFJN-CgQiBucH.js} +1 -1
- package/dist-web/assets/{wardley-L42UT6IY-CTsJTFG9.js → wardley-L42UT6IY-BsGW6T55.js} +1 -1
- package/dist-web/assets/{wardleyDiagram-YWT4CUSO-cmPISkrt.js → wardleyDiagram-YWT4CUSO-Cq3RMp6B.js} +1 -1
- package/dist-web/assets/{xychartDiagram-2RQKCTM6-BWy_dUgl.js → xychartDiagram-2RQKCTM6-WaoV-V3f.js} +1 -1
- package/dist-web/index.html +1 -1
- package/package.json +3 -3
- package/templates/harness/skills/st-code-review/SKILL.md +309 -0
- package/templates/harness/skills/st-code-review/scripts/code-review.cjs +1539 -0
- package/templates/harness/skills/st-code-review/scripts/find-strikethroo-root.cjs +116 -0
- package/templates/harness/skills/st-code-review/scripts/validate-plan-blueprint.cjs +429 -0
- package/templates/harness/skills/st-execute-blueprint/SKILL.md +48 -1
- package/templates/harness/skills/st-execute-blueprint/scripts/capture-base-commit.cjs +325 -0
- package/templates/harness/skills/st-execute-blueprint/scripts/dispatch-task-execution.cjs +46 -26
- package/templates/harness/skills/st-execute-task/scripts/dispatch-task-execution.cjs +46 -26
- package/templates/harness/skills/st-full-workflow/SKILL.md +48 -1
- package/templates/harness/skills/st-full-workflow/scripts/capture-base-commit.cjs +325 -0
- package/templates/harness/skills/st-full-workflow/scripts/dispatch-task-execution.cjs +46 -26
- package/templates/strikethroo/config/hooks/CODE_REVIEW.md +60 -0
- package/templates/strikethroo/config/schemas/self-review-v2.xsd +437 -0
- package/dist-web/assets/channel-CN9J8c_F.js +0 -1
- package/dist-web/assets/classDiagram-4FO5ZUOK-DHR8VHf9.js +0 -1
- package/dist-web/assets/classDiagram-v2-Q7XG4LA2-DHR8VHf9.js +0 -1
- package/dist-web/assets/stateDiagram-v2-BHNVJYJU-CXpXsXHt.js +0 -1
|
@@ -0,0 +1,1539 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Review-category modelling and false-positive suppression heuristics in
|
|
3
|
+
// this skill draw on PR-Agent (https://github.com/The-PR-Agent/pr-agent),
|
|
4
|
+
// used under its permissive licence. No PR-Agent code is vendored.
|
|
5
|
+
"use strict";
|
|
6
|
+
var __create = Object.create;
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
9
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
10
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
11
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
32
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
33
|
+
|
|
34
|
+
// src/skill-scripts/code-review.ts
|
|
35
|
+
var code_review_exports = {};
|
|
36
|
+
__export(code_review_exports, {
|
|
37
|
+
MAX_REVIEW_ROUNDS: () => MAX_REVIEW_ROUNDS,
|
|
38
|
+
_decideRound: () => _decideRound,
|
|
39
|
+
_exitCodeFor: () => _exitCodeFor,
|
|
40
|
+
_readBaseCommit: () => _readBaseCommit,
|
|
41
|
+
_readCumulativeDiff: () => _readCumulativeDiff,
|
|
42
|
+
_readPriorAdjudicated: () => _readPriorAdjudicated,
|
|
43
|
+
buildReviewerPrompt: () => buildReviewerPrompt,
|
|
44
|
+
createFindingsGate: () => createFindingsGate,
|
|
45
|
+
main: () => main,
|
|
46
|
+
parseReviewMandate: () => parseReviewMandate,
|
|
47
|
+
runBoundedReviewRound: () => runBoundedReviewRound,
|
|
48
|
+
runReviewRound: () => runReviewRound
|
|
49
|
+
});
|
|
50
|
+
module.exports = __toCommonJS(code_review_exports);
|
|
51
|
+
var fs6 = __toESM(require("fs"));
|
|
52
|
+
var path6 = __toESM(require("path"));
|
|
53
|
+
|
|
54
|
+
// src/types.ts
|
|
55
|
+
var SUPPORTED_HARNESSES = [
|
|
56
|
+
"claude",
|
|
57
|
+
"codex",
|
|
58
|
+
"cursor",
|
|
59
|
+
"gemini",
|
|
60
|
+
"copilot",
|
|
61
|
+
"opencode"
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// src/skill-scripts/shared/git-utils.ts
|
|
65
|
+
var import_child_process = require("child_process");
|
|
66
|
+
var execGit = (command2) => {
|
|
67
|
+
try {
|
|
68
|
+
return (0, import_child_process.execSync)(command2, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
69
|
+
} catch (_error) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var execGitDiffAllowingChanges = (command2) => {
|
|
74
|
+
try {
|
|
75
|
+
return (0, import_child_process.execSync)(command2, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
const failure = error;
|
|
78
|
+
if (failure.status === 1 && typeof failure.stdout === "string") return failure.stdout;
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/skill-scripts/shared/root.ts
|
|
84
|
+
var fs = __toESM(require("fs"));
|
|
85
|
+
var path = __toESM(require("path"));
|
|
86
|
+
var EXPECTED_SCHEMA = true ? 4 : 4;
|
|
87
|
+
var isValidStrikethrooRoot = (strikethrooPath) => {
|
|
88
|
+
try {
|
|
89
|
+
if (!fs.existsSync(strikethrooPath)) return false;
|
|
90
|
+
if (!fs.lstatSync(strikethrooPath).isDirectory()) return false;
|
|
91
|
+
const metadataPath = path.join(strikethrooPath, ".init-metadata.json");
|
|
92
|
+
if (!fs.existsSync(metadataPath)) return false;
|
|
93
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
|
|
94
|
+
return metadata && typeof metadata === "object" && "version" in metadata;
|
|
95
|
+
} catch (_err) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
var getStrikethrooAt = (directory) => {
|
|
100
|
+
const strikethrooPath = path.join(directory, ".ai", "strikethroo");
|
|
101
|
+
return isValidStrikethrooRoot(strikethrooPath) ? strikethrooPath : null;
|
|
102
|
+
};
|
|
103
|
+
var getParentPaths = (currentPath, acc = []) => {
|
|
104
|
+
const absolutePath = path.resolve(currentPath);
|
|
105
|
+
const nextAcc = [...acc, absolutePath];
|
|
106
|
+
const parentPath = path.dirname(absolutePath);
|
|
107
|
+
if (parentPath === absolutePath) return nextAcc;
|
|
108
|
+
return getParentPaths(parentPath, nextAcc);
|
|
109
|
+
};
|
|
110
|
+
var checkWorkspaceSchema = (metadataPath) => {
|
|
111
|
+
let metadata;
|
|
112
|
+
try {
|
|
113
|
+
metadata = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
|
|
114
|
+
} catch {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const actual = typeof metadata.workspaceSchemaVersion === "number" ? metadata.workspaceSchemaVersion : 1;
|
|
118
|
+
if (actual === EXPECTED_SCHEMA) return;
|
|
119
|
+
if (actual < EXPECTED_SCHEMA) {
|
|
120
|
+
process.stderr.write(
|
|
121
|
+
`Workspace schema v${actual} is older than this skill requires (v${EXPECTED_SCHEMA}). Re-run \`npx strikethroo init\` with the latest CLI to update.
|
|
122
|
+
`
|
|
123
|
+
);
|
|
124
|
+
} else {
|
|
125
|
+
process.stderr.write(
|
|
126
|
+
`This skill (built for workspace schema v${EXPECTED_SCHEMA}) is older than the workspace (v${actual}). Re-run \`npx skills add e0ipso/strikethroo\` to update skills.
|
|
127
|
+
`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
process.exit(1);
|
|
131
|
+
};
|
|
132
|
+
var findStrikethrooRoot = (startPath = process.cwd()) => {
|
|
133
|
+
const paths = getParentPaths(startPath);
|
|
134
|
+
const found = paths.find((p) => getStrikethrooAt(p));
|
|
135
|
+
if (!found) return null;
|
|
136
|
+
const root = getStrikethrooAt(found);
|
|
137
|
+
if (root) checkWorkspaceSchema(path.join(root, ".init-metadata.json"));
|
|
138
|
+
return root;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// src/skill-scripts/shared/plan-resolve.ts
|
|
142
|
+
var fs3 = __toESM(require("fs"));
|
|
143
|
+
var path3 = __toESM(require("path"));
|
|
144
|
+
|
|
145
|
+
// src/skill-scripts/shared/plan-scan.ts
|
|
146
|
+
var fs2 = __toESM(require("fs"));
|
|
147
|
+
var path2 = __toESM(require("path"));
|
|
148
|
+
|
|
149
|
+
// src/skill-scripts/shared/frontmatter.ts
|
|
150
|
+
var ID_PATTERNS = [
|
|
151
|
+
/^\s*["']?id["']?\s*:\s*["']?([+-]?\d+)["']?\s*(?:#.*)?$/im,
|
|
152
|
+
/^\s*id\s*:\s*([+-]?\d+)\s*(?:#.*)?$/im,
|
|
153
|
+
/^\s*["']?id["']?\s*:\s*"([+-]?\d+)"\s*(?:#.*)?$/im,
|
|
154
|
+
/^\s*["']?id["']?\s*:\s*'([+-]?\d+)'\s*(?:#.*)?$/im,
|
|
155
|
+
/^\s*["']id["']\s*:\s*([+-]?\d+)\s*(?:#.*)?$/im,
|
|
156
|
+
/^\s*id\s*:\s*[|>]\s*([+-]?\d+)\s*$/im
|
|
157
|
+
];
|
|
158
|
+
var validateId = (rawId) => {
|
|
159
|
+
const id = parseInt(rawId, 10);
|
|
160
|
+
if (Number.isNaN(id) || id < 0 || id > Number.MAX_SAFE_INTEGER) return null;
|
|
161
|
+
return id;
|
|
162
|
+
};
|
|
163
|
+
var extractIdFromMarkdown = (content) => {
|
|
164
|
+
const frontmatterMatch = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
|
|
165
|
+
if (!frontmatterMatch || !frontmatterMatch[1]) return null;
|
|
166
|
+
const block = frontmatterMatch[1];
|
|
167
|
+
for (const pattern of ID_PATTERNS) {
|
|
168
|
+
const match = block.match(pattern);
|
|
169
|
+
if (match && match[1]) {
|
|
170
|
+
const id = validateId(match[1]);
|
|
171
|
+
if (id !== null) return id;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
};
|
|
176
|
+
var extractPlanId = (content, _filePath) => {
|
|
177
|
+
return extractIdFromMarkdown(content);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// src/skill-scripts/shared/plan-scan.ts
|
|
181
|
+
var PLAN_EXTENSIONS = [".md"];
|
|
182
|
+
var scanPlanDir = (planDirPath, dirName, isArchive) => {
|
|
183
|
+
let entries;
|
|
184
|
+
try {
|
|
185
|
+
entries = fs2.readdirSync(planDirPath, { withFileTypes: true });
|
|
186
|
+
} catch (_err) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
return entries.filter((e) => e.isFile() && PLAN_EXTENSIONS.some((ext) => e.name.endsWith(ext))).flatMap((e) => {
|
|
190
|
+
const filePath = path2.join(planDirPath, e.name);
|
|
191
|
+
try {
|
|
192
|
+
const content = fs2.readFileSync(filePath, "utf8");
|
|
193
|
+
const id = extractPlanId(content, filePath);
|
|
194
|
+
if (id === null) return [];
|
|
195
|
+
return [{ id, file: filePath, dir: planDirPath, isArchive, name: dirName }];
|
|
196
|
+
} catch (_err) {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
var getAllPlans = (taskManagerRoot) => {
|
|
202
|
+
const sources = [
|
|
203
|
+
{ dir: path2.join(taskManagerRoot, "plans"), isArchive: false },
|
|
204
|
+
{ dir: path2.join(taskManagerRoot, "archive"), isArchive: true }
|
|
205
|
+
];
|
|
206
|
+
return sources.flatMap(({ dir, isArchive }) => {
|
|
207
|
+
if (!fs2.existsSync(dir)) return [];
|
|
208
|
+
let entries;
|
|
209
|
+
try {
|
|
210
|
+
entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
211
|
+
} catch (_err) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
return entries.filter((e) => e.isDirectory()).flatMap((e) => scanPlanDir(path2.join(dir, e.name), e.name, isArchive));
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// src/skill-scripts/shared/plan-resolve.ts
|
|
219
|
+
var isValidRootDir = (strikethrooPath) => {
|
|
220
|
+
try {
|
|
221
|
+
if (!fs3.existsSync(strikethrooPath)) return false;
|
|
222
|
+
if (!fs3.lstatSync(strikethrooPath).isDirectory()) return false;
|
|
223
|
+
const metadataPath = path3.join(strikethrooPath, ".init-metadata.json");
|
|
224
|
+
if (!fs3.existsSync(metadataPath)) return false;
|
|
225
|
+
const metadata = JSON.parse(fs3.readFileSync(metadataPath, "utf8"));
|
|
226
|
+
return metadata && typeof metadata === "object" && "version" in metadata;
|
|
227
|
+
} catch (_err) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
var checkStandardRootShortcut = (filePath) => {
|
|
232
|
+
const planDir = path3.dirname(filePath);
|
|
233
|
+
const parentDir = path3.dirname(planDir);
|
|
234
|
+
const possibleRoot = path3.dirname(parentDir);
|
|
235
|
+
const parentBase = path3.basename(parentDir);
|
|
236
|
+
if (parentBase !== "plans" && parentBase !== "archive") return null;
|
|
237
|
+
if (path3.basename(possibleRoot) !== "strikethroo") return null;
|
|
238
|
+
const dotAiDir = path3.dirname(possibleRoot);
|
|
239
|
+
if (path3.basename(dotAiDir) !== ".ai") return null;
|
|
240
|
+
return isValidRootDir(possibleRoot) ? possibleRoot : null;
|
|
241
|
+
};
|
|
242
|
+
var resolveByPath = (absolutePath) => {
|
|
243
|
+
let content;
|
|
244
|
+
try {
|
|
245
|
+
content = fs3.readFileSync(absolutePath, "utf8");
|
|
246
|
+
} catch (_err) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
const planId = extractPlanId(content, absolutePath);
|
|
250
|
+
if (planId === null) return null;
|
|
251
|
+
const tmRoot = checkStandardRootShortcut(absolutePath) || findStrikethrooRoot(path3.dirname(absolutePath));
|
|
252
|
+
if (!tmRoot) return null;
|
|
253
|
+
return {
|
|
254
|
+
planFile: absolutePath,
|
|
255
|
+
planDir: path3.dirname(absolutePath),
|
|
256
|
+
strikethrooRoot: tmRoot,
|
|
257
|
+
planId
|
|
258
|
+
};
|
|
259
|
+
};
|
|
260
|
+
var resolveByIdInAncestry = (planId, startPath, searched = /* @__PURE__ */ new Set()) => {
|
|
261
|
+
const tmRoot = findStrikethrooRoot(startPath);
|
|
262
|
+
if (!tmRoot) return null;
|
|
263
|
+
const normalized = path3.normalize(tmRoot);
|
|
264
|
+
if (searched.has(normalized)) return null;
|
|
265
|
+
searched.add(normalized);
|
|
266
|
+
const plans = getAllPlans(tmRoot);
|
|
267
|
+
const match = plans.find((p) => p.id === planId);
|
|
268
|
+
if (match) {
|
|
269
|
+
return {
|
|
270
|
+
planFile: match.file,
|
|
271
|
+
planDir: match.dir,
|
|
272
|
+
strikethrooRoot: tmRoot,
|
|
273
|
+
planId
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const parentOfRoot = path3.dirname(path3.dirname(tmRoot));
|
|
277
|
+
if (parentOfRoot === tmRoot) return null;
|
|
278
|
+
return resolveByIdInAncestry(planId, parentOfRoot, searched);
|
|
279
|
+
};
|
|
280
|
+
var resolvePlan = (input, startPath = process.cwd()) => {
|
|
281
|
+
if (input === null || input === void 0 || input === "") return null;
|
|
282
|
+
const inputStr = String(input);
|
|
283
|
+
if (inputStr.startsWith("/")) {
|
|
284
|
+
return resolveByPath(inputStr);
|
|
285
|
+
}
|
|
286
|
+
const planId = parseInt(inputStr, 10);
|
|
287
|
+
if (Number.isNaN(planId)) return null;
|
|
288
|
+
return resolveByIdInAncestry(planId, startPath);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// src/skill-scripts/shared/harness-availability.ts
|
|
292
|
+
var fs5 = __toESM(require("fs"));
|
|
293
|
+
var path5 = __toESM(require("path"));
|
|
294
|
+
var import_child_process3 = require("child_process");
|
|
295
|
+
|
|
296
|
+
// src/skill-scripts/shared/external-dispatch.ts
|
|
297
|
+
var fs4 = __toESM(require("fs"));
|
|
298
|
+
var path4 = __toESM(require("path"));
|
|
299
|
+
var import_child_process2 = require("child_process");
|
|
300
|
+
var command = (executable, argv, request) => ({
|
|
301
|
+
executable,
|
|
302
|
+
argv,
|
|
303
|
+
cwd: request.workspace,
|
|
304
|
+
stdin: request.prompt
|
|
305
|
+
});
|
|
306
|
+
var modelArgv = (model) => model === void 0 ? [] : ["--model", model];
|
|
307
|
+
var EXTERNAL_HARNESS_ADAPTERS = {
|
|
308
|
+
claude: {
|
|
309
|
+
executable: "claude",
|
|
310
|
+
buildCommand: (request) => command(
|
|
311
|
+
"claude",
|
|
312
|
+
[
|
|
313
|
+
"-p",
|
|
314
|
+
...modelArgv(request.model),
|
|
315
|
+
...request.reasoningEffort === void 0 ? [] : ["--effort", request.reasoningEffort]
|
|
316
|
+
],
|
|
317
|
+
request
|
|
318
|
+
),
|
|
319
|
+
authenticationArgv: () => ["auth", "status"]
|
|
320
|
+
},
|
|
321
|
+
codex: {
|
|
322
|
+
executable: "codex",
|
|
323
|
+
buildCommand: (request) => command(
|
|
324
|
+
"codex",
|
|
325
|
+
[
|
|
326
|
+
"exec",
|
|
327
|
+
...modelArgv(request.model),
|
|
328
|
+
...request.reasoningEffort === void 0 ? [] : ["--config", `model_reasoning_effort=${request.reasoningEffort}`],
|
|
329
|
+
"-"
|
|
330
|
+
],
|
|
331
|
+
request
|
|
332
|
+
),
|
|
333
|
+
authenticationArgv: () => ["login", "status"]
|
|
334
|
+
},
|
|
335
|
+
cursor: {
|
|
336
|
+
executable: "cursor-agent",
|
|
337
|
+
buildCommand: (request) => command("cursor-agent", ["--print", ...modelArgv(request.model)], request),
|
|
338
|
+
authenticationArgv: () => ["status"]
|
|
339
|
+
},
|
|
340
|
+
gemini: {
|
|
341
|
+
executable: "gemini",
|
|
342
|
+
// The empty positional prompt is the existing contract — content travels on
|
|
343
|
+
// stdin. It stays even when the model pair is dropped.
|
|
344
|
+
buildCommand: (request) => command("gemini", ["--prompt", "", ...modelArgv(request.model)], request),
|
|
345
|
+
authenticationArgv: () => ["auth", "status"]
|
|
346
|
+
},
|
|
347
|
+
copilot: {
|
|
348
|
+
executable: "copilot",
|
|
349
|
+
buildCommand: (request) => command("copilot", ["-p", "", ...modelArgv(request.model)], request),
|
|
350
|
+
authenticationArgv: () => ["auth", "status"]
|
|
351
|
+
},
|
|
352
|
+
opencode: {
|
|
353
|
+
executable: "opencode",
|
|
354
|
+
buildCommand: (request) => command(
|
|
355
|
+
"opencode",
|
|
356
|
+
[
|
|
357
|
+
"run",
|
|
358
|
+
...modelArgv(request.model),
|
|
359
|
+
...request.reasoningEffort === void 0 ? [] : ["--variant", request.reasoningEffort],
|
|
360
|
+
"-"
|
|
361
|
+
],
|
|
362
|
+
request
|
|
363
|
+
),
|
|
364
|
+
authenticationArgv: () => ["auth", "list"]
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
var adapterKeys = Object.keys(EXTERNAL_HARNESS_ADAPTERS).sort();
|
|
368
|
+
var harnessKeys = [...SUPPORTED_HARNESSES].sort();
|
|
369
|
+
if (adapterKeys.join("\0") !== harnessKeys.join("\0")) {
|
|
370
|
+
throw new Error("External harness adapter registry does not cover SUPPORTED_HARNESSES exactly.");
|
|
371
|
+
}
|
|
372
|
+
var reviewCommandRequest = (request) => ({
|
|
373
|
+
workspace: request.workspace,
|
|
374
|
+
prompt: request.prompt
|
|
375
|
+
});
|
|
376
|
+
var executableOnPath = (executable) => (process.env.PATH ?? "").split(path4.delimiter).some((directory) => {
|
|
377
|
+
if (!directory) return false;
|
|
378
|
+
const candidate = path4.join(directory, executable);
|
|
379
|
+
try {
|
|
380
|
+
return fs4.statSync(candidate).isFile();
|
|
381
|
+
} catch {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
var runProcess = (executable, argv, cwd, stdin, inheritOutput = false) => new Promise((resolve3, reject) => {
|
|
386
|
+
let settled = false;
|
|
387
|
+
const fail = (error) => {
|
|
388
|
+
if (settled) return;
|
|
389
|
+
settled = true;
|
|
390
|
+
reject(error);
|
|
391
|
+
};
|
|
392
|
+
const child = (0, import_child_process2.spawn)(executable, argv, {
|
|
393
|
+
cwd,
|
|
394
|
+
shell: false,
|
|
395
|
+
stdio: [
|
|
396
|
+
stdin === void 0 ? "ignore" : "pipe",
|
|
397
|
+
inheritOutput ? "inherit" : "ignore",
|
|
398
|
+
inheritOutput ? "inherit" : "ignore"
|
|
399
|
+
]
|
|
400
|
+
});
|
|
401
|
+
child.once("error", fail);
|
|
402
|
+
child.once("close", (code) => {
|
|
403
|
+
if (settled) return;
|
|
404
|
+
settled = true;
|
|
405
|
+
resolve3({ exitCode: code ?? 1 });
|
|
406
|
+
});
|
|
407
|
+
if (stdin !== void 0) {
|
|
408
|
+
child.stdin.once("error", fail);
|
|
409
|
+
try {
|
|
410
|
+
child.stdin.end(stdin);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
var dependencies = {
|
|
417
|
+
executableExists: executableOnPath,
|
|
418
|
+
authenticate: async (commandSpec, adapter) => {
|
|
419
|
+
try {
|
|
420
|
+
const result = await runProcess(
|
|
421
|
+
commandSpec.executable,
|
|
422
|
+
adapter.authenticationArgv(),
|
|
423
|
+
commandSpec.cwd
|
|
424
|
+
);
|
|
425
|
+
return result.exitCode === 0 ? { ok: true } : { ok: false, detail: `${commandSpec.executable} authentication check failed.` };
|
|
426
|
+
} catch (error) {
|
|
427
|
+
return {
|
|
428
|
+
ok: false,
|
|
429
|
+
detail: `${commandSpec.executable} authentication check failed: ${errorMessage(error)}`
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
launch: (commandSpec) => runProcess(commandSpec.executable, commandSpec.argv, commandSpec.cwd, commandSpec.stdin, true)
|
|
434
|
+
};
|
|
435
|
+
var errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
436
|
+
var prepareLaunch = async (harness, input, active, guard) => {
|
|
437
|
+
const adapter = EXTERNAL_HARNESS_ADAPTERS[harness];
|
|
438
|
+
if (!adapter) {
|
|
439
|
+
return {
|
|
440
|
+
kind: "fallback",
|
|
441
|
+
reason: "adapter-unavailable",
|
|
442
|
+
detail: `No adapter is registered for ${harness}.`
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
const blocked = guard?.();
|
|
446
|
+
if (blocked) return blocked;
|
|
447
|
+
if (!active.executableExists(adapter.executable)) {
|
|
448
|
+
return {
|
|
449
|
+
kind: "fallback",
|
|
450
|
+
reason: "executable-unavailable",
|
|
451
|
+
detail: `${adapter.executable} is unavailable.`
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
const commandSpec = adapter.buildCommand(input);
|
|
455
|
+
const authentication = await active.authenticate(commandSpec, adapter);
|
|
456
|
+
if (!authentication.ok) {
|
|
457
|
+
return {
|
|
458
|
+
kind: "fallback",
|
|
459
|
+
reason: "authentication-failed",
|
|
460
|
+
detail: authentication.detail ?? `${adapter.executable} authentication check failed.`
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
return { kind: "ready", command: commandSpec };
|
|
464
|
+
};
|
|
465
|
+
var launchPrepared = async (prepared, active, label) => {
|
|
466
|
+
if (prepared.kind === "fallback") return prepared;
|
|
467
|
+
try {
|
|
468
|
+
const launched = await active.launch(prepared.command);
|
|
469
|
+
return launched.exitCode === 0 ? { kind: "launched-success", exitCode: 0 } : { kind: "launched-failure", exitCode: launched.exitCode };
|
|
470
|
+
} catch (error) {
|
|
471
|
+
return {
|
|
472
|
+
kind: "infrastructure-failure",
|
|
473
|
+
detail: `External ${label} process failed: ${errorMessage(error)}`
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
var dispatchReview = async (request, overrides = {}) => {
|
|
478
|
+
const active = { ...dependencies, ...overrides };
|
|
479
|
+
const prepared = await prepareLaunch(request.harness, reviewCommandRequest(request), active);
|
|
480
|
+
return launchPrepared(prepared, active, "review");
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
// src/skill-scripts/shared/harness-availability.ts
|
|
484
|
+
var AVAILABILITY_REGISTRY_VERSION = 1;
|
|
485
|
+
var AVAILABLE_TTL_MS = 30 * 60 * 1e3;
|
|
486
|
+
var UNAVAILABLE_TTL_MS = 5 * 60 * 1e3;
|
|
487
|
+
var PROBE_TIMEOUT_MS = 2e4;
|
|
488
|
+
var AVAILABILITY_CACHE_RELATIVE_PATH = path5.join("runtime", "harness-availability.json");
|
|
489
|
+
var PROBE_PROMPT = "Reply with OK.";
|
|
490
|
+
var probeCommand = (executable, argv, cwd, stdin = PROBE_PROMPT) => ({ executable, argv, cwd, stdin });
|
|
491
|
+
var HARNESS_AVAILABILITY_REGISTRY = {
|
|
492
|
+
claude: {
|
|
493
|
+
version: AVAILABILITY_REGISTRY_VERSION,
|
|
494
|
+
executable: "claude",
|
|
495
|
+
buildCommand: (cwd) => probeCommand("claude", ["-p"], cwd)
|
|
496
|
+
},
|
|
497
|
+
codex: {
|
|
498
|
+
version: AVAILABILITY_REGISTRY_VERSION,
|
|
499
|
+
executable: "codex",
|
|
500
|
+
buildCommand: (cwd) => probeCommand("codex", ["exec", "-"], cwd)
|
|
501
|
+
},
|
|
502
|
+
cursor: {
|
|
503
|
+
version: AVAILABILITY_REGISTRY_VERSION,
|
|
504
|
+
executable: "cursor-agent",
|
|
505
|
+
buildCommand: (cwd) => probeCommand("cursor-agent", ["--print"], cwd)
|
|
506
|
+
},
|
|
507
|
+
gemini: {
|
|
508
|
+
version: AVAILABILITY_REGISTRY_VERSION,
|
|
509
|
+
executable: "gemini",
|
|
510
|
+
buildCommand: (cwd) => probeCommand("gemini", ["--prompt", PROBE_PROMPT], cwd, "")
|
|
511
|
+
},
|
|
512
|
+
copilot: {
|
|
513
|
+
version: AVAILABILITY_REGISTRY_VERSION,
|
|
514
|
+
executable: "copilot",
|
|
515
|
+
buildCommand: (cwd) => probeCommand("copilot", ["-p", PROBE_PROMPT], cwd, "")
|
|
516
|
+
},
|
|
517
|
+
opencode: {
|
|
518
|
+
version: AVAILABILITY_REGISTRY_VERSION,
|
|
519
|
+
executable: "opencode",
|
|
520
|
+
buildCommand: (cwd) => probeCommand("opencode", ["run", "-"], cwd)
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
var registryKeys = Object.keys(HARNESS_AVAILABILITY_REGISTRY).sort();
|
|
524
|
+
var harnessKeys2 = [...SUPPORTED_HARNESSES].sort();
|
|
525
|
+
if (registryKeys.join("\0") !== harnessKeys2.join("\0")) {
|
|
526
|
+
throw new Error("Harness availability registry does not cover SUPPORTED_HARNESSES exactly.");
|
|
527
|
+
}
|
|
528
|
+
for (const harness of SUPPORTED_HARNESSES) {
|
|
529
|
+
const availability = HARNESS_AVAILABILITY_REGISTRY[harness];
|
|
530
|
+
if (availability.executable !== EXTERNAL_HARNESS_ADAPTERS[harness].executable) {
|
|
531
|
+
throw new Error(`Harness availability executable disagrees with the ${harness} adapter.`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
var safeReason = (value, fallback) => {
|
|
535
|
+
if (typeof value !== "string") return fallback;
|
|
536
|
+
const firstLine = value.replace(/[\r\n]+/g, " ").trim().slice(0, 200);
|
|
537
|
+
return firstLine || fallback;
|
|
538
|
+
};
|
|
539
|
+
var isOutcome = (value) => {
|
|
540
|
+
if (!value || typeof value !== "object") return false;
|
|
541
|
+
const entry = value;
|
|
542
|
+
return typeof entry.available === "boolean" && typeof entry.observedAt === "number" && Number.isFinite(entry.observedAt) && typeof entry.expiresAt === "number" && Number.isFinite(entry.expiresAt) && typeof entry.reason === "string";
|
|
543
|
+
};
|
|
544
|
+
var readCache = (cachePath) => {
|
|
545
|
+
try {
|
|
546
|
+
const parsed = JSON.parse(fs5.readFileSync(cachePath, "utf8"));
|
|
547
|
+
if (!parsed || typeof parsed !== "object") throw new Error("invalid cache");
|
|
548
|
+
const record = parsed;
|
|
549
|
+
if (record.version !== 1 || !record.harnesses || typeof record.harnesses !== "object") {
|
|
550
|
+
throw new Error("invalid cache");
|
|
551
|
+
}
|
|
552
|
+
const harnesses = {};
|
|
553
|
+
for (const harness of SUPPORTED_HARNESSES) {
|
|
554
|
+
const candidate = record.harnesses[harness];
|
|
555
|
+
if (isOutcome(candidate)) harnesses[harness] = candidate;
|
|
556
|
+
}
|
|
557
|
+
return { version: 1, harnesses };
|
|
558
|
+
} catch {
|
|
559
|
+
return { version: 1, harnesses: {} };
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
var writeCache = (cachePath, harness, outcome) => {
|
|
563
|
+
fs5.mkdirSync(path5.dirname(cachePath), { recursive: true });
|
|
564
|
+
const cache = readCache(cachePath);
|
|
565
|
+
const existing = cache.harnesses[harness];
|
|
566
|
+
if (!existing || existing.observedAt <= outcome.observedAt) {
|
|
567
|
+
cache.harnesses[harness] = {
|
|
568
|
+
available: outcome.available,
|
|
569
|
+
observedAt: outcome.observedAt,
|
|
570
|
+
expiresAt: outcome.expiresAt,
|
|
571
|
+
reason: outcome.reason
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
const temporary = `${cachePath}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
575
|
+
try {
|
|
576
|
+
fs5.writeFileSync(temporary, `${JSON.stringify(cache, null, 2)}
|
|
577
|
+
`, { mode: 384 });
|
|
578
|
+
fs5.renameSync(temporary, cachePath);
|
|
579
|
+
} finally {
|
|
580
|
+
try {
|
|
581
|
+
fs5.unlinkSync(temporary);
|
|
582
|
+
} catch {
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
var runProbe = (command2, timeoutMs) => new Promise((resolve3) => {
|
|
587
|
+
let settled = false;
|
|
588
|
+
let diagnostics = "";
|
|
589
|
+
const finish = (result) => {
|
|
590
|
+
if (settled) return;
|
|
591
|
+
settled = true;
|
|
592
|
+
resolve3(result);
|
|
593
|
+
};
|
|
594
|
+
const child = (0, import_child_process3.spawn)(command2.executable, command2.argv, {
|
|
595
|
+
cwd: command2.cwd,
|
|
596
|
+
shell: false,
|
|
597
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
598
|
+
});
|
|
599
|
+
const timer = setTimeout(() => {
|
|
600
|
+
child.kill("SIGKILL");
|
|
601
|
+
finish({ exitCode: 1, timedOut: true, detail: "Probe timed out." });
|
|
602
|
+
}, timeoutMs);
|
|
603
|
+
child.stderr?.on("data", (chunk) => {
|
|
604
|
+
if (diagnostics.length < 400) diagnostics += String(chunk).slice(0, 400 - diagnostics.length);
|
|
605
|
+
});
|
|
606
|
+
child.once("error", (error) => {
|
|
607
|
+
clearTimeout(timer);
|
|
608
|
+
finish({ exitCode: 1, detail: error.message });
|
|
609
|
+
});
|
|
610
|
+
child.once("close", (code) => {
|
|
611
|
+
clearTimeout(timer);
|
|
612
|
+
finish({ exitCode: code ?? 1, detail: diagnostics });
|
|
613
|
+
});
|
|
614
|
+
child.stdin?.on("error", () => void 0);
|
|
615
|
+
child.stdin?.end(command2.stdin);
|
|
616
|
+
});
|
|
617
|
+
var defaultDependencies = {
|
|
618
|
+
now: Date.now,
|
|
619
|
+
runProbe
|
|
620
|
+
};
|
|
621
|
+
var checkHarnessAvailability = async (request, overrides = {}) => {
|
|
622
|
+
const active = { ...defaultDependencies, ...overrides };
|
|
623
|
+
const now = active.now();
|
|
624
|
+
if (request.harness === void 0 || request.harness === request.currentHarness) {
|
|
625
|
+
return {
|
|
626
|
+
harness: request.harness ?? request.currentHarness,
|
|
627
|
+
available: true,
|
|
628
|
+
observedAt: now,
|
|
629
|
+
expiresAt: now,
|
|
630
|
+
reason: "Native/current harness targets do not require a probe.",
|
|
631
|
+
source: "bypass"
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
const harness = request.harness;
|
|
635
|
+
const cachePath = path5.join(request.strikethrooRoot, AVAILABILITY_CACHE_RELATIVE_PATH);
|
|
636
|
+
const cached = readCache(cachePath).harnesses[harness];
|
|
637
|
+
if (cached && cached.expiresAt > now) return { harness, ...cached, source: "cache" };
|
|
638
|
+
const definition = HARNESS_AVAILABILITY_REGISTRY[harness];
|
|
639
|
+
let probe;
|
|
640
|
+
try {
|
|
641
|
+
probe = await active.runProbe(definition.buildCommand(request.workspace), PROBE_TIMEOUT_MS);
|
|
642
|
+
} catch (error) {
|
|
643
|
+
probe = { exitCode: 1, detail: error instanceof Error ? error.message : String(error) };
|
|
644
|
+
}
|
|
645
|
+
const available = probe.exitCode === 0 && !probe.timedOut;
|
|
646
|
+
const reason = available ? "Harness probe succeeded." : safeReason(
|
|
647
|
+
probe.detail,
|
|
648
|
+
probe.timedOut ? "Harness probe timed out." : "Harness probe failed."
|
|
649
|
+
);
|
|
650
|
+
const outcome = {
|
|
651
|
+
harness,
|
|
652
|
+
available,
|
|
653
|
+
observedAt: now,
|
|
654
|
+
expiresAt: now + (available ? AVAILABLE_TTL_MS : UNAVAILABLE_TTL_MS),
|
|
655
|
+
reason,
|
|
656
|
+
source: "probe"
|
|
657
|
+
};
|
|
658
|
+
try {
|
|
659
|
+
writeCache(cachePath, harness, outcome);
|
|
660
|
+
} catch {
|
|
661
|
+
}
|
|
662
|
+
return outcome;
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
// src/skill-scripts/shared/harness-discovery.ts
|
|
666
|
+
var discoverHarnesses = async (request, overrides = {}) => {
|
|
667
|
+
const outcomes = await Promise.all(
|
|
668
|
+
SUPPORTED_HARNESSES.map(async (harness) => {
|
|
669
|
+
try {
|
|
670
|
+
return await checkHarnessAvailability(
|
|
671
|
+
{
|
|
672
|
+
strikethrooRoot: request.strikethrooRoot,
|
|
673
|
+
workspace: request.workspace,
|
|
674
|
+
harness,
|
|
675
|
+
currentHarness: request.currentHarness
|
|
676
|
+
},
|
|
677
|
+
overrides
|
|
678
|
+
);
|
|
679
|
+
} catch (error) {
|
|
680
|
+
const now = Date.now();
|
|
681
|
+
return {
|
|
682
|
+
harness,
|
|
683
|
+
available: false,
|
|
684
|
+
observedAt: now,
|
|
685
|
+
expiresAt: now,
|
|
686
|
+
reason: error instanceof Error ? error.message : "Harness availability check failed.",
|
|
687
|
+
source: "probe"
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
})
|
|
691
|
+
);
|
|
692
|
+
const reviewerCandidates = SUPPORTED_HARNESSES.filter((harness) => {
|
|
693
|
+
if (harness === request.currentHarness) return false;
|
|
694
|
+
const outcome = outcomes.find((candidate) => candidate.harness === harness);
|
|
695
|
+
return outcome?.available === true;
|
|
696
|
+
});
|
|
697
|
+
return { outcomes, reviewerCandidates };
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
// src/skill-scripts/shared/review-findings.ts
|
|
701
|
+
var import_child_process4 = require("child_process");
|
|
702
|
+
var SEVERITY_RANK = { critical: 4, major: 3, minor: 2, info: 1 };
|
|
703
|
+
var CONFIDENCE_RANK = { high: 3, medium: 2, low: 1 };
|
|
704
|
+
var MAX_REVIEW_ROUNDS = 3;
|
|
705
|
+
var DEFAULT_SEVERITY_FLOOR = "major";
|
|
706
|
+
var DEFAULT_CONFIDENCE_FLOOR = "high";
|
|
707
|
+
var XMLLINT_TIMEOUT_MS = 3e4;
|
|
708
|
+
var isSeverity = (value) => value in SEVERITY_RANK;
|
|
709
|
+
var isConfidence = (value) => value in CONFIDENCE_RANK;
|
|
710
|
+
var SEVERITY_FLOOR_RE = /^[ \t]*#{0,6}[ \t]*severity floor[ \t]*:[ \t]*`?([a-z]+)`?/im;
|
|
711
|
+
var CONFIDENCE_FLOOR_RE = /^[ \t]*#{0,6}[ \t]*confidence floor[ \t]*:[ \t]*`?([a-z]+)`?/im;
|
|
712
|
+
var ROUND_BUDGET_RE = /^[ \t]*#{0,6}[ \t]*round budget[ \t]*:[ \t]*`?(-?\d{1,9})`?/im;
|
|
713
|
+
var parseReviewMandate = (hookContent) => {
|
|
714
|
+
const notes = [];
|
|
715
|
+
const severityMatch = SEVERITY_FLOOR_RE.exec(hookContent);
|
|
716
|
+
const statedSeverity = (severityMatch?.[1] ?? "").toLowerCase();
|
|
717
|
+
let severityFloor = DEFAULT_SEVERITY_FLOOR;
|
|
718
|
+
if (isSeverity(statedSeverity)) {
|
|
719
|
+
severityFloor = statedSeverity;
|
|
720
|
+
} else {
|
|
721
|
+
notes.push(
|
|
722
|
+
`The hook states no recognised severity floor, so the compiled default \`${DEFAULT_SEVERITY_FLOOR}\` applies.`
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
const confidenceMatch = CONFIDENCE_FLOOR_RE.exec(hookContent);
|
|
726
|
+
const statedConfidence = (confidenceMatch?.[1] ?? "").toLowerCase();
|
|
727
|
+
let confidenceFloor = DEFAULT_CONFIDENCE_FLOOR;
|
|
728
|
+
if (isConfidence(statedConfidence)) {
|
|
729
|
+
confidenceFloor = statedConfidence;
|
|
730
|
+
} else {
|
|
731
|
+
notes.push(
|
|
732
|
+
`The hook states no recognised confidence floor, so the compiled default \`${DEFAULT_CONFIDENCE_FLOOR}\` applies.`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
const budgetMatch = ROUND_BUDGET_RE.exec(hookContent);
|
|
736
|
+
let roundBudget = MAX_REVIEW_ROUNDS;
|
|
737
|
+
if (budgetMatch === null) {
|
|
738
|
+
notes.push(
|
|
739
|
+
`The hook states no round budget, so the compiled ceiling of ${MAX_REVIEW_ROUNDS} rounds applies.`
|
|
740
|
+
);
|
|
741
|
+
} else {
|
|
742
|
+
const stated = Number(budgetMatch[1]);
|
|
743
|
+
if (!Number.isInteger(stated) || stated < 1) {
|
|
744
|
+
notes.push(
|
|
745
|
+
`The hook states a round budget of "${budgetMatch[1]}", which is not a positive whole number of rounds, so the compiled ceiling of ${MAX_REVIEW_ROUNDS} applies.`
|
|
746
|
+
);
|
|
747
|
+
} else if (stated > MAX_REVIEW_ROUNDS) {
|
|
748
|
+
notes.push(
|
|
749
|
+
`The hook states a round budget of ${stated}, above the compiled ceiling of ${MAX_REVIEW_ROUNDS}. The ceiling is enforced in code and cannot be raised by editing the hook, so ${MAX_REVIEW_ROUNDS} rounds apply.`
|
|
750
|
+
);
|
|
751
|
+
} else {
|
|
752
|
+
roundBudget = stated;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return { severityFloor, confidenceFloor, roundBudget, notes };
|
|
756
|
+
};
|
|
757
|
+
var validateAgainstSchema = (xsdFile, xmlFile, timeoutMs = XMLLINT_TIMEOUT_MS) => new Promise((resolve3) => {
|
|
758
|
+
let settled = false;
|
|
759
|
+
let diagnostics = "";
|
|
760
|
+
const finish = (result) => {
|
|
761
|
+
if (settled) return;
|
|
762
|
+
settled = true;
|
|
763
|
+
resolve3(result);
|
|
764
|
+
};
|
|
765
|
+
const child = (0, import_child_process4.spawn)("xmllint", ["--nonet", "--schema", xsdFile, xmlFile, "--noout"], {
|
|
766
|
+
shell: false,
|
|
767
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
768
|
+
});
|
|
769
|
+
const timer = setTimeout(() => {
|
|
770
|
+
child.kill("SIGKILL");
|
|
771
|
+
finish({
|
|
772
|
+
kind: "validator-unavailable",
|
|
773
|
+
detail: `xmllint did not return a verdict on ${xmlFile} within ${timeoutMs} ms, so the findings could not be validated.`
|
|
774
|
+
});
|
|
775
|
+
}, timeoutMs);
|
|
776
|
+
child.stderr?.on("data", (chunk) => {
|
|
777
|
+
if (diagnostics.length < 2e3) {
|
|
778
|
+
diagnostics += String(chunk).slice(0, 2e3 - diagnostics.length);
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
child.once("error", (error) => {
|
|
782
|
+
clearTimeout(timer);
|
|
783
|
+
const code = error.code;
|
|
784
|
+
finish({
|
|
785
|
+
kind: "validator-unavailable",
|
|
786
|
+
detail: code === "ENOENT" ? "`xmllint` was not found on PATH. The review gate validates every emitted review.xml against the vendored schema and cannot certify findings without it. Install libxml2-utils (or your platform equivalent) and re-run." : `\`xmllint\` could not be run (${code ?? "unknown error"}): ${error.message}`
|
|
787
|
+
});
|
|
788
|
+
});
|
|
789
|
+
child.once("close", (code) => {
|
|
790
|
+
clearTimeout(timer);
|
|
791
|
+
finish(
|
|
792
|
+
code === 0 ? { kind: "valid" } : {
|
|
793
|
+
kind: "invalid",
|
|
794
|
+
detail: diagnostics.trim() || `xmllint exited ${code ?? "with no status"}.`
|
|
795
|
+
}
|
|
796
|
+
);
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
var SUMMARY_LIMIT = 400;
|
|
800
|
+
var decodeEntities = (text) => text.replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&#(\d+);/g, (_match, digits) => String.fromCodePoint(Number(digits))).replace(
|
|
801
|
+
/&#x([0-9a-fA-F]+);/g,
|
|
802
|
+
(_match, hex) => String.fromCodePoint(parseInt(hex, 16))
|
|
803
|
+
).replace(/&/g, "&");
|
|
804
|
+
var localName = (raw) => {
|
|
805
|
+
const colon = raw.indexOf(":");
|
|
806
|
+
return colon === -1 ? raw : raw.slice(colon + 1);
|
|
807
|
+
};
|
|
808
|
+
var ATTRIBUTE_RE = /([A-Za-z_:][-.\w:]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
809
|
+
var parseAttributes = (source) => {
|
|
810
|
+
const attributes = {};
|
|
811
|
+
ATTRIBUTE_RE.lastIndex = 0;
|
|
812
|
+
let match = ATTRIBUTE_RE.exec(source);
|
|
813
|
+
while (match !== null) {
|
|
814
|
+
attributes[localName(match[1])] = decodeEntities(match[2] ?? match[3] ?? "");
|
|
815
|
+
match = ATTRIBUTE_RE.exec(source);
|
|
816
|
+
}
|
|
817
|
+
return attributes;
|
|
818
|
+
};
|
|
819
|
+
var findTagEnd = (xml, start) => {
|
|
820
|
+
let quote = null;
|
|
821
|
+
for (let index = start + 1; index < xml.length; index += 1) {
|
|
822
|
+
const character = xml[index];
|
|
823
|
+
if (quote !== null) {
|
|
824
|
+
if (character === quote) quote = null;
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
if (character === '"' || character === "'") {
|
|
828
|
+
quote = character;
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (character === ">") return index + 1;
|
|
832
|
+
}
|
|
833
|
+
return xml.length;
|
|
834
|
+
};
|
|
835
|
+
var lineRange = (attributes) => {
|
|
836
|
+
const newStart = attributes["new-line-start"];
|
|
837
|
+
const newEnd = attributes["new-line-end"];
|
|
838
|
+
if (newStart !== void 0) return `new:${newStart}-${newEnd ?? newStart}`;
|
|
839
|
+
const oldStart = attributes["old-line-start"];
|
|
840
|
+
const oldEnd = attributes["old-line-end"];
|
|
841
|
+
if (oldStart !== void 0) return `old:${oldStart}-${oldEnd ?? oldStart}`;
|
|
842
|
+
return null;
|
|
843
|
+
};
|
|
844
|
+
var parseReviewFindings = (xml) => {
|
|
845
|
+
const findings = [];
|
|
846
|
+
let file = null;
|
|
847
|
+
let comment = null;
|
|
848
|
+
let capture = null;
|
|
849
|
+
let buffer = "";
|
|
850
|
+
let index = 0;
|
|
851
|
+
const appendText = (text) => {
|
|
852
|
+
if (capture !== null) buffer += text;
|
|
853
|
+
};
|
|
854
|
+
while (index < xml.length) {
|
|
855
|
+
const open = xml.indexOf("<", index);
|
|
856
|
+
if (open === -1) break;
|
|
857
|
+
if (open > index) appendText(decodeEntities(xml.slice(index, open)));
|
|
858
|
+
if (xml.startsWith("<!--", open)) {
|
|
859
|
+
const end = xml.indexOf("-->", open + 4);
|
|
860
|
+
index = end === -1 ? xml.length : end + 3;
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (xml.startsWith("<![CDATA[", open)) {
|
|
864
|
+
const end = xml.indexOf("]]>", open + 9);
|
|
865
|
+
appendText(xml.slice(open + 9, end === -1 ? xml.length : end));
|
|
866
|
+
index = end === -1 ? xml.length : end + 3;
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
if (xml.startsWith("<?", open)) {
|
|
870
|
+
const end = xml.indexOf("?>", open + 2);
|
|
871
|
+
index = end === -1 ? xml.length : end + 2;
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (xml.startsWith("<!", open)) {
|
|
875
|
+
index = findTagEnd(xml, open);
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
const tagEnd = findTagEnd(xml, open);
|
|
879
|
+
const raw = xml.slice(open + 1, tagEnd - 1);
|
|
880
|
+
index = tagEnd;
|
|
881
|
+
const closing = raw.startsWith("/");
|
|
882
|
+
const selfClosing = !closing && raw.trimEnd().endsWith("/");
|
|
883
|
+
const body = closing ? raw.slice(1) : selfClosing ? raw.trimEnd().slice(0, -1) : raw;
|
|
884
|
+
const nameMatch = /^\s*([^\s/>]+)/.exec(body);
|
|
885
|
+
if (nameMatch === null) continue;
|
|
886
|
+
const name = localName(nameMatch[1]);
|
|
887
|
+
const rest = body.slice(nameMatch[0].length);
|
|
888
|
+
if (closing) {
|
|
889
|
+
if (name === "body" && capture === "body") {
|
|
890
|
+
if (comment !== null) comment.summary = buffer.trim().slice(0, SUMMARY_LIMIT);
|
|
891
|
+
capture = null;
|
|
892
|
+
} else if (name === "category" && capture === "category") {
|
|
893
|
+
if (comment !== null) comment.category = buffer.trim() || null;
|
|
894
|
+
capture = null;
|
|
895
|
+
} else if (name === "comment") {
|
|
896
|
+
if (comment !== null) findings.push({ ...comment });
|
|
897
|
+
comment = null;
|
|
898
|
+
capture = null;
|
|
899
|
+
} else if (name === "file") {
|
|
900
|
+
file = null;
|
|
901
|
+
}
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
const attributes = parseAttributes(rest);
|
|
905
|
+
if (name === "file") {
|
|
906
|
+
if (!selfClosing) file = attributes["path"] ?? "";
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
if (name === "comment") {
|
|
910
|
+
const severity = attributes["severity"] ?? "";
|
|
911
|
+
const confidence = attributes["confidence"] ?? "";
|
|
912
|
+
comment = {
|
|
913
|
+
file: file ?? "",
|
|
914
|
+
location: lineRange(attributes),
|
|
915
|
+
// Absent, empty, and unrecognised all become null, and null falls below
|
|
916
|
+
// every floor. This is the one place the fail-safe default lives.
|
|
917
|
+
severity: isSeverity(severity) ? severity : null,
|
|
918
|
+
confidence: isConfidence(confidence) ? confidence : null,
|
|
919
|
+
category: null,
|
|
920
|
+
hasSuggestion: false,
|
|
921
|
+
summary: ""
|
|
922
|
+
};
|
|
923
|
+
capture = null;
|
|
924
|
+
if (selfClosing) {
|
|
925
|
+
findings.push({ ...comment });
|
|
926
|
+
comment = null;
|
|
927
|
+
}
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
if (comment === null) continue;
|
|
931
|
+
if (name === "body") {
|
|
932
|
+
capture = "body";
|
|
933
|
+
buffer = "";
|
|
934
|
+
} else if (name === "category") {
|
|
935
|
+
capture = "category";
|
|
936
|
+
buffer = "";
|
|
937
|
+
} else if (name === "suggestion") {
|
|
938
|
+
comment.hasSuggestion = true;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return findings;
|
|
942
|
+
};
|
|
943
|
+
var partitionFindings = (findings, severityFloor, confidenceFloor) => {
|
|
944
|
+
const actionable = [];
|
|
945
|
+
const recorded = [];
|
|
946
|
+
let aboveFloor = 0;
|
|
947
|
+
let aboveFloorWithoutSuggestion = 0;
|
|
948
|
+
for (const finding of findings) {
|
|
949
|
+
const reasons = [];
|
|
950
|
+
if (finding.severity === null) {
|
|
951
|
+
reasons.push("severity-absent");
|
|
952
|
+
} else if (SEVERITY_RANK[finding.severity] < SEVERITY_RANK[severityFloor]) {
|
|
953
|
+
reasons.push("severity-below-floor");
|
|
954
|
+
}
|
|
955
|
+
if (finding.confidence === null) {
|
|
956
|
+
reasons.push("confidence-absent");
|
|
957
|
+
} else if (CONFIDENCE_RANK[finding.confidence] < CONFIDENCE_RANK[confidenceFloor]) {
|
|
958
|
+
reasons.push("confidence-below-floor");
|
|
959
|
+
}
|
|
960
|
+
const clearsFloors = reasons.length === 0;
|
|
961
|
+
if (clearsFloors) aboveFloor += 1;
|
|
962
|
+
if (!finding.hasSuggestion) {
|
|
963
|
+
reasons.push("no-suggestion");
|
|
964
|
+
if (clearsFloors) aboveFloorWithoutSuggestion += 1;
|
|
965
|
+
}
|
|
966
|
+
if (reasons.length === 0) {
|
|
967
|
+
actionable.push(finding);
|
|
968
|
+
} else {
|
|
969
|
+
recorded.push({ ...finding, reasons });
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
severityFloor,
|
|
974
|
+
confidenceFloor,
|
|
975
|
+
actionable,
|
|
976
|
+
recorded,
|
|
977
|
+
counts: {
|
|
978
|
+
total: findings.length,
|
|
979
|
+
aboveFloor,
|
|
980
|
+
belowFloor: findings.length - aboveFloor,
|
|
981
|
+
actionable: actionable.length,
|
|
982
|
+
recorded: recorded.length,
|
|
983
|
+
aboveFloorWithoutSuggestion
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
// src/skill-scripts/code-review.ts
|
|
989
|
+
var HOOK_RELATIVE_PATH = path6.join("config", "hooks", "CODE_REVIEW.md");
|
|
990
|
+
var XSD_RELATIVE_PATH = path6.join("config", "schemas", "self-review-v2.xsd");
|
|
991
|
+
var REVIEW_DIR_NAME = "review";
|
|
992
|
+
var BASE_COMMIT_FILE_NAME = "base-commit.json";
|
|
993
|
+
var REVIEW_FILE_NAME = "review.xml";
|
|
994
|
+
var FINDINGS_FILE_NAME = "findings.json";
|
|
995
|
+
var SHA_RE = /^[0-9a-f]{40}$/i;
|
|
996
|
+
var errorMessage2 = (error) => error instanceof Error ? error.message : String(error);
|
|
997
|
+
var readFileOrNull = (filePath) => {
|
|
998
|
+
try {
|
|
999
|
+
return fs6.readFileSync(filePath, "utf8");
|
|
1000
|
+
} catch {
|
|
1001
|
+
return null;
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
var createFindingsGate = (mandate) => async (context) => {
|
|
1005
|
+
const roundDir = path6.dirname(context.reviewFile);
|
|
1006
|
+
const findingsFile = path6.join(roundDir, FINDINGS_FILE_NAME);
|
|
1007
|
+
const record = (payload) => {
|
|
1008
|
+
try {
|
|
1009
|
+
fs6.mkdirSync(roundDir, { recursive: true });
|
|
1010
|
+
fs6.writeFileSync(findingsFile, `${JSON.stringify(payload, null, 2)}
|
|
1011
|
+
`, "utf8");
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
throw new Error(
|
|
1014
|
+
`The review round's findings partition could not be written to ${findingsFile}: ${errorMessage2(error)}`
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
const base = {
|
|
1019
|
+
round: context.round,
|
|
1020
|
+
reviewFile: context.reviewFile,
|
|
1021
|
+
xsdFile: context.xsdFile,
|
|
1022
|
+
severityFloor: mandate.severityFloor,
|
|
1023
|
+
confidenceFloor: mandate.confidenceFloor
|
|
1024
|
+
};
|
|
1025
|
+
if (!fs6.existsSync(context.reviewFile)) {
|
|
1026
|
+
const detail = `The reviewer did not write ${context.reviewFile}. A round with no findings document cannot be read as a round with no findings.`;
|
|
1027
|
+
record({ ...base, status: "findings-absent", detail, actionable: [], recorded: [] });
|
|
1028
|
+
return { kind: "findings-absent", detail };
|
|
1029
|
+
}
|
|
1030
|
+
const validation = await validateAgainstSchema(context.xsdFile, context.reviewFile);
|
|
1031
|
+
if (validation.kind === "validator-unavailable") {
|
|
1032
|
+
record({
|
|
1033
|
+
...base,
|
|
1034
|
+
status: "validator-unavailable",
|
|
1035
|
+
detail: validation.detail,
|
|
1036
|
+
actionable: [],
|
|
1037
|
+
recorded: []
|
|
1038
|
+
});
|
|
1039
|
+
return { kind: "validator-unavailable", detail: validation.detail };
|
|
1040
|
+
}
|
|
1041
|
+
if (validation.kind === "invalid") {
|
|
1042
|
+
const detail = `${context.reviewFile} does not validate against ${context.xsdFile}, so its findings were not thresholded and none of them was applied. xmllint reported: ${validation.detail}`;
|
|
1043
|
+
record({ ...base, status: "schema-invalid", detail, actionable: [], recorded: [] });
|
|
1044
|
+
return { kind: "schema-invalid", detail };
|
|
1045
|
+
}
|
|
1046
|
+
const xml = readFileOrNull(context.reviewFile);
|
|
1047
|
+
if (xml === null) {
|
|
1048
|
+
const detail = `${context.reviewFile} validated but could not then be read.`;
|
|
1049
|
+
record({ ...base, status: "findings-absent", detail, actionable: [], recorded: [] });
|
|
1050
|
+
return { kind: "findings-absent", detail };
|
|
1051
|
+
}
|
|
1052
|
+
const partition = partitionFindings(
|
|
1053
|
+
parseReviewFindings(xml),
|
|
1054
|
+
mandate.severityFloor,
|
|
1055
|
+
mandate.confidenceFloor
|
|
1056
|
+
);
|
|
1057
|
+
record({
|
|
1058
|
+
...base,
|
|
1059
|
+
status: "evaluated",
|
|
1060
|
+
counts: partition.counts,
|
|
1061
|
+
actionable: partition.actionable,
|
|
1062
|
+
recorded: partition.recorded
|
|
1063
|
+
});
|
|
1064
|
+
return {
|
|
1065
|
+
kind: "evaluated",
|
|
1066
|
+
aboveFloor: partition.counts.aboveFloor,
|
|
1067
|
+
belowFloor: partition.counts.belowFloor,
|
|
1068
|
+
actionable: partition.counts.actionable,
|
|
1069
|
+
recorded: partition.counts.recorded,
|
|
1070
|
+
total: partition.counts.total,
|
|
1071
|
+
aboveFloorWithoutSuggestion: partition.counts.aboveFloorWithoutSuggestion,
|
|
1072
|
+
severityFloor: mandate.severityFloor,
|
|
1073
|
+
confidenceFloor: mandate.confidenceFloor,
|
|
1074
|
+
findingsFile
|
|
1075
|
+
};
|
|
1076
|
+
};
|
|
1077
|
+
var _readBaseCommit = (filePath) => {
|
|
1078
|
+
const raw = readFileOrNull(filePath);
|
|
1079
|
+
if (raw === null) return null;
|
|
1080
|
+
try {
|
|
1081
|
+
const parsed = JSON.parse(raw);
|
|
1082
|
+
if (parsed && typeof parsed === "object") {
|
|
1083
|
+
const candidate = parsed.baseCommit;
|
|
1084
|
+
if (typeof candidate === "string" && SHA_RE.test(candidate)) return candidate;
|
|
1085
|
+
}
|
|
1086
|
+
} catch {
|
|
1087
|
+
return null;
|
|
1088
|
+
}
|
|
1089
|
+
return null;
|
|
1090
|
+
};
|
|
1091
|
+
var GENERATED_ATTRIBUTES = ["linguist-generated", "linguist-vendored"];
|
|
1092
|
+
var attributeExcluded = (workspace, files) => {
|
|
1093
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
1094
|
+
if (files.length === 0) return excluded;
|
|
1095
|
+
const report = execGit(
|
|
1096
|
+
`git -C ${JSON.stringify(workspace)} check-attr ${GENERATED_ATTRIBUTES.join(" ")} -- ` + files.map((file) => JSON.stringify(file)).join(" ")
|
|
1097
|
+
);
|
|
1098
|
+
if (report === null) return excluded;
|
|
1099
|
+
for (const line of report.split("\n")) {
|
|
1100
|
+
const marker = line.lastIndexOf(": ");
|
|
1101
|
+
if (marker === -1 || line.slice(marker + 2).trim() !== "true") continue;
|
|
1102
|
+
const withoutValue = line.slice(0, marker);
|
|
1103
|
+
const attribute = withoutValue.lastIndexOf(": ");
|
|
1104
|
+
if (attribute === -1) continue;
|
|
1105
|
+
excluded.add(withoutValue.slice(0, attribute));
|
|
1106
|
+
}
|
|
1107
|
+
return excluded;
|
|
1108
|
+
};
|
|
1109
|
+
var excludedPaths = (workspace, baseCommit) => {
|
|
1110
|
+
const changed = execGit(`git -C ${JSON.stringify(workspace)} diff --name-only ${baseCommit} --`);
|
|
1111
|
+
if (changed === null || changed.trim() === "") return [];
|
|
1112
|
+
const files = changed.split("\n").filter((line) => line.trim() !== "");
|
|
1113
|
+
return [...attributeExcluded(workspace, files)];
|
|
1114
|
+
};
|
|
1115
|
+
var untrackedPaths = (workspace) => {
|
|
1116
|
+
const listed = execGit(
|
|
1117
|
+
`git -c core.quotePath=false -C ${JSON.stringify(workspace)} ls-files --others --exclude-standard`
|
|
1118
|
+
);
|
|
1119
|
+
if (listed === null || listed.trim() === "") return [];
|
|
1120
|
+
const files = listed.split("\n").filter((line) => line.trim() !== "");
|
|
1121
|
+
const excluded = attributeExcluded(workspace, files);
|
|
1122
|
+
return files.filter((file) => !excluded.has(file));
|
|
1123
|
+
};
|
|
1124
|
+
var untrackedDiff = (workspace, file) => execGitDiffAllowingChanges(
|
|
1125
|
+
`git -C ${JSON.stringify(workspace)} diff --no-index --src-prefix=a/ --dst-prefix=b/ -- /dev/null ${JSON.stringify(file)}`
|
|
1126
|
+
);
|
|
1127
|
+
var _readCumulativeDiff = (workspace, baseCommit) => {
|
|
1128
|
+
const exclusions = excludedPaths(workspace, baseCommit).map((file) => ` ${JSON.stringify(`:(exclude,literal)${file}`)}`).join("");
|
|
1129
|
+
const tracked = execGit(
|
|
1130
|
+
`git -C ${JSON.stringify(workspace)} diff ${baseCommit} -- .${exclusions}`
|
|
1131
|
+
);
|
|
1132
|
+
if (tracked === null) return null;
|
|
1133
|
+
const added = untrackedPaths(workspace).map((file) => untrackedDiff(workspace, file)).filter((diff) => diff !== null && diff.trim() !== "");
|
|
1134
|
+
return [tracked, ...added].filter((part) => part.trim() !== "").join("\n");
|
|
1135
|
+
};
|
|
1136
|
+
var defaultDependencies2 = {
|
|
1137
|
+
discover: discoverHarnesses,
|
|
1138
|
+
dispatch: dispatchReview,
|
|
1139
|
+
readDiff: _readCumulativeDiff,
|
|
1140
|
+
validatorAvailable: () => executableOnPath("xmllint")
|
|
1141
|
+
};
|
|
1142
|
+
var readReviewerSkill = () => {
|
|
1143
|
+
const skillFile = path6.resolve(__dirname, "..", "SKILL.md");
|
|
1144
|
+
const content = readFileOrNull(skillFile);
|
|
1145
|
+
return content === null ? "Load the `st-code-review` skill and follow its Operating Procedure. If that skill is not installed on this harness, follow the mandate below exactly." : content;
|
|
1146
|
+
};
|
|
1147
|
+
var renderAdjudicated = (findings) => {
|
|
1148
|
+
if (findings.length === 0) {
|
|
1149
|
+
return "None. This is the first round, or no earlier finding has been ruled on.";
|
|
1150
|
+
}
|
|
1151
|
+
return findings.map((finding) => {
|
|
1152
|
+
const attributes = [
|
|
1153
|
+
finding.severity === void 0 ? null : `severity=${finding.severity}`,
|
|
1154
|
+
finding.confidence === void 0 ? null : `confidence=${finding.confidence}`
|
|
1155
|
+
].filter((part) => part !== null).join(" ");
|
|
1156
|
+
const where = finding.location === void 0 ? finding.file : `${finding.file}:${finding.location}`;
|
|
1157
|
+
return `- [${finding.disposition}] ${where}${attributes ? ` (${attributes})` : ""} \u2014 ${finding.summary}`;
|
|
1158
|
+
}).join("\n");
|
|
1159
|
+
};
|
|
1160
|
+
var buildReviewerPrompt = (input) => [
|
|
1161
|
+
`Strikethroo code review gate \u2014 Plan ${input.planId}, review round ${input.round}.`,
|
|
1162
|
+
"",
|
|
1163
|
+
"You are the independent reviewer, running on a different harness than the one",
|
|
1164
|
+
"that wrote this code. You detect; you never fix. Do not edit, create, or delete",
|
|
1165
|
+
"source files. Do not run formatters. Do not commit. Your entire output is one",
|
|
1166
|
+
"review.xml at the path named below, plus a short report of the counts.",
|
|
1167
|
+
"",
|
|
1168
|
+
`Repository / workspace root: ${input.workspace}`,
|
|
1169
|
+
`Strikethroo workspace root: ${input.strikethrooRoot}`,
|
|
1170
|
+
`Plan document (read it in full): ${input.planFile}`,
|
|
1171
|
+
`Review mandate hook: ${input.hookFile}`,
|
|
1172
|
+
`Findings schema to validate against: ${input.xsdFile}`,
|
|
1173
|
+
`Base commit anchoring this plan's scope: ${input.baseCommit}`,
|
|
1174
|
+
`Round: ${input.round}`,
|
|
1175
|
+
`Write your findings to: ${input.reviewFile}`,
|
|
1176
|
+
"",
|
|
1177
|
+
"## Review mandate (authoritative \u2014 it overrides the reviewer instructions below)",
|
|
1178
|
+
"",
|
|
1179
|
+
input.hookContent.trim(),
|
|
1180
|
+
"",
|
|
1181
|
+
"## Reviewer instructions",
|
|
1182
|
+
"",
|
|
1183
|
+
input.skillInstructions.trim(),
|
|
1184
|
+
"",
|
|
1185
|
+
"## Prior adjudicated findings \u2014 do not re-litigate these",
|
|
1186
|
+
"",
|
|
1187
|
+
renderAdjudicated(input.adjudicatedFindings),
|
|
1188
|
+
"",
|
|
1189
|
+
"## Cumulative diff",
|
|
1190
|
+
"",
|
|
1191
|
+
`Produced with \`git diff ${input.baseCommit} --\` in ${input.workspace}: the recorded`,
|
|
1192
|
+
"base commit against the current working tree. Committed phase work and",
|
|
1193
|
+
"uncommitted changes are both in scope. Review this diff, not an incremental one.",
|
|
1194
|
+
input.diff.trim().length === 0 ? "\nThe cumulative diff is empty. Emit a <review> with no <file> children and report\nzero findings. That is not an error." : `
|
|
1195
|
+
<<<BEGIN CUMULATIVE DIFF>>>
|
|
1196
|
+
${input.diff}
|
|
1197
|
+
<<<END CUMULATIVE DIFF>>>`,
|
|
1198
|
+
""
|
|
1199
|
+
].join("\n");
|
|
1200
|
+
var asAdjudicated = (value, disposition) => {
|
|
1201
|
+
if (!Array.isArray(value)) return [];
|
|
1202
|
+
const adjudicated = [];
|
|
1203
|
+
for (const entry of value) {
|
|
1204
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
1205
|
+
const finding = entry;
|
|
1206
|
+
if (typeof finding["file"] !== "string") continue;
|
|
1207
|
+
adjudicated.push({
|
|
1208
|
+
file: finding["file"],
|
|
1209
|
+
...typeof finding["location"] === "string" ? { location: finding["location"] } : {},
|
|
1210
|
+
...typeof finding["severity"] === "string" ? { severity: finding["severity"] } : {},
|
|
1211
|
+
...typeof finding["confidence"] === "string" ? { confidence: finding["confidence"] } : {},
|
|
1212
|
+
summary: typeof finding["summary"] === "string" ? finding["summary"] : "",
|
|
1213
|
+
disposition
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
return adjudicated;
|
|
1217
|
+
};
|
|
1218
|
+
var _readPriorAdjudicated = (planDir, round) => {
|
|
1219
|
+
const carried = /* @__PURE__ */ new Map();
|
|
1220
|
+
for (let prior = 1; prior < round; prior += 1) {
|
|
1221
|
+
const raw = readFileOrNull(
|
|
1222
|
+
path6.join(planDir, REVIEW_DIR_NAME, `round-${prior}`, FINDINGS_FILE_NAME)
|
|
1223
|
+
);
|
|
1224
|
+
if (raw === null) continue;
|
|
1225
|
+
try {
|
|
1226
|
+
const parsed = JSON.parse(raw);
|
|
1227
|
+
if (parsed === null || typeof parsed !== "object") continue;
|
|
1228
|
+
const partition = parsed;
|
|
1229
|
+
const roundFindings = [
|
|
1230
|
+
...asAdjudicated(partition["actionable"], "applied"),
|
|
1231
|
+
...asAdjudicated(partition["recorded"], "recorded-below-floor")
|
|
1232
|
+
];
|
|
1233
|
+
for (const finding of roundFindings) {
|
|
1234
|
+
carried.set(`${finding.file}|${finding.location ?? ""}|${finding.summary}`, finding);
|
|
1235
|
+
}
|
|
1236
|
+
} catch {
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return [...carried.values()];
|
|
1241
|
+
};
|
|
1242
|
+
var skip = (reason, detail) => ({
|
|
1243
|
+
kind: "skipped",
|
|
1244
|
+
reason,
|
|
1245
|
+
detail
|
|
1246
|
+
});
|
|
1247
|
+
var resolveReviewContext = (startPath, validatorAvailable = () => executableOnPath("xmllint")) => {
|
|
1248
|
+
const strikethrooRoot = findStrikethrooRoot(startPath);
|
|
1249
|
+
if (!strikethrooRoot) {
|
|
1250
|
+
return {
|
|
1251
|
+
kind: "ended",
|
|
1252
|
+
result: {
|
|
1253
|
+
kind: "infrastructure-failure",
|
|
1254
|
+
detail: `No Strikethroo workspace was found from ${startPath}.`
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
const hookFile = path6.join(strikethrooRoot, HOOK_RELATIVE_PATH);
|
|
1259
|
+
const hookContent = readFileOrNull(hookFile);
|
|
1260
|
+
if (hookContent === null) {
|
|
1261
|
+
return {
|
|
1262
|
+
kind: "ended",
|
|
1263
|
+
result: skip(
|
|
1264
|
+
"hook-absent",
|
|
1265
|
+
`No code review mandate at ${hookFile}, so the review gate was skipped. Re-run \`npx strikethroo init\` to add it.`
|
|
1266
|
+
)
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
if (hookContent.trim().length === 0) {
|
|
1270
|
+
return {
|
|
1271
|
+
kind: "ended",
|
|
1272
|
+
result: skip(
|
|
1273
|
+
"hook-empty",
|
|
1274
|
+
`The code review mandate at ${hookFile} is empty, which is the documented way to disable the gate, so the review gate was skipped.`
|
|
1275
|
+
)
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
const xsdFile = path6.join(strikethrooRoot, XSD_RELATIVE_PATH);
|
|
1279
|
+
if (!fs6.existsSync(xsdFile)) {
|
|
1280
|
+
return {
|
|
1281
|
+
kind: "ended",
|
|
1282
|
+
result: skip(
|
|
1283
|
+
"xsd-absent",
|
|
1284
|
+
`No findings schema at ${xsdFile}, so findings could not be validated and the review gate was skipped. Re-run \`npx strikethroo init\` to add it.`
|
|
1285
|
+
)
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
if (!validatorAvailable()) {
|
|
1289
|
+
return {
|
|
1290
|
+
kind: "ended",
|
|
1291
|
+
result: skip(
|
|
1292
|
+
"validator-absent",
|
|
1293
|
+
"No `xmllint` on PATH, so emitted findings could not be validated against the vendored schema and the review gate was skipped. Install libxml2-utils (Debian/Ubuntu), libxml2 (Homebrew), or your platform equivalent to enable the gate."
|
|
1294
|
+
)
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
return {
|
|
1298
|
+
kind: "resolved",
|
|
1299
|
+
context: {
|
|
1300
|
+
strikethrooRoot,
|
|
1301
|
+
workspace: path6.dirname(path6.dirname(strikethrooRoot)),
|
|
1302
|
+
hookFile,
|
|
1303
|
+
hookContent,
|
|
1304
|
+
xsdFile,
|
|
1305
|
+
mandate: parseReviewMandate(hookContent)
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
};
|
|
1309
|
+
var runReviewRound = async (request, overrides = {}) => {
|
|
1310
|
+
const dependencies2 = { ...defaultDependencies2, ...overrides };
|
|
1311
|
+
const startPath = request.startPath ?? process.cwd();
|
|
1312
|
+
const resolution = resolveReviewContext(startPath, dependencies2.validatorAvailable);
|
|
1313
|
+
if (resolution.kind === "ended") return resolution.result;
|
|
1314
|
+
const { strikethrooRoot, workspace, hookFile, hookContent, xsdFile, mandate } = resolution.context;
|
|
1315
|
+
const resolved = resolvePlan(request.plan, startPath);
|
|
1316
|
+
if (!resolved) {
|
|
1317
|
+
return {
|
|
1318
|
+
kind: "infrastructure-failure",
|
|
1319
|
+
detail: `Plan "${request.plan}" was not found or is invalid.`
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
const { planDir, planFile, planId } = resolved;
|
|
1323
|
+
const baseCommitFile = path6.join(planDir, REVIEW_DIR_NAME, BASE_COMMIT_FILE_NAME);
|
|
1324
|
+
const baseCommit = _readBaseCommit(baseCommitFile);
|
|
1325
|
+
if (baseCommit === null) {
|
|
1326
|
+
return skip(
|
|
1327
|
+
"base-commit-absent",
|
|
1328
|
+
`No base commit was recorded at ${baseCommitFile}, so the review had no anchored diff scope and the review gate was skipped.`
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
const discovery = await dependencies2.discover({
|
|
1332
|
+
strikethrooRoot,
|
|
1333
|
+
workspace,
|
|
1334
|
+
currentHarness: request.currentHarness
|
|
1335
|
+
});
|
|
1336
|
+
const harness = discovery.reviewerCandidates[0];
|
|
1337
|
+
if (harness === void 0) {
|
|
1338
|
+
return skip(
|
|
1339
|
+
"no-reviewer-candidate",
|
|
1340
|
+
`No harness other than \`${request.currentHarness}\` is installed and responsive, so the review gate was skipped.`
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
const diff = dependencies2.readDiff(workspace, baseCommit);
|
|
1344
|
+
if (diff === null) {
|
|
1345
|
+
return {
|
|
1346
|
+
kind: "infrastructure-failure",
|
|
1347
|
+
detail: `git diff ${baseCommit} failed in ${workspace}. The base commit was recorded, so this is a real failure rather than an absent-scope skip.`
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
if (diff.trim() === "") {
|
|
1351
|
+
return skip(
|
|
1352
|
+
"empty-diff",
|
|
1353
|
+
`The diff from ${baseCommit} to the working tree in ${workspace} is empty, so there was nothing to review. No reviewer was dispatched, and no round was certified.`
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
const roundDir = path6.join(planDir, REVIEW_DIR_NAME, `round-${request.round}`);
|
|
1357
|
+
const reviewFile = path6.join(roundDir, REVIEW_FILE_NAME);
|
|
1358
|
+
try {
|
|
1359
|
+
fs6.mkdirSync(roundDir, { recursive: true });
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
return {
|
|
1362
|
+
kind: "infrastructure-failure",
|
|
1363
|
+
detail: `Could not create the review round directory ${roundDir}: ${errorMessage2(error)}`
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
const prompt = buildReviewerPrompt({
|
|
1367
|
+
planId,
|
|
1368
|
+
planFile,
|
|
1369
|
+
strikethrooRoot,
|
|
1370
|
+
workspace,
|
|
1371
|
+
hookFile,
|
|
1372
|
+
hookContent,
|
|
1373
|
+
xsdFile,
|
|
1374
|
+
baseCommit,
|
|
1375
|
+
round: request.round,
|
|
1376
|
+
reviewFile,
|
|
1377
|
+
diff,
|
|
1378
|
+
adjudicatedFindings: request.adjudicatedFindings ?? _readPriorAdjudicated(planDir, request.round),
|
|
1379
|
+
skillInstructions: readReviewerSkill()
|
|
1380
|
+
});
|
|
1381
|
+
const dispatched = await dependencies2.dispatch({ harness, workspace, prompt });
|
|
1382
|
+
if (dispatched.kind === "infrastructure-failure") {
|
|
1383
|
+
return { kind: "infrastructure-failure", detail: dispatched.detail };
|
|
1384
|
+
}
|
|
1385
|
+
if (dispatched.kind === "fallback") {
|
|
1386
|
+
return {
|
|
1387
|
+
kind: "fallback",
|
|
1388
|
+
harness,
|
|
1389
|
+
round: request.round,
|
|
1390
|
+
reason: dispatched.reason,
|
|
1391
|
+
detail: dispatched.detail
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
if (dispatched.kind === "launched-failure") {
|
|
1395
|
+
return {
|
|
1396
|
+
kind: "launched-failure",
|
|
1397
|
+
harness,
|
|
1398
|
+
round: request.round,
|
|
1399
|
+
reviewFile,
|
|
1400
|
+
exitCode: dispatched.exitCode,
|
|
1401
|
+
detail: `The ${harness} reviewer exited ${dispatched.exitCode}.`
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
const evaluate = dependencies2.evaluateFindings ?? createFindingsGate(mandate);
|
|
1405
|
+
const findingsGate = await evaluate({
|
|
1406
|
+
reviewFile,
|
|
1407
|
+
xsdFile,
|
|
1408
|
+
planDir,
|
|
1409
|
+
round: request.round
|
|
1410
|
+
});
|
|
1411
|
+
return {
|
|
1412
|
+
kind: "reviewed",
|
|
1413
|
+
harness,
|
|
1414
|
+
round: request.round,
|
|
1415
|
+
baseCommit,
|
|
1416
|
+
reviewFile,
|
|
1417
|
+
reviewFilePresent: fs6.existsSync(reviewFile),
|
|
1418
|
+
findingsGate
|
|
1419
|
+
};
|
|
1420
|
+
};
|
|
1421
|
+
var _decideRound = (outcome, round, roundBudget) => {
|
|
1422
|
+
if (outcome.kind !== "evaluated") {
|
|
1423
|
+
return { kind: "round-failed", detail: outcome.detail };
|
|
1424
|
+
}
|
|
1425
|
+
const recorded = `${outcome.recorded} finding(s) were recorded without being applied, of which ${outcome.aboveFloorWithoutSuggestion} cleared both floors but carried no local fix. See ${outcome.findingsFile}.`;
|
|
1426
|
+
if (outcome.actionable === 0) {
|
|
1427
|
+
return {
|
|
1428
|
+
kind: "gate-passed",
|
|
1429
|
+
detail: `No finding cleared the \`${outcome.severityFloor}\` severity floor and the \`${outcome.confidenceFloor}\` confidence floor with a local fix attached, so the review gate passed on round ${round}. ${recorded}`
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
if (round >= roundBudget) {
|
|
1433
|
+
return {
|
|
1434
|
+
kind: "budget-exhausted",
|
|
1435
|
+
actionable: outcome.actionable,
|
|
1436
|
+
detail: `Round ${round} of an enforced ${roundBudget}-round budget still reports ${outcome.actionable} actionable finding(s), so the review gate halts with the budget exhausted. The plan stays in \`plans/\` and every round's findings are recorded under its \`review/\` directory. ${recorded}`
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
return {
|
|
1440
|
+
kind: "fix-and-continue",
|
|
1441
|
+
nextRound: round + 1,
|
|
1442
|
+
actionable: outcome.actionable,
|
|
1443
|
+
detail: `${outcome.actionable} finding(s) clear both floors and carry a local fix. Dispatch them on the implementer route, re-run POST_EXECUTION in full, then run round ${round + 1} of ${roundBudget}. ${recorded}`
|
|
1444
|
+
};
|
|
1445
|
+
};
|
|
1446
|
+
var runBoundedReviewRound = async (request, overrides = {}) => {
|
|
1447
|
+
const startPath = request.startPath ?? process.cwd();
|
|
1448
|
+
const resolution = resolveReviewContext(
|
|
1449
|
+
startPath,
|
|
1450
|
+
{ ...defaultDependencies2, ...overrides }.validatorAvailable
|
|
1451
|
+
);
|
|
1452
|
+
if (resolution.kind === "ended") return resolution.result;
|
|
1453
|
+
const { mandate } = resolution.context;
|
|
1454
|
+
if (request.round > mandate.roundBudget) {
|
|
1455
|
+
return {
|
|
1456
|
+
kind: "budget-exhausted",
|
|
1457
|
+
round: request.round,
|
|
1458
|
+
roundBudget: mandate.roundBudget,
|
|
1459
|
+
roundCeiling: MAX_REVIEW_ROUNDS,
|
|
1460
|
+
mandateNotes: mandate.notes,
|
|
1461
|
+
detail: `Round ${request.round} was requested, but the review gate enforces a ${mandate.roundBudget}-round budget (compiled ceiling ${MAX_REVIEW_ROUNDS}). No reviewer was dispatched. The plan stays in \`plans/\` and the rounds already run are recorded under its \`review/\` directory.`
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
const result = await runReviewRound(request, overrides);
|
|
1465
|
+
if (result.kind !== "reviewed") return result;
|
|
1466
|
+
return {
|
|
1467
|
+
...result,
|
|
1468
|
+
decision: _decideRound(result.findingsGate, request.round, mandate.roundBudget),
|
|
1469
|
+
roundBudget: mandate.roundBudget,
|
|
1470
|
+
roundCeiling: MAX_REVIEW_ROUNDS,
|
|
1471
|
+
mandateNotes: mandate.notes
|
|
1472
|
+
};
|
|
1473
|
+
};
|
|
1474
|
+
var emit = (result, exitCode) => {
|
|
1475
|
+
process.stdout.write(`${JSON.stringify(result)}
|
|
1476
|
+
`);
|
|
1477
|
+
process.exit(exitCode);
|
|
1478
|
+
};
|
|
1479
|
+
var _exitCodeFor = (result) => {
|
|
1480
|
+
if (result.kind === "infrastructure-failure") return 2;
|
|
1481
|
+
if (result.kind === "launched-failure") return 1;
|
|
1482
|
+
if (result.kind === "budget-exhausted") return 1;
|
|
1483
|
+
if (result.kind === "reviewed" && result.decision !== void 0) {
|
|
1484
|
+
return result.decision.kind === "budget-exhausted" || result.decision.kind === "round-failed" ? 1 : 0;
|
|
1485
|
+
}
|
|
1486
|
+
return 0;
|
|
1487
|
+
};
|
|
1488
|
+
var main = async (startPath = process.cwd()) => {
|
|
1489
|
+
const [planArg, harnessArg, roundArg] = process.argv.slice(2);
|
|
1490
|
+
if (!planArg || !harnessArg || !SUPPORTED_HARNESSES.includes(harnessArg)) {
|
|
1491
|
+
emit(
|
|
1492
|
+
{
|
|
1493
|
+
kind: "infrastructure-failure",
|
|
1494
|
+
detail: `Usage: code-review.cjs <plan-id-or-path> <current-harness> [round]. <current-harness> is one of: ${SUPPORTED_HARNESSES.join(", ")}.`
|
|
1495
|
+
},
|
|
1496
|
+
2
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
const round = roundArg === void 0 ? 1 : Number(roundArg);
|
|
1500
|
+
if (!Number.isInteger(round) || round < 1) {
|
|
1501
|
+
emit(
|
|
1502
|
+
{ kind: "infrastructure-failure", detail: `Round "${roundArg}" is not a positive integer.` },
|
|
1503
|
+
2
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
const result = await runBoundedReviewRound({
|
|
1507
|
+
plan: planArg,
|
|
1508
|
+
currentHarness: harnessArg,
|
|
1509
|
+
round,
|
|
1510
|
+
startPath
|
|
1511
|
+
});
|
|
1512
|
+
emit(result, _exitCodeFor(result));
|
|
1513
|
+
};
|
|
1514
|
+
if (require.main === module) {
|
|
1515
|
+
main().catch((error) => {
|
|
1516
|
+
emit(
|
|
1517
|
+
{
|
|
1518
|
+
kind: "infrastructure-failure",
|
|
1519
|
+
detail: `Code review gate infrastructure failed: ${errorMessage2(error)}`
|
|
1520
|
+
},
|
|
1521
|
+
2
|
|
1522
|
+
);
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1526
|
+
0 && (module.exports = {
|
|
1527
|
+
MAX_REVIEW_ROUNDS,
|
|
1528
|
+
_decideRound,
|
|
1529
|
+
_exitCodeFor,
|
|
1530
|
+
_readBaseCommit,
|
|
1531
|
+
_readCumulativeDiff,
|
|
1532
|
+
_readPriorAdjudicated,
|
|
1533
|
+
buildReviewerPrompt,
|
|
1534
|
+
createFindingsGate,
|
|
1535
|
+
main,
|
|
1536
|
+
parseReviewMandate,
|
|
1537
|
+
runBoundedReviewRound,
|
|
1538
|
+
runReviewRound
|
|
1539
|
+
});
|