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