@gethmy/agent 1.22.3 → 1.22.4
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 +130 -12
- package/dist/index.js +130 -12
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -258,6 +258,25 @@ function extractBranchRef(description) {
|
|
|
258
258
|
}
|
|
259
259
|
return null;
|
|
260
260
|
}
|
|
261
|
+
function hasReviewableBranch(description) {
|
|
262
|
+
return extractBranchRef(description) !== null;
|
|
263
|
+
}
|
|
264
|
+
function hasReviewablePrLink(description) {
|
|
265
|
+
if (!description)
|
|
266
|
+
return false;
|
|
267
|
+
const m = description.match(PR_LINK_PATTERN);
|
|
268
|
+
if (!m)
|
|
269
|
+
return false;
|
|
270
|
+
try {
|
|
271
|
+
new URL(m[1]);
|
|
272
|
+
return true;
|
|
273
|
+
} catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function qualifiesForReview(description) {
|
|
278
|
+
return hasReviewableBranch(description) || hasReviewablePrLink(description);
|
|
279
|
+
}
|
|
261
280
|
function hasUnsafeDaemonBranchLine(description) {
|
|
262
281
|
if (!description)
|
|
263
282
|
return false;
|
|
@@ -267,11 +286,12 @@ function hasUnsafeDaemonBranchLine(description) {
|
|
|
267
286
|
}
|
|
268
287
|
return false;
|
|
269
288
|
}
|
|
270
|
-
var BRANCH_REF_PATTERN, DAEMON_BRANCH_LINE_PATTERN, SAFE_GIT_REF_PATTERN;
|
|
289
|
+
var BRANCH_REF_PATTERN, DAEMON_BRANCH_LINE_PATTERN, SAFE_GIT_REF_PATTERN, PR_LINK_PATTERN;
|
|
271
290
|
var init_branchRef = __esm(() => {
|
|
272
291
|
BRANCH_REF_PATTERN = /Branch:\s*`([^`]+)`/g;
|
|
273
292
|
DAEMON_BRANCH_LINE_PATTERN = /^[ \t]*Branch:\s*`([^`]+)`/gm;
|
|
274
293
|
SAFE_GIT_REF_PATTERN = /^[a-zA-Z0-9/_.+-]+$/;
|
|
294
|
+
PR_LINK_PATTERN = /PR:\s*(https?:\/\/[^\s)]+)/;
|
|
275
295
|
});
|
|
276
296
|
|
|
277
297
|
// ../harmony-shared/dist/cardLinks.js
|
|
@@ -2627,6 +2647,7 @@ function createWorktree(basePath, baseBranch, branchName, opts = {}) {
|
|
|
2627
2647
|
} catch (err) {
|
|
2628
2648
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2629
2649
|
log.warn(TAG6, `worktree add failed, attempting forced recovery: ${msg}`);
|
|
2650
|
+
removeWorktreeHoldingBranch(repoRoot, branchName, worktreeDir);
|
|
2630
2651
|
try {
|
|
2631
2652
|
execFileSync3("git", ["worktree", "remove", worktreeDir, "--force"], {
|
|
2632
2653
|
cwd: repoRoot,
|
|
@@ -2699,6 +2720,54 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
2699
2720
|
} catch {}
|
|
2700
2721
|
}
|
|
2701
2722
|
}
|
|
2723
|
+
function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
|
|
2724
|
+
let listing;
|
|
2725
|
+
try {
|
|
2726
|
+
listing = execFileSync3("git", ["worktree", "list", "--porcelain"], {
|
|
2727
|
+
cwd: repoRoot,
|
|
2728
|
+
encoding: "utf-8",
|
|
2729
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2730
|
+
});
|
|
2731
|
+
} catch {
|
|
2732
|
+
return null;
|
|
2733
|
+
}
|
|
2734
|
+
const target = `refs/heads/${branchName}`;
|
|
2735
|
+
let currentPath = null;
|
|
2736
|
+
let holderPath = null;
|
|
2737
|
+
for (const line of listing.split(`
|
|
2738
|
+
`)) {
|
|
2739
|
+
if (line.startsWith("worktree ")) {
|
|
2740
|
+
currentPath = line.slice("worktree ".length).trim();
|
|
2741
|
+
} else if (line.startsWith("branch ")) {
|
|
2742
|
+
const ref = line.slice("branch ".length).trim();
|
|
2743
|
+
if (ref === target && currentPath) {
|
|
2744
|
+
holderPath = currentPath;
|
|
2745
|
+
break;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
if (!holderPath)
|
|
2750
|
+
return null;
|
|
2751
|
+
if (exceptDir && resolve(holderPath) === resolve(exceptDir))
|
|
2752
|
+
return null;
|
|
2753
|
+
try {
|
|
2754
|
+
execFileSync3("git", ["worktree", "remove", holderPath, "--force"], {
|
|
2755
|
+
cwd: repoRoot,
|
|
2756
|
+
stdio: "pipe"
|
|
2757
|
+
});
|
|
2758
|
+
log.warn(TAG6, `Evicted worktree ${holderPath} holding branch ${branchName} so it can be reused (#732)`);
|
|
2759
|
+
} catch (err) {
|
|
2760
|
+
log.warn(TAG6, `Failed to evict worktree ${holderPath} holding ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
2761
|
+
return null;
|
|
2762
|
+
}
|
|
2763
|
+
try {
|
|
2764
|
+
execFileSync3("git", ["worktree", "prune", "--expire=now"], {
|
|
2765
|
+
cwd: repoRoot,
|
|
2766
|
+
stdio: "pipe"
|
|
2767
|
+
});
|
|
2768
|
+
} catch {}
|
|
2769
|
+
return holderPath;
|
|
2770
|
+
}
|
|
2702
2771
|
function resolveRepoRoot() {
|
|
2703
2772
|
return execFileSync3("git", ["rev-parse", "--show-toplevel"], {
|
|
2704
2773
|
encoding: "utf-8"
|
|
@@ -2814,6 +2883,7 @@ function checkoutExistingBranch(basePath, branchName) {
|
|
|
2814
2883
|
} catch (err) {
|
|
2815
2884
|
throw new Error(`Failed to fetch remote branch ${branchName}: ${gitErrorDetail(err)}`);
|
|
2816
2885
|
}
|
|
2886
|
+
removeWorktreeHoldingBranch(repoRoot, branchName, worktreeDir);
|
|
2817
2887
|
try {
|
|
2818
2888
|
execFileSync4("git", ["branch", "-D", branchName], {
|
|
2819
2889
|
cwd: repoRoot,
|
|
@@ -2854,7 +2924,7 @@ function extractBranchFromDescription(description) {
|
|
|
2854
2924
|
return branch;
|
|
2855
2925
|
}
|
|
2856
2926
|
function qualifiesForAutoReview(description) {
|
|
2857
|
-
return
|
|
2927
|
+
return qualifiesForReview(description);
|
|
2858
2928
|
}
|
|
2859
2929
|
async function resolveReviewBranch(description, cwd) {
|
|
2860
2930
|
const fromLine = extractBranchFromDescription(description);
|
|
@@ -6404,8 +6474,16 @@ class StateStore {
|
|
|
6404
6474
|
const raw = readFileSync4(this.path, "utf-8");
|
|
6405
6475
|
const parsed = JSON.parse(raw);
|
|
6406
6476
|
if (parsed?.version !== SCHEMA_VERSION) {
|
|
6407
|
-
log.warn(TAG21, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} —
|
|
6408
|
-
return
|
|
6477
|
+
log.warn(TAG21, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — migrating (preserving card budget/attempts, dropping in-flight runs)`);
|
|
6478
|
+
return {
|
|
6479
|
+
version: SCHEMA_VERSION,
|
|
6480
|
+
daemonId: null,
|
|
6481
|
+
daemonPid: null,
|
|
6482
|
+
daemonStartedAt: null,
|
|
6483
|
+
runs: [],
|
|
6484
|
+
cards: parsed.cards ?? [],
|
|
6485
|
+
daily: parsed.daily ?? []
|
|
6486
|
+
};
|
|
6409
6487
|
}
|
|
6410
6488
|
return {
|
|
6411
6489
|
version: SCHEMA_VERSION,
|
|
@@ -7749,6 +7827,20 @@ var init_cli_agent_runner = __esm(() => {
|
|
|
7749
7827
|
});
|
|
7750
7828
|
|
|
7751
7829
|
// src/prompt.ts
|
|
7830
|
+
function renderPreviousAttemptsSection(failures) {
|
|
7831
|
+
if (failures.length === 0)
|
|
7832
|
+
return "";
|
|
7833
|
+
const lines = failures.map((f) => {
|
|
7834
|
+
const tag = f.reason ? `[${f.reason}] ` : "";
|
|
7835
|
+
return `- ${tag}${f.summary}`;
|
|
7836
|
+
});
|
|
7837
|
+
return [
|
|
7838
|
+
"## Previous attempt feedback",
|
|
7839
|
+
"This is a re-attempt on the branch your last run already pushed — build on that existing work and FIX the issues below. Do NOT reimplement from scratch or revert the prior commits.",
|
|
7840
|
+
...lines
|
|
7841
|
+
].join(`
|
|
7842
|
+
`);
|
|
7843
|
+
}
|
|
7752
7844
|
async function buildPrompt(enriched, branchName, worktreePath, client, workspaceId, projectId) {
|
|
7753
7845
|
const { card } = enriched;
|
|
7754
7846
|
const pastEpisodesSection = await renderPastEpisodesSection(client, card.title, card.description ?? "", workspaceId, projectId);
|
|
@@ -8257,6 +8349,14 @@ class Worker {
|
|
|
8257
8349
|
this.heartbeatTimer = null;
|
|
8258
8350
|
}
|
|
8259
8351
|
}
|
|
8352
|
+
captureCliSessionId(sessionId) {
|
|
8353
|
+
if (!sessionId || sessionId === this.cliSessionId)
|
|
8354
|
+
return;
|
|
8355
|
+
this.cliSessionId = sessionId;
|
|
8356
|
+
if (this.runId) {
|
|
8357
|
+
this.stateStore.updateRun(this.runId, { cliSessionId: sessionId }).catch(() => {});
|
|
8358
|
+
}
|
|
8359
|
+
}
|
|
8260
8360
|
async recordPhase(phase) {
|
|
8261
8361
|
if (!this.runId)
|
|
8262
8362
|
return;
|
|
@@ -8308,7 +8408,8 @@ class Worker {
|
|
|
8308
8408
|
this.state = "preparing";
|
|
8309
8409
|
this.branchName = makeBranchName(card.short_id, card.title, this.config.worktree.failedBranchPrefix);
|
|
8310
8410
|
log.info(this.tag, `Preparing #${card.short_id} "${card.title}"`);
|
|
8311
|
-
await this.stateStore.incrementAttempt(card.id);
|
|
8411
|
+
const attemptCount = await this.stateStore.incrementAttempt(card.id);
|
|
8412
|
+
const isRework = attemptCount > 1;
|
|
8312
8413
|
this.startHeartbeat();
|
|
8313
8414
|
await this.stateStore.insertRun({
|
|
8314
8415
|
runId: this.runId,
|
|
@@ -8361,7 +8462,7 @@ class Worker {
|
|
|
8361
8462
|
await this.holdStageCard(card, stageCtx.reason);
|
|
8362
8463
|
return;
|
|
8363
8464
|
}
|
|
8364
|
-
this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName, { continueExisting: stageCtx.kind === "run" });
|
|
8465
|
+
this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName, { continueExisting: stageCtx.kind === "run" || isRework });
|
|
8365
8466
|
if (this.aborted)
|
|
8366
8467
|
return;
|
|
8367
8468
|
const enriched = {
|
|
@@ -8413,6 +8514,12 @@ class Worker {
|
|
|
8413
8514
|
mode: loop.mode
|
|
8414
8515
|
});
|
|
8415
8516
|
}
|
|
8517
|
+
} else if (isRework) {
|
|
8518
|
+
const digest = renderPreviousAttemptsSection(this.stateStore.getRecentFailures(card.id, 3));
|
|
8519
|
+
if (digest)
|
|
8520
|
+
prompt = `${digest}
|
|
8521
|
+
|
|
8522
|
+
${basePrompt}`;
|
|
8416
8523
|
}
|
|
8417
8524
|
await this.client.updateAgentProgress(card.id, {
|
|
8418
8525
|
agentIdentifier: agentIdentifier(this.id),
|
|
@@ -9214,6 +9321,7 @@ class Worker {
|
|
|
9214
9321
|
}
|
|
9215
9322
|
parser.on("text", (content) => {
|
|
9216
9323
|
this.lastRunText += content;
|
|
9324
|
+
this.captureCliSessionId(parser.sessionId);
|
|
9217
9325
|
});
|
|
9218
9326
|
parser.on("parse_error", (msg) => {
|
|
9219
9327
|
log.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
|
|
@@ -9232,8 +9340,7 @@ class Worker {
|
|
|
9232
9340
|
this.process.on("close", (code) => {
|
|
9233
9341
|
const leaderPid = this.process?.pid;
|
|
9234
9342
|
this.process = null;
|
|
9235
|
-
|
|
9236
|
-
this.cliSessionId = parser.sessionId;
|
|
9343
|
+
this.captureCliSessionId(parser.sessionId);
|
|
9237
9344
|
this.lastSessionStats = this.progressTracker?.stats;
|
|
9238
9345
|
const spawnCost = this.lastSessionStats?.cost;
|
|
9239
9346
|
if (spawnCost) {
|
|
@@ -9325,7 +9432,7 @@ class Worker {
|
|
|
9325
9432
|
`);
|
|
9326
9433
|
}
|
|
9327
9434
|
} finally {
|
|
9328
|
-
this.
|
|
9435
|
+
this.captureCliSessionId(runner.sessionId);
|
|
9329
9436
|
this.lastSessionStats = this.progressTracker?.stats;
|
|
9330
9437
|
const spawnCost = this.lastSessionStats?.cost;
|
|
9331
9438
|
if (spawnCost) {
|
|
@@ -9815,11 +9922,13 @@ async function recoverOrphans(store, client, config) {
|
|
|
9815
9922
|
continue;
|
|
9816
9923
|
}
|
|
9817
9924
|
log.info(TAG33, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
|
|
9818
|
-
await recoverRun(run, store, client, config, outcome
|
|
9925
|
+
await recoverRun(run, store, client, config, outcome, {
|
|
9926
|
+
rollbackAttempt: true
|
|
9927
|
+
});
|
|
9819
9928
|
}
|
|
9820
9929
|
return outcomes;
|
|
9821
9930
|
}
|
|
9822
|
-
async function recoverRun(run, store, client, config, outcome) {
|
|
9931
|
+
async function recoverRun(run, store, client, config, outcome, opts = {}) {
|
|
9823
9932
|
try {
|
|
9824
9933
|
await client.endAgentSession(run.cardId, {
|
|
9825
9934
|
status: "failed",
|
|
@@ -9875,6 +9984,15 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
9875
9984
|
const msg = err instanceof Error ? err.message : String(err);
|
|
9876
9985
|
outcome.errors.push(`endRun: ${msg}`);
|
|
9877
9986
|
}
|
|
9987
|
+
if (opts.rollbackAttempt && run.pipeline === "implement") {
|
|
9988
|
+
try {
|
|
9989
|
+
await store.decrementAttempt(run.cardId);
|
|
9990
|
+
outcome.actions.push("rolled back give-up attempt (daemon restart)");
|
|
9991
|
+
} catch (err) {
|
|
9992
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9993
|
+
outcome.errors.push(`decrementAttempt: ${msg}`);
|
|
9994
|
+
}
|
|
9995
|
+
}
|
|
9878
9996
|
log.info(TAG33, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
|
|
9879
9997
|
}
|
|
9880
9998
|
var TAG33 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
|
|
@@ -10040,7 +10158,7 @@ class Reconciler {
|
|
|
10040
10158
|
pipeline: run.pipeline,
|
|
10041
10159
|
actions: [],
|
|
10042
10160
|
errors: []
|
|
10043
|
-
});
|
|
10161
|
+
}, { rollbackAttempt: daemonDead });
|
|
10044
10162
|
}
|
|
10045
10163
|
}
|
|
10046
10164
|
async recoverStrandedInProgress(cards, columns, knownCardIds) {
|
package/dist/index.js
CHANGED
|
@@ -257,6 +257,25 @@ function extractBranchRef(description) {
|
|
|
257
257
|
}
|
|
258
258
|
return null;
|
|
259
259
|
}
|
|
260
|
+
function hasReviewableBranch(description) {
|
|
261
|
+
return extractBranchRef(description) !== null;
|
|
262
|
+
}
|
|
263
|
+
function hasReviewablePrLink(description) {
|
|
264
|
+
if (!description)
|
|
265
|
+
return false;
|
|
266
|
+
const m = description.match(PR_LINK_PATTERN);
|
|
267
|
+
if (!m)
|
|
268
|
+
return false;
|
|
269
|
+
try {
|
|
270
|
+
new URL(m[1]);
|
|
271
|
+
return true;
|
|
272
|
+
} catch {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function qualifiesForReview(description) {
|
|
277
|
+
return hasReviewableBranch(description) || hasReviewablePrLink(description);
|
|
278
|
+
}
|
|
260
279
|
function hasUnsafeDaemonBranchLine(description) {
|
|
261
280
|
if (!description)
|
|
262
281
|
return false;
|
|
@@ -266,11 +285,12 @@ function hasUnsafeDaemonBranchLine(description) {
|
|
|
266
285
|
}
|
|
267
286
|
return false;
|
|
268
287
|
}
|
|
269
|
-
var BRANCH_REF_PATTERN, DAEMON_BRANCH_LINE_PATTERN, SAFE_GIT_REF_PATTERN;
|
|
288
|
+
var BRANCH_REF_PATTERN, DAEMON_BRANCH_LINE_PATTERN, SAFE_GIT_REF_PATTERN, PR_LINK_PATTERN;
|
|
270
289
|
var init_branchRef = __esm(() => {
|
|
271
290
|
BRANCH_REF_PATTERN = /Branch:\s*`([^`]+)`/g;
|
|
272
291
|
DAEMON_BRANCH_LINE_PATTERN = /^[ \t]*Branch:\s*`([^`]+)`/gm;
|
|
273
292
|
SAFE_GIT_REF_PATTERN = /^[a-zA-Z0-9/_.+-]+$/;
|
|
293
|
+
PR_LINK_PATTERN = /PR:\s*(https?:\/\/[^\s)]+)/;
|
|
274
294
|
});
|
|
275
295
|
|
|
276
296
|
// ../harmony-shared/dist/cardLinks.js
|
|
@@ -2626,6 +2646,7 @@ function createWorktree(basePath, baseBranch, branchName, opts = {}) {
|
|
|
2626
2646
|
} catch (err) {
|
|
2627
2647
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2628
2648
|
log.warn(TAG6, `worktree add failed, attempting forced recovery: ${msg}`);
|
|
2649
|
+
removeWorktreeHoldingBranch(repoRoot, branchName, worktreeDir);
|
|
2629
2650
|
try {
|
|
2630
2651
|
execFileSync3("git", ["worktree", "remove", worktreeDir, "--force"], {
|
|
2631
2652
|
cwd: repoRoot,
|
|
@@ -2698,6 +2719,54 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
2698
2719
|
} catch {}
|
|
2699
2720
|
}
|
|
2700
2721
|
}
|
|
2722
|
+
function removeWorktreeHoldingBranch(repoRoot, branchName, exceptDir) {
|
|
2723
|
+
let listing;
|
|
2724
|
+
try {
|
|
2725
|
+
listing = execFileSync3("git", ["worktree", "list", "--porcelain"], {
|
|
2726
|
+
cwd: repoRoot,
|
|
2727
|
+
encoding: "utf-8",
|
|
2728
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2729
|
+
});
|
|
2730
|
+
} catch {
|
|
2731
|
+
return null;
|
|
2732
|
+
}
|
|
2733
|
+
const target = `refs/heads/${branchName}`;
|
|
2734
|
+
let currentPath = null;
|
|
2735
|
+
let holderPath = null;
|
|
2736
|
+
for (const line of listing.split(`
|
|
2737
|
+
`)) {
|
|
2738
|
+
if (line.startsWith("worktree ")) {
|
|
2739
|
+
currentPath = line.slice("worktree ".length).trim();
|
|
2740
|
+
} else if (line.startsWith("branch ")) {
|
|
2741
|
+
const ref = line.slice("branch ".length).trim();
|
|
2742
|
+
if (ref === target && currentPath) {
|
|
2743
|
+
holderPath = currentPath;
|
|
2744
|
+
break;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
if (!holderPath)
|
|
2749
|
+
return null;
|
|
2750
|
+
if (exceptDir && resolve(holderPath) === resolve(exceptDir))
|
|
2751
|
+
return null;
|
|
2752
|
+
try {
|
|
2753
|
+
execFileSync3("git", ["worktree", "remove", holderPath, "--force"], {
|
|
2754
|
+
cwd: repoRoot,
|
|
2755
|
+
stdio: "pipe"
|
|
2756
|
+
});
|
|
2757
|
+
log.warn(TAG6, `Evicted worktree ${holderPath} holding branch ${branchName} so it can be reused (#732)`);
|
|
2758
|
+
} catch (err) {
|
|
2759
|
+
log.warn(TAG6, `Failed to evict worktree ${holderPath} holding ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
2760
|
+
return null;
|
|
2761
|
+
}
|
|
2762
|
+
try {
|
|
2763
|
+
execFileSync3("git", ["worktree", "prune", "--expire=now"], {
|
|
2764
|
+
cwd: repoRoot,
|
|
2765
|
+
stdio: "pipe"
|
|
2766
|
+
});
|
|
2767
|
+
} catch {}
|
|
2768
|
+
return holderPath;
|
|
2769
|
+
}
|
|
2701
2770
|
function resolveRepoRoot() {
|
|
2702
2771
|
return execFileSync3("git", ["rev-parse", "--show-toplevel"], {
|
|
2703
2772
|
encoding: "utf-8"
|
|
@@ -2813,6 +2882,7 @@ function checkoutExistingBranch(basePath, branchName) {
|
|
|
2813
2882
|
} catch (err) {
|
|
2814
2883
|
throw new Error(`Failed to fetch remote branch ${branchName}: ${gitErrorDetail(err)}`);
|
|
2815
2884
|
}
|
|
2885
|
+
removeWorktreeHoldingBranch(repoRoot, branchName, worktreeDir);
|
|
2816
2886
|
try {
|
|
2817
2887
|
execFileSync4("git", ["branch", "-D", branchName], {
|
|
2818
2888
|
cwd: repoRoot,
|
|
@@ -2853,7 +2923,7 @@ function extractBranchFromDescription(description) {
|
|
|
2853
2923
|
return branch;
|
|
2854
2924
|
}
|
|
2855
2925
|
function qualifiesForAutoReview(description) {
|
|
2856
|
-
return
|
|
2926
|
+
return qualifiesForReview(description);
|
|
2857
2927
|
}
|
|
2858
2928
|
async function resolveReviewBranch(description, cwd) {
|
|
2859
2929
|
const fromLine = extractBranchFromDescription(description);
|
|
@@ -6403,8 +6473,16 @@ class StateStore {
|
|
|
6403
6473
|
const raw = readFileSync4(this.path, "utf-8");
|
|
6404
6474
|
const parsed = JSON.parse(raw);
|
|
6405
6475
|
if (parsed?.version !== SCHEMA_VERSION) {
|
|
6406
|
-
log.warn(TAG21, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} —
|
|
6407
|
-
return
|
|
6476
|
+
log.warn(TAG21, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — migrating (preserving card budget/attempts, dropping in-flight runs)`);
|
|
6477
|
+
return {
|
|
6478
|
+
version: SCHEMA_VERSION,
|
|
6479
|
+
daemonId: null,
|
|
6480
|
+
daemonPid: null,
|
|
6481
|
+
daemonStartedAt: null,
|
|
6482
|
+
runs: [],
|
|
6483
|
+
cards: parsed.cards ?? [],
|
|
6484
|
+
daily: parsed.daily ?? []
|
|
6485
|
+
};
|
|
6408
6486
|
}
|
|
6409
6487
|
return {
|
|
6410
6488
|
version: SCHEMA_VERSION,
|
|
@@ -7748,6 +7826,20 @@ var init_cli_agent_runner = __esm(() => {
|
|
|
7748
7826
|
});
|
|
7749
7827
|
|
|
7750
7828
|
// src/prompt.ts
|
|
7829
|
+
function renderPreviousAttemptsSection(failures) {
|
|
7830
|
+
if (failures.length === 0)
|
|
7831
|
+
return "";
|
|
7832
|
+
const lines = failures.map((f) => {
|
|
7833
|
+
const tag = f.reason ? `[${f.reason}] ` : "";
|
|
7834
|
+
return `- ${tag}${f.summary}`;
|
|
7835
|
+
});
|
|
7836
|
+
return [
|
|
7837
|
+
"## Previous attempt feedback",
|
|
7838
|
+
"This is a re-attempt on the branch your last run already pushed — build on that existing work and FIX the issues below. Do NOT reimplement from scratch or revert the prior commits.",
|
|
7839
|
+
...lines
|
|
7840
|
+
].join(`
|
|
7841
|
+
`);
|
|
7842
|
+
}
|
|
7751
7843
|
async function buildPrompt(enriched, branchName, worktreePath, client, workspaceId, projectId) {
|
|
7752
7844
|
const { card } = enriched;
|
|
7753
7845
|
const pastEpisodesSection = await renderPastEpisodesSection(client, card.title, card.description ?? "", workspaceId, projectId);
|
|
@@ -8256,6 +8348,14 @@ class Worker {
|
|
|
8256
8348
|
this.heartbeatTimer = null;
|
|
8257
8349
|
}
|
|
8258
8350
|
}
|
|
8351
|
+
captureCliSessionId(sessionId) {
|
|
8352
|
+
if (!sessionId || sessionId === this.cliSessionId)
|
|
8353
|
+
return;
|
|
8354
|
+
this.cliSessionId = sessionId;
|
|
8355
|
+
if (this.runId) {
|
|
8356
|
+
this.stateStore.updateRun(this.runId, { cliSessionId: sessionId }).catch(() => {});
|
|
8357
|
+
}
|
|
8358
|
+
}
|
|
8259
8359
|
async recordPhase(phase) {
|
|
8260
8360
|
if (!this.runId)
|
|
8261
8361
|
return;
|
|
@@ -8307,7 +8407,8 @@ class Worker {
|
|
|
8307
8407
|
this.state = "preparing";
|
|
8308
8408
|
this.branchName = makeBranchName(card.short_id, card.title, this.config.worktree.failedBranchPrefix);
|
|
8309
8409
|
log.info(this.tag, `Preparing #${card.short_id} "${card.title}"`);
|
|
8310
|
-
await this.stateStore.incrementAttempt(card.id);
|
|
8410
|
+
const attemptCount = await this.stateStore.incrementAttempt(card.id);
|
|
8411
|
+
const isRework = attemptCount > 1;
|
|
8311
8412
|
this.startHeartbeat();
|
|
8312
8413
|
await this.stateStore.insertRun({
|
|
8313
8414
|
runId: this.runId,
|
|
@@ -8360,7 +8461,7 @@ class Worker {
|
|
|
8360
8461
|
await this.holdStageCard(card, stageCtx.reason);
|
|
8361
8462
|
return;
|
|
8362
8463
|
}
|
|
8363
|
-
this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName, { continueExisting: stageCtx.kind === "run" });
|
|
8464
|
+
this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName, { continueExisting: stageCtx.kind === "run" || isRework });
|
|
8364
8465
|
if (this.aborted)
|
|
8365
8466
|
return;
|
|
8366
8467
|
const enriched = {
|
|
@@ -8412,6 +8513,12 @@ class Worker {
|
|
|
8412
8513
|
mode: loop.mode
|
|
8413
8514
|
});
|
|
8414
8515
|
}
|
|
8516
|
+
} else if (isRework) {
|
|
8517
|
+
const digest = renderPreviousAttemptsSection(this.stateStore.getRecentFailures(card.id, 3));
|
|
8518
|
+
if (digest)
|
|
8519
|
+
prompt = `${digest}
|
|
8520
|
+
|
|
8521
|
+
${basePrompt}`;
|
|
8415
8522
|
}
|
|
8416
8523
|
await this.client.updateAgentProgress(card.id, {
|
|
8417
8524
|
agentIdentifier: agentIdentifier(this.id),
|
|
@@ -9213,6 +9320,7 @@ class Worker {
|
|
|
9213
9320
|
}
|
|
9214
9321
|
parser.on("text", (content) => {
|
|
9215
9322
|
this.lastRunText += content;
|
|
9323
|
+
this.captureCliSessionId(parser.sessionId);
|
|
9216
9324
|
});
|
|
9217
9325
|
parser.on("parse_error", (msg) => {
|
|
9218
9326
|
log.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
|
|
@@ -9231,8 +9339,7 @@ class Worker {
|
|
|
9231
9339
|
this.process.on("close", (code) => {
|
|
9232
9340
|
const leaderPid = this.process?.pid;
|
|
9233
9341
|
this.process = null;
|
|
9234
|
-
|
|
9235
|
-
this.cliSessionId = parser.sessionId;
|
|
9342
|
+
this.captureCliSessionId(parser.sessionId);
|
|
9236
9343
|
this.lastSessionStats = this.progressTracker?.stats;
|
|
9237
9344
|
const spawnCost = this.lastSessionStats?.cost;
|
|
9238
9345
|
if (spawnCost) {
|
|
@@ -9324,7 +9431,7 @@ class Worker {
|
|
|
9324
9431
|
`);
|
|
9325
9432
|
}
|
|
9326
9433
|
} finally {
|
|
9327
|
-
this.
|
|
9434
|
+
this.captureCliSessionId(runner.sessionId);
|
|
9328
9435
|
this.lastSessionStats = this.progressTracker?.stats;
|
|
9329
9436
|
const spawnCost = this.lastSessionStats?.cost;
|
|
9330
9437
|
if (spawnCost) {
|
|
@@ -9814,11 +9921,13 @@ async function recoverOrphans(store, client, config) {
|
|
|
9814
9921
|
continue;
|
|
9815
9922
|
}
|
|
9816
9923
|
log.info(TAG33, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
|
|
9817
|
-
await recoverRun(run, store, client, config, outcome
|
|
9924
|
+
await recoverRun(run, store, client, config, outcome, {
|
|
9925
|
+
rollbackAttempt: true
|
|
9926
|
+
});
|
|
9818
9927
|
}
|
|
9819
9928
|
return outcomes;
|
|
9820
9929
|
}
|
|
9821
|
-
async function recoverRun(run, store, client, config, outcome) {
|
|
9930
|
+
async function recoverRun(run, store, client, config, outcome, opts = {}) {
|
|
9822
9931
|
try {
|
|
9823
9932
|
await client.endAgentSession(run.cardId, {
|
|
9824
9933
|
status: "failed",
|
|
@@ -9874,6 +9983,15 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
9874
9983
|
const msg = err instanceof Error ? err.message : String(err);
|
|
9875
9984
|
outcome.errors.push(`endRun: ${msg}`);
|
|
9876
9985
|
}
|
|
9986
|
+
if (opts.rollbackAttempt && run.pipeline === "implement") {
|
|
9987
|
+
try {
|
|
9988
|
+
await store.decrementAttempt(run.cardId);
|
|
9989
|
+
outcome.actions.push("rolled back give-up attempt (daemon restart)");
|
|
9990
|
+
} catch (err) {
|
|
9991
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9992
|
+
outcome.errors.push(`decrementAttempt: ${msg}`);
|
|
9993
|
+
}
|
|
9994
|
+
}
|
|
9877
9995
|
log.info(TAG33, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
|
|
9878
9996
|
}
|
|
9879
9997
|
var TAG33 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
|
|
@@ -10039,7 +10157,7 @@ class Reconciler {
|
|
|
10039
10157
|
pipeline: run.pipeline,
|
|
10040
10158
|
actions: [],
|
|
10041
10159
|
errors: []
|
|
10042
|
-
});
|
|
10160
|
+
}, { rollbackAttempt: daemonDead });
|
|
10043
10161
|
}
|
|
10044
10162
|
}
|
|
10045
10163
|
async recoverStrandedInProgress(cards, columns, knownCardIds) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/agent",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.4",
|
|
4
4
|
"description": "Push-based agent daemon for Harmony — watches board assignments and spawns Claude CLI workers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@anthropic-ai/claude-agent-sdk": "^0.3.178",
|
|
48
48
|
"@supabase/supabase-js": "2.95.3",
|
|
49
|
-
"@gethmy/mcp": "2.
|
|
49
|
+
"@gethmy/mcp": "2.20.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@harmony/shared": "workspace:*",
|