@gethmy/agent 1.14.4 → 1.15.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 +484 -85
- package/dist/index.js +434 -85
- 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,48 @@ 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 decideLoopContinuation(args) {
|
|
1670
|
+
const { loop, gatePassed, hasExitGate, completedIterations } = args;
|
|
1671
|
+
const max = Math.max(1, Math.floor(loop.max_iterations) || 1);
|
|
1672
|
+
if (!hasExitGate) {
|
|
1673
|
+
return completedIterations >= max ? "exit" : "iterate";
|
|
1674
|
+
}
|
|
1675
|
+
if (gatePassed)
|
|
1676
|
+
return "exit";
|
|
1677
|
+
if (completedIterations >= max)
|
|
1678
|
+
return "exhausted";
|
|
1679
|
+
return "iterate";
|
|
1680
|
+
}
|
|
1605
1681
|
function readStageDefs(def) {
|
|
1606
1682
|
if (def.steps_version !== 2)
|
|
1607
1683
|
return [];
|
|
@@ -1638,7 +1714,7 @@ function entryActionAllowlist(entryAction) {
|
|
|
1638
1714
|
return `mcp__${entryAction}`;
|
|
1639
1715
|
return null;
|
|
1640
1716
|
}
|
|
1641
|
-
var SKILL_TOOL_ALLOWLIST, HARMONY_TOOL_RE;
|
|
1717
|
+
var DEFAULT_LOOP_MAX_ITERATIONS = 5, SKILL_TOOL_ALLOWLIST, HARMONY_TOOL_RE;
|
|
1642
1718
|
var init_playbookStage = __esm(() => {
|
|
1643
1719
|
SKILL_TOOL_ALLOWLIST = {
|
|
1644
1720
|
hmy: "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*",
|
|
@@ -2187,6 +2263,10 @@ var init_review_worktree = __esm(() => {
|
|
|
2187
2263
|
});
|
|
2188
2264
|
|
|
2189
2265
|
// src/merge-monitor.ts
|
|
2266
|
+
var exports_merge_monitor = {};
|
|
2267
|
+
__export(exports_merge_monitor, {
|
|
2268
|
+
MergeMonitor: () => MergeMonitor
|
|
2269
|
+
});
|
|
2190
2270
|
import { execFile as execFile2 } from "node:child_process";
|
|
2191
2271
|
import { promisify as promisify2 } from "node:util";
|
|
2192
2272
|
|
|
@@ -2220,6 +2300,9 @@ class MergeMonitor {
|
|
|
2220
2300
|
}
|
|
2221
2301
|
log.info(TAG7, "Merge monitor stopped");
|
|
2222
2302
|
}
|
|
2303
|
+
async runOnce() {
|
|
2304
|
+
await this.tick();
|
|
2305
|
+
}
|
|
2223
2306
|
async scheduleNext(delayMs) {
|
|
2224
2307
|
await new Promise((resolve3) => {
|
|
2225
2308
|
this.timer = setTimeout(() => resolve3(), delayMs);
|
|
@@ -2257,9 +2340,10 @@ class MergeMonitor {
|
|
|
2257
2340
|
const batch = candidatesWithLabels.slice(0, 5);
|
|
2258
2341
|
log.debug(TAG7, `Checking ${batch.length} Ready to Merge card(s)`);
|
|
2259
2342
|
const results = await Promise.allSettled(batch.map(async ({ card, labels }) => {
|
|
2260
|
-
const
|
|
2343
|
+
const branchName = extractBranchFromDescription(card.description);
|
|
2344
|
+
const prUrl = resolvePrUrl(card.description ?? null, branchName, this.cwd, this.provider);
|
|
2261
2345
|
if (!prUrl) {
|
|
2262
|
-
log.debug(TAG7, `#${card.short_id} has no PR
|
|
2346
|
+
log.debug(TAG7, `#${card.short_id} has no resolvable PR — skipping`);
|
|
2263
2347
|
return;
|
|
2264
2348
|
}
|
|
2265
2349
|
const state = await checkPrMergeStatus(prUrl, this.cwd, this.provider);
|
|
@@ -5575,6 +5659,30 @@ class StateStore {
|
|
|
5575
5659
|
rec.attempts = 0;
|
|
5576
5660
|
await this.persist();
|
|
5577
5661
|
}
|
|
5662
|
+
getLoopIterations(cardId, stageId) {
|
|
5663
|
+
const rec = this.getCard(cardId);
|
|
5664
|
+
if (!rec || rec.loopStageId !== stageId)
|
|
5665
|
+
return 0;
|
|
5666
|
+
return rec.loopIterations ?? 0;
|
|
5667
|
+
}
|
|
5668
|
+
async incrementLoopIteration(cardId, stageId) {
|
|
5669
|
+
const rec = this.ensureCard(cardId);
|
|
5670
|
+
if (rec.loopStageId !== stageId) {
|
|
5671
|
+
rec.loopStageId = stageId;
|
|
5672
|
+
rec.loopIterations = 0;
|
|
5673
|
+
}
|
|
5674
|
+
rec.loopIterations = (rec.loopIterations ?? 0) + 1;
|
|
5675
|
+
await this.persist();
|
|
5676
|
+
return rec.loopIterations;
|
|
5677
|
+
}
|
|
5678
|
+
async resetLoopIterations(cardId) {
|
|
5679
|
+
const rec = this.getCard(cardId);
|
|
5680
|
+
if (!rec || rec.loopIterations == null && rec.loopStageId == null)
|
|
5681
|
+
return;
|
|
5682
|
+
rec.loopStageId = null;
|
|
5683
|
+
rec.loopIterations = 0;
|
|
5684
|
+
await this.persist();
|
|
5685
|
+
}
|
|
5578
5686
|
async resetAttempts(cardId) {
|
|
5579
5687
|
const rec = this.getCard(cardId);
|
|
5580
5688
|
if (!rec || rec.attempts === 0)
|
|
@@ -6708,6 +6816,26 @@ class CliAgentRunner {
|
|
|
6708
6816
|
this.enqueue({ kind: "playbook_advanced", source: "system", payload });
|
|
6709
6817
|
this.startTimer();
|
|
6710
6818
|
}
|
|
6819
|
+
recordLoopIterationStarted(payload) {
|
|
6820
|
+
this.enqueue({ kind: "loop_iteration_started", source: "system", payload });
|
|
6821
|
+
this.startTimer();
|
|
6822
|
+
}
|
|
6823
|
+
recordLoopIterationEvaluated(payload) {
|
|
6824
|
+
this.enqueue({
|
|
6825
|
+
kind: "loop_iteration_evaluated",
|
|
6826
|
+
source: "system",
|
|
6827
|
+
payload: { ...payload, evidence: truncateOutput(payload.evidence) }
|
|
6828
|
+
});
|
|
6829
|
+
this.startTimer();
|
|
6830
|
+
}
|
|
6831
|
+
recordLoopCompleted(payload) {
|
|
6832
|
+
this.enqueue({ kind: "loop_completed", source: "system", payload });
|
|
6833
|
+
this.startTimer();
|
|
6834
|
+
}
|
|
6835
|
+
recordLoopExhausted(payload) {
|
|
6836
|
+
this.enqueue({ kind: "loop_exhausted", source: "system", payload });
|
|
6837
|
+
this.startTimer();
|
|
6838
|
+
}
|
|
6711
6839
|
record(body) {
|
|
6712
6840
|
this.enqueue(body);
|
|
6713
6841
|
this.startTimer();
|
|
@@ -6803,7 +6931,7 @@ When finished, call harmony_end_agent_session with status="completed".`
|
|
|
6803
6931
|
}
|
|
6804
6932
|
async function renderCommentsSection(client, cardId) {
|
|
6805
6933
|
try {
|
|
6806
|
-
const { comments } = await client.
|
|
6934
|
+
const { comments } = await client.request("GET", `/cards/${encodeURIComponent(cardId)}/comments?limit=200&order=desc`);
|
|
6807
6935
|
if (!Array.isArray(comments) || comments.length === 0)
|
|
6808
6936
|
return "";
|
|
6809
6937
|
const section = serializeCommentThread(comments, {
|
|
@@ -6935,6 +7063,121 @@ async function resolveStageColumnName(client, card, stage) {
|
|
|
6935
7063
|
async function persistStagePointer(client, card, body) {
|
|
6936
7064
|
await client.request("POST", `/cards/${encodeURIComponent(card.id)}/advance-stage`, body);
|
|
6937
7065
|
}
|
|
7066
|
+
async function advanceStageRun(card, stage, stageIndex, def, evaluation, deps) {
|
|
7067
|
+
const loop = getStageLoop(stage);
|
|
7068
|
+
if (!isConvergeLoop(loop)) {
|
|
7069
|
+
if (!evaluation)
|
|
7070
|
+
return { kind: "no_advance" };
|
|
7071
|
+
return advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps);
|
|
7072
|
+
}
|
|
7073
|
+
return advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loop, deps);
|
|
7074
|
+
}
|
|
7075
|
+
function firstErrorMessage(evaluation) {
|
|
7076
|
+
const e = evaluation?.findings.find((f) => f.level === "error");
|
|
7077
|
+
return e ? e.message : null;
|
|
7078
|
+
}
|
|
7079
|
+
async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loop, deps) {
|
|
7080
|
+
const hasExitGate = loop.exit_gate != null;
|
|
7081
|
+
const gatePassed = hasExitGate ? evaluation?.passed ?? false : false;
|
|
7082
|
+
const gateResult = !hasExitGate ? "skipped" : gatePassed ? "passed" : "failed";
|
|
7083
|
+
const maxIterations = Math.max(1, Math.floor(loop.max_iterations) || 1);
|
|
7084
|
+
const iteration = await deps.stateStore.incrementLoopIteration(card.id, stage.id);
|
|
7085
|
+
const decision = decideLoopContinuation({
|
|
7086
|
+
loop,
|
|
7087
|
+
gatePassed,
|
|
7088
|
+
hasExitGate,
|
|
7089
|
+
completedIterations: iteration
|
|
7090
|
+
});
|
|
7091
|
+
const evidence = evaluation ? evaluation.findings.map((f) => `[${f.level}] ${f.message}`).join(`
|
|
7092
|
+
`) : undefined;
|
|
7093
|
+
const verdictGloss = !hasExitGate ? "count-only" : gatePassed ? "gate passed" : firstErrorMessage(evaluation) ?? "gate unmet";
|
|
7094
|
+
const summary = `iteration ${iteration}/${maxIterations} — ${verdictGloss}${decision === "iterate" ? " → retrying" : ""}`;
|
|
7095
|
+
deps.sink?.recordLoopIterationEvaluated?.({
|
|
7096
|
+
stageId: stage.id,
|
|
7097
|
+
iteration,
|
|
7098
|
+
maxIterations,
|
|
7099
|
+
gateResult,
|
|
7100
|
+
evidence,
|
|
7101
|
+
summary
|
|
7102
|
+
});
|
|
7103
|
+
log.info(TAG28, `#${card.short_id} converge loop "${stage.name}": ${summary} → ${decision}`);
|
|
7104
|
+
if (decision === "exit") {
|
|
7105
|
+
await deps.stateStore.resetLoopIterations(card.id).catch(() => {});
|
|
7106
|
+
deps.sink?.recordLoopCompleted?.({
|
|
7107
|
+
stageId: stage.id,
|
|
7108
|
+
iterations: iteration,
|
|
7109
|
+
maxIterations,
|
|
7110
|
+
reason: hasExitGate ? "exit gate passed" : `count-only loop completed ${iteration} iteration(s)`
|
|
7111
|
+
});
|
|
7112
|
+
const exitEval = evaluation?.passed ? evaluation : {
|
|
7113
|
+
passed: true,
|
|
7114
|
+
findings: [
|
|
7115
|
+
{
|
|
7116
|
+
level: "info",
|
|
7117
|
+
message: `Converge loop "${stage.name}" complete after ${iteration} iteration(s).`
|
|
7118
|
+
}
|
|
7119
|
+
],
|
|
7120
|
+
structured: evaluation?.structured ?? {}
|
|
7121
|
+
};
|
|
7122
|
+
return advanceStageOnGate(card, stage, stageIndex, def, exitEval, deps);
|
|
7123
|
+
}
|
|
7124
|
+
if (decision === "exhausted") {
|
|
7125
|
+
await deps.stateStore.resetLoopIterations(card.id).catch(() => {});
|
|
7126
|
+
await deps.stateStore.decrementAttempt(card.id).catch(() => {});
|
|
7127
|
+
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.`;
|
|
7128
|
+
deps.sink?.recordLoopExhausted?.({
|
|
7129
|
+
stageId: stage.id,
|
|
7130
|
+
iterations: iteration,
|
|
7131
|
+
maxIterations,
|
|
7132
|
+
reason
|
|
7133
|
+
});
|
|
7134
|
+
try {
|
|
7135
|
+
await deps.client.updateAgentProgress(card.id, {
|
|
7136
|
+
agentIdentifier: "claude-code-stage",
|
|
7137
|
+
agentName: "Harmony Agent",
|
|
7138
|
+
status: "waiting",
|
|
7139
|
+
currentTask: reason
|
|
7140
|
+
});
|
|
7141
|
+
} catch {}
|
|
7142
|
+
await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, {
|
|
7143
|
+
keepAttempts: true
|
|
7144
|
+
});
|
|
7145
|
+
log.info(TAG28, `#${card.short_id} LoopExhausted: ${reason}`);
|
|
7146
|
+
return { kind: "held_gate_unmet", reason };
|
|
7147
|
+
}
|
|
7148
|
+
await deps.stateStore.decrementAttempt(card.id).catch(() => {});
|
|
7149
|
+
await writeIterationHandoff(card, stage, iteration, maxIterations, evaluation, deps);
|
|
7150
|
+
const toColumn = await resolveStageColumnName(deps.client, card, stage) ?? deps.fallbackColumn;
|
|
7151
|
+
try {
|
|
7152
|
+
await deps.client.addComment(card.id, `Converge loop — ${summary}. Re-running "${stage.name}".`, { commentType: "progress" });
|
|
7153
|
+
} catch {}
|
|
7154
|
+
await runTransition(deps.client, card, {
|
|
7155
|
+
move: { columnName: toColumn },
|
|
7156
|
+
addLabels: [{ name: AGENT_LABEL }],
|
|
7157
|
+
...isAgentRunnableOwner(stage.owner) ? { assignAgent: deps.agentId } : {}
|
|
7158
|
+
}, { store: deps.stateStore, runId: deps.runId });
|
|
7159
|
+
log.info(TAG28, `#${card.short_id} converge loop "${stage.name}" — requeued to "${toColumn}" for iteration ${iteration + 1}/${maxIterations}`);
|
|
7160
|
+
return { kind: "requeued_gate_unmet", toColumn };
|
|
7161
|
+
}
|
|
7162
|
+
async function writeIterationHandoff(card, stage, iteration, maxIterations, evaluation, deps) {
|
|
7163
|
+
try {
|
|
7164
|
+
const findings = evaluation?.findings.filter((f) => f.level !== "info") ?? [];
|
|
7165
|
+
const produced = findings.length > 0 ? `Iteration ${iteration}/${maxIterations} of the "${stage.name}" converge loop did not pass its exit gate. Findings:
|
|
7166
|
+
${findings.map((f) => `- [${f.level}] ${f.message}`).join(`
|
|
7167
|
+
`)}` : `Iteration ${iteration}/${maxIterations} of the "${stage.name}" converge loop completed; the loop continues.`;
|
|
7168
|
+
const body = buildHandoffCommentBody({
|
|
7169
|
+
stageId: stage.id,
|
|
7170
|
+
stageName: stage.name,
|
|
7171
|
+
artifactType: stage.artifact_type,
|
|
7172
|
+
produced,
|
|
7173
|
+
decisions: [],
|
|
7174
|
+
nextStageNeeds: "Address the findings above on the same branch and re-run; this stage repeats until its exit gate passes."
|
|
7175
|
+
});
|
|
7176
|
+
await deps.client.addComment(card.id, body, { commentType: "decision" });
|
|
7177
|
+
} catch (err) {
|
|
7178
|
+
log.warn(TAG28, `iteration-handoff write failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
7179
|
+
}
|
|
7180
|
+
}
|
|
6938
7181
|
async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps) {
|
|
6939
7182
|
const summary = gateSummary(stage, evaluation);
|
|
6940
7183
|
deps.sink?.recordStageGateEvaluated({
|
|
@@ -7293,7 +7536,9 @@ class Worker {
|
|
|
7293
7536
|
const basePrompt = await buildPrompt(enriched, this.branchName, this.worktreePath, this.client, this.workspaceId, this.projectId);
|
|
7294
7537
|
let prompt = basePrompt;
|
|
7295
7538
|
if (stageCtx.kind === "run") {
|
|
7296
|
-
const
|
|
7539
|
+
const loop = getStageLoop(stageCtx.stage);
|
|
7540
|
+
const isLoop = isConvergeLoop(loop);
|
|
7541
|
+
const inherited = await this.loadInheritedHandoffSection(card.id, stageCtx.stage.id, { includeOwnStage: isLoop });
|
|
7297
7542
|
prompt = [buildStagePreamble(stageCtx.stage), inherited, basePrompt].filter(Boolean).join(`
|
|
7298
7543
|
|
|
7299
7544
|
`);
|
|
@@ -7302,6 +7547,16 @@ class Worker {
|
|
|
7302
7547
|
stageName: stageCtx.stage.name,
|
|
7303
7548
|
owner: stageCtx.stage.owner
|
|
7304
7549
|
});
|
|
7550
|
+
if (isLoop && loop) {
|
|
7551
|
+
const priorIterations = this.stateStore.getLoopIterations(card.id, stageCtx.stage.id);
|
|
7552
|
+
this.cliRunner?.recordLoopIterationStarted({
|
|
7553
|
+
stageId: stageCtx.stage.id,
|
|
7554
|
+
stageName: stageCtx.stage.name,
|
|
7555
|
+
iteration: priorIterations + 1,
|
|
7556
|
+
maxIterations: Math.max(1, Math.floor(loop.max_iterations) || 1),
|
|
7557
|
+
mode: loop.mode
|
|
7558
|
+
});
|
|
7559
|
+
}
|
|
7305
7560
|
}
|
|
7306
7561
|
await this.client.updateAgentProgress(card.id, {
|
|
7307
7562
|
agentIdentifier: agentIdentifier(this.id),
|
|
@@ -7463,15 +7718,19 @@ class Worker {
|
|
|
7463
7718
|
} catch {}
|
|
7464
7719
|
await this.recordOutcome(card.id, "failure");
|
|
7465
7720
|
} else if (this.runId && this.aborted) {
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7721
|
+
if (!this.completionStarted) {
|
|
7722
|
+
try {
|
|
7723
|
+
await this.client.updateCard(card.id, { assignedAgentId: null });
|
|
7724
|
+
} catch (err) {
|
|
7725
|
+
log.warn(this.tag, `failed to release card after stop: ${err instanceof Error ? err.message : err}`);
|
|
7726
|
+
}
|
|
7727
|
+
try {
|
|
7728
|
+
await runTransition(this.client, card, { removeLabels: ["agent"] });
|
|
7729
|
+
} catch (tErr) {
|
|
7730
|
+
log.warn(this.tag, `stop label cleanup failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
|
|
7731
|
+
}
|
|
7732
|
+
} else {
|
|
7733
|
+
log.info(this.tag, `cancel arrived after completion on #${card.short_id} — keeping assignment so review picks it up (#585)`);
|
|
7475
7734
|
}
|
|
7476
7735
|
try {
|
|
7477
7736
|
await this.stateStore.endRun(this.runId, "paused", {
|
|
@@ -7613,15 +7872,13 @@ class Worker {
|
|
|
7613
7872
|
} catch {}
|
|
7614
7873
|
}
|
|
7615
7874
|
}
|
|
7616
|
-
async loadInheritedHandoffSection(cardId, currentStageId) {
|
|
7875
|
+
async loadInheritedHandoffSection(cardId, currentStageId, opts = {}) {
|
|
7617
7876
|
try {
|
|
7618
|
-
const { comments } = await this.client.
|
|
7619
|
-
limit: 200
|
|
7620
|
-
});
|
|
7877
|
+
const { comments } = await this.client.request("GET", `/cards/${encodeURIComponent(cardId)}/comments?limit=200&order=desc&comment_type=decision`);
|
|
7621
7878
|
if (!Array.isArray(comments) || comments.length === 0)
|
|
7622
7879
|
return "";
|
|
7623
7880
|
const handoff = extractLatestHandoff(comments, {
|
|
7624
|
-
excludeStageId: currentStageId
|
|
7881
|
+
excludeStageId: opts.includeOwnStage ? undefined : currentStageId
|
|
7625
7882
|
});
|
|
7626
7883
|
return handoff ? renderInheritedHandoffSection(handoff) : "";
|
|
7627
7884
|
} catch (err) {
|
|
@@ -7649,7 +7906,9 @@ class Worker {
|
|
|
7649
7906
|
}
|
|
7650
7907
|
async collectStageGateEvidence(card, stage, worktreePath, subtasks) {
|
|
7651
7908
|
try {
|
|
7652
|
-
const
|
|
7909
|
+
const loop = getStageLoop(stage);
|
|
7910
|
+
const gateSource = isConvergeLoop(loop) ? loop?.exit_gate ?? null : stage.gate;
|
|
7911
|
+
const gate = normalizeGateSpec(gateSource);
|
|
7653
7912
|
if (!gate) {
|
|
7654
7913
|
return null;
|
|
7655
7914
|
}
|
|
@@ -7692,11 +7951,8 @@ class Worker {
|
|
|
7692
7951
|
}
|
|
7693
7952
|
}
|
|
7694
7953
|
async advanceFromGateEvaluation(card, stage, stageIndex, def, evaluation) {
|
|
7695
|
-
if (!evaluation) {
|
|
7696
|
-
return { kind: "no_advance" };
|
|
7697
|
-
}
|
|
7698
7954
|
try {
|
|
7699
|
-
return await
|
|
7955
|
+
return await advanceStageRun(card, stage, stageIndex, def, evaluation, {
|
|
7700
7956
|
client: this.client,
|
|
7701
7957
|
stateStore: this.stateStore,
|
|
7702
7958
|
agentId: this.agentId,
|
|
@@ -8672,6 +8928,71 @@ var init_recovery = __esm(() => {
|
|
|
8672
8928
|
init_worktree();
|
|
8673
8929
|
});
|
|
8674
8930
|
|
|
8931
|
+
// src/strand-recovery.ts
|
|
8932
|
+
var exports_strand_recovery = {};
|
|
8933
|
+
__export(exports_strand_recovery, {
|
|
8934
|
+
reclaimPreReviewStrands: () => reclaimPreReviewStrands
|
|
8935
|
+
});
|
|
8936
|
+
async function reclaimPreReviewStrands(opts) {
|
|
8937
|
+
const {
|
|
8938
|
+
client,
|
|
8939
|
+
agentId,
|
|
8940
|
+
cards,
|
|
8941
|
+
columns,
|
|
8942
|
+
labelMap,
|
|
8943
|
+
reviewColumns,
|
|
8944
|
+
approvedLabel,
|
|
8945
|
+
graceMs,
|
|
8946
|
+
knownCardIds,
|
|
8947
|
+
cwd,
|
|
8948
|
+
provider
|
|
8949
|
+
} = opts;
|
|
8950
|
+
const now = opts.now ?? Date.now();
|
|
8951
|
+
const maxPerSweep = opts.maxPerSweep ?? 5;
|
|
8952
|
+
const reviewColIds = new Set(columns.filter((c) => reviewColumns.some((n) => n.toLowerCase() === c.name.toLowerCase())).map((c) => c.id));
|
|
8953
|
+
if (reviewColIds.size === 0)
|
|
8954
|
+
return [];
|
|
8955
|
+
const reclaimed = [];
|
|
8956
|
+
let checked = 0;
|
|
8957
|
+
for (const card of cards) {
|
|
8958
|
+
if (checked >= maxPerSweep)
|
|
8959
|
+
break;
|
|
8960
|
+
if (card.archived_at || !reviewColIds.has(card.column_id) || card.assigned_agent_id != null || card.assignee_id != null || knownCardIds.has(card.id)) {
|
|
8961
|
+
continue;
|
|
8962
|
+
}
|
|
8963
|
+
const branch = extractBranchFromDescription(card.description);
|
|
8964
|
+
if (!branch)
|
|
8965
|
+
continue;
|
|
8966
|
+
const labels = resolveCardLabels(card, labelMap);
|
|
8967
|
+
if (hasLabel(labels, approvedLabel) || hasLabel(labels, NEED_REVIEW_LABEL)) {
|
|
8968
|
+
continue;
|
|
8969
|
+
}
|
|
8970
|
+
const stalledAt = Date.parse(card.updated_at ?? "");
|
|
8971
|
+
if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
|
|
8972
|
+
continue;
|
|
8973
|
+
checked++;
|
|
8974
|
+
const prUrl = resolvePrUrl(card.description ?? null, branch, cwd, provider);
|
|
8975
|
+
if (prUrl)
|
|
8976
|
+
continue;
|
|
8977
|
+
log.warn(TAG33, `#${card.short_id} stranded in review (branch pushed, no PR, unowned) — re-asserting daemon assignment`);
|
|
8978
|
+
try {
|
|
8979
|
+
await client.updateCard(card.id, { assignedAgentId: agentId });
|
|
8980
|
+
reclaimed.push(card.id);
|
|
8981
|
+
} catch (err) {
|
|
8982
|
+
log.error(TAG33, `review re-claim failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
8983
|
+
}
|
|
8984
|
+
}
|
|
8985
|
+
return reclaimed;
|
|
8986
|
+
}
|
|
8987
|
+
var TAG33 = "strand-recovery";
|
|
8988
|
+
var init_strand_recovery = __esm(() => {
|
|
8989
|
+
init_board_helpers();
|
|
8990
|
+
init_git_pr();
|
|
8991
|
+
init_log();
|
|
8992
|
+
init_review_worktree();
|
|
8993
|
+
init_types();
|
|
8994
|
+
});
|
|
8995
|
+
|
|
8675
8996
|
// src/reconcile.ts
|
|
8676
8997
|
class Reconciler {
|
|
8677
8998
|
client;
|
|
@@ -8686,6 +9007,7 @@ class Reconciler {
|
|
|
8686
9007
|
agentConfig;
|
|
8687
9008
|
timer = null;
|
|
8688
9009
|
lastTickAt = null;
|
|
9010
|
+
gitProvider = null;
|
|
8689
9011
|
get lastTick() {
|
|
8690
9012
|
return this.lastTickAt;
|
|
8691
9013
|
}
|
|
@@ -8713,7 +9035,7 @@ class Reconciler {
|
|
|
8713
9035
|
clearInterval(this.timer);
|
|
8714
9036
|
this.timer = null;
|
|
8715
9037
|
}
|
|
8716
|
-
log.info(
|
|
9038
|
+
log.info(TAG34, "Heartbeat stopped");
|
|
8717
9039
|
}
|
|
8718
9040
|
async recoverStaleRuns() {
|
|
8719
9041
|
if (!this.stateStore || !this.agentConfig)
|
|
@@ -8730,7 +9052,7 @@ class Reconciler {
|
|
|
8730
9052
|
if (!daemonDead && !(heartbeatStale && ourZombie))
|
|
8731
9053
|
continue;
|
|
8732
9054
|
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(
|
|
9055
|
+
log.warn(TAG34, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
|
|
8734
9056
|
await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
|
|
8735
9057
|
runId: run.runId,
|
|
8736
9058
|
cardId: run.cardId,
|
|
@@ -8757,13 +9079,37 @@ class Reconciler {
|
|
|
8757
9079
|
const stalledAt = Date.parse(card.updated_at ?? "");
|
|
8758
9080
|
if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
|
|
8759
9081
|
continue;
|
|
8760
|
-
log.warn(
|
|
9082
|
+
log.warn(TAG34, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
|
|
8761
9083
|
try {
|
|
8762
9084
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
8763
9085
|
} catch (err) {
|
|
8764
|
-
log.error(
|
|
9086
|
+
log.error(TAG34, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
9087
|
+
}
|
|
9088
|
+
}
|
|
9089
|
+
}
|
|
9090
|
+
async recoverStrandedReview(cards, columns, labelMap, knownCardIds) {
|
|
9091
|
+
if (this.reviewColumns.length === 0)
|
|
9092
|
+
return;
|
|
9093
|
+
if (!this.gitProvider) {
|
|
9094
|
+
try {
|
|
9095
|
+
this.gitProvider = detectGitProvider();
|
|
9096
|
+
} catch {
|
|
9097
|
+
return;
|
|
8765
9098
|
}
|
|
8766
9099
|
}
|
|
9100
|
+
await reclaimPreReviewStrands({
|
|
9101
|
+
client: this.client,
|
|
9102
|
+
agentId: this.agentId,
|
|
9103
|
+
cards,
|
|
9104
|
+
columns,
|
|
9105
|
+
labelMap,
|
|
9106
|
+
reviewColumns: this.reviewColumns,
|
|
9107
|
+
approvedLabel: this.approvedLabel,
|
|
9108
|
+
graceMs: this.agentConfig?.timing.staleHeartbeatMs ?? 120000,
|
|
9109
|
+
knownCardIds,
|
|
9110
|
+
cwd: process.cwd(),
|
|
9111
|
+
provider: this.gitProvider
|
|
9112
|
+
});
|
|
8767
9113
|
}
|
|
8768
9114
|
async releaseStalledApprovals(cards, columns, knownCardIds) {
|
|
8769
9115
|
const planning = this.agentConfig?.planning;
|
|
@@ -8784,11 +9130,11 @@ class Reconciler {
|
|
|
8784
9130
|
const parkedAt = Date.parse(card.updated_at ?? "");
|
|
8785
9131
|
if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
|
|
8786
9132
|
continue;
|
|
8787
|
-
log.warn(
|
|
9133
|
+
log.warn(TAG34, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
|
|
8788
9134
|
try {
|
|
8789
9135
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
8790
9136
|
} catch (err) {
|
|
8791
|
-
log.error(
|
|
9137
|
+
log.error(TAG34, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
8792
9138
|
}
|
|
8793
9139
|
}
|
|
8794
9140
|
}
|
|
@@ -8831,21 +9177,21 @@ class Reconciler {
|
|
|
8831
9177
|
const subtasks = card.subtasks ?? [];
|
|
8832
9178
|
const mode = route.mode;
|
|
8833
9179
|
if (route.stage) {
|
|
8834
|
-
log.info(
|
|
9180
|
+
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
9181
|
}
|
|
8836
9182
|
if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
|
|
8837
|
-
log.debug(
|
|
9183
|
+
log.debug(TAG34, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
|
|
8838
9184
|
continue;
|
|
8839
9185
|
}
|
|
8840
9186
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
8841
|
-
log.debug(
|
|
9187
|
+
log.debug(TAG34, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
|
|
8842
9188
|
continue;
|
|
8843
9189
|
}
|
|
8844
9190
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
8845
|
-
log.debug(
|
|
9191
|
+
log.debug(TAG34, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
|
|
8846
9192
|
continue;
|
|
8847
9193
|
}
|
|
8848
|
-
log.info(
|
|
9194
|
+
log.info(TAG34, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
|
|
8849
9195
|
await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
8850
9196
|
}
|
|
8851
9197
|
}
|
|
@@ -8853,25 +9199,28 @@ class Reconciler {
|
|
|
8853
9199
|
await this.recoverStaleRuns();
|
|
8854
9200
|
}
|
|
8855
9201
|
await this.recoverStrandedInProgress(cards, columns, knownCardIds);
|
|
9202
|
+
await this.recoverStrandedReview(cards, columns, labelMap, knownCardIds);
|
|
8856
9203
|
for (const knownId of knownCardIds) {
|
|
8857
9204
|
if (!allAgentCardIds.has(knownId)) {
|
|
8858
|
-
log.info(
|
|
9205
|
+
log.info(TAG34, `Missed unassign: ${knownId} — removing`);
|
|
8859
9206
|
await this.pool.removeCard(knownId);
|
|
8860
9207
|
}
|
|
8861
9208
|
}
|
|
8862
9209
|
await this.releaseStalledApprovals(cards, columns, knownCardIds);
|
|
8863
|
-
log.debug(
|
|
9210
|
+
log.debug(TAG34, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
|
|
8864
9211
|
} catch (err) {
|
|
8865
|
-
log.error(
|
|
9212
|
+
log.error(TAG34, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
|
|
8866
9213
|
}
|
|
8867
9214
|
}
|
|
8868
9215
|
}
|
|
8869
|
-
var
|
|
9216
|
+
var TAG34 = "reconcile";
|
|
8870
9217
|
var init_reconcile = __esm(() => {
|
|
8871
9218
|
init_board_helpers();
|
|
9219
|
+
init_git_pr();
|
|
8872
9220
|
init_log();
|
|
8873
9221
|
init_recovery();
|
|
8874
9222
|
init_review_worktree();
|
|
9223
|
+
init_strand_recovery();
|
|
8875
9224
|
init_types();
|
|
8876
9225
|
});
|
|
8877
9226
|
|
|
@@ -8904,7 +9253,7 @@ function prettyBanner(config, version) {
|
|
|
8904
9253
|
checks.push({ kind: "ok", message });
|
|
8905
9254
|
},
|
|
8906
9255
|
warn(message) {
|
|
8907
|
-
log.warn(
|
|
9256
|
+
log.warn(TAG35, message);
|
|
8908
9257
|
checks.push({ kind: "warn", message: message.split(`
|
|
8909
9258
|
`, 1)[0] });
|
|
8910
9259
|
},
|
|
@@ -8929,25 +9278,25 @@ function prettyBanner(config, version) {
|
|
|
8929
9278
|
};
|
|
8930
9279
|
}
|
|
8931
9280
|
function jsonBanner(config, version) {
|
|
8932
|
-
log.info(
|
|
8933
|
-
log.info(
|
|
9281
|
+
log.info(TAG35, `Harmony Agent Daemon v${version} starting...`);
|
|
9282
|
+
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
9283
|
if (config.agent.review.enabled) {
|
|
8935
|
-
log.info(
|
|
9284
|
+
log.info(TAG35, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
|
|
8936
9285
|
}
|
|
8937
9286
|
let failed = false;
|
|
8938
9287
|
return {
|
|
8939
9288
|
setProjectName(_name) {},
|
|
8940
9289
|
setGitProvider(provider) {
|
|
8941
|
-
log.info(
|
|
9290
|
+
log.info(TAG35, `Git provider: ${provider}`);
|
|
8942
9291
|
},
|
|
8943
9292
|
setHttpPort(port) {
|
|
8944
|
-
log.info(
|
|
9293
|
+
log.info(TAG35, `HTTP server on port ${port}`);
|
|
8945
9294
|
},
|
|
8946
9295
|
check(message) {
|
|
8947
|
-
log.info(
|
|
9296
|
+
log.info(TAG35, message);
|
|
8948
9297
|
},
|
|
8949
9298
|
warn(message) {
|
|
8950
|
-
log.warn(
|
|
9299
|
+
log.warn(TAG35, message);
|
|
8951
9300
|
},
|
|
8952
9301
|
fail() {
|
|
8953
9302
|
failed = true;
|
|
@@ -8955,7 +9304,7 @@ function jsonBanner(config, version) {
|
|
|
8955
9304
|
async ready(message) {
|
|
8956
9305
|
if (failed)
|
|
8957
9306
|
return;
|
|
8958
|
-
log.info(
|
|
9307
|
+
log.info(TAG35, message);
|
|
8959
9308
|
}
|
|
8960
9309
|
};
|
|
8961
9310
|
}
|
|
@@ -9036,7 +9385,7 @@ function cyan(s) {
|
|
|
9036
9385
|
function yellow(s) {
|
|
9037
9386
|
return `${ANSI.yellow}${s}${ANSI.reset}`;
|
|
9038
9387
|
}
|
|
9039
|
-
var
|
|
9388
|
+
var TAG35 = "daemon", RULE_WIDTH = 70, ANSI;
|
|
9040
9389
|
var init_startup_banner = __esm(() => {
|
|
9041
9390
|
init_log();
|
|
9042
9391
|
ANSI = {
|
|
@@ -9187,13 +9536,13 @@ class Watcher {
|
|
|
9187
9536
|
}
|
|
9188
9537
|
async start() {
|
|
9189
9538
|
if (!isPretty()) {
|
|
9190
|
-
log.info(
|
|
9539
|
+
log.info(TAG36, "Connecting to Supabase realtime (broadcast)...");
|
|
9191
9540
|
}
|
|
9192
9541
|
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
9193
9542
|
const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
|
|
9194
9543
|
this.subscribeBroadcast();
|
|
9195
9544
|
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
9196
|
-
log.debug(
|
|
9545
|
+
log.debug(TAG36, "Presence sync");
|
|
9197
9546
|
}).subscribe(async (status) => {
|
|
9198
9547
|
if (status === "SUBSCRIBED") {
|
|
9199
9548
|
await presenceChannel.track({
|
|
@@ -9206,7 +9555,7 @@ class Watcher {
|
|
|
9206
9555
|
agentName: this.identity.agentName
|
|
9207
9556
|
});
|
|
9208
9557
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
9209
|
-
log.info(
|
|
9558
|
+
log.info(TAG36, "Presence tracked on board-presence channel");
|
|
9210
9559
|
}
|
|
9211
9560
|
this.presenceTracked = true;
|
|
9212
9561
|
this.maybeResolveReady();
|
|
@@ -9219,13 +9568,13 @@ class Watcher {
|
|
|
9219
9568
|
return;
|
|
9220
9569
|
const gen = ++this.broadcastGen;
|
|
9221
9570
|
this.channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
|
|
9222
|
-
log.debug(
|
|
9571
|
+
log.debug(TAG36, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
|
|
9223
9572
|
this.onCardBroadcast({
|
|
9224
9573
|
event: "card_update",
|
|
9225
9574
|
payload: msg.payload ?? {}
|
|
9226
9575
|
});
|
|
9227
9576
|
}).on("broadcast", { event: "card_created" }, (msg) => {
|
|
9228
|
-
log.debug(
|
|
9577
|
+
log.debug(TAG36, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
|
|
9229
9578
|
this.onCardBroadcast({
|
|
9230
9579
|
event: "card_created",
|
|
9231
9580
|
payload: msg.payload ?? {}
|
|
@@ -9235,7 +9584,7 @@ class Watcher {
|
|
|
9235
9584
|
const cardId = payload.card_id;
|
|
9236
9585
|
const command = payload.command;
|
|
9237
9586
|
if (cardId && command) {
|
|
9238
|
-
log.info(
|
|
9587
|
+
log.info(TAG36, `Broadcast: agent_command ${command} for ${cardId}`);
|
|
9239
9588
|
this.onAgentCommand?.({ cardId, command });
|
|
9240
9589
|
}
|
|
9241
9590
|
}).subscribe((status) => {
|
|
@@ -9245,13 +9594,13 @@ class Watcher {
|
|
|
9245
9594
|
this.connected = true;
|
|
9246
9595
|
this.reconnectAttempts = 0;
|
|
9247
9596
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
9248
|
-
log.info(
|
|
9597
|
+
log.info(TAG36, "Broadcast subscription active");
|
|
9249
9598
|
}
|
|
9250
9599
|
this.maybeResolveReady();
|
|
9251
9600
|
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
|
|
9252
9601
|
this.connected = false;
|
|
9253
9602
|
if (!this.stopping) {
|
|
9254
|
-
log.warn(
|
|
9603
|
+
log.warn(TAG36, `Broadcast subscription ${status} — scheduling reconnect`);
|
|
9255
9604
|
this.scheduleReconnect();
|
|
9256
9605
|
}
|
|
9257
9606
|
}
|
|
@@ -9270,7 +9619,7 @@ class Watcher {
|
|
|
9270
9619
|
async reconnectBroadcast() {
|
|
9271
9620
|
if (this.stopping || !this.supabase)
|
|
9272
9621
|
return;
|
|
9273
|
-
log.warn(
|
|
9622
|
+
log.warn(TAG36, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
|
|
9274
9623
|
if (this.channel) {
|
|
9275
9624
|
const old = this.channel;
|
|
9276
9625
|
this.channel = null;
|
|
@@ -9300,10 +9649,10 @@ class Watcher {
|
|
|
9300
9649
|
this.supabase = null;
|
|
9301
9650
|
}
|
|
9302
9651
|
this.connected = false;
|
|
9303
|
-
log.info(
|
|
9652
|
+
log.info(TAG36, "Broadcast subscription stopped");
|
|
9304
9653
|
}
|
|
9305
9654
|
}
|
|
9306
|
-
var
|
|
9655
|
+
var TAG36 = "watcher";
|
|
9307
9656
|
var init_watcher = __esm(() => {
|
|
9308
9657
|
init_log();
|
|
9309
9658
|
});
|
|
@@ -9390,10 +9739,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
|
|
|
9390
9739
|
});
|
|
9391
9740
|
} catch {}
|
|
9392
9741
|
if (result.removed.length > 0) {
|
|
9393
|
-
log.info(
|
|
9742
|
+
log.info(TAG37, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
|
|
9394
9743
|
}
|
|
9395
9744
|
if (result.errors.length > 0) {
|
|
9396
|
-
log.warn(
|
|
9745
|
+
log.warn(TAG37, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
|
|
9397
9746
|
}
|
|
9398
9747
|
return result;
|
|
9399
9748
|
}
|
|
@@ -9423,7 +9772,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
9423
9772
|
} catch (err) {
|
|
9424
9773
|
const detail = gitErrorDetail2(err);
|
|
9425
9774
|
if (isTransientGitNetworkError(detail)) {
|
|
9426
|
-
log.debug(
|
|
9775
|
+
log.debug(TAG37, `Remote branch GC skipped — remote unreachable: ${detail}`);
|
|
9427
9776
|
return result;
|
|
9428
9777
|
}
|
|
9429
9778
|
result.errors.push({ ref: "fetch", error: detail });
|
|
@@ -9462,7 +9811,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
9462
9811
|
continue;
|
|
9463
9812
|
}
|
|
9464
9813
|
if (clock() > sweepDeadline) {
|
|
9465
|
-
log.debug(
|
|
9814
|
+
log.debug(TAG37, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
|
|
9466
9815
|
break;
|
|
9467
9816
|
}
|
|
9468
9817
|
try {
|
|
@@ -9475,17 +9824,17 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
9475
9824
|
} catch (err) {
|
|
9476
9825
|
const detail = gitErrorDetail2(err);
|
|
9477
9826
|
if (isTransientGitNetworkError(detail)) {
|
|
9478
|
-
log.debug(
|
|
9827
|
+
log.debug(TAG37, `Remote branch GC interrupted — remote unreachable: ${detail}`);
|
|
9479
9828
|
break;
|
|
9480
9829
|
}
|
|
9481
9830
|
result.errors.push({ ref, error: detail });
|
|
9482
9831
|
}
|
|
9483
9832
|
}
|
|
9484
9833
|
if (result.removed.length > 0) {
|
|
9485
|
-
log.info(
|
|
9834
|
+
log.info(TAG37, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
|
|
9486
9835
|
}
|
|
9487
9836
|
if (result.errors.length > 0) {
|
|
9488
|
-
log.warn(
|
|
9837
|
+
log.warn(TAG37, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
|
|
9489
9838
|
}
|
|
9490
9839
|
return result;
|
|
9491
9840
|
}
|
|
@@ -9516,13 +9865,13 @@ class WorktreeGc {
|
|
|
9516
9865
|
try {
|
|
9517
9866
|
runWorktreeGc(this.basePath, this.store);
|
|
9518
9867
|
} catch (err) {
|
|
9519
|
-
log.warn(
|
|
9868
|
+
log.warn(TAG37, `GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
9520
9869
|
}
|
|
9521
9870
|
if (this.remoteOpts) {
|
|
9522
9871
|
try {
|
|
9523
9872
|
pruneFailedRemoteBranches(this.remoteOpts);
|
|
9524
9873
|
} catch (err) {
|
|
9525
|
-
log.warn(
|
|
9874
|
+
log.warn(TAG37, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
9526
9875
|
}
|
|
9527
9876
|
}
|
|
9528
9877
|
}
|
|
@@ -9536,7 +9885,7 @@ function getRepoRoot2() {
|
|
|
9536
9885
|
return null;
|
|
9537
9886
|
}
|
|
9538
9887
|
}
|
|
9539
|
-
var
|
|
9888
|
+
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
9889
|
var init_worktree_gc = __esm(() => {
|
|
9541
9890
|
init_log();
|
|
9542
9891
|
init_worktree();
|
|
@@ -9640,7 +9989,7 @@ async function main() {
|
|
|
9640
9989
|
} catch (err) {
|
|
9641
9990
|
if (err instanceof ConfigValidationError) {
|
|
9642
9991
|
banner.fail();
|
|
9643
|
-
log.error(
|
|
9992
|
+
log.error(TAG38, err.message);
|
|
9644
9993
|
process.exit(1);
|
|
9645
9994
|
}
|
|
9646
9995
|
throw err;
|
|
@@ -9750,7 +10099,7 @@ async function main() {
|
|
|
9750
10099
|
if (shuttingDown)
|
|
9751
10100
|
return;
|
|
9752
10101
|
shuttingDown = true;
|
|
9753
|
-
log.info(
|
|
10102
|
+
log.info(TAG38, `Received ${signal}, shutting down gracefully...`);
|
|
9754
10103
|
reconciler.stop();
|
|
9755
10104
|
mergeMonitor?.stop();
|
|
9756
10105
|
worktreeGc.stop();
|
|
@@ -9760,18 +10109,18 @@ async function main() {
|
|
|
9760
10109
|
}
|
|
9761
10110
|
await watcher.stop();
|
|
9762
10111
|
await pool.shutdown();
|
|
9763
|
-
log.info(
|
|
10112
|
+
log.info(TAG38, "Daemon stopped.");
|
|
9764
10113
|
process.exit(exitCode);
|
|
9765
10114
|
};
|
|
9766
10115
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
9767
10116
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
9768
10117
|
process.on("uncaughtException", (err) => {
|
|
9769
|
-
log.error(
|
|
10118
|
+
log.error(TAG38, `Uncaught exception: ${err.message}`);
|
|
9770
10119
|
exitCode = 1;
|
|
9771
10120
|
shutdown("uncaughtException");
|
|
9772
10121
|
});
|
|
9773
10122
|
process.on("unhandledRejection", (reason) => {
|
|
9774
|
-
log.error(
|
|
10123
|
+
log.error(TAG38, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
9775
10124
|
exitCode = 1;
|
|
9776
10125
|
shutdown("unhandledRejection");
|
|
9777
10126
|
});
|
|
@@ -9824,29 +10173,29 @@ async function handleBroadcast(event, client, pool, config, agentId) {
|
|
|
9824
10173
|
if (assignedAgentId === undefined)
|
|
9825
10174
|
return;
|
|
9826
10175
|
if (assignedAgentId === agentId) {
|
|
9827
|
-
log.info(
|
|
10176
|
+
log.info(TAG38, `Broadcast: card ${cardId} assigned to agent`);
|
|
9828
10177
|
try {
|
|
9829
10178
|
await pool.resetAttemptsForReassign(cardId);
|
|
9830
10179
|
await tryEnqueueCard(cardId, client, pool, config, agentId);
|
|
9831
10180
|
} catch (err) {
|
|
9832
|
-
log.error(
|
|
10181
|
+
log.error(TAG38, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
|
|
9833
10182
|
}
|
|
9834
10183
|
} else if (pool.isCardKnown(cardId)) {
|
|
9835
|
-
log.info(
|
|
10184
|
+
log.info(TAG38, `Broadcast: card ${cardId} unassigned from agent`);
|
|
9836
10185
|
await pool.removeCard(cardId);
|
|
9837
10186
|
}
|
|
9838
10187
|
}
|
|
9839
10188
|
async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
9840
10189
|
const { card } = await client.getCard(cardId);
|
|
9841
10190
|
if (card.assigned_agent_id !== agentId) {
|
|
9842
|
-
log.debug(
|
|
10191
|
+
log.debug(TAG38, `Card ${cardId} no longer assigned to agent — skipping`);
|
|
9843
10192
|
return;
|
|
9844
10193
|
}
|
|
9845
10194
|
const board = await client.getBoard(config.projectId, { summary: true });
|
|
9846
10195
|
const columns = board.columns;
|
|
9847
10196
|
const column = columns.find((c) => c.id === card.column_id);
|
|
9848
10197
|
if (!column) {
|
|
9849
|
-
log.warn(
|
|
10198
|
+
log.warn(TAG38, `Column not found for card ${cardId}`);
|
|
9850
10199
|
return;
|
|
9851
10200
|
}
|
|
9852
10201
|
const route = classifyPickup(card, column.name, {
|
|
@@ -9855,27 +10204,27 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
9855
10204
|
playbooks: config.agent.playbooks
|
|
9856
10205
|
});
|
|
9857
10206
|
if (!route) {
|
|
9858
|
-
log.info(
|
|
10207
|
+
log.info(TAG38, `Card #${card.short_id} is in "${column.name}", not a pickup/review/stage column — skipping`);
|
|
9859
10208
|
return;
|
|
9860
10209
|
}
|
|
9861
10210
|
if (route.stage) {
|
|
9862
|
-
log.info(
|
|
10211
|
+
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
10212
|
}
|
|
9864
10213
|
const mode = route.mode;
|
|
9865
10214
|
const labelMap = buildLabelMap(board.labels ?? []);
|
|
9866
10215
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
9867
10216
|
const subtasks = card.subtasks ?? [];
|
|
9868
10217
|
if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
|
|
9869
|
-
log.debug(
|
|
10218
|
+
log.debug(TAG38, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
|
|
9870
10219
|
return;
|
|
9871
10220
|
}
|
|
9872
10221
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
9873
|
-
log.info(
|
|
10222
|
+
log.info(TAG38, `Card #${card.short_id} has no branch reference — skipping auto-review`);
|
|
9874
10223
|
return;
|
|
9875
10224
|
}
|
|
9876
10225
|
await pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
9877
10226
|
}
|
|
9878
|
-
var
|
|
10227
|
+
var TAG38 = "daemon", PKG_VERSION;
|
|
9879
10228
|
var init_src = __esm(() => {
|
|
9880
10229
|
init_board_helpers();
|
|
9881
10230
|
init_config();
|
|
@@ -9909,6 +10258,7 @@ Usage:
|
|
|
9909
10258
|
harmony-agent health Exit 0 if daemon is healthy, 1 otherwise
|
|
9910
10259
|
harmony-agent doctor Run preflight checks (don't start)
|
|
9911
10260
|
harmony-agent gc One-shot worktree garbage collection
|
|
10261
|
+
harmony-agent recover One-shot recovery of stranded Review cards
|
|
9912
10262
|
harmony-agent help Show this help
|
|
9913
10263
|
|
|
9914
10264
|
Flags:
|
|
@@ -10047,6 +10397,53 @@ async function gcCommand() {
|
|
|
10047
10397
|
}
|
|
10048
10398
|
return 0;
|
|
10049
10399
|
}
|
|
10400
|
+
async function recoverCommand() {
|
|
10401
|
+
const { loadDaemonConfig: loadDaemonConfig2, createApiClient: createApiClient2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
10402
|
+
const { MergeMonitor: MergeMonitor2 } = await Promise.resolve().then(() => (init_merge_monitor(), exports_merge_monitor));
|
|
10403
|
+
const { reclaimPreReviewStrands: reclaimPreReviewStrands2 } = await Promise.resolve().then(() => (init_strand_recovery(), exports_strand_recovery));
|
|
10404
|
+
const { detectGitProvider: detectGitProvider2 } = await Promise.resolve().then(() => (init_git_pr(), exports_git_pr));
|
|
10405
|
+
const { buildLabelMap: buildLabelMap2 } = await Promise.resolve().then(() => (init_board_helpers(), exports_board_helpers));
|
|
10406
|
+
const config = loadDaemonConfig2();
|
|
10407
|
+
const client = createApiClient2(config);
|
|
10408
|
+
const { agent: registeredAgent } = await client.registerWorkspaceAgent(config.workspaceId, {
|
|
10409
|
+
identifier: config.agentIdentifier,
|
|
10410
|
+
name: config.agentName,
|
|
10411
|
+
color: config.agentColor
|
|
10412
|
+
});
|
|
10413
|
+
const agentId = registeredAgent.id;
|
|
10414
|
+
const monitor = new MergeMonitor2(client, config.projectId, config.agent);
|
|
10415
|
+
await monitor.runOnce();
|
|
10416
|
+
const board = await client.getBoard(config.projectId);
|
|
10417
|
+
const cards = board.cards ?? [];
|
|
10418
|
+
const columns = board.columns ?? [];
|
|
10419
|
+
const labelMap = buildLabelMap2(board.labels ?? []);
|
|
10420
|
+
let provider;
|
|
10421
|
+
try {
|
|
10422
|
+
provider = detectGitProvider2();
|
|
10423
|
+
} catch {
|
|
10424
|
+
provider = "unknown";
|
|
10425
|
+
}
|
|
10426
|
+
const reclaimed = await reclaimPreReviewStrands2({
|
|
10427
|
+
client,
|
|
10428
|
+
agentId,
|
|
10429
|
+
cards,
|
|
10430
|
+
columns,
|
|
10431
|
+
labelMap,
|
|
10432
|
+
reviewColumns: config.agent.review.pickupColumns,
|
|
10433
|
+
approvedLabel: config.agent.review.approvedLabel,
|
|
10434
|
+
graceMs: config.agent.timing.staleHeartbeatMs,
|
|
10435
|
+
knownCardIds: new Set,
|
|
10436
|
+
cwd: process.cwd(),
|
|
10437
|
+
provider
|
|
10438
|
+
});
|
|
10439
|
+
process.stdout.write(`recover: merge-strand pass complete; re-asserted ${reclaimed.length} pre-review strand(s)${reclaimed.length ? `: ${reclaimed.join(", ")}` : ""}
|
|
10440
|
+
`);
|
|
10441
|
+
if (reclaimed.length) {
|
|
10442
|
+
process.stdout.write(` start/keep the daemon running so review picks these up
|
|
10443
|
+
`);
|
|
10444
|
+
}
|
|
10445
|
+
return 0;
|
|
10446
|
+
}
|
|
10050
10447
|
async function dispatch(argv) {
|
|
10051
10448
|
const args = argv.filter((a) => a !== "--pretty" && a !== "--json");
|
|
10052
10449
|
const cmd = args[0];
|
|
@@ -10064,6 +10461,8 @@ async function dispatch(argv) {
|
|
|
10064
10461
|
return doctorCommand();
|
|
10065
10462
|
case "gc":
|
|
10066
10463
|
return gcCommand();
|
|
10464
|
+
case "recover":
|
|
10465
|
+
return recoverCommand();
|
|
10067
10466
|
case "help":
|
|
10068
10467
|
case "--help":
|
|
10069
10468
|
case "-h":
|