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