@gethmy/agent 1.14.4 → 1.16.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 +576 -99
- package/dist/index.js +526 -99
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -101,6 +101,16 @@ var init_log = __esm(() => {
|
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
// src/board-helpers.ts
|
|
104
|
+
var exports_board_helpers = {};
|
|
105
|
+
__export(exports_board_helpers, {
|
|
106
|
+
resolveCardLabels: () => resolveCardLabels,
|
|
107
|
+
moveCardToColumn: () => moveCardToColumn,
|
|
108
|
+
moveCardAndAddLabel: () => moveCardAndAddLabel,
|
|
109
|
+
hasLabel: () => hasLabel,
|
|
110
|
+
findOrCreateLabel: () => findOrCreateLabel,
|
|
111
|
+
buildLabelMap: () => buildLabelMap,
|
|
112
|
+
addLabelByName: () => addLabelByName
|
|
113
|
+
});
|
|
104
114
|
function buildLabelMap(boardLabels) {
|
|
105
115
|
const map = new Map;
|
|
106
116
|
for (const label of boardLabels) {
|
|
@@ -694,6 +704,22 @@ var init_config_validation = __esm(() => {
|
|
|
694
704
|
});
|
|
695
705
|
|
|
696
706
|
// src/git-pr.ts
|
|
707
|
+
var exports_git_pr = {};
|
|
708
|
+
__export(exports_git_pr, {
|
|
709
|
+
validateGitProviderCli: () => validateGitProviderCli,
|
|
710
|
+
updateExistingPr: () => updateExistingPr,
|
|
711
|
+
resolvePrUrl: () => resolvePrUrl,
|
|
712
|
+
renameRemoteBranch: () => renameRemoteBranch,
|
|
713
|
+
remoteBranchExists: () => remoteBranchExists,
|
|
714
|
+
pushBranch: () => pushBranch,
|
|
715
|
+
getBranchWebUrl: () => getBranchWebUrl,
|
|
716
|
+
findExistingPr: () => findExistingPr,
|
|
717
|
+
extractPrUrl: () => extractPrUrl,
|
|
718
|
+
detectGitProvider: () => detectGitProvider,
|
|
719
|
+
createPullRequest: () => createPullRequest,
|
|
720
|
+
checkPrMergeStatus: () => checkPrMergeStatus,
|
|
721
|
+
buildPrBody: () => buildPrBody
|
|
722
|
+
});
|
|
697
723
|
import { execFile, execFileSync } from "node:child_process";
|
|
698
724
|
import { promisify } from "node:util";
|
|
699
725
|
function detectGitProvider(cwd) {
|
|
@@ -815,6 +841,14 @@ function extractPrUrl(description) {
|
|
|
815
841
|
return null;
|
|
816
842
|
}
|
|
817
843
|
}
|
|
844
|
+
function resolvePrUrl(description, branchName, cwd, provider) {
|
|
845
|
+
const fromDesc = extractPrUrl(description);
|
|
846
|
+
if (fromDesc)
|
|
847
|
+
return fromDesc;
|
|
848
|
+
if (!branchName)
|
|
849
|
+
return null;
|
|
850
|
+
return findExistingPr(branchName, cwd, provider) || null;
|
|
851
|
+
}
|
|
818
852
|
function remoteBranchExists(branchName, cwd) {
|
|
819
853
|
try {
|
|
820
854
|
execFileSync("git", ["ls-remote", "--exit-code", "origin", `refs/heads/${branchName}`], { cwd, stdio: "pipe" });
|
|
@@ -1601,6 +1635,51 @@ var init_logger = () => {};
|
|
|
1601
1635
|
var init_playbookCatalog = () => {};
|
|
1602
1636
|
|
|
1603
1637
|
// ../harmony-shared/dist/playbookStage.js
|
|
1638
|
+
function normalizeLoopDef(raw) {
|
|
1639
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
1640
|
+
return null;
|
|
1641
|
+
const obj = raw;
|
|
1642
|
+
if (obj.mode !== "converge" && obj.mode !== "fanout")
|
|
1643
|
+
return null;
|
|
1644
|
+
const mode = obj.mode;
|
|
1645
|
+
const rawMax = obj.max_iterations;
|
|
1646
|
+
const maxInt = typeof rawMax === "number" && Number.isFinite(rawMax) && rawMax >= 1 ? Math.floor(rawMax) : DEFAULT_LOOP_MAX_ITERATIONS;
|
|
1647
|
+
const exitGate = obj.exit_gate && typeof obj.exit_gate === "object" && !Array.isArray(obj.exit_gate) ? obj.exit_gate : null;
|
|
1648
|
+
const def = { mode, max_iterations: maxInt };
|
|
1649
|
+
if (exitGate)
|
|
1650
|
+
def.exit_gate = exitGate;
|
|
1651
|
+
if (obj.item_source && typeof obj.item_source === "object" && !Array.isArray(obj.item_source)) {
|
|
1652
|
+
def.item_source = obj.item_source;
|
|
1653
|
+
}
|
|
1654
|
+
if (typeof obj.concurrency === "number" && obj.concurrency >= 1) {
|
|
1655
|
+
def.concurrency = Math.floor(obj.concurrency);
|
|
1656
|
+
}
|
|
1657
|
+
if (obj.on_item_fail === "continue" || obj.on_item_fail === "halt") {
|
|
1658
|
+
def.on_item_fail = obj.on_item_fail;
|
|
1659
|
+
}
|
|
1660
|
+
return def;
|
|
1661
|
+
}
|
|
1662
|
+
function getStageLoop(stage) {
|
|
1663
|
+
return normalizeLoopDef(stage.loop);
|
|
1664
|
+
}
|
|
1665
|
+
function isConvergeLoop(loop) {
|
|
1666
|
+
return loop !== null && loop.mode === "converge";
|
|
1667
|
+
}
|
|
1668
|
+
function resolveLoopExitGate(stage, loop) {
|
|
1669
|
+
return loop.exit_gate ?? stage.gate ?? null;
|
|
1670
|
+
}
|
|
1671
|
+
function decideLoopContinuation(args) {
|
|
1672
|
+
const { loop, gatePassed, hasExitGate, completedIterations } = args;
|
|
1673
|
+
const max = Math.max(1, Math.floor(loop.max_iterations) || 1);
|
|
1674
|
+
if (!hasExitGate) {
|
|
1675
|
+
return completedIterations >= max ? "exit" : "iterate";
|
|
1676
|
+
}
|
|
1677
|
+
if (gatePassed)
|
|
1678
|
+
return "exit";
|
|
1679
|
+
if (completedIterations >= max)
|
|
1680
|
+
return "exhausted";
|
|
1681
|
+
return "iterate";
|
|
1682
|
+
}
|
|
1604
1683
|
function readStageDefs(def) {
|
|
1605
1684
|
if (def.steps_version !== 2)
|
|
1606
1685
|
return [];
|
|
@@ -1637,7 +1716,10 @@ function entryActionAllowlist(entryAction) {
|
|
|
1637
1716
|
return `mcp__${entryAction}`;
|
|
1638
1717
|
return null;
|
|
1639
1718
|
}
|
|
1640
|
-
|
|
1719
|
+
function stageDisallowedTools() {
|
|
1720
|
+
return STAGE_DAEMON_OWNED_TOOLS.length > 0 ? STAGE_DAEMON_OWNED_TOOLS.join(",") : null;
|
|
1721
|
+
}
|
|
1722
|
+
var DEFAULT_LOOP_MAX_ITERATIONS = 5, SKILL_TOOL_ALLOWLIST, HARMONY_TOOL_RE, STAGE_DAEMON_OWNED_TOOLS;
|
|
1641
1723
|
var init_playbookStage = __esm(() => {
|
|
1642
1724
|
SKILL_TOOL_ALLOWLIST = {
|
|
1643
1725
|
hmy: "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*",
|
|
@@ -1648,6 +1730,11 @@ var init_playbookStage = __esm(() => {
|
|
|
1648
1730
|
"hmy-standup": "Read,Grep,Glob,mcp__harmony__*"
|
|
1649
1731
|
};
|
|
1650
1732
|
HARMONY_TOOL_RE = /^harmony_[a-z_]+$/;
|
|
1733
|
+
STAGE_DAEMON_OWNED_TOOLS = [
|
|
1734
|
+
"mcp__harmony__harmony_end_agent_session",
|
|
1735
|
+
"mcp__harmony__harmony_start_agent_session",
|
|
1736
|
+
"mcp__harmony__harmony_move_card"
|
|
1737
|
+
];
|
|
1651
1738
|
});
|
|
1652
1739
|
|
|
1653
1740
|
// ../harmony-shared/dist/projectTemplates.js
|
|
@@ -2082,6 +2169,58 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
2082
2169
|
} catch {}
|
|
2083
2170
|
}
|
|
2084
2171
|
}
|
|
2172
|
+
function resolveRepoRoot() {
|
|
2173
|
+
return execFileSync3("git", ["rev-parse", "--show-toplevel"], {
|
|
2174
|
+
encoding: "utf-8"
|
|
2175
|
+
}).trim();
|
|
2176
|
+
}
|
|
2177
|
+
function branchAheadOfItsRemote(branchName, repoRoot = resolveRepoRoot()) {
|
|
2178
|
+
try {
|
|
2179
|
+
const out = execFileSync3("git", ["rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8" }).trim();
|
|
2180
|
+
return out.length > 0;
|
|
2181
|
+
} catch {
|
|
2182
|
+
return false;
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
async function rescueUnpushedBranch(client, cardId, branchName, repoRoot = resolveRepoRoot()) {
|
|
2186
|
+
const { getBranchWebUrl: getBranchWebUrl2, pushBranch: pushBranch2 } = await Promise.resolve().then(() => (init_git_pr(), exports_git_pr));
|
|
2187
|
+
try {
|
|
2188
|
+
pushBranch2(branchName, repoRoot);
|
|
2189
|
+
} catch (err) {
|
|
2190
|
+
log.error(TAG5, `push-rescue failed for ${branchName} — leaving local branch ref intact (recoverable via git reflog / the local branch): ${err instanceof Error ? err.message : err}`);
|
|
2191
|
+
return false;
|
|
2192
|
+
}
|
|
2193
|
+
log.warn(TAG5, `push-rescued unpushed branch ${branchName} to origin before teardown`);
|
|
2194
|
+
try {
|
|
2195
|
+
const url = getBranchWebUrl2(branchName, repoRoot);
|
|
2196
|
+
const recover = url ? `View it at ${url} or recover locally: \`git fetch && git checkout ${branchName}\`` : `Recover it locally: \`git fetch && git checkout ${branchName}\``;
|
|
2197
|
+
const body = `⚠ Run ended before completion. Committed work was push-rescued to ` + `\`origin/${branchName}\` so it isn't lost. ${recover}`;
|
|
2198
|
+
await client.addComment(cardId, body, { commentType: "message" });
|
|
2199
|
+
} catch (err) {
|
|
2200
|
+
log.warn(TAG5, `push-rescue comment failed for ${branchName} (work is still safe on origin): ${err instanceof Error ? err.message : err}`);
|
|
2201
|
+
}
|
|
2202
|
+
return true;
|
|
2203
|
+
}
|
|
2204
|
+
async function teardownWorktree(client, cardId, worktreePath, branchName) {
|
|
2205
|
+
let skipBranchDelete = false;
|
|
2206
|
+
if (branchName && cardId) {
|
|
2207
|
+
let repoRoot;
|
|
2208
|
+
try {
|
|
2209
|
+
repoRoot = resolveRepoRoot();
|
|
2210
|
+
} catch {
|
|
2211
|
+
cleanupWorktree(worktreePath, branchName);
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
if (branchAheadOfItsRemote(branchName, repoRoot)) {
|
|
2215
|
+
const ok = await rescueUnpushedBranch(client, cardId, branchName, repoRoot);
|
|
2216
|
+
if (!ok) {
|
|
2217
|
+
skipBranchDelete = true;
|
|
2218
|
+
log.error(TAG5, `Keeping local branch ${branchName} (push-rescue failed) to avoid orphaning its commit`);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
cleanupWorktree(worktreePath, skipBranchDelete ? undefined : branchName);
|
|
2223
|
+
}
|
|
2085
2224
|
function makeBranchName(shortId, title, prefix = "agent-attempts/") {
|
|
2086
2225
|
const slug = title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
2087
2226
|
return `${prefix}${shortId}-${slug || "task"}`;
|
|
@@ -2186,6 +2325,10 @@ var init_review_worktree = __esm(() => {
|
|
|
2186
2325
|
});
|
|
2187
2326
|
|
|
2188
2327
|
// src/merge-monitor.ts
|
|
2328
|
+
var exports_merge_monitor = {};
|
|
2329
|
+
__export(exports_merge_monitor, {
|
|
2330
|
+
MergeMonitor: () => MergeMonitor
|
|
2331
|
+
});
|
|
2189
2332
|
import { execFile as execFile2 } from "node:child_process";
|
|
2190
2333
|
import { promisify as promisify2 } from "node:util";
|
|
2191
2334
|
|
|
@@ -2219,6 +2362,9 @@ class MergeMonitor {
|
|
|
2219
2362
|
}
|
|
2220
2363
|
log.info(TAG7, "Merge monitor stopped");
|
|
2221
2364
|
}
|
|
2365
|
+
async runOnce() {
|
|
2366
|
+
await this.tick();
|
|
2367
|
+
}
|
|
2222
2368
|
async scheduleNext(delayMs) {
|
|
2223
2369
|
await new Promise((resolve3) => {
|
|
2224
2370
|
this.timer = setTimeout(() => resolve3(), delayMs);
|
|
@@ -2256,9 +2402,10 @@ class MergeMonitor {
|
|
|
2256
2402
|
const batch = candidatesWithLabels.slice(0, 5);
|
|
2257
2403
|
log.debug(TAG7, `Checking ${batch.length} Ready to Merge card(s)`);
|
|
2258
2404
|
const results = await Promise.allSettled(batch.map(async ({ card, labels }) => {
|
|
2259
|
-
const
|
|
2405
|
+
const branchName = extractBranchFromDescription(card.description);
|
|
2406
|
+
const prUrl = resolvePrUrl(card.description ?? null, branchName, this.cwd, this.provider);
|
|
2260
2407
|
if (!prUrl) {
|
|
2261
|
-
log.debug(TAG7, `#${card.short_id} has no PR
|
|
2408
|
+
log.debug(TAG7, `#${card.short_id} has no resolvable PR — skipping`);
|
|
2262
2409
|
return;
|
|
2263
2410
|
}
|
|
2264
2411
|
const state = await checkPrMergeStatus(prUrl, this.cwd, this.provider);
|
|
@@ -3424,7 +3571,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3424
3571
|
failureSummary,
|
|
3425
3572
|
...buildTokenPayload(sessionStats)
|
|
3426
3573
|
});
|
|
3427
|
-
|
|
3574
|
+
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
3428
3575
|
return false;
|
|
3429
3576
|
}
|
|
3430
3577
|
log.info(TAG14, `Pushing branch ${branchName} (pre-verify)...`);
|
|
@@ -3506,7 +3653,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3506
3653
|
recoveryBranch: branchName,
|
|
3507
3654
|
...buildTokenPayload(sessionStats)
|
|
3508
3655
|
});
|
|
3509
|
-
|
|
3656
|
+
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
3510
3657
|
return false;
|
|
3511
3658
|
}
|
|
3512
3659
|
log.info(TAG14, `Verification passed for #${card.short_id}`);
|
|
@@ -3562,7 +3709,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3562
3709
|
log.warn(TAG14, `onBeforeWorktreeCleanup hook failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
3563
3710
|
}
|
|
3564
3711
|
}
|
|
3565
|
-
|
|
3712
|
+
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
3566
3713
|
log.info(TAG14, `Completion done for #${card.short_id}${prUrl ? ` — PR: ${prUrl}` : ""}`);
|
|
3567
3714
|
return true;
|
|
3568
3715
|
}
|
|
@@ -3882,6 +4029,7 @@ class SdkAgentRunner {
|
|
|
3882
4029
|
cwd: input.cwd,
|
|
3883
4030
|
model: input.model ?? this.cfg.model,
|
|
3884
4031
|
allowedTools: allowed,
|
|
4032
|
+
...this.cfg.disallowedTools && this.cfg.disallowedTools.length > 0 ? { disallowedTools: this.cfg.disallowedTools } : {},
|
|
3885
4033
|
tools: builtinTools,
|
|
3886
4034
|
permissionMode: "dontAsk",
|
|
3887
4035
|
maxTurns: this.cfg.maxTurns,
|
|
@@ -5574,6 +5722,30 @@ class StateStore {
|
|
|
5574
5722
|
rec.attempts = 0;
|
|
5575
5723
|
await this.persist();
|
|
5576
5724
|
}
|
|
5725
|
+
getLoopIterations(cardId, stageId) {
|
|
5726
|
+
const rec = this.getCard(cardId);
|
|
5727
|
+
if (!rec || rec.loopStageId !== stageId)
|
|
5728
|
+
return 0;
|
|
5729
|
+
return rec.loopIterations ?? 0;
|
|
5730
|
+
}
|
|
5731
|
+
async incrementLoopIteration(cardId, stageId) {
|
|
5732
|
+
const rec = this.ensureCard(cardId);
|
|
5733
|
+
if (rec.loopStageId !== stageId) {
|
|
5734
|
+
rec.loopStageId = stageId;
|
|
5735
|
+
rec.loopIterations = 0;
|
|
5736
|
+
}
|
|
5737
|
+
rec.loopIterations = (rec.loopIterations ?? 0) + 1;
|
|
5738
|
+
await this.persist();
|
|
5739
|
+
return rec.loopIterations;
|
|
5740
|
+
}
|
|
5741
|
+
async resetLoopIterations(cardId) {
|
|
5742
|
+
const rec = this.getCard(cardId);
|
|
5743
|
+
if (!rec || rec.loopIterations == null && rec.loopStageId == null)
|
|
5744
|
+
return;
|
|
5745
|
+
rec.loopStageId = null;
|
|
5746
|
+
rec.loopIterations = 0;
|
|
5747
|
+
await this.persist();
|
|
5748
|
+
}
|
|
5577
5749
|
async resetAttempts(cardId) {
|
|
5578
5750
|
const rec = this.getCard(cardId);
|
|
5579
5751
|
if (!rec || rec.attempts === 0)
|
|
@@ -6707,6 +6879,26 @@ class CliAgentRunner {
|
|
|
6707
6879
|
this.enqueue({ kind: "playbook_advanced", source: "system", payload });
|
|
6708
6880
|
this.startTimer();
|
|
6709
6881
|
}
|
|
6882
|
+
recordLoopIterationStarted(payload) {
|
|
6883
|
+
this.enqueue({ kind: "loop_iteration_started", source: "system", payload });
|
|
6884
|
+
this.startTimer();
|
|
6885
|
+
}
|
|
6886
|
+
recordLoopIterationEvaluated(payload) {
|
|
6887
|
+
this.enqueue({
|
|
6888
|
+
kind: "loop_iteration_evaluated",
|
|
6889
|
+
source: "system",
|
|
6890
|
+
payload: { ...payload, evidence: truncateOutput(payload.evidence) }
|
|
6891
|
+
});
|
|
6892
|
+
this.startTimer();
|
|
6893
|
+
}
|
|
6894
|
+
recordLoopCompleted(payload) {
|
|
6895
|
+
this.enqueue({ kind: "loop_completed", source: "system", payload });
|
|
6896
|
+
this.startTimer();
|
|
6897
|
+
}
|
|
6898
|
+
recordLoopExhausted(payload) {
|
|
6899
|
+
this.enqueue({ kind: "loop_exhausted", source: "system", payload });
|
|
6900
|
+
this.startTimer();
|
|
6901
|
+
}
|
|
6710
6902
|
record(body) {
|
|
6711
6903
|
this.enqueue(body);
|
|
6712
6904
|
this.startTimer();
|
|
@@ -6789,7 +6981,7 @@ async function buildPrompt(enriched, branchName, worktreePath, client, workspace
|
|
|
6789
6981
|
variant: "execute",
|
|
6790
6982
|
customConstraints: `You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
|
|
6791
6983
|
Do NOT push to main. All your work stays on \`${branchName}\`.
|
|
6792
|
-
|
|
6984
|
+
The daemon owns the run lifecycle: once your work is committed it ends the agent session, pushes the branch, and moves the card to Review for you. Do NOT call harmony_end_agent_session, do NOT start a new session, and do NOT move the card or change its column yourself. If the skill driving this work tells you to move the card or end the session as a final step, SKIP it — it is handled for you (those tools are disabled for this run). Finish the implementation, commit, and stop.`
|
|
6793
6985
|
});
|
|
6794
6986
|
log.info(TAG27, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
|
|
6795
6987
|
return result.prompt + pastEpisodesSection;
|
|
@@ -6802,7 +6994,7 @@ When finished, call harmony_end_agent_session with status="completed".`
|
|
|
6802
6994
|
}
|
|
6803
6995
|
async function renderCommentsSection(client, cardId) {
|
|
6804
6996
|
try {
|
|
6805
|
-
const { comments } = await client.
|
|
6997
|
+
const { comments } = await client.request("GET", `/cards/${encodeURIComponent(cardId)}/comments?limit=200&order=desc`);
|
|
6806
6998
|
if (!Array.isArray(comments) || comments.length === 0)
|
|
6807
6999
|
return "";
|
|
6808
7000
|
const section = serializeCommentThread(comments, {
|
|
@@ -6899,7 +7091,7 @@ ${subtaskStr}
|
|
|
6899
7091
|
Include a brief currentTask description.
|
|
6900
7092
|
3. Implement the changes on branch \`${branchName}\`
|
|
6901
7093
|
4. Commit your work with clear, descriptive commit messages
|
|
6902
|
-
5. When
|
|
7094
|
+
5. When the work is committed, STOP. The daemon owns the run lifecycle: it ends the agent session, pushes the branch, and moves the card to Review for you. Do NOT call harmony_end_agent_session, do NOT start a new session, and do NOT move the card or change its column yourself — those tools are disabled for this run.
|
|
6903
7095
|
|
|
6904
7096
|
You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
|
|
6905
7097
|
Do NOT push to main. All your work stays on \`${branchName}\`.`;
|
|
@@ -6934,6 +7126,121 @@ async function resolveStageColumnName(client, card, stage) {
|
|
|
6934
7126
|
async function persistStagePointer(client, card, body) {
|
|
6935
7127
|
await client.request("POST", `/cards/${encodeURIComponent(card.id)}/advance-stage`, body);
|
|
6936
7128
|
}
|
|
7129
|
+
async function advanceStageRun(card, stage, stageIndex, def, evaluation, deps) {
|
|
7130
|
+
const loop = getStageLoop(stage);
|
|
7131
|
+
if (!isConvergeLoop(loop)) {
|
|
7132
|
+
if (!evaluation)
|
|
7133
|
+
return { kind: "no_advance" };
|
|
7134
|
+
return advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps);
|
|
7135
|
+
}
|
|
7136
|
+
return advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loop, deps);
|
|
7137
|
+
}
|
|
7138
|
+
function firstErrorMessage(evaluation) {
|
|
7139
|
+
const e = evaluation?.findings.find((f) => f.level === "error");
|
|
7140
|
+
return e ? e.message : null;
|
|
7141
|
+
}
|
|
7142
|
+
async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loop, deps) {
|
|
7143
|
+
const hasExitGate = resolveLoopExitGate(stage, loop) != null;
|
|
7144
|
+
const gatePassed = hasExitGate ? evaluation?.passed ?? false : false;
|
|
7145
|
+
const gateResult = !hasExitGate ? "skipped" : gatePassed ? "passed" : "failed";
|
|
7146
|
+
const maxIterations = Math.max(1, Math.floor(loop.max_iterations) || 1);
|
|
7147
|
+
const iteration = await deps.stateStore.incrementLoopIteration(card.id, stage.id);
|
|
7148
|
+
const decision = decideLoopContinuation({
|
|
7149
|
+
loop,
|
|
7150
|
+
gatePassed,
|
|
7151
|
+
hasExitGate,
|
|
7152
|
+
completedIterations: iteration
|
|
7153
|
+
});
|
|
7154
|
+
const evidence = evaluation ? evaluation.findings.map((f) => `[${f.level}] ${f.message}`).join(`
|
|
7155
|
+
`) : undefined;
|
|
7156
|
+
const verdictGloss = !hasExitGate ? "count-only" : gatePassed ? "gate passed" : firstErrorMessage(evaluation) ?? "gate unmet";
|
|
7157
|
+
const summary = `iteration ${iteration}/${maxIterations} — ${verdictGloss}${decision === "iterate" ? " → retrying" : ""}`;
|
|
7158
|
+
deps.sink?.recordLoopIterationEvaluated?.({
|
|
7159
|
+
stageId: stage.id,
|
|
7160
|
+
iteration,
|
|
7161
|
+
maxIterations,
|
|
7162
|
+
gateResult,
|
|
7163
|
+
evidence,
|
|
7164
|
+
summary
|
|
7165
|
+
});
|
|
7166
|
+
log.info(TAG28, `#${card.short_id} converge loop "${stage.name}": ${summary} → ${decision}`);
|
|
7167
|
+
if (decision === "exit") {
|
|
7168
|
+
await deps.stateStore.resetLoopIterations(card.id).catch(() => {});
|
|
7169
|
+
deps.sink?.recordLoopCompleted?.({
|
|
7170
|
+
stageId: stage.id,
|
|
7171
|
+
iterations: iteration,
|
|
7172
|
+
maxIterations,
|
|
7173
|
+
reason: hasExitGate ? "exit gate passed" : `count-only loop completed ${iteration} iteration(s)`
|
|
7174
|
+
});
|
|
7175
|
+
const exitEval = evaluation?.passed ? evaluation : {
|
|
7176
|
+
passed: true,
|
|
7177
|
+
findings: [
|
|
7178
|
+
{
|
|
7179
|
+
level: "info",
|
|
7180
|
+
message: `Converge loop "${stage.name}" complete after ${iteration} iteration(s).`
|
|
7181
|
+
}
|
|
7182
|
+
],
|
|
7183
|
+
structured: evaluation?.structured ?? {}
|
|
7184
|
+
};
|
|
7185
|
+
return advanceStageOnGate(card, stage, stageIndex, def, exitEval, deps);
|
|
7186
|
+
}
|
|
7187
|
+
if (decision === "exhausted") {
|
|
7188
|
+
await deps.stateStore.resetLoopIterations(card.id).catch(() => {});
|
|
7189
|
+
await deps.stateStore.decrementAttempt(card.id).catch(() => {});
|
|
7190
|
+
const reason = `Converge loop "${stage.name}" exhausted its ${maxIterations}-iteration budget with its exit gate still unmet — ${firstErrorMessage(evaluation) ?? "gate unmet"}. Holding for a human.`;
|
|
7191
|
+
deps.sink?.recordLoopExhausted?.({
|
|
7192
|
+
stageId: stage.id,
|
|
7193
|
+
iterations: iteration,
|
|
7194
|
+
maxIterations,
|
|
7195
|
+
reason
|
|
7196
|
+
});
|
|
7197
|
+
try {
|
|
7198
|
+
await deps.client.updateAgentProgress(card.id, {
|
|
7199
|
+
agentIdentifier: "claude-code-stage",
|
|
7200
|
+
agentName: "Harmony Agent",
|
|
7201
|
+
status: "waiting",
|
|
7202
|
+
currentTask: reason
|
|
7203
|
+
});
|
|
7204
|
+
} catch {}
|
|
7205
|
+
await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, {
|
|
7206
|
+
keepAttempts: true
|
|
7207
|
+
});
|
|
7208
|
+
log.info(TAG28, `#${card.short_id} LoopExhausted: ${reason}`);
|
|
7209
|
+
return { kind: "held_gate_unmet", reason };
|
|
7210
|
+
}
|
|
7211
|
+
await deps.stateStore.decrementAttempt(card.id).catch(() => {});
|
|
7212
|
+
await writeIterationHandoff(card, stage, iteration, maxIterations, evaluation, deps);
|
|
7213
|
+
const toColumn = await resolveStageColumnName(deps.client, card, stage) ?? deps.fallbackColumn;
|
|
7214
|
+
try {
|
|
7215
|
+
await deps.client.addComment(card.id, `Converge loop — ${summary}. Re-running "${stage.name}".`, { commentType: "progress" });
|
|
7216
|
+
} catch {}
|
|
7217
|
+
await runTransition(deps.client, card, {
|
|
7218
|
+
move: { columnName: toColumn },
|
|
7219
|
+
addLabels: [{ name: AGENT_LABEL }],
|
|
7220
|
+
...isAgentRunnableOwner(stage.owner) ? { assignAgent: deps.agentId } : {}
|
|
7221
|
+
}, { store: deps.stateStore, runId: deps.runId });
|
|
7222
|
+
log.info(TAG28, `#${card.short_id} converge loop "${stage.name}" — requeued to "${toColumn}" for iteration ${iteration + 1}/${maxIterations}`);
|
|
7223
|
+
return { kind: "requeued_gate_unmet", toColumn };
|
|
7224
|
+
}
|
|
7225
|
+
async function writeIterationHandoff(card, stage, iteration, maxIterations, evaluation, deps) {
|
|
7226
|
+
try {
|
|
7227
|
+
const findings = evaluation?.findings.filter((f) => f.level !== "info") ?? [];
|
|
7228
|
+
const produced = findings.length > 0 ? `Iteration ${iteration}/${maxIterations} of the "${stage.name}" converge loop did not pass its exit gate. Findings:
|
|
7229
|
+
${findings.map((f) => `- [${f.level}] ${f.message}`).join(`
|
|
7230
|
+
`)}` : `Iteration ${iteration}/${maxIterations} of the "${stage.name}" converge loop completed; the loop continues.`;
|
|
7231
|
+
const body = buildHandoffCommentBody({
|
|
7232
|
+
stageId: stage.id,
|
|
7233
|
+
stageName: stage.name,
|
|
7234
|
+
artifactType: stage.artifact_type,
|
|
7235
|
+
produced,
|
|
7236
|
+
decisions: [],
|
|
7237
|
+
nextStageNeeds: "Address the findings above on the same branch and re-run; this stage repeats until its exit gate passes."
|
|
7238
|
+
});
|
|
7239
|
+
await deps.client.addComment(card.id, body, { commentType: "decision" });
|
|
7240
|
+
} catch (err) {
|
|
7241
|
+
log.warn(TAG28, `iteration-handoff write failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
7242
|
+
}
|
|
7243
|
+
}
|
|
6937
7244
|
async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps) {
|
|
6938
7245
|
const summary = gateSummary(stage, evaluation);
|
|
6939
7246
|
deps.sink?.recordStageGateEvaluated({
|
|
@@ -7086,7 +7393,7 @@ function buildStagePreamble(stage) {
|
|
|
7086
7393
|
lines.push(`Hand off when done: ${summary.trim()}`);
|
|
7087
7394
|
}
|
|
7088
7395
|
}
|
|
7089
|
-
lines.push("Do only this stage's work.
|
|
7396
|
+
lines.push("Do only this stage's work, then stop. The daemon owns the run lifecycle here: it ends the agent session and performs every card move and stage advancement automatically once your stage work is done. Do NOT call `harmony_end_agent_session`, do NOT start a new session, and do NOT move the card or change its column yourself. If the skill driving this stage tells you to move the card (e.g. to Review) or end your session as a final step, SKIP that step — it is handled for you. Those tools are disabled for this run, so attempting them only wastes turns. Finish the stage's work and stop.");
|
|
7090
7397
|
if (normalizeGateSpec(stage.gate)?.kind === "review_passed") {
|
|
7091
7398
|
lines.push("", "**This stage's gate is `review_passed` — your deliverable is a review verdict, not a prose handoff.** Do NOT end the session until you have actually reviewed the change and emitted EXACTLY one JSON block as the LAST thing you output (nothing after it):", "```json", REVIEW_VERDICT_SCHEMA, "```", "Decision rules:", REVIEW_DECISION_RULES, "The board reads this verdict to decide advancement: `approved` advances the card, `rejected` sends it back. A missing or unparseable verdict blocks the card. Do NOT modify any code — this is a read-only review.");
|
|
7092
7399
|
}
|
|
@@ -7099,6 +7406,13 @@ function buildSteeringPrompt(messages) {
|
|
|
7099
7406
|
return messages.map((m, i) => `${i + 1}. ${m}`).join(`
|
|
7100
7407
|
`);
|
|
7101
7408
|
}
|
|
7409
|
+
function computeRunSpawnGating(stageAllowedTools) {
|
|
7410
|
+
const denylist = stageDisallowedTools();
|
|
7411
|
+
return {
|
|
7412
|
+
...stageAllowedTools ? { allowedTools: stageAllowedTools } : {},
|
|
7413
|
+
...denylist ? { disallowedTools: denylist } : {}
|
|
7414
|
+
};
|
|
7415
|
+
}
|
|
7102
7416
|
|
|
7103
7417
|
class Worker {
|
|
7104
7418
|
config;
|
|
@@ -7127,6 +7441,7 @@ class Worker {
|
|
|
7127
7441
|
timedOut = false;
|
|
7128
7442
|
verificationFailed = false;
|
|
7129
7443
|
held = false;
|
|
7444
|
+
activeRunSpawnOpts = null;
|
|
7130
7445
|
completionStarted = false;
|
|
7131
7446
|
sessionId = null;
|
|
7132
7447
|
runId = null;
|
|
@@ -7206,6 +7521,7 @@ class Worker {
|
|
|
7206
7521
|
this.lastRunText = "";
|
|
7207
7522
|
this.cliSessionId = null;
|
|
7208
7523
|
this.lastDrainedSeq = 0;
|
|
7524
|
+
this.activeRunSpawnOpts = null;
|
|
7209
7525
|
this.cardId = card.id;
|
|
7210
7526
|
this.startedAt = Date.now();
|
|
7211
7527
|
this.runId = newRunId();
|
|
@@ -7292,7 +7608,9 @@ class Worker {
|
|
|
7292
7608
|
const basePrompt = await buildPrompt(enriched, this.branchName, this.worktreePath, this.client, this.workspaceId, this.projectId);
|
|
7293
7609
|
let prompt = basePrompt;
|
|
7294
7610
|
if (stageCtx.kind === "run") {
|
|
7295
|
-
const
|
|
7611
|
+
const loop = getStageLoop(stageCtx.stage);
|
|
7612
|
+
const isLoop = isConvergeLoop(loop);
|
|
7613
|
+
const inherited = await this.loadInheritedHandoffSection(card.id, stageCtx.stage.id, { includeOwnStage: isLoop });
|
|
7296
7614
|
prompt = [buildStagePreamble(stageCtx.stage), inherited, basePrompt].filter(Boolean).join(`
|
|
7297
7615
|
|
|
7298
7616
|
`);
|
|
@@ -7301,6 +7619,16 @@ class Worker {
|
|
|
7301
7619
|
stageName: stageCtx.stage.name,
|
|
7302
7620
|
owner: stageCtx.stage.owner
|
|
7303
7621
|
});
|
|
7622
|
+
if (isLoop && loop) {
|
|
7623
|
+
const priorIterations = this.stateStore.getLoopIterations(card.id, stageCtx.stage.id);
|
|
7624
|
+
this.cliRunner?.recordLoopIterationStarted({
|
|
7625
|
+
stageId: stageCtx.stage.id,
|
|
7626
|
+
stageName: stageCtx.stage.name,
|
|
7627
|
+
iteration: priorIterations + 1,
|
|
7628
|
+
maxIterations: Math.max(1, Math.floor(loop.max_iterations) || 1),
|
|
7629
|
+
mode: loop.mode
|
|
7630
|
+
});
|
|
7631
|
+
}
|
|
7304
7632
|
}
|
|
7305
7633
|
await this.client.updateAgentProgress(card.id, {
|
|
7306
7634
|
agentIdentifier: agentIdentifier(this.id),
|
|
@@ -7314,9 +7642,10 @@ class Worker {
|
|
|
7314
7642
|
this.timedOut = true;
|
|
7315
7643
|
this.cancel();
|
|
7316
7644
|
}, this.config.maxTimeout);
|
|
7645
|
+
this.activeRunSpawnOpts = computeRunSpawnGating(stageCtx.kind === "run" ? stageCtx.allowedTools : null);
|
|
7317
7646
|
await this.spawnClaude(prompt, card, subtasks, {
|
|
7318
7647
|
model: this.selectImplementModel(card),
|
|
7319
|
-
...
|
|
7648
|
+
...this.activeRunSpawnOpts ?? {}
|
|
7320
7649
|
});
|
|
7321
7650
|
if (this.aborted)
|
|
7322
7651
|
return;
|
|
@@ -7391,7 +7720,7 @@ class Worker {
|
|
|
7391
7720
|
}
|
|
7392
7721
|
if (this.worktreePath) {
|
|
7393
7722
|
try {
|
|
7394
|
-
|
|
7723
|
+
await teardownWorktree(this.client, card.id, this.worktreePath, this.branchName ?? undefined);
|
|
7395
7724
|
} catch {
|
|
7396
7725
|
log.warn(this.tag, "Failed to cleanup worktree before requeue");
|
|
7397
7726
|
}
|
|
@@ -7435,7 +7764,7 @@ class Worker {
|
|
|
7435
7764
|
} else if (this.runId && this.timedOut) {
|
|
7436
7765
|
if (this.worktreePath) {
|
|
7437
7766
|
try {
|
|
7438
|
-
|
|
7767
|
+
await teardownWorktree(this.client, card.id, this.worktreePath, this.branchName ?? undefined);
|
|
7439
7768
|
} catch {
|
|
7440
7769
|
log.warn(this.tag, "Failed to cleanup worktree before requeue");
|
|
7441
7770
|
}
|
|
@@ -7462,15 +7791,19 @@ class Worker {
|
|
|
7462
7791
|
} catch {}
|
|
7463
7792
|
await this.recordOutcome(card.id, "failure");
|
|
7464
7793
|
} else if (this.runId && this.aborted) {
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7794
|
+
if (!this.completionStarted) {
|
|
7795
|
+
try {
|
|
7796
|
+
await this.client.updateCard(card.id, { assignedAgentId: null });
|
|
7797
|
+
} catch (err) {
|
|
7798
|
+
log.warn(this.tag, `failed to release card after stop: ${err instanceof Error ? err.message : err}`);
|
|
7799
|
+
}
|
|
7800
|
+
try {
|
|
7801
|
+
await runTransition(this.client, card, { removeLabels: ["agent"] });
|
|
7802
|
+
} catch (tErr) {
|
|
7803
|
+
log.warn(this.tag, `stop label cleanup failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
|
|
7804
|
+
}
|
|
7805
|
+
} else {
|
|
7806
|
+
log.info(this.tag, `cancel arrived after completion on #${card.short_id} — keeping assignment so review picks it up (#585)`);
|
|
7474
7807
|
}
|
|
7475
7808
|
try {
|
|
7476
7809
|
await this.stateStore.endRun(this.runId, "paused", {
|
|
@@ -7521,7 +7854,7 @@ class Worker {
|
|
|
7521
7854
|
await this.cliRunner.flushFinal();
|
|
7522
7855
|
} catch {}
|
|
7523
7856
|
}
|
|
7524
|
-
this.cleanup();
|
|
7857
|
+
await this.cleanup();
|
|
7525
7858
|
this.state = "idle";
|
|
7526
7859
|
this.onDone(this);
|
|
7527
7860
|
}
|
|
@@ -7612,15 +7945,13 @@ class Worker {
|
|
|
7612
7945
|
} catch {}
|
|
7613
7946
|
}
|
|
7614
7947
|
}
|
|
7615
|
-
async loadInheritedHandoffSection(cardId, currentStageId) {
|
|
7948
|
+
async loadInheritedHandoffSection(cardId, currentStageId, opts = {}) {
|
|
7616
7949
|
try {
|
|
7617
|
-
const { comments } = await this.client.
|
|
7618
|
-
limit: 200
|
|
7619
|
-
});
|
|
7950
|
+
const { comments } = await this.client.request("GET", `/cards/${encodeURIComponent(cardId)}/comments?limit=200&order=desc&comment_type=decision`);
|
|
7620
7951
|
if (!Array.isArray(comments) || comments.length === 0)
|
|
7621
7952
|
return "";
|
|
7622
7953
|
const handoff = extractLatestHandoff(comments, {
|
|
7623
|
-
excludeStageId: currentStageId
|
|
7954
|
+
excludeStageId: opts.includeOwnStage ? undefined : currentStageId
|
|
7624
7955
|
});
|
|
7625
7956
|
return handoff ? renderInheritedHandoffSection(handoff) : "";
|
|
7626
7957
|
} catch (err) {
|
|
@@ -7648,7 +7979,9 @@ class Worker {
|
|
|
7648
7979
|
}
|
|
7649
7980
|
async collectStageGateEvidence(card, stage, worktreePath, subtasks) {
|
|
7650
7981
|
try {
|
|
7651
|
-
const
|
|
7982
|
+
const loop = getStageLoop(stage);
|
|
7983
|
+
const gateSource = isConvergeLoop(loop) && loop ? resolveLoopExitGate(stage, loop) : stage.gate;
|
|
7984
|
+
const gate = normalizeGateSpec(gateSource);
|
|
7652
7985
|
if (!gate) {
|
|
7653
7986
|
return null;
|
|
7654
7987
|
}
|
|
@@ -7691,11 +8024,8 @@ class Worker {
|
|
|
7691
8024
|
}
|
|
7692
8025
|
}
|
|
7693
8026
|
async advanceFromGateEvaluation(card, stage, stageIndex, def, evaluation) {
|
|
7694
|
-
if (!evaluation) {
|
|
7695
|
-
return { kind: "no_advance" };
|
|
7696
|
-
}
|
|
7697
8027
|
try {
|
|
7698
|
-
return await
|
|
8028
|
+
return await advanceStageRun(card, stage, stageIndex, def, evaluation, {
|
|
7699
8029
|
client: this.client,
|
|
7700
8030
|
stateStore: this.stateStore,
|
|
7701
8031
|
agentId: this.agentId,
|
|
@@ -7948,7 +8278,8 @@ class Worker {
|
|
|
7948
8278
|
await this.spawnClaude(buildSteeringPrompt(messages.map((m) => m.text)), card, subtasks, {
|
|
7949
8279
|
model: this.selectImplementModel(card),
|
|
7950
8280
|
maxTurns: STEERING_MAX_TURNS,
|
|
7951
|
-
resumeSessionId: this.cliSessionId
|
|
8281
|
+
resumeSessionId: this.cliSessionId,
|
|
8282
|
+
...this.activeRunSpawnOpts ?? {}
|
|
7952
8283
|
});
|
|
7953
8284
|
} catch (err) {
|
|
7954
8285
|
log.warn(this.tag, `Steering resume failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
@@ -7976,6 +8307,7 @@ class Worker {
|
|
|
7976
8307
|
String(maxTurns),
|
|
7977
8308
|
"--allowedTools",
|
|
7978
8309
|
allowedTools,
|
|
8310
|
+
...opts.disallowedTools ? ["--disallowedTools", opts.disallowedTools] : [],
|
|
7979
8311
|
...opts.resumeSessionId ? ["--resume", opts.resumeSessionId] : [],
|
|
7980
8312
|
...this.config.claude.additionalArgs,
|
|
7981
8313
|
"--",
|
|
@@ -8063,6 +8395,7 @@ class Worker {
|
|
|
8063
8395
|
const model = opts.model ?? this.config.claude.model;
|
|
8064
8396
|
const maxTurns = opts.maxTurns ?? this.config.claude.maxTurns;
|
|
8065
8397
|
const allowedTools = (opts.allowedTools ?? IMPLEMENT_ALLOWED_TOOLS).split(",").map((t) => t.trim()).filter(Boolean);
|
|
8398
|
+
const disallowedTools = opts.disallowedTools ? opts.disallowedTools.split(",").map((t) => t.trim()).filter(Boolean) : undefined;
|
|
8066
8399
|
const initialPhase = opts.initialPhase ?? "exploring";
|
|
8067
8400
|
const sdkCfg = this.config.sdk;
|
|
8068
8401
|
log.info(this.tag, `Spawning Agent SDK runner (model=${model}, maxTurns=${maxTurns}${opts.resumeSessionId ? ", resume" : ""})`);
|
|
@@ -8082,6 +8415,7 @@ class Worker {
|
|
|
8082
8415
|
model,
|
|
8083
8416
|
maxTurns,
|
|
8084
8417
|
allowedTools,
|
|
8418
|
+
...disallowedTools ? { disallowedTools } : {},
|
|
8085
8419
|
maxBudgetUsd: sdkCfg?.maxBudgetUsd,
|
|
8086
8420
|
settingSources: sdkCfg?.settingSources,
|
|
8087
8421
|
mcpServers: sdkCfg?.mcpServers,
|
|
@@ -8148,7 +8482,7 @@ class Worker {
|
|
|
8148
8482
|
throw err;
|
|
8149
8483
|
}
|
|
8150
8484
|
}
|
|
8151
|
-
cleanup() {
|
|
8485
|
+
async cleanup() {
|
|
8152
8486
|
if (this.progressTracker) {
|
|
8153
8487
|
this.progressTracker.stop();
|
|
8154
8488
|
this.progressTracker = null;
|
|
@@ -8166,7 +8500,7 @@ class Worker {
|
|
|
8166
8500
|
}
|
|
8167
8501
|
if (this.worktreePath && (this.state === "error" || this.timedOut || this.aborted)) {
|
|
8168
8502
|
try {
|
|
8169
|
-
|
|
8503
|
+
await teardownWorktree(this.client, this.cardId, this.worktreePath, this.branchName ?? undefined);
|
|
8170
8504
|
} catch {
|
|
8171
8505
|
log.warn(this.tag, "Failed to cleanup worktree");
|
|
8172
8506
|
}
|
|
@@ -8647,7 +8981,7 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
8647
8981
|
}
|
|
8648
8982
|
if (run.worktreePath) {
|
|
8649
8983
|
try {
|
|
8650
|
-
|
|
8984
|
+
await teardownWorktree(client, run.cardId, run.worktreePath, run.branchName ?? undefined);
|
|
8651
8985
|
outcome.actions.push("cleaned up worktree");
|
|
8652
8986
|
} catch (err) {
|
|
8653
8987
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -8671,6 +9005,71 @@ var init_recovery = __esm(() => {
|
|
|
8671
9005
|
init_worktree();
|
|
8672
9006
|
});
|
|
8673
9007
|
|
|
9008
|
+
// src/strand-recovery.ts
|
|
9009
|
+
var exports_strand_recovery = {};
|
|
9010
|
+
__export(exports_strand_recovery, {
|
|
9011
|
+
reclaimPreReviewStrands: () => reclaimPreReviewStrands
|
|
9012
|
+
});
|
|
9013
|
+
async function reclaimPreReviewStrands(opts) {
|
|
9014
|
+
const {
|
|
9015
|
+
client,
|
|
9016
|
+
agentId,
|
|
9017
|
+
cards,
|
|
9018
|
+
columns,
|
|
9019
|
+
labelMap,
|
|
9020
|
+
reviewColumns,
|
|
9021
|
+
approvedLabel,
|
|
9022
|
+
graceMs,
|
|
9023
|
+
knownCardIds,
|
|
9024
|
+
cwd,
|
|
9025
|
+
provider
|
|
9026
|
+
} = opts;
|
|
9027
|
+
const now = opts.now ?? Date.now();
|
|
9028
|
+
const maxPerSweep = opts.maxPerSweep ?? 5;
|
|
9029
|
+
const reviewColIds = new Set(columns.filter((c) => reviewColumns.some((n) => n.toLowerCase() === c.name.toLowerCase())).map((c) => c.id));
|
|
9030
|
+
if (reviewColIds.size === 0)
|
|
9031
|
+
return [];
|
|
9032
|
+
const reclaimed = [];
|
|
9033
|
+
let checked = 0;
|
|
9034
|
+
for (const card of cards) {
|
|
9035
|
+
if (checked >= maxPerSweep)
|
|
9036
|
+
break;
|
|
9037
|
+
if (card.archived_at || !reviewColIds.has(card.column_id) || card.assigned_agent_id != null || card.assignee_id != null || knownCardIds.has(card.id)) {
|
|
9038
|
+
continue;
|
|
9039
|
+
}
|
|
9040
|
+
const branch = extractBranchFromDescription(card.description);
|
|
9041
|
+
if (!branch)
|
|
9042
|
+
continue;
|
|
9043
|
+
const labels = resolveCardLabels(card, labelMap);
|
|
9044
|
+
if (hasLabel(labels, approvedLabel) || hasLabel(labels, NEED_REVIEW_LABEL)) {
|
|
9045
|
+
continue;
|
|
9046
|
+
}
|
|
9047
|
+
const stalledAt = Date.parse(card.updated_at ?? "");
|
|
9048
|
+
if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
|
|
9049
|
+
continue;
|
|
9050
|
+
checked++;
|
|
9051
|
+
const prUrl = resolvePrUrl(card.description ?? null, branch, cwd, provider);
|
|
9052
|
+
if (prUrl)
|
|
9053
|
+
continue;
|
|
9054
|
+
log.warn(TAG33, `#${card.short_id} stranded in review (branch pushed, no PR, unowned) — re-asserting daemon assignment`);
|
|
9055
|
+
try {
|
|
9056
|
+
await client.updateCard(card.id, { assignedAgentId: agentId });
|
|
9057
|
+
reclaimed.push(card.id);
|
|
9058
|
+
} catch (err) {
|
|
9059
|
+
log.error(TAG33, `review re-claim failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
9060
|
+
}
|
|
9061
|
+
}
|
|
9062
|
+
return reclaimed;
|
|
9063
|
+
}
|
|
9064
|
+
var TAG33 = "strand-recovery";
|
|
9065
|
+
var init_strand_recovery = __esm(() => {
|
|
9066
|
+
init_board_helpers();
|
|
9067
|
+
init_git_pr();
|
|
9068
|
+
init_log();
|
|
9069
|
+
init_review_worktree();
|
|
9070
|
+
init_types();
|
|
9071
|
+
});
|
|
9072
|
+
|
|
8674
9073
|
// src/reconcile.ts
|
|
8675
9074
|
class Reconciler {
|
|
8676
9075
|
client;
|
|
@@ -8685,6 +9084,7 @@ class Reconciler {
|
|
|
8685
9084
|
agentConfig;
|
|
8686
9085
|
timer = null;
|
|
8687
9086
|
lastTickAt = null;
|
|
9087
|
+
gitProvider = null;
|
|
8688
9088
|
get lastTick() {
|
|
8689
9089
|
return this.lastTickAt;
|
|
8690
9090
|
}
|
|
@@ -8712,7 +9112,7 @@ class Reconciler {
|
|
|
8712
9112
|
clearInterval(this.timer);
|
|
8713
9113
|
this.timer = null;
|
|
8714
9114
|
}
|
|
8715
|
-
log.info(
|
|
9115
|
+
log.info(TAG34, "Heartbeat stopped");
|
|
8716
9116
|
}
|
|
8717
9117
|
async recoverStaleRuns() {
|
|
8718
9118
|
if (!this.stateStore || !this.agentConfig)
|
|
@@ -8729,7 +9129,7 @@ class Reconciler {
|
|
|
8729
9129
|
if (!daemonDead && !(heartbeatStale && ourZombie))
|
|
8730
9130
|
continue;
|
|
8731
9131
|
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`;
|
|
8732
|
-
log.warn(
|
|
9132
|
+
log.warn(TAG34, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
|
|
8733
9133
|
await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
|
|
8734
9134
|
runId: run.runId,
|
|
8735
9135
|
cardId: run.cardId,
|
|
@@ -8756,13 +9156,37 @@ class Reconciler {
|
|
|
8756
9156
|
const stalledAt = Date.parse(card.updated_at ?? "");
|
|
8757
9157
|
if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
|
|
8758
9158
|
continue;
|
|
8759
|
-
log.warn(
|
|
9159
|
+
log.warn(TAG34, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
|
|
8760
9160
|
try {
|
|
8761
9161
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
8762
9162
|
} catch (err) {
|
|
8763
|
-
log.error(
|
|
9163
|
+
log.error(TAG34, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
9164
|
+
}
|
|
9165
|
+
}
|
|
9166
|
+
}
|
|
9167
|
+
async recoverStrandedReview(cards, columns, labelMap, knownCardIds) {
|
|
9168
|
+
if (this.reviewColumns.length === 0)
|
|
9169
|
+
return;
|
|
9170
|
+
if (!this.gitProvider) {
|
|
9171
|
+
try {
|
|
9172
|
+
this.gitProvider = detectGitProvider();
|
|
9173
|
+
} catch {
|
|
9174
|
+
return;
|
|
8764
9175
|
}
|
|
8765
9176
|
}
|
|
9177
|
+
await reclaimPreReviewStrands({
|
|
9178
|
+
client: this.client,
|
|
9179
|
+
agentId: this.agentId,
|
|
9180
|
+
cards,
|
|
9181
|
+
columns,
|
|
9182
|
+
labelMap,
|
|
9183
|
+
reviewColumns: this.reviewColumns,
|
|
9184
|
+
approvedLabel: this.approvedLabel,
|
|
9185
|
+
graceMs: this.agentConfig?.timing.staleHeartbeatMs ?? 120000,
|
|
9186
|
+
knownCardIds,
|
|
9187
|
+
cwd: process.cwd(),
|
|
9188
|
+
provider: this.gitProvider
|
|
9189
|
+
});
|
|
8766
9190
|
}
|
|
8767
9191
|
async releaseStalledApprovals(cards, columns, knownCardIds) {
|
|
8768
9192
|
const planning = this.agentConfig?.planning;
|
|
@@ -8783,11 +9207,11 @@ class Reconciler {
|
|
|
8783
9207
|
const parkedAt = Date.parse(card.updated_at ?? "");
|
|
8784
9208
|
if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
|
|
8785
9209
|
continue;
|
|
8786
|
-
log.warn(
|
|
9210
|
+
log.warn(TAG34, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
|
|
8787
9211
|
try {
|
|
8788
9212
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
8789
9213
|
} catch (err) {
|
|
8790
|
-
log.error(
|
|
9214
|
+
log.error(TAG34, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
8791
9215
|
}
|
|
8792
9216
|
}
|
|
8793
9217
|
}
|
|
@@ -8830,21 +9254,21 @@ class Reconciler {
|
|
|
8830
9254
|
const subtasks = card.subtasks ?? [];
|
|
8831
9255
|
const mode = route.mode;
|
|
8832
9256
|
if (route.stage) {
|
|
8833
|
-
log.info(
|
|
9257
|
+
log.info(TAG34, `Stage card #${card.short_id} (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement) regardless of column`);
|
|
8834
9258
|
}
|
|
8835
9259
|
if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
|
|
8836
|
-
log.debug(
|
|
9260
|
+
log.debug(TAG34, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
|
|
8837
9261
|
continue;
|
|
8838
9262
|
}
|
|
8839
9263
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
8840
|
-
log.debug(
|
|
9264
|
+
log.debug(TAG34, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
|
|
8841
9265
|
continue;
|
|
8842
9266
|
}
|
|
8843
9267
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
8844
|
-
log.debug(
|
|
9268
|
+
log.debug(TAG34, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
|
|
8845
9269
|
continue;
|
|
8846
9270
|
}
|
|
8847
|
-
log.info(
|
|
9271
|
+
log.info(TAG34, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
|
|
8848
9272
|
await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
8849
9273
|
}
|
|
8850
9274
|
}
|
|
@@ -8852,25 +9276,28 @@ class Reconciler {
|
|
|
8852
9276
|
await this.recoverStaleRuns();
|
|
8853
9277
|
}
|
|
8854
9278
|
await this.recoverStrandedInProgress(cards, columns, knownCardIds);
|
|
9279
|
+
await this.recoverStrandedReview(cards, columns, labelMap, knownCardIds);
|
|
8855
9280
|
for (const knownId of knownCardIds) {
|
|
8856
9281
|
if (!allAgentCardIds.has(knownId)) {
|
|
8857
|
-
log.info(
|
|
9282
|
+
log.info(TAG34, `Missed unassign: ${knownId} — removing`);
|
|
8858
9283
|
await this.pool.removeCard(knownId);
|
|
8859
9284
|
}
|
|
8860
9285
|
}
|
|
8861
9286
|
await this.releaseStalledApprovals(cards, columns, knownCardIds);
|
|
8862
|
-
log.debug(
|
|
9287
|
+
log.debug(TAG34, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
|
|
8863
9288
|
} catch (err) {
|
|
8864
|
-
log.error(
|
|
9289
|
+
log.error(TAG34, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
|
|
8865
9290
|
}
|
|
8866
9291
|
}
|
|
8867
9292
|
}
|
|
8868
|
-
var
|
|
9293
|
+
var TAG34 = "reconcile";
|
|
8869
9294
|
var init_reconcile = __esm(() => {
|
|
8870
9295
|
init_board_helpers();
|
|
9296
|
+
init_git_pr();
|
|
8871
9297
|
init_log();
|
|
8872
9298
|
init_recovery();
|
|
8873
9299
|
init_review_worktree();
|
|
9300
|
+
init_strand_recovery();
|
|
8874
9301
|
init_types();
|
|
8875
9302
|
});
|
|
8876
9303
|
|
|
@@ -8903,7 +9330,7 @@ function prettyBanner(config, version) {
|
|
|
8903
9330
|
checks.push({ kind: "ok", message });
|
|
8904
9331
|
},
|
|
8905
9332
|
warn(message) {
|
|
8906
|
-
log.warn(
|
|
9333
|
+
log.warn(TAG35, message);
|
|
8907
9334
|
checks.push({ kind: "warn", message: message.split(`
|
|
8908
9335
|
`, 1)[0] });
|
|
8909
9336
|
},
|
|
@@ -8928,25 +9355,25 @@ function prettyBanner(config, version) {
|
|
|
8928
9355
|
};
|
|
8929
9356
|
}
|
|
8930
9357
|
function jsonBanner(config, version) {
|
|
8931
|
-
log.info(
|
|
8932
|
-
log.info(
|
|
9358
|
+
log.info(TAG35, `Harmony Agent Daemon v${version} starting...`);
|
|
9359
|
+
log.info(TAG35, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
|
|
8933
9360
|
if (config.agent.review.enabled) {
|
|
8934
|
-
log.info(
|
|
9361
|
+
log.info(TAG35, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
|
|
8935
9362
|
}
|
|
8936
9363
|
let failed = false;
|
|
8937
9364
|
return {
|
|
8938
9365
|
setProjectName(_name) {},
|
|
8939
9366
|
setGitProvider(provider) {
|
|
8940
|
-
log.info(
|
|
9367
|
+
log.info(TAG35, `Git provider: ${provider}`);
|
|
8941
9368
|
},
|
|
8942
9369
|
setHttpPort(port) {
|
|
8943
|
-
log.info(
|
|
9370
|
+
log.info(TAG35, `HTTP server on port ${port}`);
|
|
8944
9371
|
},
|
|
8945
9372
|
check(message) {
|
|
8946
|
-
log.info(
|
|
9373
|
+
log.info(TAG35, message);
|
|
8947
9374
|
},
|
|
8948
9375
|
warn(message) {
|
|
8949
|
-
log.warn(
|
|
9376
|
+
log.warn(TAG35, message);
|
|
8950
9377
|
},
|
|
8951
9378
|
fail() {
|
|
8952
9379
|
failed = true;
|
|
@@ -8954,7 +9381,7 @@ function jsonBanner(config, version) {
|
|
|
8954
9381
|
async ready(message) {
|
|
8955
9382
|
if (failed)
|
|
8956
9383
|
return;
|
|
8957
|
-
log.info(
|
|
9384
|
+
log.info(TAG35, message);
|
|
8958
9385
|
}
|
|
8959
9386
|
};
|
|
8960
9387
|
}
|
|
@@ -9035,7 +9462,7 @@ function cyan(s) {
|
|
|
9035
9462
|
function yellow(s) {
|
|
9036
9463
|
return `${ANSI.yellow}${s}${ANSI.reset}`;
|
|
9037
9464
|
}
|
|
9038
|
-
var
|
|
9465
|
+
var TAG35 = "daemon", RULE_WIDTH = 70, ANSI;
|
|
9039
9466
|
var init_startup_banner = __esm(() => {
|
|
9040
9467
|
init_log();
|
|
9041
9468
|
ANSI = {
|
|
@@ -9186,13 +9613,13 @@ class Watcher {
|
|
|
9186
9613
|
}
|
|
9187
9614
|
async start() {
|
|
9188
9615
|
if (!isPretty()) {
|
|
9189
|
-
log.info(
|
|
9616
|
+
log.info(TAG36, "Connecting to Supabase realtime (broadcast)...");
|
|
9190
9617
|
}
|
|
9191
9618
|
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
9192
9619
|
const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
|
|
9193
9620
|
this.subscribeBroadcast();
|
|
9194
9621
|
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
9195
|
-
log.debug(
|
|
9622
|
+
log.debug(TAG36, "Presence sync");
|
|
9196
9623
|
}).subscribe(async (status) => {
|
|
9197
9624
|
if (status === "SUBSCRIBED") {
|
|
9198
9625
|
await presenceChannel.track({
|
|
@@ -9205,7 +9632,7 @@ class Watcher {
|
|
|
9205
9632
|
agentName: this.identity.agentName
|
|
9206
9633
|
});
|
|
9207
9634
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
9208
|
-
log.info(
|
|
9635
|
+
log.info(TAG36, "Presence tracked on board-presence channel");
|
|
9209
9636
|
}
|
|
9210
9637
|
this.presenceTracked = true;
|
|
9211
9638
|
this.maybeResolveReady();
|
|
@@ -9218,13 +9645,13 @@ class Watcher {
|
|
|
9218
9645
|
return;
|
|
9219
9646
|
const gen = ++this.broadcastGen;
|
|
9220
9647
|
this.channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
|
|
9221
|
-
log.debug(
|
|
9648
|
+
log.debug(TAG36, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
|
|
9222
9649
|
this.onCardBroadcast({
|
|
9223
9650
|
event: "card_update",
|
|
9224
9651
|
payload: msg.payload ?? {}
|
|
9225
9652
|
});
|
|
9226
9653
|
}).on("broadcast", { event: "card_created" }, (msg) => {
|
|
9227
|
-
log.debug(
|
|
9654
|
+
log.debug(TAG36, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
|
|
9228
9655
|
this.onCardBroadcast({
|
|
9229
9656
|
event: "card_created",
|
|
9230
9657
|
payload: msg.payload ?? {}
|
|
@@ -9234,7 +9661,7 @@ class Watcher {
|
|
|
9234
9661
|
const cardId = payload.card_id;
|
|
9235
9662
|
const command = payload.command;
|
|
9236
9663
|
if (cardId && command) {
|
|
9237
|
-
log.info(
|
|
9664
|
+
log.info(TAG36, `Broadcast: agent_command ${command} for ${cardId}`);
|
|
9238
9665
|
this.onAgentCommand?.({ cardId, command });
|
|
9239
9666
|
}
|
|
9240
9667
|
}).subscribe((status) => {
|
|
@@ -9244,13 +9671,13 @@ class Watcher {
|
|
|
9244
9671
|
this.connected = true;
|
|
9245
9672
|
this.reconnectAttempts = 0;
|
|
9246
9673
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
9247
|
-
log.info(
|
|
9674
|
+
log.info(TAG36, "Broadcast subscription active");
|
|
9248
9675
|
}
|
|
9249
9676
|
this.maybeResolveReady();
|
|
9250
9677
|
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
|
|
9251
9678
|
this.connected = false;
|
|
9252
9679
|
if (!this.stopping) {
|
|
9253
|
-
log.warn(
|
|
9680
|
+
log.warn(TAG36, `Broadcast subscription ${status} — scheduling reconnect`);
|
|
9254
9681
|
this.scheduleReconnect();
|
|
9255
9682
|
}
|
|
9256
9683
|
}
|
|
@@ -9269,7 +9696,7 @@ class Watcher {
|
|
|
9269
9696
|
async reconnectBroadcast() {
|
|
9270
9697
|
if (this.stopping || !this.supabase)
|
|
9271
9698
|
return;
|
|
9272
|
-
log.warn(
|
|
9699
|
+
log.warn(TAG36, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
|
|
9273
9700
|
if (this.channel) {
|
|
9274
9701
|
const old = this.channel;
|
|
9275
9702
|
this.channel = null;
|
|
@@ -9299,10 +9726,10 @@ class Watcher {
|
|
|
9299
9726
|
this.supabase = null;
|
|
9300
9727
|
}
|
|
9301
9728
|
this.connected = false;
|
|
9302
|
-
log.info(
|
|
9729
|
+
log.info(TAG36, "Broadcast subscription stopped");
|
|
9303
9730
|
}
|
|
9304
9731
|
}
|
|
9305
|
-
var
|
|
9732
|
+
var TAG36 = "watcher";
|
|
9306
9733
|
var init_watcher = __esm(() => {
|
|
9307
9734
|
init_log();
|
|
9308
9735
|
});
|
|
@@ -9389,10 +9816,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
|
|
|
9389
9816
|
});
|
|
9390
9817
|
} catch {}
|
|
9391
9818
|
if (result.removed.length > 0) {
|
|
9392
|
-
log.info(
|
|
9819
|
+
log.info(TAG37, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
|
|
9393
9820
|
}
|
|
9394
9821
|
if (result.errors.length > 0) {
|
|
9395
|
-
log.warn(
|
|
9822
|
+
log.warn(TAG37, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
|
|
9396
9823
|
}
|
|
9397
9824
|
return result;
|
|
9398
9825
|
}
|
|
@@ -9422,7 +9849,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
9422
9849
|
} catch (err) {
|
|
9423
9850
|
const detail = gitErrorDetail2(err);
|
|
9424
9851
|
if (isTransientGitNetworkError(detail)) {
|
|
9425
|
-
log.debug(
|
|
9852
|
+
log.debug(TAG37, `Remote branch GC skipped — remote unreachable: ${detail}`);
|
|
9426
9853
|
return result;
|
|
9427
9854
|
}
|
|
9428
9855
|
result.errors.push({ ref: "fetch", error: detail });
|
|
@@ -9461,7 +9888,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
9461
9888
|
continue;
|
|
9462
9889
|
}
|
|
9463
9890
|
if (clock() > sweepDeadline) {
|
|
9464
|
-
log.debug(
|
|
9891
|
+
log.debug(TAG37, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
|
|
9465
9892
|
break;
|
|
9466
9893
|
}
|
|
9467
9894
|
try {
|
|
@@ -9474,17 +9901,17 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
9474
9901
|
} catch (err) {
|
|
9475
9902
|
const detail = gitErrorDetail2(err);
|
|
9476
9903
|
if (isTransientGitNetworkError(detail)) {
|
|
9477
|
-
log.debug(
|
|
9904
|
+
log.debug(TAG37, `Remote branch GC interrupted — remote unreachable: ${detail}`);
|
|
9478
9905
|
break;
|
|
9479
9906
|
}
|
|
9480
9907
|
result.errors.push({ ref, error: detail });
|
|
9481
9908
|
}
|
|
9482
9909
|
}
|
|
9483
9910
|
if (result.removed.length > 0) {
|
|
9484
|
-
log.info(
|
|
9911
|
+
log.info(TAG37, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
|
|
9485
9912
|
}
|
|
9486
9913
|
if (result.errors.length > 0) {
|
|
9487
|
-
log.warn(
|
|
9914
|
+
log.warn(TAG37, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
|
|
9488
9915
|
}
|
|
9489
9916
|
return result;
|
|
9490
9917
|
}
|
|
@@ -9515,13 +9942,13 @@ class WorktreeGc {
|
|
|
9515
9942
|
try {
|
|
9516
9943
|
runWorktreeGc(this.basePath, this.store);
|
|
9517
9944
|
} catch (err) {
|
|
9518
|
-
log.warn(
|
|
9945
|
+
log.warn(TAG37, `GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
9519
9946
|
}
|
|
9520
9947
|
if (this.remoteOpts) {
|
|
9521
9948
|
try {
|
|
9522
9949
|
pruneFailedRemoteBranches(this.remoteOpts);
|
|
9523
9950
|
} catch (err) {
|
|
9524
|
-
log.warn(
|
|
9951
|
+
log.warn(TAG37, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
9525
9952
|
}
|
|
9526
9953
|
}
|
|
9527
9954
|
}
|
|
@@ -9535,7 +9962,7 @@ function getRepoRoot2() {
|
|
|
9535
9962
|
return null;
|
|
9536
9963
|
}
|
|
9537
9964
|
}
|
|
9538
|
-
var
|
|
9965
|
+
var TAG37 = "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;
|
|
9539
9966
|
var init_worktree_gc = __esm(() => {
|
|
9540
9967
|
init_log();
|
|
9541
9968
|
init_worktree();
|
|
@@ -9639,7 +10066,7 @@ async function main() {
|
|
|
9639
10066
|
} catch (err) {
|
|
9640
10067
|
if (err instanceof ConfigValidationError) {
|
|
9641
10068
|
banner.fail();
|
|
9642
|
-
log.error(
|
|
10069
|
+
log.error(TAG38, err.message);
|
|
9643
10070
|
process.exit(1);
|
|
9644
10071
|
}
|
|
9645
10072
|
throw err;
|
|
@@ -9749,7 +10176,7 @@ async function main() {
|
|
|
9749
10176
|
if (shuttingDown)
|
|
9750
10177
|
return;
|
|
9751
10178
|
shuttingDown = true;
|
|
9752
|
-
log.info(
|
|
10179
|
+
log.info(TAG38, `Received ${signal}, shutting down gracefully...`);
|
|
9753
10180
|
reconciler.stop();
|
|
9754
10181
|
mergeMonitor?.stop();
|
|
9755
10182
|
worktreeGc.stop();
|
|
@@ -9759,18 +10186,18 @@ async function main() {
|
|
|
9759
10186
|
}
|
|
9760
10187
|
await watcher.stop();
|
|
9761
10188
|
await pool.shutdown();
|
|
9762
|
-
log.info(
|
|
10189
|
+
log.info(TAG38, "Daemon stopped.");
|
|
9763
10190
|
process.exit(exitCode);
|
|
9764
10191
|
};
|
|
9765
10192
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
9766
10193
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
9767
10194
|
process.on("uncaughtException", (err) => {
|
|
9768
|
-
log.error(
|
|
10195
|
+
log.error(TAG38, `Uncaught exception: ${err.message}`);
|
|
9769
10196
|
exitCode = 1;
|
|
9770
10197
|
shutdown("uncaughtException");
|
|
9771
10198
|
});
|
|
9772
10199
|
process.on("unhandledRejection", (reason) => {
|
|
9773
|
-
log.error(
|
|
10200
|
+
log.error(TAG38, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
9774
10201
|
exitCode = 1;
|
|
9775
10202
|
shutdown("unhandledRejection");
|
|
9776
10203
|
});
|
|
@@ -9823,29 +10250,29 @@ async function handleBroadcast(event, client, pool, config, agentId) {
|
|
|
9823
10250
|
if (assignedAgentId === undefined)
|
|
9824
10251
|
return;
|
|
9825
10252
|
if (assignedAgentId === agentId) {
|
|
9826
|
-
log.info(
|
|
10253
|
+
log.info(TAG38, `Broadcast: card ${cardId} assigned to agent`);
|
|
9827
10254
|
try {
|
|
9828
10255
|
await pool.resetAttemptsForReassign(cardId);
|
|
9829
10256
|
await tryEnqueueCard(cardId, client, pool, config, agentId);
|
|
9830
10257
|
} catch (err) {
|
|
9831
|
-
log.error(
|
|
10258
|
+
log.error(TAG38, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
|
|
9832
10259
|
}
|
|
9833
10260
|
} else if (pool.isCardKnown(cardId)) {
|
|
9834
|
-
log.info(
|
|
10261
|
+
log.info(TAG38, `Broadcast: card ${cardId} unassigned from agent`);
|
|
9835
10262
|
await pool.removeCard(cardId);
|
|
9836
10263
|
}
|
|
9837
10264
|
}
|
|
9838
10265
|
async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
9839
10266
|
const { card } = await client.getCard(cardId);
|
|
9840
10267
|
if (card.assigned_agent_id !== agentId) {
|
|
9841
|
-
log.debug(
|
|
10268
|
+
log.debug(TAG38, `Card ${cardId} no longer assigned to agent — skipping`);
|
|
9842
10269
|
return;
|
|
9843
10270
|
}
|
|
9844
10271
|
const board = await client.getBoard(config.projectId, { summary: true });
|
|
9845
10272
|
const columns = board.columns;
|
|
9846
10273
|
const column = columns.find((c) => c.id === card.column_id);
|
|
9847
10274
|
if (!column) {
|
|
9848
|
-
log.warn(
|
|
10275
|
+
log.warn(TAG38, `Column not found for card ${cardId}`);
|
|
9849
10276
|
return;
|
|
9850
10277
|
}
|
|
9851
10278
|
const route = classifyPickup(card, column.name, {
|
|
@@ -9854,27 +10281,27 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
9854
10281
|
playbooks: config.agent.playbooks
|
|
9855
10282
|
});
|
|
9856
10283
|
if (!route) {
|
|
9857
|
-
log.info(
|
|
10284
|
+
log.info(TAG38, `Card #${card.short_id} is in "${column.name}", not a pickup/review/stage column — skipping`);
|
|
9858
10285
|
return;
|
|
9859
10286
|
}
|
|
9860
10287
|
if (route.stage) {
|
|
9861
|
-
log.info(
|
|
10288
|
+
log.info(TAG38, `Card #${card.short_id} is a playbook stage card (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement pool) regardless of column`);
|
|
9862
10289
|
}
|
|
9863
10290
|
const mode = route.mode;
|
|
9864
10291
|
const labelMap = buildLabelMap(board.labels ?? []);
|
|
9865
10292
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
9866
10293
|
const subtasks = card.subtasks ?? [];
|
|
9867
10294
|
if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
|
|
9868
|
-
log.debug(
|
|
10295
|
+
log.debug(TAG38, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
|
|
9869
10296
|
return;
|
|
9870
10297
|
}
|
|
9871
10298
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
9872
|
-
log.info(
|
|
10299
|
+
log.info(TAG38, `Card #${card.short_id} has no branch reference — skipping auto-review`);
|
|
9873
10300
|
return;
|
|
9874
10301
|
}
|
|
9875
10302
|
await pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
9876
10303
|
}
|
|
9877
|
-
var
|
|
10304
|
+
var TAG38 = "daemon", PKG_VERSION;
|
|
9878
10305
|
var init_src = __esm(() => {
|
|
9879
10306
|
init_board_helpers();
|
|
9880
10307
|
init_config();
|