nightralph 0.0.54 → 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 +100 -11
- 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 +740 -137
- package/dist/index.js.map +4 -4
- package/dist/learnings.js +4 -0
- package/dist/learnings.js.map +2 -2
- package/dist/meta.json +69 -16
- package/dist/orchestrator.js +325 -93
- package/dist/orchestrator.js.map +2 -2
- package/dist/resolve.js +98 -9
- package/dist/resolve.js.map +2 -2
- package/dist/review.js +9 -2
- package/dist/review.js.map +2 -2
- package/dist/rules.js +13 -0
- package/dist/rules.js.map +2 -2
- 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 +2 -0
- package/dist/src/learnings.d.ts.map +1 -1
- package/dist/src/orchestrator.d.ts +71 -17
- package/dist/src/orchestrator.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/review.d.ts +4 -0
- package/dist/src/review.d.ts.map +1 -1
- package/dist/src/rules.d.ts +1 -0
- package/dist/src/rules.d.ts.map +1 -1
- package/dist/src/stats.d.ts +43 -0
- package/dist/src/stats.d.ts.map +1 -0
- package/dist/stats.js +111 -0
- package/dist/stats.js.map +7 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
|
|
|
5
5
|
// src/index.ts
|
|
6
6
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
7
7
|
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
|
|
8
|
-
import { dirname as dirname6, join as join9 } from "node:path";
|
|
8
|
+
import { basename as basename6, dirname as dirname6, join as join9 } from "node:path";
|
|
9
9
|
import yargs from "yargs";
|
|
10
10
|
import { hideBin } from "yargs/helpers";
|
|
11
11
|
import { select } from "@inquirer/prompts";
|
|
@@ -973,6 +973,106 @@ async function mergeWave(opts) {
|
|
|
973
973
|
}
|
|
974
974
|
__name(mergeWave, "mergeWave");
|
|
975
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
|
+
|
|
976
1076
|
// src/display.ts
|
|
977
1077
|
var ESC = "\x1B[";
|
|
978
1078
|
var ansi = {
|
|
@@ -1034,10 +1134,9 @@ var MAX_MESSAGE_BUFFER = 200;
|
|
|
1034
1134
|
var ANSI_RE = /\x1b\[[0-9;?]*[A-Za-z]/g;
|
|
1035
1135
|
function rowText(ticket) {
|
|
1036
1136
|
const label = ticketLabel(ticket);
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
}
|
|
1040
|
-
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}`;
|
|
1041
1140
|
}
|
|
1042
1141
|
__name(rowText, "rowText");
|
|
1043
1142
|
function fitLine(line, width) {
|
|
@@ -1166,6 +1265,12 @@ var StatusDisplay = class {
|
|
|
1166
1265
|
ticket.blockedBy = [...blockedBy];
|
|
1167
1266
|
this.render();
|
|
1168
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
|
+
}
|
|
1169
1274
|
log(message) {
|
|
1170
1275
|
if (!this.wave) {
|
|
1171
1276
|
this.config.stream.write(message + "\n");
|
|
@@ -1304,6 +1409,8 @@ Wave ${waveNum}${pct}: ` + tickets.map((t) => ticketLabel(t)).join(", ")
|
|
|
1304
1409
|
}
|
|
1305
1410
|
setBlocked(_num, _feature, _blockedBy) {
|
|
1306
1411
|
}
|
|
1412
|
+
setTicketStats(_num, _feature, _stats) {
|
|
1413
|
+
}
|
|
1307
1414
|
log(message) {
|
|
1308
1415
|
console.log(message);
|
|
1309
1416
|
}
|
|
@@ -1453,6 +1560,8 @@ __name(sanitize, "sanitize");
|
|
|
1453
1560
|
function renderAgentSummary(run, effort) {
|
|
1454
1561
|
const effortLine = effort ? `Effort: ${effort}.` : "";
|
|
1455
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)}.`;
|
|
1456
1565
|
const itemsText = run.completedItems.length > 0 ? run.completedItems.map(
|
|
1457
1566
|
(item) => `"${sanitize(item)}"`
|
|
1458
1567
|
).join(", ") + "." : "none.";
|
|
@@ -1460,6 +1569,8 @@ function renderAgentSummary(run, effort) {
|
|
|
1460
1569
|
return [
|
|
1461
1570
|
effortLine,
|
|
1462
1571
|
exitLine,
|
|
1572
|
+
subagentsLine,
|
|
1573
|
+
costLine,
|
|
1463
1574
|
itemsLine
|
|
1464
1575
|
].filter(Boolean).join(" ");
|
|
1465
1576
|
}
|
|
@@ -1749,6 +1860,18 @@ function installRule(installCmd) {
|
|
|
1749
1860
|
];
|
|
1750
1861
|
}
|
|
1751
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");
|
|
1752
1875
|
function testGateRule(testCmd, subject) {
|
|
1753
1876
|
if (testCmd === "") {
|
|
1754
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}.`;
|
|
@@ -1896,6 +2019,7 @@ function renderReviewPrompt(opts) {
|
|
|
1896
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.",
|
|
1897
2020
|
"- Look for correctness bugs, missing or weak tests, and code that breaks a documented contract. Ignore style preferences.",
|
|
1898
2021
|
"- You may run the tests or the type checker to confirm a suspicion.",
|
|
2022
|
+
...subagentRule(opts.preferSubagent, true),
|
|
1899
2023
|
"- Do NOT edit, stage, or commit code. Anything you change here is discarded.",
|
|
1900
2024
|
...CONFINEMENT_RULES,
|
|
1901
2025
|
"",
|
|
@@ -1950,6 +2074,7 @@ function renderFixPrompt(opts) {
|
|
|
1950
2074
|
readDocsRule("findings"),
|
|
1951
2075
|
"- Fix ONLY the listed findings. Do not fix unrelated problems you notice.",
|
|
1952
2076
|
...installRule(opts.installCmd),
|
|
2077
|
+
...subagentRule(opts.preferSubagent),
|
|
1953
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.",
|
|
1954
2079
|
testGateRule(opts.testCmd, "fix"),
|
|
1955
2080
|
LINT_RULE,
|
|
@@ -2229,7 +2354,8 @@ async function runReviewer(opts, deps, range) {
|
|
|
2229
2354
|
exclude: reviewExcludeFor(
|
|
2230
2355
|
opts.repoRoot,
|
|
2231
2356
|
opts.reviewPath
|
|
2232
|
-
)
|
|
2357
|
+
),
|
|
2358
|
+
preferSubagent: opts.preferSubagent
|
|
2233
2359
|
}),
|
|
2234
2360
|
agentCmd: opts.agentCmd,
|
|
2235
2361
|
model: opts.model,
|
|
@@ -2239,6 +2365,7 @@ async function runReviewer(opts, deps, range) {
|
|
|
2239
2365
|
label: "review",
|
|
2240
2366
|
maxTurns: opts.maxTurns,
|
|
2241
2367
|
effort,
|
|
2368
|
+
preferSubagent: opts.preferSubagent,
|
|
2242
2369
|
onLine: opts.log
|
|
2243
2370
|
});
|
|
2244
2371
|
const text = collectNotes(wt.worktreePath, FINDINGS_FILE);
|
|
@@ -2308,7 +2435,8 @@ async function runFixer(opts, deps, findings) {
|
|
|
2308
2435
|
findings,
|
|
2309
2436
|
testCmd: opts.testCmd,
|
|
2310
2437
|
installCmd: opts.installCmd,
|
|
2311
|
-
previous
|
|
2438
|
+
previous,
|
|
2439
|
+
preferSubagent: opts.preferSubagent
|
|
2312
2440
|
}),
|
|
2313
2441
|
agentCmd: opts.agentCmd,
|
|
2314
2442
|
model: opts.model,
|
|
@@ -2323,6 +2451,7 @@ async function runFixer(opts, deps, findings) {
|
|
|
2323
2451
|
label: "review-fix",
|
|
2324
2452
|
maxTurns: opts.maxTurns,
|
|
2325
2453
|
effort,
|
|
2454
|
+
preferSubagent: opts.preferSubagent,
|
|
2326
2455
|
onLine: opts.log
|
|
2327
2456
|
});
|
|
2328
2457
|
const notes = collectNotes(wt.worktreePath);
|
|
@@ -2457,6 +2586,149 @@ Reviewing ${from.slice(0, 7)}..${to.slice(0, 7)}`);
|
|
|
2457
2586
|
}
|
|
2458
2587
|
__name(runReviewPass, "runReviewPass");
|
|
2459
2588
|
|
|
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);
|
|
2597
|
+
}
|
|
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;
|
|
2623
|
+
}
|
|
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);
|
|
2675
|
+
}
|
|
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"
|
|
2700
|
+
});
|
|
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
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
return out;
|
|
2716
|
+
}
|
|
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");
|
|
2731
|
+
|
|
2460
2732
|
// src/orchestrator.ts
|
|
2461
2733
|
var execFileAsync4 = promisify4(execFile4);
|
|
2462
2734
|
function formatErrorMessage(error) {
|
|
@@ -2527,21 +2799,7 @@ function parseStatus(body) {
|
|
|
2527
2799
|
return m ? m[1].trim().toLowerCase() : "";
|
|
2528
2800
|
}
|
|
2529
2801
|
__name(parseStatus, "parseStatus");
|
|
2530
|
-
function
|
|
2531
|
-
const m = body.match(/\*\*Blocked by:\*\*\s*(.+)/i);
|
|
2532
|
-
if (!m) return [];
|
|
2533
|
-
const text = m[1];
|
|
2534
|
-
if (/none/i.test(text)) return [];
|
|
2535
|
-
const nums = [];
|
|
2536
|
-
const re = /(\d+)/g;
|
|
2537
|
-
let match;
|
|
2538
|
-
while (match = re.exec(text)) {
|
|
2539
|
-
nums.push(Number(match[1]));
|
|
2540
|
-
}
|
|
2541
|
-
return nums;
|
|
2542
|
-
}
|
|
2543
|
-
__name(parseBlockers, "parseBlockers");
|
|
2544
|
-
function scanTickets(dir, feature = "") {
|
|
2802
|
+
function scanTickets(dir, feature = "", knownFeatures = /* @__PURE__ */ new Set()) {
|
|
2545
2803
|
const files = readdirSync2(dir).filter((f) => TICKET_RE.test(f)).sort((a, b) => a.localeCompare(
|
|
2546
2804
|
b,
|
|
2547
2805
|
void 0,
|
|
@@ -2558,7 +2816,10 @@ function scanTickets(dir, feature = "") {
|
|
|
2558
2816
|
filepath,
|
|
2559
2817
|
body,
|
|
2560
2818
|
status: parseStatus(body),
|
|
2561
|
-
blockers:
|
|
2819
|
+
blockers: provisionalRefs(
|
|
2820
|
+
parseBlockerField(body, knownFeatures).refs,
|
|
2821
|
+
feature
|
|
2822
|
+
),
|
|
2562
2823
|
feature
|
|
2563
2824
|
};
|
|
2564
2825
|
});
|
|
@@ -2568,15 +2829,35 @@ function ticketKey(feature, num) {
|
|
|
2568
2829
|
return `${feature}:${num}`;
|
|
2569
2830
|
}
|
|
2570
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");
|
|
2571
2854
|
function findReadyTickets(tickets) {
|
|
2572
2855
|
const doneKeys = new Set(
|
|
2573
2856
|
tickets.filter((t) => t.status === "done").map((t) => ticketKey(t.feature, t.num))
|
|
2574
2857
|
);
|
|
2575
2858
|
return tickets.filter((t) => {
|
|
2576
2859
|
if (t.status !== "ready-for-agent") return false;
|
|
2577
|
-
return t.blockers.every(
|
|
2578
|
-
(b) => doneKeys.has(ticketKey(t.feature, b))
|
|
2579
|
-
);
|
|
2860
|
+
return t.blockers.every((r) => blockerSatisfied(r, doneKeys));
|
|
2580
2861
|
});
|
|
2581
2862
|
}
|
|
2582
2863
|
__name(findReadyTickets, "findReadyTickets");
|
|
@@ -2585,14 +2866,19 @@ function findBlockedTickets(tickets, failedKeys) {
|
|
|
2585
2866
|
tickets.map((t) => [ticketKey(t.feature, t.num), t])
|
|
2586
2867
|
);
|
|
2587
2868
|
function collectDead(ticket, visited, found) {
|
|
2588
|
-
for (const
|
|
2589
|
-
const key = ticketKey(
|
|
2869
|
+
for (const ref of ticket.blockers) {
|
|
2870
|
+
const key = ticketKey(ref.feature, ref.num);
|
|
2590
2871
|
if (visited.has(key)) continue;
|
|
2591
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
|
+
}
|
|
2592
2878
|
const blocker = byKey.get(key);
|
|
2593
2879
|
if (!blocker || blocker.status === "done") continue;
|
|
2594
2880
|
if (failedKeys.has(key) || blocker.status !== "ready-for-agent") {
|
|
2595
|
-
found.
|
|
2881
|
+
found.set(key, ref);
|
|
2596
2882
|
continue;
|
|
2597
2883
|
}
|
|
2598
2884
|
collectDead(blocker, visited, found);
|
|
@@ -2604,9 +2890,9 @@ function findBlockedTickets(tickets, failedKeys) {
|
|
|
2604
2890
|
const key = ticketKey(t.feature, t.num);
|
|
2605
2891
|
if (t.status !== "ready-for-agent") continue;
|
|
2606
2892
|
if (failedKeys.has(key)) continue;
|
|
2607
|
-
const found = /* @__PURE__ */ new
|
|
2893
|
+
const found = /* @__PURE__ */ new Map();
|
|
2608
2894
|
collectDead(t, /* @__PURE__ */ new Set([key]), found);
|
|
2609
|
-
if (found.size > 0) blocked.set(key, [...found]);
|
|
2895
|
+
if (found.size > 0) blocked.set(key, [...found.values()]);
|
|
2610
2896
|
}
|
|
2611
2897
|
return blocked;
|
|
2612
2898
|
}
|
|
@@ -2786,6 +3072,7 @@ function renderPrompt(specBody, ticket, opts) {
|
|
|
2786
3072
|
"- Work ONLY on this ticket. Do not fix unrelated problems you notice.",
|
|
2787
3073
|
"- Do NOT research external APIs or services via web search. Use the spec and ticket body as your sole reference for API shapes.",
|
|
2788
3074
|
...installRule(opts.installCmd),
|
|
3075
|
+
...subagentRule(opts.preferSubagent),
|
|
2789
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.",
|
|
2790
3077
|
testGateRule(opts.testCmd, "ticket"),
|
|
2791
3078
|
LINT_RULE,
|
|
@@ -2821,6 +3108,7 @@ function renderMergePrompt(specBody, ticket, opts = {}) {
|
|
|
2821
3108
|
"",
|
|
2822
3109
|
"Do NOT re-implement the ticket from scratch.",
|
|
2823
3110
|
...installRule(opts.installCmd),
|
|
3111
|
+
...subagentRule(opts.preferSubagent),
|
|
2824
3112
|
"Do NOT read or modify files outside your current directory (the worktree).",
|
|
2825
3113
|
"Do NOT run git push."
|
|
2826
3114
|
].join("\n");
|
|
@@ -2829,7 +3117,7 @@ __name(renderMergePrompt, "renderMergePrompt");
|
|
|
2829
3117
|
function listTickets(tickets) {
|
|
2830
3118
|
console.log("\nTickets:");
|
|
2831
3119
|
for (const t of tickets) {
|
|
2832
|
-
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";
|
|
2833
3121
|
console.log(
|
|
2834
3122
|
` ${t.filename} (${t.status}) ${blockerStr}`
|
|
2835
3123
|
);
|
|
@@ -2853,11 +3141,7 @@ function* simulateWaves(tickets) {
|
|
|
2853
3141
|
if (sim.get(key) !== "ready-for-agent") {
|
|
2854
3142
|
return false;
|
|
2855
3143
|
}
|
|
2856
|
-
return t.blockers.every(
|
|
2857
|
-
(b) => doneKeys.has(
|
|
2858
|
-
ticketKey(t.feature, b)
|
|
2859
|
-
)
|
|
2860
|
-
);
|
|
3144
|
+
return t.blockers.every((r) => blockerSatisfied(r, doneKeys));
|
|
2861
3145
|
});
|
|
2862
3146
|
if (ready.length === 0) break;
|
|
2863
3147
|
yield { wave, ready };
|
|
@@ -2890,9 +3174,7 @@ function detectDeadlocks(tickets) {
|
|
|
2890
3174
|
(t) => ticketKey(t.feature, t.num) === key
|
|
2891
3175
|
);
|
|
2892
3176
|
if (ticket.blockers.every(
|
|
2893
|
-
(
|
|
2894
|
-
ticketKey(ticket.feature, b)
|
|
2895
|
-
)
|
|
3177
|
+
(r) => blockerSatisfied(r, doneKeys)
|
|
2896
3178
|
)) {
|
|
2897
3179
|
doneKeys.add(key);
|
|
2898
3180
|
readyKeys.delete(key);
|
|
@@ -2934,40 +3216,37 @@ function getProviderArgs(cmd) {
|
|
|
2934
3216
|
return PROVIDER_ARGS[name] ?? [];
|
|
2935
3217
|
}
|
|
2936
3218
|
__name(getProviderArgs, "getProviderArgs");
|
|
2937
|
-
function
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
obj = JSON.parse(line);
|
|
2941
|
-
} catch {
|
|
2942
|
-
return line;
|
|
2943
|
-
}
|
|
3219
|
+
function formatStreamObject(value) {
|
|
3220
|
+
if (!isRecord(value)) return null;
|
|
3221
|
+
const obj = value;
|
|
2944
3222
|
if (obj.type === "assistant") {
|
|
2945
|
-
|
|
2946
|
-
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;
|
|
2947
3226
|
if (!content) return null;
|
|
2948
3227
|
for (const block of content) {
|
|
2949
|
-
if (block
|
|
2950
|
-
|
|
2951
|
-
|
|
3228
|
+
if (!isRecord(block)) continue;
|
|
3229
|
+
if (block.type !== "text") continue;
|
|
3230
|
+
if (typeof block.text === "string") return block.text;
|
|
2952
3231
|
}
|
|
2953
3232
|
return null;
|
|
2954
3233
|
}
|
|
2955
3234
|
if (obj.type === "result") {
|
|
2956
|
-
const sub = obj.subtype
|
|
2957
|
-
const cost = obj.total_cost_usd;
|
|
2958
|
-
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)})` : "";
|
|
2959
3238
|
return `${sub}${costStr}`;
|
|
2960
3239
|
}
|
|
2961
3240
|
if (obj.type === "system") {
|
|
2962
|
-
const sub = obj.subtype;
|
|
3241
|
+
const sub = typeof obj.subtype === "string" ? obj.subtype : void 0;
|
|
2963
3242
|
if (sub === "api_retry") {
|
|
2964
|
-
const error = obj.error;
|
|
2965
|
-
return `retry: ${error
|
|
3243
|
+
const error = typeof obj.error === "string" ? obj.error : "unknown";
|
|
3244
|
+
return `retry: ${error}`;
|
|
2966
3245
|
}
|
|
2967
3246
|
}
|
|
2968
3247
|
return null;
|
|
2969
3248
|
}
|
|
2970
|
-
__name(
|
|
3249
|
+
__name(formatStreamObject, "formatStreamObject");
|
|
2971
3250
|
function createPiStreamFormatter() {
|
|
2972
3251
|
const calls = /* @__PURE__ */ new Map();
|
|
2973
3252
|
let textBuf = "";
|
|
@@ -3120,10 +3399,15 @@ function spawnAgent(opts) {
|
|
|
3120
3399
|
opts.logPath,
|
|
3121
3400
|
{ flags: "w" }
|
|
3122
3401
|
);
|
|
3402
|
+
const env = isClaude && opts.preferSubagent ? {
|
|
3403
|
+
...process.env,
|
|
3404
|
+
CLAUDE_CODE_SUBAGENT_MODEL: opts.preferSubagent
|
|
3405
|
+
} : void 0;
|
|
3123
3406
|
const proc = spawn2(opts.agentCmd, args, {
|
|
3124
3407
|
stdio: ["pipe", "pipe", "pipe"],
|
|
3125
3408
|
cwd: opts.cwd,
|
|
3126
|
-
detached: true
|
|
3409
|
+
detached: true,
|
|
3410
|
+
...env ? { env } : {}
|
|
3127
3411
|
});
|
|
3128
3412
|
if (proc.pid !== void 0) runningAgents.add(proc.pid);
|
|
3129
3413
|
proc.stdout.pipe(logStream, { end: false });
|
|
@@ -3141,10 +3425,26 @@ function spawnAgent(opts) {
|
|
|
3141
3425
|
}
|
|
3142
3426
|
}
|
|
3143
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");
|
|
3144
3444
|
const rl = createInterface({ input: proc.stdout });
|
|
3145
3445
|
rl.on("line", (line) => {
|
|
3146
3446
|
if (isClaude) {
|
|
3147
|
-
const formatted =
|
|
3447
|
+
const formatted = formatClaudeLine(line);
|
|
3148
3448
|
if (formatted !== null) {
|
|
3149
3449
|
collectCompleted(formatted);
|
|
3150
3450
|
const output = `${prefix}${formatted}`;
|
|
@@ -3236,7 +3536,12 @@ function spawnAgent(opts) {
|
|
|
3236
3536
|
rlErr.close();
|
|
3237
3537
|
logStream.end();
|
|
3238
3538
|
if (promptFile) cleanupPromptFile(promptFile);
|
|
3239
|
-
resolve3({
|
|
3539
|
+
resolve3({
|
|
3540
|
+
exitCode,
|
|
3541
|
+
completedItems,
|
|
3542
|
+
timedOut,
|
|
3543
|
+
...isClaude ? streamStats.stats : {}
|
|
3544
|
+
});
|
|
3240
3545
|
}
|
|
3241
3546
|
__name(finish, "finish");
|
|
3242
3547
|
proc.on("exit", (code) => {
|
|
@@ -3280,7 +3585,7 @@ function spawnAgent(opts) {
|
|
|
3280
3585
|
});
|
|
3281
3586
|
}
|
|
3282
3587
|
__name(spawnAgent, "spawnAgent");
|
|
3283
|
-
function dryRun(tickets, specBody, testCmd) {
|
|
3588
|
+
function dryRun(tickets, specBody, testCmd, preferSubagent) {
|
|
3284
3589
|
listTickets(tickets);
|
|
3285
3590
|
console.log("\nSimulated wave order:");
|
|
3286
3591
|
for (const { wave, ready } of simulateWaves(tickets)) {
|
|
@@ -3296,6 +3601,7 @@ function dryRun(tickets, specBody, testCmd) {
|
|
|
3296
3601
|
}
|
|
3297
3602
|
console.log(`
|
|
3298
3603
|
Test command: ${testCmd || "(none)"}`);
|
|
3604
|
+
console.log(`Prefer subagent: ${preferSubagent || "(none)"}`);
|
|
3299
3605
|
const firstReady = findReadyTickets(tickets);
|
|
3300
3606
|
if (firstReady.length > 0) {
|
|
3301
3607
|
console.log(
|
|
@@ -3307,7 +3613,7 @@ Prompt for first ready ticket (${firstReady[0].filename}):
|
|
|
3307
3613
|
renderPrompt(
|
|
3308
3614
|
specBody,
|
|
3309
3615
|
firstReady[0],
|
|
3310
|
-
{ testCmd }
|
|
3616
|
+
{ testCmd, preferSubagent }
|
|
3311
3617
|
)
|
|
3312
3618
|
);
|
|
3313
3619
|
}
|
|
@@ -3353,7 +3659,8 @@ Prompt for first ready ticket (${firstReady[0].filename}):
|
|
|
3353
3659
|
firstReady[0],
|
|
3354
3660
|
{
|
|
3355
3661
|
testCmd: opts.testCmd,
|
|
3356
|
-
installCmd: opts.installCmd
|
|
3662
|
+
installCmd: opts.installCmd,
|
|
3663
|
+
preferSubagent: opts.preferSubagent
|
|
3357
3664
|
}
|
|
3358
3665
|
)
|
|
3359
3666
|
);
|
|
@@ -3369,6 +3676,9 @@ Test command: ${opts.testCmd || "(none)"}`
|
|
|
3369
3676
|
console.log(
|
|
3370
3677
|
`Effort: ${opts.effort ?? "(provider default)"}`
|
|
3371
3678
|
);
|
|
3679
|
+
console.log(
|
|
3680
|
+
`Prefer subagent: ${opts.preferSubagent || "(none)"}`
|
|
3681
|
+
);
|
|
3372
3682
|
console.log(
|
|
3373
3683
|
`Retries: ${opts.retries ?? DEFAULT_RETRIES}`
|
|
3374
3684
|
);
|
|
@@ -3469,7 +3779,10 @@ async function respawnAndRetryMerge(opts) {
|
|
|
3469
3779
|
const prompt = renderMergePrompt(
|
|
3470
3780
|
opts.specBody,
|
|
3471
3781
|
opts.ticket,
|
|
3472
|
-
{
|
|
3782
|
+
{
|
|
3783
|
+
installCmd: opts.installCmd,
|
|
3784
|
+
preferSubagent: opts.preferSubagent
|
|
3785
|
+
}
|
|
3473
3786
|
);
|
|
3474
3787
|
const logPath2 = join6(
|
|
3475
3788
|
opts.logsDir,
|
|
@@ -3488,8 +3801,10 @@ async function respawnAndRetryMerge(opts) {
|
|
|
3488
3801
|
cwd: opts.worktreePath,
|
|
3489
3802
|
label,
|
|
3490
3803
|
maxTurns: opts.maxTurns,
|
|
3804
|
+
preferSubagent: opts.preferSubagent,
|
|
3491
3805
|
onLine: opts.onLine
|
|
3492
3806
|
});
|
|
3807
|
+
opts.onAgentRun?.(result);
|
|
3493
3808
|
if (result.exitCode !== 0) {
|
|
3494
3809
|
const msg2 = ` ${opts.ticket.filename}: respawned agent exited ${result.exitCode}`;
|
|
3495
3810
|
if (opts.onLine) {
|
|
@@ -3574,17 +3889,29 @@ function attemptLogPath(logsDir, ticket, attempt, ext) {
|
|
|
3574
3889
|
return join6(logsDir, `${stem}${suffix}${ext}`);
|
|
3575
3890
|
}
|
|
3576
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");
|
|
3577
3912
|
function buildHistory(learningsPath, ticket, d) {
|
|
3578
3913
|
try {
|
|
3579
|
-
return
|
|
3580
|
-
readEntries(learningsPath),
|
|
3581
|
-
{
|
|
3582
|
-
key: ticketKey(ticket.feature, ticket.num),
|
|
3583
|
-
blockerKeys: ticket.blockers.map((b) => {
|
|
3584
|
-
return ticketKey(ticket.feature, b);
|
|
3585
|
-
})
|
|
3586
|
-
}
|
|
3587
|
-
));
|
|
3914
|
+
return historyFor(learningsPath, ticket);
|
|
3588
3915
|
} catch (err) {
|
|
3589
3916
|
d.log(
|
|
3590
3917
|
` ${ticket.filename}: could not read learnings: ${formatErrorMessage(err)}`
|
|
@@ -3661,7 +3988,8 @@ async function runTicketAttempt(opts) {
|
|
|
3661
3988
|
opts.learningsPath,
|
|
3662
3989
|
ticket,
|
|
3663
3990
|
d
|
|
3664
|
-
)
|
|
3991
|
+
),
|
|
3992
|
+
preferSubagent: opts.preferSubagent
|
|
3665
3993
|
}
|
|
3666
3994
|
),
|
|
3667
3995
|
agentCmd: opts.agentCmd,
|
|
@@ -3677,8 +4005,11 @@ async function runTicketAttempt(opts) {
|
|
|
3677
4005
|
label: ticket.filename.replace(/\.md$/, ""),
|
|
3678
4006
|
maxTurns: opts.maxTurns,
|
|
3679
4007
|
effort: opts.effort,
|
|
4008
|
+
preferSubagent: opts.preferSubagent,
|
|
4009
|
+
onStats: opts.onStats,
|
|
3680
4010
|
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
3681
4011
|
});
|
|
4012
|
+
opts.onAgentRun?.(agentResult);
|
|
3682
4013
|
let notes = null;
|
|
3683
4014
|
try {
|
|
3684
4015
|
notes = collectNotes(worktree.worktreePath);
|
|
@@ -3691,7 +4022,9 @@ async function runTicketAttempt(opts) {
|
|
|
3691
4022
|
exitCode: agentResult.exitCode,
|
|
3692
4023
|
completedItems: agentResult.completedItems,
|
|
3693
4024
|
timedOut: agentResult.timedOut,
|
|
3694
|
-
notes
|
|
4025
|
+
notes,
|
|
4026
|
+
...agentResult.subagents === void 0 ? {} : { subagents: agentResult.subagents },
|
|
4027
|
+
...agentResult.costUsd === void 0 ? {} : { costUsd: agentResult.costUsd }
|
|
3695
4028
|
};
|
|
3696
4029
|
if (agentResult.timedOut) {
|
|
3697
4030
|
d.log(
|
|
@@ -3859,6 +4192,10 @@ function recordAttempt(opts) {
|
|
|
3859
4192
|
__name(recordAttempt, "recordAttempt");
|
|
3860
4193
|
async function runTicketAttempts(opts) {
|
|
3861
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");
|
|
3862
4199
|
return runWithRetries({
|
|
3863
4200
|
maxAttempts: opts.retries + 1,
|
|
3864
4201
|
retryDelay: opts.retryDelay,
|
|
@@ -3883,7 +4220,12 @@ async function runTicketAttempts(opts) {
|
|
|
3883
4220
|
outcome = await runTicketAttempt({
|
|
3884
4221
|
...opts,
|
|
3885
4222
|
attempt,
|
|
3886
|
-
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")
|
|
3887
4229
|
});
|
|
3888
4230
|
} catch (error) {
|
|
3889
4231
|
d.log(
|
|
@@ -3941,15 +4283,77 @@ function indentFilenames(tickets) {
|
|
|
3941
4283
|
return tickets.map((t) => ` ${t.filename}`).join("\n");
|
|
3942
4284
|
}
|
|
3943
4285
|
__name(indentFilenames, "indentFilenames");
|
|
3944
|
-
function
|
|
4286
|
+
function explainOpen(tickets, failedKeys, warnings) {
|
|
3945
4287
|
const remaining = tickets.filter(
|
|
3946
4288
|
(t) => t.status === "ready-for-agent"
|
|
3947
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 = [];
|
|
3948
4296
|
const human = tickets.filter((t) => t.status === HUMAN_STATUS);
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
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")
|
|
3952
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"
|
|
4355
|
+
);
|
|
4356
|
+
const human = tickets.filter((t) => t.status === HUMAN_STATUS);
|
|
3953
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` : "");
|
|
3954
4358
|
const lines = [counts];
|
|
3955
4359
|
const features = new Set(
|
|
@@ -3978,26 +4382,54 @@ function renderFinishSummary(tickets, totalCompleted, failedKeys) {
|
|
|
3978
4382
|
lines.push(detail);
|
|
3979
4383
|
}
|
|
3980
4384
|
}
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
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])
|
|
3987
4401
|
);
|
|
3988
|
-
|
|
3989
|
-
|
|
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) {
|
|
3990
4416
|
lines.push(
|
|
3991
|
-
|
|
3992
|
-
|
|
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."
|
|
3993
4428
|
);
|
|
3994
4429
|
}
|
|
3995
|
-
lines.push(
|
|
3996
|
-
"Do that work by hand, then set each ticket's **Status:** to done" + (blockedByHuman.length > 0 ? " and run nightralph again." : ".")
|
|
3997
|
-
);
|
|
3998
4430
|
return lines.join("\n");
|
|
3999
4431
|
}
|
|
4000
|
-
__name(
|
|
4432
|
+
__name(renderNothingReady, "renderNothingReady");
|
|
4001
4433
|
function sessionTickets(tickets) {
|
|
4002
4434
|
return [...tickets].filter((t) => t.status === "ready-for-agent").sort((a, b) => a.feature.localeCompare(b.feature) || a.num - b.num);
|
|
4003
4435
|
}
|
|
@@ -4016,7 +4448,10 @@ function reportBlocked(d, tickets, failedKeys) {
|
|
|
4016
4448
|
)) {
|
|
4017
4449
|
const ticket = byKey.get(key);
|
|
4018
4450
|
if (!ticket) continue;
|
|
4019
|
-
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
|
+
});
|
|
4020
4455
|
d.setBlocked(ticket.num, ticket.feature, labels);
|
|
4021
4456
|
}
|
|
4022
4457
|
}
|
|
@@ -4028,7 +4463,7 @@ function runProgress(tickets) {
|
|
|
4028
4463
|
};
|
|
4029
4464
|
}
|
|
4030
4465
|
__name(runProgress, "runProgress");
|
|
4031
|
-
async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, testCmd, display, maxTurns, effort, retries, retryDelay, concurrency) {
|
|
4466
|
+
async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, testCmd, display, maxTurns, effort, retries, retryDelay, concurrency, warnings = [], preferSubagent) {
|
|
4032
4467
|
mkdirSync3(logsDir, { recursive: true });
|
|
4033
4468
|
const d = display ?? createDisplay();
|
|
4034
4469
|
const maxAttempts = (retries ?? DEFAULT_RETRIES) + 1;
|
|
@@ -4042,6 +4477,7 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
4042
4477
|
let waveNum = 0;
|
|
4043
4478
|
const failedKeys = /* @__PURE__ */ new Set();
|
|
4044
4479
|
const session = sessionTickets(tickets);
|
|
4480
|
+
let runStats = emptyRunStats();
|
|
4045
4481
|
while (true) {
|
|
4046
4482
|
if (waveNum > 0) reportBlocked(d, tickets, failedKeys);
|
|
4047
4483
|
const ready = findReadyTickets(tickets).filter(
|
|
@@ -4072,7 +4508,7 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
4072
4508
|
const prompt = renderPrompt(
|
|
4073
4509
|
specBody,
|
|
4074
4510
|
t,
|
|
4075
|
-
{ testCmd }
|
|
4511
|
+
{ testCmd, preferSubagent }
|
|
4076
4512
|
);
|
|
4077
4513
|
const label = t.filename.replace(
|
|
4078
4514
|
/\.md$/,
|
|
@@ -4084,6 +4520,18 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
4084
4520
|
t.feature
|
|
4085
4521
|
);
|
|
4086
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");
|
|
4087
4535
|
let level = effort;
|
|
4088
4536
|
let result = await spawnAgent({
|
|
4089
4537
|
prompt,
|
|
@@ -4094,8 +4542,11 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
4094
4542
|
label,
|
|
4095
4543
|
maxTurns,
|
|
4096
4544
|
effort: level,
|
|
4545
|
+
preferSubagent,
|
|
4546
|
+
onStats,
|
|
4097
4547
|
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
4098
4548
|
});
|
|
4549
|
+
tally(result);
|
|
4099
4550
|
for (let attempt = 2; result.exitCode !== 0 && !result.timedOut && attempt <= maxAttempts; attempt++) {
|
|
4100
4551
|
if (isKnown) {
|
|
4101
4552
|
level = escalateEffort(
|
|
@@ -4122,8 +4573,11 @@ async function runWaves(tickets, specBody, agentCmd, model, timeout, logsDir, te
|
|
|
4122
4573
|
label,
|
|
4123
4574
|
maxTurns,
|
|
4124
4575
|
effort: level,
|
|
4576
|
+
preferSubagent,
|
|
4577
|
+
onStats,
|
|
4125
4578
|
onLine: /* @__PURE__ */ __name((line) => d.log(line), "onLine")
|
|
4126
4579
|
});
|
|
4580
|
+
tally(result);
|
|
4127
4581
|
}
|
|
4128
4582
|
return { ticket: t, result };
|
|
4129
4583
|
}
|
|
@@ -4182,8 +4636,11 @@ No tickets completed this wave. ${remaining2.length} ready ticket${remaining2.le
|
|
|
4182
4636
|
d.log("\n" + renderFinishSummary(
|
|
4183
4637
|
tickets,
|
|
4184
4638
|
totalCompleted,
|
|
4185
|
-
failedKeys
|
|
4639
|
+
failedKeys,
|
|
4640
|
+
warnings
|
|
4186
4641
|
));
|
|
4642
|
+
const statsLine = renderRunStats(runStats);
|
|
4643
|
+
if (statsLine) d.log(statsLine);
|
|
4187
4644
|
d.cleanup();
|
|
4188
4645
|
return { allDone: remaining.length === 0 };
|
|
4189
4646
|
}
|
|
@@ -4270,6 +4727,10 @@ async function runWavesIsolated(opts) {
|
|
|
4270
4727
|
let waveNum = 0;
|
|
4271
4728
|
const failedKeys = /* @__PURE__ */ new Set();
|
|
4272
4729
|
const session = sessionTickets(opts.tickets);
|
|
4730
|
+
let runStats = emptyRunStats();
|
|
4731
|
+
const onAgentRun = /* @__PURE__ */ __name((result) => {
|
|
4732
|
+
runStats = addRun(runStats, result);
|
|
4733
|
+
}, "onAgentRun");
|
|
4273
4734
|
while (true) {
|
|
4274
4735
|
if (waveNum > 0) reportBlocked(d, opts.tickets, failedKeys);
|
|
4275
4736
|
const ready = findReadyTickets(opts.tickets).filter(
|
|
@@ -4311,12 +4772,14 @@ async function runWavesIsolated(opts) {
|
|
|
4311
4772
|
installCmd: opts.installCmd,
|
|
4312
4773
|
maxTurns: opts.maxTurns,
|
|
4313
4774
|
effort: opts.effort,
|
|
4775
|
+
preferSubagent: opts.preferSubagent,
|
|
4314
4776
|
learningsPath: learningsPathFor(
|
|
4315
4777
|
tracker.pathFor(t.feature)
|
|
4316
4778
|
),
|
|
4317
4779
|
retries,
|
|
4318
4780
|
retryDelay,
|
|
4319
|
-
d
|
|
4781
|
+
d,
|
|
4782
|
+
onAgentRun
|
|
4320
4783
|
})
|
|
4321
4784
|
);
|
|
4322
4785
|
const touched = new Set(ready.map((t) => {
|
|
@@ -4536,7 +4999,9 @@ async function runWavesIsolated(opts) {
|
|
|
4536
4999
|
testCmd: opts.testCmd,
|
|
4537
5000
|
installCmd: opts.installCmd,
|
|
4538
5001
|
maxTurns: opts.maxTurns,
|
|
4539
|
-
|
|
5002
|
+
preferSubagent: opts.preferSubagent,
|
|
5003
|
+
onLine: /* @__PURE__ */ __name((msg) => d.log(msg), "onLine"),
|
|
5004
|
+
onAgentRun
|
|
4540
5005
|
}) : false;
|
|
4541
5006
|
if (recovered) {
|
|
4542
5007
|
markDone(ticket);
|
|
@@ -4693,10 +5158,18 @@ No tickets completed this wave. ${remaining2.length} ready ticket${remaining2.le
|
|
|
4693
5158
|
installCmd: opts.installCmd,
|
|
4694
5159
|
maxTurns: opts.maxTurns,
|
|
4695
5160
|
effort: opts.effort,
|
|
5161
|
+
preferSubagent: opts.preferSubagent,
|
|
4696
5162
|
retries,
|
|
4697
5163
|
retryDelay,
|
|
4698
5164
|
log: /* @__PURE__ */ __name((msg) => d.log(msg), "log")
|
|
4699
|
-
}, {
|
|
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
|
+
});
|
|
4700
5173
|
reviewLine = renderReviewSummary(
|
|
4701
5174
|
outcome,
|
|
4702
5175
|
relative2(repoRoot, opts.review.reviewPath)
|
|
@@ -4712,8 +5185,11 @@ Review failed: ${formatErrorMessage(err)}`);
|
|
|
4712
5185
|
d.log("\n" + renderFinishSummary(
|
|
4713
5186
|
opts.tickets,
|
|
4714
5187
|
totalCompleted,
|
|
4715
|
-
failedKeys
|
|
5188
|
+
failedKeys,
|
|
5189
|
+
opts.warnings ?? []
|
|
4716
5190
|
));
|
|
5191
|
+
const statsLine = renderRunStats(runStats);
|
|
5192
|
+
if (statsLine) d.log(statsLine);
|
|
4717
5193
|
if (reviewLine) d.log(reviewLine);
|
|
4718
5194
|
d.log(`Work is on ${baseBranch}`);
|
|
4719
5195
|
d.cleanup();
|
|
@@ -4797,6 +5273,86 @@ function findFeatureDirs(cwd) {
|
|
|
4797
5273
|
return results;
|
|
4798
5274
|
}
|
|
4799
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");
|
|
4800
5356
|
var ALL_VALUE = "__all__";
|
|
4801
5357
|
async function resolveIssuesDir(specName, cwd = process.cwd()) {
|
|
4802
5358
|
if (specName) {
|
|
@@ -4819,14 +5375,11 @@ async function resolveIssuesDir(specName, cwd = process.cwd()) {
|
|
|
4819
5375
|
console.log(`Using feature: ${f.name}`);
|
|
4820
5376
|
return [{ dir: f.dir, spec: f.spec, name: f.name }];
|
|
4821
5377
|
}
|
|
4822
|
-
const
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
value: f.name
|
|
4828
|
-
};
|
|
4829
|
-
});
|
|
5378
|
+
const scratchDir = join8(cwd, ".scratch");
|
|
5379
|
+
const featureChoices = features.map((f) => ({
|
|
5380
|
+
name: featureChoiceLabel(scratchDir, f.name),
|
|
5381
|
+
value: f.name
|
|
5382
|
+
}));
|
|
4830
5383
|
while (true) {
|
|
4831
5384
|
const selected = await checkbox({
|
|
4832
5385
|
message: "Which features?",
|
|
@@ -4933,6 +5486,9 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
|
|
|
4933
5486
|
type: "string",
|
|
4934
5487
|
choices: ALL_EFFORT_LEVELS,
|
|
4935
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.'
|
|
4936
5492
|
}).option("retries", {
|
|
4937
5493
|
type: "number",
|
|
4938
5494
|
default: DEFAULT_RETRIES,
|
|
@@ -4962,6 +5518,7 @@ yargs(hideBin(process.argv)).scriptName("nightralph").usage("$0 <command>").comm
|
|
|
4962
5518
|
stopOnConflict: argv.X,
|
|
4963
5519
|
maxTurns: argv.maxTurns ?? argv.turns,
|
|
4964
5520
|
effort: argv.effort,
|
|
5521
|
+
preferSubagent: argv.preferSubagent,
|
|
4965
5522
|
retries: argv.retries,
|
|
4966
5523
|
retryDelay: argv.retryDelay,
|
|
4967
5524
|
concurrency: argv.concurrency,
|
|
@@ -5006,6 +5563,8 @@ function loadTickets(selections, log) {
|
|
|
5006
5563
|
let firstFeatureName = "";
|
|
5007
5564
|
let firstLogsDir = "";
|
|
5008
5565
|
const missing = [];
|
|
5566
|
+
const valid = [];
|
|
5567
|
+
const specs = /* @__PURE__ */ new Map();
|
|
5009
5568
|
for (const selection of selections) {
|
|
5010
5569
|
const { dir, spec, name } = selection;
|
|
5011
5570
|
if (!existsSync7(dir)) {
|
|
@@ -5016,11 +5575,18 @@ function loadTickets(selections, log) {
|
|
|
5016
5575
|
missing.push(`Spec file not found: ${spec}`);
|
|
5017
5576
|
continue;
|
|
5018
5577
|
}
|
|
5019
|
-
|
|
5578
|
+
specs.set(name, stripSpecSection(
|
|
5020
5579
|
readFileSync7(spec, "utf8"),
|
|
5021
5580
|
"User Stories"
|
|
5022
|
-
);
|
|
5023
|
-
|
|
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);
|
|
5024
5590
|
if (tickets.length === 0) {
|
|
5025
5591
|
log(
|
|
5026
5592
|
`No ticket files found for ${name}. Skipping.`
|
|
@@ -5030,23 +5596,7 @@ function loadTickets(selections, log) {
|
|
|
5030
5596
|
log(
|
|
5031
5597
|
`[${name}] Found ${tickets.length} tickets`
|
|
5032
5598
|
);
|
|
5033
|
-
|
|
5034
|
-
tickets.map((t) => t.num)
|
|
5035
|
-
);
|
|
5036
|
-
for (const t of tickets) {
|
|
5037
|
-
const unknown = t.blockers.filter(
|
|
5038
|
-
(b) => !knownNums.has(b)
|
|
5039
|
-
);
|
|
5040
|
-
if (unknown.length > 0) {
|
|
5041
|
-
log(
|
|
5042
|
-
`${t.filename}: blockers [${unknown.join(", ")}] not found in directory`
|
|
5043
|
-
);
|
|
5044
|
-
}
|
|
5045
|
-
t.blockers = t.blockers.filter(
|
|
5046
|
-
(b) => knownNums.has(b)
|
|
5047
|
-
);
|
|
5048
|
-
}
|
|
5049
|
-
specBodies.set(name, specBody);
|
|
5599
|
+
specBodies.set(name, specs.get(name));
|
|
5050
5600
|
progressPaths.set(
|
|
5051
5601
|
name,
|
|
5052
5602
|
join9(dirname6(dir), "progress.md")
|
|
@@ -5065,7 +5615,8 @@ function loadTickets(selections, log) {
|
|
|
5065
5615
|
issuesDirs,
|
|
5066
5616
|
firstFeatureName,
|
|
5067
5617
|
firstLogsDir,
|
|
5068
|
-
missing
|
|
5618
|
+
missing,
|
|
5619
|
+
warnings: loaded.warnings
|
|
5069
5620
|
};
|
|
5070
5621
|
}
|
|
5071
5622
|
__name(loadTickets, "loadTickets");
|
|
@@ -5078,7 +5629,7 @@ function loadTicketsOrExit(selections) {
|
|
|
5078
5629
|
return loaded;
|
|
5079
5630
|
}
|
|
5080
5631
|
__name(loadTicketsOrExit, "loadTicketsOrExit");
|
|
5081
|
-
function
|
|
5632
|
+
function checkoutLaunchBranch(plan) {
|
|
5082
5633
|
try {
|
|
5083
5634
|
execFileSync2(
|
|
5084
5635
|
"git",
|
|
@@ -5087,6 +5638,10 @@ function failStaleResume(plan, problems) {
|
|
|
5087
5638
|
);
|
|
5088
5639
|
} catch {
|
|
5089
5640
|
}
|
|
5641
|
+
}
|
|
5642
|
+
__name(checkoutLaunchBranch, "checkoutLaunchBranch");
|
|
5643
|
+
function failStaleResume(plan, problems) {
|
|
5644
|
+
checkoutLaunchBranch(plan);
|
|
5090
5645
|
const n = plan.behind;
|
|
5091
5646
|
console.error(
|
|
5092
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}.`
|
|
@@ -5094,6 +5649,25 @@ function failStaleResume(plan, problems) {
|
|
|
5094
5649
|
process.exit(2);
|
|
5095
5650
|
}
|
|
5096
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");
|
|
5097
5671
|
function reviewPathFor(issuesDirs) {
|
|
5098
5672
|
const featureDir = dirname6(issuesDirs[0]);
|
|
5099
5673
|
return issuesDirs.length === 1 ? join9(featureDir, "review.md") : join9(dirname6(featureDir), "review.md");
|
|
@@ -5105,6 +5679,14 @@ async function runExecute(argv) {
|
|
|
5105
5679
|
);
|
|
5106
5680
|
let loaded = loadTicketsOrExit(selections);
|
|
5107
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
|
+
}
|
|
5108
5690
|
const providerPath = resolveExecutable(argv.provider);
|
|
5109
5691
|
const providerIsPath = argv.provider.includes("/");
|
|
5110
5692
|
const providerMissing = providerIsPath ? "not found or not executable" : "not found on PATH";
|
|
@@ -5115,6 +5697,14 @@ async function runExecute(argv) {
|
|
|
5115
5697
|
process.exit(2);
|
|
5116
5698
|
}
|
|
5117
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
|
+
}
|
|
5118
5708
|
const isolated = isGitRepo();
|
|
5119
5709
|
const featureNames = [...loaded.specBodies.keys()];
|
|
5120
5710
|
let plan = null;
|
|
@@ -5144,6 +5734,9 @@ async function runExecute(argv) {
|
|
|
5144
5734
|
if (loaded.allTickets.length === 0) {
|
|
5145
5735
|
failStaleResume(plan, ["no tickets"]);
|
|
5146
5736
|
}
|
|
5737
|
+
if (nothingReady(loaded.allTickets)) {
|
|
5738
|
+
stopNothingReady(plan, loaded);
|
|
5739
|
+
}
|
|
5147
5740
|
}
|
|
5148
5741
|
}
|
|
5149
5742
|
const {
|
|
@@ -5183,7 +5776,8 @@ async function runExecute(argv) {
|
|
|
5183
5776
|
effort: argv.effort,
|
|
5184
5777
|
retries: argv.retries,
|
|
5185
5778
|
retryDelay: argv.retryDelay,
|
|
5186
|
-
concurrency: argv.concurrency
|
|
5779
|
+
concurrency: argv.concurrency,
|
|
5780
|
+
preferSubagent
|
|
5187
5781
|
});
|
|
5188
5782
|
console.log(
|
|
5189
5783
|
describeIntegrationBranch(
|
|
@@ -5194,7 +5788,12 @@ async function runExecute(argv) {
|
|
|
5194
5788
|
const reviewState = argv.review === false ? "off" : "on";
|
|
5195
5789
|
console.log(`Review: ${reviewState}`);
|
|
5196
5790
|
} else {
|
|
5197
|
-
dryRun(
|
|
5791
|
+
dryRun(
|
|
5792
|
+
allTickets,
|
|
5793
|
+
fallbackSpec,
|
|
5794
|
+
testCmd,
|
|
5795
|
+
preferSubagent
|
|
5796
|
+
);
|
|
5198
5797
|
}
|
|
5199
5798
|
return;
|
|
5200
5799
|
}
|
|
@@ -5227,7 +5826,9 @@ async function runExecute(argv) {
|
|
|
5227
5826
|
retries: argv.retries,
|
|
5228
5827
|
retryDelay: argv.retryDelay,
|
|
5229
5828
|
concurrency: argv.concurrency,
|
|
5829
|
+
preferSubagent,
|
|
5230
5830
|
progressPaths,
|
|
5831
|
+
warnings: loaded.warnings,
|
|
5231
5832
|
review: argv.review === false ? void 0 : {
|
|
5232
5833
|
reviewPath: reviewPathFor(loaded.issuesDirs),
|
|
5233
5834
|
forkedFrom: plan?.forkedFrom ?? void 0
|
|
@@ -5247,7 +5848,9 @@ async function runExecute(argv) {
|
|
|
5247
5848
|
argv.effort,
|
|
5248
5849
|
argv.retries,
|
|
5249
5850
|
argv.retryDelay,
|
|
5250
|
-
argv.concurrency
|
|
5851
|
+
argv.concurrency,
|
|
5852
|
+
loaded.warnings,
|
|
5853
|
+
preferSubagent
|
|
5251
5854
|
);
|
|
5252
5855
|
}
|
|
5253
5856
|
if (!result.allDone) {
|