@gethmy/agent 1.11.1 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +142 -47
- package/dist/index.js +142 -47
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -390,7 +390,7 @@ var init_types = __esm(() => {
|
|
|
390
390
|
pickupColumns: ["To Do"],
|
|
391
391
|
priorityLabels: { urgent: 100, critical: 90, bug: 50 },
|
|
392
392
|
columnBoost: true,
|
|
393
|
-
runner: "
|
|
393
|
+
runner: "sdk",
|
|
394
394
|
completion: {
|
|
395
395
|
createPR: false,
|
|
396
396
|
moveToColumn: "Review",
|
|
@@ -565,7 +565,7 @@ function loadDaemonConfig() {
|
|
|
565
565
|
}
|
|
566
566
|
};
|
|
567
567
|
if (agent.runner !== "cli" && agent.runner !== "sdk") {
|
|
568
|
-
agent.runner =
|
|
568
|
+
agent.runner = DEFAULT_AGENT_CONFIG.runner;
|
|
569
569
|
}
|
|
570
570
|
return {
|
|
571
571
|
apiKey,
|
|
@@ -1228,7 +1228,8 @@ function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root,
|
|
|
1228
1228
|
log.warn(TAG5, `fetch origin ${baseBranch} failed (attempt ${attempt}/${attempts})`);
|
|
1229
1229
|
}
|
|
1230
1230
|
}
|
|
1231
|
-
const
|
|
1231
|
+
const e = lastErr;
|
|
1232
|
+
const detail = e?.stderr?.toString?.().trim() || (lastErr instanceof Error ? lastErr.message : String(lastErr));
|
|
1232
1233
|
throw new WorktreeBaseError(`Could not fetch origin/${baseBranch} after ${attempts} attempts — ` + `refusing to build on a stale base. ${detail}`);
|
|
1233
1234
|
}
|
|
1234
1235
|
function createWorktree(basePath, baseBranch, branchName) {
|
|
@@ -1350,6 +1351,16 @@ var init_worktree = __esm(() => {
|
|
|
1350
1351
|
import { execFileSync as execFileSync4, execSync as execSync3 } from "node:child_process";
|
|
1351
1352
|
import { existsSync as existsSync3 } from "node:fs";
|
|
1352
1353
|
import { resolve as resolve2 } from "node:path";
|
|
1354
|
+
function gitErrorDetail(err) {
|
|
1355
|
+
const e = err;
|
|
1356
|
+
const stderr = e?.stderr?.toString?.().trim();
|
|
1357
|
+
if (stderr)
|
|
1358
|
+
return stderr;
|
|
1359
|
+
const stdout = e?.stdout?.toString?.().trim();
|
|
1360
|
+
if (stdout)
|
|
1361
|
+
return stdout;
|
|
1362
|
+
return err instanceof Error ? err.message : String(err);
|
|
1363
|
+
}
|
|
1353
1364
|
function checkoutExistingBranch(basePath, branchName) {
|
|
1354
1365
|
const repoRoot = execFileSync4("git", ["rev-parse", "--show-toplevel"], {
|
|
1355
1366
|
encoding: "utf-8"
|
|
@@ -1370,8 +1381,8 @@ function checkoutExistingBranch(basePath, branchName) {
|
|
|
1370
1381
|
cwd: repoRoot,
|
|
1371
1382
|
stdio: "pipe"
|
|
1372
1383
|
});
|
|
1373
|
-
} catch {
|
|
1374
|
-
throw new Error(`Failed to fetch remote branch
|
|
1384
|
+
} catch (err) {
|
|
1385
|
+
throw new Error(`Failed to fetch remote branch ${branchName}: ${gitErrorDetail(err)}`);
|
|
1375
1386
|
}
|
|
1376
1387
|
try {
|
|
1377
1388
|
execFileSync4("git", ["branch", "-D", branchName], {
|
|
@@ -1380,15 +1391,19 @@ function checkoutExistingBranch(basePath, branchName) {
|
|
|
1380
1391
|
});
|
|
1381
1392
|
} catch {}
|
|
1382
1393
|
log.info(TAG6, `Creating review worktree: ${worktreeDir} (branch: ${branchName})`);
|
|
1383
|
-
|
|
1384
|
-
"
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1394
|
+
try {
|
|
1395
|
+
execFileSync4("git", [
|
|
1396
|
+
"worktree",
|
|
1397
|
+
"add",
|
|
1398
|
+
"--track",
|
|
1399
|
+
"-b",
|
|
1400
|
+
branchName,
|
|
1401
|
+
worktreeDir,
|
|
1402
|
+
`origin/${branchName}`
|
|
1403
|
+
], { cwd: repoRoot, stdio: "pipe" });
|
|
1404
|
+
} catch (err) {
|
|
1405
|
+
throw new Error(`Failed to create review worktree for ${branchName}: ${gitErrorDetail(err)}`);
|
|
1406
|
+
}
|
|
1392
1407
|
log.info(TAG6, "Installing dependencies in review worktree...");
|
|
1393
1408
|
try {
|
|
1394
1409
|
execSync3(installCommand(), {
|
|
@@ -2613,6 +2628,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2613
2628
|
reviewFindings: [],
|
|
2614
2629
|
revertWarnings: []
|
|
2615
2630
|
};
|
|
2631
|
+
commitUncommittedChanges(worktreePath, card);
|
|
2616
2632
|
const hasCommits = checkHasCommits(worktreePath, config.worktree.baseBranch);
|
|
2617
2633
|
if (!hasCommits) {
|
|
2618
2634
|
const { maxTurnsExhausted, failureSummary } = describeNoCommitFailure(sessionStats?.cost?.numTurns ?? 0, config.claude.maxTurns);
|
|
@@ -2787,6 +2803,37 @@ function readHeadSha(worktreePath) {
|
|
|
2787
2803
|
return null;
|
|
2788
2804
|
}
|
|
2789
2805
|
}
|
|
2806
|
+
function commitUncommittedChanges(worktreePath, card) {
|
|
2807
|
+
let status = "";
|
|
2808
|
+
try {
|
|
2809
|
+
status = execFileSync9("git", ["status", "--porcelain"], {
|
|
2810
|
+
cwd: worktreePath,
|
|
2811
|
+
encoding: "utf-8"
|
|
2812
|
+
}).trim();
|
|
2813
|
+
} catch (err) {
|
|
2814
|
+
log.warn(TAG14, `git status failed in ${worktreePath}: ${err instanceof Error ? err.message : err}`);
|
|
2815
|
+
return false;
|
|
2816
|
+
}
|
|
2817
|
+
if (status.length === 0)
|
|
2818
|
+
return false;
|
|
2819
|
+
const title = card.title?.trim() || "agent changes";
|
|
2820
|
+
const message = `#${card.short_id} ${title}`;
|
|
2821
|
+
try {
|
|
2822
|
+
execFileSync9("git", ["add", "-A"], {
|
|
2823
|
+
cwd: worktreePath,
|
|
2824
|
+
encoding: "utf-8"
|
|
2825
|
+
});
|
|
2826
|
+
execFileSync9("git", ["commit", "-m", message], {
|
|
2827
|
+
cwd: worktreePath,
|
|
2828
|
+
encoding: "utf-8"
|
|
2829
|
+
});
|
|
2830
|
+
log.warn(TAG14, `Auto-committed uncommitted worktree changes for #${card.short_id} — agent ended without committing`);
|
|
2831
|
+
return true;
|
|
2832
|
+
} catch (err) {
|
|
2833
|
+
log.error(TAG14, `auto-commit failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
2834
|
+
return false;
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2790
2837
|
function checkHasCommits(worktreePath, baseBranch) {
|
|
2791
2838
|
try {
|
|
2792
2839
|
const count = execFileSync9("git", ["rev-list", "--count", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
@@ -6180,6 +6227,15 @@ class Worker {
|
|
|
6180
6227
|
...this.runLedger()
|
|
6181
6228
|
});
|
|
6182
6229
|
} catch {}
|
|
6230
|
+
const stopCost = this.lastSessionStats?.cost;
|
|
6231
|
+
if (stopCost) {
|
|
6232
|
+
const cents = Math.round(stopCost.totalCostUsd * 100);
|
|
6233
|
+
if (cents > 0) {
|
|
6234
|
+
try {
|
|
6235
|
+
await this.stateStore.addCost(card.id, cents);
|
|
6236
|
+
} catch {}
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
6183
6239
|
} else if (this.runId && this.verificationFailed) {
|
|
6184
6240
|
try {
|
|
6185
6241
|
await this.stateStore.endRun(this.runId, "paused", {
|
|
@@ -6662,7 +6718,7 @@ class Worker {
|
|
|
6662
6718
|
clearTimeout(this.timeoutTimer);
|
|
6663
6719
|
this.timeoutTimer = null;
|
|
6664
6720
|
}
|
|
6665
|
-
if (this.worktreePath && (this.state === "error" || this.timedOut)) {
|
|
6721
|
+
if (this.worktreePath && (this.state === "error" || this.timedOut || this.aborted)) {
|
|
6666
6722
|
try {
|
|
6667
6723
|
cleanupWorktree(this.worktreePath, this.branchName ?? undefined);
|
|
6668
6724
|
} catch {
|
|
@@ -7634,6 +7690,10 @@ class Watcher {
|
|
|
7634
7690
|
readyPromise = new Promise((resolve3) => {
|
|
7635
7691
|
this.readyResolve = resolve3;
|
|
7636
7692
|
});
|
|
7693
|
+
stopping = false;
|
|
7694
|
+
reconnectTimer = null;
|
|
7695
|
+
reconnectAttempts = 0;
|
|
7696
|
+
broadcastGen = 0;
|
|
7637
7697
|
get isConnected() {
|
|
7638
7698
|
return this.connected;
|
|
7639
7699
|
}
|
|
@@ -7662,7 +7722,34 @@ class Watcher {
|
|
|
7662
7722
|
}
|
|
7663
7723
|
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
7664
7724
|
const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
|
|
7665
|
-
|
|
7725
|
+
this.subscribeBroadcast();
|
|
7726
|
+
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
7727
|
+
log.debug(TAG32, "Presence sync");
|
|
7728
|
+
}).subscribe(async (status) => {
|
|
7729
|
+
if (status === "SUBSCRIBED") {
|
|
7730
|
+
await presenceChannel.track({
|
|
7731
|
+
daemonId: this.daemonId,
|
|
7732
|
+
startedAt: new Date().toISOString(),
|
|
7733
|
+
userId: this.identity.userId,
|
|
7734
|
+
agentId: this.identity.agentId,
|
|
7735
|
+
userEmail: this.identity.userEmail,
|
|
7736
|
+
agentIdentifier: this.identity.agentIdentifier,
|
|
7737
|
+
agentName: this.identity.agentName
|
|
7738
|
+
});
|
|
7739
|
+
if (!isPretty() || !this.suppressStartupLogs) {
|
|
7740
|
+
log.info(TAG32, "Presence tracked on board-presence channel");
|
|
7741
|
+
}
|
|
7742
|
+
this.presenceTracked = true;
|
|
7743
|
+
this.maybeResolveReady();
|
|
7744
|
+
}
|
|
7745
|
+
});
|
|
7746
|
+
this.presenceChannel = presenceChannel;
|
|
7747
|
+
}
|
|
7748
|
+
subscribeBroadcast() {
|
|
7749
|
+
if (!this.supabase)
|
|
7750
|
+
return;
|
|
7751
|
+
const gen = ++this.broadcastGen;
|
|
7752
|
+
this.channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
|
|
7666
7753
|
log.debug(TAG32, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
|
|
7667
7754
|
this.onCardBroadcast({
|
|
7668
7755
|
event: "card_update",
|
|
@@ -7683,46 +7770,54 @@ class Watcher {
|
|
|
7683
7770
|
this.onAgentCommand?.({ cardId, command });
|
|
7684
7771
|
}
|
|
7685
7772
|
}).subscribe((status) => {
|
|
7773
|
+
if (gen !== this.broadcastGen)
|
|
7774
|
+
return;
|
|
7686
7775
|
if (status === "SUBSCRIBED") {
|
|
7687
7776
|
this.connected = true;
|
|
7777
|
+
this.reconnectAttempts = 0;
|
|
7688
7778
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
7689
7779
|
log.info(TAG32, "Broadcast subscription active");
|
|
7690
7780
|
}
|
|
7691
7781
|
this.maybeResolveReady();
|
|
7692
|
-
} else if (status === "CHANNEL_ERROR") {
|
|
7693
|
-
this.connected = false;
|
|
7694
|
-
log.error(TAG32, "Broadcast channel error — will rely on reconciliation");
|
|
7695
|
-
} else if (status === "TIMED_OUT") {
|
|
7782
|
+
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
|
|
7696
7783
|
this.connected = false;
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
}
|
|
7701
|
-
});
|
|
7702
|
-
this.channel = channel;
|
|
7703
|
-
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
7704
|
-
log.debug(TAG32, "Presence sync");
|
|
7705
|
-
}).subscribe(async (status) => {
|
|
7706
|
-
if (status === "SUBSCRIBED") {
|
|
7707
|
-
await presenceChannel.track({
|
|
7708
|
-
daemonId: this.daemonId,
|
|
7709
|
-
startedAt: new Date().toISOString(),
|
|
7710
|
-
userId: this.identity.userId,
|
|
7711
|
-
agentId: this.identity.agentId,
|
|
7712
|
-
userEmail: this.identity.userEmail,
|
|
7713
|
-
agentIdentifier: this.identity.agentIdentifier,
|
|
7714
|
-
agentName: this.identity.agentName
|
|
7715
|
-
});
|
|
7716
|
-
if (!isPretty() || !this.suppressStartupLogs) {
|
|
7717
|
-
log.info(TAG32, "Presence tracked on board-presence channel");
|
|
7784
|
+
if (!this.stopping) {
|
|
7785
|
+
log.warn(TAG32, `Broadcast subscription ${status} — scheduling reconnect`);
|
|
7786
|
+
this.scheduleReconnect();
|
|
7718
7787
|
}
|
|
7719
|
-
this.presenceTracked = true;
|
|
7720
|
-
this.maybeResolveReady();
|
|
7721
7788
|
}
|
|
7722
7789
|
});
|
|
7723
|
-
|
|
7790
|
+
}
|
|
7791
|
+
scheduleReconnect() {
|
|
7792
|
+
if (this.stopping || this.reconnectTimer)
|
|
7793
|
+
return;
|
|
7794
|
+
const delay = Math.min(30000, 1000 * 2 ** this.reconnectAttempts);
|
|
7795
|
+
this.reconnectAttempts++;
|
|
7796
|
+
this.reconnectTimer = setTimeout(() => {
|
|
7797
|
+
this.reconnectTimer = null;
|
|
7798
|
+
this.reconnectBroadcast();
|
|
7799
|
+
}, delay);
|
|
7800
|
+
}
|
|
7801
|
+
async reconnectBroadcast() {
|
|
7802
|
+
if (this.stopping || !this.supabase)
|
|
7803
|
+
return;
|
|
7804
|
+
log.warn(TAG32, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
|
|
7805
|
+
if (this.channel) {
|
|
7806
|
+
const old = this.channel;
|
|
7807
|
+
this.channel = null;
|
|
7808
|
+
this.broadcastGen++;
|
|
7809
|
+
try {
|
|
7810
|
+
await this.supabase.removeChannel(old);
|
|
7811
|
+
} catch {}
|
|
7812
|
+
}
|
|
7813
|
+
this.subscribeBroadcast();
|
|
7724
7814
|
}
|
|
7725
7815
|
async stop() {
|
|
7816
|
+
this.stopping = true;
|
|
7817
|
+
if (this.reconnectTimer) {
|
|
7818
|
+
clearTimeout(this.reconnectTimer);
|
|
7819
|
+
this.reconnectTimer = null;
|
|
7820
|
+
}
|
|
7726
7821
|
if (this.presenceChannel) {
|
|
7727
7822
|
await this.supabase?.removeChannel(this.presenceChannel);
|
|
7728
7823
|
this.presenceChannel = null;
|
|
@@ -7758,7 +7853,7 @@ import { resolve as resolve3 } from "node:path";
|
|
|
7758
7853
|
function isTransientGitNetworkError(message) {
|
|
7759
7854
|
return TRANSIENT_GIT_NETWORK_ERROR.test(message);
|
|
7760
7855
|
}
|
|
7761
|
-
function
|
|
7856
|
+
function gitErrorDetail2(err) {
|
|
7762
7857
|
if (err && typeof err === "object" && "stderr" in err) {
|
|
7763
7858
|
const stderr = String(err.stderr ?? "").trim();
|
|
7764
7859
|
if (stderr)
|
|
@@ -7857,7 +7952,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
7857
7952
|
...GIT_NETWORK_EXEC
|
|
7858
7953
|
});
|
|
7859
7954
|
} catch (err) {
|
|
7860
|
-
const detail =
|
|
7955
|
+
const detail = gitErrorDetail2(err);
|
|
7861
7956
|
if (isTransientGitNetworkError(detail)) {
|
|
7862
7957
|
log.debug(TAG33, `Remote branch GC skipped — remote unreachable: ${detail}`);
|
|
7863
7958
|
return result;
|
|
@@ -7909,7 +8004,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
7909
8004
|
});
|
|
7910
8005
|
result.removed.push(ref);
|
|
7911
8006
|
} catch (err) {
|
|
7912
|
-
const detail =
|
|
8007
|
+
const detail = gitErrorDetail2(err);
|
|
7913
8008
|
if (isTransientGitNetworkError(detail)) {
|
|
7914
8009
|
log.debug(TAG33, `Remote branch GC interrupted — remote unreachable: ${detail}`);
|
|
7915
8010
|
break;
|
package/dist/index.js
CHANGED
|
@@ -389,7 +389,7 @@ var init_types = __esm(() => {
|
|
|
389
389
|
pickupColumns: ["To Do"],
|
|
390
390
|
priorityLabels: { urgent: 100, critical: 90, bug: 50 },
|
|
391
391
|
columnBoost: true,
|
|
392
|
-
runner: "
|
|
392
|
+
runner: "sdk",
|
|
393
393
|
completion: {
|
|
394
394
|
createPR: false,
|
|
395
395
|
moveToColumn: "Review",
|
|
@@ -564,7 +564,7 @@ function loadDaemonConfig() {
|
|
|
564
564
|
}
|
|
565
565
|
};
|
|
566
566
|
if (agent.runner !== "cli" && agent.runner !== "sdk") {
|
|
567
|
-
agent.runner =
|
|
567
|
+
agent.runner = DEFAULT_AGENT_CONFIG.runner;
|
|
568
568
|
}
|
|
569
569
|
return {
|
|
570
570
|
apiKey,
|
|
@@ -1227,7 +1227,8 @@ function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root,
|
|
|
1227
1227
|
log.warn(TAG5, `fetch origin ${baseBranch} failed (attempt ${attempt}/${attempts})`);
|
|
1228
1228
|
}
|
|
1229
1229
|
}
|
|
1230
|
-
const
|
|
1230
|
+
const e = lastErr;
|
|
1231
|
+
const detail = e?.stderr?.toString?.().trim() || (lastErr instanceof Error ? lastErr.message : String(lastErr));
|
|
1231
1232
|
throw new WorktreeBaseError(`Could not fetch origin/${baseBranch} after ${attempts} attempts — ` + `refusing to build on a stale base. ${detail}`);
|
|
1232
1233
|
}
|
|
1233
1234
|
function createWorktree(basePath, baseBranch, branchName) {
|
|
@@ -1349,6 +1350,16 @@ var init_worktree = __esm(() => {
|
|
|
1349
1350
|
import { execFileSync as execFileSync4, execSync as execSync3 } from "node:child_process";
|
|
1350
1351
|
import { existsSync as existsSync3 } from "node:fs";
|
|
1351
1352
|
import { resolve as resolve2 } from "node:path";
|
|
1353
|
+
function gitErrorDetail(err) {
|
|
1354
|
+
const e = err;
|
|
1355
|
+
const stderr = e?.stderr?.toString?.().trim();
|
|
1356
|
+
if (stderr)
|
|
1357
|
+
return stderr;
|
|
1358
|
+
const stdout = e?.stdout?.toString?.().trim();
|
|
1359
|
+
if (stdout)
|
|
1360
|
+
return stdout;
|
|
1361
|
+
return err instanceof Error ? err.message : String(err);
|
|
1362
|
+
}
|
|
1352
1363
|
function checkoutExistingBranch(basePath, branchName) {
|
|
1353
1364
|
const repoRoot = execFileSync4("git", ["rev-parse", "--show-toplevel"], {
|
|
1354
1365
|
encoding: "utf-8"
|
|
@@ -1369,8 +1380,8 @@ function checkoutExistingBranch(basePath, branchName) {
|
|
|
1369
1380
|
cwd: repoRoot,
|
|
1370
1381
|
stdio: "pipe"
|
|
1371
1382
|
});
|
|
1372
|
-
} catch {
|
|
1373
|
-
throw new Error(`Failed to fetch remote branch
|
|
1383
|
+
} catch (err) {
|
|
1384
|
+
throw new Error(`Failed to fetch remote branch ${branchName}: ${gitErrorDetail(err)}`);
|
|
1374
1385
|
}
|
|
1375
1386
|
try {
|
|
1376
1387
|
execFileSync4("git", ["branch", "-D", branchName], {
|
|
@@ -1379,15 +1390,19 @@ function checkoutExistingBranch(basePath, branchName) {
|
|
|
1379
1390
|
});
|
|
1380
1391
|
} catch {}
|
|
1381
1392
|
log.info(TAG6, `Creating review worktree: ${worktreeDir} (branch: ${branchName})`);
|
|
1382
|
-
|
|
1383
|
-
"
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1393
|
+
try {
|
|
1394
|
+
execFileSync4("git", [
|
|
1395
|
+
"worktree",
|
|
1396
|
+
"add",
|
|
1397
|
+
"--track",
|
|
1398
|
+
"-b",
|
|
1399
|
+
branchName,
|
|
1400
|
+
worktreeDir,
|
|
1401
|
+
`origin/${branchName}`
|
|
1402
|
+
], { cwd: repoRoot, stdio: "pipe" });
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
throw new Error(`Failed to create review worktree for ${branchName}: ${gitErrorDetail(err)}`);
|
|
1405
|
+
}
|
|
1391
1406
|
log.info(TAG6, "Installing dependencies in review worktree...");
|
|
1392
1407
|
try {
|
|
1393
1408
|
execSync3(installCommand(), {
|
|
@@ -2612,6 +2627,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
2612
2627
|
reviewFindings: [],
|
|
2613
2628
|
revertWarnings: []
|
|
2614
2629
|
};
|
|
2630
|
+
commitUncommittedChanges(worktreePath, card);
|
|
2615
2631
|
const hasCommits = checkHasCommits(worktreePath, config.worktree.baseBranch);
|
|
2616
2632
|
if (!hasCommits) {
|
|
2617
2633
|
const { maxTurnsExhausted, failureSummary } = describeNoCommitFailure(sessionStats?.cost?.numTurns ?? 0, config.claude.maxTurns);
|
|
@@ -2786,6 +2802,37 @@ function readHeadSha(worktreePath) {
|
|
|
2786
2802
|
return null;
|
|
2787
2803
|
}
|
|
2788
2804
|
}
|
|
2805
|
+
function commitUncommittedChanges(worktreePath, card) {
|
|
2806
|
+
let status = "";
|
|
2807
|
+
try {
|
|
2808
|
+
status = execFileSync9("git", ["status", "--porcelain"], {
|
|
2809
|
+
cwd: worktreePath,
|
|
2810
|
+
encoding: "utf-8"
|
|
2811
|
+
}).trim();
|
|
2812
|
+
} catch (err) {
|
|
2813
|
+
log.warn(TAG14, `git status failed in ${worktreePath}: ${err instanceof Error ? err.message : err}`);
|
|
2814
|
+
return false;
|
|
2815
|
+
}
|
|
2816
|
+
if (status.length === 0)
|
|
2817
|
+
return false;
|
|
2818
|
+
const title = card.title?.trim() || "agent changes";
|
|
2819
|
+
const message = `#${card.short_id} ${title}`;
|
|
2820
|
+
try {
|
|
2821
|
+
execFileSync9("git", ["add", "-A"], {
|
|
2822
|
+
cwd: worktreePath,
|
|
2823
|
+
encoding: "utf-8"
|
|
2824
|
+
});
|
|
2825
|
+
execFileSync9("git", ["commit", "-m", message], {
|
|
2826
|
+
cwd: worktreePath,
|
|
2827
|
+
encoding: "utf-8"
|
|
2828
|
+
});
|
|
2829
|
+
log.warn(TAG14, `Auto-committed uncommitted worktree changes for #${card.short_id} — agent ended without committing`);
|
|
2830
|
+
return true;
|
|
2831
|
+
} catch (err) {
|
|
2832
|
+
log.error(TAG14, `auto-commit failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
2833
|
+
return false;
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2789
2836
|
function checkHasCommits(worktreePath, baseBranch) {
|
|
2790
2837
|
try {
|
|
2791
2838
|
const count = execFileSync9("git", ["rev-list", "--count", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
@@ -6179,6 +6226,15 @@ class Worker {
|
|
|
6179
6226
|
...this.runLedger()
|
|
6180
6227
|
});
|
|
6181
6228
|
} catch {}
|
|
6229
|
+
const stopCost = this.lastSessionStats?.cost;
|
|
6230
|
+
if (stopCost) {
|
|
6231
|
+
const cents = Math.round(stopCost.totalCostUsd * 100);
|
|
6232
|
+
if (cents > 0) {
|
|
6233
|
+
try {
|
|
6234
|
+
await this.stateStore.addCost(card.id, cents);
|
|
6235
|
+
} catch {}
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
6182
6238
|
} else if (this.runId && this.verificationFailed) {
|
|
6183
6239
|
try {
|
|
6184
6240
|
await this.stateStore.endRun(this.runId, "paused", {
|
|
@@ -6661,7 +6717,7 @@ class Worker {
|
|
|
6661
6717
|
clearTimeout(this.timeoutTimer);
|
|
6662
6718
|
this.timeoutTimer = null;
|
|
6663
6719
|
}
|
|
6664
|
-
if (this.worktreePath && (this.state === "error" || this.timedOut)) {
|
|
6720
|
+
if (this.worktreePath && (this.state === "error" || this.timedOut || this.aborted)) {
|
|
6665
6721
|
try {
|
|
6666
6722
|
cleanupWorktree(this.worktreePath, this.branchName ?? undefined);
|
|
6667
6723
|
} catch {
|
|
@@ -7633,6 +7689,10 @@ class Watcher {
|
|
|
7633
7689
|
readyPromise = new Promise((resolve3) => {
|
|
7634
7690
|
this.readyResolve = resolve3;
|
|
7635
7691
|
});
|
|
7692
|
+
stopping = false;
|
|
7693
|
+
reconnectTimer = null;
|
|
7694
|
+
reconnectAttempts = 0;
|
|
7695
|
+
broadcastGen = 0;
|
|
7636
7696
|
get isConnected() {
|
|
7637
7697
|
return this.connected;
|
|
7638
7698
|
}
|
|
@@ -7661,7 +7721,34 @@ class Watcher {
|
|
|
7661
7721
|
}
|
|
7662
7722
|
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
7663
7723
|
const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
|
|
7664
|
-
|
|
7724
|
+
this.subscribeBroadcast();
|
|
7725
|
+
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
7726
|
+
log.debug(TAG32, "Presence sync");
|
|
7727
|
+
}).subscribe(async (status) => {
|
|
7728
|
+
if (status === "SUBSCRIBED") {
|
|
7729
|
+
await presenceChannel.track({
|
|
7730
|
+
daemonId: this.daemonId,
|
|
7731
|
+
startedAt: new Date().toISOString(),
|
|
7732
|
+
userId: this.identity.userId,
|
|
7733
|
+
agentId: this.identity.agentId,
|
|
7734
|
+
userEmail: this.identity.userEmail,
|
|
7735
|
+
agentIdentifier: this.identity.agentIdentifier,
|
|
7736
|
+
agentName: this.identity.agentName
|
|
7737
|
+
});
|
|
7738
|
+
if (!isPretty() || !this.suppressStartupLogs) {
|
|
7739
|
+
log.info(TAG32, "Presence tracked on board-presence channel");
|
|
7740
|
+
}
|
|
7741
|
+
this.presenceTracked = true;
|
|
7742
|
+
this.maybeResolveReady();
|
|
7743
|
+
}
|
|
7744
|
+
});
|
|
7745
|
+
this.presenceChannel = presenceChannel;
|
|
7746
|
+
}
|
|
7747
|
+
subscribeBroadcast() {
|
|
7748
|
+
if (!this.supabase)
|
|
7749
|
+
return;
|
|
7750
|
+
const gen = ++this.broadcastGen;
|
|
7751
|
+
this.channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
|
|
7665
7752
|
log.debug(TAG32, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
|
|
7666
7753
|
this.onCardBroadcast({
|
|
7667
7754
|
event: "card_update",
|
|
@@ -7682,46 +7769,54 @@ class Watcher {
|
|
|
7682
7769
|
this.onAgentCommand?.({ cardId, command });
|
|
7683
7770
|
}
|
|
7684
7771
|
}).subscribe((status) => {
|
|
7772
|
+
if (gen !== this.broadcastGen)
|
|
7773
|
+
return;
|
|
7685
7774
|
if (status === "SUBSCRIBED") {
|
|
7686
7775
|
this.connected = true;
|
|
7776
|
+
this.reconnectAttempts = 0;
|
|
7687
7777
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
7688
7778
|
log.info(TAG32, "Broadcast subscription active");
|
|
7689
7779
|
}
|
|
7690
7780
|
this.maybeResolveReady();
|
|
7691
|
-
} else if (status === "CHANNEL_ERROR") {
|
|
7692
|
-
this.connected = false;
|
|
7693
|
-
log.error(TAG32, "Broadcast channel error — will rely on reconciliation");
|
|
7694
|
-
} else if (status === "TIMED_OUT") {
|
|
7781
|
+
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
|
|
7695
7782
|
this.connected = false;
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
}
|
|
7700
|
-
});
|
|
7701
|
-
this.channel = channel;
|
|
7702
|
-
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
7703
|
-
log.debug(TAG32, "Presence sync");
|
|
7704
|
-
}).subscribe(async (status) => {
|
|
7705
|
-
if (status === "SUBSCRIBED") {
|
|
7706
|
-
await presenceChannel.track({
|
|
7707
|
-
daemonId: this.daemonId,
|
|
7708
|
-
startedAt: new Date().toISOString(),
|
|
7709
|
-
userId: this.identity.userId,
|
|
7710
|
-
agentId: this.identity.agentId,
|
|
7711
|
-
userEmail: this.identity.userEmail,
|
|
7712
|
-
agentIdentifier: this.identity.agentIdentifier,
|
|
7713
|
-
agentName: this.identity.agentName
|
|
7714
|
-
});
|
|
7715
|
-
if (!isPretty() || !this.suppressStartupLogs) {
|
|
7716
|
-
log.info(TAG32, "Presence tracked on board-presence channel");
|
|
7783
|
+
if (!this.stopping) {
|
|
7784
|
+
log.warn(TAG32, `Broadcast subscription ${status} — scheduling reconnect`);
|
|
7785
|
+
this.scheduleReconnect();
|
|
7717
7786
|
}
|
|
7718
|
-
this.presenceTracked = true;
|
|
7719
|
-
this.maybeResolveReady();
|
|
7720
7787
|
}
|
|
7721
7788
|
});
|
|
7722
|
-
|
|
7789
|
+
}
|
|
7790
|
+
scheduleReconnect() {
|
|
7791
|
+
if (this.stopping || this.reconnectTimer)
|
|
7792
|
+
return;
|
|
7793
|
+
const delay = Math.min(30000, 1000 * 2 ** this.reconnectAttempts);
|
|
7794
|
+
this.reconnectAttempts++;
|
|
7795
|
+
this.reconnectTimer = setTimeout(() => {
|
|
7796
|
+
this.reconnectTimer = null;
|
|
7797
|
+
this.reconnectBroadcast();
|
|
7798
|
+
}, delay);
|
|
7799
|
+
}
|
|
7800
|
+
async reconnectBroadcast() {
|
|
7801
|
+
if (this.stopping || !this.supabase)
|
|
7802
|
+
return;
|
|
7803
|
+
log.warn(TAG32, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
|
|
7804
|
+
if (this.channel) {
|
|
7805
|
+
const old = this.channel;
|
|
7806
|
+
this.channel = null;
|
|
7807
|
+
this.broadcastGen++;
|
|
7808
|
+
try {
|
|
7809
|
+
await this.supabase.removeChannel(old);
|
|
7810
|
+
} catch {}
|
|
7811
|
+
}
|
|
7812
|
+
this.subscribeBroadcast();
|
|
7723
7813
|
}
|
|
7724
7814
|
async stop() {
|
|
7815
|
+
this.stopping = true;
|
|
7816
|
+
if (this.reconnectTimer) {
|
|
7817
|
+
clearTimeout(this.reconnectTimer);
|
|
7818
|
+
this.reconnectTimer = null;
|
|
7819
|
+
}
|
|
7725
7820
|
if (this.presenceChannel) {
|
|
7726
7821
|
await this.supabase?.removeChannel(this.presenceChannel);
|
|
7727
7822
|
this.presenceChannel = null;
|
|
@@ -7757,7 +7852,7 @@ import { resolve as resolve3 } from "node:path";
|
|
|
7757
7852
|
function isTransientGitNetworkError(message) {
|
|
7758
7853
|
return TRANSIENT_GIT_NETWORK_ERROR.test(message);
|
|
7759
7854
|
}
|
|
7760
|
-
function
|
|
7855
|
+
function gitErrorDetail2(err) {
|
|
7761
7856
|
if (err && typeof err === "object" && "stderr" in err) {
|
|
7762
7857
|
const stderr = String(err.stderr ?? "").trim();
|
|
7763
7858
|
if (stderr)
|
|
@@ -7856,7 +7951,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
7856
7951
|
...GIT_NETWORK_EXEC
|
|
7857
7952
|
});
|
|
7858
7953
|
} catch (err) {
|
|
7859
|
-
const detail =
|
|
7954
|
+
const detail = gitErrorDetail2(err);
|
|
7860
7955
|
if (isTransientGitNetworkError(detail)) {
|
|
7861
7956
|
log.debug(TAG33, `Remote branch GC skipped — remote unreachable: ${detail}`);
|
|
7862
7957
|
return result;
|
|
@@ -7908,7 +8003,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
7908
8003
|
});
|
|
7909
8004
|
result.removed.push(ref);
|
|
7910
8005
|
} catch (err) {
|
|
7911
|
-
const detail =
|
|
8006
|
+
const detail = gitErrorDetail2(err);
|
|
7912
8007
|
if (isTransientGitNetworkError(detail)) {
|
|
7913
8008
|
log.debug(TAG33, `Remote branch GC interrupted — remote unreachable: ${detail}`);
|
|
7914
8009
|
break;
|