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