nightralph 0.0.53 → 0.0.56
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 +181 -19
- package/dist/blockers.js +149 -0
- package/dist/blockers.js.map +7 -0
- package/dist/display.js +12 -4
- package/dist/display.js.map +2 -2
- package/dist/docs-templates/issue-tracker.md +9 -1
- package/dist/index.js +2735 -415
- package/dist/index.js.map +4 -4
- package/dist/learnings.js +360 -0
- package/dist/learnings.js.map +7 -0
- package/dist/meta.json +289 -16
- package/dist/orchestrator.js +753 -188
- package/dist/orchestrator.js.map +3 -3
- package/dist/preflight.js +34 -0
- package/dist/preflight.js.map +7 -0
- package/dist/progress.js +5 -4
- package/dist/progress.js.map +2 -2
- package/dist/resolve.js +98 -9
- package/dist/resolve.js.map +2 -2
- package/dist/retry.js +83 -0
- package/dist/retry.js.map +7 -0
- package/dist/review.js +669 -0
- package/dist/review.js.map +7 -0
- package/dist/rules.js +67 -0
- package/dist/rules.js.map +7 -0
- package/dist/skills/to-tickets/SKILL.md +5 -5
- package/dist/src/blockers.d.ts +45 -0
- package/dist/src/blockers.d.ts.map +1 -0
- package/dist/src/display.d.ts +9 -0
- package/dist/src/display.d.ts.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/learnings.d.ts +70 -0
- package/dist/src/learnings.d.ts.map +1 -0
- package/dist/src/orchestrator.d.ts +85 -28
- package/dist/src/orchestrator.d.ts.map +1 -1
- package/dist/src/preflight.d.ts +6 -0
- package/dist/src/preflight.d.ts.map +1 -0
- package/dist/src/progress.d.ts +1 -1
- package/dist/src/progress.d.ts.map +1 -1
- package/dist/src/resolve.d.ts +24 -0
- package/dist/src/resolve.d.ts.map +1 -1
- package/dist/src/retry.d.ts +26 -0
- package/dist/src/retry.d.ts.map +1 -0
- package/dist/src/review.d.ts +131 -0
- package/dist/src/review.d.ts.map +1 -0
- package/dist/src/rules.d.ts +11 -0
- package/dist/src/rules.d.ts.map +1 -0
- package/dist/src/stats.d.ts +43 -0
- package/dist/src/stats.d.ts.map +1 -0
- package/dist/src/testcmd.d.ts +1 -0
- package/dist/src/testcmd.d.ts.map +1 -1
- package/dist/src/worktree.d.ts +30 -1
- package/dist/src/worktree.d.ts.map +1 -1
- package/dist/stats.js +111 -0
- package/dist/stats.js.map +7 -0
- package/dist/testcmd.js +13 -0
- package/dist/testcmd.js.map +2 -2
- package/dist/worktree.js +236 -32
- package/dist/worktree.js.map +2 -2
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -3,8 +3,9 @@ var __defProp = Object.defineProperty;
|
|
|
3
3
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
4
4
|
|
|
5
5
|
// src/index.ts
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
7
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
|
|
8
|
+
import { basename as basename6, dirname as dirname6, join as join9 } from "node:path";
|
|
8
9
|
import yargs from "yargs";
|
|
9
10
|
import { hideBin } from "yargs/helpers";
|
|
10
11
|
import { select } from "@inquirer/prompts";
|
|
@@ -198,14 +199,15 @@ __name(setup, "setup");
|
|
|
198
199
|
import { spawn as spawn2, execFile as execFile4 } from "node:child_process";
|
|
199
200
|
import {
|
|
200
201
|
createWriteStream,
|
|
201
|
-
mkdirSync as
|
|
202
|
-
readFileSync as
|
|
203
|
-
writeFileSync as
|
|
202
|
+
mkdirSync as mkdirSync3,
|
|
203
|
+
readFileSync as readFileSync6,
|
|
204
|
+
writeFileSync as writeFileSync5,
|
|
204
205
|
unlinkSync,
|
|
205
|
-
readdirSync as readdirSync2
|
|
206
|
+
readdirSync as readdirSync2,
|
|
207
|
+
statSync
|
|
206
208
|
} from "node:fs";
|
|
207
209
|
import { tmpdir } from "node:os";
|
|
208
|
-
import { join as
|
|
210
|
+
import { join as join6, dirname as dirname4, basename as basename5, relative as relative2 } from "node:path";
|
|
209
211
|
import { createInterface } from "node:readline";
|
|
210
212
|
import { promisify as promisify4 } from "node:util";
|
|
211
213
|
|
|
@@ -233,15 +235,138 @@ function getRepoName() {
|
|
|
233
235
|
return basename(dirname(resolve(commonDir)));
|
|
234
236
|
}
|
|
235
237
|
__name(getRepoName, "getRepoName");
|
|
236
|
-
function getCurrentBranch() {
|
|
238
|
+
function getCurrentBranch(cwd) {
|
|
237
239
|
const stdout = execFileSync(
|
|
238
240
|
"git",
|
|
239
241
|
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
240
|
-
{ encoding: "utf8" }
|
|
242
|
+
{ cwd, encoding: "utf8" }
|
|
241
243
|
);
|
|
242
244
|
return stdout.trim();
|
|
243
245
|
}
|
|
244
246
|
__name(getCurrentBranch, "getCurrentBranch");
|
|
247
|
+
function localBranchExists(repoRoot, name) {
|
|
248
|
+
try {
|
|
249
|
+
execFileSync(
|
|
250
|
+
"git",
|
|
251
|
+
["show-ref", "--verify", "--quiet", `refs/heads/${name}`],
|
|
252
|
+
{ cwd: repoRoot, stdio: "ignore" }
|
|
253
|
+
);
|
|
254
|
+
return true;
|
|
255
|
+
} catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
__name(localBranchExists, "localBranchExists");
|
|
260
|
+
function getDefaultBranch(repoRoot) {
|
|
261
|
+
try {
|
|
262
|
+
const ref = execFileSync(
|
|
263
|
+
"git",
|
|
264
|
+
[
|
|
265
|
+
"symbolic-ref",
|
|
266
|
+
"--quiet",
|
|
267
|
+
"--short",
|
|
268
|
+
"refs/remotes/origin/HEAD"
|
|
269
|
+
],
|
|
270
|
+
{
|
|
271
|
+
cwd: repoRoot,
|
|
272
|
+
encoding: "utf8",
|
|
273
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
274
|
+
}
|
|
275
|
+
).trim();
|
|
276
|
+
if (ref) return ref.replace(/^origin\//, "");
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
for (const name of ["main", "master"]) {
|
|
280
|
+
if (localBranchExists(repoRoot, name)) return name;
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
__name(getDefaultBranch, "getDefaultBranch");
|
|
285
|
+
function integrationBranchName(features) {
|
|
286
|
+
const distinct = [...new Set(features.filter(Boolean))];
|
|
287
|
+
return distinct.length === 1 ? `nightralph--${distinct[0]}` : "nightralph--all";
|
|
288
|
+
}
|
|
289
|
+
__name(integrationBranchName, "integrationBranchName");
|
|
290
|
+
function planIntegrationBranch(current, defaultBranch, features) {
|
|
291
|
+
if (!defaultBranch || current === "HEAD") return null;
|
|
292
|
+
if (current !== defaultBranch) return null;
|
|
293
|
+
return integrationBranchName(features);
|
|
294
|
+
}
|
|
295
|
+
__name(planIntegrationBranch, "planIntegrationBranch");
|
|
296
|
+
function gitText(cwd, args) {
|
|
297
|
+
return execFileSync("git", args, { cwd, encoding: "utf8" }).trim();
|
|
298
|
+
}
|
|
299
|
+
__name(gitText, "gitText");
|
|
300
|
+
function prepareIntegrationBranch(repoRoot, features) {
|
|
301
|
+
const current = getCurrentBranch(repoRoot);
|
|
302
|
+
const defaultBranch = getDefaultBranch(repoRoot);
|
|
303
|
+
const name = planIntegrationBranch(
|
|
304
|
+
current,
|
|
305
|
+
defaultBranch,
|
|
306
|
+
features
|
|
307
|
+
);
|
|
308
|
+
if (!name) {
|
|
309
|
+
const onIntegration = defaultBranch !== null && current === integrationBranchName(features);
|
|
310
|
+
return {
|
|
311
|
+
branch: current,
|
|
312
|
+
created: false,
|
|
313
|
+
resumed: false,
|
|
314
|
+
behind: 0,
|
|
315
|
+
fastForwarded: false,
|
|
316
|
+
launchBranch: current,
|
|
317
|
+
forkedFrom: onIntegration ? defaultBranch : null
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
if (isDirty(repoRoot)) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
"Working tree is dirty; commit or stash changes before running"
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
if (localBranchExists(repoRoot, name)) {
|
|
326
|
+
const contained = isAncestor(repoRoot, name, current);
|
|
327
|
+
gitText(repoRoot, ["checkout", "--quiet", name]);
|
|
328
|
+
let behind = Number(gitText(
|
|
329
|
+
repoRoot,
|
|
330
|
+
["rev-list", "--count", `${name}..${current}`]
|
|
331
|
+
));
|
|
332
|
+
const fastForwarded = contained && behind > 0;
|
|
333
|
+
if (fastForwarded) {
|
|
334
|
+
gitText(repoRoot, ["merge", "--ff-only", "--quiet", current]);
|
|
335
|
+
behind = 0;
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
branch: name,
|
|
339
|
+
created: false,
|
|
340
|
+
resumed: true,
|
|
341
|
+
behind,
|
|
342
|
+
fastForwarded,
|
|
343
|
+
launchBranch: current,
|
|
344
|
+
forkedFrom: defaultBranch
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
gitText(repoRoot, ["checkout", "--quiet", "-b", name]);
|
|
348
|
+
return {
|
|
349
|
+
branch: name,
|
|
350
|
+
created: true,
|
|
351
|
+
resumed: false,
|
|
352
|
+
behind: 0,
|
|
353
|
+
fastForwarded: false,
|
|
354
|
+
launchBranch: current,
|
|
355
|
+
forkedFrom: defaultBranch
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
__name(prepareIntegrationBranch, "prepareIntegrationBranch");
|
|
359
|
+
function describeIntegrationBranch(repoRoot, features) {
|
|
360
|
+
const current = getCurrentBranch(repoRoot);
|
|
361
|
+
const name = planIntegrationBranch(
|
|
362
|
+
current,
|
|
363
|
+
getDefaultBranch(repoRoot),
|
|
364
|
+
features
|
|
365
|
+
);
|
|
366
|
+
if (!name) return `Branch: merges into ${current}`;
|
|
367
|
+
return localBranchExists(repoRoot, name) ? `Branch: would resume ${name}` : `Branch: would create ${name} from ${current}`;
|
|
368
|
+
}
|
|
369
|
+
__name(describeIntegrationBranch, "describeIntegrationBranch");
|
|
245
370
|
async function branchExists(repoRoot, branchName) {
|
|
246
371
|
try {
|
|
247
372
|
await execFileAsync(
|
|
@@ -261,16 +386,20 @@ function buildBranchName(repoName, ticket) {
|
|
|
261
386
|
return `${repoName}--${prefix}${num}-${ticket.slug}`;
|
|
262
387
|
}
|
|
263
388
|
__name(buildBranchName, "buildBranchName");
|
|
264
|
-
async function
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
);
|
|
389
|
+
async function removeWorktreeNow(worktreePath, opts) {
|
|
390
|
+
const args = [
|
|
391
|
+
"-C",
|
|
392
|
+
worktreePath,
|
|
393
|
+
"worktree",
|
|
394
|
+
"remove"
|
|
395
|
+
];
|
|
396
|
+
if (opts?.force) args.push("--force");
|
|
397
|
+
args.push(worktreePath);
|
|
398
|
+
await execFileAsync("git", args);
|
|
399
|
+
}
|
|
400
|
+
__name(removeWorktreeNow, "removeWorktreeNow");
|
|
401
|
+
async function addWorktreeNow(opts) {
|
|
402
|
+
const { repoRoot, baseBranch, branchName, worktreePath } = opts;
|
|
274
403
|
if (existsSync2(worktreePath)) {
|
|
275
404
|
try {
|
|
276
405
|
await execFileAsync(
|
|
@@ -310,36 +439,56 @@ async function createWorktreeNow(opts) {
|
|
|
310
439
|
);
|
|
311
440
|
return { worktreePath, branchName };
|
|
312
441
|
}
|
|
313
|
-
__name(
|
|
442
|
+
__name(addWorktreeNow, "addWorktreeNow");
|
|
314
443
|
var worktreeQueue = Promise.resolve();
|
|
315
|
-
function
|
|
316
|
-
const result = worktreeQueue.then(
|
|
444
|
+
function enqueue(fn) {
|
|
445
|
+
const result = worktreeQueue.then(fn);
|
|
317
446
|
worktreeQueue = result.catch(() => {
|
|
318
447
|
});
|
|
319
448
|
return result;
|
|
320
449
|
}
|
|
450
|
+
__name(enqueue, "enqueue");
|
|
451
|
+
function createWorktree(opts) {
|
|
452
|
+
const { repoRoot, repoName, ticket } = opts;
|
|
453
|
+
const num = String(ticket.num).padStart(2, "0");
|
|
454
|
+
const prefix = ticket.feature ? `${ticket.feature}-` : "";
|
|
455
|
+
return enqueue(() => addWorktreeNow({
|
|
456
|
+
repoRoot,
|
|
457
|
+
baseBranch: opts.baseBranch,
|
|
458
|
+
branchName: buildBranchName(repoName, ticket),
|
|
459
|
+
worktreePath: join2(
|
|
460
|
+
repoRoot,
|
|
461
|
+
"..",
|
|
462
|
+
`${repoName}.${prefix}${num}-${ticket.slug}`
|
|
463
|
+
)
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
321
466
|
__name(createWorktree, "createWorktree");
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
args.push(worktreePath);
|
|
331
|
-
await execFileAsync("git", args);
|
|
467
|
+
function createNamedWorktree(opts) {
|
|
468
|
+
const { repoRoot, repoName, name } = opts;
|
|
469
|
+
return enqueue(() => addWorktreeNow({
|
|
470
|
+
repoRoot,
|
|
471
|
+
baseBranch: opts.baseBranch,
|
|
472
|
+
branchName: `${repoName}--${name}`,
|
|
473
|
+
worktreePath: join2(repoRoot, "..", `${repoName}.${name}`)
|
|
474
|
+
}));
|
|
332
475
|
}
|
|
333
|
-
__name(
|
|
476
|
+
__name(createNamedWorktree, "createNamedWorktree");
|
|
334
477
|
function removeWorktree(worktreePath, opts) {
|
|
335
|
-
|
|
336
|
-
() => removeWorktreeNow(worktreePath, opts)
|
|
337
|
-
);
|
|
338
|
-
worktreeQueue = result.catch(() => {
|
|
339
|
-
});
|
|
340
|
-
return result;
|
|
478
|
+
return enqueue(() => removeWorktreeNow(worktreePath, opts));
|
|
341
479
|
}
|
|
342
480
|
__name(removeWorktree, "removeWorktree");
|
|
481
|
+
function deleteBranch(repoRoot, name) {
|
|
482
|
+
return enqueue(async () => {
|
|
483
|
+
if (!await branchExists(repoRoot, name)) return;
|
|
484
|
+
await execFileAsync(
|
|
485
|
+
"git",
|
|
486
|
+
["branch", "-D", name],
|
|
487
|
+
{ cwd: repoRoot }
|
|
488
|
+
);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
__name(deleteBranch, "deleteBranch");
|
|
343
492
|
function hasNewCommits(repoRoot, base, branch) {
|
|
344
493
|
const stdout = execFileSync(
|
|
345
494
|
"git",
|
|
@@ -349,6 +498,51 @@ function hasNewCommits(repoRoot, base, branch) {
|
|
|
349
498
|
return Number(stdout.trim()) > 0;
|
|
350
499
|
}
|
|
351
500
|
__name(hasNewCommits, "hasNewCommits");
|
|
501
|
+
function headCommit(cwd) {
|
|
502
|
+
return gitText(cwd, ["rev-parse", "HEAD"]);
|
|
503
|
+
}
|
|
504
|
+
__name(headCommit, "headCommit");
|
|
505
|
+
function mergeBase(cwd, a, b) {
|
|
506
|
+
try {
|
|
507
|
+
return gitText(cwd, ["merge-base", a, b]) || null;
|
|
508
|
+
} catch {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
__name(mergeBase, "mergeBase");
|
|
513
|
+
function isAncestor(cwd, ancestor, of) {
|
|
514
|
+
try {
|
|
515
|
+
execFileSync(
|
|
516
|
+
"git",
|
|
517
|
+
["merge-base", "--is-ancestor", ancestor, of],
|
|
518
|
+
{ cwd, stdio: "ignore" }
|
|
519
|
+
);
|
|
520
|
+
return true;
|
|
521
|
+
} catch {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
__name(isAncestor, "isAncestor");
|
|
526
|
+
function hasChangesOutside(cwd, from, to, excludeDir) {
|
|
527
|
+
try {
|
|
528
|
+
execFileSync(
|
|
529
|
+
"git",
|
|
530
|
+
[
|
|
531
|
+
"diff",
|
|
532
|
+
"--quiet",
|
|
533
|
+
`${from}..${to}`,
|
|
534
|
+
"--",
|
|
535
|
+
".",
|
|
536
|
+
`:(exclude)${excludeDir}`
|
|
537
|
+
],
|
|
538
|
+
{ cwd, stdio: "ignore" }
|
|
539
|
+
);
|
|
540
|
+
return false;
|
|
541
|
+
} catch {
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
__name(hasChangesOutside, "hasChangesOutside");
|
|
352
546
|
function isDirty(cwd) {
|
|
353
547
|
const stdout = execFileSync(
|
|
354
548
|
"git",
|
|
@@ -555,16 +749,17 @@ function writeProgress(progressPath, data) {
|
|
|
555
749
|
);
|
|
556
750
|
}
|
|
557
751
|
__name(writeProgress, "writeProgress");
|
|
558
|
-
async function commitProgress(repoRoot,
|
|
752
|
+
async function commitProgress(repoRoot, paths, message) {
|
|
753
|
+
const list = typeof paths === "string" ? [paths] : paths;
|
|
559
754
|
await execFileAsync2(
|
|
560
755
|
"git",
|
|
561
|
-
["add", "--force",
|
|
756
|
+
["add", "--force", "--", ...list],
|
|
562
757
|
{ cwd: repoRoot }
|
|
563
758
|
);
|
|
564
759
|
try {
|
|
565
760
|
await execFileAsync2(
|
|
566
761
|
"git",
|
|
567
|
-
["diff", "--cached", "--quiet", "--",
|
|
762
|
+
["diff", "--cached", "--quiet", "--", ...list],
|
|
568
763
|
{ cwd: repoRoot }
|
|
569
764
|
);
|
|
570
765
|
return false;
|
|
@@ -572,7 +767,7 @@ async function commitProgress(repoRoot, progressPath, message) {
|
|
|
572
767
|
}
|
|
573
768
|
await execFileAsync2(
|
|
574
769
|
"git",
|
|
575
|
-
["commit", "-m", message, "--",
|
|
770
|
+
["commit", "-m", message, "--", ...list],
|
|
576
771
|
{ cwd: repoRoot }
|
|
577
772
|
);
|
|
578
773
|
return true;
|
|
@@ -778,6 +973,106 @@ async function mergeWave(opts) {
|
|
|
778
973
|
}
|
|
779
974
|
__name(mergeWave, "mergeWave");
|
|
780
975
|
|
|
976
|
+
// src/stats.ts
|
|
977
|
+
var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
|
|
978
|
+
function isRecord(value) {
|
|
979
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
980
|
+
}
|
|
981
|
+
__name(isRecord, "isRecord");
|
|
982
|
+
function emptyStreamStats() {
|
|
983
|
+
return { stats: { subagents: 0 }, seenToolIds: /* @__PURE__ */ new Set() };
|
|
984
|
+
}
|
|
985
|
+
__name(emptyStreamStats, "emptyStreamStats");
|
|
986
|
+
function nextStreamStats(state, obj) {
|
|
987
|
+
if (!isRecord(obj)) return state;
|
|
988
|
+
if (obj.type === "assistant") return countSubagents(state, obj);
|
|
989
|
+
if (obj.type === "result") return applyResult(state, obj);
|
|
990
|
+
return state;
|
|
991
|
+
}
|
|
992
|
+
__name(nextStreamStats, "nextStreamStats");
|
|
993
|
+
function countSubagents(state, obj) {
|
|
994
|
+
if (obj.parent_tool_use_id != null) return state;
|
|
995
|
+
const message = obj.message;
|
|
996
|
+
if (!isRecord(message) || !Array.isArray(message.content)) {
|
|
997
|
+
return state;
|
|
998
|
+
}
|
|
999
|
+
const ids = [];
|
|
1000
|
+
for (const block of message.content) {
|
|
1001
|
+
if (!isRecord(block) || block.type !== "tool_use") continue;
|
|
1002
|
+
if (typeof block.name !== "string" || !SUBAGENT_TOOLS.has(block.name)) continue;
|
|
1003
|
+
if (typeof block.id !== "string") continue;
|
|
1004
|
+
if (state.seenToolIds.has(block.id)) continue;
|
|
1005
|
+
if (ids.includes(block.id)) continue;
|
|
1006
|
+
ids.push(block.id);
|
|
1007
|
+
}
|
|
1008
|
+
if (ids.length === 0) return state;
|
|
1009
|
+
return {
|
|
1010
|
+
stats: {
|
|
1011
|
+
...state.stats,
|
|
1012
|
+
subagents: state.stats.subagents + ids.length
|
|
1013
|
+
},
|
|
1014
|
+
seenToolIds: /* @__PURE__ */ new Set([...state.seenToolIds, ...ids])
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
__name(countSubagents, "countSubagents");
|
|
1018
|
+
function applyResult(state, obj) {
|
|
1019
|
+
const cost = obj.total_cost_usd;
|
|
1020
|
+
const hasCost = typeof cost === "number" && Number.isFinite(cost);
|
|
1021
|
+
const subagentStats = obj.subagent_stats;
|
|
1022
|
+
const spawned = isRecord(subagentStats) ? subagentStats.spawned : void 0;
|
|
1023
|
+
const hasSpawned = typeof spawned === "number" && Number.isInteger(spawned) && spawned >= 0;
|
|
1024
|
+
if (!hasCost && !hasSpawned) return state;
|
|
1025
|
+
const subagents = hasSpawned ? spawned : state.stats.subagents;
|
|
1026
|
+
const costUsd = hasCost ? cost : state.stats.costUsd;
|
|
1027
|
+
if (subagents === state.stats.subagents && costUsd === state.stats.costUsd) return state;
|
|
1028
|
+
return {
|
|
1029
|
+
stats: costUsd === void 0 ? { subagents } : { subagents, costUsd },
|
|
1030
|
+
seenToolIds: state.seenToolIds
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
__name(applyResult, "applyResult");
|
|
1034
|
+
function addStats(total, run) {
|
|
1035
|
+
const subagents = total.subagents + (run.subagents ?? 0);
|
|
1036
|
+
if (total.costUsd === void 0 && run.costUsd === void 0) {
|
|
1037
|
+
return { subagents };
|
|
1038
|
+
}
|
|
1039
|
+
return {
|
|
1040
|
+
subagents,
|
|
1041
|
+
costUsd: (total.costUsd ?? 0) + (run.costUsd ?? 0)
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
__name(addStats, "addStats");
|
|
1045
|
+
function formatStats(stats) {
|
|
1046
|
+
if (!stats) return null;
|
|
1047
|
+
if (stats.subagents === 0 && stats.costUsd === void 0) {
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
const count = `${stats.subagents} ` + (stats.subagents === 1 ? "subagent" : "subagents");
|
|
1051
|
+
return stats.costUsd === void 0 ? count : `${count}, $${stats.costUsd.toFixed(2)}`;
|
|
1052
|
+
}
|
|
1053
|
+
__name(formatStats, "formatStats");
|
|
1054
|
+
function emptyRunStats() {
|
|
1055
|
+
return { agentRuns: 0, withCost: 0, subagents: 0, costUsd: 0 };
|
|
1056
|
+
}
|
|
1057
|
+
__name(emptyRunStats, "emptyRunStats");
|
|
1058
|
+
function addRun(run, result) {
|
|
1059
|
+
return {
|
|
1060
|
+
agentRuns: run.agentRuns + 1,
|
|
1061
|
+
withCost: run.withCost + (result.costUsd === void 0 ? 0 : 1),
|
|
1062
|
+
subagents: run.subagents + (result.subagents ?? 0),
|
|
1063
|
+
costUsd: run.costUsd + (result.costUsd ?? 0)
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
__name(addRun, "addRun");
|
|
1067
|
+
function renderRunStats(run) {
|
|
1068
|
+
if (run.agentRuns === 0) return null;
|
|
1069
|
+
if (run.withCost === 0 && run.subagents === 0) return null;
|
|
1070
|
+
const runs = `${run.agentRuns} agent ` + (run.agentRuns === 1 ? "run" : "runs");
|
|
1071
|
+
const partial = run.withCost < run.agentRuns ? ` (cost from ${run.withCost} of ${run.agentRuns} runs)` : "";
|
|
1072
|
+
return `Subagents: ${run.subagents} started across ${runs}, cost $${run.costUsd.toFixed(2)}${partial}`;
|
|
1073
|
+
}
|
|
1074
|
+
__name(renderRunStats, "renderRunStats");
|
|
1075
|
+
|
|
781
1076
|
// src/display.ts
|
|
782
1077
|
var ESC = "\x1B[";
|
|
783
1078
|
var ansi = {
|
|
@@ -839,10 +1134,9 @@ var MAX_MESSAGE_BUFFER = 200;
|
|
|
839
1134
|
var ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
|
|
840
1135
|
function rowText(ticket) {
|
|
841
1136
|
const label = ticketLabel(ticket);
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
}
|
|
845
|
-
return `${label} (blocked by ${ticket.blockedBy.join(", ")})`;
|
|
1137
|
+
const base = ticket.status === "blocked" && ticket.blockedBy?.length ? `${label} (blocked by ${ticket.blockedBy.join(", ")})` : label;
|
|
1138
|
+
const suffix = formatStats(ticket.stats);
|
|
1139
|
+
return suffix === null ? base : `${base} - ${suffix}`;
|
|
846
1140
|
}
|
|
847
1141
|
__name(rowText, "rowText");
|
|
848
1142
|
function fitLine(line, width) {
|
|
@@ -971,6 +1265,12 @@ var StatusDisplay = class {
|
|
|
971
1265
|
ticket.blockedBy = [...blockedBy];
|
|
972
1266
|
this.render();
|
|
973
1267
|
}
|
|
1268
|
+
setTicketStats(num, feature, stats) {
|
|
1269
|
+
const ticket = this.findTicket(num, feature);
|
|
1270
|
+
if (!ticket) return;
|
|
1271
|
+
ticket.stats = { ...stats };
|
|
1272
|
+
this.render();
|
|
1273
|
+
}
|
|
974
1274
|
log(message) {
|
|
975
1275
|
if (!this.wave) {
|
|
976
1276
|
this.config.stream.write(message + "\n");
|
|
@@ -1105,95 +1405,1329 @@ var PlainDisplay = class {
|
|
|
1105
1405
|
Wave ${waveNum}${pct}: ` + tickets.map((t) => ticketLabel(t)).join(", ")
|
|
1106
1406
|
);
|
|
1107
1407
|
}
|
|
1108
|
-
setTicketStatus(_num, _status, _feature) {
|
|
1109
|
-
}
|
|
1110
|
-
setBlocked(_num, _feature, _blockedBy) {
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1408
|
+
setTicketStatus(_num, _status, _feature) {
|
|
1409
|
+
}
|
|
1410
|
+
setBlocked(_num, _feature, _blockedBy) {
|
|
1411
|
+
}
|
|
1412
|
+
setTicketStats(_num, _feature, _stats) {
|
|
1413
|
+
}
|
|
1414
|
+
log(message) {
|
|
1415
|
+
console.log(message);
|
|
1416
|
+
}
|
|
1417
|
+
cleanup() {
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
function createDisplay(stream) {
|
|
1421
|
+
const s = stream ?? process.stdout;
|
|
1422
|
+
if (s.isTTY) {
|
|
1423
|
+
return new StatusDisplay({ stream: s });
|
|
1424
|
+
}
|
|
1425
|
+
return new PlainDisplay();
|
|
1426
|
+
}
|
|
1427
|
+
__name(createDisplay, "createDisplay");
|
|
1428
|
+
|
|
1429
|
+
// src/testcmd.ts
|
|
1430
|
+
import { spawn } from "node:child_process";
|
|
1431
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
1432
|
+
import { join as join3 } from "node:path";
|
|
1433
|
+
function detectTestCmd(projectDir) {
|
|
1434
|
+
const pkgPath = join3(projectDir, "package.json");
|
|
1435
|
+
if (!existsSync3(pkgPath)) return null;
|
|
1436
|
+
try {
|
|
1437
|
+
const pkg = JSON.parse(
|
|
1438
|
+
readFileSync3(pkgPath, "utf8")
|
|
1439
|
+
);
|
|
1440
|
+
return pkg.scripts?.test ? "npm test" : null;
|
|
1441
|
+
} catch {
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
__name(detectTestCmd, "detectTestCmd");
|
|
1446
|
+
function detectInstallCmd(projectDir) {
|
|
1447
|
+
const has = /* @__PURE__ */ __name((name) => existsSync3(join3(projectDir, name)), "has");
|
|
1448
|
+
if (!has("package.json")) return null;
|
|
1449
|
+
if (has("pnpm-lock.yaml")) {
|
|
1450
|
+
return "pnpm install --prefer-offline --frozen-lockfile";
|
|
1451
|
+
}
|
|
1452
|
+
if (has("package-lock.json")) {
|
|
1453
|
+
return "npm ci --prefer-offline --no-audit --no-fund";
|
|
1454
|
+
}
|
|
1455
|
+
return "npm install --prefer-offline --no-audit --no-fund --no-package-lock";
|
|
1456
|
+
}
|
|
1457
|
+
__name(detectInstallCmd, "detectInstallCmd");
|
|
1458
|
+
function runTestCmd(opts) {
|
|
1459
|
+
return new Promise((resolve3) => {
|
|
1460
|
+
let settled = false;
|
|
1461
|
+
const child = spawn("sh", ["-c", opts.cmd], {
|
|
1462
|
+
cwd: opts.cwd,
|
|
1463
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1464
|
+
detached: true
|
|
1465
|
+
});
|
|
1466
|
+
if (child.pid !== void 0 && opts.registry) {
|
|
1467
|
+
opts.registry.add(child.pid);
|
|
1468
|
+
}
|
|
1469
|
+
const chunks = [];
|
|
1470
|
+
child.stdout.on("data", (d) => chunks.push(String(d)));
|
|
1471
|
+
child.stderr.on("data", (d) => chunks.push(String(d)));
|
|
1472
|
+
const timer = setTimeout(() => {
|
|
1473
|
+
if (child.pid) {
|
|
1474
|
+
try {
|
|
1475
|
+
process.kill(-child.pid, "SIGKILL");
|
|
1476
|
+
} catch {
|
|
1477
|
+
child.kill("SIGKILL");
|
|
1478
|
+
}
|
|
1479
|
+
} else {
|
|
1480
|
+
child.kill("SIGKILL");
|
|
1481
|
+
}
|
|
1482
|
+
}, opts.timeout * 1e3);
|
|
1483
|
+
child.on("error", (err) => {
|
|
1484
|
+
if (settled) return;
|
|
1485
|
+
settled = true;
|
|
1486
|
+
clearTimeout(timer);
|
|
1487
|
+
if (child.pid !== void 0 && opts.registry) {
|
|
1488
|
+
opts.registry.delete(child.pid);
|
|
1489
|
+
}
|
|
1490
|
+
chunks.push(String(err));
|
|
1491
|
+
resolve3({
|
|
1492
|
+
exitCode: 1,
|
|
1493
|
+
output: chunks.join("")
|
|
1494
|
+
});
|
|
1495
|
+
});
|
|
1496
|
+
child.on("close", (code) => {
|
|
1497
|
+
if (settled) return;
|
|
1498
|
+
settled = true;
|
|
1499
|
+
clearTimeout(timer);
|
|
1500
|
+
if (child.pid !== void 0 && opts.registry) {
|
|
1501
|
+
opts.registry.delete(child.pid);
|
|
1502
|
+
}
|
|
1503
|
+
resolve3({
|
|
1504
|
+
exitCode: code ?? 1,
|
|
1505
|
+
output: chunks.join("")
|
|
1506
|
+
});
|
|
1507
|
+
});
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
__name(runTestCmd, "runTestCmd");
|
|
1511
|
+
|
|
1512
|
+
// src/learnings.ts
|
|
1513
|
+
import {
|
|
1514
|
+
appendFileSync,
|
|
1515
|
+
existsSync as existsSync4,
|
|
1516
|
+
mkdirSync as mkdirSync2,
|
|
1517
|
+
readFileSync as readFileSync4,
|
|
1518
|
+
rmSync,
|
|
1519
|
+
writeFileSync as writeFileSync3
|
|
1520
|
+
} from "node:fs";
|
|
1521
|
+
import { basename as basename2, dirname as dirname2, join as join4 } from "node:path";
|
|
1522
|
+
var MAX_OWN_ENTRIES = 4;
|
|
1523
|
+
var MAX_ENTRY_CHARS = 4e3;
|
|
1524
|
+
var LOG_TAIL_LINES = 40;
|
|
1525
|
+
var NOTES_PATH = ".nightralph/notes.md";
|
|
1526
|
+
var SENTINEL_PREFIX = "<!-- nightralph:entry";
|
|
1527
|
+
var TRUNCATED_MARKER = "[truncated]";
|
|
1528
|
+
var LEARNING_OUTCOMES = [
|
|
1529
|
+
"done",
|
|
1530
|
+
"exit",
|
|
1531
|
+
"tests",
|
|
1532
|
+
"install",
|
|
1533
|
+
"no-commits",
|
|
1534
|
+
"error",
|
|
1535
|
+
"merge"
|
|
1536
|
+
];
|
|
1537
|
+
function encodeKey(key) {
|
|
1538
|
+
return key.replace(/%/g, "%25").replace(/\s/g, (c) => encodeURIComponent(c));
|
|
1539
|
+
}
|
|
1540
|
+
__name(encodeKey, "encodeKey");
|
|
1541
|
+
function fence(content) {
|
|
1542
|
+
const longest = Math.max(
|
|
1543
|
+
0,
|
|
1544
|
+
...[...content.matchAll(/`+/g)].map((m) => m[0].length)
|
|
1545
|
+
);
|
|
1546
|
+
const ticks = "`".repeat(Math.max(3, longest + 1));
|
|
1547
|
+
return `${ticks}
|
|
1548
|
+
${content}
|
|
1549
|
+
${ticks}`;
|
|
1550
|
+
}
|
|
1551
|
+
__name(fence, "fence");
|
|
1552
|
+
function sanitize(text) {
|
|
1553
|
+
if (!text) return "";
|
|
1554
|
+
return text.replace(
|
|
1555
|
+
new RegExp(SENTINEL_PREFIX, "g"),
|
|
1556
|
+
"<!-- nightralph-entry"
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
__name(sanitize, "sanitize");
|
|
1560
|
+
function renderAgentSummary(run, effort) {
|
|
1561
|
+
const effortLine = effort ? `Effort: ${effort}.` : "";
|
|
1562
|
+
const exitLine = `Agent exit: ${run.exitCode}.`;
|
|
1563
|
+
const subagentsLine = run.subagents === void 0 ? "" : `Subagents: ${run.subagents}.`;
|
|
1564
|
+
const costLine = run.costUsd === void 0 ? "" : `Cost: $${run.costUsd.toFixed(2)}.`;
|
|
1565
|
+
const itemsText = run.completedItems.length > 0 ? run.completedItems.map(
|
|
1566
|
+
(item) => `"${sanitize(item)}"`
|
|
1567
|
+
).join(", ") + "." : "none.";
|
|
1568
|
+
const itemsLine = `Checked items: ${itemsText}`;
|
|
1569
|
+
return [
|
|
1570
|
+
effortLine,
|
|
1571
|
+
exitLine,
|
|
1572
|
+
subagentsLine,
|
|
1573
|
+
costLine,
|
|
1574
|
+
itemsLine
|
|
1575
|
+
].filter(Boolean).join(" ");
|
|
1576
|
+
}
|
|
1577
|
+
__name(renderAgentSummary, "renderAgentSummary");
|
|
1578
|
+
function renderNotesSection(notes) {
|
|
1579
|
+
const notesSanitized = sanitize(notes);
|
|
1580
|
+
return notesSanitized.trim() ? `### Agent notes
|
|
1581
|
+
${notesSanitized}` : "The agent left no notes.";
|
|
1582
|
+
}
|
|
1583
|
+
__name(renderNotesSection, "renderNotesSection");
|
|
1584
|
+
function formatDate(date) {
|
|
1585
|
+
const year = date.getFullYear();
|
|
1586
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
1587
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
1588
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
1589
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
1590
|
+
return `${year}-${month}-${day} ${hours}:${minutes}`;
|
|
1591
|
+
}
|
|
1592
|
+
__name(formatDate, "formatDate");
|
|
1593
|
+
function isLearningOutcome(value) {
|
|
1594
|
+
return LEARNING_OUTCOMES.some((o) => o === value);
|
|
1595
|
+
}
|
|
1596
|
+
__name(isLearningOutcome, "isLearningOutcome");
|
|
1597
|
+
function renderEntry(input) {
|
|
1598
|
+
const encodedKey = encodeKey(input.key);
|
|
1599
|
+
const hasRun = "run" in input;
|
|
1600
|
+
const timedOut = hasRun && input.run.timedOut;
|
|
1601
|
+
const timedOutStr = timedOut ? " timedout=true" : "";
|
|
1602
|
+
const sentinel = `<!-- nightralph:entry ticket=${encodedKey} attempt=${input.attempt} outcome=${input.outcome}${timedOutStr} -->`;
|
|
1603
|
+
const timestamp = formatDate(input.now);
|
|
1604
|
+
const attemptStr = input.outcome === "merge" ? "" : ` attempt ${input.attempt}`;
|
|
1605
|
+
let description;
|
|
1606
|
+
switch (input.outcome) {
|
|
1607
|
+
case "done":
|
|
1608
|
+
description = "done";
|
|
1609
|
+
break;
|
|
1610
|
+
case "exit":
|
|
1611
|
+
description = `failed (exit ${input.run.exitCode})`;
|
|
1612
|
+
break;
|
|
1613
|
+
case "tests":
|
|
1614
|
+
description = "failed (tests)";
|
|
1615
|
+
break;
|
|
1616
|
+
case "install":
|
|
1617
|
+
description = "failed (install)";
|
|
1618
|
+
break;
|
|
1619
|
+
case "no-commits":
|
|
1620
|
+
description = "no commits";
|
|
1621
|
+
break;
|
|
1622
|
+
case "error":
|
|
1623
|
+
description = "error";
|
|
1624
|
+
break;
|
|
1625
|
+
case "merge":
|
|
1626
|
+
description = "merge failed";
|
|
1627
|
+
break;
|
|
1628
|
+
default: {
|
|
1629
|
+
const _exhaustive = input;
|
|
1630
|
+
description = _exhaustive;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
const timedOutPrefix = timedOut ? "timed out, then " : "";
|
|
1634
|
+
const heading = `## ${timestamp} -- ${input.label}${attemptStr} -- ${timedOutPrefix}${description}`;
|
|
1635
|
+
let body;
|
|
1636
|
+
switch (input.outcome) {
|
|
1637
|
+
case "done":
|
|
1638
|
+
case "exit":
|
|
1639
|
+
case "no-commits": {
|
|
1640
|
+
const run = input.run;
|
|
1641
|
+
const summary = renderAgentSummary(run, input.effort);
|
|
1642
|
+
const notesSection = renderNotesSection(run.notes);
|
|
1643
|
+
body = summary + "\n\n" + notesSection;
|
|
1644
|
+
break;
|
|
1645
|
+
}
|
|
1646
|
+
case "tests": {
|
|
1647
|
+
const logTail = sanitize(input.logTail);
|
|
1648
|
+
const fenced = fence(logTail);
|
|
1649
|
+
const summary = renderAgentSummary(
|
|
1650
|
+
input.run,
|
|
1651
|
+
input.effort
|
|
1652
|
+
);
|
|
1653
|
+
const notesSection = renderNotesSection(input.run.notes);
|
|
1654
|
+
const lastNLines = `Last ${LOG_TAIL_LINES} lines of \`${sanitize(input.logName)}\`:`;
|
|
1655
|
+
body = summary + `
|
|
1656
|
+
|
|
1657
|
+
Test gate failed.
|
|
1658
|
+
${lastNLines}
|
|
1659
|
+
${fenced}
|
|
1660
|
+
|
|
1661
|
+
` + notesSection;
|
|
1662
|
+
break;
|
|
1663
|
+
}
|
|
1664
|
+
case "install": {
|
|
1665
|
+
const logTail = sanitize(input.logTail);
|
|
1666
|
+
const fenced = fence(logTail);
|
|
1667
|
+
const lastNLines = `Last ${LOG_TAIL_LINES} lines of \`${sanitize(input.logName)}\`:`;
|
|
1668
|
+
body = `Install failed.
|
|
1669
|
+
${lastNLines}
|
|
1670
|
+
` + fenced;
|
|
1671
|
+
break;
|
|
1672
|
+
}
|
|
1673
|
+
case "error": {
|
|
1674
|
+
const errorText = sanitize(input.error);
|
|
1675
|
+
const fenced = fence(errorText);
|
|
1676
|
+
body = fenced;
|
|
1677
|
+
break;
|
|
1678
|
+
}
|
|
1679
|
+
case "merge": {
|
|
1680
|
+
const stderrText = sanitize(input.stderr);
|
|
1681
|
+
const fenced = fence(stderrText);
|
|
1682
|
+
body = `Merge failed.
|
|
1683
|
+
${fenced}`;
|
|
1684
|
+
break;
|
|
1685
|
+
}
|
|
1686
|
+
default: {
|
|
1687
|
+
const unknown = input;
|
|
1688
|
+
throw new Error(`Unknown outcome: ${JSON.stringify(unknown)}`);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
return `${sentinel}
|
|
1692
|
+
${heading}
|
|
1693
|
+
|
|
1694
|
+
${body}
|
|
1695
|
+
|
|
1696
|
+
---
|
|
1697
|
+
`;
|
|
1698
|
+
}
|
|
1699
|
+
__name(renderEntry, "renderEntry");
|
|
1700
|
+
function parseEntries(text) {
|
|
1701
|
+
const lines = text.split("\n").map((line) => {
|
|
1702
|
+
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
1703
|
+
});
|
|
1704
|
+
const entries = [];
|
|
1705
|
+
let currentSentinel = null;
|
|
1706
|
+
let currentBody = [];
|
|
1707
|
+
const sentinelRegex = /^<!-- nightralph:entry (.*) -->$/;
|
|
1708
|
+
for (const line of lines) {
|
|
1709
|
+
const match = line.match(sentinelRegex);
|
|
1710
|
+
if (match) {
|
|
1711
|
+
if (currentSentinel !== null) {
|
|
1712
|
+
const entry = parseSentinel(currentSentinel, currentBody);
|
|
1713
|
+
if (entry) {
|
|
1714
|
+
entries.push(entry);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
currentSentinel = match[1];
|
|
1718
|
+
currentBody = [];
|
|
1719
|
+
} else if (currentSentinel !== null) {
|
|
1720
|
+
currentBody.push(line);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
if (currentSentinel !== null) {
|
|
1724
|
+
const entry = parseSentinel(currentSentinel, currentBody);
|
|
1725
|
+
if (entry) {
|
|
1726
|
+
entries.push(entry);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
return entries;
|
|
1730
|
+
}
|
|
1731
|
+
__name(parseEntries, "parseEntries");
|
|
1732
|
+
function parseSentinel(sentinel, bodyLines) {
|
|
1733
|
+
const parts = sentinel.split(/\s+/).filter(Boolean);
|
|
1734
|
+
const attrs = {};
|
|
1735
|
+
for (const part of parts) {
|
|
1736
|
+
const idx = part.indexOf("=");
|
|
1737
|
+
if (idx > 0) {
|
|
1738
|
+
const key2 = part.slice(0, idx);
|
|
1739
|
+
const value = part.slice(idx + 1);
|
|
1740
|
+
if (value) {
|
|
1741
|
+
attrs[key2] = value;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
let key = attrs.ticket;
|
|
1746
|
+
if (!key) return null;
|
|
1747
|
+
try {
|
|
1748
|
+
key = decodeURIComponent(key);
|
|
1749
|
+
} catch (_e) {
|
|
1750
|
+
return null;
|
|
1751
|
+
}
|
|
1752
|
+
if (!key.trim()) return null;
|
|
1753
|
+
const attemptStr = attrs.attempt;
|
|
1754
|
+
if (!attemptStr) return null;
|
|
1755
|
+
if (!/^\d+$/.test(attemptStr)) return null;
|
|
1756
|
+
const attempt = Number(attemptStr);
|
|
1757
|
+
const outcome = attrs.outcome;
|
|
1758
|
+
if (!outcome || !isLearningOutcome(outcome)) return null;
|
|
1759
|
+
const timedOut = attrs.timedout === "true";
|
|
1760
|
+
let body = bodyLines.join("\n").trim();
|
|
1761
|
+
const lines = body.split("\n");
|
|
1762
|
+
if (lines.length > 0 && lines[lines.length - 1] === "---") {
|
|
1763
|
+
body = lines.slice(0, -1).join("\n").trim();
|
|
1764
|
+
}
|
|
1765
|
+
return {
|
|
1766
|
+
key,
|
|
1767
|
+
attempt,
|
|
1768
|
+
outcome,
|
|
1769
|
+
timedOut,
|
|
1770
|
+
body
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
__name(parseSentinel, "parseSentinel");
|
|
1774
|
+
function selectHistory(entries, opts) {
|
|
1775
|
+
const own = entries.filter((e) => e.key === opts.key).slice(-MAX_OWN_ENTRIES);
|
|
1776
|
+
const blockers = opts.blockerKeys.flatMap((k) => {
|
|
1777
|
+
const done = entries.filter((e) => {
|
|
1778
|
+
return e.key === k && e.outcome === "done";
|
|
1779
|
+
});
|
|
1780
|
+
const last = done[done.length - 1];
|
|
1781
|
+
return last ? [last] : [];
|
|
1782
|
+
});
|
|
1783
|
+
return { own, blockers };
|
|
1784
|
+
}
|
|
1785
|
+
__name(selectHistory, "selectHistory");
|
|
1786
|
+
function renderHistory(selection) {
|
|
1787
|
+
if (selection.own.length === 0 && selection.blockers.length === 0) {
|
|
1788
|
+
return "";
|
|
1789
|
+
}
|
|
1790
|
+
const framing = "These are records of earlier runs. The ticket or spec may have changed since then; where they disagree, follow the ticket and spec. Do not repeat an approach these notes say failed unless you can see why it failed.";
|
|
1791
|
+
const parts = [framing, ""];
|
|
1792
|
+
if (selection.own.length > 0) {
|
|
1793
|
+
parts.push("---- PREVIOUS ATTEMPTS AT THIS TICKET ----");
|
|
1794
|
+
parts.push(selection.own.map((e) => capEntry(e)).join("\n\n"));
|
|
1795
|
+
parts.push("");
|
|
1796
|
+
}
|
|
1797
|
+
if (selection.blockers.length > 0) {
|
|
1798
|
+
parts.push("---- WHAT THE BLOCKING TICKETS DID ----");
|
|
1799
|
+
parts.push(selection.blockers.map((e) => capEntry(e)).join("\n\n"));
|
|
1800
|
+
}
|
|
1801
|
+
return parts.join("\n");
|
|
1802
|
+
}
|
|
1803
|
+
__name(renderHistory, "renderHistory");
|
|
1804
|
+
function capEntry(entry) {
|
|
1805
|
+
if (entry.body.length <= MAX_ENTRY_CHARS) {
|
|
1806
|
+
return entry.body;
|
|
1807
|
+
}
|
|
1808
|
+
return entry.body.slice(0, MAX_ENTRY_CHARS) + "\n" + TRUNCATED_MARKER;
|
|
1809
|
+
}
|
|
1810
|
+
__name(capEntry, "capEntry");
|
|
1811
|
+
function tailLines(text, n) {
|
|
1812
|
+
let lines = text.split("\n");
|
|
1813
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
1814
|
+
lines = lines.slice(0, -1);
|
|
1815
|
+
}
|
|
1816
|
+
if (lines.length > n) {
|
|
1817
|
+
lines = lines.slice(-n);
|
|
1818
|
+
}
|
|
1819
|
+
return lines.join("\n");
|
|
1820
|
+
}
|
|
1821
|
+
__name(tailLines, "tailLines");
|
|
1822
|
+
function learningsPathFor(progressPath) {
|
|
1823
|
+
return join4(dirname2(progressPath), "learnings.md");
|
|
1824
|
+
}
|
|
1825
|
+
__name(learningsPathFor, "learningsPathFor");
|
|
1826
|
+
function readEntries(path) {
|
|
1827
|
+
if (!existsSync4(path)) return [];
|
|
1828
|
+
return parseEntries(readFileSync4(path, "utf8"));
|
|
1829
|
+
}
|
|
1830
|
+
__name(readEntries, "readEntries");
|
|
1831
|
+
function appendEntry(path, text) {
|
|
1832
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1833
|
+
appendFileSync(path, text);
|
|
1834
|
+
}
|
|
1835
|
+
__name(appendEntry, "appendEntry");
|
|
1836
|
+
function collectNotes(worktreePath, file = basename2(NOTES_PATH)) {
|
|
1837
|
+
const dir = join4(worktreePath, dirname2(NOTES_PATH));
|
|
1838
|
+
const path = join4(dir, file);
|
|
1839
|
+
const text = existsSync4(path) ? readFileSync4(path, "utf8").trim() : "";
|
|
1840
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1841
|
+
return text || null;
|
|
1842
|
+
}
|
|
1843
|
+
__name(collectNotes, "collectNotes");
|
|
1844
|
+
function ensureNotesIgnored(worktreePath) {
|
|
1845
|
+
const dir = join4(worktreePath, dirname2(NOTES_PATH));
|
|
1846
|
+
mkdirSync2(dir, { recursive: true });
|
|
1847
|
+
writeFileSync3(join4(dir, ".gitignore"), "*\n");
|
|
1848
|
+
}
|
|
1849
|
+
__name(ensureNotesIgnored, "ensureNotesIgnored");
|
|
1850
|
+
|
|
1851
|
+
// src/rules.ts
|
|
1852
|
+
function readDocsRule(subject) {
|
|
1853
|
+
return `- Read the files directly relevant to the ${subject} before writing code. Also read any AGENTS.md in or above the directories you will touch, plus CONTEXT.md and the relevant docs/adr/ entries if they exist. If they do not exist, proceed silently.`;
|
|
1854
|
+
}
|
|
1855
|
+
__name(readDocsRule, "readDocsRule");
|
|
1856
|
+
function installRule(installCmd) {
|
|
1857
|
+
if (!installCmd) return [];
|
|
1858
|
+
return [
|
|
1859
|
+
`- Dependencies are already installed with \`${installCmd}\`. Do NOT reinstall them with any other command; to reinstall, run that same command. To add a dependency, use the package manager that command uses, so its lockfile stays in sync.`
|
|
1860
|
+
];
|
|
1861
|
+
}
|
|
1862
|
+
__name(installRule, "installRule");
|
|
1863
|
+
function subagentRule(preferSubagent, readOnly = false) {
|
|
1864
|
+
if (!preferSubagent) return [];
|
|
1865
|
+
return [
|
|
1866
|
+
`- Hand read-only work to subagents: exploring the codebase, searching code, and experiments or test runs whose output would flood your context. Subagents default to the ${preferSubagent} model.`,
|
|
1867
|
+
"- A subagent starts with no context. Brief each one with the objective, what you already know, its boundaries, and the reply you expect (file:line references, or pass/fail with the relevant output).",
|
|
1868
|
+
...readOnly ? [] : [
|
|
1869
|
+
"- Subagents never edit files or run git commands that write. You make every edit and every commit."
|
|
1870
|
+
],
|
|
1871
|
+
"- Do not delegate a lookup that takes a single tool call."
|
|
1872
|
+
];
|
|
1873
|
+
}
|
|
1874
|
+
__name(subagentRule, "subagentRule");
|
|
1875
|
+
function testGateRule(testCmd, subject) {
|
|
1876
|
+
if (testCmd === "") {
|
|
1877
|
+
return `- Before each commit, run the tests relevant to what you changed. Do not commit with failing tests. The orchestrator runs no test command for this ${subject}.`;
|
|
1878
|
+
}
|
|
1879
|
+
return `- While iterating, run only the tests relevant to what you changed. Run \`${testCmd}\` in full once, as the final gate before your last commit. Do not commit with failing tests. The orchestrator runs the same command after you exit and rejects the ${subject} if it fails.`;
|
|
1880
|
+
}
|
|
1881
|
+
__name(testGateRule, "testGateRule");
|
|
1882
|
+
var LINT_RULE = "- If the project has a lint script, run it and fix what it reports.";
|
|
1883
|
+
function integrityRule(unit) {
|
|
1884
|
+
return `- INTEGRITY: never delete, skip, weaken, or write tautological tests to force a green result. Only report a ${unit} done when its real tests genuinely pass. If an item cannot be completed honestly, leave it unchecked and record what blocked you in your notes file.`;
|
|
1885
|
+
}
|
|
1886
|
+
__name(integrityRule, "integrityRule");
|
|
1887
|
+
var CONFINEMENT_RULES = [
|
|
1888
|
+
"- Do NOT read or modify files outside your current directory (the worktree).",
|
|
1889
|
+
"- Do NOT run git push."
|
|
1890
|
+
];
|
|
1891
|
+
function commitRule(unit) {
|
|
1892
|
+
return `- Commit after each ${unit}, and make sure everything is committed when finished, except your notes file.`;
|
|
1893
|
+
}
|
|
1894
|
+
__name(commitRule, "commitRule");
|
|
1895
|
+
function notesRule(unit, subject) {
|
|
1896
|
+
return `- Keep notes for this attempt in \`${NOTES_PATH}\` (create the directory). Update it after each ${unit} and as soon as you hit a blocker, not only at the end. Record what you implemented, files changed, how you read any vague acceptance criteria, what blocked you, and anything the next attempt at this ${subject} should know. Never commit, git add, or delete this file; the orchestrator collects it.`;
|
|
1897
|
+
}
|
|
1898
|
+
__name(notesRule, "notesRule");
|
|
1899
|
+
function agentsMdRule(subject) {
|
|
1900
|
+
return `- Before your final commit, record reusable learnings in the nearest AGENTS.md (in or above the directories you modified; prefer that over the repo root). Good additions: module conventions, gotchas, files that must change together, how to test that area, environment requirements. Do NOT add ${subject}-specific details or temporary debugging notes. Only write when you have knowledge that would help future work in that directory. Reusable patterns go in AGENTS.md; anything specific to this ${subject} goes in your notes file.`;
|
|
1901
|
+
}
|
|
1902
|
+
__name(agentsMdRule, "agentsMdRule");
|
|
1903
|
+
|
|
1904
|
+
// src/review.ts
|
|
1905
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1906
|
+
import { basename as basename4, dirname as dirname3, join as join5, relative } from "node:path";
|
|
1907
|
+
|
|
1908
|
+
// src/retry.ts
|
|
1909
|
+
import { basename as basename3 } from "node:path";
|
|
1910
|
+
var EFFORT_LADDERS = {
|
|
1911
|
+
pi: [
|
|
1912
|
+
"off",
|
|
1913
|
+
"minimal",
|
|
1914
|
+
"low",
|
|
1915
|
+
"medium",
|
|
1916
|
+
"high",
|
|
1917
|
+
"xhigh",
|
|
1918
|
+
"max"
|
|
1919
|
+
],
|
|
1920
|
+
claude: [
|
|
1921
|
+
"low",
|
|
1922
|
+
"medium",
|
|
1923
|
+
"high",
|
|
1924
|
+
"xhigh",
|
|
1925
|
+
"max"
|
|
1926
|
+
],
|
|
1927
|
+
codex: ["low", "medium", "high"]
|
|
1928
|
+
};
|
|
1929
|
+
var ALL_EFFORT_LEVELS = [
|
|
1930
|
+
"off",
|
|
1931
|
+
"minimal",
|
|
1932
|
+
"low",
|
|
1933
|
+
"medium",
|
|
1934
|
+
"high",
|
|
1935
|
+
"xhigh",
|
|
1936
|
+
"max"
|
|
1937
|
+
];
|
|
1938
|
+
function escalateEffort(provider, level) {
|
|
1939
|
+
if (!level) return "high";
|
|
1940
|
+
const ladder = EFFORT_LADDERS[provider];
|
|
1941
|
+
const i = ladder.indexOf(level);
|
|
1942
|
+
if (i === -1) return level;
|
|
1943
|
+
return ladder[Math.min(i + 1, ladder.length - 1)];
|
|
1944
|
+
}
|
|
1945
|
+
__name(escalateEffort, "escalateEffort");
|
|
1946
|
+
function retryDelayFor(base, attempt) {
|
|
1947
|
+
return base * 2 ** (attempt - 2);
|
|
1948
|
+
}
|
|
1949
|
+
__name(retryDelayFor, "retryDelayFor");
|
|
1950
|
+
function sleep(ms) {
|
|
1951
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1952
|
+
}
|
|
1953
|
+
__name(sleep, "sleep");
|
|
1954
|
+
async function runWithRetries(opts, run) {
|
|
1955
|
+
const maxAttempts = Math.max(1, opts.maxAttempts);
|
|
1956
|
+
const providerName = basename3(opts.agentCmd);
|
|
1957
|
+
const isKnown = providerName in EFFORT_LADDERS;
|
|
1958
|
+
const wait = opts.wait ?? sleep;
|
|
1959
|
+
let effort = opts.effort;
|
|
1960
|
+
for (let attempt = 1; ; attempt++) {
|
|
1961
|
+
if (attempt === 1) {
|
|
1962
|
+
opts.log(` Starting ${opts.label}`);
|
|
1963
|
+
} else {
|
|
1964
|
+
if (isKnown) {
|
|
1965
|
+
effort = escalateEffort(
|
|
1966
|
+
providerName,
|
|
1967
|
+
effort
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
const delay = retryDelayFor(opts.retryDelay, attempt);
|
|
1971
|
+
opts.log(
|
|
1972
|
+
` ${opts.label}: retrying (attempt ${attempt}/${maxAttempts}` + (isKnown && effort ? `, effort ${effort}` : "") + (delay > 0 ? `, waiting ${delay}s` : "") + ")"
|
|
1973
|
+
);
|
|
1974
|
+
if (delay > 0) await wait(delay * 1e3);
|
|
1975
|
+
}
|
|
1976
|
+
const step = await run(attempt, effort);
|
|
1977
|
+
if (step.stop || attempt >= maxAttempts) return step.result;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
__name(runWithRetries, "runWithRetries");
|
|
1981
|
+
|
|
1982
|
+
// src/review.ts
|
|
1983
|
+
var FINDINGS_FILE = "findings.md";
|
|
1984
|
+
var FINDINGS_PATH = `${dirname3(NOTES_PATH)}/${FINDINGS_FILE}`;
|
|
1985
|
+
var NO_FINDINGS = "No findings.";
|
|
1986
|
+
var REVIEW_EXCLUDE = ".scratch";
|
|
1987
|
+
var REVIEW_MARKER = "---- REVIEW INSTRUCTIONS ----";
|
|
1988
|
+
var FIX_MARKER = "---- REVIEW FINDINGS ----";
|
|
1989
|
+
function reviewExcludeFor(repoRoot, reviewPath) {
|
|
1990
|
+
let dir = dirname3(reviewPath);
|
|
1991
|
+
while (dirname3(dir) !== dir) {
|
|
1992
|
+
if (basename4(dir) === ".scratch") {
|
|
1993
|
+
const rel = relative(repoRoot, dir);
|
|
1994
|
+
if (rel && !rel.startsWith("..")) return rel;
|
|
1995
|
+
break;
|
|
1996
|
+
}
|
|
1997
|
+
dir = dirname3(dir);
|
|
1998
|
+
}
|
|
1999
|
+
return REVIEW_EXCLUDE;
|
|
2000
|
+
}
|
|
2001
|
+
__name(reviewExcludeFor, "reviewExcludeFor");
|
|
2002
|
+
function reviewDiffCommand(from, to, exclude = REVIEW_EXCLUDE) {
|
|
2003
|
+
return `git diff ${from}..${to} -- . ':(exclude)${exclude}'`;
|
|
2004
|
+
}
|
|
2005
|
+
__name(reviewDiffCommand, "reviewDiffCommand");
|
|
2006
|
+
function renderReviewPrompt(opts) {
|
|
2007
|
+
return [
|
|
2008
|
+
"---- SPEC CONTEXT ----",
|
|
2009
|
+
opts.specBody,
|
|
2010
|
+
"",
|
|
2011
|
+
REVIEW_MARKER,
|
|
2012
|
+
"You are reviewing work that autonomous agents merged into this branch, one ticket at a time. Review the combined change as a whole.",
|
|
2013
|
+
"",
|
|
2014
|
+
"The change to review:",
|
|
2015
|
+
"",
|
|
2016
|
+
` ${reviewDiffCommand(opts.from, opts.to, opts.exclude)}`,
|
|
2017
|
+
"",
|
|
2018
|
+
"Follow these rules:",
|
|
2019
|
+
"- Read any AGENTS.md in or above the directories the change touches, plus CONTEXT.md and the relevant docs/adr/ entries if they exist. Treat their contracts and the spec above as the standard the code must meet.",
|
|
2020
|
+
"- Look for correctness bugs, missing or weak tests, and code that breaks a documented contract. Ignore style preferences.",
|
|
2021
|
+
"- You may run the tests or the type checker to confirm a suspicion.",
|
|
2022
|
+
...subagentRule(opts.preferSubagent, true),
|
|
2023
|
+
"- Do NOT edit, stage, or commit code. Anything you change here is discarded.",
|
|
2024
|
+
...CONFINEMENT_RULES,
|
|
2025
|
+
"",
|
|
2026
|
+
`Write your findings to \`${FINDINGS_PATH}\` (create the directory), most severe first, in exactly this form:`,
|
|
2027
|
+
"",
|
|
2028
|
+
"- [ ] <one-line summary of the finding>",
|
|
2029
|
+
" <file:line, and a concrete input or state that goes wrong>",
|
|
2030
|
+
"",
|
|
2031
|
+
"If you find nothing worth fixing, write the single line: " + NO_FINDINGS,
|
|
2032
|
+
"Never git add or commit that file; the orchestrator collects it.",
|
|
2033
|
+
""
|
|
2034
|
+
].join("\n");
|
|
2035
|
+
}
|
|
2036
|
+
__name(renderReviewPrompt, "renderReviewPrompt");
|
|
2037
|
+
var FIX_FAILURE_TEXT = {
|
|
2038
|
+
install: "dependency install failed",
|
|
2039
|
+
exit: "the agent exited with an error",
|
|
2040
|
+
"no-commits": "the agent made no commits",
|
|
2041
|
+
tests: "the test command failed after the agent finished",
|
|
2042
|
+
merge: "the fix branch did not merge cleanly"
|
|
2043
|
+
};
|
|
2044
|
+
function findingLines(findings) {
|
|
2045
|
+
return findings.flatMap((f) => [
|
|
2046
|
+
`- [ ] ${f.summary}`,
|
|
2047
|
+
...f.detail ? f.detail.split("\n").map((l) => ` ${l}`) : []
|
|
2048
|
+
]);
|
|
2049
|
+
}
|
|
2050
|
+
__name(findingLines, "findingLines");
|
|
2051
|
+
function previousBlock(previous) {
|
|
2052
|
+
if (!previous) return [];
|
|
2053
|
+
return [
|
|
2054
|
+
"---- PREVIOUS FIX ATTEMPT ----",
|
|
2055
|
+
`It failed: ${FIX_FAILURE_TEXT[previous.reason]}.`,
|
|
2056
|
+
previous.notes ?? "It left no notes.",
|
|
2057
|
+
""
|
|
2058
|
+
];
|
|
2059
|
+
}
|
|
2060
|
+
__name(previousBlock, "previousBlock");
|
|
2061
|
+
function renderFixPrompt(opts) {
|
|
2062
|
+
return [
|
|
2063
|
+
"---- SPEC CONTEXT ----",
|
|
2064
|
+
opts.specBody,
|
|
2065
|
+
"",
|
|
2066
|
+
FIX_MARKER,
|
|
2067
|
+
...findingLines(opts.findings),
|
|
2068
|
+
"",
|
|
2069
|
+
...previousBlock(opts.previous),
|
|
2070
|
+
"---- AGENT INSTRUCTIONS ----",
|
|
2071
|
+
"You are an autonomous coding agent. Fix the review findings above using /tdd.",
|
|
2072
|
+
"",
|
|
2073
|
+
"Follow these rules:",
|
|
2074
|
+
readDocsRule("findings"),
|
|
2075
|
+
"- Fix ONLY the listed findings. Do not fix unrelated problems you notice.",
|
|
2076
|
+
...installRule(opts.installCmd),
|
|
2077
|
+
...subagentRule(opts.preferSubagent),
|
|
2078
|
+
"- Use /tdd for every finding: write a failing test that shows the problem, then write only enough code to make it pass, then refactor.",
|
|
2079
|
+
testGateRule(opts.testCmd, "fix"),
|
|
2080
|
+
LINT_RULE,
|
|
2081
|
+
integrityRule("finding"),
|
|
2082
|
+
...CONFINEMENT_RULES,
|
|
2083
|
+
commitRule("finding"),
|
|
2084
|
+
notesRule("finding", "fix"),
|
|
2085
|
+
"- After fixing each finding, print a line: [x] <finding summary> (matching the summary text exactly).",
|
|
2086
|
+
"- If a finding is wrong or cannot be fixed honestly, leave it unchecked and explain why in your notes file.",
|
|
2087
|
+
agentsMdRule("fix"),
|
|
2088
|
+
""
|
|
2089
|
+
].join("\n");
|
|
2090
|
+
}
|
|
2091
|
+
__name(renderFixPrompt, "renderFixPrompt");
|
|
2092
|
+
var SENTINEL_PREFIX2 = "<!-- nightralph:review";
|
|
2093
|
+
var SENTINEL_RE = /^<!-- nightralph:review (.*) -->$/;
|
|
2094
|
+
var SHA_RE = /^[0-9a-f]{4,64}$/;
|
|
2095
|
+
var FIX_HEADING = "### Fix";
|
|
2096
|
+
var FINDING_RE = /^ ?[-*+]\s+\[([ xX])\]\s+(.+)$/;
|
|
2097
|
+
var REVIEW_STATUSES = [
|
|
2098
|
+
"clean",
|
|
2099
|
+
"fixed",
|
|
2100
|
+
"unresolved",
|
|
2101
|
+
"review-failed"
|
|
2102
|
+
];
|
|
2103
|
+
function isReviewStatus(value) {
|
|
2104
|
+
return REVIEW_STATUSES.includes(value);
|
|
2105
|
+
}
|
|
2106
|
+
__name(isReviewStatus, "isReviewStatus");
|
|
2107
|
+
function collapse(s) {
|
|
2108
|
+
return s.replace(/\s+/g, " ").trim();
|
|
2109
|
+
}
|
|
2110
|
+
__name(collapse, "collapse");
|
|
2111
|
+
function normalize(s) {
|
|
2112
|
+
return collapse(s).toLowerCase();
|
|
2113
|
+
}
|
|
2114
|
+
__name(normalize, "normalize");
|
|
2115
|
+
function sanitize2(text) {
|
|
2116
|
+
return text.replace(
|
|
2117
|
+
new RegExp(SENTINEL_PREFIX2, "g"),
|
|
2118
|
+
"<!-- nightralph-review"
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
__name(sanitize2, "sanitize");
|
|
2122
|
+
function splitLines(text) {
|
|
2123
|
+
return text.split("\n").map((line) => {
|
|
2124
|
+
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
__name(splitLines, "splitLines");
|
|
2128
|
+
function parseFindings(text) {
|
|
2129
|
+
const findings = [];
|
|
2130
|
+
const details = [];
|
|
2131
|
+
for (const line of splitLines(text)) {
|
|
2132
|
+
const m = line.match(FINDING_RE);
|
|
2133
|
+
if (m) {
|
|
2134
|
+
findings.push({
|
|
2135
|
+
summary: collapse(m[2]),
|
|
2136
|
+
detail: "",
|
|
2137
|
+
checked: m[1] !== " "
|
|
2138
|
+
});
|
|
2139
|
+
details.push([]);
|
|
2140
|
+
} else if (findings.length > 0 && line.trim()) {
|
|
2141
|
+
details[details.length - 1].push(line.trim());
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
if (findings.length > 0) {
|
|
2145
|
+
return findings.map((f, i) => {
|
|
2146
|
+
return { ...f, detail: details[i].join("\n") };
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
const clean = splitLines(text).some((line) => {
|
|
2150
|
+
return normalize(line) === NO_FINDINGS.toLowerCase();
|
|
2151
|
+
});
|
|
2152
|
+
return clean ? [] : null;
|
|
2153
|
+
}
|
|
2154
|
+
__name(parseFindings, "parseFindings");
|
|
2155
|
+
function markFixed(findings, completedItems) {
|
|
2156
|
+
const done = new Set(completedItems.map(normalize));
|
|
2157
|
+
return findings.map((f) => {
|
|
2158
|
+
return { ...f, checked: done.has(normalize(f.summary)) };
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
__name(markFixed, "markFixed");
|
|
2162
|
+
function headline(input) {
|
|
2163
|
+
const n = input.findings.length;
|
|
2164
|
+
const fixed = input.findings.filter((f) => f.checked).length;
|
|
2165
|
+
switch (input.status) {
|
|
2166
|
+
case "clean":
|
|
2167
|
+
return "no findings";
|
|
2168
|
+
case "fixed":
|
|
2169
|
+
return `${fixed} of ${n} fixed`;
|
|
2170
|
+
case "unresolved":
|
|
2171
|
+
return `${n} finding${n === 1 ? "" : "s"}, fix failed`;
|
|
2172
|
+
case "review-failed":
|
|
2173
|
+
return "review failed";
|
|
2174
|
+
default: {
|
|
2175
|
+
const _exhaustive = input.status;
|
|
2176
|
+
return _exhaustive;
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
__name(headline, "headline");
|
|
2181
|
+
function renderFindings(input) {
|
|
2182
|
+
if (input.status === "review-failed") {
|
|
2183
|
+
return ["The reviewer left no usable findings file."];
|
|
2184
|
+
}
|
|
2185
|
+
if (input.findings.length === 0) return [NO_FINDINGS];
|
|
2186
|
+
return input.findings.flatMap((f) => {
|
|
2187
|
+
const box = f.checked ? "[x]" : "[ ]";
|
|
2188
|
+
const detail = f.detail ? f.detail.split("\n").map((l) => ` ${sanitize2(l)}`) : [];
|
|
2189
|
+
return [`- ${box} ${sanitize2(collapse(f.summary))}`, ...detail];
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
__name(renderFindings, "renderFindings");
|
|
2193
|
+
function renderFix(fix) {
|
|
2194
|
+
if (!fix) return [];
|
|
2195
|
+
return [
|
|
2196
|
+
"",
|
|
2197
|
+
FIX_HEADING,
|
|
2198
|
+
`- attempts: ${fix.attempts} of ${fix.maxAttempts}`,
|
|
2199
|
+
...fix.effort ? [`- effort: ${fix.effort}`] : [],
|
|
2200
|
+
`- agent exit code: ${fix.exitCode ?? "none"}`,
|
|
2201
|
+
`- result: ${sanitize2(fix.result)}`,
|
|
2202
|
+
"",
|
|
2203
|
+
fix.notes ? sanitize2(fix.notes) : "The fixer left no notes."
|
|
2204
|
+
];
|
|
2205
|
+
}
|
|
2206
|
+
__name(renderFix, "renderFix");
|
|
2207
|
+
function renderReviewEntry(input) {
|
|
2208
|
+
return [
|
|
2209
|
+
`${SENTINEL_PREFIX2} from=${input.from} to=${input.to} status=${input.status} -->`,
|
|
2210
|
+
`## ${formatDate(input.now)} -- review ${input.from.slice(0, 7)}..${input.to.slice(0, 7)} -- ${headline(input)}`,
|
|
2211
|
+
...renderFindings(input),
|
|
2212
|
+
...renderFix(input.fix),
|
|
2213
|
+
"---",
|
|
2214
|
+
""
|
|
2215
|
+
].join("\n");
|
|
2216
|
+
}
|
|
2217
|
+
__name(renderReviewEntry, "renderReviewEntry");
|
|
2218
|
+
function parseEntry(sentinel, bodyLines) {
|
|
2219
|
+
const attrs = {};
|
|
2220
|
+
for (const part of sentinel.split(/\s+/).filter(Boolean)) {
|
|
2221
|
+
const idx = part.indexOf("=");
|
|
2222
|
+
if (idx > 0) attrs[part.slice(0, idx)] = part.slice(idx + 1);
|
|
2223
|
+
}
|
|
2224
|
+
const { from, to, status } = attrs;
|
|
2225
|
+
if (!from || !SHA_RE.test(from)) return null;
|
|
2226
|
+
if (!to || !SHA_RE.test(to)) return null;
|
|
2227
|
+
if (!status || !isReviewStatus(status)) return null;
|
|
2228
|
+
const lines = [...bodyLines];
|
|
2229
|
+
while (lines.length > 0 && !lines[lines.length - 1].trim()) {
|
|
2230
|
+
lines.pop();
|
|
2231
|
+
}
|
|
2232
|
+
if (lines[lines.length - 1] === "---") lines.pop();
|
|
2233
|
+
const fixAt = lines.indexOf(FIX_HEADING);
|
|
2234
|
+
const findingLines2 = fixAt === -1 ? lines : lines.slice(0, fixAt);
|
|
2235
|
+
return {
|
|
2236
|
+
from,
|
|
2237
|
+
to,
|
|
2238
|
+
status,
|
|
2239
|
+
findings: parseFindings(findingLines2.join("\n")) ?? [],
|
|
2240
|
+
body: lines.join("\n").trim()
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
__name(parseEntry, "parseEntry");
|
|
2244
|
+
function parseReviewEntries(text) {
|
|
2245
|
+
const entries = [];
|
|
2246
|
+
let sentinel = null;
|
|
2247
|
+
let body = [];
|
|
2248
|
+
const flush = /* @__PURE__ */ __name(() => {
|
|
2249
|
+
if (sentinel === null) return;
|
|
2250
|
+
const entry = parseEntry(sentinel, body);
|
|
2251
|
+
if (entry) entries.push(entry);
|
|
2252
|
+
}, "flush");
|
|
2253
|
+
for (const line of splitLines(text)) {
|
|
2254
|
+
const m = line.match(SENTINEL_RE);
|
|
2255
|
+
if (m) {
|
|
2256
|
+
flush();
|
|
2257
|
+
sentinel = m[1];
|
|
2258
|
+
body = [];
|
|
2259
|
+
} else if (sentinel !== null) {
|
|
2260
|
+
body.push(line);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
flush();
|
|
2264
|
+
return entries;
|
|
2265
|
+
}
|
|
2266
|
+
__name(parseReviewEntries, "parseReviewEntries");
|
|
2267
|
+
function selectReviewBase(entries, opts) {
|
|
2268
|
+
const last = [...entries].reverse().find((e) => {
|
|
2269
|
+
return e.status !== "review-failed";
|
|
2270
|
+
});
|
|
2271
|
+
if (last && opts.isAncestor(last.to)) return last.to;
|
|
2272
|
+
return opts.forkPoint ?? opts.launchCommit;
|
|
2273
|
+
}
|
|
2274
|
+
__name(selectReviewBase, "selectReviewBase");
|
|
2275
|
+
function renderReviewSummary(outcome, reviewPath) {
|
|
2276
|
+
const see = ` -- see ${reviewPath}`;
|
|
2277
|
+
const plural = outcome.findings === 1 ? "" : "s";
|
|
2278
|
+
switch (outcome.status) {
|
|
2279
|
+
case "skipped":
|
|
2280
|
+
return null;
|
|
2281
|
+
case "clean":
|
|
2282
|
+
return `Review: no findings${see}`;
|
|
2283
|
+
case "fixed":
|
|
2284
|
+
return `Review: ${outcome.findings} finding${plural}, ${outcome.fixed} fixed${see}`;
|
|
2285
|
+
case "unresolved":
|
|
2286
|
+
return `Review: ${outcome.findings} finding${plural}, fix failed${see}`;
|
|
2287
|
+
case "review-failed":
|
|
2288
|
+
return `Review: the reviewer failed${see}`;
|
|
2289
|
+
default: {
|
|
2290
|
+
const _exhaustive = outcome.status;
|
|
2291
|
+
return _exhaustive;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
__name(renderReviewSummary, "renderReviewSummary");
|
|
2296
|
+
function logPath(logsDir, stem, attempt, ext) {
|
|
2297
|
+
const suffix = attempt > 1 ? `.attempt${attempt}` : "";
|
|
2298
|
+
return join5(logsDir, `${stem}${suffix}${ext}`);
|
|
2299
|
+
}
|
|
2300
|
+
__name(logPath, "logPath");
|
|
2301
|
+
async function runStep(opts, deps, cmd, cwd, path) {
|
|
2302
|
+
const result = await runTestCmd({
|
|
2303
|
+
cmd,
|
|
2304
|
+
cwd,
|
|
2305
|
+
timeout: opts.timeout,
|
|
2306
|
+
registry: deps.registry
|
|
2307
|
+
});
|
|
2308
|
+
writeFileSync4(path, result.output);
|
|
2309
|
+
return result.exitCode === 0;
|
|
2310
|
+
}
|
|
2311
|
+
__name(runStep, "runStep");
|
|
2312
|
+
async function discardWorktree(opts, wt) {
|
|
2313
|
+
try {
|
|
2314
|
+
await removeWorktree(wt.worktreePath, { force: true });
|
|
2315
|
+
await deleteBranch(opts.repoRoot, wt.branchName);
|
|
2316
|
+
} catch (err) {
|
|
2317
|
+
opts.log(
|
|
2318
|
+
` Failed to remove ${wt.worktreePath}: ` + (err instanceof Error ? err.message : String(err))
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
__name(discardWorktree, "discardWorktree");
|
|
2323
|
+
async function runReviewer(opts, deps, range) {
|
|
2324
|
+
return runWithRetries({
|
|
2325
|
+
maxAttempts: opts.retries + 1,
|
|
2326
|
+
retryDelay: opts.retryDelay,
|
|
2327
|
+
agentCmd: opts.agentCmd,
|
|
2328
|
+
effort: opts.effort,
|
|
2329
|
+
label: "review",
|
|
2330
|
+
log: opts.log
|
|
2331
|
+
}, async (attempt, effort) => {
|
|
2332
|
+
const wt = await createNamedWorktree({
|
|
2333
|
+
repoRoot: opts.repoRoot,
|
|
2334
|
+
repoName: opts.repoName,
|
|
2335
|
+
baseBranch: opts.baseBranch,
|
|
2336
|
+
name: "review"
|
|
2337
|
+
});
|
|
2338
|
+
try {
|
|
2339
|
+
if (opts.installCmd && !await runStep(
|
|
2340
|
+
opts,
|
|
2341
|
+
deps,
|
|
2342
|
+
opts.installCmd,
|
|
2343
|
+
wt.worktreePath,
|
|
2344
|
+
logPath(opts.logsDir, "review", attempt, ".install.log")
|
|
2345
|
+
)) {
|
|
2346
|
+
opts.log(" review: install failed");
|
|
2347
|
+
return { result: null, stop: false };
|
|
2348
|
+
}
|
|
2349
|
+
ensureNotesIgnored(wt.worktreePath);
|
|
2350
|
+
await deps.spawnAgent({
|
|
2351
|
+
prompt: renderReviewPrompt({
|
|
2352
|
+
specBody: opts.specBody,
|
|
2353
|
+
...range,
|
|
2354
|
+
exclude: reviewExcludeFor(
|
|
2355
|
+
opts.repoRoot,
|
|
2356
|
+
opts.reviewPath
|
|
2357
|
+
),
|
|
2358
|
+
preferSubagent: opts.preferSubagent
|
|
2359
|
+
}),
|
|
2360
|
+
agentCmd: opts.agentCmd,
|
|
2361
|
+
model: opts.model,
|
|
2362
|
+
timeout: opts.timeout,
|
|
2363
|
+
logPath: logPath(opts.logsDir, "review", attempt, ".log"),
|
|
2364
|
+
cwd: wt.worktreePath,
|
|
2365
|
+
label: "review",
|
|
2366
|
+
maxTurns: opts.maxTurns,
|
|
2367
|
+
effort,
|
|
2368
|
+
preferSubagent: opts.preferSubagent,
|
|
2369
|
+
onLine: opts.log
|
|
2370
|
+
});
|
|
2371
|
+
const text = collectNotes(wt.worktreePath, FINDINGS_FILE);
|
|
2372
|
+
const findings = text === null ? null : parseFindings(text);
|
|
2373
|
+
if (findings === null) {
|
|
2374
|
+
opts.log(" review: no usable findings file");
|
|
2375
|
+
}
|
|
2376
|
+
return { result: findings, stop: findings !== null };
|
|
2377
|
+
} finally {
|
|
2378
|
+
await discardWorktree(opts, wt);
|
|
2379
|
+
}
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
__name(runReviewer, "runReviewer");
|
|
2383
|
+
async function runFixer(opts, deps, findings) {
|
|
2384
|
+
const unchecked = findings.map((f) => ({ ...f, checked: false }));
|
|
2385
|
+
let previous;
|
|
2386
|
+
return runWithRetries({
|
|
2387
|
+
maxAttempts: opts.retries + 1,
|
|
2388
|
+
retryDelay: opts.retryDelay,
|
|
2389
|
+
agentCmd: opts.agentCmd,
|
|
2390
|
+
effort: opts.effort,
|
|
2391
|
+
label: "review fix",
|
|
2392
|
+
log: opts.log
|
|
2393
|
+
}, async (attempt, effort) => {
|
|
2394
|
+
const fail = /* @__PURE__ */ __name((reason, exitCode, notes) => {
|
|
2395
|
+
previous = { reason, notes };
|
|
2396
|
+
opts.log(` review fix: ${FIX_FAILURE_TEXT[reason]}`);
|
|
2397
|
+
return {
|
|
2398
|
+
result: {
|
|
2399
|
+
merged: false,
|
|
2400
|
+
findings: unchecked,
|
|
2401
|
+
attempts: attempt,
|
|
2402
|
+
effort,
|
|
2403
|
+
exitCode,
|
|
2404
|
+
result: FIX_FAILURE_TEXT[reason],
|
|
2405
|
+
notes
|
|
2406
|
+
},
|
|
2407
|
+
stop: false
|
|
2408
|
+
};
|
|
2409
|
+
}, "fail");
|
|
2410
|
+
const wt = await createNamedWorktree({
|
|
2411
|
+
repoRoot: opts.repoRoot,
|
|
2412
|
+
repoName: opts.repoName,
|
|
2413
|
+
baseBranch: opts.baseBranch,
|
|
2414
|
+
name: "review-fix"
|
|
2415
|
+
});
|
|
2416
|
+
try {
|
|
2417
|
+
if (opts.installCmd && !await runStep(
|
|
2418
|
+
opts,
|
|
2419
|
+
deps,
|
|
2420
|
+
opts.installCmd,
|
|
2421
|
+
wt.worktreePath,
|
|
2422
|
+
logPath(
|
|
2423
|
+
opts.logsDir,
|
|
2424
|
+
"review-fix",
|
|
2425
|
+
attempt,
|
|
2426
|
+
".install.log"
|
|
2427
|
+
)
|
|
2428
|
+
)) {
|
|
2429
|
+
return fail("install", null, null);
|
|
2430
|
+
}
|
|
2431
|
+
ensureNotesIgnored(wt.worktreePath);
|
|
2432
|
+
const agent = await deps.spawnAgent({
|
|
2433
|
+
prompt: renderFixPrompt({
|
|
2434
|
+
specBody: opts.specBody,
|
|
2435
|
+
findings,
|
|
2436
|
+
testCmd: opts.testCmd,
|
|
2437
|
+
installCmd: opts.installCmd,
|
|
2438
|
+
previous,
|
|
2439
|
+
preferSubagent: opts.preferSubagent
|
|
2440
|
+
}),
|
|
2441
|
+
agentCmd: opts.agentCmd,
|
|
2442
|
+
model: opts.model,
|
|
2443
|
+
timeout: opts.timeout,
|
|
2444
|
+
logPath: logPath(
|
|
2445
|
+
opts.logsDir,
|
|
2446
|
+
"review-fix",
|
|
2447
|
+
attempt,
|
|
2448
|
+
".log"
|
|
2449
|
+
),
|
|
2450
|
+
cwd: wt.worktreePath,
|
|
2451
|
+
label: "review-fix",
|
|
2452
|
+
maxTurns: opts.maxTurns,
|
|
2453
|
+
effort,
|
|
2454
|
+
preferSubagent: opts.preferSubagent,
|
|
2455
|
+
onLine: opts.log
|
|
2456
|
+
});
|
|
2457
|
+
const notes = collectNotes(wt.worktreePath);
|
|
2458
|
+
if (!agent.timedOut && agent.exitCode !== 0) {
|
|
2459
|
+
return fail("exit", agent.exitCode, notes);
|
|
2460
|
+
}
|
|
2461
|
+
if (isDirty(wt.worktreePath)) {
|
|
2462
|
+
await commitDirty(
|
|
2463
|
+
wt.worktreePath,
|
|
2464
|
+
"nightralph: auto-commit review fix remaining changes"
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
if (!hasNewCommits(
|
|
2468
|
+
opts.repoRoot,
|
|
2469
|
+
opts.baseBranch,
|
|
2470
|
+
wt.branchName
|
|
2471
|
+
)) {
|
|
2472
|
+
return fail("no-commits", agent.exitCode, notes);
|
|
2473
|
+
}
|
|
2474
|
+
if (opts.testCmd === "") {
|
|
2475
|
+
opts.log(" review fix: no test gate");
|
|
2476
|
+
} else if (!await runStep(
|
|
2477
|
+
opts,
|
|
2478
|
+
deps,
|
|
2479
|
+
opts.testCmd,
|
|
2480
|
+
wt.worktreePath,
|
|
2481
|
+
logPath(
|
|
2482
|
+
opts.logsDir,
|
|
2483
|
+
"review-fix",
|
|
2484
|
+
attempt,
|
|
2485
|
+
".test.log"
|
|
2486
|
+
)
|
|
2487
|
+
)) {
|
|
2488
|
+
return fail("tests", agent.exitCode, notes);
|
|
2489
|
+
}
|
|
2490
|
+
const merged = await mergeBranch(
|
|
2491
|
+
opts.repoRoot,
|
|
2492
|
+
wt.branchName
|
|
2493
|
+
);
|
|
2494
|
+
if (!merged.success) {
|
|
2495
|
+
return fail(
|
|
2496
|
+
"merge",
|
|
2497
|
+
agent.exitCode,
|
|
2498
|
+
[notes, merged.stderr].filter(Boolean).join("\n\n") || null
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
opts.log(" review fix: merged");
|
|
2502
|
+
return {
|
|
2503
|
+
result: {
|
|
2504
|
+
merged: true,
|
|
2505
|
+
findings: markFixed(findings, agent.completedItems),
|
|
2506
|
+
attempts: attempt,
|
|
2507
|
+
effort,
|
|
2508
|
+
exitCode: agent.exitCode,
|
|
2509
|
+
result: "merged",
|
|
2510
|
+
notes
|
|
2511
|
+
},
|
|
2512
|
+
stop: true
|
|
2513
|
+
};
|
|
2514
|
+
} finally {
|
|
2515
|
+
await discardWorktree(opts, wt);
|
|
2516
|
+
}
|
|
2517
|
+
});
|
|
2518
|
+
}
|
|
2519
|
+
__name(runFixer, "runFixer");
|
|
2520
|
+
function readReviewEntries(path) {
|
|
2521
|
+
if (!existsSync5(path)) return [];
|
|
2522
|
+
return parseReviewEntries(readFileSync5(path, "utf8"));
|
|
2523
|
+
}
|
|
2524
|
+
__name(readReviewEntries, "readReviewEntries");
|
|
2525
|
+
async function runReviewPass(opts, deps) {
|
|
2526
|
+
const now = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
2527
|
+
const to = headCommit(opts.repoRoot);
|
|
2528
|
+
const forkPoint = opts.forkedFrom ? mergeBase(opts.repoRoot, opts.forkedFrom, to) : null;
|
|
2529
|
+
const from = selectReviewBase(readReviewEntries(opts.reviewPath), {
|
|
2530
|
+
isAncestor: /* @__PURE__ */ __name((sha) => isAncestor(opts.repoRoot, sha, to), "isAncestor"),
|
|
2531
|
+
forkPoint,
|
|
2532
|
+
launchCommit: opts.launchCommit
|
|
2533
|
+
});
|
|
2534
|
+
const exclude = reviewExcludeFor(opts.repoRoot, opts.reviewPath);
|
|
2535
|
+
if (!hasChangesOutside(opts.repoRoot, from, to, exclude)) {
|
|
2536
|
+
opts.log("\nReview: nothing new to review");
|
|
2537
|
+
return { status: "skipped", findings: 0, fixed: 0 };
|
|
2538
|
+
}
|
|
2539
|
+
opts.log(`
|
|
2540
|
+
Reviewing ${from.slice(0, 7)}..${to.slice(0, 7)}`);
|
|
2541
|
+
const findings = await runReviewer(opts, deps, { from, to });
|
|
2542
|
+
let entry;
|
|
2543
|
+
if (findings === null) {
|
|
2544
|
+
entry = {
|
|
2545
|
+
from,
|
|
2546
|
+
to,
|
|
2547
|
+
status: "review-failed",
|
|
2548
|
+
now: now(),
|
|
2549
|
+
findings: []
|
|
2550
|
+
};
|
|
2551
|
+
} else if (findings.length === 0) {
|
|
2552
|
+
entry = { from, to, status: "clean", now: now(), findings: [] };
|
|
2553
|
+
} else {
|
|
2554
|
+
opts.log(
|
|
2555
|
+
`Review: ${findings.length} finding${findings.length === 1 ? "" : "s"}`
|
|
2556
|
+
);
|
|
2557
|
+
const fix = await runFixer(opts, deps, findings);
|
|
2558
|
+
entry = {
|
|
2559
|
+
from,
|
|
2560
|
+
// A merged fix counts as reviewed
|
|
2561
|
+
to: fix.merged ? headCommit(opts.repoRoot) : to,
|
|
2562
|
+
status: fix.merged ? "fixed" : "unresolved",
|
|
2563
|
+
now: now(),
|
|
2564
|
+
findings: fix.findings,
|
|
2565
|
+
fix: {
|
|
2566
|
+
attempts: fix.attempts,
|
|
2567
|
+
maxAttempts: opts.retries + 1,
|
|
2568
|
+
effort: fix.effort,
|
|
2569
|
+
exitCode: fix.exitCode,
|
|
2570
|
+
result: fix.result,
|
|
2571
|
+
notes: fix.notes
|
|
2572
|
+
}
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2575
|
+
appendEntry(opts.reviewPath, renderReviewEntry(entry));
|
|
2576
|
+
await commitProgress(
|
|
2577
|
+
opts.repoRoot,
|
|
2578
|
+
opts.reviewPath,
|
|
2579
|
+
"nightralph: record review"
|
|
2580
|
+
);
|
|
2581
|
+
return {
|
|
2582
|
+
status: entry.status,
|
|
2583
|
+
findings: entry.findings.length,
|
|
2584
|
+
fixed: entry.findings.filter((f) => f.checked).length
|
|
2585
|
+
};
|
|
1124
2586
|
}
|
|
1125
|
-
__name(
|
|
2587
|
+
__name(runReviewPass, "runReviewPass");
|
|
1126
2588
|
|
|
1127
|
-
// src/
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
const pkg = JSON.parse(
|
|
1136
|
-
readFileSync3(pkgPath, "utf8")
|
|
1137
|
-
);
|
|
1138
|
-
return pkg.scripts?.test ? "npm test" : null;
|
|
1139
|
-
} catch {
|
|
1140
|
-
return null;
|
|
1141
|
-
}
|
|
2589
|
+
// src/blockers.ts
|
|
2590
|
+
var FIELD_RE = /\*\*Blocked by:\*\*/i;
|
|
2591
|
+
var NEXT_FIELD_RE = /^\s*(?:[-*]\s+)?\*\*[^*\n]+:\*\*/;
|
|
2592
|
+
var HEADING_RE = /^\s*#{1,6}\s/;
|
|
2593
|
+
var CHECKLIST_RE = /^\s*[-*+]\s+\[[ xX]\]/;
|
|
2594
|
+
var TOKEN_RE = /[A-Za-z0-9_-]+/g;
|
|
2595
|
+
function endsField(line) {
|
|
2596
|
+
return line.trim() === "" || NEXT_FIELD_RE.test(line) || HEADING_RE.test(line) || CHECKLIST_RE.test(line);
|
|
1142
2597
|
}
|
|
1143
|
-
__name(
|
|
1144
|
-
function
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
if (
|
|
1153
|
-
|
|
2598
|
+
__name(endsField, "endsField");
|
|
2599
|
+
function fieldText(body) {
|
|
2600
|
+
const lines = body.split(/\r?\n/);
|
|
2601
|
+
const start = lines.findIndex((l) => FIELD_RE.test(l));
|
|
2602
|
+
if (start === -1) return null;
|
|
2603
|
+
const first = lines[start];
|
|
2604
|
+
const m = FIELD_RE.exec(first);
|
|
2605
|
+
const parts = [first.slice(m.index + m[0].length)];
|
|
2606
|
+
for (const line of lines.slice(start + 1)) {
|
|
2607
|
+
if (endsField(line)) break;
|
|
2608
|
+
parts.push(line);
|
|
2609
|
+
}
|
|
2610
|
+
return parts.join("\n");
|
|
2611
|
+
}
|
|
2612
|
+
__name(fieldText, "fieldText");
|
|
2613
|
+
function stripNotes(text) {
|
|
2614
|
+
const chars = [...text];
|
|
2615
|
+
const drop = new Array(chars.length).fill(false);
|
|
2616
|
+
const ticks = [];
|
|
2617
|
+
chars.forEach((ch, i) => {
|
|
2618
|
+
if (ch === "`") ticks.push(i);
|
|
2619
|
+
});
|
|
2620
|
+
for (let i = 0; i + 1 < ticks.length; i += 2) {
|
|
2621
|
+
for (let j = ticks[i]; j <= ticks[i + 1]; j++) {
|
|
2622
|
+
drop[j] = true;
|
|
1154
2623
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
2624
|
+
}
|
|
2625
|
+
const open = [];
|
|
2626
|
+
chars.forEach((ch, i) => {
|
|
2627
|
+
if (drop[i]) return;
|
|
2628
|
+
if (ch === "(") {
|
|
2629
|
+
open.push(i);
|
|
2630
|
+
} else if (ch === ")" && open.length > 0) {
|
|
2631
|
+
const from = open.pop();
|
|
2632
|
+
for (let j = from; j <= i; j++) drop[j] = true;
|
|
2633
|
+
}
|
|
2634
|
+
});
|
|
2635
|
+
return chars.map((ch, i) => drop[i] ? " " : ch).join("");
|
|
2636
|
+
}
|
|
2637
|
+
__name(stripNotes, "stripNotes");
|
|
2638
|
+
function looksLikeFeature(word) {
|
|
2639
|
+
const segments = word.split("/");
|
|
2640
|
+
return (segments.length > 1 || word.includes("-")) && /[a-z]/.test(word) && segments.every((s) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(s));
|
|
2641
|
+
}
|
|
2642
|
+
__name(looksLikeFeature, "looksLikeFeature");
|
|
2643
|
+
function wordBefore(text, start) {
|
|
2644
|
+
let i = start;
|
|
2645
|
+
while (i > 0 && /\s/.test(text[i - 1])) i--;
|
|
2646
|
+
if (i === start) return null;
|
|
2647
|
+
const end = i;
|
|
2648
|
+
while (i > 0 && /[A-Za-z0-9_/-]/.test(text[i - 1])) i--;
|
|
2649
|
+
return i === end ? null : text.slice(i, end);
|
|
2650
|
+
}
|
|
2651
|
+
__name(wordBefore, "wordBefore");
|
|
2652
|
+
function parseBlockerField(body, knownFeatures) {
|
|
2653
|
+
const text = fieldText(body);
|
|
2654
|
+
if (text === null) return { refs: [], typos: [] };
|
|
2655
|
+
const clean = stripNotes(text);
|
|
2656
|
+
if (/^\s*none\b/i.test(clean)) return { refs: [], typos: [] };
|
|
2657
|
+
const refs = [];
|
|
2658
|
+
const typos = [];
|
|
2659
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2660
|
+
const add = /* @__PURE__ */ __name((num, feature) => {
|
|
2661
|
+
const id = `${feature ?? ""}:${num}`;
|
|
2662
|
+
if (seen.has(id)) return;
|
|
2663
|
+
seen.add(id);
|
|
2664
|
+
refs.push(feature === void 0 ? { num } : { feature, num });
|
|
2665
|
+
}, "add");
|
|
2666
|
+
for (const m of clean.matchAll(TOKEN_RE)) {
|
|
2667
|
+
const word = m[0];
|
|
2668
|
+
if (/^\d+(?:-\d+)*$/.test(word)) {
|
|
2669
|
+
const before = wordBefore(clean, m.index ?? 0);
|
|
2670
|
+
let feature;
|
|
2671
|
+
if (before !== null && knownFeatures.has(before)) {
|
|
2672
|
+
feature = before;
|
|
2673
|
+
} else if (before !== null && looksLikeFeature(before) && !typos.includes(before)) {
|
|
2674
|
+
typos.push(before);
|
|
1175
2675
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
2676
|
+
for (const n of word.split("-").map(Number)) add(n, feature);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
return { refs, typos };
|
|
2680
|
+
}
|
|
2681
|
+
__name(parseBlockerField, "parseBlockerField");
|
|
2682
|
+
function refId(feature, num) {
|
|
2683
|
+
return `${feature}:${num}`;
|
|
2684
|
+
}
|
|
2685
|
+
__name(refId, "refId");
|
|
2686
|
+
function resolveBlockers(raw, ownFeature, selected, lookup) {
|
|
2687
|
+
const out = [];
|
|
2688
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2689
|
+
for (const r of raw) {
|
|
2690
|
+
const feature = r.feature ?? ownFeature;
|
|
2691
|
+
const id = refId(feature, r.num);
|
|
2692
|
+
if (seen.has(id)) continue;
|
|
2693
|
+
seen.add(id);
|
|
2694
|
+
const nums = selected.get(feature);
|
|
2695
|
+
if (nums) {
|
|
2696
|
+
out.push({
|
|
2697
|
+
feature,
|
|
2698
|
+
num: r.num,
|
|
2699
|
+
where: nums.has(r.num) ? "run" : "missing"
|
|
1180
2700
|
});
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
2701
|
+
continue;
|
|
2702
|
+
}
|
|
2703
|
+
const status = lookup(feature, r.num);
|
|
2704
|
+
if (status === void 0) {
|
|
2705
|
+
out.push({ feature, num: r.num, where: "missing" });
|
|
2706
|
+
} else {
|
|
2707
|
+
out.push({
|
|
2708
|
+
feature,
|
|
2709
|
+
num: r.num,
|
|
2710
|
+
where: status === "done" ? "done" : "pending",
|
|
2711
|
+
status
|
|
1192
2712
|
});
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
return out;
|
|
1195
2716
|
}
|
|
1196
|
-
__name(
|
|
2717
|
+
__name(resolveBlockers, "resolveBlockers");
|
|
2718
|
+
function provisionalRefs(raw, ownFeature) {
|
|
2719
|
+
const out = [];
|
|
2720
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2721
|
+
for (const r of raw) {
|
|
2722
|
+
const feature = r.feature ?? ownFeature;
|
|
2723
|
+
const id = refId(feature, r.num);
|
|
2724
|
+
if (seen.has(id)) continue;
|
|
2725
|
+
seen.add(id);
|
|
2726
|
+
out.push({ feature, num: r.num, where: "run" });
|
|
2727
|
+
}
|
|
2728
|
+
return out;
|
|
2729
|
+
}
|
|
2730
|
+
__name(provisionalRefs, "provisionalRefs");
|
|
1197
2731
|
|
|
1198
2732
|
// src/orchestrator.ts
|
|
1199
2733
|
var execFileAsync4 = promisify4(execFile4);
|
|
@@ -1265,21 +2799,7 @@ function parseStatus(body) {
|
|
|
1265
2799
|
return m ? m[1].trim().toLowerCase() : "";
|
|
1266
2800
|
}
|
|
1267
2801
|
__name(parseStatus, "parseStatus");
|
|
1268
|
-
function
|
|
1269
|
-
const m = body.match(/\*\*Blocked by:\*\*\s*(.+)/i);
|
|
1270
|
-
if (!m) return [];
|
|
1271
|
-
const text = m[1];
|
|
1272
|
-
if (/none/i.test(text)) return [];
|
|
1273
|
-
const nums = [];
|
|
1274
|
-
const re = /(\d+)/g;
|
|
1275
|
-
let match;
|
|
1276
|
-
while (match = re.exec(text)) {
|
|
1277
|
-
nums.push(Number(match[1]));
|
|
1278
|
-
}
|
|
1279
|
-
return nums;
|
|
1280
|
-
}
|
|
1281
|
-
__name(parseBlockers, "parseBlockers");
|
|
1282
|
-
function scanTickets(dir, feature = "") {
|
|
2802
|
+
function scanTickets(dir, feature = "", knownFeatures = /* @__PURE__ */ new Set()) {
|
|
1283
2803
|
const files = readdirSync2(dir).filter((f) => TICKET_RE.test(f)).sort((a, b) => a.localeCompare(
|
|
1284
2804
|
b,
|
|
1285
2805
|
void 0,
|
|
@@ -1287,8 +2807,8 @@ function scanTickets(dir, feature = "") {
|
|
|
1287
2807
|
));
|
|
1288
2808
|
return files.map((filename) => {
|
|
1289
2809
|
const parsed = parseTicketFilename(filename);
|
|
1290
|
-
const filepath =
|
|
1291
|
-
const body =
|
|
2810
|
+
const filepath = join6(dir, filename);
|
|
2811
|
+
const body = readFileSync6(filepath, "utf8");
|
|
1292
2812
|
return {
|
|
1293
2813
|
num: parsed.num,
|
|
1294
2814
|
slug: parsed.slug,
|
|
@@ -1296,7 +2816,10 @@ function scanTickets(dir, feature = "") {
|
|
|
1296
2816
|
filepath,
|
|
1297
2817
|
body,
|
|
1298
2818
|
status: parseStatus(body),
|
|
1299
|
-
blockers:
|
|
2819
|
+
blockers: provisionalRefs(
|
|
2820
|
+
parseBlockerField(body, knownFeatures).refs,
|
|
2821
|
+
feature
|
|
2822
|
+
),
|
|
1300
2823
|
feature
|
|
1301
2824
|
};
|
|
1302
2825
|
});
|
|
@@ -1306,15 +2829,35 @@ function ticketKey(feature, num) {
|
|
|
1306
2829
|
return `${feature}:${num}`;
|
|
1307
2830
|
}
|
|
1308
2831
|
__name(ticketKey, "ticketKey");
|
|
2832
|
+
function blockerSatisfied(ref, doneKeys) {
|
|
2833
|
+
switch (ref.where) {
|
|
2834
|
+
case "run":
|
|
2835
|
+
return doneKeys.has(ticketKey(ref.feature, ref.num));
|
|
2836
|
+
case "done":
|
|
2837
|
+
return true;
|
|
2838
|
+
case "pending":
|
|
2839
|
+
case "missing":
|
|
2840
|
+
return false;
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
__name(blockerSatisfied, "blockerSatisfied");
|
|
2844
|
+
function blockerLabel(ref) {
|
|
2845
|
+
const nn = String(ref.num).padStart(2, "0");
|
|
2846
|
+
const base = ref.feature ? `${ref.feature}/${nn}` : nn;
|
|
2847
|
+
if (ref.where === "pending") {
|
|
2848
|
+
return `${base} (${ref.status ?? "not done"}, not in run)`;
|
|
2849
|
+
}
|
|
2850
|
+
if (ref.where === "missing") return `${base} (not found)`;
|
|
2851
|
+
return base;
|
|
2852
|
+
}
|
|
2853
|
+
__name(blockerLabel, "blockerLabel");
|
|
1309
2854
|
function findReadyTickets(tickets) {
|
|
1310
2855
|
const doneKeys = new Set(
|
|
1311
2856
|
tickets.filter((t) => t.status === "done").map((t) => ticketKey(t.feature, t.num))
|
|
1312
2857
|
);
|
|
1313
2858
|
return tickets.filter((t) => {
|
|
1314
2859
|
if (t.status !== "ready-for-agent") return false;
|
|
1315
|
-
return t.blockers.every(
|
|
1316
|
-
(b) => doneKeys.has(ticketKey(t.feature, b))
|
|
1317
|
-
);
|
|
2860
|
+
return t.blockers.every((r) => blockerSatisfied(r, doneKeys));
|
|
1318
2861
|
});
|
|
1319
2862
|
}
|
|
1320
2863
|
__name(findReadyTickets, "findReadyTickets");
|
|
@@ -1323,14 +2866,19 @@ function findBlockedTickets(tickets, failedKeys) {
|
|
|
1323
2866
|
tickets.map((t) => [ticketKey(t.feature, t.num), t])
|
|
1324
2867
|
);
|
|
1325
2868
|
function collectDead(ticket, visited, found) {
|
|
1326
|
-
for (const
|
|
1327
|
-
const key = ticketKey(
|
|
2869
|
+
for (const ref of ticket.blockers) {
|
|
2870
|
+
const key = ticketKey(ref.feature, ref.num);
|
|
1328
2871
|
if (visited.has(key)) continue;
|
|
1329
2872
|
visited.add(key);
|
|
2873
|
+
if (ref.where === "done") continue;
|
|
2874
|
+
if (ref.where === "pending" || ref.where === "missing") {
|
|
2875
|
+
found.set(key, ref);
|
|
2876
|
+
continue;
|
|
2877
|
+
}
|
|
1330
2878
|
const blocker = byKey.get(key);
|
|
1331
2879
|
if (!blocker || blocker.status === "done") continue;
|
|
1332
2880
|
if (failedKeys.has(key) || blocker.status !== "ready-for-agent") {
|
|
1333
|
-
found.
|
|
2881
|
+
found.set(key, ref);
|
|
1334
2882
|
continue;
|
|
1335
2883
|
}
|
|
1336
2884
|
collectDead(blocker, visited, found);
|
|
@@ -1342,9 +2890,9 @@ function findBlockedTickets(tickets, failedKeys) {
|
|
|
1342
2890
|
const key = ticketKey(t.feature, t.num);
|
|
1343
2891
|
if (t.status !== "ready-for-agent") continue;
|
|
1344
2892
|
if (failedKeys.has(key)) continue;
|
|
1345
|
-
const found = /* @__PURE__ */ new
|
|
2893
|
+
const found = /* @__PURE__ */ new Map();
|
|
1346
2894
|
collectDead(t, /* @__PURE__ */ new Set([key]), found);
|
|
1347
|
-
if (found.size > 0) blocked.set(key, [...found]);
|
|
2895
|
+
if (found.size > 0) blocked.set(key, [...found.values()]);
|
|
1348
2896
|
}
|
|
1349
2897
|
return blocked;
|
|
1350
2898
|
}
|
|
@@ -1399,7 +2947,7 @@ function checkItems(ticket, completedItems) {
|
|
|
1399
2947
|
if (completedItems.length === 0) return;
|
|
1400
2948
|
let body;
|
|
1401
2949
|
try {
|
|
1402
|
-
body =
|
|
2950
|
+
body = readFileSync6(ticket.filepath, "utf8");
|
|
1403
2951
|
} catch {
|
|
1404
2952
|
body = ticket.body;
|
|
1405
2953
|
}
|
|
@@ -1423,7 +2971,7 @@ function checkItems(ticket, completedItems) {
|
|
|
1423
2971
|
}
|
|
1424
2972
|
if (!changed) return;
|
|
1425
2973
|
body = lines.join("\n");
|
|
1426
|
-
|
|
2974
|
+
writeFileSync5(ticket.filepath, body);
|
|
1427
2975
|
ticket.body = body;
|
|
1428
2976
|
}
|
|
1429
2977
|
__name(checkItems, "checkItems");
|
|
@@ -1447,7 +2995,7 @@ __name(checkAllItems, "checkAllItems");
|
|
|
1447
2995
|
function markDone(ticket) {
|
|
1448
2996
|
let body;
|
|
1449
2997
|
try {
|
|
1450
|
-
body =
|
|
2998
|
+
body = readFileSync6(ticket.filepath, "utf8");
|
|
1451
2999
|
} catch {
|
|
1452
3000
|
body = ticket.body;
|
|
1453
3001
|
}
|
|
@@ -1456,7 +3004,7 @@ function markDone(ticket) {
|
|
|
1456
3004
|
/\*\*Status:\*\*\s*.+/i,
|
|
1457
3005
|
"**Status:** done"
|
|
1458
3006
|
);
|
|
1459
|
-
|
|
3007
|
+
writeFileSync5(ticket.filepath, updated);
|
|
1460
3008
|
ticket.status = "done";
|
|
1461
3009
|
ticket.body = updated;
|
|
1462
3010
|
}
|
|
@@ -1489,11 +3037,23 @@ function stripSpecSection(specBody, heading) {
|
|
|
1489
3037
|
return out.join("\n");
|
|
1490
3038
|
}
|
|
1491
3039
|
__name(stripSpecSection, "stripSpecSection");
|
|
3040
|
+
function reviewSpecBody(specBody, specBodies) {
|
|
3041
|
+
if (!specBodies || specBodies.size <= 1) return specBody;
|
|
3042
|
+
return [...specBodies].map(([name, body]) => `# Feature: ${name}
|
|
3043
|
+
|
|
3044
|
+
${body}`).join("\n\n");
|
|
3045
|
+
}
|
|
3046
|
+
__name(reviewSpecBody, "reviewSpecBody");
|
|
1492
3047
|
function lookupSpec(specBodies, fallback, feature) {
|
|
1493
3048
|
if (!feature || !specBodies) return fallback;
|
|
1494
3049
|
return specBodies.get(feature) ?? fallback;
|
|
1495
3050
|
}
|
|
1496
3051
|
__name(lookupSpec, "lookupSpec");
|
|
3052
|
+
function historyBlock(history) {
|
|
3053
|
+
if (!history) return [];
|
|
3054
|
+
return [history, ""];
|
|
3055
|
+
}
|
|
3056
|
+
__name(historyBlock, "historyBlock");
|
|
1497
3057
|
function renderPrompt(specBody, ticket, opts) {
|
|
1498
3058
|
return [
|
|
1499
3059
|
"---- SPEC CONTEXT ----",
|
|
@@ -1503,27 +3063,30 @@ function renderPrompt(specBody, ticket, opts) {
|
|
|
1503
3063
|
ticket.filename,
|
|
1504
3064
|
ticket.body,
|
|
1505
3065
|
"",
|
|
3066
|
+
...historyBlock(opts.history),
|
|
1506
3067
|
"---- AGENT INSTRUCTIONS ----",
|
|
1507
3068
|
"You are an autonomous coding agent. Implement the ticket using /tdd.",
|
|
1508
3069
|
"",
|
|
1509
3070
|
"Follow these rules:",
|
|
1510
|
-
"
|
|
3071
|
+
readDocsRule("ticket"),
|
|
1511
3072
|
"- Work ONLY on this ticket. Do not fix unrelated problems you notice.",
|
|
1512
3073
|
"- Do NOT research external APIs or services via web search. Use the spec and ticket body as your sole reference for API shapes.",
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
"-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
"
|
|
1519
|
-
|
|
3074
|
+
...installRule(opts.installCmd),
|
|
3075
|
+
...subagentRule(opts.preferSubagent),
|
|
3076
|
+
"- Use /tdd for every checklist item. For each item, write a failing test at the seams named in the spec's Testing Decisions section, then write only enough code to make it pass, then refactor. For a checklist item that is pure scaffolding, config, or docs (where a test adds no value), skip test-first and record that decision in your notes file.",
|
|
3077
|
+
testGateRule(opts.testCmd, "ticket"),
|
|
3078
|
+
LINT_RULE,
|
|
3079
|
+
integrityRule("checklist item"),
|
|
3080
|
+
...CONFINEMENT_RULES,
|
|
3081
|
+
commitRule("checklist item"),
|
|
3082
|
+
notesRule("checklist item", "ticket"),
|
|
1520
3083
|
"- After completing each checklist item in the ticket, print a line: [x] <item text> (matching the checklist text exactly).",
|
|
1521
|
-
"
|
|
3084
|
+
agentsMdRule("ticket"),
|
|
1522
3085
|
""
|
|
1523
3086
|
].join("\n");
|
|
1524
3087
|
}
|
|
1525
3088
|
__name(renderPrompt, "renderPrompt");
|
|
1526
|
-
function renderMergePrompt(specBody, ticket) {
|
|
3089
|
+
function renderMergePrompt(specBody, ticket, opts = {}) {
|
|
1527
3090
|
return [
|
|
1528
3091
|
"---- SPEC CONTEXT ----",
|
|
1529
3092
|
specBody,
|
|
@@ -1544,6 +3107,8 @@ function renderMergePrompt(specBody, ticket) {
|
|
|
1544
3107
|
"4. Commit the resolved files.",
|
|
1545
3108
|
"",
|
|
1546
3109
|
"Do NOT re-implement the ticket from scratch.",
|
|
3110
|
+
...installRule(opts.installCmd),
|
|
3111
|
+
...subagentRule(opts.preferSubagent),
|
|
1547
3112
|
"Do NOT read or modify files outside your current directory (the worktree).",
|
|
1548
3113
|
"Do NOT run git push."
|
|
1549
3114
|
].join("\n");
|
|
@@ -1552,7 +3117,7 @@ __name(renderMergePrompt, "renderMergePrompt");
|
|
|
1552
3117
|
function listTickets(tickets) {
|
|
1553
3118
|
console.log("\nTickets:");
|
|
1554
3119
|
for (const t of tickets) {
|
|
1555
|
-
const blockerStr = t.blockers.length > 0 ? `blocked by [${t.blockers.join(", ")}]` : "no blockers";
|
|
3120
|
+
const blockerStr = t.blockers.length > 0 ? `blocked by [${t.blockers.map(blockerLabel).join(", ")}]` : "no blockers";
|
|
1556
3121
|
console.log(
|
|
1557
3122
|
` ${t.filename} (${t.status}) ${blockerStr}`
|
|
1558
3123
|
);
|
|
@@ -1576,11 +3141,7 @@ function* simulateWaves(tickets) {
|
|
|
1576
3141
|
if (sim.get(key) !== "ready-for-agent") {
|
|
1577
3142
|
return false;
|
|
1578
3143
|
}
|
|
1579
|
-
return t.blockers.every(
|
|
1580
|
-
(b) => doneKeys.has(
|
|
1581
|
-
ticketKey(t.feature, b)
|
|
1582
|
-
)
|
|
1583
|
-
);
|
|
3144
|
+
return t.blockers.every((r) => blockerSatisfied(r, doneKeys));
|
|
1584
3145
|
});
|
|
1585
3146
|
if (ready.length === 0) break;
|
|
1586
3147
|
yield { wave, ready };
|
|
@@ -1613,9 +3174,7 @@ function detectDeadlocks(tickets) {
|
|
|
1613
3174
|
(t) => ticketKey(t.feature, t.num) === key
|
|
1614
3175
|
);
|
|
1615
3176
|
if (ticket.blockers.every(
|
|
1616
|
-
(
|
|
1617
|
-
ticketKey(ticket.feature, b)
|
|
1618
|
-
)
|
|
3177
|
+
(r) => blockerSatisfied(r, doneKeys)
|
|
1619
3178
|
)) {
|
|
1620
3179
|
doneKeys.add(key);
|
|
1621
3180
|
readyKeys.delete(key);
|
|
@@ -1653,80 +3212,41 @@ var PROVIDER_ARGS = {
|
|
|
1653
3212
|
]
|
|
1654
3213
|
};
|
|
1655
3214
|
function getProviderArgs(cmd) {
|
|
1656
|
-
const name =
|
|
3215
|
+
const name = basename5(cmd);
|
|
1657
3216
|
return PROVIDER_ARGS[name] ?? [];
|
|
1658
3217
|
}
|
|
1659
3218
|
__name(getProviderArgs, "getProviderArgs");
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
"minimal",
|
|
1664
|
-
"low",
|
|
1665
|
-
"medium",
|
|
1666
|
-
"high",
|
|
1667
|
-
"xhigh",
|
|
1668
|
-
"max"
|
|
1669
|
-
],
|
|
1670
|
-
claude: [
|
|
1671
|
-
"low",
|
|
1672
|
-
"medium",
|
|
1673
|
-
"high",
|
|
1674
|
-
"xhigh",
|
|
1675
|
-
"max"
|
|
1676
|
-
],
|
|
1677
|
-
codex: ["low", "medium", "high"]
|
|
1678
|
-
};
|
|
1679
|
-
var ALL_EFFORT_LEVELS = [
|
|
1680
|
-
"off",
|
|
1681
|
-
"minimal",
|
|
1682
|
-
"low",
|
|
1683
|
-
"medium",
|
|
1684
|
-
"high",
|
|
1685
|
-
"xhigh",
|
|
1686
|
-
"max"
|
|
1687
|
-
];
|
|
1688
|
-
function escalateEffort(provider, level) {
|
|
1689
|
-
if (!level) return "high";
|
|
1690
|
-
const ladder = EFFORT_LADDERS[provider];
|
|
1691
|
-
const i = ladder.indexOf(level);
|
|
1692
|
-
if (i === -1) return level;
|
|
1693
|
-
return ladder[Math.min(i + 1, ladder.length - 1)];
|
|
1694
|
-
}
|
|
1695
|
-
__name(escalateEffort, "escalateEffort");
|
|
1696
|
-
function formatStreamLine(line) {
|
|
1697
|
-
let obj;
|
|
1698
|
-
try {
|
|
1699
|
-
obj = JSON.parse(line);
|
|
1700
|
-
} catch {
|
|
1701
|
-
return line;
|
|
1702
|
-
}
|
|
3219
|
+
function formatStreamObject(value) {
|
|
3220
|
+
if (!isRecord(value)) return null;
|
|
3221
|
+
const obj = value;
|
|
1703
3222
|
if (obj.type === "assistant") {
|
|
1704
|
-
|
|
1705
|
-
const
|
|
3223
|
+
if (obj.parent_tool_use_id != null) return null;
|
|
3224
|
+
const msg = isRecord(obj.message) ? obj.message : void 0;
|
|
3225
|
+
const content = Array.isArray(msg?.content) ? msg.content : void 0;
|
|
1706
3226
|
if (!content) return null;
|
|
1707
3227
|
for (const block of content) {
|
|
1708
|
-
if (block
|
|
1709
|
-
|
|
1710
|
-
|
|
3228
|
+
if (!isRecord(block)) continue;
|
|
3229
|
+
if (block.type !== "text") continue;
|
|
3230
|
+
if (typeof block.text === "string") return block.text;
|
|
1711
3231
|
}
|
|
1712
3232
|
return null;
|
|
1713
3233
|
}
|
|
1714
3234
|
if (obj.type === "result") {
|
|
1715
|
-
const sub = obj.subtype
|
|
1716
|
-
const cost = obj.total_cost_usd;
|
|
1717
|
-
const costStr = cost
|
|
3235
|
+
const sub = typeof obj.subtype === "string" ? obj.subtype : "done";
|
|
3236
|
+
const cost = typeof obj.total_cost_usd === "number" ? obj.total_cost_usd : void 0;
|
|
3237
|
+
const costStr = cost !== void 0 ? ` ($${cost.toFixed(2)})` : "";
|
|
1718
3238
|
return `${sub}${costStr}`;
|
|
1719
3239
|
}
|
|
1720
3240
|
if (obj.type === "system") {
|
|
1721
|
-
const sub = obj.subtype;
|
|
3241
|
+
const sub = typeof obj.subtype === "string" ? obj.subtype : void 0;
|
|
1722
3242
|
if (sub === "api_retry") {
|
|
1723
|
-
const error = obj.error;
|
|
1724
|
-
return `retry: ${error
|
|
3243
|
+
const error = typeof obj.error === "string" ? obj.error : "unknown";
|
|
3244
|
+
return `retry: ${error}`;
|
|
1725
3245
|
}
|
|
1726
3246
|
}
|
|
1727
3247
|
return null;
|
|
1728
3248
|
}
|
|
1729
|
-
__name(
|
|
3249
|
+
__name(formatStreamObject, "formatStreamObject");
|
|
1730
3250
|
function createPiStreamFormatter() {
|
|
1731
3251
|
const calls = /* @__PURE__ */ new Map();
|
|
1732
3252
|
let textBuf = "";
|
|
@@ -1793,8 +3313,8 @@ function createPiStreamFormatter() {
|
|
|
1793
3313
|
__name(createPiStreamFormatter, "createPiStreamFormatter");
|
|
1794
3314
|
function writePromptFile(prompt) {
|
|
1795
3315
|
const name = `nightralph-${Date.now()}-${Math.random().toString(36).slice(2)}.md`;
|
|
1796
|
-
const filePath =
|
|
1797
|
-
|
|
3316
|
+
const filePath = join6(tmpdir(), name);
|
|
3317
|
+
writeFileSync5(filePath, prompt);
|
|
1798
3318
|
return filePath;
|
|
1799
3319
|
}
|
|
1800
3320
|
__name(writePromptFile, "writePromptFile");
|
|
@@ -1835,7 +3355,7 @@ function spawnAgent(opts) {
|
|
|
1835
3355
|
if (opts.model) {
|
|
1836
3356
|
args.push("--model", opts.model);
|
|
1837
3357
|
}
|
|
1838
|
-
const providerName =
|
|
3358
|
+
const providerName = basename5(opts.agentCmd);
|
|
1839
3359
|
const isClaude = providerName === "claude";
|
|
1840
3360
|
const isPi = providerName === "pi";
|
|
1841
3361
|
if (opts.maxTurns) {
|
|
@@ -1879,10 +3399,15 @@ function spawnAgent(opts) {
|
|
|
1879
3399
|
opts.logPath,
|
|
1880
3400
|
{ flags: "w" }
|
|
1881
3401
|
);
|
|
3402
|
+
const env = isClaude && opts.preferSubagent ? {
|
|
3403
|
+
...process.env,
|
|
3404
|
+
CLAUDE_CODE_SUBAGENT_MODEL: opts.preferSubagent
|
|
3405
|
+
} : void 0;
|
|
1882
3406
|
const proc = spawn2(opts.agentCmd, args, {
|
|
1883
3407
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1884
3408
|
cwd: opts.cwd,
|
|
1885
|
-
detached: true
|
|
3409
|
+
detached: true,
|
|
3410
|
+
...env ? { env } : {}
|
|
1886
3411
|
});
|
|
1887
3412
|
if (proc.pid !== void 0) runningAgents.add(proc.pid);
|
|
1888
3413
|
proc.stdout.pipe(logStream, { end: false });
|
|
@@ -1900,10 +3425,26 @@ function spawnAgent(opts) {
|
|
|
1900
3425
|
}
|
|
1901
3426
|
}
|
|
1902
3427
|
__name(collectCompleted, "collectCompleted");
|
|
3428
|
+
let streamStats = emptyStreamStats();
|
|
3429
|
+
function formatClaudeLine(line) {
|
|
3430
|
+
let obj;
|
|
3431
|
+
try {
|
|
3432
|
+
obj = JSON.parse(line);
|
|
3433
|
+
} catch {
|
|
3434
|
+
return line;
|
|
3435
|
+
}
|
|
3436
|
+
const next = nextStreamStats(streamStats, obj);
|
|
3437
|
+
if (next !== streamStats) {
|
|
3438
|
+
streamStats = next;
|
|
3439
|
+
opts.onStats?.(next.stats);
|
|
3440
|
+
}
|
|
3441
|
+
return formatStreamObject(obj);
|
|
3442
|
+
}
|
|
3443
|
+
__name(formatClaudeLine, "formatClaudeLine");
|
|
1903
3444
|
const rl = createInterface({ input: proc.stdout });
|
|
1904
3445
|
rl.on("line", (line) => {
|
|
1905
3446
|
if (isClaude) {
|
|
1906
|
-
const formatted =
|
|
3447
|
+
const formatted = formatClaudeLine(line);
|
|
1907
3448
|
if (formatted !== null) {
|
|
1908
3449
|
collectCompleted(formatted);
|
|
1909
3450
|
const output = `${prefix}${formatted}`;
|
|
@@ -1969,7 +3510,7 @@ function spawnAgent(opts) {
|
|
|
1969
3510
|
proc.stdin.write(opts.prompt);
|
|
1970
3511
|
}
|
|
1971
3512
|
proc.stdin.end();
|
|
1972
|
-
return new Promise((
|
|
3513
|
+
return new Promise((resolve3) => {
|
|
1973
3514
|
let settled = false;
|
|
1974
3515
|
let graceTimer;
|
|
1975
3516
|
function finish(exitCode) {
|
|
@@ -1995,7 +3536,12 @@ function spawnAgent(opts) {
|
|
|
1995
3536
|
rlErr.close();
|
|
1996
3537
|
logStream.end();
|
|
1997
3538
|
if (promptFile) cleanupPromptFile(promptFile);
|
|
1998
|
-
|
|
3539
|
+
resolve3({
|
|
3540
|
+
exitCode,
|
|
3541
|
+
completedItems,
|
|
3542
|
+
timedOut,
|
|
3543
|
+
...isClaude ? streamStats.stats : {}
|
|
3544
|
+
});
|
|
1999
3545
|
}
|
|
2000
3546
|
__name(finish, "finish");
|
|
2001
3547
|
proc.on("exit", (code) => {
|
|
@@ -2019,22 +3565,27 @@ function spawnAgent(opts) {
|
|
|
2019
3565
|
}
|
|
2020
3566
|
rl.close();
|
|
2021
3567
|
rlErr.close();
|
|
2022
|
-
|
|
3568
|
+
const msg = "agent process error: " + formatErrorMessage(err);
|
|
3569
|
+
logStream.write(`nightralph: ${msg}
|
|
3570
|
+
`);
|
|
2023
3571
|
if (promptFile) cleanupPromptFile(promptFile);
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
3572
|
+
if (opts.onLine) {
|
|
3573
|
+
opts.onLine(`${prefix}${msg}`);
|
|
3574
|
+
} else {
|
|
3575
|
+
console.error(`${prefix}${msg}`);
|
|
3576
|
+
}
|
|
3577
|
+
logStream.end(() => {
|
|
3578
|
+
resolve3({
|
|
3579
|
+
exitCode: 1,
|
|
3580
|
+
completedItems,
|
|
3581
|
+
timedOut
|
|
3582
|
+
});
|
|
2032
3583
|
});
|
|
2033
3584
|
});
|
|
2034
3585
|
});
|
|
2035
3586
|
}
|
|
2036
3587
|
__name(spawnAgent, "spawnAgent");
|
|
2037
|
-
function dryRun(tickets, specBody, testCmd) {
|
|
3588
|
+
function dryRun(tickets, specBody, testCmd, preferSubagent) {
|
|
2038
3589
|
listTickets(tickets);
|
|
2039
3590
|
console.log("\nSimulated wave order:");
|
|
2040
3591
|
for (const { wave, ready } of simulateWaves(tickets)) {
|
|
@@ -2048,6 +3599,9 @@ function dryRun(tickets, specBody, testCmd) {
|
|
|
2048
3599
|
"\nDeadlocked tickets: " + stuck.map((t) => t.filename).join(", ")
|
|
2049
3600
|
);
|
|
2050
3601
|
}
|
|
3602
|
+
console.log(`
|
|
3603
|
+
Test command: ${testCmd || "(none)"}`);
|
|
3604
|
+
console.log(`Prefer subagent: ${preferSubagent || "(none)"}`);
|
|
2051
3605
|
const firstReady = findReadyTickets(tickets);
|
|
2052
3606
|
if (firstReady.length > 0) {
|
|
2053
3607
|
console.log(
|
|
@@ -2059,7 +3613,7 @@ Prompt for first ready ticket (${firstReady[0].filename}):
|
|
|
2059
3613
|
renderPrompt(
|
|
2060
3614
|
specBody,
|
|
2061
3615
|
firstReady[0],
|
|
2062
|
-
{ testCmd }
|
|
3616
|
+
{ testCmd, preferSubagent }
|
|
2063
3617
|
)
|
|
2064
3618
|
);
|
|
2065
3619
|
}
|
|
@@ -2103,18 +3657,28 @@ Prompt for first ready ticket (${firstReady[0].filename}):
|
|
|
2103
3657
|
renderPrompt(
|
|
2104
3658
|
opts.specBody,
|
|
2105
3659
|
firstReady[0],
|
|
2106
|
-
{
|
|
3660
|
+
{
|
|
3661
|
+
testCmd: opts.testCmd,
|
|
3662
|
+
installCmd: opts.installCmd,
|
|
3663
|
+
preferSubagent: opts.preferSubagent
|
|
3664
|
+
}
|
|
2107
3665
|
)
|
|
2108
3666
|
);
|
|
2109
3667
|
}
|
|
2110
3668
|
const strategyDesc = opts.conflictStrategy === "respawn" ? "respawn (re-run agent on conflict)" : "stop (stop merging on conflict)";
|
|
2111
3669
|
console.log(
|
|
2112
3670
|
`
|
|
2113
|
-
Test command: ${opts.testCmd}`
|
|
3671
|
+
Test command: ${opts.testCmd || "(none)"}`
|
|
3672
|
+
);
|
|
3673
|
+
console.log(
|
|
3674
|
+
`Install: ${opts.installCmd || "(none)"}`
|
|
2114
3675
|
);
|
|
2115
3676
|
console.log(
|
|
2116
3677
|
`Effort: ${opts.effort ?? "(provider default)"}`
|
|
2117
3678
|
);
|
|
3679
|
+
console.log(
|
|
3680
|
+
`Prefer subagent: ${opts.preferSubagent || "(none)"}`
|
|
3681
|
+
);
|
|
2118
3682
|
console.log(
|
|
2119
3683
|
`Retries: ${opts.retries ?? DEFAULT_RETRIES}`
|
|
2120
3684
|
);
|
|
@@ -2131,7 +3695,34 @@ Merge strategy: ${strategyDesc}`
|
|
|
2131
3695
|
console.log(`Progress file: ${opts.progressPath}`);
|
|
2132
3696
|
}
|
|
2133
3697
|
__name(dryRunIsolated, "dryRunIsolated");
|
|
3698
|
+
async function runInstall(opts) {
|
|
3699
|
+
const result = await runTestCmd({
|
|
3700
|
+
cmd: opts.installCmd,
|
|
3701
|
+
cwd: opts.worktreePath,
|
|
3702
|
+
timeout: opts.timeout,
|
|
3703
|
+
registry: opts.registry
|
|
3704
|
+
});
|
|
3705
|
+
const installLogPath = attemptLogPath(
|
|
3706
|
+
opts.logsDir,
|
|
3707
|
+
opts.ticket,
|
|
3708
|
+
opts.attempt ?? 1,
|
|
3709
|
+
".install.log"
|
|
3710
|
+
);
|
|
3711
|
+
writeFileSync5(installLogPath, result.output);
|
|
3712
|
+
if (result.exitCode !== 0) {
|
|
3713
|
+
opts.log(
|
|
3714
|
+
` ${opts.ticket.filename}: install failed (exit ${result.exitCode}); see ${installLogPath}`
|
|
3715
|
+
);
|
|
3716
|
+
return false;
|
|
3717
|
+
}
|
|
3718
|
+
return true;
|
|
3719
|
+
}
|
|
3720
|
+
__name(runInstall, "runInstall");
|
|
2134
3721
|
async function runTestGate(opts) {
|
|
3722
|
+
if (opts.testCmd === "") {
|
|
3723
|
+
opts.log(` ${opts.ticket.filename}: no test gate`);
|
|
3724
|
+
return true;
|
|
3725
|
+
}
|
|
2135
3726
|
const result = await runTestCmd({
|
|
2136
3727
|
cmd: opts.testCmd,
|
|
2137
3728
|
cwd: opts.worktreePath,
|
|
@@ -2144,7 +3735,7 @@ async function runTestGate(opts) {
|
|
|
2144
3735
|
opts.attempt ?? 1,
|
|
2145
3736
|
".test.log"
|
|
2146
3737
|
);
|
|
2147
|
-
|
|
3738
|
+
writeFileSync5(testLogPath, result.output);
|
|
2148
3739
|
if (result.exitCode !== 0) {
|
|
2149
3740
|
opts.log(
|
|
2150
3741
|
` ${opts.ticket.filename}: tests failed (exit ${result.exitCode}); see ${testLogPath}`
|
|
@@ -2173,11 +3764,27 @@ async function respawnAndRetryMerge(opts) {
|
|
|
2173
3764
|
} else {
|
|
2174
3765
|
console.log(msg1);
|
|
2175
3766
|
}
|
|
3767
|
+
if (opts.installCmd) {
|
|
3768
|
+
const installed = await runInstall({
|
|
3769
|
+
installCmd: opts.installCmd,
|
|
3770
|
+
ticket: opts.ticket,
|
|
3771
|
+
worktreePath: opts.worktreePath,
|
|
3772
|
+
timeout: opts.timeout,
|
|
3773
|
+
logsDir: opts.logsDir,
|
|
3774
|
+
log,
|
|
3775
|
+
registry: runningAgents
|
|
3776
|
+
});
|
|
3777
|
+
if (!installed) return false;
|
|
3778
|
+
}
|
|
2176
3779
|
const prompt = renderMergePrompt(
|
|
2177
3780
|
opts.specBody,
|
|
2178
|
-
opts.ticket
|
|
3781
|
+
opts.ticket,
|
|
3782
|
+
{
|
|
3783
|
+
installCmd: opts.installCmd,
|
|
3784
|
+
preferSubagent: opts.preferSubagent
|
|
3785
|
+
}
|
|
2179
3786
|
);
|
|
2180
|
-
const
|
|
3787
|
+
const logPath2 = join6(
|
|
2181
3788
|
opts.logsDir,
|
|
2182
3789
|
opts.ticket.filename.replace(/\.md$/, ".respawn.log")
|
|
2183
3790
|
);
|
|
@@ -2190,12 +3797,14 @@ async function respawnAndRetryMerge(opts) {
|
|
|
2190
3797
|
agentCmd: opts.agentCmd,
|
|
2191
3798
|
model: opts.model,
|
|
2192
3799
|
timeout: opts.timeout,
|
|
2193
|
-
logPath,
|
|
3800
|
+
logPath: logPath2,
|
|
2194
3801
|
cwd: opts.worktreePath,
|
|
2195
3802
|
label,
|
|
2196
3803
|
maxTurns: opts.maxTurns,
|
|
3804
|
+
preferSubagent: opts.preferSubagent,
|
|
2197
3805
|
onLine: opts.onLine
|
|
2198
3806
|
});
|
|
3807
|
+
opts.onAgentRun?.(result);
|
|
2199
3808
|
if (result.exitCode !== 0) {
|
|
2200
3809
|
const msg2 = ` ${opts.ticket.filename}: respawned agent exited ${result.exitCode}`;
|
|
2201
3810
|
if (opts.onLine) {
|
|
@@ -2274,20 +3883,43 @@ async function runPool(items, limit, fn) {
|
|
|
2274
3883
|
return results;
|
|
2275
3884
|
}
|
|
2276
3885
|
__name(runPool, "runPool");
|
|
2277
|
-
function retryDelayFor(base, attempt) {
|
|
2278
|
-
return base * 2 ** (attempt - 2);
|
|
2279
|
-
}
|
|
2280
|
-
__name(retryDelayFor, "retryDelayFor");
|
|
2281
|
-
function sleep(ms) {
|
|
2282
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2283
|
-
}
|
|
2284
|
-
__name(sleep, "sleep");
|
|
2285
3886
|
function attemptLogPath(logsDir, ticket, attempt, ext) {
|
|
2286
3887
|
const stem = ticket.filename.replace(/\.md$/, "");
|
|
2287
3888
|
const suffix = attempt > 1 ? `.attempt${attempt}` : "";
|
|
2288
|
-
return
|
|
3889
|
+
return join6(logsDir, `${stem}${suffix}${ext}`);
|
|
2289
3890
|
}
|
|
2290
3891
|
__name(attemptLogPath, "attemptLogPath");
|
|
3892
|
+
function historyFor(learningsPath, ticket) {
|
|
3893
|
+
const featureDir = dirname4(dirname4(ticket.filepath));
|
|
3894
|
+
const scratchDir = ticket.feature ? join6(featureDir, ...ticket.feature.split("/").map(() => "..")) : dirname4(featureDir);
|
|
3895
|
+
const others = new Set(
|
|
3896
|
+
ticket.blockers.map((r) => r.feature).filter((f) => f !== ticket.feature)
|
|
3897
|
+
);
|
|
3898
|
+
const entries = [
|
|
3899
|
+
...readEntries(learningsPath),
|
|
3900
|
+
...[...others].flatMap((f) => readEntries(
|
|
3901
|
+
join6(scratchDir, f, "learnings.md")
|
|
3902
|
+
))
|
|
3903
|
+
];
|
|
3904
|
+
return renderHistory(selectHistory(entries, {
|
|
3905
|
+
key: ticketKey(ticket.feature, ticket.num),
|
|
3906
|
+
blockerKeys: ticket.blockers.map((r) => {
|
|
3907
|
+
return ticketKey(r.feature, r.num);
|
|
3908
|
+
})
|
|
3909
|
+
}));
|
|
3910
|
+
}
|
|
3911
|
+
__name(historyFor, "historyFor");
|
|
3912
|
+
function buildHistory(learningsPath, ticket, d) {
|
|
3913
|
+
try {
|
|
3914
|
+
return historyFor(learningsPath, ticket);
|
|
3915
|
+
} catch (err) {
|
|
3916
|
+
d.log(
|
|
3917
|
+
` ${ticket.filename}: could not read learnings: ${formatErrorMessage(err)}`
|
|
3918
|
+
);
|
|
3919
|
+
return "";
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
__name(buildHistory, "buildHistory");
|
|
2291
3923
|
async function runTicketAttempt(opts) {
|
|
2292
3924
|
const { ticket, d } = opts;
|
|
2293
3925
|
let worktree;
|
|
@@ -2301,11 +3933,43 @@ async function runTicketAttempt(opts) {
|
|
|
2301
3933
|
} catch (error) {
|
|
2302
3934
|
return { kind: "worktree-error", error };
|
|
2303
3935
|
}
|
|
2304
|
-
d.setTicketStatus(
|
|
2305
|
-
ticket.num,
|
|
2306
|
-
"in-progress",
|
|
2307
|
-
ticket.feature
|
|
2308
|
-
);
|
|
3936
|
+
d.setTicketStatus(
|
|
3937
|
+
ticket.num,
|
|
3938
|
+
"in-progress",
|
|
3939
|
+
ticket.feature
|
|
3940
|
+
);
|
|
3941
|
+
if (opts.installCmd) {
|
|
3942
|
+
const installed = await runInstall({
|
|
3943
|
+
installCmd: opts.installCmd,
|
|
3944
|
+
ticket,
|
|
3945
|
+
worktreePath: worktree.worktreePath,
|
|
3946
|
+
timeout: opts.timeout,
|
|
3947
|
+
logsDir: opts.logsDir,
|
|
3948
|
+
attempt: opts.attempt,
|
|
3949
|
+
log: /* @__PURE__ */ __name((msg) => d.log(msg), "log"),
|
|
3950
|
+
registry: runningAgents
|
|
3951
|
+
});
|
|
3952
|
+
if (!installed) {
|
|
3953
|
+
await removeWorktreeQuietly(
|
|
3954
|
+
worktree.worktreePath,
|
|
3955
|
+
d
|
|
3956
|
+
);
|
|
3957
|
+
return {
|
|
3958
|
+
kind: "failed",
|
|
3959
|
+
reason: "install",
|
|
3960
|
+
branchName: worktree.branchName,
|
|
3961
|
+
exitCode: 1,
|
|
3962
|
+
completedItems: []
|
|
3963
|
+
};
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
try {
|
|
3967
|
+
ensureNotesIgnored(worktree.worktreePath);
|
|
3968
|
+
} catch (err) {
|
|
3969
|
+
d.log(
|
|
3970
|
+
` ${ticket.filename}: could not set up the notes directory: ${formatErrorMessage(err)}`
|
|
3971
|
+
);
|
|
3972
|
+
}
|
|
2309
3973
|
const ticketSpec = lookupSpec(
|
|
2310
3974
|
opts.specBodies,
|
|
2311
3975
|
opts.specBody,
|
|
@@ -2315,7 +3979,18 @@ async function runTicketAttempt(opts) {
|
|
|
2315
3979
|
prompt: renderPrompt(
|
|
2316
3980
|
ticketSpec,
|
|
2317
3981
|
ticket,
|
|
2318
|
-
{
|
|
3982
|
+
{
|
|
3983
|
+
testCmd: opts.testCmd,
|
|
3984
|
+
installCmd: opts.installCmd,
|
|
3985
|
+
// Rebuilt every attempt, so a retry sees the
|
|
3986
|
+
// entry its previous attempt just wrote
|
|
3987
|
+
history: buildHistory(
|
|
3988
|
+
opts.learningsPath,
|
|
3989
|
+
ticket,
|
|
3990
|
+
d
|
|
3991
|
+
),
|
|
3992
|
+
preferSubagent: opts.preferSubagent
|
|
3993
|
+
}
|
|
2319
3994
|
),
|
|
2320
3995
|
agentCmd: opts.agentCmd,
|
|
2321
3996
|
model: opts.model,
|
|
@@ -2330,8 +4005,27 @@ async function runTicketAttempt(opts) {
|
|
|
2330
4005
|
label: ticket.filename.replace(/\.md$/, ""),
|
|
2331
4006
|
maxTurns: opts.maxTurns,
|
|
2332
4007
|
effort: opts.effort,
|
|
4008
|
+
preferSubagent: opts.preferSubagent,
|
|
4009
|
+
onStats: opts.onStats,
|
|
2333
4010
|
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
2334
4011
|
});
|
|
4012
|
+
opts.onAgentRun?.(agentResult);
|
|
4013
|
+
let notes = null;
|
|
4014
|
+
try {
|
|
4015
|
+
notes = collectNotes(worktree.worktreePath);
|
|
4016
|
+
} catch (err) {
|
|
4017
|
+
d.log(
|
|
4018
|
+
` ${ticket.filename}: could not collect notes: ${formatErrorMessage(err)}`
|
|
4019
|
+
);
|
|
4020
|
+
}
|
|
4021
|
+
const agentRun = {
|
|
4022
|
+
exitCode: agentResult.exitCode,
|
|
4023
|
+
completedItems: agentResult.completedItems,
|
|
4024
|
+
timedOut: agentResult.timedOut,
|
|
4025
|
+
notes,
|
|
4026
|
+
...agentResult.subagents === void 0 ? {} : { subagents: agentResult.subagents },
|
|
4027
|
+
...agentResult.costUsd === void 0 ? {} : { costUsd: agentResult.costUsd }
|
|
4028
|
+
};
|
|
2335
4029
|
if (agentResult.timedOut) {
|
|
2336
4030
|
d.log(
|
|
2337
4031
|
` ${ticket.filename}: timed out; keeping the work it committed`
|
|
@@ -2346,7 +4040,8 @@ async function runTicketAttempt(opts) {
|
|
|
2346
4040
|
reason: "exit",
|
|
2347
4041
|
branchName: worktree.branchName,
|
|
2348
4042
|
exitCode: agentResult.exitCode,
|
|
2349
|
-
completedItems: agentResult.completedItems
|
|
4043
|
+
completedItems: agentResult.completedItems,
|
|
4044
|
+
agentRun
|
|
2350
4045
|
};
|
|
2351
4046
|
}
|
|
2352
4047
|
if (isDirty(worktree.worktreePath)) {
|
|
@@ -2369,7 +4064,8 @@ async function runTicketAttempt(opts) {
|
|
|
2369
4064
|
return {
|
|
2370
4065
|
kind: "no-commits",
|
|
2371
4066
|
worktreePath: worktree.worktreePath,
|
|
2372
|
-
branchName: worktree.branchName
|
|
4067
|
+
branchName: worktree.branchName,
|
|
4068
|
+
agentRun
|
|
2373
4069
|
};
|
|
2374
4070
|
}
|
|
2375
4071
|
const passed = await runTestGate({
|
|
@@ -2392,73 +4088,189 @@ async function runTicketAttempt(opts) {
|
|
|
2392
4088
|
reason: "tests",
|
|
2393
4089
|
branchName: worktree.branchName,
|
|
2394
4090
|
exitCode: 1,
|
|
2395
|
-
completedItems: agentResult.completedItems
|
|
4091
|
+
completedItems: agentResult.completedItems,
|
|
4092
|
+
agentRun
|
|
2396
4093
|
};
|
|
2397
4094
|
}
|
|
2398
4095
|
return {
|
|
2399
4096
|
kind: "ready",
|
|
2400
4097
|
worktreePath: worktree.worktreePath,
|
|
2401
4098
|
branchName: worktree.branchName,
|
|
2402
|
-
completedItems: agentResult.completedItems
|
|
4099
|
+
completedItems: agentResult.completedItems,
|
|
4100
|
+
agentRun
|
|
2403
4101
|
};
|
|
2404
4102
|
}
|
|
2405
4103
|
__name(runTicketAttempt, "runTicketAttempt");
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
4104
|
+
function readLogTail(path) {
|
|
4105
|
+
try {
|
|
4106
|
+
return tailLines(readFileSync6(path, "utf8"), LOG_TAIL_LINES);
|
|
4107
|
+
} catch {
|
|
4108
|
+
return "";
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
__name(readLogTail, "readLogTail");
|
|
4112
|
+
function attemptEntry(opts) {
|
|
4113
|
+
const { logsDir, ticket, attempt, effort, outcome } = opts;
|
|
4114
|
+
const base = {
|
|
4115
|
+
key: ticketKey(ticket.feature, ticket.num),
|
|
4116
|
+
label: ticketLabel(ticket),
|
|
4117
|
+
attempt,
|
|
4118
|
+
now: /* @__PURE__ */ new Date()
|
|
2416
4119
|
};
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
4120
|
+
const logFor = /* @__PURE__ */ __name((ext) => {
|
|
4121
|
+
const path = attemptLogPath(logsDir, ticket, attempt, ext);
|
|
4122
|
+
return { logName: basename5(path), logTail: readLogTail(path) };
|
|
4123
|
+
}, "logFor");
|
|
4124
|
+
switch (outcome.kind) {
|
|
4125
|
+
case "ready":
|
|
4126
|
+
return {
|
|
4127
|
+
...base,
|
|
4128
|
+
outcome: "done",
|
|
4129
|
+
run: outcome.agentRun,
|
|
4130
|
+
effort
|
|
4131
|
+
};
|
|
4132
|
+
case "failed":
|
|
4133
|
+
switch (outcome.reason) {
|
|
4134
|
+
case "exit":
|
|
4135
|
+
return {
|
|
4136
|
+
...base,
|
|
4137
|
+
outcome: "exit",
|
|
4138
|
+
run: outcome.agentRun,
|
|
4139
|
+
effort
|
|
4140
|
+
};
|
|
4141
|
+
case "tests":
|
|
4142
|
+
return {
|
|
4143
|
+
...base,
|
|
4144
|
+
outcome: "tests",
|
|
4145
|
+
run: outcome.agentRun,
|
|
4146
|
+
effort,
|
|
4147
|
+
...logFor(".test.log")
|
|
4148
|
+
};
|
|
4149
|
+
case "install":
|
|
4150
|
+
return {
|
|
4151
|
+
...base,
|
|
4152
|
+
outcome: "install",
|
|
4153
|
+
...logFor(".install.log")
|
|
4154
|
+
};
|
|
4155
|
+
default: {
|
|
4156
|
+
const _exhaustive = outcome;
|
|
4157
|
+
return _exhaustive;
|
|
4158
|
+
}
|
|
2426
4159
|
}
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
4160
|
+
case "no-commits":
|
|
4161
|
+
return {
|
|
4162
|
+
...base,
|
|
4163
|
+
outcome: "no-commits",
|
|
4164
|
+
run: outcome.agentRun,
|
|
4165
|
+
effort
|
|
4166
|
+
};
|
|
4167
|
+
case "error":
|
|
4168
|
+
case "worktree-error":
|
|
4169
|
+
return {
|
|
4170
|
+
...base,
|
|
4171
|
+
outcome: "error",
|
|
4172
|
+
error: formatErrorMessage(outcome.error)
|
|
4173
|
+
};
|
|
4174
|
+
default: {
|
|
4175
|
+
const _exhaustive = outcome;
|
|
4176
|
+
return _exhaustive;
|
|
2432
4177
|
}
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
__name(attemptEntry, "attemptEntry");
|
|
4181
|
+
function recordAttempt(opts) {
|
|
4182
|
+
try {
|
|
4183
|
+
const entry = attemptEntry(opts);
|
|
4184
|
+
if (!entry) return;
|
|
4185
|
+
appendEntry(opts.learningsPath, renderEntry(entry));
|
|
4186
|
+
} catch (err) {
|
|
4187
|
+
opts.d.log(
|
|
4188
|
+
` ${opts.ticket.filename}: could not record learnings: ${formatErrorMessage(err)}`
|
|
4189
|
+
);
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
__name(recordAttempt, "recordAttempt");
|
|
4193
|
+
async function runTicketAttempts(opts) {
|
|
4194
|
+
const { ticket, d } = opts;
|
|
4195
|
+
let prior = { subagents: 0 };
|
|
4196
|
+
const showStats = /* @__PURE__ */ __name((stats) => {
|
|
4197
|
+
d.setTicketStats(ticket.num, ticket.feature, stats);
|
|
4198
|
+
}, "showStats");
|
|
4199
|
+
return runWithRetries({
|
|
4200
|
+
maxAttempts: opts.retries + 1,
|
|
4201
|
+
retryDelay: opts.retryDelay,
|
|
4202
|
+
agentCmd: opts.agentCmd,
|
|
4203
|
+
effort: opts.effort,
|
|
4204
|
+
label: ticket.filename,
|
|
4205
|
+
log: /* @__PURE__ */ __name((msg) => d.log(msg), "log")
|
|
4206
|
+
}, async (attempt, effort) => {
|
|
4207
|
+
const record = /* @__PURE__ */ __name((o) => {
|
|
4208
|
+
recordAttempt({
|
|
4209
|
+
learningsPath: opts.learningsPath,
|
|
4210
|
+
logsDir: opts.logsDir,
|
|
4211
|
+
ticket,
|
|
4212
|
+
attempt,
|
|
4213
|
+
effort,
|
|
4214
|
+
outcome: o,
|
|
4215
|
+
d
|
|
4216
|
+
});
|
|
4217
|
+
}, "record");
|
|
4218
|
+
let outcome;
|
|
2433
4219
|
try {
|
|
2434
4220
|
outcome = await runTicketAttempt({
|
|
2435
4221
|
...opts,
|
|
2436
4222
|
attempt,
|
|
2437
|
-
effort
|
|
4223
|
+
effort,
|
|
4224
|
+
onStats: /* @__PURE__ */ __name((stats) => showStats(addStats(prior, stats)), "onStats"),
|
|
4225
|
+
onAgentRun: /* @__PURE__ */ __name((result) => {
|
|
4226
|
+
prior = addStats(prior, result);
|
|
4227
|
+
opts.onAgentRun?.(result);
|
|
4228
|
+
}, "onAgentRun")
|
|
2438
4229
|
});
|
|
2439
4230
|
} catch (error) {
|
|
2440
4231
|
d.log(
|
|
2441
4232
|
` ${ticket.filename}: attempt ${attempt} threw: ${formatErrorMessage(error)}`
|
|
2442
4233
|
);
|
|
2443
|
-
|
|
4234
|
+
const errorOutcome = { kind: "error", error };
|
|
4235
|
+
record(errorOutcome);
|
|
4236
|
+
return { result: errorOutcome, stop: true };
|
|
2444
4237
|
}
|
|
4238
|
+
record(outcome);
|
|
2445
4239
|
if (outcome.kind === "ready") {
|
|
2446
4240
|
d.setTicketStatus(
|
|
2447
4241
|
ticket.num,
|
|
2448
4242
|
"awaiting-merge",
|
|
2449
4243
|
ticket.feature
|
|
2450
4244
|
);
|
|
2451
|
-
return outcome;
|
|
4245
|
+
return { result: outcome, stop: true };
|
|
2452
4246
|
}
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
4247
|
+
return {
|
|
4248
|
+
result: outcome,
|
|
4249
|
+
stop: outcome.kind === "worktree-error"
|
|
4250
|
+
};
|
|
4251
|
+
});
|
|
2456
4252
|
}
|
|
2457
4253
|
__name(runTicketAttempts, "runTicketAttempts");
|
|
4254
|
+
function failureNote(reason) {
|
|
4255
|
+
if (reason === "tests") return "tests failed";
|
|
4256
|
+
if (reason === "install") return "install failed";
|
|
4257
|
+
return "failed";
|
|
4258
|
+
}
|
|
4259
|
+
__name(failureNote, "failureNote");
|
|
4260
|
+
function isRegularFile(path) {
|
|
4261
|
+
try {
|
|
4262
|
+
return statSync(path).isFile();
|
|
4263
|
+
} catch {
|
|
4264
|
+
return false;
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
__name(isRegularFile, "isRegularFile");
|
|
2458
4268
|
async function saveProgress(repoRoot, progressPath, progress, message, d) {
|
|
2459
4269
|
writeProgress(progressPath, progress);
|
|
4270
|
+
const learningsPath = learningsPathFor(progressPath);
|
|
4271
|
+
const paths = isRegularFile(learningsPath) ? [progressPath, learningsPath] : progressPath;
|
|
2460
4272
|
try {
|
|
2461
|
-
await commitProgress(repoRoot,
|
|
4273
|
+
await commitProgress(repoRoot, paths, message);
|
|
2462
4274
|
} catch (err) {
|
|
2463
4275
|
d.log(
|
|
2464
4276
|
" Failed to commit progress: " + formatErrorMessage(err)
|
|
@@ -2471,15 +4283,77 @@ function indentFilenames(tickets) {
|
|
|
2471
4283
|
return tickets.map((t) => ` ${t.filename}`).join("\n");
|
|
2472
4284
|
}
|
|
2473
4285
|
__name(indentFilenames, "indentFilenames");
|
|
2474
|
-
function
|
|
4286
|
+
function explainOpen(tickets, failedKeys, warnings) {
|
|
2475
4287
|
const remaining = tickets.filter(
|
|
2476
4288
|
(t) => t.status === "ready-for-agent"
|
|
2477
4289
|
);
|
|
4290
|
+
const open = remaining.filter(
|
|
4291
|
+
(t) => !failedKeys.has(ticketKey(t.feature, t.num))
|
|
4292
|
+
);
|
|
4293
|
+
const dead = findBlockedTickets(tickets, failedKeys);
|
|
4294
|
+
const deadRefs = /* @__PURE__ */ __name((t) => dead.get(ticketKey(t.feature, t.num)) ?? [], "deadRefs");
|
|
4295
|
+
const lines = [];
|
|
2478
4296
|
const human = tickets.filter((t) => t.status === HUMAN_STATUS);
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
4297
|
+
if (human.length > 0) {
|
|
4298
|
+
const humanKeys = new Set(
|
|
4299
|
+
human.map((t) => ticketKey(t.feature, t.num))
|
|
4300
|
+
);
|
|
4301
|
+
const blockedByHuman = open.filter((t) => deadRefs(t).some(
|
|
4302
|
+
(r) => humanKeys.has(ticketKey(r.feature, r.num))
|
|
4303
|
+
));
|
|
4304
|
+
const noun = human.length === 1 ? "ticket is" : "tickets are";
|
|
4305
|
+
lines.push(
|
|
4306
|
+
"",
|
|
4307
|
+
(remaining.length === 0 && failedKeys.size === 0 ? `All remaining ${noun} marked ${HUMAN_STATUS}. ` : `${human.length} ${noun} marked ${HUMAN_STATUS}. `) + "nightralph only runs ready-for-agent tickets, so an agent cannot finish these:",
|
|
4308
|
+
indentFilenames(human)
|
|
4309
|
+
);
|
|
4310
|
+
if (blockedByHuman.length > 0) {
|
|
4311
|
+
const n = blockedByHuman.length;
|
|
4312
|
+
lines.push(
|
|
4313
|
+
`${n} ready-for-agent ticket${n === 1 ? " is" : "s are"} blocked until they are done:`,
|
|
4314
|
+
indentFilenames(blockedByHuman)
|
|
4315
|
+
);
|
|
4316
|
+
}
|
|
4317
|
+
lines.push(
|
|
4318
|
+
"Do that work by hand, then set each ticket's **Status:** to done" + (blockedByHuman.length > 0 ? " and run nightralph again." : ".")
|
|
4319
|
+
);
|
|
4320
|
+
}
|
|
4321
|
+
const blockers = [];
|
|
4322
|
+
const outside = open.filter(
|
|
4323
|
+
(t) => deadRefs(t).some((r) => r.where === "pending")
|
|
4324
|
+
);
|
|
4325
|
+
if (outside.length > 0) {
|
|
4326
|
+
const n = outside.length;
|
|
4327
|
+
blockers.push(
|
|
4328
|
+
`${n} ready-for-agent ticket${n === 1 ? " waits" : "s wait"} on tickets outside this run. Select those features too, or finish them first:`,
|
|
4329
|
+
...outside.map((t) => ` ${ticketLabel(toRef(t))} waits on ` + deadRefs(t).filter((r) => r.where === "pending").map(blockerLabel).join(", "))
|
|
4330
|
+
);
|
|
4331
|
+
}
|
|
4332
|
+
const missing = open.flatMap((t) => t.blockers.filter((r) => r.where === "missing").map((r) => ` ${ticketLabel(toRef(t))}: ${blockerLabel(r)}`));
|
|
4333
|
+
if (missing.length > 0) {
|
|
4334
|
+
const n = missing.length;
|
|
4335
|
+
blockers.push(
|
|
4336
|
+
`${n} blocker reference${n === 1 ? "" : "s"} match no ticket. Fix the **Blocked by:** line:`,
|
|
4337
|
+
...missing
|
|
4338
|
+
);
|
|
4339
|
+
}
|
|
4340
|
+
const typos = warnings.filter((w) => w.kind === "typo");
|
|
4341
|
+
if (typos.length > 0) {
|
|
4342
|
+
const n = typos.length;
|
|
4343
|
+
blockers.push(
|
|
4344
|
+
`${n} word${n === 1 ? "" : "s"} in a **Blocked by:** line look${n === 1 ? "s" : ""} like a feature name but match no .scratch/<name>/issues directory:`,
|
|
4345
|
+
...typos.map((w) => ` ${w.ticket}: ${w.text}`)
|
|
4346
|
+
);
|
|
4347
|
+
}
|
|
4348
|
+
if (blockers.length > 0) lines.push("", ...blockers);
|
|
4349
|
+
return lines;
|
|
4350
|
+
}
|
|
4351
|
+
__name(explainOpen, "explainOpen");
|
|
4352
|
+
function renderFinishSummary(tickets, totalCompleted, failedKeys, warnings = []) {
|
|
4353
|
+
const remaining = tickets.filter(
|
|
4354
|
+
(t) => t.status === "ready-for-agent"
|
|
2482
4355
|
);
|
|
4356
|
+
const human = tickets.filter((t) => t.status === HUMAN_STATUS);
|
|
2483
4357
|
const counts = `Finished: ${totalCompleted} completed` + (remaining.length > 0 ? `, ${remaining.length} remaining` : "") + (failedKeys.size > 0 ? `, ${failedKeys.size} failed` : "") + (human.length > 0 ? `, ${human.length} need a human` : "");
|
|
2484
4358
|
const lines = [counts];
|
|
2485
4359
|
const features = new Set(
|
|
@@ -2508,26 +4382,54 @@ function renderFinishSummary(tickets, totalCompleted, failedKeys) {
|
|
|
2508
4382
|
lines.push(detail);
|
|
2509
4383
|
}
|
|
2510
4384
|
}
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
4385
|
+
lines.push(...explainOpen(tickets, failedKeys, warnings));
|
|
4386
|
+
return lines.join("\n");
|
|
4387
|
+
}
|
|
4388
|
+
__name(renderFinishSummary, "renderFinishSummary");
|
|
4389
|
+
function nothingReady(tickets) {
|
|
4390
|
+
return findReadyTickets(tickets).length === 0 && tickets.some((t) => t.status === "ready-for-agent");
|
|
4391
|
+
}
|
|
4392
|
+
__name(nothingReady, "nothingReady");
|
|
4393
|
+
function renderNothingReady(tickets, warnings) {
|
|
4394
|
+
const lines = [
|
|
4395
|
+
"Nothing to run: every ready-for-agent ticket is waiting on a blocker.",
|
|
4396
|
+
...explainOpen(tickets, /* @__PURE__ */ new Set(), warnings)
|
|
4397
|
+
];
|
|
4398
|
+
const dead = findBlockedTickets(tickets, /* @__PURE__ */ new Set());
|
|
4399
|
+
const byKey = new Map(
|
|
4400
|
+
tickets.map((t) => [ticketKey(t.feature, t.num), t])
|
|
2517
4401
|
);
|
|
2518
|
-
|
|
2519
|
-
|
|
4402
|
+
const other = [];
|
|
4403
|
+
for (const [key, refs] of dead) {
|
|
4404
|
+
const stuck = refs.flatMap((r) => {
|
|
4405
|
+
const b = byKey.get(ticketKey(r.feature, r.num));
|
|
4406
|
+
return b && b.status !== HUMAN_STATUS ? [`${ticketLabel(toRef(b))} (${b.status})`] : [];
|
|
4407
|
+
});
|
|
4408
|
+
const t = byKey.get(key);
|
|
4409
|
+
if (t && stuck.length > 0) {
|
|
4410
|
+
other.push(
|
|
4411
|
+
` ${ticketLabel(toRef(t))} waits on ` + stuck.join(", ")
|
|
4412
|
+
);
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
if (other.length > 0) {
|
|
2520
4416
|
lines.push(
|
|
2521
|
-
|
|
2522
|
-
|
|
4417
|
+
"",
|
|
4418
|
+
"These tickets wait on tickets that are not ready-for-agent:",
|
|
4419
|
+
...other,
|
|
4420
|
+
"Set those tickets to ready-for-agent or done, or fix the **Blocked by:** lines."
|
|
4421
|
+
);
|
|
4422
|
+
}
|
|
4423
|
+
const cycle = detectDeadlocks(tickets).filter((t) => !dead.has(ticketKey(t.feature, t.num)));
|
|
4424
|
+
if (cycle.length > 0) {
|
|
4425
|
+
lines.push(
|
|
4426
|
+
"",
|
|
4427
|
+
"Deadlocked tickets (waiting on each other): " + cycle.map((t) => ticketLabel(toRef(t))).join(", ") + ". Fix their **Blocked by:** lines."
|
|
2523
4428
|
);
|
|
2524
4429
|
}
|
|
2525
|
-
lines.push(
|
|
2526
|
-
"Do that work by hand, then set each ticket's **Status:** to done" + (blockedByHuman.length > 0 ? " and run nightralph again." : ".")
|
|
2527
|
-
);
|
|
2528
4430
|
return lines.join("\n");
|
|
2529
4431
|
}
|
|
2530
|
-
__name(
|
|
4432
|
+
__name(renderNothingReady, "renderNothingReady");
|
|
2531
4433
|
function sessionTickets(tickets) {
|
|
2532
4434
|
return [...tickets].filter((t) => t.status === "ready-for-agent").sort((a, b) => a.feature.localeCompare(b.feature) || a.num - b.num);
|
|
2533
4435
|
}
|
|
@@ -2546,7 +4448,10 @@ function reportBlocked(d, tickets, failedKeys) {
|
|
|
2546
4448
|
)) {
|
|
2547
4449
|
const ticket = byKey.get(key);
|
|
2548
4450
|
if (!ticket) continue;
|
|
2549
|
-
const labels = dead.map((
|
|
4451
|
+
const labels = dead.map((ref) => {
|
|
4452
|
+
const t = byKey.get(ticketKey(ref.feature, ref.num));
|
|
4453
|
+
return t ? ticketLabel(toRef(t)) : blockerLabel(ref);
|
|
4454
|
+
});
|
|
2550
4455
|
d.setBlocked(ticket.num, ticket.feature, labels);
|
|
2551
4456
|
}
|
|
2552
4457
|
}
|
|
@@ -2558,13 +4463,13 @@ function runProgress(tickets) {
|
|
|
2558
4463
|
};
|
|
2559
4464
|
}
|
|
2560
4465
|
__name(runProgress, "runProgress");
|
|
2561
|
-
async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, testCmd, display, maxTurns, effort, retries, retryDelay, concurrency) {
|
|
2562
|
-
|
|
4466
|
+
async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, testCmd, display, maxTurns, effort, retries, retryDelay, concurrency, warnings = [], preferSubagent) {
|
|
4467
|
+
mkdirSync3(logsDir, { recursive: true });
|
|
2563
4468
|
const d = display ?? createDisplay();
|
|
2564
4469
|
const maxAttempts = (retries ?? DEFAULT_RETRIES) + 1;
|
|
2565
4470
|
const delayBase = retryDelay ?? 0;
|
|
2566
4471
|
const poolSize = concurrency ?? DEFAULT_CONCURRENCY;
|
|
2567
|
-
const providerName =
|
|
4472
|
+
const providerName = basename5(agentCmd);
|
|
2568
4473
|
const isKnown = providerName in EFFORT_LADDERS;
|
|
2569
4474
|
let totalWaves = 0;
|
|
2570
4475
|
for (const _ of simulateWaves(tickets)) totalWaves++;
|
|
@@ -2572,6 +4477,7 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
2572
4477
|
let waveNum = 0;
|
|
2573
4478
|
const failedKeys = /* @__PURE__ */ new Set();
|
|
2574
4479
|
const session = sessionTickets(tickets);
|
|
4480
|
+
let runStats = emptyRunStats();
|
|
2575
4481
|
while (true) {
|
|
2576
4482
|
if (waveNum > 0) reportBlocked(d, tickets, failedKeys);
|
|
2577
4483
|
const ready = findReadyTickets(tickets).filter(
|
|
@@ -2602,7 +4508,7 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
2602
4508
|
const prompt = renderPrompt(
|
|
2603
4509
|
specBody,
|
|
2604
4510
|
t,
|
|
2605
|
-
{ testCmd }
|
|
4511
|
+
{ testCmd, preferSubagent }
|
|
2606
4512
|
);
|
|
2607
4513
|
const label = t.filename.replace(
|
|
2608
4514
|
/\.md$/,
|
|
@@ -2614,6 +4520,18 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
2614
4520
|
t.feature
|
|
2615
4521
|
);
|
|
2616
4522
|
d.log(` Starting ${t.filename}`);
|
|
4523
|
+
let prior = { subagents: 0 };
|
|
4524
|
+
const onStats = /* @__PURE__ */ __name((stats) => {
|
|
4525
|
+
d.setTicketStats(
|
|
4526
|
+
t.num,
|
|
4527
|
+
t.feature,
|
|
4528
|
+
addStats(prior, stats)
|
|
4529
|
+
);
|
|
4530
|
+
}, "onStats");
|
|
4531
|
+
const tally = /* @__PURE__ */ __name((r) => {
|
|
4532
|
+
prior = addStats(prior, r);
|
|
4533
|
+
runStats = addRun(runStats, r);
|
|
4534
|
+
}, "tally");
|
|
2617
4535
|
let level = effort;
|
|
2618
4536
|
let result = await spawnAgent({
|
|
2619
4537
|
prompt,
|
|
@@ -2624,8 +4542,11 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
2624
4542
|
label,
|
|
2625
4543
|
maxTurns,
|
|
2626
4544
|
effort: level,
|
|
4545
|
+
preferSubagent,
|
|
4546
|
+
onStats,
|
|
2627
4547
|
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
2628
4548
|
});
|
|
4549
|
+
tally(result);
|
|
2629
4550
|
for (let attempt = 2; result.exitCode !== 0 && !result.timedOut && attempt <= maxAttempts; attempt++) {
|
|
2630
4551
|
if (isKnown) {
|
|
2631
4552
|
level = escalateEffort(
|
|
@@ -2652,8 +4573,11 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
2652
4573
|
label,
|
|
2653
4574
|
maxTurns,
|
|
2654
4575
|
effort: level,
|
|
4576
|
+
preferSubagent,
|
|
4577
|
+
onStats,
|
|
2655
4578
|
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
2656
4579
|
});
|
|
4580
|
+
tally(result);
|
|
2657
4581
|
}
|
|
2658
4582
|
return { ticket: t, result };
|
|
2659
4583
|
}
|
|
@@ -2712,14 +4636,17 @@ No tickets completed this wave. ${remaining2.length} ready ticket${remaining2.le
|
|
|
2712
4636
|
d.log("\n" + renderFinishSummary(
|
|
2713
4637
|
tickets,
|
|
2714
4638
|
totalCompleted,
|
|
2715
|
-
failedKeys
|
|
4639
|
+
failedKeys,
|
|
4640
|
+
warnings
|
|
2716
4641
|
));
|
|
4642
|
+
const statsLine = renderRunStats(runStats);
|
|
4643
|
+
if (statsLine) d.log(statsLine);
|
|
2717
4644
|
d.cleanup();
|
|
2718
4645
|
return { allDone: remaining.length === 0 };
|
|
2719
4646
|
}
|
|
2720
4647
|
__name(runWaves, "runWaves");
|
|
2721
4648
|
async function runWavesIsolated(opts) {
|
|
2722
|
-
|
|
4649
|
+
mkdirSync3(opts.logsDir, { recursive: true });
|
|
2723
4650
|
const d = opts.display ?? createDisplay();
|
|
2724
4651
|
const retries = opts.retries ?? DEFAULT_RETRIES;
|
|
2725
4652
|
const retryDelay = opts.retryDelay ?? 0;
|
|
@@ -2734,12 +4661,13 @@ async function runWavesIsolated(opts) {
|
|
|
2734
4661
|
"Working tree is dirty; commit or stash changes before running"
|
|
2735
4662
|
);
|
|
2736
4663
|
}
|
|
4664
|
+
const launchCommit = headCommit(repoRoot);
|
|
2737
4665
|
const features = new Set(
|
|
2738
4666
|
opts.tickets.map((t) => t.feature).filter(Boolean)
|
|
2739
4667
|
);
|
|
2740
4668
|
const multiFeature = features.size > 1 && opts.progressPaths !== void 0;
|
|
2741
|
-
const fallbackProgressPath =
|
|
2742
|
-
|
|
4669
|
+
const fallbackProgressPath = join6(
|
|
4670
|
+
dirname4(opts.logsDir),
|
|
2743
4671
|
"progress.md"
|
|
2744
4672
|
);
|
|
2745
4673
|
let tracker;
|
|
@@ -2799,6 +4727,10 @@ async function runWavesIsolated(opts) {
|
|
|
2799
4727
|
let waveNum = 0;
|
|
2800
4728
|
const failedKeys = /* @__PURE__ */ new Set();
|
|
2801
4729
|
const session = sessionTickets(opts.tickets);
|
|
4730
|
+
let runStats = emptyRunStats();
|
|
4731
|
+
const onAgentRun = /* @__PURE__ */ __name((result) => {
|
|
4732
|
+
runStats = addRun(runStats, result);
|
|
4733
|
+
}, "onAgentRun");
|
|
2802
4734
|
while (true) {
|
|
2803
4735
|
if (waveNum > 0) reportBlocked(d, opts.tickets, failedKeys);
|
|
2804
4736
|
const ready = findReadyTickets(opts.tickets).filter(
|
|
@@ -2837,13 +4769,36 @@ async function runWavesIsolated(opts) {
|
|
|
2837
4769
|
timeout: opts.timeout,
|
|
2838
4770
|
logsDir: opts.logsDir,
|
|
2839
4771
|
testCmd: opts.testCmd,
|
|
4772
|
+
installCmd: opts.installCmd,
|
|
2840
4773
|
maxTurns: opts.maxTurns,
|
|
2841
4774
|
effort: opts.effort,
|
|
4775
|
+
preferSubagent: opts.preferSubagent,
|
|
4776
|
+
learningsPath: learningsPathFor(
|
|
4777
|
+
tracker.pathFor(t.feature)
|
|
4778
|
+
),
|
|
2842
4779
|
retries,
|
|
2843
4780
|
retryDelay,
|
|
2844
|
-
d
|
|
4781
|
+
d,
|
|
4782
|
+
onAgentRun
|
|
2845
4783
|
})
|
|
2846
4784
|
);
|
|
4785
|
+
const touched = new Set(ready.map((t) => {
|
|
4786
|
+
return learningsPathFor(tracker.pathFor(t.feature));
|
|
4787
|
+
}));
|
|
4788
|
+
for (const path of touched) {
|
|
4789
|
+
if (!isRegularFile(path)) continue;
|
|
4790
|
+
try {
|
|
4791
|
+
await commitProgress(
|
|
4792
|
+
repoRoot,
|
|
4793
|
+
path,
|
|
4794
|
+
"nightralph: record attempt learnings"
|
|
4795
|
+
);
|
|
4796
|
+
} catch (err) {
|
|
4797
|
+
d.log(
|
|
4798
|
+
" Failed to commit learnings: " + formatErrorMessage(err)
|
|
4799
|
+
);
|
|
4800
|
+
}
|
|
4801
|
+
}
|
|
2847
4802
|
const outcomes = settled.map(
|
|
2848
4803
|
(r) => r.status === "fulfilled" ? r.value : { kind: "error", error: r.reason }
|
|
2849
4804
|
);
|
|
@@ -2947,7 +4902,7 @@ async function runWavesIsolated(opts) {
|
|
|
2947
4902
|
repoRoot,
|
|
2948
4903
|
tracker.pathFor(ticket.feature),
|
|
2949
4904
|
tracker.dataFor(ticket.feature),
|
|
2950
|
-
`nightralph: ${ticket.filename} ` + (outcome.reason
|
|
4905
|
+
`nightralph: ${ticket.filename} ` + failureNote(outcome.reason),
|
|
2951
4906
|
d
|
|
2952
4907
|
);
|
|
2953
4908
|
continue;
|
|
@@ -2983,6 +4938,7 @@ async function runWavesIsolated(opts) {
|
|
|
2983
4938
|
branches: successfulBranches,
|
|
2984
4939
|
strategy: opts.conflictStrategy
|
|
2985
4940
|
});
|
|
4941
|
+
const doneThisWave = [];
|
|
2986
4942
|
for (const branchResult of mergeResult.merged) {
|
|
2987
4943
|
const bk = branchToKey.get(
|
|
2988
4944
|
branchResult.branch
|
|
@@ -2991,6 +4947,7 @@ async function runWavesIsolated(opts) {
|
|
|
2991
4947
|
if (!ticket) continue;
|
|
2992
4948
|
if (branchResult.merged) {
|
|
2993
4949
|
markDone(ticket);
|
|
4950
|
+
doneThisWave.push(ticket);
|
|
2994
4951
|
waveCompleted++;
|
|
2995
4952
|
totalCompleted++;
|
|
2996
4953
|
d.setTicketStatus(
|
|
@@ -3040,11 +4997,15 @@ async function runWavesIsolated(opts) {
|
|
|
3040
4997
|
timeout: opts.timeout,
|
|
3041
4998
|
logsDir: opts.logsDir,
|
|
3042
4999
|
testCmd: opts.testCmd,
|
|
5000
|
+
installCmd: opts.installCmd,
|
|
3043
5001
|
maxTurns: opts.maxTurns,
|
|
3044
|
-
|
|
5002
|
+
preferSubagent: opts.preferSubagent,
|
|
5003
|
+
onLine: /* @__PURE__ */ __name((msg) => d.log(msg), "onLine"),
|
|
5004
|
+
onAgentRun
|
|
3045
5005
|
}) : false;
|
|
3046
5006
|
if (recovered) {
|
|
3047
5007
|
markDone(ticket);
|
|
5008
|
+
doneThisWave.push(ticket);
|
|
3048
5009
|
waveCompleted++;
|
|
3049
5010
|
totalCompleted++;
|
|
3050
5011
|
d.setTicketStatus(
|
|
@@ -3100,6 +5061,28 @@ async function runWavesIsolated(opts) {
|
|
|
3100
5061
|
exitCode: 0,
|
|
3101
5062
|
done: false
|
|
3102
5063
|
});
|
|
5064
|
+
try {
|
|
5065
|
+
appendEntry(
|
|
5066
|
+
learningsPathFor(
|
|
5067
|
+
tracker.pathFor(ticket.feature)
|
|
5068
|
+
),
|
|
5069
|
+
renderEntry({
|
|
5070
|
+
outcome: "merge",
|
|
5071
|
+
key: ticketKey(
|
|
5072
|
+
ticket.feature,
|
|
5073
|
+
ticket.num
|
|
5074
|
+
),
|
|
5075
|
+
label: ticketLabel(ticket),
|
|
5076
|
+
attempt: 0,
|
|
5077
|
+
now: /* @__PURE__ */ new Date(),
|
|
5078
|
+
stderr: branchResult.stderr ?? ""
|
|
5079
|
+
})
|
|
5080
|
+
);
|
|
5081
|
+
} catch (err) {
|
|
5082
|
+
d.log(
|
|
5083
|
+
` ${ticket.filename}: could not record learnings: ` + formatErrorMessage(err)
|
|
5084
|
+
);
|
|
5085
|
+
}
|
|
3103
5086
|
await saveProgress(
|
|
3104
5087
|
repoRoot,
|
|
3105
5088
|
tracker.pathFor(ticket.feature),
|
|
@@ -3110,6 +5093,7 @@ async function runWavesIsolated(opts) {
|
|
|
3110
5093
|
}
|
|
3111
5094
|
}
|
|
3112
5095
|
}
|
|
5096
|
+
await commitTicketFiles(repoRoot, doneThisWave, d);
|
|
3113
5097
|
if (mergeResult.stoppedAt) {
|
|
3114
5098
|
d.log(
|
|
3115
5099
|
`
|
|
@@ -3152,31 +5136,109 @@ No tickets completed this wave. ${remaining2.length} ready ticket${remaining2.le
|
|
|
3152
5136
|
}
|
|
3153
5137
|
}
|
|
3154
5138
|
if (waveNum > 0) reportBlocked(d, opts.tickets, failedKeys);
|
|
5139
|
+
let reviewLine = null;
|
|
5140
|
+
if (opts.review) {
|
|
5141
|
+
try {
|
|
5142
|
+
const outcome = await runReviewPass({
|
|
5143
|
+
repoRoot,
|
|
5144
|
+
repoName,
|
|
5145
|
+
baseBranch,
|
|
5146
|
+
reviewPath: opts.review.reviewPath,
|
|
5147
|
+
forkedFrom: opts.review.forkedFrom,
|
|
5148
|
+
launchCommit,
|
|
5149
|
+
specBody: reviewSpecBody(
|
|
5150
|
+
opts.specBody,
|
|
5151
|
+
opts.specBodies
|
|
5152
|
+
),
|
|
5153
|
+
agentCmd: opts.agentCmd,
|
|
5154
|
+
model: opts.model,
|
|
5155
|
+
timeout: opts.timeout,
|
|
5156
|
+
logsDir: opts.logsDir,
|
|
5157
|
+
testCmd: opts.testCmd,
|
|
5158
|
+
installCmd: opts.installCmd,
|
|
5159
|
+
maxTurns: opts.maxTurns,
|
|
5160
|
+
effort: opts.effort,
|
|
5161
|
+
preferSubagent: opts.preferSubagent,
|
|
5162
|
+
retries,
|
|
5163
|
+
retryDelay,
|
|
5164
|
+
log: /* @__PURE__ */ __name((msg) => d.log(msg), "log")
|
|
5165
|
+
}, {
|
|
5166
|
+
spawnAgent: /* @__PURE__ */ __name(async (spawnOpts) => {
|
|
5167
|
+
const result = await spawnAgent(spawnOpts);
|
|
5168
|
+
onAgentRun(result);
|
|
5169
|
+
return result;
|
|
5170
|
+
}, "spawnAgent"),
|
|
5171
|
+
registry: runningAgents
|
|
5172
|
+
});
|
|
5173
|
+
reviewLine = renderReviewSummary(
|
|
5174
|
+
outcome,
|
|
5175
|
+
relative2(repoRoot, opts.review.reviewPath)
|
|
5176
|
+
);
|
|
5177
|
+
} catch (err) {
|
|
5178
|
+
d.log(`
|
|
5179
|
+
Review failed: ${formatErrorMessage(err)}`);
|
|
5180
|
+
}
|
|
5181
|
+
}
|
|
3155
5182
|
const remaining = opts.tickets.filter(
|
|
3156
5183
|
(t) => t.status === "ready-for-agent"
|
|
3157
5184
|
);
|
|
3158
5185
|
d.log("\n" + renderFinishSummary(
|
|
3159
5186
|
opts.tickets,
|
|
3160
5187
|
totalCompleted,
|
|
3161
|
-
failedKeys
|
|
5188
|
+
failedKeys,
|
|
5189
|
+
opts.warnings ?? []
|
|
3162
5190
|
));
|
|
5191
|
+
const statsLine = renderRunStats(runStats);
|
|
5192
|
+
if (statsLine) d.log(statsLine);
|
|
5193
|
+
if (reviewLine) d.log(reviewLine);
|
|
5194
|
+
d.log(`Work is on ${baseBranch}`);
|
|
3163
5195
|
d.cleanup();
|
|
3164
|
-
return { allDone: remaining.length === 0 };
|
|
5196
|
+
return { allDone: remaining.length === 0, branch: baseBranch };
|
|
3165
5197
|
}
|
|
3166
5198
|
__name(runWavesIsolated, "runWavesIsolated");
|
|
3167
5199
|
|
|
5200
|
+
// src/preflight.ts
|
|
5201
|
+
import { accessSync, constants, statSync as statSync2 } from "node:fs";
|
|
5202
|
+
import { delimiter, join as join7, resolve as resolve2 } from "node:path";
|
|
5203
|
+
function isExecutableFile(file) {
|
|
5204
|
+
try {
|
|
5205
|
+
if (!statSync2(file).isFile()) return false;
|
|
5206
|
+
accessSync(file, constants.X_OK);
|
|
5207
|
+
return true;
|
|
5208
|
+
} catch {
|
|
5209
|
+
return false;
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
__name(isExecutableFile, "isExecutableFile");
|
|
5213
|
+
function resolveExecutable(cmd, opts = {}) {
|
|
5214
|
+
if (cmd === "") return null;
|
|
5215
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
5216
|
+
if (cmd.includes("/")) {
|
|
5217
|
+
const full = resolve2(cwd, cmd);
|
|
5218
|
+
return isExecutableFile(full) ? full : null;
|
|
5219
|
+
}
|
|
5220
|
+
const path = opts.path ?? process.env.PATH ?? "";
|
|
5221
|
+
for (const dir of path.split(delimiter)) {
|
|
5222
|
+
if (dir === "") continue;
|
|
5223
|
+
const candidate = join7(dir, cmd);
|
|
5224
|
+
if (isExecutableFile(candidate)) return candidate;
|
|
5225
|
+
}
|
|
5226
|
+
return null;
|
|
5227
|
+
}
|
|
5228
|
+
__name(resolveExecutable, "resolveExecutable");
|
|
5229
|
+
|
|
3168
5230
|
// src/resolve.ts
|
|
3169
|
-
import { existsSync as
|
|
3170
|
-
import { dirname as
|
|
5231
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3 } from "node:fs";
|
|
5232
|
+
import { dirname as dirname5, join as join8 } from "node:path";
|
|
3171
5233
|
import { fileURLToPath } from "node:url";
|
|
3172
5234
|
import { checkbox, Separator } from "@inquirer/prompts";
|
|
3173
5235
|
function getScriptDir(metaUrl) {
|
|
3174
|
-
return
|
|
5236
|
+
return dirname5(fileURLToPath(metaUrl));
|
|
3175
5237
|
}
|
|
3176
5238
|
__name(getScriptDir, "getScriptDir");
|
|
3177
5239
|
function findFeatureDirs(cwd) {
|
|
3178
|
-
const scratchDir =
|
|
3179
|
-
if (!
|
|
5240
|
+
const scratchDir = join8(cwd, ".scratch");
|
|
5241
|
+
if (!existsSync6(scratchDir)) return [];
|
|
3180
5242
|
const entries = readdirSync3(
|
|
3181
5243
|
scratchDir,
|
|
3182
5244
|
{ withFileTypes: true }
|
|
@@ -3184,18 +5246,18 @@ function findFeatureDirs(cwd) {
|
|
|
3184
5246
|
const results = [];
|
|
3185
5247
|
for (const entry of entries) {
|
|
3186
5248
|
if (!entry.isDirectory()) continue;
|
|
3187
|
-
const issuesDir =
|
|
5249
|
+
const issuesDir = join8(
|
|
3188
5250
|
scratchDir,
|
|
3189
5251
|
entry.name,
|
|
3190
5252
|
"issues"
|
|
3191
5253
|
);
|
|
3192
|
-
const specFile =
|
|
5254
|
+
const specFile = join8(
|
|
3193
5255
|
scratchDir,
|
|
3194
5256
|
entry.name,
|
|
3195
5257
|
"spec.md"
|
|
3196
5258
|
);
|
|
3197
|
-
if (!
|
|
3198
|
-
const progressFile =
|
|
5259
|
+
if (!existsSync6(issuesDir)) continue;
|
|
5260
|
+
const progressFile = join8(
|
|
3199
5261
|
scratchDir,
|
|
3200
5262
|
entry.name,
|
|
3201
5263
|
"progress.md"
|
|
@@ -3211,13 +5273,93 @@ function findFeatureDirs(cwd) {
|
|
|
3211
5273
|
return results;
|
|
3212
5274
|
}
|
|
3213
5275
|
__name(findFeatureDirs, "findFeatureDirs");
|
|
5276
|
+
function knownFeatureNames(scratchDir) {
|
|
5277
|
+
const names = /* @__PURE__ */ new Set();
|
|
5278
|
+
const walk = /* @__PURE__ */ __name((rel) => {
|
|
5279
|
+
const dir = rel === "" ? scratchDir : join8(scratchDir, rel);
|
|
5280
|
+
for (const e of readdirSync3(dir, { withFileTypes: true })) {
|
|
5281
|
+
if (!e.isDirectory() || e.name === "issues") continue;
|
|
5282
|
+
const name = rel === "" ? e.name : `${rel}/${e.name}`;
|
|
5283
|
+
if (existsSync6(join8(scratchDir, name, "issues"))) {
|
|
5284
|
+
names.add(name);
|
|
5285
|
+
}
|
|
5286
|
+
walk(name);
|
|
5287
|
+
}
|
|
5288
|
+
}, "walk");
|
|
5289
|
+
if (existsSync6(scratchDir)) walk("");
|
|
5290
|
+
return names;
|
|
5291
|
+
}
|
|
5292
|
+
__name(knownFeatureNames, "knownFeatureNames");
|
|
5293
|
+
function loadSelection(scratchDir, names) {
|
|
5294
|
+
const known = knownFeatureNames(scratchDir);
|
|
5295
|
+
const issuesDir = /* @__PURE__ */ __name((name) => join8(scratchDir, name, "issues"), "issuesDir");
|
|
5296
|
+
const tickets = [];
|
|
5297
|
+
const selected = /* @__PURE__ */ new Map();
|
|
5298
|
+
for (const name of new Set(names)) {
|
|
5299
|
+
const dir = issuesDir(name);
|
|
5300
|
+
const found = existsSync6(dir) ? scanTickets(dir, name, known) : [];
|
|
5301
|
+
selected.set(name, new Set(found.map((t) => t.num)));
|
|
5302
|
+
tickets.push(...found);
|
|
5303
|
+
}
|
|
5304
|
+
const outside = /* @__PURE__ */ new Map();
|
|
5305
|
+
const lookup = /* @__PURE__ */ __name((feature, num) => {
|
|
5306
|
+
if (!known.has(feature)) return void 0;
|
|
5307
|
+
let statuses = outside.get(feature);
|
|
5308
|
+
if (!statuses) {
|
|
5309
|
+
statuses = new Map(
|
|
5310
|
+
scanTickets(issuesDir(feature), feature).map((t) => [t.num, t.status])
|
|
5311
|
+
);
|
|
5312
|
+
outside.set(feature, statuses);
|
|
5313
|
+
}
|
|
5314
|
+
return statuses.get(num);
|
|
5315
|
+
}, "lookup");
|
|
5316
|
+
const warnings = [];
|
|
5317
|
+
for (const t of tickets) {
|
|
5318
|
+
const parsed = parseBlockerField(t.body, known);
|
|
5319
|
+
t.blockers = resolveBlockers(
|
|
5320
|
+
parsed.refs,
|
|
5321
|
+
t.feature,
|
|
5322
|
+
selected,
|
|
5323
|
+
lookup
|
|
5324
|
+
);
|
|
5325
|
+
const ticket = ticketLabel(t);
|
|
5326
|
+
for (const word of parsed.typos) {
|
|
5327
|
+
warnings.push({ ticket, kind: "typo", text: word });
|
|
5328
|
+
}
|
|
5329
|
+
for (const ref of t.blockers) {
|
|
5330
|
+
if (ref.where !== "missing") continue;
|
|
5331
|
+
warnings.push({
|
|
5332
|
+
ticket,
|
|
5333
|
+
kind: "missing",
|
|
5334
|
+
text: blockerLabel(ref)
|
|
5335
|
+
});
|
|
5336
|
+
}
|
|
5337
|
+
}
|
|
5338
|
+
return { tickets, warnings };
|
|
5339
|
+
}
|
|
5340
|
+
__name(loadSelection, "loadSelection");
|
|
5341
|
+
function featureCounts(scratchDir, name) {
|
|
5342
|
+
const { tickets, warnings } = loadSelection(scratchDir, [name]);
|
|
5343
|
+
return {
|
|
5344
|
+
ready: findReadyTickets(tickets).length,
|
|
5345
|
+
total: tickets.length,
|
|
5346
|
+
badBlockers: warnings.filter((w) => w.kind === "missing").length
|
|
5347
|
+
};
|
|
5348
|
+
}
|
|
5349
|
+
__name(featureCounts, "featureCounts");
|
|
5350
|
+
function featureChoiceLabel(scratchDir, name) {
|
|
5351
|
+
const c = featureCounts(scratchDir, name);
|
|
5352
|
+
const bad = c.badBlockers === 0 ? "" : `, ${c.badBlockers} bad blocker` + (c.badBlockers === 1 ? "" : "s");
|
|
5353
|
+
return `${name} (${c.ready}/${c.total} ready${bad})`;
|
|
5354
|
+
}
|
|
5355
|
+
__name(featureChoiceLabel, "featureChoiceLabel");
|
|
3214
5356
|
var ALL_VALUE = "__all__";
|
|
3215
5357
|
async function resolveIssuesDir(specName, cwd = process.cwd()) {
|
|
3216
5358
|
if (specName) {
|
|
3217
|
-
const featureDir =
|
|
5359
|
+
const featureDir = join8(cwd, ".scratch", specName);
|
|
3218
5360
|
return [{
|
|
3219
|
-
dir:
|
|
3220
|
-
spec:
|
|
5361
|
+
dir: join8(featureDir, "issues"),
|
|
5362
|
+
spec: join8(featureDir, "spec.md"),
|
|
3221
5363
|
name: specName
|
|
3222
5364
|
}];
|
|
3223
5365
|
}
|
|
@@ -3233,14 +5375,11 @@ async function resolveIssuesDir(specName, cwd = process.cwd()) {
|
|
|
3233
5375
|
console.log(`Using feature: ${f.name}`);
|
|
3234
5376
|
return [{ dir: f.dir, spec: f.spec, name: f.name }];
|
|
3235
5377
|
}
|
|
3236
|
-
const
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
value: f.name
|
|
3242
|
-
};
|
|
3243
|
-
});
|
|
5378
|
+
const scratchDir = join8(cwd, ".scratch");
|
|
5379
|
+
const featureChoices = features.map((f) => ({
|
|
5380
|
+
name: featureChoiceLabel(scratchDir, f.name),
|
|
5381
|
+
value: f.name
|
|
5382
|
+
}));
|
|
3244
5383
|
while (true) {
|
|
3245
5384
|
const selected = await checkbox({
|
|
3246
5385
|
message: "Which features?",
|
|
@@ -3296,8 +5435,8 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
|
|
|
3296
5435
|
}
|
|
3297
5436
|
}
|
|
3298
5437
|
setup({
|
|
3299
|
-
bundledSkillsDir:
|
|
3300
|
-
docsTemplatesDir:
|
|
5438
|
+
bundledSkillsDir: join9(scriptDir, "skills"),
|
|
5439
|
+
docsTemplatesDir: join9(
|
|
3301
5440
|
scriptDir,
|
|
3302
5441
|
"docs-templates"
|
|
3303
5442
|
),
|
|
@@ -3332,7 +5471,10 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
|
|
|
3332
5471
|
default: 3600
|
|
3333
5472
|
}).option("test-cmd", {
|
|
3334
5473
|
type: "string",
|
|
3335
|
-
describe: 'Command run in each worktree after the agent exits; non-zero exit rejects the ticket. Defaults to "npm test".'
|
|
5474
|
+
describe: 'Command run in each worktree after the agent exits; non-zero exit rejects the ticket. Defaults to "npm test" when package.json has a test script; otherwise the run has no test gate. "" disables it.'
|
|
5475
|
+
}).option("install-cmd", {
|
|
5476
|
+
type: "string",
|
|
5477
|
+
describe: `Command run in each fresh worktree before the agent starts. Defaults to the install command for the project's lockfile (npm ci or pnpm install); "" disables it.`
|
|
3336
5478
|
}).option("X", {
|
|
3337
5479
|
type: "boolean",
|
|
3338
5480
|
describe: "Stop on merge conflict instead of re-spawning",
|
|
@@ -3344,6 +5486,9 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
|
|
|
3344
5486
|
type: "string",
|
|
3345
5487
|
choices: ALL_EFFORT_LEVELS,
|
|
3346
5488
|
describe: "Starting effort level. Omitted = provider default. Retries step up from here."
|
|
5489
|
+
}).option("prefer-subagent", {
|
|
5490
|
+
type: "string",
|
|
5491
|
+
describe: 'claude only: model that subagents default to (CLAUDE_CODE_SUBAGENT_MODEL), plus a prompt rule to hand read-only work to them. "" disables it.'
|
|
3347
5492
|
}).option("retries", {
|
|
3348
5493
|
type: "number",
|
|
3349
5494
|
default: DEFAULT_RETRIES,
|
|
@@ -3356,6 +5501,10 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
|
|
|
3356
5501
|
type: "number",
|
|
3357
5502
|
default: DEFAULT_CONCURRENCY,
|
|
3358
5503
|
describe: "Max agents running at once within a wave."
|
|
5504
|
+
}).option("review", {
|
|
5505
|
+
type: "boolean",
|
|
5506
|
+
default: true,
|
|
5507
|
+
describe: "Review the merged work and run one fix pass at the end. --no-review skips it."
|
|
3359
5508
|
}),
|
|
3360
5509
|
async (argv) => {
|
|
3361
5510
|
await runExecute({
|
|
@@ -3365,15 +5514,27 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
|
|
|
3365
5514
|
dryRun: argv.dryRun,
|
|
3366
5515
|
timeout: argv.timeout,
|
|
3367
5516
|
testCmd: argv.testCmd,
|
|
5517
|
+
installCmd: argv.installCmd,
|
|
3368
5518
|
stopOnConflict: argv.X,
|
|
3369
5519
|
maxTurns: argv.maxTurns ?? argv.turns,
|
|
3370
5520
|
effort: argv.effort,
|
|
5521
|
+
preferSubagent: argv.preferSubagent,
|
|
3371
5522
|
retries: argv.retries,
|
|
3372
5523
|
retryDelay: argv.retryDelay,
|
|
3373
|
-
concurrency: argv.concurrency
|
|
5524
|
+
concurrency: argv.concurrency,
|
|
5525
|
+
review: argv.review
|
|
3374
5526
|
});
|
|
3375
5527
|
}
|
|
3376
|
-
).strict().demandCommand(1, "Specify a command or provider").help().
|
|
5528
|
+
).strict().demandCommand(1, "Specify a command or provider").help().fail((msg, err, y) => {
|
|
5529
|
+
if (err && !msg) {
|
|
5530
|
+
console.error(formatErrorMessage(err));
|
|
5531
|
+
process.exit(2);
|
|
5532
|
+
}
|
|
5533
|
+
y.showHelp();
|
|
5534
|
+
console.error(`
|
|
5535
|
+
${msg}`);
|
|
5536
|
+
process.exit(1);
|
|
5537
|
+
}).parse();
|
|
3377
5538
|
function isGitRepo() {
|
|
3378
5539
|
try {
|
|
3379
5540
|
getRepoRoot();
|
|
@@ -3394,80 +5555,215 @@ function extractFeatureName(spec) {
|
|
|
3394
5555
|
return parts[parts.length - 2] ?? parts[parts.length - 1] ?? "feature";
|
|
3395
5556
|
}
|
|
3396
5557
|
__name(extractFeatureName, "extractFeatureName");
|
|
3397
|
-
|
|
3398
|
-
const selections = await resolveIssuesDir(
|
|
3399
|
-
argv.spec
|
|
3400
|
-
);
|
|
5558
|
+
function loadTickets(selections, log) {
|
|
3401
5559
|
const allTickets = [];
|
|
3402
5560
|
const specBodies = /* @__PURE__ */ new Map();
|
|
3403
5561
|
const progressPaths = /* @__PURE__ */ new Map();
|
|
5562
|
+
const issuesDirs = [];
|
|
3404
5563
|
let firstFeatureName = "";
|
|
3405
5564
|
let firstLogsDir = "";
|
|
5565
|
+
const missing = [];
|
|
5566
|
+
const valid = [];
|
|
5567
|
+
const specs = /* @__PURE__ */ new Map();
|
|
3406
5568
|
for (const selection of selections) {
|
|
3407
5569
|
const { dir, spec, name } = selection;
|
|
3408
|
-
if (!
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
);
|
|
3412
|
-
process.exit(2);
|
|
5570
|
+
if (!existsSync7(dir)) {
|
|
5571
|
+
missing.push(`Directory not found: ${dir}`);
|
|
5572
|
+
continue;
|
|
3413
5573
|
}
|
|
3414
|
-
if (!
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
);
|
|
3418
|
-
process.exit(2);
|
|
5574
|
+
if (!existsSync7(spec)) {
|
|
5575
|
+
missing.push(`Spec file not found: ${spec}`);
|
|
5576
|
+
continue;
|
|
3419
5577
|
}
|
|
3420
|
-
|
|
3421
|
-
|
|
5578
|
+
specs.set(name, stripSpecSection(
|
|
5579
|
+
readFileSync7(spec, "utf8"),
|
|
3422
5580
|
"User Stories"
|
|
3423
|
-
);
|
|
3424
|
-
|
|
5581
|
+
));
|
|
5582
|
+
valid.push(selection);
|
|
5583
|
+
}
|
|
5584
|
+
const loaded = valid.length > 0 ? loadSelection(
|
|
5585
|
+
join9(process.cwd(), ".scratch"),
|
|
5586
|
+
valid.map((s) => s.name)
|
|
5587
|
+
) : { tickets: [], warnings: [] };
|
|
5588
|
+
for (const { dir, spec, name } of valid) {
|
|
5589
|
+
const tickets = loaded.tickets.filter((t) => t.feature === name);
|
|
3425
5590
|
if (tickets.length === 0) {
|
|
3426
|
-
|
|
5591
|
+
log(
|
|
3427
5592
|
`No ticket files found for ${name}. Skipping.`
|
|
3428
5593
|
);
|
|
3429
5594
|
continue;
|
|
3430
5595
|
}
|
|
3431
|
-
|
|
5596
|
+
log(
|
|
3432
5597
|
`[${name}] Found ${tickets.length} tickets`
|
|
3433
5598
|
);
|
|
3434
|
-
|
|
3435
|
-
tickets.map((t) => t.num)
|
|
3436
|
-
);
|
|
3437
|
-
for (const t of tickets) {
|
|
3438
|
-
const unknown = t.blockers.filter(
|
|
3439
|
-
(b) => !knownNums.has(b)
|
|
3440
|
-
);
|
|
3441
|
-
if (unknown.length > 0) {
|
|
3442
|
-
console.warn(
|
|
3443
|
-
`${t.filename}: blockers [${unknown.join(", ")}] not found in directory`
|
|
3444
|
-
);
|
|
3445
|
-
}
|
|
3446
|
-
t.blockers = t.blockers.filter(
|
|
3447
|
-
(b) => knownNums.has(b)
|
|
3448
|
-
);
|
|
3449
|
-
}
|
|
3450
|
-
specBodies.set(name, specBody);
|
|
5599
|
+
specBodies.set(name, specs.get(name));
|
|
3451
5600
|
progressPaths.set(
|
|
3452
5601
|
name,
|
|
3453
|
-
|
|
5602
|
+
join9(dirname6(dir), "progress.md")
|
|
3454
5603
|
);
|
|
5604
|
+
issuesDirs.push(dir);
|
|
3455
5605
|
allTickets.push(...tickets);
|
|
3456
5606
|
if (!firstFeatureName) {
|
|
3457
5607
|
firstFeatureName = extractFeatureName(spec);
|
|
3458
|
-
firstLogsDir =
|
|
5608
|
+
firstLogsDir = join9(dirname6(dir), "logs");
|
|
5609
|
+
}
|
|
5610
|
+
}
|
|
5611
|
+
return {
|
|
5612
|
+
allTickets,
|
|
5613
|
+
specBodies,
|
|
5614
|
+
progressPaths,
|
|
5615
|
+
issuesDirs,
|
|
5616
|
+
firstFeatureName,
|
|
5617
|
+
firstLogsDir,
|
|
5618
|
+
missing,
|
|
5619
|
+
warnings: loaded.warnings
|
|
5620
|
+
};
|
|
5621
|
+
}
|
|
5622
|
+
__name(loadTickets, "loadTickets");
|
|
5623
|
+
function loadTicketsOrExit(selections) {
|
|
5624
|
+
const loaded = loadTickets(selections, console.log);
|
|
5625
|
+
if (loaded.missing.length > 0) {
|
|
5626
|
+
for (const msg of loaded.missing) console.error(msg);
|
|
5627
|
+
process.exit(2);
|
|
5628
|
+
}
|
|
5629
|
+
return loaded;
|
|
5630
|
+
}
|
|
5631
|
+
__name(loadTicketsOrExit, "loadTicketsOrExit");
|
|
5632
|
+
function checkoutLaunchBranch(plan) {
|
|
5633
|
+
try {
|
|
5634
|
+
execFileSync2(
|
|
5635
|
+
"git",
|
|
5636
|
+
["checkout", "--quiet", plan.launchBranch],
|
|
5637
|
+
{ cwd: getRepoRoot(), stdio: "ignore" }
|
|
5638
|
+
);
|
|
5639
|
+
} catch {
|
|
5640
|
+
}
|
|
5641
|
+
}
|
|
5642
|
+
__name(checkoutLaunchBranch, "checkoutLaunchBranch");
|
|
5643
|
+
function failStaleResume(plan, problems) {
|
|
5644
|
+
checkoutLaunchBranch(plan);
|
|
5645
|
+
const n = plan.behind;
|
|
5646
|
+
console.error(
|
|
5647
|
+
`Cannot resume ${plan.branch}: ${problems.join("; ")}. It is ${n} commit${n === 1 ? "" : "s"} behind ${plan.launchBranch}. Merge ${plan.launchBranch} into it, or delete it with \`git branch -D ${plan.branch}\`. Back on ${plan.launchBranch}.`
|
|
5648
|
+
);
|
|
5649
|
+
process.exit(2);
|
|
5650
|
+
}
|
|
5651
|
+
__name(failStaleResume, "failStaleResume");
|
|
5652
|
+
function stopNothingReady(plan, loaded) {
|
|
5653
|
+
checkoutLaunchBranch(plan);
|
|
5654
|
+
console.log(
|
|
5655
|
+
`Nothing to run on ${plan.branch}. Back on ${plan.launchBranch}.
|
|
5656
|
+
` + renderNothingReady(loaded.allTickets, loaded.warnings)
|
|
5657
|
+
);
|
|
5658
|
+
process.exit(0);
|
|
5659
|
+
}
|
|
5660
|
+
__name(stopNothingReady, "stopNothingReady");
|
|
5661
|
+
function willResume(features) {
|
|
5662
|
+
const root = getRepoRoot();
|
|
5663
|
+
const name = planIntegrationBranch(
|
|
5664
|
+
getCurrentBranch(root),
|
|
5665
|
+
getDefaultBranch(root),
|
|
5666
|
+
features
|
|
5667
|
+
);
|
|
5668
|
+
return name !== null && localBranchExists(root, name);
|
|
5669
|
+
}
|
|
5670
|
+
__name(willResume, "willResume");
|
|
5671
|
+
function reviewPathFor(issuesDirs) {
|
|
5672
|
+
const featureDir = dirname6(issuesDirs[0]);
|
|
5673
|
+
return issuesDirs.length === 1 ? join9(featureDir, "review.md") : join9(dirname6(featureDir), "review.md");
|
|
5674
|
+
}
|
|
5675
|
+
__name(reviewPathFor, "reviewPathFor");
|
|
5676
|
+
async function runExecute(argv) {
|
|
5677
|
+
const selections = await resolveIssuesDir(
|
|
5678
|
+
argv.spec
|
|
5679
|
+
);
|
|
5680
|
+
let loaded = loadTicketsOrExit(selections);
|
|
5681
|
+
if (loaded.allTickets.length === 0) return;
|
|
5682
|
+
const resuming = /* @__PURE__ */ __name(() => !argv.dryRun && isGitRepo() && willResume([...loaded.specBodies.keys()]), "resuming");
|
|
5683
|
+
if (nothingReady(loaded.allTickets) && !resuming()) {
|
|
5684
|
+
console.log(renderNothingReady(
|
|
5685
|
+
loaded.allTickets,
|
|
5686
|
+
loaded.warnings
|
|
5687
|
+
));
|
|
5688
|
+
return;
|
|
5689
|
+
}
|
|
5690
|
+
const providerPath = resolveExecutable(argv.provider);
|
|
5691
|
+
const providerIsPath = argv.provider.includes("/");
|
|
5692
|
+
const providerMissing = providerIsPath ? "not found or not executable" : "not found on PATH";
|
|
5693
|
+
if (providerPath === null && !argv.dryRun) {
|
|
5694
|
+
console.error(
|
|
5695
|
+
`Provider '${argv.provider}' ${providerMissing}`
|
|
5696
|
+
);
|
|
5697
|
+
process.exit(2);
|
|
5698
|
+
}
|
|
5699
|
+
const agentCmd = providerIsPath ? providerPath ?? argv.provider : argv.provider;
|
|
5700
|
+
let preferSubagent = argv.preferSubagent || void 0;
|
|
5701
|
+
const providerName = basename6(argv.provider);
|
|
5702
|
+
if (preferSubagent && providerName !== "claude") {
|
|
5703
|
+
console.warn(
|
|
5704
|
+
`--prefer-subagent not supported by ${providerName}, skipping`
|
|
5705
|
+
);
|
|
5706
|
+
preferSubagent = void 0;
|
|
5707
|
+
}
|
|
5708
|
+
const isolated = isGitRepo();
|
|
5709
|
+
const featureNames = [...loaded.specBodies.keys()];
|
|
5710
|
+
let plan = null;
|
|
5711
|
+
if (isolated && !argv.dryRun) {
|
|
5712
|
+
try {
|
|
5713
|
+
plan = prepareIntegrationBranch(
|
|
5714
|
+
getRepoRoot(),
|
|
5715
|
+
featureNames
|
|
5716
|
+
);
|
|
5717
|
+
} catch (err) {
|
|
5718
|
+
console.error(formatErrorMessage(err));
|
|
5719
|
+
process.exit(2);
|
|
5720
|
+
}
|
|
5721
|
+
if (plan.created) {
|
|
5722
|
+
console.log(
|
|
5723
|
+
`Created branch ${plan.branch} from ${plan.launchBranch}`
|
|
5724
|
+
);
|
|
5725
|
+
} else if (plan.resumed) {
|
|
5726
|
+
console.log(
|
|
5727
|
+
`Resumed ${plan.branch}` + (plan.fastForwarded ? ` (fast-forwarded to ${plan.launchBranch})` : "") + (plan.behind > 0 ? ` (${plan.launchBranch} is ${plan.behind} commit${plan.behind === 1 ? "" : "s"} ahead)` : "")
|
|
5728
|
+
);
|
|
5729
|
+
loaded = loadTickets(selections, () => {
|
|
5730
|
+
});
|
|
5731
|
+
if (loaded.missing.length > 0) {
|
|
5732
|
+
failStaleResume(plan, loaded.missing);
|
|
5733
|
+
}
|
|
5734
|
+
if (loaded.allTickets.length === 0) {
|
|
5735
|
+
failStaleResume(plan, ["no tickets"]);
|
|
5736
|
+
}
|
|
5737
|
+
if (nothingReady(loaded.allTickets)) {
|
|
5738
|
+
stopNothingReady(plan, loaded);
|
|
5739
|
+
}
|
|
3459
5740
|
}
|
|
3460
5741
|
}
|
|
3461
|
-
|
|
5742
|
+
const {
|
|
5743
|
+
allTickets,
|
|
5744
|
+
specBodies,
|
|
5745
|
+
progressPaths,
|
|
5746
|
+
firstFeatureName,
|
|
5747
|
+
firstLogsDir
|
|
5748
|
+
} = loaded;
|
|
3462
5749
|
const timeout = argv.timeout ?? 900;
|
|
3463
|
-
const testCmd = argv.testCmd
|
|
3464
|
-
|
|
3465
|
-
)
|
|
5750
|
+
const testCmd = argv.testCmd ?? detectTestCmd(
|
|
5751
|
+
isolated ? getRepoRoot() : process.cwd()
|
|
5752
|
+
) ?? "";
|
|
5753
|
+
if (argv.testCmd === void 0 && testCmd === "") {
|
|
5754
|
+
console.log(
|
|
5755
|
+
"No test command detected; running without a test gate. Pass --test-cmd to add one."
|
|
5756
|
+
);
|
|
5757
|
+
}
|
|
5758
|
+
const installCmd = argv.installCmd ?? (isolated ? detectInstallCmd(getRepoRoot()) ?? void 0 : void 0);
|
|
3466
5759
|
const conflictStrategy = argv.stopOnConflict ? "stop" : "respawn";
|
|
3467
5760
|
const logsDir = firstLogsDir;
|
|
3468
5761
|
const fallbackSpec = specBodies.values().next().value;
|
|
3469
5762
|
if (argv.dryRun) {
|
|
3470
|
-
|
|
5763
|
+
console.log(
|
|
5764
|
+
`Provider: ${providerPath ?? providerMissing}`
|
|
5765
|
+
);
|
|
5766
|
+
if (isolated) {
|
|
3471
5767
|
const repoName = getRepoName();
|
|
3472
5768
|
dryRunIsolated({
|
|
3473
5769
|
tickets: allTickets,
|
|
@@ -3476,13 +5772,28 @@ async function runExecute(argv) {
|
|
|
3476
5772
|
conflictStrategy,
|
|
3477
5773
|
progressPath: progressPaths.values().next().value,
|
|
3478
5774
|
testCmd,
|
|
5775
|
+
installCmd,
|
|
3479
5776
|
effort: argv.effort,
|
|
3480
5777
|
retries: argv.retries,
|
|
3481
5778
|
retryDelay: argv.retryDelay,
|
|
3482
|
-
concurrency: argv.concurrency
|
|
5779
|
+
concurrency: argv.concurrency,
|
|
5780
|
+
preferSubagent
|
|
3483
5781
|
});
|
|
5782
|
+
console.log(
|
|
5783
|
+
describeIntegrationBranch(
|
|
5784
|
+
getRepoRoot(),
|
|
5785
|
+
featureNames
|
|
5786
|
+
)
|
|
5787
|
+
);
|
|
5788
|
+
const reviewState = argv.review === false ? "off" : "on";
|
|
5789
|
+
console.log(`Review: ${reviewState}`);
|
|
3484
5790
|
} else {
|
|
3485
|
-
dryRun(
|
|
5791
|
+
dryRun(
|
|
5792
|
+
allTickets,
|
|
5793
|
+
fallbackSpec,
|
|
5794
|
+
testCmd,
|
|
5795
|
+
preferSubagent
|
|
5796
|
+
);
|
|
3486
5797
|
}
|
|
3487
5798
|
return;
|
|
3488
5799
|
}
|
|
@@ -3496,12 +5807,12 @@ async function runExecute(argv) {
|
|
|
3496
5807
|
process.on("SIGINT", () => onSignal(130));
|
|
3497
5808
|
process.on("SIGTERM", () => onSignal(143));
|
|
3498
5809
|
let result;
|
|
3499
|
-
if (
|
|
5810
|
+
if (isolated) {
|
|
3500
5811
|
result = await runWavesIsolated({
|
|
3501
5812
|
tickets: allTickets,
|
|
3502
5813
|
specBody: fallbackSpec,
|
|
3503
5814
|
specBodies,
|
|
3504
|
-
agentCmd
|
|
5815
|
+
agentCmd,
|
|
3505
5816
|
model: argv.model ?? null,
|
|
3506
5817
|
timeout,
|
|
3507
5818
|
logsDir,
|
|
@@ -3509,18 +5820,25 @@ async function runExecute(argv) {
|
|
|
3509
5820
|
conflictStrategy,
|
|
3510
5821
|
display,
|
|
3511
5822
|
testCmd,
|
|
5823
|
+
installCmd,
|
|
3512
5824
|
maxTurns: argv.maxTurns,
|
|
3513
5825
|
effort: argv.effort,
|
|
3514
5826
|
retries: argv.retries,
|
|
3515
5827
|
retryDelay: argv.retryDelay,
|
|
3516
5828
|
concurrency: argv.concurrency,
|
|
3517
|
-
|
|
5829
|
+
preferSubagent,
|
|
5830
|
+
progressPaths,
|
|
5831
|
+
warnings: loaded.warnings,
|
|
5832
|
+
review: argv.review === false ? void 0 : {
|
|
5833
|
+
reviewPath: reviewPathFor(loaded.issuesDirs),
|
|
5834
|
+
forkedFrom: plan?.forkedFrom ?? void 0
|
|
5835
|
+
}
|
|
3518
5836
|
});
|
|
3519
5837
|
} else {
|
|
3520
5838
|
result = await runWaves(
|
|
3521
5839
|
allTickets,
|
|
3522
5840
|
fallbackSpec,
|
|
3523
|
-
|
|
5841
|
+
agentCmd,
|
|
3524
5842
|
argv.model ?? null,
|
|
3525
5843
|
timeout,
|
|
3526
5844
|
logsDir,
|
|
@@ -3530,7 +5848,9 @@ async function runExecute(argv) {
|
|
|
3530
5848
|
argv.effort,
|
|
3531
5849
|
argv.retries,
|
|
3532
5850
|
argv.retryDelay,
|
|
3533
|
-
argv.concurrency
|
|
5851
|
+
argv.concurrency,
|
|
5852
|
+
loaded.warnings,
|
|
5853
|
+
preferSubagent
|
|
3534
5854
|
);
|
|
3535
5855
|
}
|
|
3536
5856
|
if (!result.allDone) {
|