@gethmy/agent 1.10.8 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1115 -332
- package/dist/index.js +1115 -332
- package/package.json +4 -3
package/dist/cli.js
CHANGED
|
@@ -385,11 +385,12 @@ var DEFAULT_AGENT_CONFIG, IN_PROGRESS_COLUMN = "In Progress", NEED_REVIEW_LABEL
|
|
|
385
385
|
var init_types = __esm(() => {
|
|
386
386
|
init_plan_phase();
|
|
387
387
|
DEFAULT_AGENT_CONFIG = {
|
|
388
|
-
poolSize:
|
|
388
|
+
poolSize: 6,
|
|
389
389
|
maxTimeout: 1800000,
|
|
390
390
|
pickupColumns: ["To Do"],
|
|
391
391
|
priorityLabels: { urgent: 100, critical: 90, bug: 50 },
|
|
392
392
|
columnBoost: true,
|
|
393
|
+
runner: "cli",
|
|
393
394
|
completion: {
|
|
394
395
|
createPR: false,
|
|
395
396
|
moveToColumn: "Review",
|
|
@@ -424,13 +425,14 @@ var init_types = __esm(() => {
|
|
|
424
425
|
autoFix: true,
|
|
425
426
|
maxFixAttempts: 1,
|
|
426
427
|
deepReview: false,
|
|
428
|
+
revertGuard: true,
|
|
427
429
|
devServerBasePort: 4200,
|
|
428
430
|
timeout: 120000,
|
|
429
431
|
failColumn: "To Do"
|
|
430
432
|
},
|
|
431
433
|
review: {
|
|
432
434
|
enabled: true,
|
|
433
|
-
poolSize:
|
|
435
|
+
poolSize: 3,
|
|
434
436
|
pickupColumns: ["Review"],
|
|
435
437
|
moveToColumn: "Done",
|
|
436
438
|
failColumn: "To Do",
|
|
@@ -562,6 +564,9 @@ function loadDaemonConfig() {
|
|
|
562
564
|
...agentOverrides.planning ?? {}
|
|
563
565
|
}
|
|
564
566
|
};
|
|
567
|
+
if (agent.runner !== "cli" && agent.runner !== "sdk") {
|
|
568
|
+
agent.runner = "cli";
|
|
569
|
+
}
|
|
565
570
|
return {
|
|
566
571
|
apiKey,
|
|
567
572
|
apiUrl,
|
|
@@ -1209,6 +1214,23 @@ var init_pm = __esm(() => {
|
|
|
1209
1214
|
import { execFileSync as execFileSync3, execSync as execSync2 } from "node:child_process";
|
|
1210
1215
|
import { existsSync as existsSync2, rmSync } from "node:fs";
|
|
1211
1216
|
import { resolve } from "node:path";
|
|
1217
|
+
function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root, branch) => execFileSync3("git", ["fetch", "origin", branch], {
|
|
1218
|
+
cwd: root,
|
|
1219
|
+
stdio: "pipe"
|
|
1220
|
+
})) {
|
|
1221
|
+
let lastErr;
|
|
1222
|
+
for (let attempt = 1;attempt <= attempts; attempt++) {
|
|
1223
|
+
try {
|
|
1224
|
+
fetchImpl(repoRoot, baseBranch);
|
|
1225
|
+
return;
|
|
1226
|
+
} catch (err) {
|
|
1227
|
+
lastErr = err;
|
|
1228
|
+
log.warn(TAG5, `fetch origin ${baseBranch} failed (attempt ${attempt}/${attempts})`);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
const detail = lastErr instanceof Error ? lastErr.message : String(lastErr);
|
|
1232
|
+
throw new WorktreeBaseError(`Could not fetch origin/${baseBranch} after ${attempts} attempts — ` + `refusing to build on a stale base. ${detail}`);
|
|
1233
|
+
}
|
|
1212
1234
|
function createWorktree(basePath, baseBranch, branchName) {
|
|
1213
1235
|
const repoRoot = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
|
|
1214
1236
|
encoding: "utf-8"
|
|
@@ -1224,14 +1246,7 @@ function createWorktree(basePath, baseBranch, branchName) {
|
|
|
1224
1246
|
stdio: "pipe"
|
|
1225
1247
|
});
|
|
1226
1248
|
} catch {}
|
|
1227
|
-
|
|
1228
|
-
execFileSync3("git", ["fetch", "origin", baseBranch], {
|
|
1229
|
-
cwd: repoRoot,
|
|
1230
|
-
stdio: "pipe"
|
|
1231
|
-
});
|
|
1232
|
-
} catch {
|
|
1233
|
-
log.warn(TAG5, "Failed to fetch latest — continuing with local state");
|
|
1234
|
-
}
|
|
1249
|
+
fetchBaseBranch(repoRoot, baseBranch);
|
|
1235
1250
|
log.info(TAG5, `Creating worktree: ${worktreeDir} (branch: ${branchName})`);
|
|
1236
1251
|
try {
|
|
1237
1252
|
execFileSync3("git", [
|
|
@@ -1319,10 +1334,16 @@ function makeBranchName(shortId, title, prefix = "agent-attempts/") {
|
|
|
1319
1334
|
const slug = title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
1320
1335
|
return `${prefix}${shortId}-${slug || "task"}`;
|
|
1321
1336
|
}
|
|
1322
|
-
var TAG5 = "worktree";
|
|
1337
|
+
var TAG5 = "worktree", WorktreeBaseError;
|
|
1323
1338
|
var init_worktree = __esm(() => {
|
|
1324
1339
|
init_log();
|
|
1325
1340
|
init_pm();
|
|
1341
|
+
WorktreeBaseError = class WorktreeBaseError extends Error {
|
|
1342
|
+
constructor(message) {
|
|
1343
|
+
super(message);
|
|
1344
|
+
this.name = "WorktreeBaseError";
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1326
1347
|
});
|
|
1327
1348
|
|
|
1328
1349
|
// src/review-worktree.ts
|
|
@@ -2200,41 +2221,91 @@ var init_project_type = __esm(() => {
|
|
|
2200
2221
|
_cache = new Map;
|
|
2201
2222
|
});
|
|
2202
2223
|
|
|
2224
|
+
// src/revert-guard.ts
|
|
2225
|
+
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
2226
|
+
function isTestFile(path) {
|
|
2227
|
+
return TEST_FILE.test(path);
|
|
2228
|
+
}
|
|
2229
|
+
function filterTestFiles(paths) {
|
|
2230
|
+
return paths.filter(isTestFile);
|
|
2231
|
+
}
|
|
2232
|
+
function refetchBase(worktreePath, baseBranch) {
|
|
2233
|
+
try {
|
|
2234
|
+
execFileSync7("git", ["fetch", "origin", baseBranch], {
|
|
2235
|
+
cwd: worktreePath,
|
|
2236
|
+
stdio: "pipe"
|
|
2237
|
+
});
|
|
2238
|
+
} catch {
|
|
2239
|
+
log.warn(TAG12, "Failed to re-fetch base for revert guard — using last fetch");
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
|
|
2243
|
+
try {
|
|
2244
|
+
const out = execFileSync7("git", ["diff", "--diff-filter=D", "--name-only", `origin/${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8" });
|
|
2245
|
+
return out.split(`
|
|
2246
|
+
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
2247
|
+
} catch (err) {
|
|
2248
|
+
log.warn(TAG12, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
|
|
2249
|
+
return [];
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
function findDeletedTestFiles(worktreePath, baseBranch) {
|
|
2253
|
+
refetchBase(worktreePath, baseBranch);
|
|
2254
|
+
return filterTestFiles(listDeletedFilesAgainstBase(worktreePath, baseBranch));
|
|
2255
|
+
}
|
|
2256
|
+
var TAG12 = "revert-guard", TEST_FILE;
|
|
2257
|
+
var init_revert_guard = __esm(() => {
|
|
2258
|
+
init_log();
|
|
2259
|
+
TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
2260
|
+
});
|
|
2261
|
+
|
|
2203
2262
|
// src/verification.ts
|
|
2204
|
-
import { execFileSync as
|
|
2263
|
+
import { execFileSync as execFileSync8, spawn } from "node:child_process";
|
|
2205
2264
|
async function runVerification(worktreePath, config, workerId) {
|
|
2206
2265
|
const result = {
|
|
2207
2266
|
passed: true,
|
|
2208
2267
|
buildErrors: [],
|
|
2209
2268
|
lintWarnings: [],
|
|
2210
|
-
reviewFindings: []
|
|
2269
|
+
reviewFindings: [],
|
|
2270
|
+
revertWarnings: []
|
|
2211
2271
|
};
|
|
2272
|
+
if (config.verification.revertGuard) {
|
|
2273
|
+
log.info(TAG13, `[worker:${workerId}] Checking for reverted merged work...`);
|
|
2274
|
+
const deletedTests = findDeletedTestFiles(worktreePath, config.worktree.baseBranch);
|
|
2275
|
+
if (deletedTests.length > 0) {
|
|
2276
|
+
result.revertWarnings = deletedTests.map((f) => `Branch deletes test file '${f}' relative to current ${config.worktree.baseBranch} — ` + "likely an accidental revert of already-merged work. Restore the test or rebase on current main.");
|
|
2277
|
+
log.warn(TAG13, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
|
|
2278
|
+
result.passed = false;
|
|
2279
|
+
} else {
|
|
2280
|
+
log.info(TAG13, `[worker:${workerId}] Revert guard passed`);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2212
2283
|
if (config.verification.build) {
|
|
2213
|
-
log.info(
|
|
2284
|
+
log.info(TAG13, `[worker:${workerId}] Running build...`);
|
|
2214
2285
|
result.buildErrors = runBuild(worktreePath, config.verification.timeout);
|
|
2215
2286
|
if (result.buildErrors.length > 0) {
|
|
2216
|
-
log.warn(
|
|
2287
|
+
log.warn(TAG13, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
|
|
2217
2288
|
result.passed = false;
|
|
2218
2289
|
} else {
|
|
2219
|
-
log.info(
|
|
2290
|
+
log.info(TAG13, `[worker:${workerId}] Build passed`);
|
|
2220
2291
|
}
|
|
2221
2292
|
}
|
|
2222
2293
|
if (config.verification.lint) {
|
|
2223
|
-
log.info(
|
|
2294
|
+
log.info(TAG13, `[worker:${workerId}] Running lint...`);
|
|
2224
2295
|
result.lintWarnings = runLint(worktreePath, config.verification.timeout);
|
|
2225
2296
|
if (result.lintWarnings.length > 0) {
|
|
2226
|
-
log.warn(
|
|
2297
|
+
log.warn(TAG13, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
|
|
2227
2298
|
} else {
|
|
2228
|
-
log.info(
|
|
2299
|
+
log.info(TAG13, `[worker:${workerId}] Lint passed`);
|
|
2229
2300
|
}
|
|
2230
2301
|
}
|
|
2231
2302
|
if (config.verification.deepReview) {
|
|
2232
|
-
log.info(
|
|
2303
|
+
log.info(TAG13, `[worker:${workerId}] Running deep review...`);
|
|
2233
2304
|
result.reviewFindings = await runDeepReview(worktreePath, config, workerId);
|
|
2234
2305
|
if (result.reviewFindings.length > 0) {
|
|
2235
|
-
log.warn(
|
|
2306
|
+
log.warn(TAG13, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
|
|
2236
2307
|
} else {
|
|
2237
|
-
log.info(
|
|
2308
|
+
log.info(TAG13, `[worker:${workerId}] Deep review passed`);
|
|
2238
2309
|
}
|
|
2239
2310
|
}
|
|
2240
2311
|
return result;
|
|
@@ -2242,11 +2313,11 @@ async function runVerification(worktreePath, config, workerId) {
|
|
|
2242
2313
|
function runBuild(worktreePath, timeout) {
|
|
2243
2314
|
const command = buildCommand(worktreePath);
|
|
2244
2315
|
if (!command) {
|
|
2245
|
-
log.warn(
|
|
2316
|
+
log.warn(TAG13, `No known build toolchain for ${worktreePath} — skipping build`);
|
|
2246
2317
|
return [];
|
|
2247
2318
|
}
|
|
2248
2319
|
try {
|
|
2249
|
-
|
|
2320
|
+
execFileSync8(command.cmd, command.args, {
|
|
2250
2321
|
cwd: worktreePath,
|
|
2251
2322
|
timeout,
|
|
2252
2323
|
stdio: "pipe"
|
|
@@ -2259,11 +2330,11 @@ function runBuild(worktreePath, timeout) {
|
|
|
2259
2330
|
function runLint(worktreePath, timeout) {
|
|
2260
2331
|
const command = lintCommand(worktreePath);
|
|
2261
2332
|
if (!command) {
|
|
2262
|
-
log.info(
|
|
2333
|
+
log.info(TAG13, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
|
|
2263
2334
|
return [];
|
|
2264
2335
|
}
|
|
2265
2336
|
try {
|
|
2266
|
-
|
|
2337
|
+
execFileSync8(command.cmd, command.args, {
|
|
2267
2338
|
cwd: worktreePath,
|
|
2268
2339
|
timeout,
|
|
2269
2340
|
stdio: "pipe"
|
|
@@ -2275,7 +2346,7 @@ function runLint(worktreePath, timeout) {
|
|
|
2275
2346
|
}
|
|
2276
2347
|
async function runDeepReview(worktreePath, config, workerId) {
|
|
2277
2348
|
if (!supportsDevServer(worktreePath)) {
|
|
2278
|
-
log.info(
|
|
2349
|
+
log.info(TAG13, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
|
|
2279
2350
|
return [];
|
|
2280
2351
|
}
|
|
2281
2352
|
const port = config.verification.devServerBasePort + workerId;
|
|
@@ -2290,12 +2361,12 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2290
2361
|
await waitForDevServer(devServer, 30000);
|
|
2291
2362
|
await probeDevServer(port);
|
|
2292
2363
|
} catch (err) {
|
|
2293
|
-
log.error(
|
|
2364
|
+
log.error(TAG13, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
|
|
2294
2365
|
return [];
|
|
2295
2366
|
}
|
|
2296
2367
|
let diff = "";
|
|
2297
2368
|
try {
|
|
2298
|
-
diff =
|
|
2369
|
+
diff = execFileSync8("git", ["diff", `origin/${config.worktree.baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
|
|
2299
2370
|
} catch {
|
|
2300
2371
|
diff = "(unable to retrieve diff)";
|
|
2301
2372
|
}
|
|
@@ -2312,7 +2383,7 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2312
2383
|
].join(`
|
|
2313
2384
|
`);
|
|
2314
2385
|
const leanSources = config.claude.leanSettingSources;
|
|
2315
|
-
const output =
|
|
2386
|
+
const output = execFileSync8("claude", [
|
|
2316
2387
|
"--print",
|
|
2317
2388
|
"--model",
|
|
2318
2389
|
"sonnet",
|
|
@@ -2329,7 +2400,7 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2329
2400
|
});
|
|
2330
2401
|
return parseReviewFindings(output);
|
|
2331
2402
|
} catch (err) {
|
|
2332
|
-
log.error(
|
|
2403
|
+
log.error(TAG13, `Deep review failed: ${err instanceof Error ? err.message : err}`);
|
|
2333
2404
|
return [];
|
|
2334
2405
|
} finally {
|
|
2335
2406
|
if (devServer && !devServer.killed) {
|
|
@@ -2365,8 +2436,8 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
2365
2436
|
"--",
|
|
2366
2437
|
fixPrompt
|
|
2367
2438
|
];
|
|
2368
|
-
log.info(
|
|
2369
|
-
|
|
2439
|
+
log.info(TAG13, "Spawning Claude for auto-fix...");
|
|
2440
|
+
execFileSync8("claude", args, {
|
|
2370
2441
|
cwd: worktreePath,
|
|
2371
2442
|
timeout: config.verification.timeout,
|
|
2372
2443
|
stdio: "pipe"
|
|
@@ -2379,6 +2450,9 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
2379
2450
|
const url = recovery.branchUrl ? ` (${recovery.branchUrl})` : "";
|
|
2380
2451
|
items.push(`Recovery: \`${cmd}\`${url}`);
|
|
2381
2452
|
}
|
|
2453
|
+
for (const warn of result.revertWarnings) {
|
|
2454
|
+
items.push(`Revert: ${warn}`);
|
|
2455
|
+
}
|
|
2382
2456
|
for (const err of result.buildErrors) {
|
|
2383
2457
|
items.push(`Build: ${err}`);
|
|
2384
2458
|
}
|
|
@@ -2396,7 +2470,7 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
2396
2470
|
try {
|
|
2397
2471
|
await client.createSubtask(cardId, title);
|
|
2398
2472
|
} catch (err) {
|
|
2399
|
-
log.error(
|
|
2473
|
+
log.error(TAG13, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
|
|
2400
2474
|
}
|
|
2401
2475
|
}));
|
|
2402
2476
|
if (overflow > 0) {
|
|
@@ -2404,7 +2478,7 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
2404
2478
|
await client.createSubtask(cardId, `...and ${overflow} more issues`);
|
|
2405
2479
|
} catch {}
|
|
2406
2480
|
}
|
|
2407
|
-
log.info(
|
|
2481
|
+
log.info(TAG13, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
|
|
2408
2482
|
}
|
|
2409
2483
|
function parseErrorOutput(err) {
|
|
2410
2484
|
const stderr = err?.stderr?.toString() ?? "";
|
|
@@ -2488,11 +2562,12 @@ async function probeDevServer(port, timeoutMs = 5000) {
|
|
|
2488
2562
|
clearTimeout(timer);
|
|
2489
2563
|
}
|
|
2490
2564
|
}
|
|
2491
|
-
var
|
|
2565
|
+
var TAG13 = "verification", DevServerReadinessError;
|
|
2492
2566
|
var init_verification = __esm(() => {
|
|
2493
2567
|
init_log();
|
|
2494
2568
|
init_pm();
|
|
2495
2569
|
init_project_type();
|
|
2570
|
+
init_revert_guard();
|
|
2496
2571
|
DevServerReadinessError = class DevServerReadinessError extends Error {
|
|
2497
2572
|
constructor(message) {
|
|
2498
2573
|
super(message);
|
|
@@ -2502,7 +2577,7 @@ var init_verification = __esm(() => {
|
|
|
2502
2577
|
});
|
|
2503
2578
|
|
|
2504
2579
|
// src/completion.ts
|
|
2505
|
-
import { execFileSync as
|
|
2580
|
+
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
2506
2581
|
function formatTokenCount(tokens) {
|
|
2507
2582
|
if (tokens >= 1e6)
|
|
2508
2583
|
return `${(tokens / 1e6).toFixed(1)}M`;
|
|
@@ -2510,6 +2585,13 @@ function formatTokenCount(tokens) {
|
|
|
2510
2585
|
return `${(tokens / 1000).toFixed(1)}k`;
|
|
2511
2586
|
return String(tokens);
|
|
2512
2587
|
}
|
|
2588
|
+
function describeNoCommitFailure(numTurns, maxTurns) {
|
|
2589
|
+
const maxTurnsExhausted = maxTurns > 0 && numTurns >= maxTurns;
|
|
2590
|
+
return {
|
|
2591
|
+
maxTurnsExhausted,
|
|
2592
|
+
failureSummary: maxTurnsExhausted ? `Agent exhausted its ${maxTurns}-turn budget without committing any changes` : "Agent finished without making any changes to commit"
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2513
2595
|
function buildTokenPayload(stats) {
|
|
2514
2596
|
if (!stats?.cost)
|
|
2515
2597
|
return {};
|
|
@@ -2528,26 +2610,30 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2528
2610
|
passed: true,
|
|
2529
2611
|
buildErrors: [],
|
|
2530
2612
|
lintWarnings: [],
|
|
2531
|
-
reviewFindings: []
|
|
2613
|
+
reviewFindings: [],
|
|
2614
|
+
revertWarnings: []
|
|
2532
2615
|
};
|
|
2533
2616
|
const hasCommits = checkHasCommits(worktreePath, config.worktree.baseBranch);
|
|
2534
2617
|
if (!hasCommits) {
|
|
2535
|
-
|
|
2618
|
+
const { maxTurnsExhausted, failureSummary } = describeNoCommitFailure(sessionStats?.cost?.numTurns ?? 0, config.claude.maxTurns);
|
|
2619
|
+
log.warn(TAG14, `No commits on branch ${branchName} — ${failureSummary}; counting as a failed attempt`);
|
|
2620
|
+
await moveCardToColumn(client, card, config.pickupColumns[0] ?? "To Do");
|
|
2536
2621
|
await client.endAgentSession(card.id, {
|
|
2537
|
-
status: "
|
|
2538
|
-
|
|
2622
|
+
status: "failed",
|
|
2623
|
+
failureReason: maxTurnsExhausted ? "timeout" : "other",
|
|
2624
|
+
failureSummary,
|
|
2539
2625
|
...buildTokenPayload(sessionStats)
|
|
2540
2626
|
});
|
|
2541
2627
|
cleanupWorktree(worktreePath, branchName);
|
|
2542
|
-
return
|
|
2628
|
+
return false;
|
|
2543
2629
|
}
|
|
2544
|
-
log.info(
|
|
2630
|
+
log.info(TAG14, `Pushing branch ${branchName} (pre-verify)...`);
|
|
2545
2631
|
let lastPushedSha = null;
|
|
2546
2632
|
try {
|
|
2547
2633
|
pushBranch(branchName, worktreePath);
|
|
2548
2634
|
lastPushedSha = readHeadSha(worktreePath);
|
|
2549
2635
|
} catch (err) {
|
|
2550
|
-
log.error(
|
|
2636
|
+
log.error(TAG14, `pre-verify push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
2551
2637
|
}
|
|
2552
2638
|
const recoveryUrl = lastPushedSha ? getBranchWebUrl(branchName, worktreePath) : null;
|
|
2553
2639
|
if (config.verification.enabled) {
|
|
@@ -2562,7 +2648,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2562
2648
|
let autoFixAttempts = 0;
|
|
2563
2649
|
if (!result.passed && config.verification.autoFix) {
|
|
2564
2650
|
for (let attempt = 0;attempt < config.verification.maxFixAttempts; attempt++) {
|
|
2565
|
-
log.info(
|
|
2651
|
+
log.info(TAG14, `Auto-fix attempt ${attempt + 1}/${config.verification.maxFixAttempts}`);
|
|
2566
2652
|
await client.updateAgentProgress(card.id, {
|
|
2567
2653
|
agentIdentifier: agentIdentifier(workerId),
|
|
2568
2654
|
agentName: AGENT_NAME,
|
|
@@ -2575,14 +2661,14 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2575
2661
|
result = await runVerification(worktreePath, config, workerId);
|
|
2576
2662
|
autoFixAttempts = attempt + 1;
|
|
2577
2663
|
if (result.passed) {
|
|
2578
|
-
log.info(
|
|
2664
|
+
log.info(TAG14, `Auto-fix succeeded on attempt ${attempt + 1}`);
|
|
2579
2665
|
const sha = readHeadSha(worktreePath);
|
|
2580
2666
|
if (sha && sha !== lastPushedSha) {
|
|
2581
2667
|
try {
|
|
2582
2668
|
pushBranch(branchName, worktreePath);
|
|
2583
2669
|
lastPushedSha = sha;
|
|
2584
2670
|
} catch (err) {
|
|
2585
|
-
log.warn(
|
|
2671
|
+
log.warn(TAG14, `post-fix push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
2586
2672
|
}
|
|
2587
2673
|
}
|
|
2588
2674
|
break;
|
|
@@ -2591,14 +2677,14 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2591
2677
|
}
|
|
2592
2678
|
verificationResult = result;
|
|
2593
2679
|
if (!result.passed) {
|
|
2594
|
-
log.warn(
|
|
2680
|
+
log.warn(TAG14, `Verification failed for #${card.short_id} — reporting findings`);
|
|
2595
2681
|
const failSha = readHeadSha(worktreePath);
|
|
2596
2682
|
if (failSha && failSha !== lastPushedSha) {
|
|
2597
2683
|
try {
|
|
2598
2684
|
pushBranch(branchName, worktreePath);
|
|
2599
2685
|
lastPushedSha = failSha;
|
|
2600
2686
|
} catch (err) {
|
|
2601
|
-
log.warn(
|
|
2687
|
+
log.warn(TAG14, `post-fail push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
2602
2688
|
}
|
|
2603
2689
|
}
|
|
2604
2690
|
const failureSummary = buildVerificationFailureSummary(result, autoFixAttempts);
|
|
@@ -2609,7 +2695,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2609
2695
|
recoveryBranch: branchName
|
|
2610
2696
|
});
|
|
2611
2697
|
} catch (err) {
|
|
2612
|
-
log.debug(
|
|
2698
|
+
log.debug(TAG14, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
|
|
2613
2699
|
}
|
|
2614
2700
|
await reportFindings(client, card.id, result, lastPushedSha ? { branchName, branchUrl: recoveryUrl } : null);
|
|
2615
2701
|
await moveCardToColumn(client, card, config.verification.failColumn);
|
|
@@ -2623,7 +2709,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2623
2709
|
cleanupWorktree(worktreePath, branchName);
|
|
2624
2710
|
return false;
|
|
2625
2711
|
}
|
|
2626
|
-
log.info(
|
|
2712
|
+
log.info(TAG14, `Verification passed for #${card.short_id}`);
|
|
2627
2713
|
}
|
|
2628
2714
|
let prUrl = null;
|
|
2629
2715
|
if (config.completion.createPR) {
|
|
@@ -2636,7 +2722,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2636
2722
|
try {
|
|
2637
2723
|
await onMovedToCompletion(card);
|
|
2638
2724
|
} catch (err) {
|
|
2639
|
-
log.warn(
|
|
2725
|
+
log.warn(TAG14, `successor promotion failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
2640
2726
|
}
|
|
2641
2727
|
}
|
|
2642
2728
|
}
|
|
@@ -2670,11 +2756,14 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2670
2756
|
});
|
|
2671
2757
|
}
|
|
2672
2758
|
cleanupWorktree(worktreePath, branchName);
|
|
2673
|
-
log.info(
|
|
2759
|
+
log.info(TAG14, `Completion done for #${card.short_id}${prUrl ? ` — PR: ${prUrl}` : ""}`);
|
|
2674
2760
|
return true;
|
|
2675
2761
|
}
|
|
2676
2762
|
function buildVerificationFailureSummary(result, autoFixAttempts) {
|
|
2677
2763
|
const counts = [];
|
|
2764
|
+
if (result.revertWarnings.length > 0) {
|
|
2765
|
+
counts.push(`${result.revertWarnings.length} reverted test(s)`);
|
|
2766
|
+
}
|
|
2678
2767
|
if (result.buildErrors.length > 0) {
|
|
2679
2768
|
counts.push(`${result.buildErrors.length} build error(s)`);
|
|
2680
2769
|
}
|
|
@@ -2690,7 +2779,7 @@ function buildVerificationFailureSummary(result, autoFixAttempts) {
|
|
|
2690
2779
|
}
|
|
2691
2780
|
function readHeadSha(worktreePath) {
|
|
2692
2781
|
try {
|
|
2693
|
-
return
|
|
2782
|
+
return execFileSync9("git", ["rev-parse", "HEAD"], {
|
|
2694
2783
|
cwd: worktreePath,
|
|
2695
2784
|
encoding: "utf-8"
|
|
2696
2785
|
}).trim();
|
|
@@ -2700,7 +2789,7 @@ function readHeadSha(worktreePath) {
|
|
|
2700
2789
|
}
|
|
2701
2790
|
function checkHasCommits(worktreePath, baseBranch) {
|
|
2702
2791
|
try {
|
|
2703
|
-
const count =
|
|
2792
|
+
const count = execFileSync9("git", ["rev-list", "--count", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
2704
2793
|
return parseInt(count, 10) > 0;
|
|
2705
2794
|
} catch {
|
|
2706
2795
|
return false;
|
|
@@ -2709,7 +2798,7 @@ function checkHasCommits(worktreePath, baseBranch) {
|
|
|
2709
2798
|
async function postSummary(client, card, branchName, worktreePath, prUrl, baseBranch, sessionStats) {
|
|
2710
2799
|
let commitLog = "";
|
|
2711
2800
|
try {
|
|
2712
|
-
commitLog =
|
|
2801
|
+
commitLog = execFileSync9("git", ["log", "--oneline", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
2713
2802
|
} catch {}
|
|
2714
2803
|
const SUMMARY_MARKER = `---
|
|
2715
2804
|
**Agent completed**`;
|
|
@@ -2754,12 +2843,12 @@ ${commitLog}
|
|
|
2754
2843
|
description: baseDesc + parts.join(`
|
|
2755
2844
|
`)
|
|
2756
2845
|
});
|
|
2757
|
-
log.info(
|
|
2846
|
+
log.info(TAG14, `Posted completion summary to #${card.short_id}`);
|
|
2758
2847
|
} catch (err) {
|
|
2759
|
-
log.error(
|
|
2848
|
+
log.error(TAG14, `Failed to post summary: ${err instanceof Error ? err.message : err}`);
|
|
2760
2849
|
}
|
|
2761
2850
|
}
|
|
2762
|
-
var
|
|
2851
|
+
var TAG14 = "completion";
|
|
2763
2852
|
var init_completion = __esm(() => {
|
|
2764
2853
|
init_board_helpers();
|
|
2765
2854
|
init_episode_writer();
|
|
@@ -2799,7 +2888,21 @@ function signalGroup(proc, signal) {
|
|
|
2799
2888
|
} catch (err) {
|
|
2800
2889
|
const code = err.code;
|
|
2801
2890
|
if (code !== "ESRCH") {
|
|
2802
|
-
log.warn(
|
|
2891
|
+
log.warn(TAG15, `signal ${signal} to pgid ${proc.pid} failed: ${err instanceof Error ? err.message : err}`);
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
function reapGroup(pgid) {
|
|
2896
|
+
if (!pgid || pgid <= 1 || pgid === process.pid)
|
|
2897
|
+
return;
|
|
2898
|
+
if (process.platform === "win32")
|
|
2899
|
+
return;
|
|
2900
|
+
try {
|
|
2901
|
+
process.kill(-pgid, "SIGKILL");
|
|
2902
|
+
} catch (err) {
|
|
2903
|
+
const code = err.code;
|
|
2904
|
+
if (code !== "ESRCH") {
|
|
2905
|
+
log.warn(TAG15, `reapGroup(${pgid}) failed: ${err instanceof Error ? err.message : err}`);
|
|
2803
2906
|
}
|
|
2804
2907
|
}
|
|
2805
2908
|
}
|
|
@@ -2824,7 +2927,7 @@ async function terminateGroup(proc, opts) {
|
|
|
2824
2927
|
return;
|
|
2825
2928
|
signalGroup(proc, "SIGKILL");
|
|
2826
2929
|
}
|
|
2827
|
-
var
|
|
2930
|
+
var TAG15 = "pgroup";
|
|
2828
2931
|
var init_process_group = __esm(() => {
|
|
2829
2932
|
init_log();
|
|
2830
2933
|
});
|
|
@@ -2854,8 +2957,8 @@ class ProgressTracker {
|
|
|
2854
2957
|
filesEdited = new Set;
|
|
2855
2958
|
filesRead = new Set;
|
|
2856
2959
|
lastCost = null;
|
|
2857
|
-
|
|
2858
|
-
|
|
2960
|
+
runEventSink = null;
|
|
2961
|
+
lastEmittedProgress = -1;
|
|
2859
2962
|
lastAssistantText = "";
|
|
2860
2963
|
assistantTextBlocks = [];
|
|
2861
2964
|
constructor(client, cardId, workerId, subtasks, initialPhase = "exploring") {
|
|
@@ -2868,32 +2971,15 @@ class ProgressTracker {
|
|
|
2868
2971
|
this.phase = initialPhase;
|
|
2869
2972
|
this.progress = PHASES[initialPhase].min;
|
|
2870
2973
|
}
|
|
2871
|
-
|
|
2872
|
-
this.
|
|
2974
|
+
setRunEventSink(sink) {
|
|
2975
|
+
this.runEventSink = sink;
|
|
2873
2976
|
}
|
|
2874
2977
|
attach(parser) {
|
|
2875
2978
|
parser.on("tool_start", (name, input) => {
|
|
2876
2979
|
this.onToolStart(name, input);
|
|
2877
|
-
const desc = this.describeToolAction(name, input);
|
|
2878
|
-
if (desc) {
|
|
2879
|
-
this.pushLogEntry({
|
|
2880
|
-
phase: this.phase,
|
|
2881
|
-
eventType: "tool_start",
|
|
2882
|
-
toolName: name,
|
|
2883
|
-
description: desc,
|
|
2884
|
-
metadata: this.extractToolMetadata(name, input)
|
|
2885
|
-
});
|
|
2886
|
-
}
|
|
2887
2980
|
});
|
|
2888
2981
|
parser.on("tool_end", (name, _id, content) => {
|
|
2889
2982
|
this.onToolEnd(name, content);
|
|
2890
|
-
this.pushLogEntry({
|
|
2891
|
-
phase: this.phase,
|
|
2892
|
-
eventType: "tool_end",
|
|
2893
|
-
toolName: name,
|
|
2894
|
-
description: `Completed: ${name}`,
|
|
2895
|
-
metadata: {}
|
|
2896
|
-
});
|
|
2897
2983
|
});
|
|
2898
2984
|
parser.on("text", (content) => {
|
|
2899
2985
|
this.onText(content);
|
|
@@ -2903,6 +2989,37 @@ class ProgressTracker {
|
|
|
2903
2989
|
});
|
|
2904
2990
|
this.startHeartbeat();
|
|
2905
2991
|
}
|
|
2992
|
+
ingest(draft) {
|
|
2993
|
+
switch (draft.kind) {
|
|
2994
|
+
case "run_started":
|
|
2995
|
+
this.startHeartbeat();
|
|
2996
|
+
break;
|
|
2997
|
+
case "tool_started":
|
|
2998
|
+
this.onToolStart(draft.payload.toolName, draft.payload.input);
|
|
2999
|
+
break;
|
|
3000
|
+
case "tool_ended":
|
|
3001
|
+
this.onToolEnd(draft.payload.toolName, draft.payload.output);
|
|
3002
|
+
break;
|
|
3003
|
+
case "assistant_text":
|
|
3004
|
+
this.onText(draft.payload.text);
|
|
3005
|
+
break;
|
|
3006
|
+
case "cost_updated": {
|
|
3007
|
+
const p = draft.payload;
|
|
3008
|
+
this.lastCost = {
|
|
3009
|
+
totalCostUsd: p.totalCostUsd,
|
|
3010
|
+
totalInputTokens: p.inputTokens,
|
|
3011
|
+
totalOutputTokens: p.outputTokens,
|
|
3012
|
+
totalCacheCreationInputTokens: p.cacheCreationInputTokens,
|
|
3013
|
+
totalCacheReadInputTokens: p.cacheReadInputTokens,
|
|
3014
|
+
durationMs: p.durationMs ?? 0,
|
|
3015
|
+
durationApiMs: 0,
|
|
3016
|
+
numTurns: p.numTurns,
|
|
3017
|
+
modelName: p.modelName
|
|
3018
|
+
};
|
|
3019
|
+
break;
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
2906
3023
|
stop() {
|
|
2907
3024
|
this.stopped = true;
|
|
2908
3025
|
if (this.pendingUpdate) {
|
|
@@ -2927,7 +3044,7 @@ class ProgressTracker {
|
|
|
2927
3044
|
}
|
|
2928
3045
|
onToolStart(name, input) {
|
|
2929
3046
|
this.toolCallCount++;
|
|
2930
|
-
log.debug(
|
|
3047
|
+
log.debug(TAG16, `Tool: ${name} (count: ${this.toolCallCount}, phase: ${this.phase})`);
|
|
2931
3048
|
const filePath = this.extractString(input, "file_path");
|
|
2932
3049
|
if (filePath) {
|
|
2933
3050
|
if (EDIT_TOOLS.has(name)) {
|
|
@@ -2998,17 +3115,12 @@ class ProgressTracker {
|
|
|
2998
3115
|
transitionTo(newPhase) {
|
|
2999
3116
|
if (PHASE_ORDER[newPhase] <= PHASE_ORDER[this.phase])
|
|
3000
3117
|
return;
|
|
3001
|
-
log.info(
|
|
3118
|
+
log.info(TAG16, `Phase: ${this.phase} → ${newPhase}`);
|
|
3119
|
+
const previousPhase = this.phase;
|
|
3120
|
+
this.runEventSink?.recordPhaseChanged(newPhase, previousPhase);
|
|
3002
3121
|
this.phase = newPhase;
|
|
3003
3122
|
this.progress = Math.max(this.progress, PHASES[newPhase].min);
|
|
3004
3123
|
this.lastAction = "";
|
|
3005
|
-
this.pushLogEntry({
|
|
3006
|
-
phase: newPhase,
|
|
3007
|
-
eventType: "phase_change",
|
|
3008
|
-
toolName: null,
|
|
3009
|
-
description: `Entering ${newPhase} phase`,
|
|
3010
|
-
metadata: {}
|
|
3011
|
-
});
|
|
3012
3124
|
this.scheduleUpdate(PHASES[newPhase].label);
|
|
3013
3125
|
}
|
|
3014
3126
|
incrementProgress() {
|
|
@@ -3097,7 +3209,7 @@ class ProgressTracker {
|
|
|
3097
3209
|
}
|
|
3098
3210
|
sendUpdate(currentTask) {
|
|
3099
3211
|
this.lastUpdateAt = Date.now();
|
|
3100
|
-
log.debug(
|
|
3212
|
+
log.debug(TAG16, `Progress: ${this.progress}% — ${currentTask}`);
|
|
3101
3213
|
this.client.updateAgentProgress(this.cardId, {
|
|
3102
3214
|
agentIdentifier: agentIdentifier(this.workerId),
|
|
3103
3215
|
agentName: AGENT_NAME,
|
|
@@ -3114,9 +3226,17 @@ class ProgressTracker {
|
|
|
3114
3226
|
modelName: this.lastCost?.modelName,
|
|
3115
3227
|
numTurns: this.lastCost?.numTurns ?? 0
|
|
3116
3228
|
}).catch((err) => {
|
|
3117
|
-
log.warn(
|
|
3229
|
+
log.warn(TAG16, `Failed to send progress update: ${err}`);
|
|
3118
3230
|
});
|
|
3119
|
-
this.
|
|
3231
|
+
if (this.runEventSink && this.progress !== this.lastEmittedProgress) {
|
|
3232
|
+
this.lastEmittedProgress = this.progress;
|
|
3233
|
+
this.runEventSink.recordProgress({
|
|
3234
|
+
progressPercent: this.progress,
|
|
3235
|
+
currentTask: truncate(currentTask, MAX_TASK_LENGTH),
|
|
3236
|
+
phase: this.phase,
|
|
3237
|
+
filesChanged: this.filesEdited.size
|
|
3238
|
+
});
|
|
3239
|
+
}
|
|
3120
3240
|
}
|
|
3121
3241
|
startHeartbeat() {
|
|
3122
3242
|
if (this.heartbeatTimer) {
|
|
@@ -3130,55 +3250,6 @@ class ProgressTracker {
|
|
|
3130
3250
|
}
|
|
3131
3251
|
}, HEARTBEAT_MS);
|
|
3132
3252
|
}
|
|
3133
|
-
flushFinal() {
|
|
3134
|
-
this.flushActivityLog();
|
|
3135
|
-
}
|
|
3136
|
-
pushLogEntry(entry) {
|
|
3137
|
-
this.logBuffer.push({
|
|
3138
|
-
...entry,
|
|
3139
|
-
createdAt: new Date().toISOString()
|
|
3140
|
-
});
|
|
3141
|
-
if (this.logBuffer.length > MAX_LOG_BUFFER) {
|
|
3142
|
-
this.logBuffer.shift();
|
|
3143
|
-
}
|
|
3144
|
-
}
|
|
3145
|
-
flushActivityLog() {
|
|
3146
|
-
if (!this.sessionId || this.logBuffer.length === 0)
|
|
3147
|
-
return;
|
|
3148
|
-
const raw = [...this.logBuffer];
|
|
3149
|
-
this.logBuffer = [];
|
|
3150
|
-
this.client.flushActivityLog(this.cardId, {
|
|
3151
|
-
sessionId: this.sessionId,
|
|
3152
|
-
entries: raw.map((e) => ({
|
|
3153
|
-
...e,
|
|
3154
|
-
phase: e.phase ?? undefined,
|
|
3155
|
-
toolName: e.toolName ?? undefined
|
|
3156
|
-
}))
|
|
3157
|
-
}).catch((err) => {
|
|
3158
|
-
log.warn(TAG15, `Failed to flush activity log: ${err}`);
|
|
3159
|
-
this.logBuffer.unshift(...raw);
|
|
3160
|
-
if (this.logBuffer.length > MAX_LOG_BUFFER) {
|
|
3161
|
-
this.logBuffer.length = MAX_LOG_BUFFER;
|
|
3162
|
-
}
|
|
3163
|
-
});
|
|
3164
|
-
}
|
|
3165
|
-
extractToolMetadata(_name, input) {
|
|
3166
|
-
const meta = {};
|
|
3167
|
-
const fp = this.extractString(input, "file_path");
|
|
3168
|
-
if (fp)
|
|
3169
|
-
meta.file_path = fp;
|
|
3170
|
-
const cmd = this.extractString(input, "command");
|
|
3171
|
-
if (cmd)
|
|
3172
|
-
meta.command = cmd.split(`
|
|
3173
|
-
`)[0].slice(0, 200);
|
|
3174
|
-
const pattern = this.extractString(input, "pattern");
|
|
3175
|
-
if (pattern)
|
|
3176
|
-
meta.pattern = pattern;
|
|
3177
|
-
const desc = this.extractString(input, "description");
|
|
3178
|
-
if (desc)
|
|
3179
|
-
meta.description = desc;
|
|
3180
|
-
return meta;
|
|
3181
|
-
}
|
|
3182
3253
|
extractString(input, key) {
|
|
3183
3254
|
if (typeof input === "object" && input !== null && key in input) {
|
|
3184
3255
|
return String(input[key]);
|
|
@@ -3186,7 +3257,7 @@ class ProgressTracker {
|
|
|
3186
3257
|
return null;
|
|
3187
3258
|
}
|
|
3188
3259
|
}
|
|
3189
|
-
var
|
|
3260
|
+
var TAG16 = "progress-tracker", THROTTLE_MS = 5000, HEARTBEAT_MS = 60000, MAX_TASK_LENGTH = 120, MAX_TEXT_BLOCKS = 40, SENTENCE_SPLIT, ACTION_PREFIX, GIT_COMMIT_RE, BUILD_CMD_RE, PHASES, PHASE_ORDER, EDIT_TOOLS, FILE_TOOL_VERBS;
|
|
3190
3261
|
var init_progress_tracker = __esm(() => {
|
|
3191
3262
|
init_log();
|
|
3192
3263
|
init_types();
|
|
@@ -3222,6 +3293,41 @@ var init_progress_tracker = __esm(() => {
|
|
|
3222
3293
|
|
|
3223
3294
|
// src/review-completion.ts
|
|
3224
3295
|
import { readFileSync as readFileSync2, statSync } from "node:fs";
|
|
3296
|
+
function clampSubtaskTitle(title) {
|
|
3297
|
+
return title.length > MAX_SUBTASK_TITLE ? `${title.slice(0, MAX_SUBTASK_TITLE - 3)}...` : title;
|
|
3298
|
+
}
|
|
3299
|
+
function renderFindingBlock(f) {
|
|
3300
|
+
const locationLine = f.location ? `
|
|
3301
|
+
Location: ${f.location}` : "";
|
|
3302
|
+
return `**[${f.severity}] ${f.title}**
|
|
3303
|
+
${f.description}${locationLine}`;
|
|
3304
|
+
}
|
|
3305
|
+
function buildFindingComments(findings) {
|
|
3306
|
+
const header = `**Review findings — ${findings.length} blocking issue(s) to resolve.**`;
|
|
3307
|
+
const sep = `
|
|
3308
|
+
|
|
3309
|
+
`;
|
|
3310
|
+
const bodies = [];
|
|
3311
|
+
let current = header;
|
|
3312
|
+
for (const f of findings) {
|
|
3313
|
+
let block = renderFindingBlock(f);
|
|
3314
|
+
const maxBlock = COMMENT_BODY_BUDGET - header.length - sep.length;
|
|
3315
|
+
if (block.length > maxBlock) {
|
|
3316
|
+
const suffix = `
|
|
3317
|
+
…[truncated]`;
|
|
3318
|
+
block = `${block.slice(0, Math.max(0, maxBlock - suffix.length))}${suffix}`;
|
|
3319
|
+
}
|
|
3320
|
+
if (current.length + sep.length + block.length > COMMENT_BODY_BUDGET) {
|
|
3321
|
+
bodies.push(current);
|
|
3322
|
+
current = `${header}${sep}${block}`;
|
|
3323
|
+
} else {
|
|
3324
|
+
current = `${current}${sep}${block}`;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
if (current.length > header.length)
|
|
3328
|
+
bodies.push(current);
|
|
3329
|
+
return bodies;
|
|
3330
|
+
}
|
|
3225
3331
|
function tailRunLog(path, bytes = RUN_LOG_TAIL_BYTES) {
|
|
3226
3332
|
try {
|
|
3227
3333
|
const size = statSync(path).size;
|
|
@@ -3261,7 +3367,7 @@ function parseReviewOutput(stdout) {
|
|
|
3261
3367
|
try {
|
|
3262
3368
|
const parsed = JSON.parse(raw);
|
|
3263
3369
|
if (parsed && typeof parsed === "object" && "verdict" in parsed) {
|
|
3264
|
-
log.debug(
|
|
3370
|
+
log.debug(TAG17, "Parsed review output from fenced JSON block");
|
|
3265
3371
|
return extractResult(parsed);
|
|
3266
3372
|
}
|
|
3267
3373
|
} catch {}
|
|
@@ -3287,21 +3393,21 @@ function parseReviewOutput(stdout) {
|
|
|
3287
3393
|
try {
|
|
3288
3394
|
const parsed = JSON.parse(candidates[i]);
|
|
3289
3395
|
if (parsed && typeof parsed === "object" && "verdict" in parsed) {
|
|
3290
|
-
log.debug(
|
|
3396
|
+
log.debug(TAG17, "Parsed review output from raw JSON object");
|
|
3291
3397
|
return extractResult(parsed);
|
|
3292
3398
|
}
|
|
3293
3399
|
} catch {}
|
|
3294
3400
|
}
|
|
3295
3401
|
const verdictMatch = stdout.match(/"verdict"\s*:\s*"(approved|rejected)"/i);
|
|
3296
3402
|
if (verdictMatch) {
|
|
3297
|
-
log.warn(
|
|
3403
|
+
log.warn(TAG17, `Parsed verdict via regex fallback — findings lost (${verdictMatch[1]})`);
|
|
3298
3404
|
return {
|
|
3299
3405
|
verdict: verdictMatch[1].toLowerCase(),
|
|
3300
3406
|
summary: "Parsed via regex fallback — original JSON was malformed. Check run log.",
|
|
3301
3407
|
findings: []
|
|
3302
3408
|
};
|
|
3303
3409
|
}
|
|
3304
|
-
log.warn(
|
|
3410
|
+
log.warn(TAG17, "Failed to parse review JSON output — returning error verdict (card stays in Review)");
|
|
3305
3411
|
return {
|
|
3306
3412
|
verdict: "error",
|
|
3307
3413
|
summary: stdout.slice(0, 500),
|
|
@@ -3334,7 +3440,7 @@ async function postReviewComment(client, card, commentType, body) {
|
|
|
3334
3440
|
try {
|
|
3335
3441
|
await client.addComment(card.id, body, { commentType });
|
|
3336
3442
|
} catch (err) {
|
|
3337
|
-
log.error(
|
|
3443
|
+
log.error(TAG17, `Failed to post review comment to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
3338
3444
|
}
|
|
3339
3445
|
}
|
|
3340
3446
|
async function runReviewCompletion(client, card, result, config, worktreePath, branchName, sessionStats, runLogPath, workspaceId, agentSessionId, stateStore) {
|
|
@@ -3348,11 +3454,11 @@ async function runReviewCompletion(client, card, result, config, worktreePath, b
|
|
|
3348
3454
|
const currentCycle = getReviewCycle(freshDesc) + 1;
|
|
3349
3455
|
const maxCycles = config.review.maxReviewCycles;
|
|
3350
3456
|
if (result.verdict === "error") {
|
|
3351
|
-
log.warn(
|
|
3457
|
+
log.warn(TAG17, `#${card.short_id} review output unparseable — labelling "${NEED_REVIEW_LABEL}" for manual inspection`);
|
|
3352
3458
|
try {
|
|
3353
3459
|
await addLabelByName(client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR);
|
|
3354
3460
|
} catch (err) {
|
|
3355
|
-
log.warn(
|
|
3461
|
+
log.warn(TAG17, `Failed to add "${NEED_REVIEW_LABEL}" label: ${err instanceof Error ? err.message : err}`);
|
|
3356
3462
|
}
|
|
3357
3463
|
if (config.review.postFindings) {
|
|
3358
3464
|
const rawTail = runLogPath ? tailRunLog(runLogPath) : null;
|
|
@@ -3395,7 +3501,7 @@ ${runLogTail}
|
|
|
3395
3501
|
renameRemoteBranch(branchName, newRef, worktreePath);
|
|
3396
3502
|
approvedBranch = newRef;
|
|
3397
3503
|
} catch (err) {
|
|
3398
|
-
log.warn(
|
|
3504
|
+
log.warn(TAG17, `Branch rename failed (continuing on ${branchName}): ${err instanceof Error ? err.message : err}`);
|
|
3399
3505
|
}
|
|
3400
3506
|
}
|
|
3401
3507
|
if (config.review.createPR && approvedBranch) {
|
|
@@ -3404,6 +3510,21 @@ ${runLogTail}
|
|
|
3404
3510
|
}
|
|
3405
3511
|
}
|
|
3406
3512
|
await addLabelByName(client, card, config.review.approvedLabel, config.review.approvedLabelColor);
|
|
3513
|
+
if (prUrl) {
|
|
3514
|
+
try {
|
|
3515
|
+
const { card: latest } = await client.getCard(card.id);
|
|
3516
|
+
const desc = latest.description || "";
|
|
3517
|
+
if (!extractPrUrl(desc)) {
|
|
3518
|
+
const separator = desc ? `
|
|
3519
|
+
` : "";
|
|
3520
|
+
await client.updateCard(card.id, {
|
|
3521
|
+
description: `${desc}${separator}PR: ${prUrl}`
|
|
3522
|
+
});
|
|
3523
|
+
}
|
|
3524
|
+
} catch (err) {
|
|
3525
|
+
log.warn(TAG17, `Failed to persist PR URL to #${card.short_id} description: ${err instanceof Error ? err.message : err}`);
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3407
3528
|
if (config.review.postFindings) {
|
|
3408
3529
|
const scopeLine = result.scopeCheck ? `Scope: ${result.scopeCheck.status}${result.scopeCheck.notes ? ` — ${result.scopeCheck.notes}` : ""}` : "";
|
|
3409
3530
|
const body = [
|
|
@@ -3422,14 +3543,14 @@ ${runLogTail}
|
|
|
3422
3543
|
progressPercent: 100,
|
|
3423
3544
|
...buildTokenPayload(sessionStats)
|
|
3424
3545
|
});
|
|
3425
|
-
log.info(
|
|
3546
|
+
log.info(TAG17, `#${card.short_id} approved${prUrl ? ` — PR: ${prUrl}` : ""} — labeled "${config.review.approvedLabel}"`);
|
|
3426
3547
|
} else {
|
|
3427
3548
|
const criticalFindings = result.findings.filter((f) => f.severity === "critical").slice(0, MAX_FINDINGS);
|
|
3428
3549
|
const majorFindings = result.findings.filter((f) => f.severity === "major").slice(0, MAX_FINDINGS);
|
|
3429
3550
|
const linkedFindings = [...criticalFindings, ...majorFindings];
|
|
3430
3551
|
const minorFindings = result.findings.filter((f) => f.severity === "minor").slice(0, MAX_FINDINGS);
|
|
3431
3552
|
if (currentCycle >= maxCycles) {
|
|
3432
|
-
log.warn(
|
|
3553
|
+
log.warn(TAG17, `#${card.short_id} reached max review cycles (${maxCycles}), moving to Done with note`);
|
|
3433
3554
|
await moveCardToColumn(client, card, config.review.moveToColumn);
|
|
3434
3555
|
const body = [
|
|
3435
3556
|
"**Review — needs human review.**",
|
|
@@ -3467,28 +3588,21 @@ ${runLogTail}
|
|
|
3467
3588
|
if (config.review.postFindings) {
|
|
3468
3589
|
await Promise.all(linkedFindings.map(async (finding) => {
|
|
3469
3590
|
try {
|
|
3470
|
-
|
|
3471
|
-
**Location:** ${finding.location}` : "";
|
|
3472
|
-
const newCard = await client.createCard(card.project_id, {
|
|
3473
|
-
title: `[Review: ${finding.severity}] ${finding.title}`,
|
|
3474
|
-
description: `Found during review of #${card.short_id} (${finding.severity}):
|
|
3475
|
-
|
|
3476
|
-
${finding.description}${locationLine}`
|
|
3477
|
-
});
|
|
3478
|
-
const newCardId = newCard?.card?.id;
|
|
3479
|
-
if (newCardId) {
|
|
3480
|
-
await client.addLinkToCard(card.id, newCardId, "relates_to");
|
|
3481
|
-
}
|
|
3591
|
+
await client.createSubtask(card.id, clampSubtaskTitle(`[${finding.severity}] ${finding.title}`));
|
|
3482
3592
|
} catch (err) {
|
|
3483
|
-
log.error(
|
|
3593
|
+
log.error(TAG17, `Failed to create finding subtask: ${err instanceof Error ? err.message : err}`);
|
|
3484
3594
|
}
|
|
3485
3595
|
}));
|
|
3596
|
+
if (linkedFindings.length > 0) {
|
|
3597
|
+
for (const body2 of buildFindingComments(linkedFindings)) {
|
|
3598
|
+
await postReviewComment(client, card, "finding", body2);
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3486
3601
|
await Promise.all(minorFindings.map(async (finding) => {
|
|
3487
3602
|
try {
|
|
3488
|
-
|
|
3489
|
-
await client.createSubtask(card.id, title);
|
|
3603
|
+
await client.createSubtask(card.id, clampSubtaskTitle(finding.title));
|
|
3490
3604
|
} catch (err) {
|
|
3491
|
-
log.error(
|
|
3605
|
+
log.error(TAG17, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
|
|
3492
3606
|
}
|
|
3493
3607
|
}));
|
|
3494
3608
|
const baseDesc = stripReviewSummary(freshDesc);
|
|
@@ -3496,7 +3610,7 @@ ${finding.description}${locationLine}`
|
|
|
3496
3610
|
try {
|
|
3497
3611
|
await client.updateCard(card.id, { description: updatedDesc });
|
|
3498
3612
|
} catch (err) {
|
|
3499
|
-
log.error(
|
|
3613
|
+
log.error(TAG17, `Failed to update review cycle marker: ${err instanceof Error ? err.message : err}`);
|
|
3500
3614
|
}
|
|
3501
3615
|
const scopeLine = result.scopeCheck ? `Scope: ${result.scopeCheck.status}${result.scopeCheck.notes ? ` — ${result.scopeCheck.notes}` : ""}` : "";
|
|
3502
3616
|
const body = [
|
|
@@ -3512,9 +3626,9 @@ ${finding.description}${locationLine}`
|
|
|
3512
3626
|
if (config.planning.enabled && card.plan_id) {
|
|
3513
3627
|
try {
|
|
3514
3628
|
await client.updateCard(card.id, { needsPlanRefresh: true });
|
|
3515
|
-
log.info(
|
|
3629
|
+
log.info(TAG17, `#${card.short_id} flagged needs_plan_refresh after rejected review`);
|
|
3516
3630
|
} catch (err) {
|
|
3517
|
-
log.warn(
|
|
3631
|
+
log.warn(TAG17, `Failed to flag needs_plan_refresh for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
3518
3632
|
}
|
|
3519
3633
|
}
|
|
3520
3634
|
await moveCardToColumn(client, card, config.review.failColumn);
|
|
@@ -3528,10 +3642,10 @@ ${finding.description}${locationLine}`
|
|
|
3528
3642
|
recoveryBranch
|
|
3529
3643
|
});
|
|
3530
3644
|
} catch (err) {
|
|
3531
|
-
log.debug(
|
|
3645
|
+
log.debug(TAG17, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
|
|
3532
3646
|
}
|
|
3533
3647
|
if (recoveryBranch) {
|
|
3534
|
-
log.info(
|
|
3648
|
+
log.info(TAG17, `#${card.short_id} recovery branch ${recoveryBranch}${recoveryUrl ? ` (${recoveryUrl})` : ""}`);
|
|
3535
3649
|
}
|
|
3536
3650
|
await client.endAgentSession(card.id, {
|
|
3537
3651
|
status: "failed",
|
|
@@ -3540,7 +3654,7 @@ ${finding.description}${locationLine}`
|
|
|
3540
3654
|
recoveryBranch,
|
|
3541
3655
|
...buildTokenPayload(sessionStats)
|
|
3542
3656
|
});
|
|
3543
|
-
log.info(
|
|
3657
|
+
log.info(TAG17, `#${card.short_id} rejected (cycle ${currentCycle}/${maxCycles}) — moved to "${config.review.failColumn}"`);
|
|
3544
3658
|
}
|
|
3545
3659
|
if (workspaceId && (result.verdict === "approved" || result.verdict === "rejected")) {
|
|
3546
3660
|
const originalEpisodeId = await findLatestImplementEpisode(client, workspaceId, card.project_id, card.short_id);
|
|
@@ -3562,7 +3676,7 @@ ${finding.description}${locationLine}`
|
|
|
3562
3676
|
cleanupWorktree(worktreePath, branchName);
|
|
3563
3677
|
}
|
|
3564
3678
|
}
|
|
3565
|
-
var
|
|
3679
|
+
var TAG17 = "review-completion", MAX_FINDINGS = 10, MAX_SUBTASK_TITLE = 120, COMMENT_BODY_BUDGET = 9500, REVIEW_MARKER = `---
|
|
3566
3680
|
**Review:`, RUN_LOG_TAIL_BYTES = 2048;
|
|
3567
3681
|
var init_review_completion = __esm(() => {
|
|
3568
3682
|
init_board_helpers();
|
|
@@ -3573,7 +3687,6 @@ var init_review_completion = __esm(() => {
|
|
|
3573
3687
|
init_types();
|
|
3574
3688
|
init_worktree();
|
|
3575
3689
|
});
|
|
3576
|
-
|
|
3577
3690
|
// ../harmony-shared/dist/cardLinks.js
|
|
3578
3691
|
var init_cardLinks = () => {};
|
|
3579
3692
|
// ../harmony-shared/dist/classification.js
|
|
@@ -3975,7 +4088,7 @@ class StateStore {
|
|
|
3975
4088
|
const raw = readFileSync3(this.path, "utf-8");
|
|
3976
4089
|
const parsed = JSON.parse(raw);
|
|
3977
4090
|
if (parsed?.version !== SCHEMA_VERSION) {
|
|
3978
|
-
log.warn(
|
|
4091
|
+
log.warn(TAG18, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — starting fresh`);
|
|
3979
4092
|
return emptyState();
|
|
3980
4093
|
}
|
|
3981
4094
|
return {
|
|
@@ -3988,7 +4101,7 @@ class StateStore {
|
|
|
3988
4101
|
daily: parsed.daily ?? []
|
|
3989
4102
|
};
|
|
3990
4103
|
} catch (err) {
|
|
3991
|
-
log.error(
|
|
4104
|
+
log.error(TAG18, `failed to read state file: ${err instanceof Error ? err.message : err}`);
|
|
3992
4105
|
return emptyState();
|
|
3993
4106
|
}
|
|
3994
4107
|
}
|
|
@@ -4144,7 +4257,7 @@ class StateStore {
|
|
|
4144
4257
|
return this.state.daily.find((d) => d.date === key)?.costCents ?? 0;
|
|
4145
4258
|
}
|
|
4146
4259
|
}
|
|
4147
|
-
var
|
|
4260
|
+
var TAG18 = "state-store", SCHEMA_VERSION = 1;
|
|
4148
4261
|
var init_state_store = __esm(() => {
|
|
4149
4262
|
init_log();
|
|
4150
4263
|
});
|
|
@@ -4171,7 +4284,7 @@ function normalizeToolResultContent(raw) {
|
|
|
4171
4284
|
return String(raw);
|
|
4172
4285
|
}
|
|
4173
4286
|
}
|
|
4174
|
-
var
|
|
4287
|
+
var TAG19 = "stream-parser", StreamParser;
|
|
4175
4288
|
var init_stream_parser = __esm(() => {
|
|
4176
4289
|
init_log();
|
|
4177
4290
|
StreamParser = class StreamParser extends EventEmitter {
|
|
@@ -4180,6 +4293,10 @@ var init_stream_parser = __esm(() => {
|
|
|
4180
4293
|
toolNames = new Map;
|
|
4181
4294
|
hasEmittedText = false;
|
|
4182
4295
|
observedModel;
|
|
4296
|
+
capturedSessionId;
|
|
4297
|
+
get sessionId() {
|
|
4298
|
+
return this.capturedSessionId;
|
|
4299
|
+
}
|
|
4183
4300
|
attach(stream) {
|
|
4184
4301
|
if (this.attached) {
|
|
4185
4302
|
throw new Error("StreamParser already attached to a stream");
|
|
@@ -4215,14 +4332,14 @@ var init_stream_parser = __esm(() => {
|
|
|
4215
4332
|
try {
|
|
4216
4333
|
msg = JSON.parse(line);
|
|
4217
4334
|
} catch {
|
|
4218
|
-
log.debug(
|
|
4335
|
+
log.debug(TAG19, `Non-JSON line: ${line.slice(0, 100)}`);
|
|
4219
4336
|
return;
|
|
4220
4337
|
}
|
|
4221
4338
|
try {
|
|
4222
4339
|
this.handleMessage(msg);
|
|
4223
4340
|
} catch (err) {
|
|
4224
4341
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4225
|
-
log.warn(
|
|
4342
|
+
log.warn(TAG19, `Error handling stream event: ${errMsg}`);
|
|
4226
4343
|
this.emit("parse_error", errMsg);
|
|
4227
4344
|
}
|
|
4228
4345
|
}
|
|
@@ -4234,6 +4351,9 @@ var init_stream_parser = __esm(() => {
|
|
|
4234
4351
|
this.observedModel = msg.message.model;
|
|
4235
4352
|
}
|
|
4236
4353
|
}
|
|
4354
|
+
if (!this.capturedSessionId && typeof msg.session_id === "string") {
|
|
4355
|
+
this.capturedSessionId = msg.session_id;
|
|
4356
|
+
}
|
|
4237
4357
|
switch (msg.type) {
|
|
4238
4358
|
case "assistant": {
|
|
4239
4359
|
const blocks = msg.message?.content;
|
|
@@ -4244,10 +4364,11 @@ var init_stream_parser = __esm(() => {
|
|
|
4244
4364
|
this.emit("text", block.text);
|
|
4245
4365
|
this.hasEmittedText = true;
|
|
4246
4366
|
} else if (block.type === "tool_use" && typeof block.name === "string") {
|
|
4247
|
-
|
|
4248
|
-
|
|
4367
|
+
const toolUseId = typeof block.id === "string" ? block.id : undefined;
|
|
4368
|
+
if (toolUseId) {
|
|
4369
|
+
this.toolNames.set(toolUseId, block.name);
|
|
4249
4370
|
}
|
|
4250
|
-
this.emit("tool_start", block.name, block.input);
|
|
4371
|
+
this.emit("tool_start", block.name, block.input, toolUseId);
|
|
4251
4372
|
}
|
|
4252
4373
|
}
|
|
4253
4374
|
break;
|
|
@@ -4304,7 +4425,7 @@ async function withRetry(step, cardShortId, op, attempts, backoffMs) {
|
|
|
4304
4425
|
const msg2 = err instanceof Error ? err.message : String(err);
|
|
4305
4426
|
if (i < attempts - 1) {
|
|
4306
4427
|
const wait = backoffMs * 2 ** i;
|
|
4307
|
-
log.warn(
|
|
4428
|
+
log.warn(TAG20, `${step} failed for #${cardShortId} (attempt ${i + 1}/${attempts}): ${msg2} — retrying in ${wait}ms`);
|
|
4308
4429
|
await new Promise((r) => setTimeout(r, wait));
|
|
4309
4430
|
}
|
|
4310
4431
|
}
|
|
@@ -4326,10 +4447,10 @@ async function runTransition(client, card, plan, opts = {}) {
|
|
|
4326
4447
|
if (opts.strictColumn) {
|
|
4327
4448
|
throw new TransitionError("move", 1, msg);
|
|
4328
4449
|
}
|
|
4329
|
-
log.warn(
|
|
4450
|
+
log.warn(TAG20, `#${shortId}: ${msg} — skipping move`);
|
|
4330
4451
|
} else if (card.column_id !== target.id) {
|
|
4331
4452
|
await withRetry("move", shortId, () => client.moveCard(card.id, target.id), attempts, backoffMs);
|
|
4332
|
-
log.info(
|
|
4453
|
+
log.info(TAG20, `#${shortId} → "${target.name}"`);
|
|
4333
4454
|
card.column_id = target.id;
|
|
4334
4455
|
}
|
|
4335
4456
|
}
|
|
@@ -4342,7 +4463,7 @@ async function runTransition(client, card, plan, opts = {}) {
|
|
|
4342
4463
|
continue;
|
|
4343
4464
|
await withRetry("addLabel", shortId, () => client.addLabelToCard(card.id, labelId), attempts, backoffMs);
|
|
4344
4465
|
existing.add(labelId);
|
|
4345
|
-
log.info(
|
|
4466
|
+
log.info(TAG20, `#${shortId} +label "${name}"`);
|
|
4346
4467
|
}
|
|
4347
4468
|
card.labelIds = Array.from(existing);
|
|
4348
4469
|
}
|
|
@@ -4354,17 +4475,17 @@ async function runTransition(client, card, plan, opts = {}) {
|
|
|
4354
4475
|
continue;
|
|
4355
4476
|
await withRetry("removeLabel", shortId, () => client.removeLabelFromCard(card.id, match.id), attempts, backoffMs);
|
|
4356
4477
|
existing.delete(match.id);
|
|
4357
|
-
log.info(
|
|
4478
|
+
log.info(TAG20, `#${shortId} -label "${name}"`);
|
|
4358
4479
|
}
|
|
4359
4480
|
card.labelIds = Array.from(existing);
|
|
4360
4481
|
}
|
|
4361
4482
|
if (plan.updateCard) {
|
|
4362
4483
|
await withRetry("updateCard", shortId, () => client.updateCard(card.id, plan.updateCard), attempts, backoffMs);
|
|
4363
|
-
log.info(
|
|
4484
|
+
log.info(TAG20, `#${shortId} updated`);
|
|
4364
4485
|
}
|
|
4365
4486
|
if (plan.endSession) {
|
|
4366
4487
|
await withRetry("endSession", shortId, () => client.endAgentSession(card.id, plan.endSession), attempts, backoffMs);
|
|
4367
|
-
log.info(
|
|
4488
|
+
log.info(TAG20, `#${shortId} session ended (${plan.endSession.status})`);
|
|
4368
4489
|
}
|
|
4369
4490
|
if (opts.store && opts.runId) {
|
|
4370
4491
|
try {
|
|
@@ -4377,11 +4498,11 @@ async function ensureLabel(client, projectId, name, color, attempts, backoffMs)
|
|
|
4377
4498
|
const result = await withRetry("addLabel", 0, () => client.createLabel(projectId, { name, color: color ?? "#8b5cf6" }), attempts, backoffMs);
|
|
4378
4499
|
return result?.label?.id ?? null;
|
|
4379
4500
|
} catch (err) {
|
|
4380
|
-
log.warn(
|
|
4501
|
+
log.warn(TAG20, `ensureLabel "${name}" failed: ${err instanceof Error ? err.message : err}`);
|
|
4381
4502
|
return null;
|
|
4382
4503
|
}
|
|
4383
4504
|
}
|
|
4384
|
-
var
|
|
4505
|
+
var TAG20 = "transition", TransitionError;
|
|
4385
4506
|
var init_transitions = __esm(() => {
|
|
4386
4507
|
init_log();
|
|
4387
4508
|
TransitionError = class TransitionError extends Error {
|
|
@@ -4399,7 +4520,7 @@ var init_transitions = __esm(() => {
|
|
|
4399
4520
|
});
|
|
4400
4521
|
|
|
4401
4522
|
// src/review-worker.ts
|
|
4402
|
-
import { execFileSync as
|
|
4523
|
+
import { execFileSync as execFileSync10 } from "node:child_process";
|
|
4403
4524
|
|
|
4404
4525
|
class ReviewWorker {
|
|
4405
4526
|
config;
|
|
@@ -4465,7 +4586,7 @@ class ReviewWorker {
|
|
|
4465
4586
|
}
|
|
4466
4587
|
}
|
|
4467
4588
|
get tag() {
|
|
4468
|
-
return `${
|
|
4589
|
+
return `${TAG21}:${this.id}`;
|
|
4469
4590
|
}
|
|
4470
4591
|
get isIdle() {
|
|
4471
4592
|
return this.state === "idle";
|
|
@@ -4522,7 +4643,7 @@ class ReviewWorker {
|
|
|
4522
4643
|
let localDiff = null;
|
|
4523
4644
|
if (localMode) {
|
|
4524
4645
|
log.info(this.tag, `No branch found for #${card.short_id}, attempting local review`);
|
|
4525
|
-
this.worktreePath =
|
|
4646
|
+
this.worktreePath = execFileSync10("git", ["rev-parse", "--show-toplevel"], {
|
|
4526
4647
|
encoding: "utf-8",
|
|
4527
4648
|
timeout: 5000
|
|
4528
4649
|
}).trim();
|
|
@@ -4589,7 +4710,7 @@ class ReviewWorker {
|
|
|
4589
4710
|
if (localMode) {
|
|
4590
4711
|
diff = localDiff ?? "";
|
|
4591
4712
|
} else {
|
|
4592
|
-
diff =
|
|
4713
|
+
diff = execFileSync10("git", ["diff", `origin/${this.config.worktree.baseBranch}..HEAD`], { cwd, encoding: "utf-8", timeout: 30000 });
|
|
4593
4714
|
}
|
|
4594
4715
|
} catch {
|
|
4595
4716
|
diff = "(unable to retrieve diff)";
|
|
@@ -4869,7 +4990,7 @@ class ReviewWorker {
|
|
|
4869
4990
|
}
|
|
4870
4991
|
resolveLocalChanges(repoRoot, shortId) {
|
|
4871
4992
|
try {
|
|
4872
|
-
const localChanges =
|
|
4993
|
+
const localChanges = execFileSync10("git", ["diff", "HEAD"], {
|
|
4873
4994
|
cwd: repoRoot,
|
|
4874
4995
|
encoding: "utf-8",
|
|
4875
4996
|
timeout: 5000
|
|
@@ -4881,7 +5002,7 @@ class ReviewWorker {
|
|
|
4881
5002
|
log.warn(this.tag, "Failed to check uncommitted changes");
|
|
4882
5003
|
}
|
|
4883
5004
|
try {
|
|
4884
|
-
const matchingCommits =
|
|
5005
|
+
const matchingCommits = execFileSync10("git", ["log", "--format=%H", "-20", `--grep=#${shortId}`], { cwd: repoRoot, encoding: "utf-8", timeout: 1e4 }).trim();
|
|
4885
5006
|
if (matchingCommits) {
|
|
4886
5007
|
const hashes = matchingCommits.split(`
|
|
4887
5008
|
`).filter((h) => /^[0-9a-f]{4,40}$/i.test(h));
|
|
@@ -4891,7 +5012,7 @@ class ReviewWorker {
|
|
|
4891
5012
|
const diffs = [];
|
|
4892
5013
|
for (const hash of hashes) {
|
|
4893
5014
|
try {
|
|
4894
|
-
const commitDiff =
|
|
5015
|
+
const commitDiff = execFileSync10("git", ["diff", `${hash}~1..${hash}`], { cwd: repoRoot, encoding: "utf-8", timeout: 30000 });
|
|
4895
5016
|
if (commitDiff)
|
|
4896
5017
|
diffs.push(commitDiff);
|
|
4897
5018
|
} catch {
|
|
@@ -4934,7 +5055,7 @@ class ReviewWorker {
|
|
|
4934
5055
|
this.lastSessionStats = null;
|
|
4935
5056
|
}
|
|
4936
5057
|
}
|
|
4937
|
-
var
|
|
5058
|
+
var TAG21 = "review-worker", CANCEL_SIGINT_TIMEOUT = 30000, CANCEL_SIGTERM_TIMEOUT = 1e4;
|
|
4938
5059
|
var init_review_worker = __esm(() => {
|
|
4939
5060
|
init_board_helpers();
|
|
4940
5061
|
init_completion();
|
|
@@ -4983,7 +5104,7 @@ class SleepGuard {
|
|
|
4983
5104
|
if (!this.child.killed)
|
|
4984
5105
|
this.child.kill("SIGTERM");
|
|
4985
5106
|
this.child = null;
|
|
4986
|
-
log.info(
|
|
5107
|
+
log.info(TAG22, "sleep assertion released");
|
|
4987
5108
|
}
|
|
4988
5109
|
}
|
|
4989
5110
|
start() {
|
|
@@ -4998,7 +5119,7 @@ class SleepGuard {
|
|
|
4998
5119
|
spawned = true;
|
|
4999
5120
|
});
|
|
5000
5121
|
child.on("error", (err) => {
|
|
5001
|
-
log.warn(
|
|
5122
|
+
log.warn(TAG22, `caffeinate unavailable: ${err.message}`);
|
|
5002
5123
|
if (this.child === child)
|
|
5003
5124
|
this.child = null;
|
|
5004
5125
|
});
|
|
@@ -5011,13 +5132,13 @@ class SleepGuard {
|
|
|
5011
5132
|
});
|
|
5012
5133
|
child.unref();
|
|
5013
5134
|
this.child = child;
|
|
5014
|
-
log.info(
|
|
5135
|
+
log.info(TAG22, "sleep assertion acquired (caffeinate -i)");
|
|
5015
5136
|
} catch (err) {
|
|
5016
|
-
log.warn(
|
|
5137
|
+
log.warn(TAG22, `failed to spawn caffeinate: ${err instanceof Error ? err.message : err}`);
|
|
5017
5138
|
}
|
|
5018
5139
|
}
|
|
5019
5140
|
}
|
|
5020
|
-
var
|
|
5141
|
+
var TAG22 = "sleep-guard";
|
|
5021
5142
|
var init_sleep_guard = __esm(() => {
|
|
5022
5143
|
init_log();
|
|
5023
5144
|
});
|
|
@@ -5028,7 +5149,7 @@ async function fetchBlocksLinks(client, cardId) {
|
|
|
5028
5149
|
const { links } = await client.getCardLinks(cardId);
|
|
5029
5150
|
return links.filter((l) => l.link_type === "blocks");
|
|
5030
5151
|
} catch (err) {
|
|
5031
|
-
log.warn(
|
|
5152
|
+
log.warn(TAG23, `link fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
5032
5153
|
return null;
|
|
5033
5154
|
}
|
|
5034
5155
|
}
|
|
@@ -5060,31 +5181,175 @@ async function promoteUnblockedSuccessors(completedCard, deps) {
|
|
|
5060
5181
|
const successors = links.filter((l) => l.direction === "outgoing" && !l.target_card.done);
|
|
5061
5182
|
if (successors.length === 0)
|
|
5062
5183
|
return;
|
|
5063
|
-
log.info(
|
|
5184
|
+
log.info(TAG23, `#${completedCard.short_id} completed — checking ${successors.length} chained successor(s)`);
|
|
5064
5185
|
for (const link of successors) {
|
|
5065
5186
|
const successorId = link.target_card.id;
|
|
5066
5187
|
try {
|
|
5067
5188
|
const { card } = await deps.client.getCard(successorId);
|
|
5068
5189
|
if (card.assigned_agent_id === deps.agentId) {} else if (card.assigned_agent_id === null && !card.assignee_id) {
|
|
5069
|
-
log.info(
|
|
5190
|
+
log.info(TAG23, `successor #${card.short_id} unassigned — auto-assigning to continue chain`);
|
|
5070
5191
|
await deps.client.updateCard(successorId, {
|
|
5071
5192
|
assignedAgentId: deps.agentId
|
|
5072
5193
|
});
|
|
5073
5194
|
} else {
|
|
5074
|
-
log.debug(
|
|
5195
|
+
log.debug(TAG23, `successor #${card.short_id} assigned to different entity — skipping`);
|
|
5075
5196
|
continue;
|
|
5076
5197
|
}
|
|
5077
5198
|
await deps.enqueue(successorId);
|
|
5078
5199
|
} catch (err) {
|
|
5079
|
-
log.warn(
|
|
5200
|
+
log.warn(TAG23, `promotion failed for successor ${successorId}: ${err instanceof Error ? err.message : err}`);
|
|
5080
5201
|
}
|
|
5081
5202
|
}
|
|
5082
5203
|
}
|
|
5083
|
-
var
|
|
5204
|
+
var TAG23 = "unblock";
|
|
5084
5205
|
var init_unblock = __esm(() => {
|
|
5085
5206
|
init_log();
|
|
5086
5207
|
});
|
|
5087
5208
|
|
|
5209
|
+
// src/cli-agent-runner.ts
|
|
5210
|
+
class CliAgentRunner {
|
|
5211
|
+
client;
|
|
5212
|
+
cardId;
|
|
5213
|
+
sessionId;
|
|
5214
|
+
buffer = [];
|
|
5215
|
+
flushTimer = null;
|
|
5216
|
+
flushing = false;
|
|
5217
|
+
stopped = false;
|
|
5218
|
+
constructor(client, cardId, sessionId) {
|
|
5219
|
+
this.client = client;
|
|
5220
|
+
this.cardId = cardId;
|
|
5221
|
+
this.sessionId = sessionId;
|
|
5222
|
+
}
|
|
5223
|
+
attach(parser) {
|
|
5224
|
+
if (this.stopped)
|
|
5225
|
+
return;
|
|
5226
|
+
parser.on("tool_start", (name, input, toolUseId) => {
|
|
5227
|
+
this.enqueue({
|
|
5228
|
+
kind: "tool_started",
|
|
5229
|
+
source: "agent",
|
|
5230
|
+
payload: { toolName: name, toolUseId, input }
|
|
5231
|
+
});
|
|
5232
|
+
});
|
|
5233
|
+
parser.on("tool_end", (name, toolUseId, content) => {
|
|
5234
|
+
this.enqueue({
|
|
5235
|
+
kind: "tool_ended",
|
|
5236
|
+
source: "agent",
|
|
5237
|
+
payload: {
|
|
5238
|
+
toolName: name,
|
|
5239
|
+
toolUseId,
|
|
5240
|
+
output: content === undefined ? undefined : content.slice(0, MAX_OUTPUT_LEN)
|
|
5241
|
+
}
|
|
5242
|
+
});
|
|
5243
|
+
});
|
|
5244
|
+
parser.on("text", (content) => {
|
|
5245
|
+
const text = content.trim();
|
|
5246
|
+
if (!text)
|
|
5247
|
+
return;
|
|
5248
|
+
this.enqueue({
|
|
5249
|
+
kind: "assistant_text",
|
|
5250
|
+
source: "agent",
|
|
5251
|
+
payload: { text: text.slice(0, MAX_TEXT_LEN) }
|
|
5252
|
+
});
|
|
5253
|
+
});
|
|
5254
|
+
parser.on("cost_update", (cost) => {
|
|
5255
|
+
this.enqueue({
|
|
5256
|
+
kind: "cost_updated",
|
|
5257
|
+
source: "agent",
|
|
5258
|
+
payload: mapCost(cost)
|
|
5259
|
+
});
|
|
5260
|
+
});
|
|
5261
|
+
this.startTimer();
|
|
5262
|
+
}
|
|
5263
|
+
recordRunStarted(payload) {
|
|
5264
|
+
this.enqueue({ kind: "run_started", source: "system", payload });
|
|
5265
|
+
this.startTimer();
|
|
5266
|
+
}
|
|
5267
|
+
recordPhaseChanged(phase, previousPhase) {
|
|
5268
|
+
this.enqueue({
|
|
5269
|
+
kind: "phase_changed",
|
|
5270
|
+
source: "system",
|
|
5271
|
+
payload: { phase, previousPhase }
|
|
5272
|
+
});
|
|
5273
|
+
}
|
|
5274
|
+
recordProgress(payload) {
|
|
5275
|
+
this.enqueue({ kind: "progress", source: "system", payload });
|
|
5276
|
+
}
|
|
5277
|
+
recordError(payload) {
|
|
5278
|
+
this.enqueue({ kind: "error", source: "system", payload });
|
|
5279
|
+
}
|
|
5280
|
+
recordFinished(payload) {
|
|
5281
|
+
this.enqueue({ kind: "run_finished", source: "system", payload });
|
|
5282
|
+
}
|
|
5283
|
+
record(body) {
|
|
5284
|
+
this.enqueue(body);
|
|
5285
|
+
this.startTimer();
|
|
5286
|
+
}
|
|
5287
|
+
enqueue(body) {
|
|
5288
|
+
if (this.stopped)
|
|
5289
|
+
return;
|
|
5290
|
+
this.buffer.push({ ...body, createdAt: new Date().toISOString() });
|
|
5291
|
+
if (this.buffer.length >= MAX_BUFFER)
|
|
5292
|
+
this.flush();
|
|
5293
|
+
}
|
|
5294
|
+
startTimer() {
|
|
5295
|
+
if (this.flushTimer || this.stopped)
|
|
5296
|
+
return;
|
|
5297
|
+
this.flushTimer = setInterval(() => void this.flush(), FLUSH_INTERVAL_MS);
|
|
5298
|
+
this.flushTimer.unref?.();
|
|
5299
|
+
}
|
|
5300
|
+
async flush() {
|
|
5301
|
+
if (this.flushing || this.buffer.length === 0)
|
|
5302
|
+
return;
|
|
5303
|
+
this.flushing = true;
|
|
5304
|
+
const batch = this.buffer.splice(0, MAX_BUFFER);
|
|
5305
|
+
try {
|
|
5306
|
+
await this.client.appendAgentRunEvents(this.cardId, {
|
|
5307
|
+
sessionId: this.sessionId,
|
|
5308
|
+
events: batch
|
|
5309
|
+
});
|
|
5310
|
+
} catch (err) {
|
|
5311
|
+
log.warn(TAG24, `Failed to flush run events: ${err}`);
|
|
5312
|
+
this.buffer.unshift(...batch);
|
|
5313
|
+
if (this.buffer.length > MAX_BUFFER) {
|
|
5314
|
+
this.buffer.length = MAX_BUFFER;
|
|
5315
|
+
}
|
|
5316
|
+
} finally {
|
|
5317
|
+
this.flushing = false;
|
|
5318
|
+
}
|
|
5319
|
+
}
|
|
5320
|
+
async flushFinal() {
|
|
5321
|
+
for (let i = 0;this.buffer.length > 0 && i < 5; i++) {
|
|
5322
|
+
const before = this.buffer.length;
|
|
5323
|
+
await this.flush();
|
|
5324
|
+
if (this.buffer.length >= before)
|
|
5325
|
+
break;
|
|
5326
|
+
}
|
|
5327
|
+
}
|
|
5328
|
+
stop() {
|
|
5329
|
+
this.stopped = true;
|
|
5330
|
+
if (this.flushTimer) {
|
|
5331
|
+
clearInterval(this.flushTimer);
|
|
5332
|
+
this.flushTimer = null;
|
|
5333
|
+
}
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
function mapCost(cost) {
|
|
5337
|
+
return {
|
|
5338
|
+
totalCostUsd: cost.totalCostUsd,
|
|
5339
|
+
inputTokens: cost.totalInputTokens,
|
|
5340
|
+
outputTokens: cost.totalOutputTokens,
|
|
5341
|
+
cacheCreationInputTokens: cost.totalCacheCreationInputTokens,
|
|
5342
|
+
cacheReadInputTokens: cost.totalCacheReadInputTokens,
|
|
5343
|
+
numTurns: cost.numTurns,
|
|
5344
|
+
modelName: cost.modelName,
|
|
5345
|
+
durationMs: cost.durationMs
|
|
5346
|
+
};
|
|
5347
|
+
}
|
|
5348
|
+
var TAG24 = "cli-agent-runner", FLUSH_INTERVAL_MS = 2000, MAX_BUFFER = 1000, MAX_TEXT_LEN = 8000, MAX_OUTPUT_LEN = 4000;
|
|
5349
|
+
var init_cli_agent_runner = __esm(() => {
|
|
5350
|
+
init_log();
|
|
5351
|
+
});
|
|
5352
|
+
|
|
5088
5353
|
// src/model-tier.ts
|
|
5089
5354
|
function clampWithdrawn(model) {
|
|
5090
5355
|
return WITHDRAWN_MODEL.test(model) ? MAX_IMPLEMENT_MODEL : model;
|
|
@@ -5135,11 +5400,11 @@ async function buildPrompt(enriched, branchName, worktreePath, client, workspace
|
|
|
5135
5400
|
Do NOT push to main. All your work stays on \`${branchName}\`.
|
|
5136
5401
|
When finished, call harmony_end_agent_session with status="completed".`
|
|
5137
5402
|
});
|
|
5138
|
-
log.info(
|
|
5403
|
+
log.info(TAG25, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
|
|
5139
5404
|
return result.prompt + pastEpisodesSection;
|
|
5140
5405
|
} catch (err) {
|
|
5141
5406
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5142
|
-
log.warn(
|
|
5407
|
+
log.warn(TAG25, `Failed to generate prompt via API, using fallback: ${msg}`);
|
|
5143
5408
|
const commentsSection = await renderCommentsSection(client, card.id);
|
|
5144
5409
|
return buildFallbackPrompt(enriched, branchName, worktreePath) + commentsSection + pastEpisodesSection;
|
|
5145
5410
|
}
|
|
@@ -5157,7 +5422,7 @@ async function renderCommentsSection(client, cardId) {
|
|
|
5157
5422
|
|
|
5158
5423
|
${section}` : "";
|
|
5159
5424
|
} catch (err) {
|
|
5160
|
-
log.warn(
|
|
5425
|
+
log.warn(TAG25, "comment-thread fetch failed", {
|
|
5161
5426
|
event: "comment_fetch_failed",
|
|
5162
5427
|
error: err instanceof Error ? err.message : String(err)
|
|
5163
5428
|
});
|
|
@@ -5207,7 +5472,7 @@ ${description}`.trim();
|
|
|
5207
5472
|
## Similar past tasks
|
|
5208
5473
|
${bullets}`;
|
|
5209
5474
|
} catch (err) {
|
|
5210
|
-
log.warn(
|
|
5475
|
+
log.warn(TAG25, "past-episodes recall failed", {
|
|
5211
5476
|
event: "episode_recall_failed",
|
|
5212
5477
|
error: err instanceof Error ? err.message : String(err)
|
|
5213
5478
|
});
|
|
@@ -5248,13 +5513,346 @@ ${subtaskStr}
|
|
|
5248
5513
|
You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
|
|
5249
5514
|
Do NOT push to main. All your work stays on \`${branchName}\`.`;
|
|
5250
5515
|
}
|
|
5251
|
-
var
|
|
5516
|
+
var TAG25 = "prompt";
|
|
5252
5517
|
var init_prompt = __esm(() => {
|
|
5253
5518
|
init_dist();
|
|
5254
5519
|
init_log();
|
|
5255
5520
|
});
|
|
5256
5521
|
|
|
5522
|
+
// src/sdk-agent-runner.ts
|
|
5523
|
+
import {
|
|
5524
|
+
query
|
|
5525
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
5526
|
+
function mapSdkErrorKind(e) {
|
|
5527
|
+
switch (e) {
|
|
5528
|
+
case "authentication_failed":
|
|
5529
|
+
case "oauth_org_not_allowed":
|
|
5530
|
+
return "auth";
|
|
5531
|
+
case "billing_error":
|
|
5532
|
+
return "out_of_credits";
|
|
5533
|
+
case "rate_limit":
|
|
5534
|
+
case "overloaded":
|
|
5535
|
+
return "rate_limit";
|
|
5536
|
+
default:
|
|
5537
|
+
return null;
|
|
5538
|
+
}
|
|
5539
|
+
}
|
|
5540
|
+
|
|
5541
|
+
class SdkAgentRunner {
|
|
5542
|
+
cfg;
|
|
5543
|
+
abort = null;
|
|
5544
|
+
capturedSessionId;
|
|
5545
|
+
child = null;
|
|
5546
|
+
leaderPid;
|
|
5547
|
+
capturedStderr = "";
|
|
5548
|
+
toolNames = new Map;
|
|
5549
|
+
observedModel;
|
|
5550
|
+
effectiveModel;
|
|
5551
|
+
constructor(cfg = {}) {
|
|
5552
|
+
this.cfg = cfg;
|
|
5553
|
+
}
|
|
5554
|
+
get sessionId() {
|
|
5555
|
+
return this.capturedSessionId;
|
|
5556
|
+
}
|
|
5557
|
+
get capturedStderrText() {
|
|
5558
|
+
return this.capturedStderr;
|
|
5559
|
+
}
|
|
5560
|
+
start(input) {
|
|
5561
|
+
return this.run(input);
|
|
5562
|
+
}
|
|
5563
|
+
resume(input) {
|
|
5564
|
+
return this.run(input, input.resumeSessionId);
|
|
5565
|
+
}
|
|
5566
|
+
async send(_message) {
|
|
5567
|
+
throw new Error("SdkAgentRunner.send() (streaming-input steering) is not wired; the worker steers via stop→resume");
|
|
5568
|
+
}
|
|
5569
|
+
async stop(_reason) {
|
|
5570
|
+
this.abort?.abort();
|
|
5571
|
+
if (this.child) {
|
|
5572
|
+
await terminateGroup(this.child, {
|
|
5573
|
+
sigintTimeoutMs: STOP_SIGINT_MS,
|
|
5574
|
+
sigtermTimeoutMs: STOP_SIGTERM_MS
|
|
5575
|
+
});
|
|
5576
|
+
}
|
|
5577
|
+
reapGroup(this.leaderPid);
|
|
5578
|
+
}
|
|
5579
|
+
async* run(input, resumeSessionId) {
|
|
5580
|
+
this.abort = new AbortController;
|
|
5581
|
+
this.capturedStderr = "";
|
|
5582
|
+
this.child = null;
|
|
5583
|
+
this.leaderPid = undefined;
|
|
5584
|
+
this.toolNames.clear();
|
|
5585
|
+
this.observedModel = undefined;
|
|
5586
|
+
this.effectiveModel = input.model ?? this.cfg.model;
|
|
5587
|
+
yield {
|
|
5588
|
+
kind: "run_started",
|
|
5589
|
+
source: "system",
|
|
5590
|
+
payload: { runner: "sdk", model: input.model }
|
|
5591
|
+
};
|
|
5592
|
+
const allowed = this.cfg.allowedTools ?? SDK_ALLOWED_TOOLS;
|
|
5593
|
+
const builtinTools = allowed.filter((t) => !t.startsWith("mcp__") && !t.includes("*"));
|
|
5594
|
+
const options = {
|
|
5595
|
+
cwd: input.cwd,
|
|
5596
|
+
model: input.model ?? this.cfg.model,
|
|
5597
|
+
allowedTools: allowed,
|
|
5598
|
+
tools: builtinTools,
|
|
5599
|
+
permissionMode: "dontAsk",
|
|
5600
|
+
maxTurns: this.cfg.maxTurns,
|
|
5601
|
+
abortController: this.abort,
|
|
5602
|
+
...resumeSessionId ? { resume: resumeSessionId } : {},
|
|
5603
|
+
...this.cfg.maxBudgetUsd ? { maxBudgetUsd: this.cfg.maxBudgetUsd } : {},
|
|
5604
|
+
...this.cfg.settingSources ? { settingSources: this.cfg.settingSources } : {},
|
|
5605
|
+
...this.cfg.mcpServers ? { mcpServers: this.cfg.mcpServers } : {},
|
|
5606
|
+
...this.cfg.strictMcpConfig ? { strictMcpConfig: true } : {},
|
|
5607
|
+
stderr: (data) => {
|
|
5608
|
+
this.capturedStderr += data;
|
|
5609
|
+
},
|
|
5610
|
+
spawnClaudeCodeProcess: (spawnOpts) => this.spawn(spawnOpts)
|
|
5611
|
+
};
|
|
5612
|
+
try {
|
|
5613
|
+
const q = query({ prompt: input.prompt, options });
|
|
5614
|
+
let failureReason = null;
|
|
5615
|
+
for await (const msg of q) {
|
|
5616
|
+
for (const ev of this.mapMessage(msg)) {
|
|
5617
|
+
if (ev.kind === "error") {
|
|
5618
|
+
failureReason = ev.payload.errorKind ?? "crash";
|
|
5619
|
+
}
|
|
5620
|
+
yield ev;
|
|
5621
|
+
}
|
|
5622
|
+
}
|
|
5623
|
+
yield {
|
|
5624
|
+
kind: "run_finished",
|
|
5625
|
+
source: "system",
|
|
5626
|
+
payload: failureReason ? { status: "failed", failureReason } : { status: "completed" }
|
|
5627
|
+
};
|
|
5628
|
+
} catch (err) {
|
|
5629
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5630
|
+
const cls = classifyRunError(`${message}
|
|
5631
|
+
${this.capturedStderr}`);
|
|
5632
|
+
yield {
|
|
5633
|
+
kind: "error",
|
|
5634
|
+
source: "system",
|
|
5635
|
+
payload: {
|
|
5636
|
+
message,
|
|
5637
|
+
errorKind: cls.kind,
|
|
5638
|
+
retryable: cls.kind !== "auth" && cls.kind !== null
|
|
5639
|
+
}
|
|
5640
|
+
};
|
|
5641
|
+
yield {
|
|
5642
|
+
kind: "run_finished",
|
|
5643
|
+
source: "system",
|
|
5644
|
+
payload: { status: "failed", failureReason: cls.kind ?? "crash" }
|
|
5645
|
+
};
|
|
5646
|
+
} finally {
|
|
5647
|
+
reapGroup(this.leaderPid);
|
|
5648
|
+
}
|
|
5649
|
+
}
|
|
5650
|
+
spawn(spawnOpts) {
|
|
5651
|
+
const child = spawnInGroup(spawnOpts.command, spawnOpts.args, {
|
|
5652
|
+
cwd: spawnOpts.cwd,
|
|
5653
|
+
env: spawnOpts.env,
|
|
5654
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
5655
|
+
});
|
|
5656
|
+
this.child = child;
|
|
5657
|
+
this.leaderPid = child.pid;
|
|
5658
|
+
child.stderr?.on("data", (d) => {
|
|
5659
|
+
this.capturedStderr += d.toString();
|
|
5660
|
+
});
|
|
5661
|
+
this.cfg.onSpawn?.(child);
|
|
5662
|
+
return {
|
|
5663
|
+
stdin: child.stdin,
|
|
5664
|
+
stdout: child.stdout,
|
|
5665
|
+
get killed() {
|
|
5666
|
+
return child.killed;
|
|
5667
|
+
},
|
|
5668
|
+
get exitCode() {
|
|
5669
|
+
return child.exitCode;
|
|
5670
|
+
},
|
|
5671
|
+
kill: (signal) => {
|
|
5672
|
+
if (!child.pid)
|
|
5673
|
+
return false;
|
|
5674
|
+
try {
|
|
5675
|
+
process.kill(-child.pid, signal);
|
|
5676
|
+
return true;
|
|
5677
|
+
} catch {
|
|
5678
|
+
return child.kill(signal);
|
|
5679
|
+
}
|
|
5680
|
+
},
|
|
5681
|
+
on: (event, listener) => child.on(event, listener),
|
|
5682
|
+
once: (event, listener) => child.once(event, listener),
|
|
5683
|
+
off: (event, listener) => child.off(event, listener)
|
|
5684
|
+
};
|
|
5685
|
+
}
|
|
5686
|
+
*mapMessage(msg) {
|
|
5687
|
+
const sid = msg.session_id;
|
|
5688
|
+
if (sid && !this.capturedSessionId)
|
|
5689
|
+
this.capturedSessionId = sid;
|
|
5690
|
+
const model = msg.message?.model ?? msg.model;
|
|
5691
|
+
if (typeof model === "string" && !this.observedModel) {
|
|
5692
|
+
this.observedModel = model;
|
|
5693
|
+
}
|
|
5694
|
+
switch (msg.type) {
|
|
5695
|
+
case "assistant": {
|
|
5696
|
+
const am = msg;
|
|
5697
|
+
if (am.error) {
|
|
5698
|
+
yield {
|
|
5699
|
+
kind: "error",
|
|
5700
|
+
source: "system",
|
|
5701
|
+
payload: {
|
|
5702
|
+
message: `assistant error: ${am.error}`,
|
|
5703
|
+
errorKind: mapSdkErrorKind(am.error)
|
|
5704
|
+
}
|
|
5705
|
+
};
|
|
5706
|
+
}
|
|
5707
|
+
const blocks = am.message?.content;
|
|
5708
|
+
if (Array.isArray(blocks)) {
|
|
5709
|
+
for (const b of blocks) {
|
|
5710
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
5711
|
+
const text = b.text.trim();
|
|
5712
|
+
if (text) {
|
|
5713
|
+
yield {
|
|
5714
|
+
kind: "assistant_text",
|
|
5715
|
+
source: "agent",
|
|
5716
|
+
payload: { text: text.slice(0, MAX_TEXT_LEN2) }
|
|
5717
|
+
};
|
|
5718
|
+
}
|
|
5719
|
+
} else if (b.type === "tool_use" && typeof b.name === "string") {
|
|
5720
|
+
if (typeof b.id === "string")
|
|
5721
|
+
this.toolNames.set(b.id, b.name);
|
|
5722
|
+
yield {
|
|
5723
|
+
kind: "tool_started",
|
|
5724
|
+
source: "agent",
|
|
5725
|
+
payload: { toolName: b.name, toolUseId: b.id, input: b.input }
|
|
5726
|
+
};
|
|
5727
|
+
}
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
5730
|
+
break;
|
|
5731
|
+
}
|
|
5732
|
+
case "user": {
|
|
5733
|
+
const um = msg;
|
|
5734
|
+
const blocks = um.message?.content;
|
|
5735
|
+
if (Array.isArray(blocks)) {
|
|
5736
|
+
for (const b of blocks) {
|
|
5737
|
+
if (b.type === "tool_result" && typeof b.tool_use_id === "string") {
|
|
5738
|
+
const toolName = this.toolNames.get(b.tool_use_id) ?? "";
|
|
5739
|
+
this.toolNames.delete(b.tool_use_id);
|
|
5740
|
+
yield {
|
|
5741
|
+
kind: "tool_ended",
|
|
5742
|
+
source: "agent",
|
|
5743
|
+
payload: {
|
|
5744
|
+
toolName,
|
|
5745
|
+
toolUseId: b.tool_use_id,
|
|
5746
|
+
output: normalize(b.content)?.slice(0, MAX_OUTPUT_LEN2),
|
|
5747
|
+
isError: b.is_error
|
|
5748
|
+
}
|
|
5749
|
+
};
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5752
|
+
}
|
|
5753
|
+
break;
|
|
5754
|
+
}
|
|
5755
|
+
case "result": {
|
|
5756
|
+
const r = msg;
|
|
5757
|
+
if (typeof r.total_cost_usd === "number") {
|
|
5758
|
+
yield {
|
|
5759
|
+
kind: "cost_updated",
|
|
5760
|
+
source: "agent",
|
|
5761
|
+
payload: {
|
|
5762
|
+
totalCostUsd: r.total_cost_usd,
|
|
5763
|
+
inputTokens: r.usage?.input_tokens ?? 0,
|
|
5764
|
+
outputTokens: r.usage?.output_tokens ?? 0,
|
|
5765
|
+
cacheCreationInputTokens: r.usage?.cache_creation_input_tokens ?? 0,
|
|
5766
|
+
cacheReadInputTokens: r.usage?.cache_read_input_tokens ?? 0,
|
|
5767
|
+
numTurns: r.num_turns ?? 0,
|
|
5768
|
+
durationMs: r.duration_ms,
|
|
5769
|
+
modelName: this.observedModel ?? this.effectiveModel
|
|
5770
|
+
}
|
|
5771
|
+
};
|
|
5772
|
+
}
|
|
5773
|
+
if (r.subtype && r.subtype !== "success") {
|
|
5774
|
+
const joined = (r.errors ?? []).join(`
|
|
5775
|
+
`);
|
|
5776
|
+
const cls = classifyRunError(`${r.subtype}
|
|
5777
|
+
${joined}
|
|
5778
|
+
${this.capturedStderr}`);
|
|
5779
|
+
yield {
|
|
5780
|
+
kind: "error",
|
|
5781
|
+
source: "system",
|
|
5782
|
+
payload: {
|
|
5783
|
+
message: `result ${r.subtype}: ${joined || "(no detail)"}`,
|
|
5784
|
+
errorKind: cls.kind,
|
|
5785
|
+
retryable: cls.kind !== "auth" && cls.kind !== null
|
|
5786
|
+
}
|
|
5787
|
+
};
|
|
5788
|
+
}
|
|
5789
|
+
break;
|
|
5790
|
+
}
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5793
|
+
}
|
|
5794
|
+
function normalize(raw) {
|
|
5795
|
+
if (raw == null)
|
|
5796
|
+
return;
|
|
5797
|
+
if (typeof raw === "string")
|
|
5798
|
+
return raw;
|
|
5799
|
+
if (Array.isArray(raw)) {
|
|
5800
|
+
const parts = [];
|
|
5801
|
+
for (const b of raw) {
|
|
5802
|
+
if (b && typeof b === "object" && "text" in b && typeof b.text === "string") {
|
|
5803
|
+
parts.push(b.text);
|
|
5804
|
+
}
|
|
5805
|
+
}
|
|
5806
|
+
return parts.length ? parts.join("") : JSON.stringify(raw);
|
|
5807
|
+
}
|
|
5808
|
+
try {
|
|
5809
|
+
return JSON.stringify(raw);
|
|
5810
|
+
} catch {
|
|
5811
|
+
return String(raw);
|
|
5812
|
+
}
|
|
5813
|
+
}
|
|
5814
|
+
var SDK_ALLOWED_TOOLS, MAX_TEXT_LEN2 = 8000, MAX_OUTPUT_LEN2 = 4000, STOP_SIGINT_MS = 2000, STOP_SIGTERM_MS = 2000;
|
|
5815
|
+
var init_sdk_agent_runner = __esm(() => {
|
|
5816
|
+
init_error_classifier();
|
|
5817
|
+
init_process_group();
|
|
5818
|
+
SDK_ALLOWED_TOOLS = [
|
|
5819
|
+
"Bash",
|
|
5820
|
+
"Read",
|
|
5821
|
+
"Write",
|
|
5822
|
+
"Edit",
|
|
5823
|
+
"Glob",
|
|
5824
|
+
"Grep",
|
|
5825
|
+
"Agent",
|
|
5826
|
+
"mcp__harmony__*"
|
|
5827
|
+
];
|
|
5828
|
+
});
|
|
5829
|
+
|
|
5257
5830
|
// src/worker.ts
|
|
5831
|
+
function sdkDraftLogLine(ev) {
|
|
5832
|
+
switch (ev.kind) {
|
|
5833
|
+
case "assistant_text":
|
|
5834
|
+
return ev.payload.text.slice(0, 200);
|
|
5835
|
+
case "tool_started":
|
|
5836
|
+
return ev.payload.toolName;
|
|
5837
|
+
case "tool_ended":
|
|
5838
|
+
return `${ev.payload.toolUseId ?? ""}${ev.payload.isError ? " (error)" : ""}`;
|
|
5839
|
+
case "cost_updated":
|
|
5840
|
+
return `$${ev.payload.totalCostUsd.toFixed(4)} turns=${ev.payload.numTurns}`;
|
|
5841
|
+
case "error":
|
|
5842
|
+
return ev.payload.message.slice(0, 200);
|
|
5843
|
+
case "run_finished":
|
|
5844
|
+
return ev.payload.status;
|
|
5845
|
+
default:
|
|
5846
|
+
return "";
|
|
5847
|
+
}
|
|
5848
|
+
}
|
|
5849
|
+
function buildSteeringPrompt(messages) {
|
|
5850
|
+
if (messages.length === 1)
|
|
5851
|
+
return messages[0];
|
|
5852
|
+
return messages.map((m, i) => `${i + 1}. ${m}`).join(`
|
|
5853
|
+
`);
|
|
5854
|
+
}
|
|
5855
|
+
|
|
5258
5856
|
class Worker {
|
|
5259
5857
|
config;
|
|
5260
5858
|
client;
|
|
@@ -5275,12 +5873,16 @@ class Worker {
|
|
|
5275
5873
|
timeoutTimer = null;
|
|
5276
5874
|
heartbeatTimer = null;
|
|
5277
5875
|
progressTracker = null;
|
|
5876
|
+
cliRunner = null;
|
|
5877
|
+
sdkRunner = null;
|
|
5278
5878
|
lastSessionStats;
|
|
5279
5879
|
aborted = false;
|
|
5280
5880
|
timedOut = false;
|
|
5281
5881
|
verificationFailed = false;
|
|
5282
5882
|
sessionId = null;
|
|
5283
5883
|
runId = null;
|
|
5884
|
+
cliSessionId = null;
|
|
5885
|
+
lastDrainedSeq = 0;
|
|
5284
5886
|
runCostCents = 0;
|
|
5285
5887
|
runTurns = 0;
|
|
5286
5888
|
constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted, onApiError) {
|
|
@@ -5327,7 +5929,7 @@ class Worker {
|
|
|
5327
5929
|
}
|
|
5328
5930
|
}
|
|
5329
5931
|
get tag() {
|
|
5330
|
-
return `${
|
|
5932
|
+
return `${TAG26}:${this.id}`;
|
|
5331
5933
|
}
|
|
5332
5934
|
get isIdle() {
|
|
5333
5935
|
return this.state === "idle";
|
|
@@ -5349,6 +5951,8 @@ class Worker {
|
|
|
5349
5951
|
this.verificationFailed = false;
|
|
5350
5952
|
this.runCostCents = 0;
|
|
5351
5953
|
this.runTurns = 0;
|
|
5954
|
+
this.cliSessionId = null;
|
|
5955
|
+
this.lastDrainedSeq = 0;
|
|
5352
5956
|
this.cardId = card.id;
|
|
5353
5957
|
this.startedAt = Date.now();
|
|
5354
5958
|
this.runId = newRunId();
|
|
@@ -5386,9 +5990,16 @@ class Worker {
|
|
|
5386
5990
|
});
|
|
5387
5991
|
const sid = session && typeof session === "object" && "id" in session ? session.id : null;
|
|
5388
5992
|
if (!sid) {
|
|
5389
|
-
log.warn(
|
|
5993
|
+
log.warn(TAG26, "startAgentSession returned no session id");
|
|
5390
5994
|
}
|
|
5391
5995
|
this.sessionId = sid;
|
|
5996
|
+
if (this.sessionId) {
|
|
5997
|
+
this.cliRunner = new CliAgentRunner(this.client, card.id, this.sessionId);
|
|
5998
|
+
this.cliRunner.recordRunStarted({
|
|
5999
|
+
runner: this.config.runner,
|
|
6000
|
+
model: this.config.claude.model
|
|
6001
|
+
});
|
|
6002
|
+
}
|
|
5392
6003
|
await this.recordPhase("preparing");
|
|
5393
6004
|
const moved = await moveCardAndAddLabel(this.client, card, IN_PROGRESS_COLUMN, "agent");
|
|
5394
6005
|
if (!moved) {
|
|
@@ -5435,6 +6046,9 @@ class Worker {
|
|
|
5435
6046
|
await this.spawnClaude(prompt, card, subtasks, {
|
|
5436
6047
|
model: this.selectImplementModel(card)
|
|
5437
6048
|
});
|
|
6049
|
+
if (this.aborted)
|
|
6050
|
+
return;
|
|
6051
|
+
await this.drainSteeringMessages(card, subtasks);
|
|
5438
6052
|
if (this.aborted)
|
|
5439
6053
|
return;
|
|
5440
6054
|
if (this.timeoutTimer) {
|
|
@@ -5461,7 +6075,17 @@ class Worker {
|
|
|
5461
6075
|
log.error(this.tag, `Error on #${card.short_id}: ${msg}`);
|
|
5462
6076
|
const rawStderr = err?.stderr;
|
|
5463
6077
|
const errClass = classifyRunError(typeof rawStderr === "string" && rawStderr ? rawStderr : msg);
|
|
6078
|
+
const sdkKind = err?.errorKind;
|
|
6079
|
+
if (errClass.kind === null && sdkKind != null)
|
|
6080
|
+
errClass.kind = sdkKind;
|
|
5464
6081
|
const apiError = errClass.kind !== null;
|
|
6082
|
+
const baseError = err instanceof WorktreeBaseError;
|
|
6083
|
+
const noBudgetBurn = apiError || baseError;
|
|
6084
|
+
this.cliRunner?.recordError({
|
|
6085
|
+
message: msg.slice(0, 500),
|
|
6086
|
+
errorKind: errClass.kind,
|
|
6087
|
+
retryable: noBudgetBurn
|
|
6088
|
+
});
|
|
5465
6089
|
if (apiError) {
|
|
5466
6090
|
try {
|
|
5467
6091
|
this.onApiError?.(errClass);
|
|
@@ -5476,7 +6100,7 @@ class Worker {
|
|
|
5476
6100
|
this.worktreePath = null;
|
|
5477
6101
|
}
|
|
5478
6102
|
const failureReason = apiError ? errClass.kind : "other";
|
|
5479
|
-
const failureSummary = apiError ? describeApiError(errClass.kind) : `Run failed: ${msg.slice(0, 300)}`;
|
|
6103
|
+
const failureSummary = apiError ? describeApiError(errClass.kind) : baseError ? "Could not fetch current base branch — requeued without counting an attempt" : `Run failed: ${msg.slice(0, 300)}`;
|
|
5480
6104
|
try {
|
|
5481
6105
|
await runTransition(this.client, card, {
|
|
5482
6106
|
move: { columnName: this.config.pickupColumns[0] ?? "To Do" },
|
|
@@ -5497,7 +6121,7 @@ class Worker {
|
|
|
5497
6121
|
...this.runLedger()
|
|
5498
6122
|
});
|
|
5499
6123
|
} catch {}
|
|
5500
|
-
if (
|
|
6124
|
+
if (noBudgetBurn) {
|
|
5501
6125
|
await this.stateStore.decrementAttempt(card.id);
|
|
5502
6126
|
} else {
|
|
5503
6127
|
await this.recordOutcome(card.id, "failure");
|
|
@@ -5555,6 +6179,26 @@ class Worker {
|
|
|
5555
6179
|
} catch {}
|
|
5556
6180
|
await this.recordOutcome(card.id, "failure");
|
|
5557
6181
|
}
|
|
6182
|
+
if (this.cliRunner) {
|
|
6183
|
+
if (succeeded) {
|
|
6184
|
+
this.cliRunner.recordFinished({ status: "completed" });
|
|
6185
|
+
} else if (this.timedOut) {
|
|
6186
|
+
this.cliRunner.recordFinished({
|
|
6187
|
+
status: "failed",
|
|
6188
|
+
failureReason: "timeout"
|
|
6189
|
+
});
|
|
6190
|
+
} else if (this.aborted) {
|
|
6191
|
+
this.cliRunner.recordFinished({
|
|
6192
|
+
status: "stopped",
|
|
6193
|
+
stopReason: "user_requested"
|
|
6194
|
+
});
|
|
6195
|
+
} else {
|
|
6196
|
+
this.cliRunner.recordFinished({ status: "failed" });
|
|
6197
|
+
}
|
|
6198
|
+
try {
|
|
6199
|
+
await this.cliRunner.flushFinal();
|
|
6200
|
+
} catch {}
|
|
6201
|
+
}
|
|
5558
6202
|
this.cleanup();
|
|
5559
6203
|
this.state = "idle";
|
|
5560
6204
|
this.onDone(this);
|
|
@@ -5644,7 +6288,9 @@ class Worker {
|
|
|
5644
6288
|
this.aborted = true;
|
|
5645
6289
|
this.state = "cancelling";
|
|
5646
6290
|
log.info(this.tag, `Cancelling work on ${this.cardId}`);
|
|
5647
|
-
if (this.
|
|
6291
|
+
if (this.sdkRunner) {
|
|
6292
|
+
await this.sdkRunner.stop(this.timedOut ? "timeout" : "user_requested");
|
|
6293
|
+
} else if (this.process && !this.process.killed) {
|
|
5648
6294
|
await terminateGroup(this.process, {
|
|
5649
6295
|
sigintTimeoutMs: CANCEL_SIGINT_TIMEOUT2,
|
|
5650
6296
|
sigtermTimeoutMs: CANCEL_SIGTERM_TIMEOUT2
|
|
@@ -5679,7 +6325,9 @@ class Worker {
|
|
|
5679
6325
|
const planTimeout = setTimeout(() => {
|
|
5680
6326
|
planTimedOut = true;
|
|
5681
6327
|
log.warn(this.tag, "Planning pass exceeded timeout — abandoning, implementing directly");
|
|
5682
|
-
if (this.
|
|
6328
|
+
if (this.sdkRunner) {
|
|
6329
|
+
this.sdkRunner.stop("timeout").catch(() => {});
|
|
6330
|
+
} else if (this.process && !this.process.killed) {
|
|
5683
6331
|
terminateGroup(this.process, {
|
|
5684
6332
|
sigintTimeoutMs: 1e4,
|
|
5685
6333
|
sigtermTimeoutMs: 5000
|
|
@@ -5773,7 +6421,40 @@ class Worker {
|
|
|
5773
6421
|
}
|
|
5774
6422
|
return false;
|
|
5775
6423
|
}
|
|
5776
|
-
async
|
|
6424
|
+
async drainSteeringMessages(card, subtasks) {
|
|
6425
|
+
if (!this.cliSessionId || !this.sessionId || !this.cardId)
|
|
6426
|
+
return;
|
|
6427
|
+
for (let i = 0;i < MAX_STEERING_ITERATIONS && !this.aborted; i++) {
|
|
6428
|
+
let messages;
|
|
6429
|
+
try {
|
|
6430
|
+
const res = await this.client.getPendingUserMessages(this.cardId, this.sessionId, this.lastDrainedSeq);
|
|
6431
|
+
messages = res.messages ?? [];
|
|
6432
|
+
} catch (err) {
|
|
6433
|
+
log.warn(this.tag, `Failed to fetch steering messages (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
6434
|
+
return;
|
|
6435
|
+
}
|
|
6436
|
+
if (messages.length === 0)
|
|
6437
|
+
return;
|
|
6438
|
+
this.lastDrainedSeq = Math.max(this.lastDrainedSeq, ...messages.map((m) => m.seq));
|
|
6439
|
+
log.info(this.tag, `Steering #${card.short_id}: resuming with ${messages.length} queued message(s)`);
|
|
6440
|
+
this.state = "running";
|
|
6441
|
+
await this.recordPhase("running");
|
|
6442
|
+
try {
|
|
6443
|
+
await this.spawnClaude(buildSteeringPrompt(messages.map((m) => m.text)), card, subtasks, {
|
|
6444
|
+
model: this.selectImplementModel(card),
|
|
6445
|
+
maxTurns: STEERING_MAX_TURNS,
|
|
6446
|
+
resumeSessionId: this.cliSessionId
|
|
6447
|
+
});
|
|
6448
|
+
} catch (err) {
|
|
6449
|
+
log.warn(this.tag, `Steering resume failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
6450
|
+
return;
|
|
6451
|
+
}
|
|
6452
|
+
}
|
|
6453
|
+
}
|
|
6454
|
+
spawnClaude(prompt, card, subtasks, opts = {}) {
|
|
6455
|
+
return this.config.runner === "sdk" ? this.spawnClaudeSdk(prompt, card, subtasks, opts) : this.spawnClaudeCli(prompt, card, subtasks, opts);
|
|
6456
|
+
}
|
|
6457
|
+
async spawnClaudeCli(prompt, card, subtasks, opts = {}) {
|
|
5777
6458
|
const model = opts.model ?? this.config.claude.model;
|
|
5778
6459
|
const maxTurns = opts.maxTurns ?? this.config.claude.maxTurns;
|
|
5779
6460
|
const allowedTools = opts.allowedTools ?? IMPLEMENT_ALLOWED_TOOLS;
|
|
@@ -5790,6 +6471,7 @@ class Worker {
|
|
|
5790
6471
|
String(maxTurns),
|
|
5791
6472
|
"--allowedTools",
|
|
5792
6473
|
allowedTools,
|
|
6474
|
+
...opts.resumeSessionId ? ["--resume", opts.resumeSessionId] : [],
|
|
5793
6475
|
...this.config.claude.additionalArgs,
|
|
5794
6476
|
"--",
|
|
5795
6477
|
prompt
|
|
@@ -5809,10 +6491,11 @@ class Worker {
|
|
|
5809
6491
|
});
|
|
5810
6492
|
const parser = new StreamParser;
|
|
5811
6493
|
this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks, initialPhase);
|
|
5812
|
-
if (this.sessionId) {
|
|
5813
|
-
this.progressTracker.setSessionId(this.sessionId);
|
|
5814
|
-
}
|
|
5815
6494
|
this.progressTracker.attach(parser);
|
|
6495
|
+
this.cliRunner?.attach(parser);
|
|
6496
|
+
if (this.cliRunner) {
|
|
6497
|
+
this.progressTracker.setRunEventSink(this.cliRunner);
|
|
6498
|
+
}
|
|
5816
6499
|
if (this.process.stdout) {
|
|
5817
6500
|
parser.attach(this.process.stdout);
|
|
5818
6501
|
if (runLog) {
|
|
@@ -5836,14 +6519,16 @@ class Worker {
|
|
|
5836
6519
|
reject(new Error(`Failed to spawn claude: ${err.message}`));
|
|
5837
6520
|
});
|
|
5838
6521
|
this.process.on("close", (code) => {
|
|
6522
|
+
const leaderPid = this.process?.pid;
|
|
5839
6523
|
this.process = null;
|
|
6524
|
+
if (parser.sessionId)
|
|
6525
|
+
this.cliSessionId = parser.sessionId;
|
|
5840
6526
|
this.lastSessionStats = this.progressTracker?.stats;
|
|
5841
6527
|
const spawnCost = this.lastSessionStats?.cost;
|
|
5842
6528
|
if (spawnCost) {
|
|
5843
6529
|
this.runCostCents += Math.round(spawnCost.totalCostUsd * 100);
|
|
5844
6530
|
this.runTurns += spawnCost.numTurns;
|
|
5845
6531
|
}
|
|
5846
|
-
this.progressTracker?.flushFinal();
|
|
5847
6532
|
this.progressTracker?.stop();
|
|
5848
6533
|
this.progressTracker = null;
|
|
5849
6534
|
if (runLog) {
|
|
@@ -5853,6 +6538,7 @@ class Worker {
|
|
|
5853
6538
|
`);
|
|
5854
6539
|
runLog.stream.end();
|
|
5855
6540
|
}
|
|
6541
|
+
reapGroup(leaderPid);
|
|
5856
6542
|
if (this.aborted) {
|
|
5857
6543
|
resolve3();
|
|
5858
6544
|
} else if (code === 0) {
|
|
@@ -5865,11 +6551,101 @@ class Worker {
|
|
|
5865
6551
|
});
|
|
5866
6552
|
});
|
|
5867
6553
|
}
|
|
6554
|
+
async spawnClaudeSdk(prompt, card, subtasks, opts = {}) {
|
|
6555
|
+
const model = opts.model ?? this.config.claude.model;
|
|
6556
|
+
const maxTurns = opts.maxTurns ?? this.config.claude.maxTurns;
|
|
6557
|
+
const allowedTools = (opts.allowedTools ?? IMPLEMENT_ALLOWED_TOOLS).split(",").map((t) => t.trim()).filter(Boolean);
|
|
6558
|
+
const initialPhase = opts.initialPhase ?? "exploring";
|
|
6559
|
+
const sdkCfg = this.config.sdk;
|
|
6560
|
+
log.info(this.tag, `Spawning Agent SDK runner (model=${model}, maxTurns=${maxTurns}${opts.resumeSessionId ? ", resume" : ""})`);
|
|
6561
|
+
const runLog = openRunLog(this.tag, this.runId, card.short_id);
|
|
6562
|
+
if (runLog) {
|
|
6563
|
+
log.info(this.tag, `Run log: ${runLog.path}`);
|
|
6564
|
+
runLog.stream.write(`# run=${this.runId} card=#${card.short_id} runner=sdk started=${new Date().toISOString()}
|
|
6565
|
+
` + `# model=${model} maxTurns=${maxTurns} <prompt:${prompt.length} chars>
|
|
6566
|
+
|
|
6567
|
+
`);
|
|
6568
|
+
}
|
|
6569
|
+
this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks, initialPhase);
|
|
6570
|
+
if (this.cliRunner) {
|
|
6571
|
+
this.progressTracker.setRunEventSink(this.cliRunner);
|
|
6572
|
+
}
|
|
6573
|
+
const runner = new SdkAgentRunner({
|
|
6574
|
+
model,
|
|
6575
|
+
maxTurns,
|
|
6576
|
+
allowedTools,
|
|
6577
|
+
maxBudgetUsd: sdkCfg?.maxBudgetUsd,
|
|
6578
|
+
settingSources: sdkCfg?.settingSources,
|
|
6579
|
+
mcpServers: sdkCfg?.mcpServers,
|
|
6580
|
+
strictMcpConfig: sdkCfg?.strictMcpConfig,
|
|
6581
|
+
onSpawn: (child) => {
|
|
6582
|
+
this.process = child;
|
|
6583
|
+
}
|
|
6584
|
+
});
|
|
6585
|
+
this.sdkRunner = runner;
|
|
6586
|
+
const baseInput = {
|
|
6587
|
+
sessionId: this.sessionId ?? "",
|
|
6588
|
+
cardId: card.id,
|
|
6589
|
+
workspaceId: this.workspaceId,
|
|
6590
|
+
prompt,
|
|
6591
|
+
cwd: this.worktreePath,
|
|
6592
|
+
model
|
|
6593
|
+
};
|
|
6594
|
+
const stream = opts.resumeSessionId ? runner.resume({ ...baseInput, resumeSessionId: opts.resumeSessionId }) : runner.start(baseInput);
|
|
6595
|
+
let failure = null;
|
|
6596
|
+
let failureKind = null;
|
|
6597
|
+
try {
|
|
6598
|
+
for await (const ev of stream) {
|
|
6599
|
+
this.progressTracker?.ingest(ev);
|
|
6600
|
+
if (ev.source === "agent")
|
|
6601
|
+
this.cliRunner?.record(ev);
|
|
6602
|
+
if (ev.kind === "error") {
|
|
6603
|
+
failure = ev.payload.message;
|
|
6604
|
+
if (ev.payload.errorKind != null)
|
|
6605
|
+
failureKind = ev.payload.errorKind;
|
|
6606
|
+
}
|
|
6607
|
+
runLog?.stream.write(`[${ev.kind}] ${sdkDraftLogLine(ev)}
|
|
6608
|
+
`);
|
|
6609
|
+
}
|
|
6610
|
+
} finally {
|
|
6611
|
+
this.cliSessionId = runner.sessionId ?? this.cliSessionId;
|
|
6612
|
+
this.lastSessionStats = this.progressTracker?.stats;
|
|
6613
|
+
const spawnCost = this.lastSessionStats?.cost;
|
|
6614
|
+
if (spawnCost) {
|
|
6615
|
+
this.runCostCents += Math.round(spawnCost.totalCostUsd * 100);
|
|
6616
|
+
this.runTurns += spawnCost.numTurns;
|
|
6617
|
+
}
|
|
6618
|
+
if (runLog) {
|
|
6619
|
+
const stats = this.lastSessionStats;
|
|
6620
|
+
runLog.stream.write(`
|
|
6621
|
+
# runner=sdk aborted=${this.aborted} ` + `toolCalls=${stats?.toolCalls ?? 0} filesEdited=${stats?.filesEdited ?? 0} ` + `cost=$${stats?.cost?.totalCostUsd.toFixed(4) ?? "0"} ` + `ended=${new Date().toISOString()}
|
|
6622
|
+
`);
|
|
6623
|
+
runLog.stream.end();
|
|
6624
|
+
}
|
|
6625
|
+
this.progressTracker?.stop();
|
|
6626
|
+
this.progressTracker = null;
|
|
6627
|
+
this.process = null;
|
|
6628
|
+
this.sdkRunner = null;
|
|
6629
|
+
}
|
|
6630
|
+
if (this.aborted)
|
|
6631
|
+
return;
|
|
6632
|
+
if (failure) {
|
|
6633
|
+
const err = new Error(failure);
|
|
6634
|
+
err.stderr = runner.capturedStderrText;
|
|
6635
|
+
err.errorKind = failureKind;
|
|
6636
|
+
throw err;
|
|
6637
|
+
}
|
|
6638
|
+
}
|
|
5868
6639
|
cleanup() {
|
|
5869
6640
|
if (this.progressTracker) {
|
|
5870
6641
|
this.progressTracker.stop();
|
|
5871
6642
|
this.progressTracker = null;
|
|
5872
6643
|
}
|
|
6644
|
+
if (this.cliRunner) {
|
|
6645
|
+
this.cliRunner.stop();
|
|
6646
|
+
this.cliRunner = null;
|
|
6647
|
+
}
|
|
6648
|
+
this.sdkRunner = null;
|
|
5873
6649
|
this.stopHeartbeat();
|
|
5874
6650
|
this.lastSessionStats = undefined;
|
|
5875
6651
|
if (this.timeoutTimer) {
|
|
@@ -5894,9 +6670,10 @@ class Worker {
|
|
|
5894
6670
|
this.runTurns = 0;
|
|
5895
6671
|
}
|
|
5896
6672
|
}
|
|
5897
|
-
var
|
|
6673
|
+
var TAG26 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4, STEERING_MAX_TURNS = 15, MAX_STEERING_ITERATIONS = 10, PLAN_ALLOWED_TOOLS = "Read,Grep,Glob,mcp__harmony__*", IMPLEMENT_ALLOWED_TOOLS = "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*", PLAN_PHASE_TIMEOUT;
|
|
5898
6674
|
var init_worker = __esm(() => {
|
|
5899
6675
|
init_board_helpers();
|
|
6676
|
+
init_cli_agent_runner();
|
|
5900
6677
|
init_completion();
|
|
5901
6678
|
init_error_classifier();
|
|
5902
6679
|
init_log();
|
|
@@ -5906,6 +6683,7 @@ var init_worker = __esm(() => {
|
|
|
5906
6683
|
init_progress_tracker();
|
|
5907
6684
|
init_prompt();
|
|
5908
6685
|
init_run_log();
|
|
6686
|
+
init_sdk_agent_runner();
|
|
5909
6687
|
init_state_store();
|
|
5910
6688
|
init_stream_parser();
|
|
5911
6689
|
init_transitions();
|
|
@@ -5964,39 +6742,39 @@ class Pool {
|
|
|
5964
6742
|
}
|
|
5965
6743
|
async enqueue(card, column, labels, subtasks, mode = "implement") {
|
|
5966
6744
|
if (this.implQueue.has(card.id) || this.reviewQueue.has(card.id) || this.isCardActive(card.id)) {
|
|
5967
|
-
log.debug(
|
|
6745
|
+
log.debug(TAG27, `Card ${card.id} already queued or active, skipping`);
|
|
5968
6746
|
return;
|
|
5969
6747
|
}
|
|
5970
6748
|
if (mode === "implement") {
|
|
5971
6749
|
if (this.authPaused) {
|
|
5972
|
-
log.debug(
|
|
6750
|
+
log.debug(TAG27, `#${card.short_id} held — agent paused (auth error)`);
|
|
5973
6751
|
await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
|
|
5974
6752
|
return;
|
|
5975
6753
|
}
|
|
5976
6754
|
const cooldownMs = this.apiCooldownRemainingMs();
|
|
5977
6755
|
if (cooldownMs > 0) {
|
|
5978
|
-
log.debug(
|
|
6756
|
+
log.debug(TAG27, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
|
|
5979
6757
|
await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
|
|
5980
6758
|
return;
|
|
5981
6759
|
}
|
|
5982
6760
|
const decision = this.budget.check(card.id);
|
|
5983
6761
|
if (!decision.allow) {
|
|
5984
6762
|
if (decision.reason === "daily_budget") {
|
|
5985
|
-
log.warn(
|
|
6763
|
+
log.warn(TAG27, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
|
|
5986
6764
|
await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
|
|
5987
6765
|
} else {
|
|
5988
|
-
log.debug(
|
|
6766
|
+
log.debug(TAG27, `#${card.short_id} gave up: ${decision.detail}`);
|
|
5989
6767
|
}
|
|
5990
6768
|
return;
|
|
5991
6769
|
}
|
|
5992
6770
|
const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
|
|
5993
6771
|
if (blockers === null) {
|
|
5994
|
-
log.warn(
|
|
6772
|
+
log.warn(TAG27, `#${card.short_id} blocker check failed — deferring to next tick`);
|
|
5995
6773
|
return;
|
|
5996
6774
|
}
|
|
5997
6775
|
if (blockers.length > 0) {
|
|
5998
6776
|
const list = blockers.map((b) => `#${b.shortId}`).join(", ");
|
|
5999
|
-
log.info(
|
|
6777
|
+
log.info(TAG27, `#${card.short_id} blocked by ${list} — waiting`);
|
|
6000
6778
|
await this.emitWaiting(card.id, `Blocked by ${list} — waiting for chain`);
|
|
6001
6779
|
return;
|
|
6002
6780
|
}
|
|
@@ -6025,7 +6803,7 @@ class Pool {
|
|
|
6025
6803
|
});
|
|
6026
6804
|
this.lastWaitingEmit.set(cardId, currentTask);
|
|
6027
6805
|
} catch (err) {
|
|
6028
|
-
log.debug(
|
|
6806
|
+
log.debug(TAG27, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
6029
6807
|
}
|
|
6030
6808
|
}
|
|
6031
6809
|
noteApiError(err) {
|
|
@@ -6033,7 +6811,7 @@ class Pool {
|
|
|
6033
6811
|
return;
|
|
6034
6812
|
if (err.kind === "auth") {
|
|
6035
6813
|
if (!this.authPaused) {
|
|
6036
|
-
log.error(
|
|
6814
|
+
log.error(TAG27, "Auth error from Claude CLI — pausing implement pickups until the daemon is restarted with valid credentials");
|
|
6037
6815
|
}
|
|
6038
6816
|
this.authPaused = true;
|
|
6039
6817
|
return;
|
|
@@ -6042,7 +6820,7 @@ class Pool {
|
|
|
6042
6820
|
const until = Date.now() + cooldownMs;
|
|
6043
6821
|
if (until > this.apiCooldownUntil) {
|
|
6044
6822
|
this.apiCooldownUntil = until;
|
|
6045
|
-
log.warn(
|
|
6823
|
+
log.warn(TAG27, `${describeApiError(err.kind)} — pausing implement pickups for ${Math.round(cooldownMs / 1000)}s`);
|
|
6046
6824
|
}
|
|
6047
6825
|
}
|
|
6048
6826
|
apiCooldownRemainingMs() {
|
|
@@ -6055,13 +6833,13 @@ class Pool {
|
|
|
6055
6833
|
const removed = queue.remove(cardId);
|
|
6056
6834
|
if (removed) {
|
|
6057
6835
|
this.cardDataCache.delete(cardId);
|
|
6058
|
-
log.info(
|
|
6836
|
+
log.info(TAG27, `Removed #${removed.shortId} from ${removed.mode} queue`);
|
|
6059
6837
|
return;
|
|
6060
6838
|
}
|
|
6061
6839
|
}
|
|
6062
6840
|
const worker = this.implWorkers.find((w) => w.cardId === cardId) ?? this.reviewWorkers.find((w) => w.cardId === cardId);
|
|
6063
6841
|
if (worker) {
|
|
6064
|
-
log.info(
|
|
6842
|
+
log.info(TAG27, `Cancelling worker ${worker.id} for card ${cardId}`);
|
|
6065
6843
|
await worker.cancel();
|
|
6066
6844
|
}
|
|
6067
6845
|
}
|
|
@@ -6094,10 +6872,10 @@ class Pool {
|
|
|
6094
6872
|
async handleAgentCommand(cardId, command) {
|
|
6095
6873
|
const worker = this.implWorkers.find((w) => w.cardId === cardId && w.isActive) ?? this.reviewWorkers.find((w) => w.cardId === cardId && w.isActive);
|
|
6096
6874
|
if (!worker) {
|
|
6097
|
-
log.debug(
|
|
6875
|
+
log.debug(TAG27, `No active worker for card ${cardId}, ignoring ${command}`);
|
|
6098
6876
|
return;
|
|
6099
6877
|
}
|
|
6100
|
-
log.info(
|
|
6878
|
+
log.info(TAG27, `Agent command: ${command} → worker ${worker.id} (card ${cardId})`);
|
|
6101
6879
|
switch (command) {
|
|
6102
6880
|
case "pause":
|
|
6103
6881
|
await worker.pause();
|
|
@@ -6145,7 +6923,7 @@ class Pool {
|
|
|
6145
6923
|
};
|
|
6146
6924
|
}
|
|
6147
6925
|
async shutdown() {
|
|
6148
|
-
log.info(
|
|
6926
|
+
log.info(TAG27, "Shutting down pool...");
|
|
6149
6927
|
this.shuttingDown = true;
|
|
6150
6928
|
const active = [
|
|
6151
6929
|
...this.implWorkers.filter((w) => w.isActive),
|
|
@@ -6153,7 +6931,7 @@ class Pool {
|
|
|
6153
6931
|
];
|
|
6154
6932
|
await Promise.all(active.map((w) => w.cancel()));
|
|
6155
6933
|
this.sleepGuard.stop();
|
|
6156
|
-
log.info(
|
|
6934
|
+
log.info(TAG27, "Pool shutdown complete");
|
|
6157
6935
|
}
|
|
6158
6936
|
cardDataCache = new Map;
|
|
6159
6937
|
tryDispatchFor(workers, queue, label) {
|
|
@@ -6161,7 +6939,7 @@ class Pool {
|
|
|
6161
6939
|
return false;
|
|
6162
6940
|
const idle = workers.find((w) => w.isIdle);
|
|
6163
6941
|
if (!idle) {
|
|
6164
|
-
log.debug(
|
|
6942
|
+
log.debug(TAG27, `No idle ${label} workers (queue: ${queue.length})`);
|
|
6165
6943
|
return false;
|
|
6166
6944
|
}
|
|
6167
6945
|
const next = queue.dequeue();
|
|
@@ -6169,18 +6947,18 @@ class Pool {
|
|
|
6169
6947
|
return false;
|
|
6170
6948
|
const data = this.cardDataCache.get(next.cardId);
|
|
6171
6949
|
if (!data) {
|
|
6172
|
-
log.warn(
|
|
6950
|
+
log.warn(TAG27, `No cached data for card ${next.cardId}, skipping`);
|
|
6173
6951
|
return false;
|
|
6174
6952
|
}
|
|
6175
6953
|
this.cardDataCache.delete(next.cardId);
|
|
6176
6954
|
this.lastWaitingEmit.delete(next.cardId);
|
|
6177
|
-
log.info(
|
|
6955
|
+
log.info(TAG27, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
|
|
6178
6956
|
this.sleepGuard.acquire();
|
|
6179
6957
|
idle.run(data.card, data.column, data.labels, data.subtasks);
|
|
6180
6958
|
return true;
|
|
6181
6959
|
}
|
|
6182
6960
|
}
|
|
6183
|
-
var
|
|
6961
|
+
var TAG27 = "pool";
|
|
6184
6962
|
var init_pool = __esm(() => {
|
|
6185
6963
|
init_error_classifier();
|
|
6186
6964
|
init_log();
|
|
@@ -6222,7 +7000,7 @@ function load(path) {
|
|
|
6222
7000
|
return parsed;
|
|
6223
7001
|
return {};
|
|
6224
7002
|
} catch (err) {
|
|
6225
|
-
log.warn(
|
|
7003
|
+
log.warn(TAG28, `failed to read ${path}: ${err instanceof Error ? err.message : err}`);
|
|
6226
7004
|
return {};
|
|
6227
7005
|
}
|
|
6228
7006
|
}
|
|
@@ -6240,7 +7018,7 @@ function recordDaemonPort(projectId, entry, path = defaultRegistryPath()) {
|
|
|
6240
7018
|
registry[projectId] = { ...entry, updatedAt: Date.now() };
|
|
6241
7019
|
save(path, registry);
|
|
6242
7020
|
} catch (err) {
|
|
6243
|
-
log.warn(
|
|
7021
|
+
log.warn(TAG28, `failed to record port for ${projectId}: ${err instanceof Error ? err.message : err}`);
|
|
6244
7022
|
}
|
|
6245
7023
|
}
|
|
6246
7024
|
function lookupDaemonPort(projectId, path = defaultRegistryPath()) {
|
|
@@ -6256,10 +7034,10 @@ function clearDaemonPort(projectId, pid, path = defaultRegistryPath()) {
|
|
|
6256
7034
|
delete registry[projectId];
|
|
6257
7035
|
save(path, registry);
|
|
6258
7036
|
} catch (err) {
|
|
6259
|
-
log.warn(
|
|
7037
|
+
log.warn(TAG28, `failed to clear port for ${projectId}: ${err instanceof Error ? err.message : err}`);
|
|
6260
7038
|
}
|
|
6261
7039
|
}
|
|
6262
|
-
var
|
|
7040
|
+
var TAG28 = "port-registry";
|
|
6263
7041
|
var init_port_registry = __esm(() => {
|
|
6264
7042
|
init_log();
|
|
6265
7043
|
});
|
|
@@ -6280,7 +7058,7 @@ async function fetchCardSafely(client, cardId) {
|
|
|
6280
7058
|
const { card } = await client.getCard(cardId);
|
|
6281
7059
|
return card;
|
|
6282
7060
|
} catch (err) {
|
|
6283
|
-
log.warn(
|
|
7061
|
+
log.warn(TAG29, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
6284
7062
|
return null;
|
|
6285
7063
|
}
|
|
6286
7064
|
}
|
|
@@ -6290,7 +7068,7 @@ async function recoverOrphans(store, client, config) {
|
|
|
6290
7068
|
return [];
|
|
6291
7069
|
}
|
|
6292
7070
|
const outcomes = [];
|
|
6293
|
-
log.info(
|
|
7071
|
+
log.info(TAG29, `recovering ${active.length} orphan run(s) from prior daemon`);
|
|
6294
7072
|
for (const run of active) {
|
|
6295
7073
|
const outcome = {
|
|
6296
7074
|
runId: run.runId,
|
|
@@ -6302,11 +7080,11 @@ async function recoverOrphans(store, client, config) {
|
|
|
6302
7080
|
};
|
|
6303
7081
|
outcomes.push(outcome);
|
|
6304
7082
|
if (isProcessAlive(run.daemonPid, process.pid)) {
|
|
6305
|
-
log.warn(
|
|
7083
|
+
log.warn(TAG29, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
|
|
6306
7084
|
outcome.actions.push("skipped: daemon pid still alive");
|
|
6307
7085
|
continue;
|
|
6308
7086
|
}
|
|
6309
|
-
log.info(
|
|
7087
|
+
log.info(TAG29, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
|
|
6310
7088
|
await recoverRun(run, store, client, config, outcome);
|
|
6311
7089
|
}
|
|
6312
7090
|
return outcomes;
|
|
@@ -6324,7 +7102,7 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
6324
7102
|
} catch (err) {
|
|
6325
7103
|
const msg = err instanceof Error ? err.message : String(err);
|
|
6326
7104
|
outcome.errors.push(`endAgentSession: ${msg}`);
|
|
6327
|
-
log.warn(
|
|
7105
|
+
log.warn(TAG29, `endAgentSession failed for ${run.cardId}: ${msg}`);
|
|
6328
7106
|
}
|
|
6329
7107
|
const card = await fetchCardSafely(client, run.cardId);
|
|
6330
7108
|
if (card) {
|
|
@@ -6367,9 +7145,9 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
6367
7145
|
const msg = err instanceof Error ? err.message : String(err);
|
|
6368
7146
|
outcome.errors.push(`endRun: ${msg}`);
|
|
6369
7147
|
}
|
|
6370
|
-
log.info(
|
|
7148
|
+
log.info(TAG29, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
|
|
6371
7149
|
}
|
|
6372
|
-
var
|
|
7150
|
+
var TAG29 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
|
|
6373
7151
|
var init_recovery = __esm(() => {
|
|
6374
7152
|
init_board_helpers();
|
|
6375
7153
|
init_log();
|
|
@@ -6417,7 +7195,7 @@ class Reconciler {
|
|
|
6417
7195
|
clearInterval(this.timer);
|
|
6418
7196
|
this.timer = null;
|
|
6419
7197
|
}
|
|
6420
|
-
log.info(
|
|
7198
|
+
log.info(TAG30, "Heartbeat stopped");
|
|
6421
7199
|
}
|
|
6422
7200
|
async recoverStaleRuns() {
|
|
6423
7201
|
if (!this.stateStore || !this.agentConfig)
|
|
@@ -6434,7 +7212,7 @@ class Reconciler {
|
|
|
6434
7212
|
if (!daemonDead && !(heartbeatStale && ourZombie))
|
|
6435
7213
|
continue;
|
|
6436
7214
|
const reason = daemonDead ? `foreign daemon ${run.daemonPid} is dead` : `our worker lost card ${run.cardId} with ${Math.round((now - run.lastHeartbeatAt) / 1000)}s stale heartbeat`;
|
|
6437
|
-
log.warn(
|
|
7215
|
+
log.warn(TAG30, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
|
|
6438
7216
|
await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
|
|
6439
7217
|
runId: run.runId,
|
|
6440
7218
|
cardId: run.cardId,
|
|
@@ -6461,11 +7239,11 @@ class Reconciler {
|
|
|
6461
7239
|
const stalledAt = Date.parse(card.updated_at ?? "");
|
|
6462
7240
|
if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
|
|
6463
7241
|
continue;
|
|
6464
|
-
log.warn(
|
|
7242
|
+
log.warn(TAG30, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
|
|
6465
7243
|
try {
|
|
6466
7244
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
6467
7245
|
} catch (err) {
|
|
6468
|
-
log.error(
|
|
7246
|
+
log.error(TAG30, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
6469
7247
|
}
|
|
6470
7248
|
}
|
|
6471
7249
|
}
|
|
@@ -6488,11 +7266,11 @@ class Reconciler {
|
|
|
6488
7266
|
const parkedAt = Date.parse(card.updated_at ?? "");
|
|
6489
7267
|
if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
|
|
6490
7268
|
continue;
|
|
6491
|
-
log.warn(
|
|
7269
|
+
log.warn(TAG30, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
|
|
6492
7270
|
try {
|
|
6493
7271
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
6494
7272
|
} catch (err) {
|
|
6495
|
-
log.error(
|
|
7273
|
+
log.error(TAG30, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
6496
7274
|
}
|
|
6497
7275
|
}
|
|
6498
7276
|
}
|
|
@@ -6521,18 +7299,18 @@ class Reconciler {
|
|
|
6521
7299
|
const subtasks = card.subtasks ?? [];
|
|
6522
7300
|
const mode = reviewColumnIds.has(card.column_id) ? "review" : "implement";
|
|
6523
7301
|
if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
|
|
6524
|
-
log.debug(
|
|
7302
|
+
log.debug(TAG30, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
|
|
6525
7303
|
continue;
|
|
6526
7304
|
}
|
|
6527
7305
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
6528
|
-
log.debug(
|
|
7306
|
+
log.debug(TAG30, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
|
|
6529
7307
|
continue;
|
|
6530
7308
|
}
|
|
6531
7309
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
6532
|
-
log.debug(
|
|
7310
|
+
log.debug(TAG30, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
|
|
6533
7311
|
continue;
|
|
6534
7312
|
}
|
|
6535
|
-
log.info(
|
|
7313
|
+
log.info(TAG30, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
|
|
6536
7314
|
await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
6537
7315
|
}
|
|
6538
7316
|
}
|
|
@@ -6542,18 +7320,18 @@ class Reconciler {
|
|
|
6542
7320
|
await this.recoverStrandedInProgress(cards, columns, knownCardIds);
|
|
6543
7321
|
for (const knownId of knownCardIds) {
|
|
6544
7322
|
if (!allAgentCardIds.has(knownId)) {
|
|
6545
|
-
log.info(
|
|
7323
|
+
log.info(TAG30, `Missed unassign: ${knownId} — removing`);
|
|
6546
7324
|
await this.pool.removeCard(knownId);
|
|
6547
7325
|
}
|
|
6548
7326
|
}
|
|
6549
7327
|
await this.releaseStalledApprovals(cards, columns, knownCardIds);
|
|
6550
|
-
log.debug(
|
|
7328
|
+
log.debug(TAG30, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
|
|
6551
7329
|
} catch (err) {
|
|
6552
|
-
log.error(
|
|
7330
|
+
log.error(TAG30, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
|
|
6553
7331
|
}
|
|
6554
7332
|
}
|
|
6555
7333
|
}
|
|
6556
|
-
var
|
|
7334
|
+
var TAG30 = "reconcile";
|
|
6557
7335
|
var init_reconcile = __esm(() => {
|
|
6558
7336
|
init_board_helpers();
|
|
6559
7337
|
init_log();
|
|
@@ -6591,7 +7369,7 @@ function prettyBanner(config, version) {
|
|
|
6591
7369
|
checks.push({ kind: "ok", message });
|
|
6592
7370
|
},
|
|
6593
7371
|
warn(message) {
|
|
6594
|
-
log.warn(
|
|
7372
|
+
log.warn(TAG31, message);
|
|
6595
7373
|
checks.push({ kind: "warn", message: message.split(`
|
|
6596
7374
|
`, 1)[0] });
|
|
6597
7375
|
},
|
|
@@ -6616,25 +7394,25 @@ function prettyBanner(config, version) {
|
|
|
6616
7394
|
};
|
|
6617
7395
|
}
|
|
6618
7396
|
function jsonBanner(config, version) {
|
|
6619
|
-
log.info(
|
|
6620
|
-
log.info(
|
|
7397
|
+
log.info(TAG31, `Harmony Agent Daemon v${version} starting...`);
|
|
7398
|
+
log.info(TAG31, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
|
|
6621
7399
|
if (config.agent.review.enabled) {
|
|
6622
|
-
log.info(
|
|
7400
|
+
log.info(TAG31, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
|
|
6623
7401
|
}
|
|
6624
7402
|
let failed = false;
|
|
6625
7403
|
return {
|
|
6626
7404
|
setProjectName(_name) {},
|
|
6627
7405
|
setGitProvider(provider) {
|
|
6628
|
-
log.info(
|
|
7406
|
+
log.info(TAG31, `Git provider: ${provider}`);
|
|
6629
7407
|
},
|
|
6630
7408
|
setHttpPort(port) {
|
|
6631
|
-
log.info(
|
|
7409
|
+
log.info(TAG31, `HTTP server on port ${port}`);
|
|
6632
7410
|
},
|
|
6633
7411
|
check(message) {
|
|
6634
|
-
log.info(
|
|
7412
|
+
log.info(TAG31, message);
|
|
6635
7413
|
},
|
|
6636
7414
|
warn(message) {
|
|
6637
|
-
log.warn(
|
|
7415
|
+
log.warn(TAG31, message);
|
|
6638
7416
|
},
|
|
6639
7417
|
fail() {
|
|
6640
7418
|
failed = true;
|
|
@@ -6642,7 +7420,7 @@ function jsonBanner(config, version) {
|
|
|
6642
7420
|
async ready(message) {
|
|
6643
7421
|
if (failed)
|
|
6644
7422
|
return;
|
|
6645
|
-
log.info(
|
|
7423
|
+
log.info(TAG31, message);
|
|
6646
7424
|
}
|
|
6647
7425
|
};
|
|
6648
7426
|
}
|
|
@@ -6688,6 +7466,7 @@ function configRows(config, projectName, gitProvider, httpPort) {
|
|
|
6688
7466
|
label: "Model",
|
|
6689
7467
|
value: `${modelDesc} · ${poolDesc} · ${flowDesc}`
|
|
6690
7468
|
});
|
|
7469
|
+
rows.push({ label: "Runner", value: runnerDesc(config.agent.runner) });
|
|
6691
7470
|
const tail = [];
|
|
6692
7471
|
if (gitProvider)
|
|
6693
7472
|
tail.push(gitProvider);
|
|
@@ -6705,6 +7484,9 @@ function titleRule(title) {
|
|
|
6705
7484
|
const suffix = "─".repeat(Math.max(3, RULE_WIDTH - prefix.length - title.length - surround.length));
|
|
6706
7485
|
return dim(`${prefix}${title}${surround}${suffix}`);
|
|
6707
7486
|
}
|
|
7487
|
+
function runnerDesc(runner) {
|
|
7488
|
+
return runner === "sdk" ? "sdk (Agent SDK)" : "cli (Claude CLI)";
|
|
7489
|
+
}
|
|
6708
7490
|
function shortenId(id) {
|
|
6709
7491
|
if (id.length <= 8)
|
|
6710
7492
|
return id;
|
|
@@ -6719,7 +7501,7 @@ function cyan(s) {
|
|
|
6719
7501
|
function yellow(s) {
|
|
6720
7502
|
return `${ANSI.yellow}${s}${ANSI.reset}`;
|
|
6721
7503
|
}
|
|
6722
|
-
var
|
|
7504
|
+
var TAG31 = "daemon", RULE_WIDTH = 70, ANSI;
|
|
6723
7505
|
var init_startup_banner = __esm(() => {
|
|
6724
7506
|
init_log();
|
|
6725
7507
|
ANSI = {
|
|
@@ -6866,18 +7648,18 @@ class Watcher {
|
|
|
6866
7648
|
}
|
|
6867
7649
|
async start() {
|
|
6868
7650
|
if (!isPretty()) {
|
|
6869
|
-
log.info(
|
|
7651
|
+
log.info(TAG32, "Connecting to Supabase realtime (broadcast)...");
|
|
6870
7652
|
}
|
|
6871
7653
|
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
6872
7654
|
const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
|
|
6873
7655
|
const channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
|
|
6874
|
-
log.debug(
|
|
7656
|
+
log.debug(TAG32, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
|
|
6875
7657
|
this.onCardBroadcast({
|
|
6876
7658
|
event: "card_update",
|
|
6877
7659
|
payload: msg.payload ?? {}
|
|
6878
7660
|
});
|
|
6879
7661
|
}).on("broadcast", { event: "card_created" }, (msg) => {
|
|
6880
|
-
log.debug(
|
|
7662
|
+
log.debug(TAG32, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
|
|
6881
7663
|
this.onCardBroadcast({
|
|
6882
7664
|
event: "card_created",
|
|
6883
7665
|
payload: msg.payload ?? {}
|
|
@@ -6887,29 +7669,29 @@ class Watcher {
|
|
|
6887
7669
|
const cardId = payload.card_id;
|
|
6888
7670
|
const command = payload.command;
|
|
6889
7671
|
if (cardId && command) {
|
|
6890
|
-
log.info(
|
|
7672
|
+
log.info(TAG32, `Broadcast: agent_command ${command} for ${cardId}`);
|
|
6891
7673
|
this.onAgentCommand?.({ cardId, command });
|
|
6892
7674
|
}
|
|
6893
7675
|
}).subscribe((status) => {
|
|
6894
7676
|
if (status === "SUBSCRIBED") {
|
|
6895
7677
|
this.connected = true;
|
|
6896
7678
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
6897
|
-
log.info(
|
|
7679
|
+
log.info(TAG32, "Broadcast subscription active");
|
|
6898
7680
|
}
|
|
6899
7681
|
this.maybeResolveReady();
|
|
6900
7682
|
} else if (status === "CHANNEL_ERROR") {
|
|
6901
7683
|
this.connected = false;
|
|
6902
|
-
log.error(
|
|
7684
|
+
log.error(TAG32, "Broadcast channel error — will rely on reconciliation");
|
|
6903
7685
|
} else if (status === "TIMED_OUT") {
|
|
6904
7686
|
this.connected = false;
|
|
6905
|
-
log.warn(
|
|
7687
|
+
log.warn(TAG32, "Broadcast subscription timed out — retrying...");
|
|
6906
7688
|
} else if (status === "CLOSED") {
|
|
6907
7689
|
this.connected = false;
|
|
6908
7690
|
}
|
|
6909
7691
|
});
|
|
6910
7692
|
this.channel = channel;
|
|
6911
7693
|
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
6912
|
-
log.debug(
|
|
7694
|
+
log.debug(TAG32, "Presence sync");
|
|
6913
7695
|
}).subscribe(async (status) => {
|
|
6914
7696
|
if (status === "SUBSCRIBED") {
|
|
6915
7697
|
await presenceChannel.track({
|
|
@@ -6922,7 +7704,7 @@ class Watcher {
|
|
|
6922
7704
|
agentName: this.identity.agentName
|
|
6923
7705
|
});
|
|
6924
7706
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
6925
|
-
log.info(
|
|
7707
|
+
log.info(TAG32, "Presence tracked on board-presence channel");
|
|
6926
7708
|
}
|
|
6927
7709
|
this.presenceTracked = true;
|
|
6928
7710
|
this.maybeResolveReady();
|
|
@@ -6944,10 +7726,10 @@ class Watcher {
|
|
|
6944
7726
|
this.supabase = null;
|
|
6945
7727
|
}
|
|
6946
7728
|
this.connected = false;
|
|
6947
|
-
log.info(
|
|
7729
|
+
log.info(TAG32, "Broadcast subscription stopped");
|
|
6948
7730
|
}
|
|
6949
7731
|
}
|
|
6950
|
-
var
|
|
7732
|
+
var TAG32 = "watcher";
|
|
6951
7733
|
var init_watcher = __esm(() => {
|
|
6952
7734
|
init_log();
|
|
6953
7735
|
});
|
|
@@ -6960,7 +7742,7 @@ __export(exports_worktree_gc, {
|
|
|
6960
7742
|
isTransientGitNetworkError: () => isTransientGitNetworkError,
|
|
6961
7743
|
WorktreeGc: () => WorktreeGc
|
|
6962
7744
|
});
|
|
6963
|
-
import { execFileSync as
|
|
7745
|
+
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
6964
7746
|
import { readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
6965
7747
|
import { resolve as resolve3 } from "node:path";
|
|
6966
7748
|
function isTransientGitNetworkError(message) {
|
|
@@ -7028,16 +7810,16 @@ function runWorktreeGc(basePath, store, opts = {}) {
|
|
|
7028
7810
|
}
|
|
7029
7811
|
}
|
|
7030
7812
|
try {
|
|
7031
|
-
|
|
7813
|
+
execFileSync11("git", ["worktree", "prune", "--expire=now"], {
|
|
7032
7814
|
cwd: repoRoot,
|
|
7033
7815
|
stdio: "pipe"
|
|
7034
7816
|
});
|
|
7035
7817
|
} catch {}
|
|
7036
7818
|
if (result.removed.length > 0) {
|
|
7037
|
-
log.info(
|
|
7819
|
+
log.info(TAG33, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
|
|
7038
7820
|
}
|
|
7039
7821
|
if (result.errors.length > 0) {
|
|
7040
|
-
log.warn(
|
|
7822
|
+
log.warn(TAG33, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
|
|
7041
7823
|
}
|
|
7042
7824
|
return result;
|
|
7043
7825
|
}
|
|
@@ -7059,7 +7841,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
7059
7841
|
return result;
|
|
7060
7842
|
}
|
|
7061
7843
|
try {
|
|
7062
|
-
|
|
7844
|
+
execFileSync11("git", ["fetch", "--prune", "origin"], {
|
|
7063
7845
|
cwd: repoRoot,
|
|
7064
7846
|
stdio: "pipe",
|
|
7065
7847
|
...GIT_NETWORK_EXEC
|
|
@@ -7067,7 +7849,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
7067
7849
|
} catch (err) {
|
|
7068
7850
|
const detail = gitErrorDetail(err);
|
|
7069
7851
|
if (isTransientGitNetworkError(detail)) {
|
|
7070
|
-
log.debug(
|
|
7852
|
+
log.debug(TAG33, `Remote branch GC skipped — remote unreachable: ${detail}`);
|
|
7071
7853
|
return result;
|
|
7072
7854
|
}
|
|
7073
7855
|
result.errors.push({ ref: "fetch", error: detail });
|
|
@@ -7075,7 +7857,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
7075
7857
|
const refPattern = `refs/remotes/origin/${opts.prefix}*`;
|
|
7076
7858
|
let listing = "";
|
|
7077
7859
|
try {
|
|
7078
|
-
listing =
|
|
7860
|
+
listing = execFileSync11("git", [
|
|
7079
7861
|
"for-each-ref",
|
|
7080
7862
|
"--format=%(refname:strip=3) %(committerdate:unix)",
|
|
7081
7863
|
refPattern
|
|
@@ -7106,11 +7888,11 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
7106
7888
|
continue;
|
|
7107
7889
|
}
|
|
7108
7890
|
if (clock() > sweepDeadline) {
|
|
7109
|
-
log.debug(
|
|
7891
|
+
log.debug(TAG33, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
|
|
7110
7892
|
break;
|
|
7111
7893
|
}
|
|
7112
7894
|
try {
|
|
7113
|
-
|
|
7895
|
+
execFileSync11("git", ["push", "origin", `:refs/heads/${ref}`], {
|
|
7114
7896
|
cwd: repoRoot,
|
|
7115
7897
|
stdio: "pipe",
|
|
7116
7898
|
...GIT_NETWORK_EXEC
|
|
@@ -7119,17 +7901,17 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
7119
7901
|
} catch (err) {
|
|
7120
7902
|
const detail = gitErrorDetail(err);
|
|
7121
7903
|
if (isTransientGitNetworkError(detail)) {
|
|
7122
|
-
log.debug(
|
|
7904
|
+
log.debug(TAG33, `Remote branch GC interrupted — remote unreachable: ${detail}`);
|
|
7123
7905
|
break;
|
|
7124
7906
|
}
|
|
7125
7907
|
result.errors.push({ ref, error: detail });
|
|
7126
7908
|
}
|
|
7127
7909
|
}
|
|
7128
7910
|
if (result.removed.length > 0) {
|
|
7129
|
-
log.info(
|
|
7911
|
+
log.info(TAG33, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
|
|
7130
7912
|
}
|
|
7131
7913
|
if (result.errors.length > 0) {
|
|
7132
|
-
log.warn(
|
|
7914
|
+
log.warn(TAG33, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
|
|
7133
7915
|
}
|
|
7134
7916
|
return result;
|
|
7135
7917
|
}
|
|
@@ -7160,27 +7942,27 @@ class WorktreeGc {
|
|
|
7160
7942
|
try {
|
|
7161
7943
|
runWorktreeGc(this.basePath, this.store);
|
|
7162
7944
|
} catch (err) {
|
|
7163
|
-
log.warn(
|
|
7945
|
+
log.warn(TAG33, `GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
7164
7946
|
}
|
|
7165
7947
|
if (this.remoteOpts) {
|
|
7166
7948
|
try {
|
|
7167
7949
|
pruneFailedRemoteBranches(this.remoteOpts);
|
|
7168
7950
|
} catch (err) {
|
|
7169
|
-
log.warn(
|
|
7951
|
+
log.warn(TAG33, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
7170
7952
|
}
|
|
7171
7953
|
}
|
|
7172
7954
|
}
|
|
7173
7955
|
}
|
|
7174
7956
|
function getRepoRoot2() {
|
|
7175
7957
|
try {
|
|
7176
|
-
return
|
|
7958
|
+
return execFileSync11("git", ["rev-parse", "--show-toplevel"], {
|
|
7177
7959
|
encoding: "utf-8"
|
|
7178
7960
|
}).trim();
|
|
7179
7961
|
} catch {
|
|
7180
7962
|
return null;
|
|
7181
7963
|
}
|
|
7182
7964
|
}
|
|
7183
|
-
var
|
|
7965
|
+
var TAG33 = "worktree-gc", GIT_NETWORK_TIMEOUT_MS = 30000, GIT_SSH_CONNECT_TIMEOUT_SECS = 10, GIT_PRUNE_SWEEP_BUDGET_MS = 60000, GIT_NETWORK_EXEC, TRANSIENT_GIT_NETWORK_ERROR;
|
|
7184
7966
|
var init_worktree_gc = __esm(() => {
|
|
7185
7967
|
init_log();
|
|
7186
7968
|
init_worktree();
|
|
@@ -7214,12 +7996,12 @@ __export(exports_src, {
|
|
|
7214
7996
|
validatePrerequisites: () => validatePrerequisites,
|
|
7215
7997
|
main: () => main
|
|
7216
7998
|
});
|
|
7217
|
-
import { execFileSync as
|
|
7999
|
+
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
7218
8000
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7219
8001
|
import { createRequire as createRequire2 } from "node:module";
|
|
7220
8002
|
async function validatePrerequisites(config, banner) {
|
|
7221
8003
|
try {
|
|
7222
|
-
const ver =
|
|
8004
|
+
const ver = execFileSync12("claude", ["--version"], {
|
|
7223
8005
|
encoding: "utf-8"
|
|
7224
8006
|
}).trim();
|
|
7225
8007
|
banner.check(`Claude CLI ${ver}`);
|
|
@@ -7234,14 +8016,14 @@ async function validatePrerequisites(config, banner) {
|
|
|
7234
8016
|
validateGitProviderCli(provider);
|
|
7235
8017
|
}
|
|
7236
8018
|
try {
|
|
7237
|
-
const status =
|
|
8019
|
+
const status = execFileSync12("git", ["status", "--porcelain"], {
|
|
7238
8020
|
encoding: "utf-8"
|
|
7239
8021
|
}).trim();
|
|
7240
8022
|
if (status) {
|
|
7241
8023
|
banner.warn(`Working directory has uncommitted changes:
|
|
7242
8024
|
${status}`);
|
|
7243
8025
|
}
|
|
7244
|
-
|
|
8026
|
+
execFileSync12("git", ["rev-parse", "--verify", `origin/${config.agent.worktree.baseBranch}`], {
|
|
7245
8027
|
encoding: "utf-8",
|
|
7246
8028
|
stdio: "pipe"
|
|
7247
8029
|
});
|
|
@@ -7284,7 +8066,7 @@ async function main() {
|
|
|
7284
8066
|
} catch (err) {
|
|
7285
8067
|
if (err instanceof ConfigValidationError) {
|
|
7286
8068
|
banner.fail();
|
|
7287
|
-
log.error(
|
|
8069
|
+
log.error(TAG34, err.message);
|
|
7288
8070
|
process.exit(1);
|
|
7289
8071
|
}
|
|
7290
8072
|
throw err;
|
|
@@ -7352,6 +8134,7 @@ async function main() {
|
|
|
7352
8134
|
daemonPid: process.pid,
|
|
7353
8135
|
startedAt,
|
|
7354
8136
|
uptimeMs: Date.now() - startedAt,
|
|
8137
|
+
runner: config.agent.runner,
|
|
7355
8138
|
workers: pool.snapshotWorkers().map((w) => ({
|
|
7356
8139
|
...w,
|
|
7357
8140
|
phase: null
|
|
@@ -7393,7 +8176,7 @@ async function main() {
|
|
|
7393
8176
|
if (shuttingDown)
|
|
7394
8177
|
return;
|
|
7395
8178
|
shuttingDown = true;
|
|
7396
|
-
log.info(
|
|
8179
|
+
log.info(TAG34, `Received ${signal}, shutting down gracefully...`);
|
|
7397
8180
|
reconciler.stop();
|
|
7398
8181
|
mergeMonitor?.stop();
|
|
7399
8182
|
worktreeGc.stop();
|
|
@@ -7403,18 +8186,18 @@ async function main() {
|
|
|
7403
8186
|
}
|
|
7404
8187
|
await watcher.stop();
|
|
7405
8188
|
await pool.shutdown();
|
|
7406
|
-
log.info(
|
|
8189
|
+
log.info(TAG34, "Daemon stopped.");
|
|
7407
8190
|
process.exit(exitCode);
|
|
7408
8191
|
};
|
|
7409
8192
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
7410
8193
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
7411
8194
|
process.on("uncaughtException", (err) => {
|
|
7412
|
-
log.error(
|
|
8195
|
+
log.error(TAG34, `Uncaught exception: ${err.message}`);
|
|
7413
8196
|
exitCode = 1;
|
|
7414
8197
|
shutdown("uncaughtException");
|
|
7415
8198
|
});
|
|
7416
8199
|
process.on("unhandledRejection", (reason) => {
|
|
7417
|
-
log.error(
|
|
8200
|
+
log.error(TAG34, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
7418
8201
|
exitCode = 1;
|
|
7419
8202
|
shutdown("unhandledRejection");
|
|
7420
8203
|
});
|
|
@@ -7467,35 +8250,35 @@ async function handleBroadcast(event, client, pool, config, agentId) {
|
|
|
7467
8250
|
if (assignedAgentId === undefined)
|
|
7468
8251
|
return;
|
|
7469
8252
|
if (assignedAgentId === agentId) {
|
|
7470
|
-
log.info(
|
|
8253
|
+
log.info(TAG34, `Broadcast: card ${cardId} assigned to agent`);
|
|
7471
8254
|
try {
|
|
7472
8255
|
await pool.resetAttemptsForReassign(cardId);
|
|
7473
8256
|
await tryEnqueueCard(cardId, client, pool, config, agentId);
|
|
7474
8257
|
} catch (err) {
|
|
7475
|
-
log.error(
|
|
8258
|
+
log.error(TAG34, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
|
|
7476
8259
|
}
|
|
7477
8260
|
} else if (pool.isCardKnown(cardId)) {
|
|
7478
|
-
log.info(
|
|
8261
|
+
log.info(TAG34, `Broadcast: card ${cardId} unassigned from agent`);
|
|
7479
8262
|
await pool.removeCard(cardId);
|
|
7480
8263
|
}
|
|
7481
8264
|
}
|
|
7482
8265
|
async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
7483
8266
|
const { card } = await client.getCard(cardId);
|
|
7484
8267
|
if (card.assigned_agent_id !== agentId) {
|
|
7485
|
-
log.debug(
|
|
8268
|
+
log.debug(TAG34, `Card ${cardId} no longer assigned to agent — skipping`);
|
|
7486
8269
|
return;
|
|
7487
8270
|
}
|
|
7488
8271
|
const board = await client.getBoard(config.projectId, { summary: true });
|
|
7489
8272
|
const columns = board.columns;
|
|
7490
8273
|
const column = columns.find((c) => c.id === card.column_id);
|
|
7491
8274
|
if (!column) {
|
|
7492
|
-
log.warn(
|
|
8275
|
+
log.warn(TAG34, `Column not found for card ${cardId}`);
|
|
7493
8276
|
return;
|
|
7494
8277
|
}
|
|
7495
8278
|
const isPickupColumn = config.agent.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
|
|
7496
8279
|
const isReviewColumn = config.agent.review.enabled && config.agent.review.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
|
|
7497
8280
|
if (!isPickupColumn && !isReviewColumn) {
|
|
7498
|
-
log.info(
|
|
8281
|
+
log.info(TAG34, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
|
|
7499
8282
|
return;
|
|
7500
8283
|
}
|
|
7501
8284
|
const mode = isReviewColumn ? "review" : "implement";
|
|
@@ -7503,16 +8286,16 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
7503
8286
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
7504
8287
|
const subtasks = card.subtasks ?? [];
|
|
7505
8288
|
if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
|
|
7506
|
-
log.debug(
|
|
8289
|
+
log.debug(TAG34, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
|
|
7507
8290
|
return;
|
|
7508
8291
|
}
|
|
7509
8292
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
7510
|
-
log.info(
|
|
8293
|
+
log.info(TAG34, `Card #${card.short_id} has no branch reference — skipping auto-review`);
|
|
7511
8294
|
return;
|
|
7512
8295
|
}
|
|
7513
8296
|
await pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
7514
8297
|
}
|
|
7515
|
-
var
|
|
8298
|
+
var TAG34 = "daemon", PKG_VERSION;
|
|
7516
8299
|
var init_src = __esm(() => {
|
|
7517
8300
|
init_board_helpers();
|
|
7518
8301
|
init_config();
|