@gethmy/agent 1.10.8 → 1.10.9
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 +359 -239
- package/dist/index.js +359 -239
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -424,6 +424,7 @@ var init_types = __esm(() => {
|
|
|
424
424
|
autoFix: true,
|
|
425
425
|
maxFixAttempts: 1,
|
|
426
426
|
deepReview: false,
|
|
427
|
+
revertGuard: true,
|
|
427
428
|
devServerBasePort: 4200,
|
|
428
429
|
timeout: 120000,
|
|
429
430
|
failColumn: "To Do"
|
|
@@ -1209,6 +1210,23 @@ var init_pm = __esm(() => {
|
|
|
1209
1210
|
import { execFileSync as execFileSync3, execSync as execSync2 } from "node:child_process";
|
|
1210
1211
|
import { existsSync as existsSync2, rmSync } from "node:fs";
|
|
1211
1212
|
import { resolve } from "node:path";
|
|
1213
|
+
function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root, branch) => execFileSync3("git", ["fetch", "origin", branch], {
|
|
1214
|
+
cwd: root,
|
|
1215
|
+
stdio: "pipe"
|
|
1216
|
+
})) {
|
|
1217
|
+
let lastErr;
|
|
1218
|
+
for (let attempt = 1;attempt <= attempts; attempt++) {
|
|
1219
|
+
try {
|
|
1220
|
+
fetchImpl(repoRoot, baseBranch);
|
|
1221
|
+
return;
|
|
1222
|
+
} catch (err) {
|
|
1223
|
+
lastErr = err;
|
|
1224
|
+
log.warn(TAG5, `fetch origin ${baseBranch} failed (attempt ${attempt}/${attempts})`);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
const detail = lastErr instanceof Error ? lastErr.message : String(lastErr);
|
|
1228
|
+
throw new WorktreeBaseError(`Could not fetch origin/${baseBranch} after ${attempts} attempts — ` + `refusing to build on a stale base. ${detail}`);
|
|
1229
|
+
}
|
|
1212
1230
|
function createWorktree(basePath, baseBranch, branchName) {
|
|
1213
1231
|
const repoRoot = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
|
|
1214
1232
|
encoding: "utf-8"
|
|
@@ -1224,14 +1242,7 @@ function createWorktree(basePath, baseBranch, branchName) {
|
|
|
1224
1242
|
stdio: "pipe"
|
|
1225
1243
|
});
|
|
1226
1244
|
} catch {}
|
|
1227
|
-
|
|
1228
|
-
execFileSync3("git", ["fetch", "origin", baseBranch], {
|
|
1229
|
-
cwd: repoRoot,
|
|
1230
|
-
stdio: "pipe"
|
|
1231
|
-
});
|
|
1232
|
-
} catch {
|
|
1233
|
-
log.warn(TAG5, "Failed to fetch latest — continuing with local state");
|
|
1234
|
-
}
|
|
1245
|
+
fetchBaseBranch(repoRoot, baseBranch);
|
|
1235
1246
|
log.info(TAG5, `Creating worktree: ${worktreeDir} (branch: ${branchName})`);
|
|
1236
1247
|
try {
|
|
1237
1248
|
execFileSync3("git", [
|
|
@@ -1319,10 +1330,16 @@ function makeBranchName(shortId, title, prefix = "agent-attempts/") {
|
|
|
1319
1330
|
const slug = title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
1320
1331
|
return `${prefix}${shortId}-${slug || "task"}`;
|
|
1321
1332
|
}
|
|
1322
|
-
var TAG5 = "worktree";
|
|
1333
|
+
var TAG5 = "worktree", WorktreeBaseError;
|
|
1323
1334
|
var init_worktree = __esm(() => {
|
|
1324
1335
|
init_log();
|
|
1325
1336
|
init_pm();
|
|
1337
|
+
WorktreeBaseError = class WorktreeBaseError extends Error {
|
|
1338
|
+
constructor(message) {
|
|
1339
|
+
super(message);
|
|
1340
|
+
this.name = "WorktreeBaseError";
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1326
1343
|
});
|
|
1327
1344
|
|
|
1328
1345
|
// src/review-worktree.ts
|
|
@@ -2200,41 +2217,91 @@ var init_project_type = __esm(() => {
|
|
|
2200
2217
|
_cache = new Map;
|
|
2201
2218
|
});
|
|
2202
2219
|
|
|
2220
|
+
// src/revert-guard.ts
|
|
2221
|
+
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
2222
|
+
function isTestFile(path) {
|
|
2223
|
+
return TEST_FILE.test(path);
|
|
2224
|
+
}
|
|
2225
|
+
function filterTestFiles(paths) {
|
|
2226
|
+
return paths.filter(isTestFile);
|
|
2227
|
+
}
|
|
2228
|
+
function refetchBase(worktreePath, baseBranch) {
|
|
2229
|
+
try {
|
|
2230
|
+
execFileSync7("git", ["fetch", "origin", baseBranch], {
|
|
2231
|
+
cwd: worktreePath,
|
|
2232
|
+
stdio: "pipe"
|
|
2233
|
+
});
|
|
2234
|
+
} catch {
|
|
2235
|
+
log.warn(TAG12, "Failed to re-fetch base for revert guard — using last fetch");
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
|
|
2239
|
+
try {
|
|
2240
|
+
const out = execFileSync7("git", ["diff", "--diff-filter=D", "--name-only", `origin/${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8" });
|
|
2241
|
+
return out.split(`
|
|
2242
|
+
`).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
2243
|
+
} catch (err) {
|
|
2244
|
+
log.warn(TAG12, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
|
|
2245
|
+
return [];
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
function findDeletedTestFiles(worktreePath, baseBranch) {
|
|
2249
|
+
refetchBase(worktreePath, baseBranch);
|
|
2250
|
+
return filterTestFiles(listDeletedFilesAgainstBase(worktreePath, baseBranch));
|
|
2251
|
+
}
|
|
2252
|
+
var TAG12 = "revert-guard", TEST_FILE;
|
|
2253
|
+
var init_revert_guard = __esm(() => {
|
|
2254
|
+
init_log();
|
|
2255
|
+
TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
2256
|
+
});
|
|
2257
|
+
|
|
2203
2258
|
// src/verification.ts
|
|
2204
|
-
import { execFileSync as
|
|
2259
|
+
import { execFileSync as execFileSync8, spawn } from "node:child_process";
|
|
2205
2260
|
async function runVerification(worktreePath, config, workerId) {
|
|
2206
2261
|
const result = {
|
|
2207
2262
|
passed: true,
|
|
2208
2263
|
buildErrors: [],
|
|
2209
2264
|
lintWarnings: [],
|
|
2210
|
-
reviewFindings: []
|
|
2265
|
+
reviewFindings: [],
|
|
2266
|
+
revertWarnings: []
|
|
2211
2267
|
};
|
|
2268
|
+
if (config.verification.revertGuard) {
|
|
2269
|
+
log.info(TAG13, `[worker:${workerId}] Checking for reverted merged work...`);
|
|
2270
|
+
const deletedTests = findDeletedTestFiles(worktreePath, config.worktree.baseBranch);
|
|
2271
|
+
if (deletedTests.length > 0) {
|
|
2272
|
+
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.");
|
|
2273
|
+
log.warn(TAG13, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
|
|
2274
|
+
result.passed = false;
|
|
2275
|
+
} else {
|
|
2276
|
+
log.info(TAG13, `[worker:${workerId}] Revert guard passed`);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2212
2279
|
if (config.verification.build) {
|
|
2213
|
-
log.info(
|
|
2280
|
+
log.info(TAG13, `[worker:${workerId}] Running build...`);
|
|
2214
2281
|
result.buildErrors = runBuild(worktreePath, config.verification.timeout);
|
|
2215
2282
|
if (result.buildErrors.length > 0) {
|
|
2216
|
-
log.warn(
|
|
2283
|
+
log.warn(TAG13, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
|
|
2217
2284
|
result.passed = false;
|
|
2218
2285
|
} else {
|
|
2219
|
-
log.info(
|
|
2286
|
+
log.info(TAG13, `[worker:${workerId}] Build passed`);
|
|
2220
2287
|
}
|
|
2221
2288
|
}
|
|
2222
2289
|
if (config.verification.lint) {
|
|
2223
|
-
log.info(
|
|
2290
|
+
log.info(TAG13, `[worker:${workerId}] Running lint...`);
|
|
2224
2291
|
result.lintWarnings = runLint(worktreePath, config.verification.timeout);
|
|
2225
2292
|
if (result.lintWarnings.length > 0) {
|
|
2226
|
-
log.warn(
|
|
2293
|
+
log.warn(TAG13, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
|
|
2227
2294
|
} else {
|
|
2228
|
-
log.info(
|
|
2295
|
+
log.info(TAG13, `[worker:${workerId}] Lint passed`);
|
|
2229
2296
|
}
|
|
2230
2297
|
}
|
|
2231
2298
|
if (config.verification.deepReview) {
|
|
2232
|
-
log.info(
|
|
2299
|
+
log.info(TAG13, `[worker:${workerId}] Running deep review...`);
|
|
2233
2300
|
result.reviewFindings = await runDeepReview(worktreePath, config, workerId);
|
|
2234
2301
|
if (result.reviewFindings.length > 0) {
|
|
2235
|
-
log.warn(
|
|
2302
|
+
log.warn(TAG13, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
|
|
2236
2303
|
} else {
|
|
2237
|
-
log.info(
|
|
2304
|
+
log.info(TAG13, `[worker:${workerId}] Deep review passed`);
|
|
2238
2305
|
}
|
|
2239
2306
|
}
|
|
2240
2307
|
return result;
|
|
@@ -2242,11 +2309,11 @@ async function runVerification(worktreePath, config, workerId) {
|
|
|
2242
2309
|
function runBuild(worktreePath, timeout) {
|
|
2243
2310
|
const command = buildCommand(worktreePath);
|
|
2244
2311
|
if (!command) {
|
|
2245
|
-
log.warn(
|
|
2312
|
+
log.warn(TAG13, `No known build toolchain for ${worktreePath} — skipping build`);
|
|
2246
2313
|
return [];
|
|
2247
2314
|
}
|
|
2248
2315
|
try {
|
|
2249
|
-
|
|
2316
|
+
execFileSync8(command.cmd, command.args, {
|
|
2250
2317
|
cwd: worktreePath,
|
|
2251
2318
|
timeout,
|
|
2252
2319
|
stdio: "pipe"
|
|
@@ -2259,11 +2326,11 @@ function runBuild(worktreePath, timeout) {
|
|
|
2259
2326
|
function runLint(worktreePath, timeout) {
|
|
2260
2327
|
const command = lintCommand(worktreePath);
|
|
2261
2328
|
if (!command) {
|
|
2262
|
-
log.info(
|
|
2329
|
+
log.info(TAG13, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
|
|
2263
2330
|
return [];
|
|
2264
2331
|
}
|
|
2265
2332
|
try {
|
|
2266
|
-
|
|
2333
|
+
execFileSync8(command.cmd, command.args, {
|
|
2267
2334
|
cwd: worktreePath,
|
|
2268
2335
|
timeout,
|
|
2269
2336
|
stdio: "pipe"
|
|
@@ -2275,7 +2342,7 @@ function runLint(worktreePath, timeout) {
|
|
|
2275
2342
|
}
|
|
2276
2343
|
async function runDeepReview(worktreePath, config, workerId) {
|
|
2277
2344
|
if (!supportsDevServer(worktreePath)) {
|
|
2278
|
-
log.info(
|
|
2345
|
+
log.info(TAG13, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
|
|
2279
2346
|
return [];
|
|
2280
2347
|
}
|
|
2281
2348
|
const port = config.verification.devServerBasePort + workerId;
|
|
@@ -2290,12 +2357,12 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2290
2357
|
await waitForDevServer(devServer, 30000);
|
|
2291
2358
|
await probeDevServer(port);
|
|
2292
2359
|
} catch (err) {
|
|
2293
|
-
log.error(
|
|
2360
|
+
log.error(TAG13, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
|
|
2294
2361
|
return [];
|
|
2295
2362
|
}
|
|
2296
2363
|
let diff = "";
|
|
2297
2364
|
try {
|
|
2298
|
-
diff =
|
|
2365
|
+
diff = execFileSync8("git", ["diff", `origin/${config.worktree.baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
|
|
2299
2366
|
} catch {
|
|
2300
2367
|
diff = "(unable to retrieve diff)";
|
|
2301
2368
|
}
|
|
@@ -2312,7 +2379,7 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2312
2379
|
].join(`
|
|
2313
2380
|
`);
|
|
2314
2381
|
const leanSources = config.claude.leanSettingSources;
|
|
2315
|
-
const output =
|
|
2382
|
+
const output = execFileSync8("claude", [
|
|
2316
2383
|
"--print",
|
|
2317
2384
|
"--model",
|
|
2318
2385
|
"sonnet",
|
|
@@ -2329,7 +2396,7 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2329
2396
|
});
|
|
2330
2397
|
return parseReviewFindings(output);
|
|
2331
2398
|
} catch (err) {
|
|
2332
|
-
log.error(
|
|
2399
|
+
log.error(TAG13, `Deep review failed: ${err instanceof Error ? err.message : err}`);
|
|
2333
2400
|
return [];
|
|
2334
2401
|
} finally {
|
|
2335
2402
|
if (devServer && !devServer.killed) {
|
|
@@ -2365,8 +2432,8 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
2365
2432
|
"--",
|
|
2366
2433
|
fixPrompt
|
|
2367
2434
|
];
|
|
2368
|
-
log.info(
|
|
2369
|
-
|
|
2435
|
+
log.info(TAG13, "Spawning Claude for auto-fix...");
|
|
2436
|
+
execFileSync8("claude", args, {
|
|
2370
2437
|
cwd: worktreePath,
|
|
2371
2438
|
timeout: config.verification.timeout,
|
|
2372
2439
|
stdio: "pipe"
|
|
@@ -2379,6 +2446,9 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
2379
2446
|
const url = recovery.branchUrl ? ` (${recovery.branchUrl})` : "";
|
|
2380
2447
|
items.push(`Recovery: \`${cmd}\`${url}`);
|
|
2381
2448
|
}
|
|
2449
|
+
for (const warn of result.revertWarnings) {
|
|
2450
|
+
items.push(`Revert: ${warn}`);
|
|
2451
|
+
}
|
|
2382
2452
|
for (const err of result.buildErrors) {
|
|
2383
2453
|
items.push(`Build: ${err}`);
|
|
2384
2454
|
}
|
|
@@ -2396,7 +2466,7 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
2396
2466
|
try {
|
|
2397
2467
|
await client.createSubtask(cardId, title);
|
|
2398
2468
|
} catch (err) {
|
|
2399
|
-
log.error(
|
|
2469
|
+
log.error(TAG13, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
|
|
2400
2470
|
}
|
|
2401
2471
|
}));
|
|
2402
2472
|
if (overflow > 0) {
|
|
@@ -2404,7 +2474,7 @@ async function reportFindings(client, cardId, result, recovery) {
|
|
|
2404
2474
|
await client.createSubtask(cardId, `...and ${overflow} more issues`);
|
|
2405
2475
|
} catch {}
|
|
2406
2476
|
}
|
|
2407
|
-
log.info(
|
|
2477
|
+
log.info(TAG13, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
|
|
2408
2478
|
}
|
|
2409
2479
|
function parseErrorOutput(err) {
|
|
2410
2480
|
const stderr = err?.stderr?.toString() ?? "";
|
|
@@ -2488,11 +2558,12 @@ async function probeDevServer(port, timeoutMs = 5000) {
|
|
|
2488
2558
|
clearTimeout(timer);
|
|
2489
2559
|
}
|
|
2490
2560
|
}
|
|
2491
|
-
var
|
|
2561
|
+
var TAG13 = "verification", DevServerReadinessError;
|
|
2492
2562
|
var init_verification = __esm(() => {
|
|
2493
2563
|
init_log();
|
|
2494
2564
|
init_pm();
|
|
2495
2565
|
init_project_type();
|
|
2566
|
+
init_revert_guard();
|
|
2496
2567
|
DevServerReadinessError = class DevServerReadinessError extends Error {
|
|
2497
2568
|
constructor(message) {
|
|
2498
2569
|
super(message);
|
|
@@ -2502,7 +2573,7 @@ var init_verification = __esm(() => {
|
|
|
2502
2573
|
});
|
|
2503
2574
|
|
|
2504
2575
|
// src/completion.ts
|
|
2505
|
-
import { execFileSync as
|
|
2576
|
+
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
2506
2577
|
function formatTokenCount(tokens) {
|
|
2507
2578
|
if (tokens >= 1e6)
|
|
2508
2579
|
return `${(tokens / 1e6).toFixed(1)}M`;
|
|
@@ -2528,11 +2599,12 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2528
2599
|
passed: true,
|
|
2529
2600
|
buildErrors: [],
|
|
2530
2601
|
lintWarnings: [],
|
|
2531
|
-
reviewFindings: []
|
|
2602
|
+
reviewFindings: [],
|
|
2603
|
+
revertWarnings: []
|
|
2532
2604
|
};
|
|
2533
2605
|
const hasCommits = checkHasCommits(worktreePath, config.worktree.baseBranch);
|
|
2534
2606
|
if (!hasCommits) {
|
|
2535
|
-
log.warn(
|
|
2607
|
+
log.warn(TAG14, `No commits on branch ${branchName} — skipping completion`);
|
|
2536
2608
|
await client.endAgentSession(card.id, {
|
|
2537
2609
|
status: "completed",
|
|
2538
2610
|
progressPercent: 100,
|
|
@@ -2541,13 +2613,13 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2541
2613
|
cleanupWorktree(worktreePath, branchName);
|
|
2542
2614
|
return true;
|
|
2543
2615
|
}
|
|
2544
|
-
log.info(
|
|
2616
|
+
log.info(TAG14, `Pushing branch ${branchName} (pre-verify)...`);
|
|
2545
2617
|
let lastPushedSha = null;
|
|
2546
2618
|
try {
|
|
2547
2619
|
pushBranch(branchName, worktreePath);
|
|
2548
2620
|
lastPushedSha = readHeadSha(worktreePath);
|
|
2549
2621
|
} catch (err) {
|
|
2550
|
-
log.error(
|
|
2622
|
+
log.error(TAG14, `pre-verify push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
2551
2623
|
}
|
|
2552
2624
|
const recoveryUrl = lastPushedSha ? getBranchWebUrl(branchName, worktreePath) : null;
|
|
2553
2625
|
if (config.verification.enabled) {
|
|
@@ -2562,7 +2634,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2562
2634
|
let autoFixAttempts = 0;
|
|
2563
2635
|
if (!result.passed && config.verification.autoFix) {
|
|
2564
2636
|
for (let attempt = 0;attempt < config.verification.maxFixAttempts; attempt++) {
|
|
2565
|
-
log.info(
|
|
2637
|
+
log.info(TAG14, `Auto-fix attempt ${attempt + 1}/${config.verification.maxFixAttempts}`);
|
|
2566
2638
|
await client.updateAgentProgress(card.id, {
|
|
2567
2639
|
agentIdentifier: agentIdentifier(workerId),
|
|
2568
2640
|
agentName: AGENT_NAME,
|
|
@@ -2575,14 +2647,14 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2575
2647
|
result = await runVerification(worktreePath, config, workerId);
|
|
2576
2648
|
autoFixAttempts = attempt + 1;
|
|
2577
2649
|
if (result.passed) {
|
|
2578
|
-
log.info(
|
|
2650
|
+
log.info(TAG14, `Auto-fix succeeded on attempt ${attempt + 1}`);
|
|
2579
2651
|
const sha = readHeadSha(worktreePath);
|
|
2580
2652
|
if (sha && sha !== lastPushedSha) {
|
|
2581
2653
|
try {
|
|
2582
2654
|
pushBranch(branchName, worktreePath);
|
|
2583
2655
|
lastPushedSha = sha;
|
|
2584
2656
|
} catch (err) {
|
|
2585
|
-
log.warn(
|
|
2657
|
+
log.warn(TAG14, `post-fix push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
2586
2658
|
}
|
|
2587
2659
|
}
|
|
2588
2660
|
break;
|
|
@@ -2591,14 +2663,14 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2591
2663
|
}
|
|
2592
2664
|
verificationResult = result;
|
|
2593
2665
|
if (!result.passed) {
|
|
2594
|
-
log.warn(
|
|
2666
|
+
log.warn(TAG14, `Verification failed for #${card.short_id} — reporting findings`);
|
|
2595
2667
|
const failSha = readHeadSha(worktreePath);
|
|
2596
2668
|
if (failSha && failSha !== lastPushedSha) {
|
|
2597
2669
|
try {
|
|
2598
2670
|
pushBranch(branchName, worktreePath);
|
|
2599
2671
|
lastPushedSha = failSha;
|
|
2600
2672
|
} catch (err) {
|
|
2601
|
-
log.warn(
|
|
2673
|
+
log.warn(TAG14, `post-fail push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
2602
2674
|
}
|
|
2603
2675
|
}
|
|
2604
2676
|
const failureSummary = buildVerificationFailureSummary(result, autoFixAttempts);
|
|
@@ -2609,7 +2681,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2609
2681
|
recoveryBranch: branchName
|
|
2610
2682
|
});
|
|
2611
2683
|
} catch (err) {
|
|
2612
|
-
log.debug(
|
|
2684
|
+
log.debug(TAG14, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
|
|
2613
2685
|
}
|
|
2614
2686
|
await reportFindings(client, card.id, result, lastPushedSha ? { branchName, branchUrl: recoveryUrl } : null);
|
|
2615
2687
|
await moveCardToColumn(client, card, config.verification.failColumn);
|
|
@@ -2623,7 +2695,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2623
2695
|
cleanupWorktree(worktreePath, branchName);
|
|
2624
2696
|
return false;
|
|
2625
2697
|
}
|
|
2626
|
-
log.info(
|
|
2698
|
+
log.info(TAG14, `Verification passed for #${card.short_id}`);
|
|
2627
2699
|
}
|
|
2628
2700
|
let prUrl = null;
|
|
2629
2701
|
if (config.completion.createPR) {
|
|
@@ -2636,7 +2708,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2636
2708
|
try {
|
|
2637
2709
|
await onMovedToCompletion(card);
|
|
2638
2710
|
} catch (err) {
|
|
2639
|
-
log.warn(
|
|
2711
|
+
log.warn(TAG14, `successor promotion failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
2640
2712
|
}
|
|
2641
2713
|
}
|
|
2642
2714
|
}
|
|
@@ -2670,11 +2742,14 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2670
2742
|
});
|
|
2671
2743
|
}
|
|
2672
2744
|
cleanupWorktree(worktreePath, branchName);
|
|
2673
|
-
log.info(
|
|
2745
|
+
log.info(TAG14, `Completion done for #${card.short_id}${prUrl ? ` — PR: ${prUrl}` : ""}`);
|
|
2674
2746
|
return true;
|
|
2675
2747
|
}
|
|
2676
2748
|
function buildVerificationFailureSummary(result, autoFixAttempts) {
|
|
2677
2749
|
const counts = [];
|
|
2750
|
+
if (result.revertWarnings.length > 0) {
|
|
2751
|
+
counts.push(`${result.revertWarnings.length} reverted test(s)`);
|
|
2752
|
+
}
|
|
2678
2753
|
if (result.buildErrors.length > 0) {
|
|
2679
2754
|
counts.push(`${result.buildErrors.length} build error(s)`);
|
|
2680
2755
|
}
|
|
@@ -2690,7 +2765,7 @@ function buildVerificationFailureSummary(result, autoFixAttempts) {
|
|
|
2690
2765
|
}
|
|
2691
2766
|
function readHeadSha(worktreePath) {
|
|
2692
2767
|
try {
|
|
2693
|
-
return
|
|
2768
|
+
return execFileSync9("git", ["rev-parse", "HEAD"], {
|
|
2694
2769
|
cwd: worktreePath,
|
|
2695
2770
|
encoding: "utf-8"
|
|
2696
2771
|
}).trim();
|
|
@@ -2700,7 +2775,7 @@ function readHeadSha(worktreePath) {
|
|
|
2700
2775
|
}
|
|
2701
2776
|
function checkHasCommits(worktreePath, baseBranch) {
|
|
2702
2777
|
try {
|
|
2703
|
-
const count =
|
|
2778
|
+
const count = execFileSync9("git", ["rev-list", "--count", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
2704
2779
|
return parseInt(count, 10) > 0;
|
|
2705
2780
|
} catch {
|
|
2706
2781
|
return false;
|
|
@@ -2709,7 +2784,7 @@ function checkHasCommits(worktreePath, baseBranch) {
|
|
|
2709
2784
|
async function postSummary(client, card, branchName, worktreePath, prUrl, baseBranch, sessionStats) {
|
|
2710
2785
|
let commitLog = "";
|
|
2711
2786
|
try {
|
|
2712
|
-
commitLog =
|
|
2787
|
+
commitLog = execFileSync9("git", ["log", "--oneline", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
2713
2788
|
} catch {}
|
|
2714
2789
|
const SUMMARY_MARKER = `---
|
|
2715
2790
|
**Agent completed**`;
|
|
@@ -2754,12 +2829,12 @@ ${commitLog}
|
|
|
2754
2829
|
description: baseDesc + parts.join(`
|
|
2755
2830
|
`)
|
|
2756
2831
|
});
|
|
2757
|
-
log.info(
|
|
2832
|
+
log.info(TAG14, `Posted completion summary to #${card.short_id}`);
|
|
2758
2833
|
} catch (err) {
|
|
2759
|
-
log.error(
|
|
2834
|
+
log.error(TAG14, `Failed to post summary: ${err instanceof Error ? err.message : err}`);
|
|
2760
2835
|
}
|
|
2761
2836
|
}
|
|
2762
|
-
var
|
|
2837
|
+
var TAG14 = "completion";
|
|
2763
2838
|
var init_completion = __esm(() => {
|
|
2764
2839
|
init_board_helpers();
|
|
2765
2840
|
init_episode_writer();
|
|
@@ -2799,7 +2874,7 @@ function signalGroup(proc, signal) {
|
|
|
2799
2874
|
} catch (err) {
|
|
2800
2875
|
const code = err.code;
|
|
2801
2876
|
if (code !== "ESRCH") {
|
|
2802
|
-
log.warn(
|
|
2877
|
+
log.warn(TAG15, `signal ${signal} to pgid ${proc.pid} failed: ${err instanceof Error ? err.message : err}`);
|
|
2803
2878
|
}
|
|
2804
2879
|
}
|
|
2805
2880
|
}
|
|
@@ -2824,7 +2899,7 @@ async function terminateGroup(proc, opts) {
|
|
|
2824
2899
|
return;
|
|
2825
2900
|
signalGroup(proc, "SIGKILL");
|
|
2826
2901
|
}
|
|
2827
|
-
var
|
|
2902
|
+
var TAG15 = "pgroup";
|
|
2828
2903
|
var init_process_group = __esm(() => {
|
|
2829
2904
|
init_log();
|
|
2830
2905
|
});
|
|
@@ -2927,7 +3002,7 @@ class ProgressTracker {
|
|
|
2927
3002
|
}
|
|
2928
3003
|
onToolStart(name, input) {
|
|
2929
3004
|
this.toolCallCount++;
|
|
2930
|
-
log.debug(
|
|
3005
|
+
log.debug(TAG16, `Tool: ${name} (count: ${this.toolCallCount}, phase: ${this.phase})`);
|
|
2931
3006
|
const filePath = this.extractString(input, "file_path");
|
|
2932
3007
|
if (filePath) {
|
|
2933
3008
|
if (EDIT_TOOLS.has(name)) {
|
|
@@ -2998,7 +3073,7 @@ class ProgressTracker {
|
|
|
2998
3073
|
transitionTo(newPhase) {
|
|
2999
3074
|
if (PHASE_ORDER[newPhase] <= PHASE_ORDER[this.phase])
|
|
3000
3075
|
return;
|
|
3001
|
-
log.info(
|
|
3076
|
+
log.info(TAG16, `Phase: ${this.phase} → ${newPhase}`);
|
|
3002
3077
|
this.phase = newPhase;
|
|
3003
3078
|
this.progress = Math.max(this.progress, PHASES[newPhase].min);
|
|
3004
3079
|
this.lastAction = "";
|
|
@@ -3097,7 +3172,7 @@ class ProgressTracker {
|
|
|
3097
3172
|
}
|
|
3098
3173
|
sendUpdate(currentTask) {
|
|
3099
3174
|
this.lastUpdateAt = Date.now();
|
|
3100
|
-
log.debug(
|
|
3175
|
+
log.debug(TAG16, `Progress: ${this.progress}% — ${currentTask}`);
|
|
3101
3176
|
this.client.updateAgentProgress(this.cardId, {
|
|
3102
3177
|
agentIdentifier: agentIdentifier(this.workerId),
|
|
3103
3178
|
agentName: AGENT_NAME,
|
|
@@ -3114,7 +3189,7 @@ class ProgressTracker {
|
|
|
3114
3189
|
modelName: this.lastCost?.modelName,
|
|
3115
3190
|
numTurns: this.lastCost?.numTurns ?? 0
|
|
3116
3191
|
}).catch((err) => {
|
|
3117
|
-
log.warn(
|
|
3192
|
+
log.warn(TAG16, `Failed to send progress update: ${err}`);
|
|
3118
3193
|
});
|
|
3119
3194
|
this.flushActivityLog();
|
|
3120
3195
|
}
|
|
@@ -3155,7 +3230,7 @@ class ProgressTracker {
|
|
|
3155
3230
|
toolName: e.toolName ?? undefined
|
|
3156
3231
|
}))
|
|
3157
3232
|
}).catch((err) => {
|
|
3158
|
-
log.warn(
|
|
3233
|
+
log.warn(TAG16, `Failed to flush activity log: ${err}`);
|
|
3159
3234
|
this.logBuffer.unshift(...raw);
|
|
3160
3235
|
if (this.logBuffer.length > MAX_LOG_BUFFER) {
|
|
3161
3236
|
this.logBuffer.length = MAX_LOG_BUFFER;
|
|
@@ -3186,7 +3261,7 @@ class ProgressTracker {
|
|
|
3186
3261
|
return null;
|
|
3187
3262
|
}
|
|
3188
3263
|
}
|
|
3189
|
-
var
|
|
3264
|
+
var TAG16 = "progress-tracker", THROTTLE_MS = 5000, HEARTBEAT_MS = 60000, MAX_TASK_LENGTH = 120, MAX_LOG_BUFFER = 500, MAX_TEXT_BLOCKS = 40, SENTENCE_SPLIT, ACTION_PREFIX, GIT_COMMIT_RE, BUILD_CMD_RE, PHASES, PHASE_ORDER, EDIT_TOOLS, FILE_TOOL_VERBS;
|
|
3190
3265
|
var init_progress_tracker = __esm(() => {
|
|
3191
3266
|
init_log();
|
|
3192
3267
|
init_types();
|
|
@@ -3222,6 +3297,41 @@ var init_progress_tracker = __esm(() => {
|
|
|
3222
3297
|
|
|
3223
3298
|
// src/review-completion.ts
|
|
3224
3299
|
import { readFileSync as readFileSync2, statSync } from "node:fs";
|
|
3300
|
+
function clampSubtaskTitle(title) {
|
|
3301
|
+
return title.length > MAX_SUBTASK_TITLE ? `${title.slice(0, MAX_SUBTASK_TITLE - 3)}...` : title;
|
|
3302
|
+
}
|
|
3303
|
+
function renderFindingBlock(f) {
|
|
3304
|
+
const locationLine = f.location ? `
|
|
3305
|
+
Location: ${f.location}` : "";
|
|
3306
|
+
return `**[${f.severity}] ${f.title}**
|
|
3307
|
+
${f.description}${locationLine}`;
|
|
3308
|
+
}
|
|
3309
|
+
function buildFindingComments(findings) {
|
|
3310
|
+
const header = `**Review findings — ${findings.length} blocking issue(s) to resolve.**`;
|
|
3311
|
+
const sep = `
|
|
3312
|
+
|
|
3313
|
+
`;
|
|
3314
|
+
const bodies = [];
|
|
3315
|
+
let current = header;
|
|
3316
|
+
for (const f of findings) {
|
|
3317
|
+
let block = renderFindingBlock(f);
|
|
3318
|
+
const maxBlock = COMMENT_BODY_BUDGET - header.length - sep.length;
|
|
3319
|
+
if (block.length > maxBlock) {
|
|
3320
|
+
const suffix = `
|
|
3321
|
+
…[truncated]`;
|
|
3322
|
+
block = `${block.slice(0, Math.max(0, maxBlock - suffix.length))}${suffix}`;
|
|
3323
|
+
}
|
|
3324
|
+
if (current.length + sep.length + block.length > COMMENT_BODY_BUDGET) {
|
|
3325
|
+
bodies.push(current);
|
|
3326
|
+
current = `${header}${sep}${block}`;
|
|
3327
|
+
} else {
|
|
3328
|
+
current = `${current}${sep}${block}`;
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
if (current.length > header.length)
|
|
3332
|
+
bodies.push(current);
|
|
3333
|
+
return bodies;
|
|
3334
|
+
}
|
|
3225
3335
|
function tailRunLog(path, bytes = RUN_LOG_TAIL_BYTES) {
|
|
3226
3336
|
try {
|
|
3227
3337
|
const size = statSync(path).size;
|
|
@@ -3261,7 +3371,7 @@ function parseReviewOutput(stdout) {
|
|
|
3261
3371
|
try {
|
|
3262
3372
|
const parsed = JSON.parse(raw);
|
|
3263
3373
|
if (parsed && typeof parsed === "object" && "verdict" in parsed) {
|
|
3264
|
-
log.debug(
|
|
3374
|
+
log.debug(TAG17, "Parsed review output from fenced JSON block");
|
|
3265
3375
|
return extractResult(parsed);
|
|
3266
3376
|
}
|
|
3267
3377
|
} catch {}
|
|
@@ -3287,21 +3397,21 @@ function parseReviewOutput(stdout) {
|
|
|
3287
3397
|
try {
|
|
3288
3398
|
const parsed = JSON.parse(candidates[i]);
|
|
3289
3399
|
if (parsed && typeof parsed === "object" && "verdict" in parsed) {
|
|
3290
|
-
log.debug(
|
|
3400
|
+
log.debug(TAG17, "Parsed review output from raw JSON object");
|
|
3291
3401
|
return extractResult(parsed);
|
|
3292
3402
|
}
|
|
3293
3403
|
} catch {}
|
|
3294
3404
|
}
|
|
3295
3405
|
const verdictMatch = stdout.match(/"verdict"\s*:\s*"(approved|rejected)"/i);
|
|
3296
3406
|
if (verdictMatch) {
|
|
3297
|
-
log.warn(
|
|
3407
|
+
log.warn(TAG17, `Parsed verdict via regex fallback — findings lost (${verdictMatch[1]})`);
|
|
3298
3408
|
return {
|
|
3299
3409
|
verdict: verdictMatch[1].toLowerCase(),
|
|
3300
3410
|
summary: "Parsed via regex fallback — original JSON was malformed. Check run log.",
|
|
3301
3411
|
findings: []
|
|
3302
3412
|
};
|
|
3303
3413
|
}
|
|
3304
|
-
log.warn(
|
|
3414
|
+
log.warn(TAG17, "Failed to parse review JSON output — returning error verdict (card stays in Review)");
|
|
3305
3415
|
return {
|
|
3306
3416
|
verdict: "error",
|
|
3307
3417
|
summary: stdout.slice(0, 500),
|
|
@@ -3334,7 +3444,7 @@ async function postReviewComment(client, card, commentType, body) {
|
|
|
3334
3444
|
try {
|
|
3335
3445
|
await client.addComment(card.id, body, { commentType });
|
|
3336
3446
|
} catch (err) {
|
|
3337
|
-
log.error(
|
|
3447
|
+
log.error(TAG17, `Failed to post review comment to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
3338
3448
|
}
|
|
3339
3449
|
}
|
|
3340
3450
|
async function runReviewCompletion(client, card, result, config, worktreePath, branchName, sessionStats, runLogPath, workspaceId, agentSessionId, stateStore) {
|
|
@@ -3348,11 +3458,11 @@ async function runReviewCompletion(client, card, result, config, worktreePath, b
|
|
|
3348
3458
|
const currentCycle = getReviewCycle(freshDesc) + 1;
|
|
3349
3459
|
const maxCycles = config.review.maxReviewCycles;
|
|
3350
3460
|
if (result.verdict === "error") {
|
|
3351
|
-
log.warn(
|
|
3461
|
+
log.warn(TAG17, `#${card.short_id} review output unparseable — labelling "${NEED_REVIEW_LABEL}" for manual inspection`);
|
|
3352
3462
|
try {
|
|
3353
3463
|
await addLabelByName(client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR);
|
|
3354
3464
|
} catch (err) {
|
|
3355
|
-
log.warn(
|
|
3465
|
+
log.warn(TAG17, `Failed to add "${NEED_REVIEW_LABEL}" label: ${err instanceof Error ? err.message : err}`);
|
|
3356
3466
|
}
|
|
3357
3467
|
if (config.review.postFindings) {
|
|
3358
3468
|
const rawTail = runLogPath ? tailRunLog(runLogPath) : null;
|
|
@@ -3395,7 +3505,7 @@ ${runLogTail}
|
|
|
3395
3505
|
renameRemoteBranch(branchName, newRef, worktreePath);
|
|
3396
3506
|
approvedBranch = newRef;
|
|
3397
3507
|
} catch (err) {
|
|
3398
|
-
log.warn(
|
|
3508
|
+
log.warn(TAG17, `Branch rename failed (continuing on ${branchName}): ${err instanceof Error ? err.message : err}`);
|
|
3399
3509
|
}
|
|
3400
3510
|
}
|
|
3401
3511
|
if (config.review.createPR && approvedBranch) {
|
|
@@ -3404,6 +3514,21 @@ ${runLogTail}
|
|
|
3404
3514
|
}
|
|
3405
3515
|
}
|
|
3406
3516
|
await addLabelByName(client, card, config.review.approvedLabel, config.review.approvedLabelColor);
|
|
3517
|
+
if (prUrl) {
|
|
3518
|
+
try {
|
|
3519
|
+
const { card: latest } = await client.getCard(card.id);
|
|
3520
|
+
const desc = latest.description || "";
|
|
3521
|
+
if (!extractPrUrl(desc)) {
|
|
3522
|
+
const separator = desc ? `
|
|
3523
|
+
` : "";
|
|
3524
|
+
await client.updateCard(card.id, {
|
|
3525
|
+
description: `${desc}${separator}PR: ${prUrl}`
|
|
3526
|
+
});
|
|
3527
|
+
}
|
|
3528
|
+
} catch (err) {
|
|
3529
|
+
log.warn(TAG17, `Failed to persist PR URL to #${card.short_id} description: ${err instanceof Error ? err.message : err}`);
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3407
3532
|
if (config.review.postFindings) {
|
|
3408
3533
|
const scopeLine = result.scopeCheck ? `Scope: ${result.scopeCheck.status}${result.scopeCheck.notes ? ` — ${result.scopeCheck.notes}` : ""}` : "";
|
|
3409
3534
|
const body = [
|
|
@@ -3422,14 +3547,14 @@ ${runLogTail}
|
|
|
3422
3547
|
progressPercent: 100,
|
|
3423
3548
|
...buildTokenPayload(sessionStats)
|
|
3424
3549
|
});
|
|
3425
|
-
log.info(
|
|
3550
|
+
log.info(TAG17, `#${card.short_id} approved${prUrl ? ` — PR: ${prUrl}` : ""} — labeled "${config.review.approvedLabel}"`);
|
|
3426
3551
|
} else {
|
|
3427
3552
|
const criticalFindings = result.findings.filter((f) => f.severity === "critical").slice(0, MAX_FINDINGS);
|
|
3428
3553
|
const majorFindings = result.findings.filter((f) => f.severity === "major").slice(0, MAX_FINDINGS);
|
|
3429
3554
|
const linkedFindings = [...criticalFindings, ...majorFindings];
|
|
3430
3555
|
const minorFindings = result.findings.filter((f) => f.severity === "minor").slice(0, MAX_FINDINGS);
|
|
3431
3556
|
if (currentCycle >= maxCycles) {
|
|
3432
|
-
log.warn(
|
|
3557
|
+
log.warn(TAG17, `#${card.short_id} reached max review cycles (${maxCycles}), moving to Done with note`);
|
|
3433
3558
|
await moveCardToColumn(client, card, config.review.moveToColumn);
|
|
3434
3559
|
const body = [
|
|
3435
3560
|
"**Review — needs human review.**",
|
|
@@ -3467,28 +3592,21 @@ ${runLogTail}
|
|
|
3467
3592
|
if (config.review.postFindings) {
|
|
3468
3593
|
await Promise.all(linkedFindings.map(async (finding) => {
|
|
3469
3594
|
try {
|
|
3470
|
-
|
|
3471
|
-
**Location:** ${finding.location}` : "";
|
|
3472
|
-
const newCard = await client.createCard(card.project_id, {
|
|
3473
|
-
title: `[Review: ${finding.severity}] ${finding.title}`,
|
|
3474
|
-
description: `Found during review of #${card.short_id} (${finding.severity}):
|
|
3475
|
-
|
|
3476
|
-
${finding.description}${locationLine}`
|
|
3477
|
-
});
|
|
3478
|
-
const newCardId = newCard?.card?.id;
|
|
3479
|
-
if (newCardId) {
|
|
3480
|
-
await client.addLinkToCard(card.id, newCardId, "relates_to");
|
|
3481
|
-
}
|
|
3595
|
+
await client.createSubtask(card.id, clampSubtaskTitle(`[${finding.severity}] ${finding.title}`));
|
|
3482
3596
|
} catch (err) {
|
|
3483
|
-
log.error(
|
|
3597
|
+
log.error(TAG17, `Failed to create finding subtask: ${err instanceof Error ? err.message : err}`);
|
|
3484
3598
|
}
|
|
3485
3599
|
}));
|
|
3600
|
+
if (linkedFindings.length > 0) {
|
|
3601
|
+
for (const body2 of buildFindingComments(linkedFindings)) {
|
|
3602
|
+
await postReviewComment(client, card, "finding", body2);
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3486
3605
|
await Promise.all(minorFindings.map(async (finding) => {
|
|
3487
3606
|
try {
|
|
3488
|
-
|
|
3489
|
-
await client.createSubtask(card.id, title);
|
|
3607
|
+
await client.createSubtask(card.id, clampSubtaskTitle(finding.title));
|
|
3490
3608
|
} catch (err) {
|
|
3491
|
-
log.error(
|
|
3609
|
+
log.error(TAG17, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
|
|
3492
3610
|
}
|
|
3493
3611
|
}));
|
|
3494
3612
|
const baseDesc = stripReviewSummary(freshDesc);
|
|
@@ -3496,7 +3614,7 @@ ${finding.description}${locationLine}`
|
|
|
3496
3614
|
try {
|
|
3497
3615
|
await client.updateCard(card.id, { description: updatedDesc });
|
|
3498
3616
|
} catch (err) {
|
|
3499
|
-
log.error(
|
|
3617
|
+
log.error(TAG17, `Failed to update review cycle marker: ${err instanceof Error ? err.message : err}`);
|
|
3500
3618
|
}
|
|
3501
3619
|
const scopeLine = result.scopeCheck ? `Scope: ${result.scopeCheck.status}${result.scopeCheck.notes ? ` — ${result.scopeCheck.notes}` : ""}` : "";
|
|
3502
3620
|
const body = [
|
|
@@ -3512,9 +3630,9 @@ ${finding.description}${locationLine}`
|
|
|
3512
3630
|
if (config.planning.enabled && card.plan_id) {
|
|
3513
3631
|
try {
|
|
3514
3632
|
await client.updateCard(card.id, { needsPlanRefresh: true });
|
|
3515
|
-
log.info(
|
|
3633
|
+
log.info(TAG17, `#${card.short_id} flagged needs_plan_refresh after rejected review`);
|
|
3516
3634
|
} catch (err) {
|
|
3517
|
-
log.warn(
|
|
3635
|
+
log.warn(TAG17, `Failed to flag needs_plan_refresh for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
3518
3636
|
}
|
|
3519
3637
|
}
|
|
3520
3638
|
await moveCardToColumn(client, card, config.review.failColumn);
|
|
@@ -3528,10 +3646,10 @@ ${finding.description}${locationLine}`
|
|
|
3528
3646
|
recoveryBranch
|
|
3529
3647
|
});
|
|
3530
3648
|
} catch (err) {
|
|
3531
|
-
log.debug(
|
|
3649
|
+
log.debug(TAG17, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
|
|
3532
3650
|
}
|
|
3533
3651
|
if (recoveryBranch) {
|
|
3534
|
-
log.info(
|
|
3652
|
+
log.info(TAG17, `#${card.short_id} recovery branch ${recoveryBranch}${recoveryUrl ? ` (${recoveryUrl})` : ""}`);
|
|
3535
3653
|
}
|
|
3536
3654
|
await client.endAgentSession(card.id, {
|
|
3537
3655
|
status: "failed",
|
|
@@ -3540,7 +3658,7 @@ ${finding.description}${locationLine}`
|
|
|
3540
3658
|
recoveryBranch,
|
|
3541
3659
|
...buildTokenPayload(sessionStats)
|
|
3542
3660
|
});
|
|
3543
|
-
log.info(
|
|
3661
|
+
log.info(TAG17, `#${card.short_id} rejected (cycle ${currentCycle}/${maxCycles}) — moved to "${config.review.failColumn}"`);
|
|
3544
3662
|
}
|
|
3545
3663
|
if (workspaceId && (result.verdict === "approved" || result.verdict === "rejected")) {
|
|
3546
3664
|
const originalEpisodeId = await findLatestImplementEpisode(client, workspaceId, card.project_id, card.short_id);
|
|
@@ -3562,7 +3680,7 @@ ${finding.description}${locationLine}`
|
|
|
3562
3680
|
cleanupWorktree(worktreePath, branchName);
|
|
3563
3681
|
}
|
|
3564
3682
|
}
|
|
3565
|
-
var
|
|
3683
|
+
var TAG17 = "review-completion", MAX_FINDINGS = 10, MAX_SUBTASK_TITLE = 120, COMMENT_BODY_BUDGET = 9500, REVIEW_MARKER = `---
|
|
3566
3684
|
**Review:`, RUN_LOG_TAIL_BYTES = 2048;
|
|
3567
3685
|
var init_review_completion = __esm(() => {
|
|
3568
3686
|
init_board_helpers();
|
|
@@ -3975,7 +4093,7 @@ class StateStore {
|
|
|
3975
4093
|
const raw = readFileSync3(this.path, "utf-8");
|
|
3976
4094
|
const parsed = JSON.parse(raw);
|
|
3977
4095
|
if (parsed?.version !== SCHEMA_VERSION) {
|
|
3978
|
-
log.warn(
|
|
4096
|
+
log.warn(TAG18, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — starting fresh`);
|
|
3979
4097
|
return emptyState();
|
|
3980
4098
|
}
|
|
3981
4099
|
return {
|
|
@@ -3988,7 +4106,7 @@ class StateStore {
|
|
|
3988
4106
|
daily: parsed.daily ?? []
|
|
3989
4107
|
};
|
|
3990
4108
|
} catch (err) {
|
|
3991
|
-
log.error(
|
|
4109
|
+
log.error(TAG18, `failed to read state file: ${err instanceof Error ? err.message : err}`);
|
|
3992
4110
|
return emptyState();
|
|
3993
4111
|
}
|
|
3994
4112
|
}
|
|
@@ -4144,7 +4262,7 @@ class StateStore {
|
|
|
4144
4262
|
return this.state.daily.find((d) => d.date === key)?.costCents ?? 0;
|
|
4145
4263
|
}
|
|
4146
4264
|
}
|
|
4147
|
-
var
|
|
4265
|
+
var TAG18 = "state-store", SCHEMA_VERSION = 1;
|
|
4148
4266
|
var init_state_store = __esm(() => {
|
|
4149
4267
|
init_log();
|
|
4150
4268
|
});
|
|
@@ -4171,7 +4289,7 @@ function normalizeToolResultContent(raw) {
|
|
|
4171
4289
|
return String(raw);
|
|
4172
4290
|
}
|
|
4173
4291
|
}
|
|
4174
|
-
var
|
|
4292
|
+
var TAG19 = "stream-parser", StreamParser;
|
|
4175
4293
|
var init_stream_parser = __esm(() => {
|
|
4176
4294
|
init_log();
|
|
4177
4295
|
StreamParser = class StreamParser extends EventEmitter {
|
|
@@ -4215,14 +4333,14 @@ var init_stream_parser = __esm(() => {
|
|
|
4215
4333
|
try {
|
|
4216
4334
|
msg = JSON.parse(line);
|
|
4217
4335
|
} catch {
|
|
4218
|
-
log.debug(
|
|
4336
|
+
log.debug(TAG19, `Non-JSON line: ${line.slice(0, 100)}`);
|
|
4219
4337
|
return;
|
|
4220
4338
|
}
|
|
4221
4339
|
try {
|
|
4222
4340
|
this.handleMessage(msg);
|
|
4223
4341
|
} catch (err) {
|
|
4224
4342
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
4225
|
-
log.warn(
|
|
4343
|
+
log.warn(TAG19, `Error handling stream event: ${errMsg}`);
|
|
4226
4344
|
this.emit("parse_error", errMsg);
|
|
4227
4345
|
}
|
|
4228
4346
|
}
|
|
@@ -4304,7 +4422,7 @@ async function withRetry(step, cardShortId, op, attempts, backoffMs) {
|
|
|
4304
4422
|
const msg2 = err instanceof Error ? err.message : String(err);
|
|
4305
4423
|
if (i < attempts - 1) {
|
|
4306
4424
|
const wait = backoffMs * 2 ** i;
|
|
4307
|
-
log.warn(
|
|
4425
|
+
log.warn(TAG20, `${step} failed for #${cardShortId} (attempt ${i + 1}/${attempts}): ${msg2} — retrying in ${wait}ms`);
|
|
4308
4426
|
await new Promise((r) => setTimeout(r, wait));
|
|
4309
4427
|
}
|
|
4310
4428
|
}
|
|
@@ -4326,10 +4444,10 @@ async function runTransition(client, card, plan, opts = {}) {
|
|
|
4326
4444
|
if (opts.strictColumn) {
|
|
4327
4445
|
throw new TransitionError("move", 1, msg);
|
|
4328
4446
|
}
|
|
4329
|
-
log.warn(
|
|
4447
|
+
log.warn(TAG20, `#${shortId}: ${msg} — skipping move`);
|
|
4330
4448
|
} else if (card.column_id !== target.id) {
|
|
4331
4449
|
await withRetry("move", shortId, () => client.moveCard(card.id, target.id), attempts, backoffMs);
|
|
4332
|
-
log.info(
|
|
4450
|
+
log.info(TAG20, `#${shortId} → "${target.name}"`);
|
|
4333
4451
|
card.column_id = target.id;
|
|
4334
4452
|
}
|
|
4335
4453
|
}
|
|
@@ -4342,7 +4460,7 @@ async function runTransition(client, card, plan, opts = {}) {
|
|
|
4342
4460
|
continue;
|
|
4343
4461
|
await withRetry("addLabel", shortId, () => client.addLabelToCard(card.id, labelId), attempts, backoffMs);
|
|
4344
4462
|
existing.add(labelId);
|
|
4345
|
-
log.info(
|
|
4463
|
+
log.info(TAG20, `#${shortId} +label "${name}"`);
|
|
4346
4464
|
}
|
|
4347
4465
|
card.labelIds = Array.from(existing);
|
|
4348
4466
|
}
|
|
@@ -4354,17 +4472,17 @@ async function runTransition(client, card, plan, opts = {}) {
|
|
|
4354
4472
|
continue;
|
|
4355
4473
|
await withRetry("removeLabel", shortId, () => client.removeLabelFromCard(card.id, match.id), attempts, backoffMs);
|
|
4356
4474
|
existing.delete(match.id);
|
|
4357
|
-
log.info(
|
|
4475
|
+
log.info(TAG20, `#${shortId} -label "${name}"`);
|
|
4358
4476
|
}
|
|
4359
4477
|
card.labelIds = Array.from(existing);
|
|
4360
4478
|
}
|
|
4361
4479
|
if (plan.updateCard) {
|
|
4362
4480
|
await withRetry("updateCard", shortId, () => client.updateCard(card.id, plan.updateCard), attempts, backoffMs);
|
|
4363
|
-
log.info(
|
|
4481
|
+
log.info(TAG20, `#${shortId} updated`);
|
|
4364
4482
|
}
|
|
4365
4483
|
if (plan.endSession) {
|
|
4366
4484
|
await withRetry("endSession", shortId, () => client.endAgentSession(card.id, plan.endSession), attempts, backoffMs);
|
|
4367
|
-
log.info(
|
|
4485
|
+
log.info(TAG20, `#${shortId} session ended (${plan.endSession.status})`);
|
|
4368
4486
|
}
|
|
4369
4487
|
if (opts.store && opts.runId) {
|
|
4370
4488
|
try {
|
|
@@ -4377,11 +4495,11 @@ async function ensureLabel(client, projectId, name, color, attempts, backoffMs)
|
|
|
4377
4495
|
const result = await withRetry("addLabel", 0, () => client.createLabel(projectId, { name, color: color ?? "#8b5cf6" }), attempts, backoffMs);
|
|
4378
4496
|
return result?.label?.id ?? null;
|
|
4379
4497
|
} catch (err) {
|
|
4380
|
-
log.warn(
|
|
4498
|
+
log.warn(TAG20, `ensureLabel "${name}" failed: ${err instanceof Error ? err.message : err}`);
|
|
4381
4499
|
return null;
|
|
4382
4500
|
}
|
|
4383
4501
|
}
|
|
4384
|
-
var
|
|
4502
|
+
var TAG20 = "transition", TransitionError;
|
|
4385
4503
|
var init_transitions = __esm(() => {
|
|
4386
4504
|
init_log();
|
|
4387
4505
|
TransitionError = class TransitionError extends Error {
|
|
@@ -4399,7 +4517,7 @@ var init_transitions = __esm(() => {
|
|
|
4399
4517
|
});
|
|
4400
4518
|
|
|
4401
4519
|
// src/review-worker.ts
|
|
4402
|
-
import { execFileSync as
|
|
4520
|
+
import { execFileSync as execFileSync10 } from "node:child_process";
|
|
4403
4521
|
|
|
4404
4522
|
class ReviewWorker {
|
|
4405
4523
|
config;
|
|
@@ -4465,7 +4583,7 @@ class ReviewWorker {
|
|
|
4465
4583
|
}
|
|
4466
4584
|
}
|
|
4467
4585
|
get tag() {
|
|
4468
|
-
return `${
|
|
4586
|
+
return `${TAG21}:${this.id}`;
|
|
4469
4587
|
}
|
|
4470
4588
|
get isIdle() {
|
|
4471
4589
|
return this.state === "idle";
|
|
@@ -4522,7 +4640,7 @@ class ReviewWorker {
|
|
|
4522
4640
|
let localDiff = null;
|
|
4523
4641
|
if (localMode) {
|
|
4524
4642
|
log.info(this.tag, `No branch found for #${card.short_id}, attempting local review`);
|
|
4525
|
-
this.worktreePath =
|
|
4643
|
+
this.worktreePath = execFileSync10("git", ["rev-parse", "--show-toplevel"], {
|
|
4526
4644
|
encoding: "utf-8",
|
|
4527
4645
|
timeout: 5000
|
|
4528
4646
|
}).trim();
|
|
@@ -4589,7 +4707,7 @@ class ReviewWorker {
|
|
|
4589
4707
|
if (localMode) {
|
|
4590
4708
|
diff = localDiff ?? "";
|
|
4591
4709
|
} else {
|
|
4592
|
-
diff =
|
|
4710
|
+
diff = execFileSync10("git", ["diff", `origin/${this.config.worktree.baseBranch}..HEAD`], { cwd, encoding: "utf-8", timeout: 30000 });
|
|
4593
4711
|
}
|
|
4594
4712
|
} catch {
|
|
4595
4713
|
diff = "(unable to retrieve diff)";
|
|
@@ -4869,7 +4987,7 @@ class ReviewWorker {
|
|
|
4869
4987
|
}
|
|
4870
4988
|
resolveLocalChanges(repoRoot, shortId) {
|
|
4871
4989
|
try {
|
|
4872
|
-
const localChanges =
|
|
4990
|
+
const localChanges = execFileSync10("git", ["diff", "HEAD"], {
|
|
4873
4991
|
cwd: repoRoot,
|
|
4874
4992
|
encoding: "utf-8",
|
|
4875
4993
|
timeout: 5000
|
|
@@ -4881,7 +4999,7 @@ class ReviewWorker {
|
|
|
4881
4999
|
log.warn(this.tag, "Failed to check uncommitted changes");
|
|
4882
5000
|
}
|
|
4883
5001
|
try {
|
|
4884
|
-
const matchingCommits =
|
|
5002
|
+
const matchingCommits = execFileSync10("git", ["log", "--format=%H", "-20", `--grep=#${shortId}`], { cwd: repoRoot, encoding: "utf-8", timeout: 1e4 }).trim();
|
|
4885
5003
|
if (matchingCommits) {
|
|
4886
5004
|
const hashes = matchingCommits.split(`
|
|
4887
5005
|
`).filter((h) => /^[0-9a-f]{4,40}$/i.test(h));
|
|
@@ -4891,7 +5009,7 @@ class ReviewWorker {
|
|
|
4891
5009
|
const diffs = [];
|
|
4892
5010
|
for (const hash of hashes) {
|
|
4893
5011
|
try {
|
|
4894
|
-
const commitDiff =
|
|
5012
|
+
const commitDiff = execFileSync10("git", ["diff", `${hash}~1..${hash}`], { cwd: repoRoot, encoding: "utf-8", timeout: 30000 });
|
|
4895
5013
|
if (commitDiff)
|
|
4896
5014
|
diffs.push(commitDiff);
|
|
4897
5015
|
} catch {
|
|
@@ -4934,7 +5052,7 @@ class ReviewWorker {
|
|
|
4934
5052
|
this.lastSessionStats = null;
|
|
4935
5053
|
}
|
|
4936
5054
|
}
|
|
4937
|
-
var
|
|
5055
|
+
var TAG21 = "review-worker", CANCEL_SIGINT_TIMEOUT = 30000, CANCEL_SIGTERM_TIMEOUT = 1e4;
|
|
4938
5056
|
var init_review_worker = __esm(() => {
|
|
4939
5057
|
init_board_helpers();
|
|
4940
5058
|
init_completion();
|
|
@@ -4983,7 +5101,7 @@ class SleepGuard {
|
|
|
4983
5101
|
if (!this.child.killed)
|
|
4984
5102
|
this.child.kill("SIGTERM");
|
|
4985
5103
|
this.child = null;
|
|
4986
|
-
log.info(
|
|
5104
|
+
log.info(TAG22, "sleep assertion released");
|
|
4987
5105
|
}
|
|
4988
5106
|
}
|
|
4989
5107
|
start() {
|
|
@@ -4998,7 +5116,7 @@ class SleepGuard {
|
|
|
4998
5116
|
spawned = true;
|
|
4999
5117
|
});
|
|
5000
5118
|
child.on("error", (err) => {
|
|
5001
|
-
log.warn(
|
|
5119
|
+
log.warn(TAG22, `caffeinate unavailable: ${err.message}`);
|
|
5002
5120
|
if (this.child === child)
|
|
5003
5121
|
this.child = null;
|
|
5004
5122
|
});
|
|
@@ -5011,13 +5129,13 @@ class SleepGuard {
|
|
|
5011
5129
|
});
|
|
5012
5130
|
child.unref();
|
|
5013
5131
|
this.child = child;
|
|
5014
|
-
log.info(
|
|
5132
|
+
log.info(TAG22, "sleep assertion acquired (caffeinate -i)");
|
|
5015
5133
|
} catch (err) {
|
|
5016
|
-
log.warn(
|
|
5134
|
+
log.warn(TAG22, `failed to spawn caffeinate: ${err instanceof Error ? err.message : err}`);
|
|
5017
5135
|
}
|
|
5018
5136
|
}
|
|
5019
5137
|
}
|
|
5020
|
-
var
|
|
5138
|
+
var TAG22 = "sleep-guard";
|
|
5021
5139
|
var init_sleep_guard = __esm(() => {
|
|
5022
5140
|
init_log();
|
|
5023
5141
|
});
|
|
@@ -5028,7 +5146,7 @@ async function fetchBlocksLinks(client, cardId) {
|
|
|
5028
5146
|
const { links } = await client.getCardLinks(cardId);
|
|
5029
5147
|
return links.filter((l) => l.link_type === "blocks");
|
|
5030
5148
|
} catch (err) {
|
|
5031
|
-
log.warn(
|
|
5149
|
+
log.warn(TAG23, `link fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
5032
5150
|
return null;
|
|
5033
5151
|
}
|
|
5034
5152
|
}
|
|
@@ -5060,27 +5178,27 @@ async function promoteUnblockedSuccessors(completedCard, deps) {
|
|
|
5060
5178
|
const successors = links.filter((l) => l.direction === "outgoing" && !l.target_card.done);
|
|
5061
5179
|
if (successors.length === 0)
|
|
5062
5180
|
return;
|
|
5063
|
-
log.info(
|
|
5181
|
+
log.info(TAG23, `#${completedCard.short_id} completed — checking ${successors.length} chained successor(s)`);
|
|
5064
5182
|
for (const link of successors) {
|
|
5065
5183
|
const successorId = link.target_card.id;
|
|
5066
5184
|
try {
|
|
5067
5185
|
const { card } = await deps.client.getCard(successorId);
|
|
5068
5186
|
if (card.assigned_agent_id === deps.agentId) {} else if (card.assigned_agent_id === null && !card.assignee_id) {
|
|
5069
|
-
log.info(
|
|
5187
|
+
log.info(TAG23, `successor #${card.short_id} unassigned — auto-assigning to continue chain`);
|
|
5070
5188
|
await deps.client.updateCard(successorId, {
|
|
5071
5189
|
assignedAgentId: deps.agentId
|
|
5072
5190
|
});
|
|
5073
5191
|
} else {
|
|
5074
|
-
log.debug(
|
|
5192
|
+
log.debug(TAG23, `successor #${card.short_id} assigned to different entity — skipping`);
|
|
5075
5193
|
continue;
|
|
5076
5194
|
}
|
|
5077
5195
|
await deps.enqueue(successorId);
|
|
5078
5196
|
} catch (err) {
|
|
5079
|
-
log.warn(
|
|
5197
|
+
log.warn(TAG23, `promotion failed for successor ${successorId}: ${err instanceof Error ? err.message : err}`);
|
|
5080
5198
|
}
|
|
5081
5199
|
}
|
|
5082
5200
|
}
|
|
5083
|
-
var
|
|
5201
|
+
var TAG23 = "unblock";
|
|
5084
5202
|
var init_unblock = __esm(() => {
|
|
5085
5203
|
init_log();
|
|
5086
5204
|
});
|
|
@@ -5135,11 +5253,11 @@ async function buildPrompt(enriched, branchName, worktreePath, client, workspace
|
|
|
5135
5253
|
Do NOT push to main. All your work stays on \`${branchName}\`.
|
|
5136
5254
|
When finished, call harmony_end_agent_session with status="completed".`
|
|
5137
5255
|
});
|
|
5138
|
-
log.info(
|
|
5256
|
+
log.info(TAG24, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
|
|
5139
5257
|
return result.prompt + pastEpisodesSection;
|
|
5140
5258
|
} catch (err) {
|
|
5141
5259
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5142
|
-
log.warn(
|
|
5260
|
+
log.warn(TAG24, `Failed to generate prompt via API, using fallback: ${msg}`);
|
|
5143
5261
|
const commentsSection = await renderCommentsSection(client, card.id);
|
|
5144
5262
|
return buildFallbackPrompt(enriched, branchName, worktreePath) + commentsSection + pastEpisodesSection;
|
|
5145
5263
|
}
|
|
@@ -5157,7 +5275,7 @@ async function renderCommentsSection(client, cardId) {
|
|
|
5157
5275
|
|
|
5158
5276
|
${section}` : "";
|
|
5159
5277
|
} catch (err) {
|
|
5160
|
-
log.warn(
|
|
5278
|
+
log.warn(TAG24, "comment-thread fetch failed", {
|
|
5161
5279
|
event: "comment_fetch_failed",
|
|
5162
5280
|
error: err instanceof Error ? err.message : String(err)
|
|
5163
5281
|
});
|
|
@@ -5207,7 +5325,7 @@ ${description}`.trim();
|
|
|
5207
5325
|
## Similar past tasks
|
|
5208
5326
|
${bullets}`;
|
|
5209
5327
|
} catch (err) {
|
|
5210
|
-
log.warn(
|
|
5328
|
+
log.warn(TAG24, "past-episodes recall failed", {
|
|
5211
5329
|
event: "episode_recall_failed",
|
|
5212
5330
|
error: err instanceof Error ? err.message : String(err)
|
|
5213
5331
|
});
|
|
@@ -5248,7 +5366,7 @@ ${subtaskStr}
|
|
|
5248
5366
|
You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
|
|
5249
5367
|
Do NOT push to main. All your work stays on \`${branchName}\`.`;
|
|
5250
5368
|
}
|
|
5251
|
-
var
|
|
5369
|
+
var TAG24 = "prompt";
|
|
5252
5370
|
var init_prompt = __esm(() => {
|
|
5253
5371
|
init_dist();
|
|
5254
5372
|
init_log();
|
|
@@ -5327,7 +5445,7 @@ class Worker {
|
|
|
5327
5445
|
}
|
|
5328
5446
|
}
|
|
5329
5447
|
get tag() {
|
|
5330
|
-
return `${
|
|
5448
|
+
return `${TAG25}:${this.id}`;
|
|
5331
5449
|
}
|
|
5332
5450
|
get isIdle() {
|
|
5333
5451
|
return this.state === "idle";
|
|
@@ -5386,7 +5504,7 @@ class Worker {
|
|
|
5386
5504
|
});
|
|
5387
5505
|
const sid = session && typeof session === "object" && "id" in session ? session.id : null;
|
|
5388
5506
|
if (!sid) {
|
|
5389
|
-
log.warn(
|
|
5507
|
+
log.warn(TAG25, "startAgentSession returned no session id");
|
|
5390
5508
|
}
|
|
5391
5509
|
this.sessionId = sid;
|
|
5392
5510
|
await this.recordPhase("preparing");
|
|
@@ -5462,6 +5580,8 @@ class Worker {
|
|
|
5462
5580
|
const rawStderr = err?.stderr;
|
|
5463
5581
|
const errClass = classifyRunError(typeof rawStderr === "string" && rawStderr ? rawStderr : msg);
|
|
5464
5582
|
const apiError = errClass.kind !== null;
|
|
5583
|
+
const baseError = err instanceof WorktreeBaseError;
|
|
5584
|
+
const noBudgetBurn = apiError || baseError;
|
|
5465
5585
|
if (apiError) {
|
|
5466
5586
|
try {
|
|
5467
5587
|
this.onApiError?.(errClass);
|
|
@@ -5476,7 +5596,7 @@ class Worker {
|
|
|
5476
5596
|
this.worktreePath = null;
|
|
5477
5597
|
}
|
|
5478
5598
|
const failureReason = apiError ? errClass.kind : "other";
|
|
5479
|
-
const failureSummary = apiError ? describeApiError(errClass.kind) : `Run failed: ${msg.slice(0, 300)}`;
|
|
5599
|
+
const failureSummary = apiError ? describeApiError(errClass.kind) : baseError ? "Could not fetch current base branch — requeued without counting an attempt" : `Run failed: ${msg.slice(0, 300)}`;
|
|
5480
5600
|
try {
|
|
5481
5601
|
await runTransition(this.client, card, {
|
|
5482
5602
|
move: { columnName: this.config.pickupColumns[0] ?? "To Do" },
|
|
@@ -5497,7 +5617,7 @@ class Worker {
|
|
|
5497
5617
|
...this.runLedger()
|
|
5498
5618
|
});
|
|
5499
5619
|
} catch {}
|
|
5500
|
-
if (
|
|
5620
|
+
if (noBudgetBurn) {
|
|
5501
5621
|
await this.stateStore.decrementAttempt(card.id);
|
|
5502
5622
|
} else {
|
|
5503
5623
|
await this.recordOutcome(card.id, "failure");
|
|
@@ -5894,7 +6014,7 @@ class Worker {
|
|
|
5894
6014
|
this.runTurns = 0;
|
|
5895
6015
|
}
|
|
5896
6016
|
}
|
|
5897
|
-
var
|
|
6017
|
+
var TAG25 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4, PLAN_ALLOWED_TOOLS = "Read,Grep,Glob,mcp__harmony__*", IMPLEMENT_ALLOWED_TOOLS = "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*", PLAN_PHASE_TIMEOUT;
|
|
5898
6018
|
var init_worker = __esm(() => {
|
|
5899
6019
|
init_board_helpers();
|
|
5900
6020
|
init_completion();
|
|
@@ -5964,39 +6084,39 @@ class Pool {
|
|
|
5964
6084
|
}
|
|
5965
6085
|
async enqueue(card, column, labels, subtasks, mode = "implement") {
|
|
5966
6086
|
if (this.implQueue.has(card.id) || this.reviewQueue.has(card.id) || this.isCardActive(card.id)) {
|
|
5967
|
-
log.debug(
|
|
6087
|
+
log.debug(TAG26, `Card ${card.id} already queued or active, skipping`);
|
|
5968
6088
|
return;
|
|
5969
6089
|
}
|
|
5970
6090
|
if (mode === "implement") {
|
|
5971
6091
|
if (this.authPaused) {
|
|
5972
|
-
log.debug(
|
|
6092
|
+
log.debug(TAG26, `#${card.short_id} held — agent paused (auth error)`);
|
|
5973
6093
|
await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
|
|
5974
6094
|
return;
|
|
5975
6095
|
}
|
|
5976
6096
|
const cooldownMs = this.apiCooldownRemainingMs();
|
|
5977
6097
|
if (cooldownMs > 0) {
|
|
5978
|
-
log.debug(
|
|
6098
|
+
log.debug(TAG26, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
|
|
5979
6099
|
await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
|
|
5980
6100
|
return;
|
|
5981
6101
|
}
|
|
5982
6102
|
const decision = this.budget.check(card.id);
|
|
5983
6103
|
if (!decision.allow) {
|
|
5984
6104
|
if (decision.reason === "daily_budget") {
|
|
5985
|
-
log.warn(
|
|
6105
|
+
log.warn(TAG26, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
|
|
5986
6106
|
await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
|
|
5987
6107
|
} else {
|
|
5988
|
-
log.debug(
|
|
6108
|
+
log.debug(TAG26, `#${card.short_id} gave up: ${decision.detail}`);
|
|
5989
6109
|
}
|
|
5990
6110
|
return;
|
|
5991
6111
|
}
|
|
5992
6112
|
const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
|
|
5993
6113
|
if (blockers === null) {
|
|
5994
|
-
log.warn(
|
|
6114
|
+
log.warn(TAG26, `#${card.short_id} blocker check failed — deferring to next tick`);
|
|
5995
6115
|
return;
|
|
5996
6116
|
}
|
|
5997
6117
|
if (blockers.length > 0) {
|
|
5998
6118
|
const list = blockers.map((b) => `#${b.shortId}`).join(", ");
|
|
5999
|
-
log.info(
|
|
6119
|
+
log.info(TAG26, `#${card.short_id} blocked by ${list} — waiting`);
|
|
6000
6120
|
await this.emitWaiting(card.id, `Blocked by ${list} — waiting for chain`);
|
|
6001
6121
|
return;
|
|
6002
6122
|
}
|
|
@@ -6025,7 +6145,7 @@ class Pool {
|
|
|
6025
6145
|
});
|
|
6026
6146
|
this.lastWaitingEmit.set(cardId, currentTask);
|
|
6027
6147
|
} catch (err) {
|
|
6028
|
-
log.debug(
|
|
6148
|
+
log.debug(TAG26, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
6029
6149
|
}
|
|
6030
6150
|
}
|
|
6031
6151
|
noteApiError(err) {
|
|
@@ -6033,7 +6153,7 @@ class Pool {
|
|
|
6033
6153
|
return;
|
|
6034
6154
|
if (err.kind === "auth") {
|
|
6035
6155
|
if (!this.authPaused) {
|
|
6036
|
-
log.error(
|
|
6156
|
+
log.error(TAG26, "Auth error from Claude CLI — pausing implement pickups until the daemon is restarted with valid credentials");
|
|
6037
6157
|
}
|
|
6038
6158
|
this.authPaused = true;
|
|
6039
6159
|
return;
|
|
@@ -6042,7 +6162,7 @@ class Pool {
|
|
|
6042
6162
|
const until = Date.now() + cooldownMs;
|
|
6043
6163
|
if (until > this.apiCooldownUntil) {
|
|
6044
6164
|
this.apiCooldownUntil = until;
|
|
6045
|
-
log.warn(
|
|
6165
|
+
log.warn(TAG26, `${describeApiError(err.kind)} — pausing implement pickups for ${Math.round(cooldownMs / 1000)}s`);
|
|
6046
6166
|
}
|
|
6047
6167
|
}
|
|
6048
6168
|
apiCooldownRemainingMs() {
|
|
@@ -6055,13 +6175,13 @@ class Pool {
|
|
|
6055
6175
|
const removed = queue.remove(cardId);
|
|
6056
6176
|
if (removed) {
|
|
6057
6177
|
this.cardDataCache.delete(cardId);
|
|
6058
|
-
log.info(
|
|
6178
|
+
log.info(TAG26, `Removed #${removed.shortId} from ${removed.mode} queue`);
|
|
6059
6179
|
return;
|
|
6060
6180
|
}
|
|
6061
6181
|
}
|
|
6062
6182
|
const worker = this.implWorkers.find((w) => w.cardId === cardId) ?? this.reviewWorkers.find((w) => w.cardId === cardId);
|
|
6063
6183
|
if (worker) {
|
|
6064
|
-
log.info(
|
|
6184
|
+
log.info(TAG26, `Cancelling worker ${worker.id} for card ${cardId}`);
|
|
6065
6185
|
await worker.cancel();
|
|
6066
6186
|
}
|
|
6067
6187
|
}
|
|
@@ -6094,10 +6214,10 @@ class Pool {
|
|
|
6094
6214
|
async handleAgentCommand(cardId, command) {
|
|
6095
6215
|
const worker = this.implWorkers.find((w) => w.cardId === cardId && w.isActive) ?? this.reviewWorkers.find((w) => w.cardId === cardId && w.isActive);
|
|
6096
6216
|
if (!worker) {
|
|
6097
|
-
log.debug(
|
|
6217
|
+
log.debug(TAG26, `No active worker for card ${cardId}, ignoring ${command}`);
|
|
6098
6218
|
return;
|
|
6099
6219
|
}
|
|
6100
|
-
log.info(
|
|
6220
|
+
log.info(TAG26, `Agent command: ${command} → worker ${worker.id} (card ${cardId})`);
|
|
6101
6221
|
switch (command) {
|
|
6102
6222
|
case "pause":
|
|
6103
6223
|
await worker.pause();
|
|
@@ -6145,7 +6265,7 @@ class Pool {
|
|
|
6145
6265
|
};
|
|
6146
6266
|
}
|
|
6147
6267
|
async shutdown() {
|
|
6148
|
-
log.info(
|
|
6268
|
+
log.info(TAG26, "Shutting down pool...");
|
|
6149
6269
|
this.shuttingDown = true;
|
|
6150
6270
|
const active = [
|
|
6151
6271
|
...this.implWorkers.filter((w) => w.isActive),
|
|
@@ -6153,7 +6273,7 @@ class Pool {
|
|
|
6153
6273
|
];
|
|
6154
6274
|
await Promise.all(active.map((w) => w.cancel()));
|
|
6155
6275
|
this.sleepGuard.stop();
|
|
6156
|
-
log.info(
|
|
6276
|
+
log.info(TAG26, "Pool shutdown complete");
|
|
6157
6277
|
}
|
|
6158
6278
|
cardDataCache = new Map;
|
|
6159
6279
|
tryDispatchFor(workers, queue, label) {
|
|
@@ -6161,7 +6281,7 @@ class Pool {
|
|
|
6161
6281
|
return false;
|
|
6162
6282
|
const idle = workers.find((w) => w.isIdle);
|
|
6163
6283
|
if (!idle) {
|
|
6164
|
-
log.debug(
|
|
6284
|
+
log.debug(TAG26, `No idle ${label} workers (queue: ${queue.length})`);
|
|
6165
6285
|
return false;
|
|
6166
6286
|
}
|
|
6167
6287
|
const next = queue.dequeue();
|
|
@@ -6169,18 +6289,18 @@ class Pool {
|
|
|
6169
6289
|
return false;
|
|
6170
6290
|
const data = this.cardDataCache.get(next.cardId);
|
|
6171
6291
|
if (!data) {
|
|
6172
|
-
log.warn(
|
|
6292
|
+
log.warn(TAG26, `No cached data for card ${next.cardId}, skipping`);
|
|
6173
6293
|
return false;
|
|
6174
6294
|
}
|
|
6175
6295
|
this.cardDataCache.delete(next.cardId);
|
|
6176
6296
|
this.lastWaitingEmit.delete(next.cardId);
|
|
6177
|
-
log.info(
|
|
6297
|
+
log.info(TAG26, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
|
|
6178
6298
|
this.sleepGuard.acquire();
|
|
6179
6299
|
idle.run(data.card, data.column, data.labels, data.subtasks);
|
|
6180
6300
|
return true;
|
|
6181
6301
|
}
|
|
6182
6302
|
}
|
|
6183
|
-
var
|
|
6303
|
+
var TAG26 = "pool";
|
|
6184
6304
|
var init_pool = __esm(() => {
|
|
6185
6305
|
init_error_classifier();
|
|
6186
6306
|
init_log();
|
|
@@ -6222,7 +6342,7 @@ function load(path) {
|
|
|
6222
6342
|
return parsed;
|
|
6223
6343
|
return {};
|
|
6224
6344
|
} catch (err) {
|
|
6225
|
-
log.warn(
|
|
6345
|
+
log.warn(TAG27, `failed to read ${path}: ${err instanceof Error ? err.message : err}`);
|
|
6226
6346
|
return {};
|
|
6227
6347
|
}
|
|
6228
6348
|
}
|
|
@@ -6240,7 +6360,7 @@ function recordDaemonPort(projectId, entry, path = defaultRegistryPath()) {
|
|
|
6240
6360
|
registry[projectId] = { ...entry, updatedAt: Date.now() };
|
|
6241
6361
|
save(path, registry);
|
|
6242
6362
|
} catch (err) {
|
|
6243
|
-
log.warn(
|
|
6363
|
+
log.warn(TAG27, `failed to record port for ${projectId}: ${err instanceof Error ? err.message : err}`);
|
|
6244
6364
|
}
|
|
6245
6365
|
}
|
|
6246
6366
|
function lookupDaemonPort(projectId, path = defaultRegistryPath()) {
|
|
@@ -6256,10 +6376,10 @@ function clearDaemonPort(projectId, pid, path = defaultRegistryPath()) {
|
|
|
6256
6376
|
delete registry[projectId];
|
|
6257
6377
|
save(path, registry);
|
|
6258
6378
|
} catch (err) {
|
|
6259
|
-
log.warn(
|
|
6379
|
+
log.warn(TAG27, `failed to clear port for ${projectId}: ${err instanceof Error ? err.message : err}`);
|
|
6260
6380
|
}
|
|
6261
6381
|
}
|
|
6262
|
-
var
|
|
6382
|
+
var TAG27 = "port-registry";
|
|
6263
6383
|
var init_port_registry = __esm(() => {
|
|
6264
6384
|
init_log();
|
|
6265
6385
|
});
|
|
@@ -6280,7 +6400,7 @@ async function fetchCardSafely(client, cardId) {
|
|
|
6280
6400
|
const { card } = await client.getCard(cardId);
|
|
6281
6401
|
return card;
|
|
6282
6402
|
} catch (err) {
|
|
6283
|
-
log.warn(
|
|
6403
|
+
log.warn(TAG28, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
6284
6404
|
return null;
|
|
6285
6405
|
}
|
|
6286
6406
|
}
|
|
@@ -6290,7 +6410,7 @@ async function recoverOrphans(store, client, config) {
|
|
|
6290
6410
|
return [];
|
|
6291
6411
|
}
|
|
6292
6412
|
const outcomes = [];
|
|
6293
|
-
log.info(
|
|
6413
|
+
log.info(TAG28, `recovering ${active.length} orphan run(s) from prior daemon`);
|
|
6294
6414
|
for (const run of active) {
|
|
6295
6415
|
const outcome = {
|
|
6296
6416
|
runId: run.runId,
|
|
@@ -6302,11 +6422,11 @@ async function recoverOrphans(store, client, config) {
|
|
|
6302
6422
|
};
|
|
6303
6423
|
outcomes.push(outcome);
|
|
6304
6424
|
if (isProcessAlive(run.daemonPid, process.pid)) {
|
|
6305
|
-
log.warn(
|
|
6425
|
+
log.warn(TAG28, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
|
|
6306
6426
|
outcome.actions.push("skipped: daemon pid still alive");
|
|
6307
6427
|
continue;
|
|
6308
6428
|
}
|
|
6309
|
-
log.info(
|
|
6429
|
+
log.info(TAG28, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
|
|
6310
6430
|
await recoverRun(run, store, client, config, outcome);
|
|
6311
6431
|
}
|
|
6312
6432
|
return outcomes;
|
|
@@ -6324,7 +6444,7 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
6324
6444
|
} catch (err) {
|
|
6325
6445
|
const msg = err instanceof Error ? err.message : String(err);
|
|
6326
6446
|
outcome.errors.push(`endAgentSession: ${msg}`);
|
|
6327
|
-
log.warn(
|
|
6447
|
+
log.warn(TAG28, `endAgentSession failed for ${run.cardId}: ${msg}`);
|
|
6328
6448
|
}
|
|
6329
6449
|
const card = await fetchCardSafely(client, run.cardId);
|
|
6330
6450
|
if (card) {
|
|
@@ -6367,9 +6487,9 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
6367
6487
|
const msg = err instanceof Error ? err.message : String(err);
|
|
6368
6488
|
outcome.errors.push(`endRun: ${msg}`);
|
|
6369
6489
|
}
|
|
6370
|
-
log.info(
|
|
6490
|
+
log.info(TAG28, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
|
|
6371
6491
|
}
|
|
6372
|
-
var
|
|
6492
|
+
var TAG28 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
|
|
6373
6493
|
var init_recovery = __esm(() => {
|
|
6374
6494
|
init_board_helpers();
|
|
6375
6495
|
init_log();
|
|
@@ -6417,7 +6537,7 @@ class Reconciler {
|
|
|
6417
6537
|
clearInterval(this.timer);
|
|
6418
6538
|
this.timer = null;
|
|
6419
6539
|
}
|
|
6420
|
-
log.info(
|
|
6540
|
+
log.info(TAG29, "Heartbeat stopped");
|
|
6421
6541
|
}
|
|
6422
6542
|
async recoverStaleRuns() {
|
|
6423
6543
|
if (!this.stateStore || !this.agentConfig)
|
|
@@ -6434,7 +6554,7 @@ class Reconciler {
|
|
|
6434
6554
|
if (!daemonDead && !(heartbeatStale && ourZombie))
|
|
6435
6555
|
continue;
|
|
6436
6556
|
const reason = daemonDead ? `foreign daemon ${run.daemonPid} is dead` : `our worker lost card ${run.cardId} with ${Math.round((now - run.lastHeartbeatAt) / 1000)}s stale heartbeat`;
|
|
6437
|
-
log.warn(
|
|
6557
|
+
log.warn(TAG29, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
|
|
6438
6558
|
await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
|
|
6439
6559
|
runId: run.runId,
|
|
6440
6560
|
cardId: run.cardId,
|
|
@@ -6461,11 +6581,11 @@ class Reconciler {
|
|
|
6461
6581
|
const stalledAt = Date.parse(card.updated_at ?? "");
|
|
6462
6582
|
if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
|
|
6463
6583
|
continue;
|
|
6464
|
-
log.warn(
|
|
6584
|
+
log.warn(TAG29, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
|
|
6465
6585
|
try {
|
|
6466
6586
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
6467
6587
|
} catch (err) {
|
|
6468
|
-
log.error(
|
|
6588
|
+
log.error(TAG29, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
6469
6589
|
}
|
|
6470
6590
|
}
|
|
6471
6591
|
}
|
|
@@ -6488,11 +6608,11 @@ class Reconciler {
|
|
|
6488
6608
|
const parkedAt = Date.parse(card.updated_at ?? "");
|
|
6489
6609
|
if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
|
|
6490
6610
|
continue;
|
|
6491
|
-
log.warn(
|
|
6611
|
+
log.warn(TAG29, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
|
|
6492
6612
|
try {
|
|
6493
6613
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
6494
6614
|
} catch (err) {
|
|
6495
|
-
log.error(
|
|
6615
|
+
log.error(TAG29, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
6496
6616
|
}
|
|
6497
6617
|
}
|
|
6498
6618
|
}
|
|
@@ -6521,18 +6641,18 @@ class Reconciler {
|
|
|
6521
6641
|
const subtasks = card.subtasks ?? [];
|
|
6522
6642
|
const mode = reviewColumnIds.has(card.column_id) ? "review" : "implement";
|
|
6523
6643
|
if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
|
|
6524
|
-
log.debug(
|
|
6644
|
+
log.debug(TAG29, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
|
|
6525
6645
|
continue;
|
|
6526
6646
|
}
|
|
6527
6647
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
6528
|
-
log.debug(
|
|
6648
|
+
log.debug(TAG29, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
|
|
6529
6649
|
continue;
|
|
6530
6650
|
}
|
|
6531
6651
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
6532
|
-
log.debug(
|
|
6652
|
+
log.debug(TAG29, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
|
|
6533
6653
|
continue;
|
|
6534
6654
|
}
|
|
6535
|
-
log.info(
|
|
6655
|
+
log.info(TAG29, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
|
|
6536
6656
|
await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
6537
6657
|
}
|
|
6538
6658
|
}
|
|
@@ -6542,18 +6662,18 @@ class Reconciler {
|
|
|
6542
6662
|
await this.recoverStrandedInProgress(cards, columns, knownCardIds);
|
|
6543
6663
|
for (const knownId of knownCardIds) {
|
|
6544
6664
|
if (!allAgentCardIds.has(knownId)) {
|
|
6545
|
-
log.info(
|
|
6665
|
+
log.info(TAG29, `Missed unassign: ${knownId} — removing`);
|
|
6546
6666
|
await this.pool.removeCard(knownId);
|
|
6547
6667
|
}
|
|
6548
6668
|
}
|
|
6549
6669
|
await this.releaseStalledApprovals(cards, columns, knownCardIds);
|
|
6550
|
-
log.debug(
|
|
6670
|
+
log.debug(TAG29, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
|
|
6551
6671
|
} catch (err) {
|
|
6552
|
-
log.error(
|
|
6672
|
+
log.error(TAG29, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
|
|
6553
6673
|
}
|
|
6554
6674
|
}
|
|
6555
6675
|
}
|
|
6556
|
-
var
|
|
6676
|
+
var TAG29 = "reconcile";
|
|
6557
6677
|
var init_reconcile = __esm(() => {
|
|
6558
6678
|
init_board_helpers();
|
|
6559
6679
|
init_log();
|
|
@@ -6591,7 +6711,7 @@ function prettyBanner(config, version) {
|
|
|
6591
6711
|
checks.push({ kind: "ok", message });
|
|
6592
6712
|
},
|
|
6593
6713
|
warn(message) {
|
|
6594
|
-
log.warn(
|
|
6714
|
+
log.warn(TAG30, message);
|
|
6595
6715
|
checks.push({ kind: "warn", message: message.split(`
|
|
6596
6716
|
`, 1)[0] });
|
|
6597
6717
|
},
|
|
@@ -6616,25 +6736,25 @@ function prettyBanner(config, version) {
|
|
|
6616
6736
|
};
|
|
6617
6737
|
}
|
|
6618
6738
|
function jsonBanner(config, version) {
|
|
6619
|
-
log.info(
|
|
6620
|
-
log.info(
|
|
6739
|
+
log.info(TAG30, `Harmony Agent Daemon v${version} starting...`);
|
|
6740
|
+
log.info(TAG30, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
|
|
6621
6741
|
if (config.agent.review.enabled) {
|
|
6622
|
-
log.info(
|
|
6742
|
+
log.info(TAG30, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
|
|
6623
6743
|
}
|
|
6624
6744
|
let failed = false;
|
|
6625
6745
|
return {
|
|
6626
6746
|
setProjectName(_name) {},
|
|
6627
6747
|
setGitProvider(provider) {
|
|
6628
|
-
log.info(
|
|
6748
|
+
log.info(TAG30, `Git provider: ${provider}`);
|
|
6629
6749
|
},
|
|
6630
6750
|
setHttpPort(port) {
|
|
6631
|
-
log.info(
|
|
6751
|
+
log.info(TAG30, `HTTP server on port ${port}`);
|
|
6632
6752
|
},
|
|
6633
6753
|
check(message) {
|
|
6634
|
-
log.info(
|
|
6754
|
+
log.info(TAG30, message);
|
|
6635
6755
|
},
|
|
6636
6756
|
warn(message) {
|
|
6637
|
-
log.warn(
|
|
6757
|
+
log.warn(TAG30, message);
|
|
6638
6758
|
},
|
|
6639
6759
|
fail() {
|
|
6640
6760
|
failed = true;
|
|
@@ -6642,7 +6762,7 @@ function jsonBanner(config, version) {
|
|
|
6642
6762
|
async ready(message) {
|
|
6643
6763
|
if (failed)
|
|
6644
6764
|
return;
|
|
6645
|
-
log.info(
|
|
6765
|
+
log.info(TAG30, message);
|
|
6646
6766
|
}
|
|
6647
6767
|
};
|
|
6648
6768
|
}
|
|
@@ -6719,7 +6839,7 @@ function cyan(s) {
|
|
|
6719
6839
|
function yellow(s) {
|
|
6720
6840
|
return `${ANSI.yellow}${s}${ANSI.reset}`;
|
|
6721
6841
|
}
|
|
6722
|
-
var
|
|
6842
|
+
var TAG30 = "daemon", RULE_WIDTH = 70, ANSI;
|
|
6723
6843
|
var init_startup_banner = __esm(() => {
|
|
6724
6844
|
init_log();
|
|
6725
6845
|
ANSI = {
|
|
@@ -6866,18 +6986,18 @@ class Watcher {
|
|
|
6866
6986
|
}
|
|
6867
6987
|
async start() {
|
|
6868
6988
|
if (!isPretty()) {
|
|
6869
|
-
log.info(
|
|
6989
|
+
log.info(TAG31, "Connecting to Supabase realtime (broadcast)...");
|
|
6870
6990
|
}
|
|
6871
6991
|
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
6872
6992
|
const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
|
|
6873
6993
|
const channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
|
|
6874
|
-
log.debug(
|
|
6994
|
+
log.debug(TAG31, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
|
|
6875
6995
|
this.onCardBroadcast({
|
|
6876
6996
|
event: "card_update",
|
|
6877
6997
|
payload: msg.payload ?? {}
|
|
6878
6998
|
});
|
|
6879
6999
|
}).on("broadcast", { event: "card_created" }, (msg) => {
|
|
6880
|
-
log.debug(
|
|
7000
|
+
log.debug(TAG31, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
|
|
6881
7001
|
this.onCardBroadcast({
|
|
6882
7002
|
event: "card_created",
|
|
6883
7003
|
payload: msg.payload ?? {}
|
|
@@ -6887,29 +7007,29 @@ class Watcher {
|
|
|
6887
7007
|
const cardId = payload.card_id;
|
|
6888
7008
|
const command = payload.command;
|
|
6889
7009
|
if (cardId && command) {
|
|
6890
|
-
log.info(
|
|
7010
|
+
log.info(TAG31, `Broadcast: agent_command ${command} for ${cardId}`);
|
|
6891
7011
|
this.onAgentCommand?.({ cardId, command });
|
|
6892
7012
|
}
|
|
6893
7013
|
}).subscribe((status) => {
|
|
6894
7014
|
if (status === "SUBSCRIBED") {
|
|
6895
7015
|
this.connected = true;
|
|
6896
7016
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
6897
|
-
log.info(
|
|
7017
|
+
log.info(TAG31, "Broadcast subscription active");
|
|
6898
7018
|
}
|
|
6899
7019
|
this.maybeResolveReady();
|
|
6900
7020
|
} else if (status === "CHANNEL_ERROR") {
|
|
6901
7021
|
this.connected = false;
|
|
6902
|
-
log.error(
|
|
7022
|
+
log.error(TAG31, "Broadcast channel error — will rely on reconciliation");
|
|
6903
7023
|
} else if (status === "TIMED_OUT") {
|
|
6904
7024
|
this.connected = false;
|
|
6905
|
-
log.warn(
|
|
7025
|
+
log.warn(TAG31, "Broadcast subscription timed out — retrying...");
|
|
6906
7026
|
} else if (status === "CLOSED") {
|
|
6907
7027
|
this.connected = false;
|
|
6908
7028
|
}
|
|
6909
7029
|
});
|
|
6910
7030
|
this.channel = channel;
|
|
6911
7031
|
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
6912
|
-
log.debug(
|
|
7032
|
+
log.debug(TAG31, "Presence sync");
|
|
6913
7033
|
}).subscribe(async (status) => {
|
|
6914
7034
|
if (status === "SUBSCRIBED") {
|
|
6915
7035
|
await presenceChannel.track({
|
|
@@ -6922,7 +7042,7 @@ class Watcher {
|
|
|
6922
7042
|
agentName: this.identity.agentName
|
|
6923
7043
|
});
|
|
6924
7044
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
6925
|
-
log.info(
|
|
7045
|
+
log.info(TAG31, "Presence tracked on board-presence channel");
|
|
6926
7046
|
}
|
|
6927
7047
|
this.presenceTracked = true;
|
|
6928
7048
|
this.maybeResolveReady();
|
|
@@ -6944,10 +7064,10 @@ class Watcher {
|
|
|
6944
7064
|
this.supabase = null;
|
|
6945
7065
|
}
|
|
6946
7066
|
this.connected = false;
|
|
6947
|
-
log.info(
|
|
7067
|
+
log.info(TAG31, "Broadcast subscription stopped");
|
|
6948
7068
|
}
|
|
6949
7069
|
}
|
|
6950
|
-
var
|
|
7070
|
+
var TAG31 = "watcher";
|
|
6951
7071
|
var init_watcher = __esm(() => {
|
|
6952
7072
|
init_log();
|
|
6953
7073
|
});
|
|
@@ -6960,7 +7080,7 @@ __export(exports_worktree_gc, {
|
|
|
6960
7080
|
isTransientGitNetworkError: () => isTransientGitNetworkError,
|
|
6961
7081
|
WorktreeGc: () => WorktreeGc
|
|
6962
7082
|
});
|
|
6963
|
-
import { execFileSync as
|
|
7083
|
+
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
6964
7084
|
import { readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
6965
7085
|
import { resolve as resolve3 } from "node:path";
|
|
6966
7086
|
function isTransientGitNetworkError(message) {
|
|
@@ -7028,16 +7148,16 @@ function runWorktreeGc(basePath, store, opts = {}) {
|
|
|
7028
7148
|
}
|
|
7029
7149
|
}
|
|
7030
7150
|
try {
|
|
7031
|
-
|
|
7151
|
+
execFileSync11("git", ["worktree", "prune", "--expire=now"], {
|
|
7032
7152
|
cwd: repoRoot,
|
|
7033
7153
|
stdio: "pipe"
|
|
7034
7154
|
});
|
|
7035
7155
|
} catch {}
|
|
7036
7156
|
if (result.removed.length > 0) {
|
|
7037
|
-
log.info(
|
|
7157
|
+
log.info(TAG32, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
|
|
7038
7158
|
}
|
|
7039
7159
|
if (result.errors.length > 0) {
|
|
7040
|
-
log.warn(
|
|
7160
|
+
log.warn(TAG32, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
|
|
7041
7161
|
}
|
|
7042
7162
|
return result;
|
|
7043
7163
|
}
|
|
@@ -7059,7 +7179,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
7059
7179
|
return result;
|
|
7060
7180
|
}
|
|
7061
7181
|
try {
|
|
7062
|
-
|
|
7182
|
+
execFileSync11("git", ["fetch", "--prune", "origin"], {
|
|
7063
7183
|
cwd: repoRoot,
|
|
7064
7184
|
stdio: "pipe",
|
|
7065
7185
|
...GIT_NETWORK_EXEC
|
|
@@ -7067,7 +7187,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
7067
7187
|
} catch (err) {
|
|
7068
7188
|
const detail = gitErrorDetail(err);
|
|
7069
7189
|
if (isTransientGitNetworkError(detail)) {
|
|
7070
|
-
log.debug(
|
|
7190
|
+
log.debug(TAG32, `Remote branch GC skipped — remote unreachable: ${detail}`);
|
|
7071
7191
|
return result;
|
|
7072
7192
|
}
|
|
7073
7193
|
result.errors.push({ ref: "fetch", error: detail });
|
|
@@ -7075,7 +7195,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
7075
7195
|
const refPattern = `refs/remotes/origin/${opts.prefix}*`;
|
|
7076
7196
|
let listing = "";
|
|
7077
7197
|
try {
|
|
7078
|
-
listing =
|
|
7198
|
+
listing = execFileSync11("git", [
|
|
7079
7199
|
"for-each-ref",
|
|
7080
7200
|
"--format=%(refname:strip=3) %(committerdate:unix)",
|
|
7081
7201
|
refPattern
|
|
@@ -7106,11 +7226,11 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
7106
7226
|
continue;
|
|
7107
7227
|
}
|
|
7108
7228
|
if (clock() > sweepDeadline) {
|
|
7109
|
-
log.debug(
|
|
7229
|
+
log.debug(TAG32, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
|
|
7110
7230
|
break;
|
|
7111
7231
|
}
|
|
7112
7232
|
try {
|
|
7113
|
-
|
|
7233
|
+
execFileSync11("git", ["push", "origin", `:refs/heads/${ref}`], {
|
|
7114
7234
|
cwd: repoRoot,
|
|
7115
7235
|
stdio: "pipe",
|
|
7116
7236
|
...GIT_NETWORK_EXEC
|
|
@@ -7119,17 +7239,17 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
7119
7239
|
} catch (err) {
|
|
7120
7240
|
const detail = gitErrorDetail(err);
|
|
7121
7241
|
if (isTransientGitNetworkError(detail)) {
|
|
7122
|
-
log.debug(
|
|
7242
|
+
log.debug(TAG32, `Remote branch GC interrupted — remote unreachable: ${detail}`);
|
|
7123
7243
|
break;
|
|
7124
7244
|
}
|
|
7125
7245
|
result.errors.push({ ref, error: detail });
|
|
7126
7246
|
}
|
|
7127
7247
|
}
|
|
7128
7248
|
if (result.removed.length > 0) {
|
|
7129
|
-
log.info(
|
|
7249
|
+
log.info(TAG32, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
|
|
7130
7250
|
}
|
|
7131
7251
|
if (result.errors.length > 0) {
|
|
7132
|
-
log.warn(
|
|
7252
|
+
log.warn(TAG32, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
|
|
7133
7253
|
}
|
|
7134
7254
|
return result;
|
|
7135
7255
|
}
|
|
@@ -7160,27 +7280,27 @@ class WorktreeGc {
|
|
|
7160
7280
|
try {
|
|
7161
7281
|
runWorktreeGc(this.basePath, this.store);
|
|
7162
7282
|
} catch (err) {
|
|
7163
|
-
log.warn(
|
|
7283
|
+
log.warn(TAG32, `GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
7164
7284
|
}
|
|
7165
7285
|
if (this.remoteOpts) {
|
|
7166
7286
|
try {
|
|
7167
7287
|
pruneFailedRemoteBranches(this.remoteOpts);
|
|
7168
7288
|
} catch (err) {
|
|
7169
|
-
log.warn(
|
|
7289
|
+
log.warn(TAG32, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
7170
7290
|
}
|
|
7171
7291
|
}
|
|
7172
7292
|
}
|
|
7173
7293
|
}
|
|
7174
7294
|
function getRepoRoot2() {
|
|
7175
7295
|
try {
|
|
7176
|
-
return
|
|
7296
|
+
return execFileSync11("git", ["rev-parse", "--show-toplevel"], {
|
|
7177
7297
|
encoding: "utf-8"
|
|
7178
7298
|
}).trim();
|
|
7179
7299
|
} catch {
|
|
7180
7300
|
return null;
|
|
7181
7301
|
}
|
|
7182
7302
|
}
|
|
7183
|
-
var
|
|
7303
|
+
var TAG32 = "worktree-gc", GIT_NETWORK_TIMEOUT_MS = 30000, GIT_SSH_CONNECT_TIMEOUT_SECS = 10, GIT_PRUNE_SWEEP_BUDGET_MS = 60000, GIT_NETWORK_EXEC, TRANSIENT_GIT_NETWORK_ERROR;
|
|
7184
7304
|
var init_worktree_gc = __esm(() => {
|
|
7185
7305
|
init_log();
|
|
7186
7306
|
init_worktree();
|
|
@@ -7214,12 +7334,12 @@ __export(exports_src, {
|
|
|
7214
7334
|
validatePrerequisites: () => validatePrerequisites,
|
|
7215
7335
|
main: () => main
|
|
7216
7336
|
});
|
|
7217
|
-
import { execFileSync as
|
|
7337
|
+
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
7218
7338
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7219
7339
|
import { createRequire as createRequire2 } from "node:module";
|
|
7220
7340
|
async function validatePrerequisites(config, banner) {
|
|
7221
7341
|
try {
|
|
7222
|
-
const ver =
|
|
7342
|
+
const ver = execFileSync12("claude", ["--version"], {
|
|
7223
7343
|
encoding: "utf-8"
|
|
7224
7344
|
}).trim();
|
|
7225
7345
|
banner.check(`Claude CLI ${ver}`);
|
|
@@ -7234,14 +7354,14 @@ async function validatePrerequisites(config, banner) {
|
|
|
7234
7354
|
validateGitProviderCli(provider);
|
|
7235
7355
|
}
|
|
7236
7356
|
try {
|
|
7237
|
-
const status =
|
|
7357
|
+
const status = execFileSync12("git", ["status", "--porcelain"], {
|
|
7238
7358
|
encoding: "utf-8"
|
|
7239
7359
|
}).trim();
|
|
7240
7360
|
if (status) {
|
|
7241
7361
|
banner.warn(`Working directory has uncommitted changes:
|
|
7242
7362
|
${status}`);
|
|
7243
7363
|
}
|
|
7244
|
-
|
|
7364
|
+
execFileSync12("git", ["rev-parse", "--verify", `origin/${config.agent.worktree.baseBranch}`], {
|
|
7245
7365
|
encoding: "utf-8",
|
|
7246
7366
|
stdio: "pipe"
|
|
7247
7367
|
});
|
|
@@ -7284,7 +7404,7 @@ async function main() {
|
|
|
7284
7404
|
} catch (err) {
|
|
7285
7405
|
if (err instanceof ConfigValidationError) {
|
|
7286
7406
|
banner.fail();
|
|
7287
|
-
log.error(
|
|
7407
|
+
log.error(TAG33, err.message);
|
|
7288
7408
|
process.exit(1);
|
|
7289
7409
|
}
|
|
7290
7410
|
throw err;
|
|
@@ -7393,7 +7513,7 @@ async function main() {
|
|
|
7393
7513
|
if (shuttingDown)
|
|
7394
7514
|
return;
|
|
7395
7515
|
shuttingDown = true;
|
|
7396
|
-
log.info(
|
|
7516
|
+
log.info(TAG33, `Received ${signal}, shutting down gracefully...`);
|
|
7397
7517
|
reconciler.stop();
|
|
7398
7518
|
mergeMonitor?.stop();
|
|
7399
7519
|
worktreeGc.stop();
|
|
@@ -7403,18 +7523,18 @@ async function main() {
|
|
|
7403
7523
|
}
|
|
7404
7524
|
await watcher.stop();
|
|
7405
7525
|
await pool.shutdown();
|
|
7406
|
-
log.info(
|
|
7526
|
+
log.info(TAG33, "Daemon stopped.");
|
|
7407
7527
|
process.exit(exitCode);
|
|
7408
7528
|
};
|
|
7409
7529
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
7410
7530
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
7411
7531
|
process.on("uncaughtException", (err) => {
|
|
7412
|
-
log.error(
|
|
7532
|
+
log.error(TAG33, `Uncaught exception: ${err.message}`);
|
|
7413
7533
|
exitCode = 1;
|
|
7414
7534
|
shutdown("uncaughtException");
|
|
7415
7535
|
});
|
|
7416
7536
|
process.on("unhandledRejection", (reason) => {
|
|
7417
|
-
log.error(
|
|
7537
|
+
log.error(TAG33, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
7418
7538
|
exitCode = 1;
|
|
7419
7539
|
shutdown("unhandledRejection");
|
|
7420
7540
|
});
|
|
@@ -7467,35 +7587,35 @@ async function handleBroadcast(event, client, pool, config, agentId) {
|
|
|
7467
7587
|
if (assignedAgentId === undefined)
|
|
7468
7588
|
return;
|
|
7469
7589
|
if (assignedAgentId === agentId) {
|
|
7470
|
-
log.info(
|
|
7590
|
+
log.info(TAG33, `Broadcast: card ${cardId} assigned to agent`);
|
|
7471
7591
|
try {
|
|
7472
7592
|
await pool.resetAttemptsForReassign(cardId);
|
|
7473
7593
|
await tryEnqueueCard(cardId, client, pool, config, agentId);
|
|
7474
7594
|
} catch (err) {
|
|
7475
|
-
log.error(
|
|
7595
|
+
log.error(TAG33, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
|
|
7476
7596
|
}
|
|
7477
7597
|
} else if (pool.isCardKnown(cardId)) {
|
|
7478
|
-
log.info(
|
|
7598
|
+
log.info(TAG33, `Broadcast: card ${cardId} unassigned from agent`);
|
|
7479
7599
|
await pool.removeCard(cardId);
|
|
7480
7600
|
}
|
|
7481
7601
|
}
|
|
7482
7602
|
async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
7483
7603
|
const { card } = await client.getCard(cardId);
|
|
7484
7604
|
if (card.assigned_agent_id !== agentId) {
|
|
7485
|
-
log.debug(
|
|
7605
|
+
log.debug(TAG33, `Card ${cardId} no longer assigned to agent — skipping`);
|
|
7486
7606
|
return;
|
|
7487
7607
|
}
|
|
7488
7608
|
const board = await client.getBoard(config.projectId, { summary: true });
|
|
7489
7609
|
const columns = board.columns;
|
|
7490
7610
|
const column = columns.find((c) => c.id === card.column_id);
|
|
7491
7611
|
if (!column) {
|
|
7492
|
-
log.warn(
|
|
7612
|
+
log.warn(TAG33, `Column not found for card ${cardId}`);
|
|
7493
7613
|
return;
|
|
7494
7614
|
}
|
|
7495
7615
|
const isPickupColumn = config.agent.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
|
|
7496
7616
|
const isReviewColumn = config.agent.review.enabled && config.agent.review.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
|
|
7497
7617
|
if (!isPickupColumn && !isReviewColumn) {
|
|
7498
|
-
log.info(
|
|
7618
|
+
log.info(TAG33, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
|
|
7499
7619
|
return;
|
|
7500
7620
|
}
|
|
7501
7621
|
const mode = isReviewColumn ? "review" : "implement";
|
|
@@ -7503,16 +7623,16 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
7503
7623
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
7504
7624
|
const subtasks = card.subtasks ?? [];
|
|
7505
7625
|
if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
|
|
7506
|
-
log.debug(
|
|
7626
|
+
log.debug(TAG33, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
|
|
7507
7627
|
return;
|
|
7508
7628
|
}
|
|
7509
7629
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
7510
|
-
log.info(
|
|
7630
|
+
log.info(TAG33, `Card #${card.short_id} has no branch reference — skipping auto-review`);
|
|
7511
7631
|
return;
|
|
7512
7632
|
}
|
|
7513
7633
|
await pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
7514
7634
|
}
|
|
7515
|
-
var
|
|
7635
|
+
var TAG33 = "daemon", PKG_VERSION;
|
|
7516
7636
|
var init_src = __esm(() => {
|
|
7517
7637
|
init_board_helpers();
|
|
7518
7638
|
init_config();
|