@codexstar/bug-hunter 3.0.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/CHANGELOG.md +151 -0
- package/LICENSE +21 -0
- package/README.md +665 -0
- package/SKILL.md +624 -0
- package/bin/bug-hunter +222 -0
- package/evals/evals.json +362 -0
- package/modes/_dispatch.md +121 -0
- package/modes/extended.md +94 -0
- package/modes/fix-loop.md +115 -0
- package/modes/fix-pipeline.md +384 -0
- package/modes/large-codebase.md +212 -0
- package/modes/local-sequential.md +143 -0
- package/modes/loop.md +125 -0
- package/modes/parallel.md +113 -0
- package/modes/scaled.md +76 -0
- package/modes/single-file.md +38 -0
- package/modes/small.md +86 -0
- package/package.json +56 -0
- package/prompts/doc-lookup.md +44 -0
- package/prompts/examples/hunter-examples.md +131 -0
- package/prompts/examples/skeptic-examples.md +87 -0
- package/prompts/fixer.md +103 -0
- package/prompts/hunter.md +146 -0
- package/prompts/recon.md +159 -0
- package/prompts/referee.md +122 -0
- package/prompts/skeptic.md +143 -0
- package/prompts/threat-model.md +122 -0
- package/scripts/bug-hunter-state.cjs +537 -0
- package/scripts/code-index.cjs +541 -0
- package/scripts/context7-api.cjs +133 -0
- package/scripts/delta-mode.cjs +219 -0
- package/scripts/dep-scan.cjs +343 -0
- package/scripts/doc-lookup.cjs +316 -0
- package/scripts/fix-lock.cjs +167 -0
- package/scripts/init-test-fixture.sh +19 -0
- package/scripts/payload-guard.cjs +197 -0
- package/scripts/run-bug-hunter.cjs +892 -0
- package/scripts/tests/bug-hunter-state.test.cjs +87 -0
- package/scripts/tests/code-index.test.cjs +57 -0
- package/scripts/tests/delta-mode.test.cjs +47 -0
- package/scripts/tests/fix-lock.test.cjs +36 -0
- package/scripts/tests/fixtures/flaky-worker.cjs +63 -0
- package/scripts/tests/fixtures/low-confidence-worker.cjs +73 -0
- package/scripts/tests/fixtures/success-worker.cjs +42 -0
- package/scripts/tests/payload-guard.test.cjs +41 -0
- package/scripts/tests/run-bug-hunter.test.cjs +403 -0
- package/scripts/tests/test-utils.cjs +59 -0
- package/scripts/tests/worktree-harvest.test.cjs +297 -0
- package/scripts/triage.cjs +528 -0
- package/scripts/worktree-harvest.cjs +516 -0
- package/templates/subagent-wrapper.md +109 -0
|
@@ -0,0 +1,892 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const childProcess = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const BACKEND_PRIORITY = ['spawn_agent', 'subagent', 'teams', 'local-sequential'];
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 120000;
|
|
9
|
+
const DEFAULT_MAX_RETRIES = 1;
|
|
10
|
+
const DEFAULT_BACKOFF_MS = 1000;
|
|
11
|
+
const DEFAULT_CHUNK_SIZE = 30;
|
|
12
|
+
const DEFAULT_CONFIDENCE_THRESHOLD = 75;
|
|
13
|
+
const DEFAULT_CANARY_SIZE = 3;
|
|
14
|
+
const DEFAULT_DELTA_HOPS = 2;
|
|
15
|
+
const DEFAULT_EXPANSION_CAP = 40;
|
|
16
|
+
|
|
17
|
+
function usage() {
|
|
18
|
+
console.error('Usage:');
|
|
19
|
+
console.error(' run-bug-hunter.cjs preflight [--skill-dir <path>] [--available-backends <csv>] [--backend <name>]');
|
|
20
|
+
console.error(' run-bug-hunter.cjs run --files-json <path> [--mode <name>] [--skill-dir <path>] [--state <path>] [--chunk-size <n>] [--worker-cmd <template>] [--timeout-ms <n>] [--max-retries <n>] [--backoff-ms <n>] [--available-backends <csv>] [--backend <name>] [--fail-fast <true|false>] [--use-index <true|false>] [--index-path <path>] [--delta-mode <true|false>] [--changed-files-json <path>] [--delta-hops <n>] [--expand-on-low-confidence <true|false>] [--confidence-threshold <n>] [--canary-size <n>] [--expansion-cap <n>]');
|
|
21
|
+
console.error(' run-bug-hunter.cjs plan --files-json <path> [--mode <name>] [--skill-dir <path>] [--chunk-size <n>] [--plan-path <path>]');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function nowIso() {
|
|
25
|
+
return new Date().toISOString();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ensureDir(dirPath) {
|
|
29
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseArgs(argv) {
|
|
33
|
+
const [command, ...rest] = argv;
|
|
34
|
+
const options = {};
|
|
35
|
+
let index = 0;
|
|
36
|
+
while (index < rest.length) {
|
|
37
|
+
const token = rest[index];
|
|
38
|
+
if (!token.startsWith('--')) {
|
|
39
|
+
index += 1;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const key = token.slice(2);
|
|
43
|
+
const value = rest[index + 1];
|
|
44
|
+
if (!value || value.startsWith('--')) {
|
|
45
|
+
options[key] = 'true';
|
|
46
|
+
index += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
options[key] = value;
|
|
50
|
+
index += 2;
|
|
51
|
+
}
|
|
52
|
+
return { command, options };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toPositiveInt(value, fallback) {
|
|
56
|
+
const parsed = Number.parseInt(String(value || ''), 10);
|
|
57
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
58
|
+
return fallback;
|
|
59
|
+
}
|
|
60
|
+
return parsed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toBoolean(value, fallback) {
|
|
64
|
+
if (value === undefined) {
|
|
65
|
+
return fallback;
|
|
66
|
+
}
|
|
67
|
+
const normalized = String(value).toLowerCase();
|
|
68
|
+
if (normalized === 'true') {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (normalized === 'false') {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return fallback;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveSkillDir(options) {
|
|
78
|
+
if (options['skill-dir']) {
|
|
79
|
+
return path.resolve(options['skill-dir']);
|
|
80
|
+
}
|
|
81
|
+
return path.resolve(__dirname, '..');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getAvailableBackends(options) {
|
|
85
|
+
if (options['available-backends']) {
|
|
86
|
+
return String(options['available-backends'])
|
|
87
|
+
.split(',')
|
|
88
|
+
.map((item) => item.trim())
|
|
89
|
+
.filter(Boolean);
|
|
90
|
+
}
|
|
91
|
+
if (process.env.BUG_HUNTER_BACKENDS) {
|
|
92
|
+
return String(process.env.BUG_HUNTER_BACKENDS)
|
|
93
|
+
.split(',')
|
|
94
|
+
.map((item) => item.trim())
|
|
95
|
+
.filter(Boolean);
|
|
96
|
+
}
|
|
97
|
+
return ['local-sequential'];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function selectBackend(options) {
|
|
101
|
+
const forcedBackend = options.backend || process.env.BUG_HUNTER_BACKEND;
|
|
102
|
+
if (forcedBackend) {
|
|
103
|
+
if (!BACKEND_PRIORITY.includes(forcedBackend)) {
|
|
104
|
+
throw new Error(`Unsupported backend: ${forcedBackend}`);
|
|
105
|
+
}
|
|
106
|
+
return { selected: forcedBackend, available: getAvailableBackends(options), forced: true };
|
|
107
|
+
}
|
|
108
|
+
const available = getAvailableBackends(options);
|
|
109
|
+
const selected = BACKEND_PRIORITY.find((backend) => available.includes(backend)) || 'local-sequential';
|
|
110
|
+
return { selected, available, forced: false };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function requiredScripts(skillDir) {
|
|
114
|
+
return [
|
|
115
|
+
path.join(skillDir, 'scripts', 'bug-hunter-state.cjs'),
|
|
116
|
+
path.join(skillDir, 'scripts', 'payload-guard.cjs'),
|
|
117
|
+
path.join(skillDir, 'scripts', 'fix-lock.cjs'),
|
|
118
|
+
path.join(skillDir, 'scripts', 'doc-lookup.cjs'),
|
|
119
|
+
path.join(skillDir, 'scripts', 'context7-api.cjs'),
|
|
120
|
+
path.join(skillDir, 'scripts', 'delta-mode.cjs')
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function preflight(options) {
|
|
125
|
+
const skillDir = resolveSkillDir(options);
|
|
126
|
+
const missing = requiredScripts(skillDir).filter((filePath) => !fs.existsSync(filePath));
|
|
127
|
+
const backend = selectBackend(options);
|
|
128
|
+
return {
|
|
129
|
+
ok: missing.length === 0,
|
|
130
|
+
skillDir,
|
|
131
|
+
backend,
|
|
132
|
+
missing
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function runJsonScript(scriptPath, args) {
|
|
137
|
+
const result = childProcess.spawnSync('node', [scriptPath, ...args], {
|
|
138
|
+
encoding: 'utf8'
|
|
139
|
+
});
|
|
140
|
+
if (result.status !== 0) {
|
|
141
|
+
const stderr = (result.stderr || '').trim();
|
|
142
|
+
const stdout = (result.stdout || '').trim();
|
|
143
|
+
throw new Error(stderr || stdout || `Script failed: ${scriptPath}`);
|
|
144
|
+
}
|
|
145
|
+
const output = (result.stdout || '').trim();
|
|
146
|
+
if (!output) {
|
|
147
|
+
return {};
|
|
148
|
+
}
|
|
149
|
+
return JSON.parse(output);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function appendJournal(logPath, event) {
|
|
153
|
+
ensureDir(path.dirname(logPath));
|
|
154
|
+
const line = JSON.stringify({ at: nowIso(), ...event });
|
|
155
|
+
fs.appendFileSync(logPath, `${line}\n`, 'utf8');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function fillTemplate(template, variables) {
|
|
159
|
+
return template.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, key) => {
|
|
160
|
+
if (!(key in variables)) {
|
|
161
|
+
return match;
|
|
162
|
+
}
|
|
163
|
+
return String(variables[key]);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function sleep(ms) {
|
|
168
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function runCommandOnce({ command, timeoutMs }) {
|
|
172
|
+
return new Promise((resolve) => {
|
|
173
|
+
const child = childProcess.spawn('/bin/zsh', ['-lc', command], {
|
|
174
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
175
|
+
});
|
|
176
|
+
let stdout = '';
|
|
177
|
+
let stderr = '';
|
|
178
|
+
let timeoutHit = false;
|
|
179
|
+
|
|
180
|
+
const timer = setTimeout(() => {
|
|
181
|
+
timeoutHit = true;
|
|
182
|
+
child.kill('SIGTERM');
|
|
183
|
+
setTimeout(() => {
|
|
184
|
+
if (!child.killed) {
|
|
185
|
+
child.kill('SIGKILL');
|
|
186
|
+
}
|
|
187
|
+
}, 2000);
|
|
188
|
+
}, timeoutMs);
|
|
189
|
+
|
|
190
|
+
child.stdout.on('data', (chunk) => {
|
|
191
|
+
stdout += chunk.toString();
|
|
192
|
+
});
|
|
193
|
+
child.stderr.on('data', (chunk) => {
|
|
194
|
+
stderr += chunk.toString();
|
|
195
|
+
});
|
|
196
|
+
child.on('close', (code) => {
|
|
197
|
+
clearTimeout(timer);
|
|
198
|
+
resolve({
|
|
199
|
+
ok: code === 0 && !timeoutHit,
|
|
200
|
+
code: code || 0,
|
|
201
|
+
timeoutHit,
|
|
202
|
+
stdout: stdout.trim(),
|
|
203
|
+
stderr: stderr.trim()
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function runWithRetry({
|
|
210
|
+
command,
|
|
211
|
+
timeoutMs,
|
|
212
|
+
maxRetries,
|
|
213
|
+
backoffMs,
|
|
214
|
+
journalPath,
|
|
215
|
+
phase,
|
|
216
|
+
chunkId
|
|
217
|
+
}) {
|
|
218
|
+
const attempts = maxRetries + 1;
|
|
219
|
+
let lastResult = null;
|
|
220
|
+
|
|
221
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
222
|
+
appendJournal(journalPath, {
|
|
223
|
+
event: 'attempt-start',
|
|
224
|
+
phase,
|
|
225
|
+
chunkId,
|
|
226
|
+
attempt,
|
|
227
|
+
attempts,
|
|
228
|
+
timeoutMs
|
|
229
|
+
});
|
|
230
|
+
const result = await runCommandOnce({ command, timeoutMs });
|
|
231
|
+
lastResult = result;
|
|
232
|
+
appendJournal(journalPath, {
|
|
233
|
+
event: 'attempt-end',
|
|
234
|
+
phase,
|
|
235
|
+
chunkId,
|
|
236
|
+
attempt,
|
|
237
|
+
ok: result.ok,
|
|
238
|
+
code: result.code,
|
|
239
|
+
timeoutHit: result.timeoutHit,
|
|
240
|
+
stderr: result.stderr.slice(0, 500)
|
|
241
|
+
});
|
|
242
|
+
if (result.ok) {
|
|
243
|
+
return { ok: true, result, attemptsUsed: attempt };
|
|
244
|
+
}
|
|
245
|
+
if (attempt < attempts) {
|
|
246
|
+
const delayMs = backoffMs * 2 ** (attempt - 1);
|
|
247
|
+
appendJournal(journalPath, {
|
|
248
|
+
event: 'retry-backoff',
|
|
249
|
+
phase,
|
|
250
|
+
chunkId,
|
|
251
|
+
attempt,
|
|
252
|
+
delayMs
|
|
253
|
+
});
|
|
254
|
+
await sleep(delayMs);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
ok: false,
|
|
260
|
+
result: lastResult,
|
|
261
|
+
attemptsUsed: attempts
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function readJson(filePath) {
|
|
266
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function writeJson(filePath, value) {
|
|
270
|
+
ensureDir(path.dirname(filePath));
|
|
271
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function toArray(value) {
|
|
275
|
+
return Array.isArray(value) ? value : [];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function severityRank(severity) {
|
|
279
|
+
const normalized = String(severity || '').toLowerCase();
|
|
280
|
+
if (normalized === 'critical') {
|
|
281
|
+
return 3;
|
|
282
|
+
}
|
|
283
|
+
if (normalized === 'high') {
|
|
284
|
+
return 2;
|
|
285
|
+
}
|
|
286
|
+
if (normalized === 'medium') {
|
|
287
|
+
return 1;
|
|
288
|
+
}
|
|
289
|
+
if (normalized === 'low') {
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
return -1;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function buildHeuristicFactCard({ chunkId, scanFiles, findings, index }) {
|
|
296
|
+
const files = toArray(scanFiles).map((item) => path.resolve(String(item)));
|
|
297
|
+
const findingsList = toArray(findings);
|
|
298
|
+
const apiContracts = [];
|
|
299
|
+
const authAssumptions = [];
|
|
300
|
+
const invariants = [];
|
|
301
|
+
|
|
302
|
+
for (const filePath of files) {
|
|
303
|
+
const meta = index && index.files ? index.files[filePath] : null;
|
|
304
|
+
if (!meta) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const relative = meta.relativePath || filePath;
|
|
308
|
+
const boundaries = toArray(meta.trustBoundaries);
|
|
309
|
+
if (boundaries.includes('external-input')) {
|
|
310
|
+
apiContracts.push(`${relative}: external-input boundary`);
|
|
311
|
+
}
|
|
312
|
+
if (boundaries.includes('auth')) {
|
|
313
|
+
authAssumptions.push(`${relative}: auth boundary must preserve identity and authorization checks`);
|
|
314
|
+
}
|
|
315
|
+
if (boundaries.includes('data-store')) {
|
|
316
|
+
invariants.push(`${relative}: data-store writes must keep state transitions atomic`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
for (const finding of findingsList) {
|
|
321
|
+
const claim = String((finding && finding.claim) || '').trim();
|
|
322
|
+
if (!claim) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
invariants.push(`Finding invariant: ${claim}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
chunkId,
|
|
330
|
+
createdAt: nowIso(),
|
|
331
|
+
apiContracts: [...new Set(apiContracts)].slice(0, 10),
|
|
332
|
+
authAssumptions: [...new Set(authAssumptions)].slice(0, 10),
|
|
333
|
+
invariants: [...new Set(invariants)].slice(0, 12)
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function buildConsistencyReport({ bugLedger, confidenceThreshold }) {
|
|
338
|
+
const conflicts = [];
|
|
339
|
+
const byBugId = new Map();
|
|
340
|
+
const byLocation = new Map();
|
|
341
|
+
|
|
342
|
+
for (const entry of bugLedger) {
|
|
343
|
+
const bugId = String(entry.bugId || '').trim();
|
|
344
|
+
const locationKey = `${entry.file || ''}|${entry.lines || ''}`;
|
|
345
|
+
if (bugId) {
|
|
346
|
+
if (!byBugId.has(bugId)) {
|
|
347
|
+
byBugId.set(bugId, []);
|
|
348
|
+
}
|
|
349
|
+
byBugId.get(bugId).push(entry);
|
|
350
|
+
}
|
|
351
|
+
if (!byLocation.has(locationKey)) {
|
|
352
|
+
byLocation.set(locationKey, []);
|
|
353
|
+
}
|
|
354
|
+
byLocation.get(locationKey).push(entry);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const [bugId, entries] of byBugId.entries()) {
|
|
358
|
+
const uniqueKeys = new Set(entries.map((entry) => entry.key));
|
|
359
|
+
if (uniqueKeys.size > 1) {
|
|
360
|
+
conflicts.push({
|
|
361
|
+
type: 'bug-id-reused',
|
|
362
|
+
bugId,
|
|
363
|
+
count: uniqueKeys.size,
|
|
364
|
+
files: [...new Set(entries.map((entry) => entry.file))].sort()
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
for (const [location, entries] of byLocation.entries()) {
|
|
370
|
+
const claims = [...new Set(entries.map((entry) => String(entry.claim || '').trim()).filter(Boolean))];
|
|
371
|
+
if (claims.length > 1) {
|
|
372
|
+
conflicts.push({
|
|
373
|
+
type: 'location-claim-conflict',
|
|
374
|
+
location,
|
|
375
|
+
claims: claims.slice(0, 5)
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const lowConfidence = bugLedger.filter((entry) => {
|
|
381
|
+
const confidence = entry.confidence;
|
|
382
|
+
return confidence === null || confidence === undefined || Number(confidence) < confidenceThreshold;
|
|
383
|
+
}).length;
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
checkedAt: nowIso(),
|
|
387
|
+
confidenceThreshold,
|
|
388
|
+
totalFindings: bugLedger.length,
|
|
389
|
+
lowConfidenceFindings: lowConfidence,
|
|
390
|
+
conflicts
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function buildFixPlan({ bugLedger, confidenceThreshold, canarySize }) {
|
|
395
|
+
const withConfidence = bugLedger.map((entry) => {
|
|
396
|
+
const confidenceRaw = entry.confidence;
|
|
397
|
+
const confidence = Number.isFinite(Number(confidenceRaw)) ? Number(confidenceRaw) : null;
|
|
398
|
+
return {
|
|
399
|
+
...entry,
|
|
400
|
+
confidence
|
|
401
|
+
};
|
|
402
|
+
});
|
|
403
|
+
const eligible = withConfidence
|
|
404
|
+
.filter((entry) => entry.confidence !== null && entry.confidence >= confidenceThreshold)
|
|
405
|
+
.sort((left, right) => {
|
|
406
|
+
const severityDiff = severityRank(right.severity) - severityRank(left.severity);
|
|
407
|
+
if (severityDiff !== 0) {
|
|
408
|
+
return severityDiff;
|
|
409
|
+
}
|
|
410
|
+
const confidenceDiff = (right.confidence || 0) - (left.confidence || 0);
|
|
411
|
+
if (confidenceDiff !== 0) {
|
|
412
|
+
return confidenceDiff;
|
|
413
|
+
}
|
|
414
|
+
return String(left.key).localeCompare(String(right.key));
|
|
415
|
+
});
|
|
416
|
+
const manualReview = withConfidence
|
|
417
|
+
.filter((entry) => entry.confidence === null || entry.confidence < confidenceThreshold);
|
|
418
|
+
const canary = eligible.slice(0, canarySize);
|
|
419
|
+
const rollout = eligible.slice(canarySize);
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
generatedAt: nowIso(),
|
|
423
|
+
confidenceThreshold,
|
|
424
|
+
canarySize,
|
|
425
|
+
totals: {
|
|
426
|
+
findings: withConfidence.length,
|
|
427
|
+
eligible: eligible.length,
|
|
428
|
+
canary: canary.length,
|
|
429
|
+
rollout: rollout.length,
|
|
430
|
+
manualReview: manualReview.length
|
|
431
|
+
},
|
|
432
|
+
canary,
|
|
433
|
+
rollout,
|
|
434
|
+
manualReview
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function loadIndex(indexPath) {
|
|
439
|
+
if (!indexPath || !fs.existsSync(indexPath)) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
return readJson(indexPath);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function normalizeFiles(files) {
|
|
446
|
+
return [...new Set(toArray(files).map((filePath) => path.resolve(String(filePath))))].sort();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function processPendingChunks({
|
|
450
|
+
statePath,
|
|
451
|
+
stateScript,
|
|
452
|
+
chunksDir,
|
|
453
|
+
journalPath,
|
|
454
|
+
workerCmdTemplate,
|
|
455
|
+
timeoutMs,
|
|
456
|
+
maxRetries,
|
|
457
|
+
backoffMs,
|
|
458
|
+
failFast,
|
|
459
|
+
backend,
|
|
460
|
+
mode,
|
|
461
|
+
skillDir,
|
|
462
|
+
index
|
|
463
|
+
}) {
|
|
464
|
+
while (true) {
|
|
465
|
+
const next = runJsonScript(stateScript, ['next-chunk', statePath]);
|
|
466
|
+
if (next.done) {
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
const chunk = next.chunk;
|
|
470
|
+
const chunkFilesJsonPath = path.join(chunksDir, `${chunk.id}-files.json`);
|
|
471
|
+
const scanFilesJsonPath = path.join(chunksDir, `${chunk.id}-scan-files.json`);
|
|
472
|
+
const findingsJsonPath = path.join(chunksDir, `${chunk.id}-findings.json`);
|
|
473
|
+
const factsJsonPath = path.join(chunksDir, `${chunk.id}-facts.json`);
|
|
474
|
+
writeJson(chunkFilesJsonPath, chunk.files);
|
|
475
|
+
|
|
476
|
+
const hashFilterResult = runJsonScript(stateScript, ['hash-filter', statePath, chunkFilesJsonPath]);
|
|
477
|
+
const scanFiles = hashFilterResult.scan || [];
|
|
478
|
+
if (scanFiles.length === 0) {
|
|
479
|
+
appendJournal(journalPath, {
|
|
480
|
+
event: 'chunk-skip',
|
|
481
|
+
chunkId: chunk.id,
|
|
482
|
+
reason: 'hash-cache-no-changes'
|
|
483
|
+
});
|
|
484
|
+
runJsonScript(stateScript, ['mark-chunk', statePath, chunk.id, 'done']);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
writeJson(scanFilesJsonPath, scanFiles);
|
|
489
|
+
if (fs.existsSync(findingsJsonPath)) {
|
|
490
|
+
fs.unlinkSync(findingsJsonPath);
|
|
491
|
+
}
|
|
492
|
+
if (fs.existsSync(factsJsonPath)) {
|
|
493
|
+
fs.unlinkSync(factsJsonPath);
|
|
494
|
+
}
|
|
495
|
+
runJsonScript(stateScript, ['mark-chunk', statePath, chunk.id, 'in_progress']);
|
|
496
|
+
|
|
497
|
+
const command = fillTemplate(workerCmdTemplate, {
|
|
498
|
+
chunkId: chunk.id,
|
|
499
|
+
chunkFilesJson: chunkFilesJsonPath,
|
|
500
|
+
scanFilesJson: scanFilesJsonPath,
|
|
501
|
+
findingsJson: findingsJsonPath,
|
|
502
|
+
factsJson: factsJsonPath,
|
|
503
|
+
backend,
|
|
504
|
+
mode,
|
|
505
|
+
statePath,
|
|
506
|
+
skillDir
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
const runResult = await runWithRetry({
|
|
510
|
+
command,
|
|
511
|
+
timeoutMs,
|
|
512
|
+
maxRetries,
|
|
513
|
+
backoffMs,
|
|
514
|
+
journalPath,
|
|
515
|
+
phase: 'chunk-worker',
|
|
516
|
+
chunkId: chunk.id
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
if (!runResult.ok) {
|
|
520
|
+
const errorMessage = (runResult.result && runResult.result.stderr) || 'worker failed';
|
|
521
|
+
runJsonScript(stateScript, ['mark-chunk', statePath, chunk.id, 'failed', errorMessage.slice(0, 240)]);
|
|
522
|
+
appendJournal(journalPath, {
|
|
523
|
+
event: 'chunk-failed',
|
|
524
|
+
chunkId: chunk.id,
|
|
525
|
+
errorMessage: errorMessage.slice(0, 500)
|
|
526
|
+
});
|
|
527
|
+
if (failFast) {
|
|
528
|
+
throw new Error(`Chunk ${chunk.id} failed and fail-fast is enabled`);
|
|
529
|
+
}
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
let findings = [];
|
|
534
|
+
if (fs.existsSync(findingsJsonPath)) {
|
|
535
|
+
runJsonScript(stateScript, ['record-findings', statePath, findingsJsonPath, 'orchestrator']);
|
|
536
|
+
findings = readJson(findingsJsonPath);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (fs.existsSync(factsJsonPath)) {
|
|
540
|
+
runJsonScript(stateScript, ['record-fact-card', statePath, chunk.id, factsJsonPath]);
|
|
541
|
+
} else {
|
|
542
|
+
const factCard = buildHeuristicFactCard({
|
|
543
|
+
chunkId: chunk.id,
|
|
544
|
+
scanFiles,
|
|
545
|
+
findings,
|
|
546
|
+
index
|
|
547
|
+
});
|
|
548
|
+
writeJson(factsJsonPath, factCard);
|
|
549
|
+
runJsonScript(stateScript, ['record-fact-card', statePath, chunk.id, factsJsonPath]);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
runJsonScript(stateScript, ['hash-update', statePath, scanFilesJsonPath, 'scanned']);
|
|
553
|
+
runJsonScript(stateScript, ['mark-chunk', statePath, chunk.id, 'done']);
|
|
554
|
+
appendJournal(journalPath, {
|
|
555
|
+
event: 'chunk-done',
|
|
556
|
+
chunkId: chunk.id,
|
|
557
|
+
attemptsUsed: runResult.attemptsUsed
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function prepareIndexAndScope({
|
|
563
|
+
options,
|
|
564
|
+
skillDir,
|
|
565
|
+
statePath,
|
|
566
|
+
filesJsonPath,
|
|
567
|
+
journalPath
|
|
568
|
+
}) {
|
|
569
|
+
const useIndex = toBoolean(options['use-index'], false);
|
|
570
|
+
const deltaMode = toBoolean(options['delta-mode'], false);
|
|
571
|
+
const deltaHops = toPositiveInt(options['delta-hops'], DEFAULT_DELTA_HOPS);
|
|
572
|
+
const codeIndexScript = path.join(skillDir, 'scripts', 'code-index.cjs');
|
|
573
|
+
const deltaModeScript = path.join(skillDir, 'scripts', 'delta-mode.cjs');
|
|
574
|
+
const scopeDir = path.dirname(statePath);
|
|
575
|
+
const indexPath = path.resolve(options['index-path'] || path.join(scopeDir, 'index.json'));
|
|
576
|
+
|
|
577
|
+
let activeFilesJsonPath = filesJsonPath;
|
|
578
|
+
let deltaResult = null;
|
|
579
|
+
|
|
580
|
+
if (useIndex || deltaMode) {
|
|
581
|
+
if (!fs.existsSync(codeIndexScript)) {
|
|
582
|
+
if (deltaMode) {
|
|
583
|
+
throw new Error('code-index.cjs is required when --delta-mode=true');
|
|
584
|
+
}
|
|
585
|
+
appendJournal(journalPath, {
|
|
586
|
+
event: 'index-skip',
|
|
587
|
+
reason: 'missing-code-index',
|
|
588
|
+
codeIndexScript
|
|
589
|
+
});
|
|
590
|
+
return {
|
|
591
|
+
indexPath: null,
|
|
592
|
+
deltaMode: false,
|
|
593
|
+
deltaHops,
|
|
594
|
+
deltaResult: null,
|
|
595
|
+
activeFilesJsonPath
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
runJsonScript(codeIndexScript, ['build', indexPath, filesJsonPath, process.cwd()]);
|
|
599
|
+
appendJournal(journalPath, {
|
|
600
|
+
event: 'index-built',
|
|
601
|
+
indexPath
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (deltaMode) {
|
|
606
|
+
if (!options['changed-files-json']) {
|
|
607
|
+
throw new Error('--changed-files-json is required when --delta-mode=true');
|
|
608
|
+
}
|
|
609
|
+
const changedFilesJsonPath = path.resolve(options['changed-files-json']);
|
|
610
|
+
deltaResult = runJsonScript(deltaModeScript, [
|
|
611
|
+
'select',
|
|
612
|
+
indexPath,
|
|
613
|
+
changedFilesJsonPath,
|
|
614
|
+
String(deltaHops)
|
|
615
|
+
]);
|
|
616
|
+
const deltaSelectedPath = path.resolve(scopeDir, 'delta-selected-files.json');
|
|
617
|
+
writeJson(deltaSelectedPath, deltaResult.selected || []);
|
|
618
|
+
activeFilesJsonPath = deltaSelectedPath;
|
|
619
|
+
appendJournal(journalPath, {
|
|
620
|
+
event: 'delta-selected',
|
|
621
|
+
selected: (deltaResult.selected || []).length,
|
|
622
|
+
expansionCandidates: (deltaResult.expansionCandidates || []).length
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return {
|
|
627
|
+
indexPath: (useIndex || deltaMode) ? indexPath : null,
|
|
628
|
+
deltaMode,
|
|
629
|
+
deltaHops,
|
|
630
|
+
deltaResult,
|
|
631
|
+
activeFilesJsonPath
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async function runPipeline(options) {
|
|
636
|
+
if (!options['files-json']) {
|
|
637
|
+
throw new Error('--files-json is required for run command');
|
|
638
|
+
}
|
|
639
|
+
const skillDir = resolveSkillDir(options);
|
|
640
|
+
const preflightResult = preflight(options);
|
|
641
|
+
if (!preflightResult.ok) {
|
|
642
|
+
throw new Error(`Missing helper scripts: ${preflightResult.missing.join(', ')}`);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const backend = preflightResult.backend.selected;
|
|
646
|
+
const mode = options.mode || 'extended';
|
|
647
|
+
const filesJsonPath = path.resolve(options['files-json']);
|
|
648
|
+
const statePath = path.resolve(options.state || '.bug-hunter/state.json');
|
|
649
|
+
const chunkSize = toPositiveInt(options['chunk-size'], DEFAULT_CHUNK_SIZE);
|
|
650
|
+
const timeoutMs = toPositiveInt(options['timeout-ms'], DEFAULT_TIMEOUT_MS);
|
|
651
|
+
const maxRetries = toPositiveInt(options['max-retries'], DEFAULT_MAX_RETRIES);
|
|
652
|
+
const backoffMs = toPositiveInt(options['backoff-ms'], DEFAULT_BACKOFF_MS);
|
|
653
|
+
const failFast = toBoolean(options['fail-fast'], false);
|
|
654
|
+
const workerCmdTemplate = options['worker-cmd'] || 'node -e "process.exit(0)"';
|
|
655
|
+
const confidenceThreshold = toPositiveInt(options['confidence-threshold'], DEFAULT_CONFIDENCE_THRESHOLD);
|
|
656
|
+
const canarySize = toPositiveInt(options['canary-size'], DEFAULT_CANARY_SIZE);
|
|
657
|
+
const expansionCap = toPositiveInt(options['expansion-cap'], DEFAULT_EXPANSION_CAP);
|
|
658
|
+
const expandOnLowConfidence = toBoolean(options['expand-on-low-confidence'], true);
|
|
659
|
+
const journalPath = path.resolve(options['journal-path'] || '.bug-hunter/run.log');
|
|
660
|
+
const stateScript = path.join(skillDir, 'scripts', 'bug-hunter-state.cjs');
|
|
661
|
+
const deltaModeScript = path.join(skillDir, 'scripts', 'delta-mode.cjs');
|
|
662
|
+
const chunksDir = path.resolve(path.dirname(statePath), 'chunks');
|
|
663
|
+
const consistencyReportPath = path.resolve(options['consistency-report'] || path.join(path.dirname(statePath), 'consistency.json'));
|
|
664
|
+
const fixPlanPath = path.resolve(options['fix-plan-path'] || path.join(path.dirname(statePath), 'fix-plan.json'));
|
|
665
|
+
const factsPath = path.resolve(options['facts-path'] || path.join(path.dirname(statePath), 'bug-hunter-facts.json'));
|
|
666
|
+
ensureDir(chunksDir);
|
|
667
|
+
|
|
668
|
+
appendJournal(journalPath, {
|
|
669
|
+
event: 'run-start',
|
|
670
|
+
mode,
|
|
671
|
+
backend,
|
|
672
|
+
statePath,
|
|
673
|
+
filesJsonPath,
|
|
674
|
+
timeoutMs,
|
|
675
|
+
maxRetries,
|
|
676
|
+
backoffMs
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
const scope = prepareIndexAndScope({
|
|
680
|
+
options,
|
|
681
|
+
skillDir,
|
|
682
|
+
statePath,
|
|
683
|
+
filesJsonPath,
|
|
684
|
+
journalPath
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
if (!fs.existsSync(statePath)) {
|
|
688
|
+
runJsonScript(stateScript, ['init', statePath, mode, scope.activeFilesJsonPath, String(chunkSize)]);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
let index = loadIndex(scope.indexPath);
|
|
692
|
+
await processPendingChunks({
|
|
693
|
+
statePath,
|
|
694
|
+
stateScript,
|
|
695
|
+
chunksDir,
|
|
696
|
+
journalPath,
|
|
697
|
+
workerCmdTemplate,
|
|
698
|
+
timeoutMs,
|
|
699
|
+
maxRetries,
|
|
700
|
+
backoffMs,
|
|
701
|
+
failFast,
|
|
702
|
+
backend,
|
|
703
|
+
mode,
|
|
704
|
+
skillDir,
|
|
705
|
+
index
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
if (scope.deltaMode && expandOnLowConfidence) {
|
|
709
|
+
const state = readJson(statePath);
|
|
710
|
+
const lowConfidenceFiles = normalizeFiles(state.bugLedger
|
|
711
|
+
.filter((entry) => {
|
|
712
|
+
return entry.confidence === null || entry.confidence === undefined || Number(entry.confidence) < confidenceThreshold;
|
|
713
|
+
})
|
|
714
|
+
.map((entry) => entry.file));
|
|
715
|
+
if (lowConfidenceFiles.length > 0 && scope.indexPath) {
|
|
716
|
+
const lowConfidenceFilesJsonPath = path.resolve(path.dirname(statePath), 'low-confidence-files.json');
|
|
717
|
+
const selectedFilesJsonPath = scope.activeFilesJsonPath;
|
|
718
|
+
writeJson(lowConfidenceFilesJsonPath, lowConfidenceFiles);
|
|
719
|
+
const expansion = runJsonScript(deltaModeScript, [
|
|
720
|
+
'expand',
|
|
721
|
+
scope.indexPath,
|
|
722
|
+
lowConfidenceFilesJsonPath,
|
|
723
|
+
selectedFilesJsonPath,
|
|
724
|
+
String(scope.deltaHops || DEFAULT_DELTA_HOPS)
|
|
725
|
+
]);
|
|
726
|
+
const expandedFiles = [
|
|
727
|
+
...toArray(expansion.expanded),
|
|
728
|
+
...toArray(expansion.overlayOnly)
|
|
729
|
+
];
|
|
730
|
+
const cappedExpandedFiles = normalizeFiles(expandedFiles).slice(0, expansionCap);
|
|
731
|
+
if (cappedExpandedFiles.length > 0) {
|
|
732
|
+
const expansionFilesJsonPath = path.resolve(path.dirname(statePath), 'delta-expansion-files.json');
|
|
733
|
+
writeJson(expansionFilesJsonPath, cappedExpandedFiles);
|
|
734
|
+
const appendResult = runJsonScript(stateScript, ['append-files', statePath, expansionFilesJsonPath]);
|
|
735
|
+
appendJournal(journalPath, {
|
|
736
|
+
event: 'delta-expansion',
|
|
737
|
+
lowConfidenceFiles: lowConfidenceFiles.length,
|
|
738
|
+
expansionCandidates: expandedFiles.length,
|
|
739
|
+
expansionAppended: appendResult.appended || 0
|
|
740
|
+
});
|
|
741
|
+
if ((appendResult.appended || 0) > 0) {
|
|
742
|
+
const mergedSelected = normalizeFiles([
|
|
743
|
+
...readJson(selectedFilesJsonPath),
|
|
744
|
+
...cappedExpandedFiles
|
|
745
|
+
]);
|
|
746
|
+
writeJson(selectedFilesJsonPath, mergedSelected);
|
|
747
|
+
await processPendingChunks({
|
|
748
|
+
statePath,
|
|
749
|
+
stateScript,
|
|
750
|
+
chunksDir,
|
|
751
|
+
journalPath,
|
|
752
|
+
workerCmdTemplate,
|
|
753
|
+
timeoutMs,
|
|
754
|
+
maxRetries,
|
|
755
|
+
backoffMs,
|
|
756
|
+
failFast,
|
|
757
|
+
backend,
|
|
758
|
+
mode,
|
|
759
|
+
skillDir,
|
|
760
|
+
index
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const finalState = readJson(statePath);
|
|
768
|
+
const status = runJsonScript(stateScript, ['status', statePath]);
|
|
769
|
+
const consistency = buildConsistencyReport({
|
|
770
|
+
bugLedger: toArray(finalState.bugLedger),
|
|
771
|
+
confidenceThreshold
|
|
772
|
+
});
|
|
773
|
+
writeJson(consistencyReportPath, consistency);
|
|
774
|
+
runJsonScript(stateScript, ['set-consistency', statePath, consistencyReportPath]);
|
|
775
|
+
|
|
776
|
+
const fixPlan = buildFixPlan({
|
|
777
|
+
bugLedger: toArray(finalState.bugLedger),
|
|
778
|
+
confidenceThreshold,
|
|
779
|
+
canarySize
|
|
780
|
+
});
|
|
781
|
+
writeJson(fixPlanPath, fixPlan);
|
|
782
|
+
runJsonScript(stateScript, ['set-fix-plan', statePath, fixPlanPath]);
|
|
783
|
+
|
|
784
|
+
writeJson(factsPath, finalState.factCards || {});
|
|
785
|
+
|
|
786
|
+
appendJournal(journalPath, {
|
|
787
|
+
event: 'run-end',
|
|
788
|
+
status: status.summary,
|
|
789
|
+
consistencyConflicts: consistency.conflicts.length,
|
|
790
|
+
canary: fixPlan.totals.canary
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
return {
|
|
794
|
+
ok: true,
|
|
795
|
+
backend,
|
|
796
|
+
journalPath,
|
|
797
|
+
statePath,
|
|
798
|
+
indexPath: scope.indexPath,
|
|
799
|
+
deltaMode: scope.deltaMode,
|
|
800
|
+
deltaSummary: scope.deltaResult ? {
|
|
801
|
+
selectedCount: (scope.deltaResult.selected || []).length,
|
|
802
|
+
expansionCandidatesCount: (scope.deltaResult.expansionCandidates || []).length
|
|
803
|
+
} : null,
|
|
804
|
+
consistencyReportPath,
|
|
805
|
+
fixPlanPath,
|
|
806
|
+
factsPath,
|
|
807
|
+
status: status.summary,
|
|
808
|
+
consistency: {
|
|
809
|
+
conflicts: consistency.conflicts.length,
|
|
810
|
+
lowConfidenceFindings: consistency.lowConfidenceFindings
|
|
811
|
+
},
|
|
812
|
+
fixPlan: fixPlan.totals
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async function main() {
|
|
817
|
+
const { command, options } = parseArgs(process.argv.slice(2));
|
|
818
|
+
if (!command) {
|
|
819
|
+
usage();
|
|
820
|
+
process.exit(1);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (command === 'preflight') {
|
|
824
|
+
const result = preflight(options);
|
|
825
|
+
console.log(JSON.stringify(result, null, 2));
|
|
826
|
+
if (!result.ok) {
|
|
827
|
+
process.exit(1);
|
|
828
|
+
}
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (command === 'run') {
|
|
833
|
+
const result = await runPipeline(options);
|
|
834
|
+
console.log(JSON.stringify(result, null, 2));
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (command === 'plan') {
|
|
839
|
+
if (!options['files-json']) {
|
|
840
|
+
throw new Error('--files-json is required for plan command');
|
|
841
|
+
}
|
|
842
|
+
const skillDir = resolveSkillDir(options);
|
|
843
|
+
const filesJsonPath = path.resolve(options['files-json']);
|
|
844
|
+
const mode = options.mode || 'extended';
|
|
845
|
+
const chunkSize = toPositiveInt(options['chunk-size'], DEFAULT_CHUNK_SIZE);
|
|
846
|
+
const planPath = path.resolve(options['plan-path'] || '.bug-hunter/plan.json');
|
|
847
|
+
|
|
848
|
+
const files = readJson(filesJsonPath);
|
|
849
|
+
const totalFiles = files.length;
|
|
850
|
+
|
|
851
|
+
const chunks = [];
|
|
852
|
+
for (let i = 0; i < totalFiles; i += chunkSize) {
|
|
853
|
+
const chunkFiles = files.slice(i, i + chunkSize);
|
|
854
|
+
chunks.push({
|
|
855
|
+
id: `chunk-${chunks.length + 1}`,
|
|
856
|
+
files: chunkFiles,
|
|
857
|
+
fileCount: chunkFiles.length,
|
|
858
|
+
status: 'pending'
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const planOutput = {
|
|
863
|
+
generatedAt: nowIso(),
|
|
864
|
+
mode,
|
|
865
|
+
skillDir,
|
|
866
|
+
totalFiles,
|
|
867
|
+
chunkSize,
|
|
868
|
+
chunkCount: chunks.length,
|
|
869
|
+
phases: ['recon', 'hunter', 'skeptic', 'referee'],
|
|
870
|
+
chunks,
|
|
871
|
+
instructions: [
|
|
872
|
+
'This plan was generated for LLM agent consumption.',
|
|
873
|
+
'The agent should process chunks in order, using the state scripts to track progress.',
|
|
874
|
+
'For local-sequential mode: read modes/local-sequential.md for execution instructions.',
|
|
875
|
+
'For subagent mode: read modes/extended.md or modes/scaled.md for dispatch patterns.'
|
|
876
|
+
]
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
writeJson(planPath, planOutput);
|
|
880
|
+
console.log(JSON.stringify(planOutput, null, 2));
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
usage();
|
|
885
|
+
process.exit(1);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
main().catch((error) => {
|
|
889
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
890
|
+
console.error(message);
|
|
891
|
+
process.exit(1);
|
|
892
|
+
});
|