@gethmy/agent 1.14.3 → 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 +504 -86
- package/dist/index.js +454 -86
- 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({
|
|
@@ -7088,6 +7331,9 @@ function buildStagePreamble(stage) {
|
|
|
7088
7331
|
}
|
|
7089
7332
|
}
|
|
7090
7333
|
lines.push("Do only this stage's work. When the stage's handoff is met, end your session — advancement to the next stage is handled by the board.");
|
|
7334
|
+
if (normalizeGateSpec(stage.gate)?.kind === "review_passed") {
|
|
7335
|
+
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.");
|
|
7336
|
+
}
|
|
7091
7337
|
return lines.filter(Boolean).join(`
|
|
7092
7338
|
`);
|
|
7093
7339
|
}
|
|
@@ -7132,6 +7378,7 @@ class Worker {
|
|
|
7132
7378
|
lastDrainedSeq = 0;
|
|
7133
7379
|
runCostCents = 0;
|
|
7134
7380
|
runTurns = 0;
|
|
7381
|
+
lastRunText = "";
|
|
7135
7382
|
constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted, onApiError) {
|
|
7136
7383
|
this.config = config;
|
|
7137
7384
|
this.client = client;
|
|
@@ -7200,6 +7447,7 @@ class Worker {
|
|
|
7200
7447
|
this.completionStarted = false;
|
|
7201
7448
|
this.runCostCents = 0;
|
|
7202
7449
|
this.runTurns = 0;
|
|
7450
|
+
this.lastRunText = "";
|
|
7203
7451
|
this.cliSessionId = null;
|
|
7204
7452
|
this.lastDrainedSeq = 0;
|
|
7205
7453
|
this.cardId = card.id;
|
|
@@ -7288,7 +7536,9 @@ class Worker {
|
|
|
7288
7536
|
const basePrompt = await buildPrompt(enriched, this.branchName, this.worktreePath, this.client, this.workspaceId, this.projectId);
|
|
7289
7537
|
let prompt = basePrompt;
|
|
7290
7538
|
if (stageCtx.kind === "run") {
|
|
7291
|
-
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 });
|
|
7292
7542
|
prompt = [buildStagePreamble(stageCtx.stage), inherited, basePrompt].filter(Boolean).join(`
|
|
7293
7543
|
|
|
7294
7544
|
`);
|
|
@@ -7297,6 +7547,16 @@ class Worker {
|
|
|
7297
7547
|
stageName: stageCtx.stage.name,
|
|
7298
7548
|
owner: stageCtx.stage.owner
|
|
7299
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
|
+
}
|
|
7300
7560
|
}
|
|
7301
7561
|
await this.client.updateAgentProgress(card.id, {
|
|
7302
7562
|
agentIdentifier: agentIdentifier(this.id),
|
|
@@ -7458,15 +7718,19 @@ class Worker {
|
|
|
7458
7718
|
} catch {}
|
|
7459
7719
|
await this.recordOutcome(card.id, "failure");
|
|
7460
7720
|
} else if (this.runId && this.aborted) {
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
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)`);
|
|
7470
7734
|
}
|
|
7471
7735
|
try {
|
|
7472
7736
|
await this.stateStore.endRun(this.runId, "paused", {
|
|
@@ -7608,15 +7872,13 @@ class Worker {
|
|
|
7608
7872
|
} catch {}
|
|
7609
7873
|
}
|
|
7610
7874
|
}
|
|
7611
|
-
async loadInheritedHandoffSection(cardId, currentStageId) {
|
|
7875
|
+
async loadInheritedHandoffSection(cardId, currentStageId, opts = {}) {
|
|
7612
7876
|
try {
|
|
7613
|
-
const { comments } = await this.client.
|
|
7614
|
-
limit: 200
|
|
7615
|
-
});
|
|
7877
|
+
const { comments } = await this.client.request("GET", `/cards/${encodeURIComponent(cardId)}/comments?limit=200&order=desc&comment_type=decision`);
|
|
7616
7878
|
if (!Array.isArray(comments) || comments.length === 0)
|
|
7617
7879
|
return "";
|
|
7618
7880
|
const handoff = extractLatestHandoff(comments, {
|
|
7619
|
-
excludeStageId: currentStageId
|
|
7881
|
+
excludeStageId: opts.includeOwnStage ? undefined : currentStageId
|
|
7620
7882
|
});
|
|
7621
7883
|
return handoff ? renderInheritedHandoffSection(handoff) : "";
|
|
7622
7884
|
} catch (err) {
|
|
@@ -7644,7 +7906,9 @@ class Worker {
|
|
|
7644
7906
|
}
|
|
7645
7907
|
async collectStageGateEvidence(card, stage, worktreePath, subtasks) {
|
|
7646
7908
|
try {
|
|
7647
|
-
const
|
|
7909
|
+
const loop = getStageLoop(stage);
|
|
7910
|
+
const gateSource = isConvergeLoop(loop) ? loop?.exit_gate ?? null : stage.gate;
|
|
7911
|
+
const gate = normalizeGateSpec(gateSource);
|
|
7648
7912
|
if (!gate) {
|
|
7649
7913
|
return null;
|
|
7650
7914
|
}
|
|
@@ -7652,6 +7916,10 @@ class Worker {
|
|
|
7652
7916
|
log.info(this.tag, `Stage "${stage.name}" gate "${gate.kind}" is advisory — skipping enforcement`);
|
|
7653
7917
|
return null;
|
|
7654
7918
|
}
|
|
7919
|
+
const review = gate.kind === "review_passed" ? parseReviewOutput(this.lastRunText) : undefined;
|
|
7920
|
+
if (review) {
|
|
7921
|
+
log.info(this.tag, `Review-gated stage "${stage.name}" verdict: ${review.verdict} (${review.findings.length} finding(s))`);
|
|
7922
|
+
}
|
|
7655
7923
|
const registry = buildGateCollectorRegistry({
|
|
7656
7924
|
build: {
|
|
7657
7925
|
worktreePath,
|
|
@@ -7662,7 +7930,8 @@ class Worker {
|
|
|
7662
7930
|
worktreePath,
|
|
7663
7931
|
artifactType: stage.artifact_type
|
|
7664
7932
|
},
|
|
7665
|
-
checklist: { subtasks, cardDone: card.done ?? false }
|
|
7933
|
+
checklist: { subtasks, cardDone: card.done ?? false },
|
|
7934
|
+
...review ? { review } : {}
|
|
7666
7935
|
});
|
|
7667
7936
|
const context = {
|
|
7668
7937
|
cardId: card.id,
|
|
@@ -7682,11 +7951,8 @@ class Worker {
|
|
|
7682
7951
|
}
|
|
7683
7952
|
}
|
|
7684
7953
|
async advanceFromGateEvaluation(card, stage, stageIndex, def, evaluation) {
|
|
7685
|
-
if (!evaluation) {
|
|
7686
|
-
return { kind: "no_advance" };
|
|
7687
|
-
}
|
|
7688
7954
|
try {
|
|
7689
|
-
return await
|
|
7955
|
+
return await advanceStageRun(card, stage, stageIndex, def, evaluation, {
|
|
7690
7956
|
client: this.client,
|
|
7691
7957
|
stateStore: this.stateStore,
|
|
7692
7958
|
agentId: this.agentId,
|
|
@@ -8000,6 +8266,9 @@ class Worker {
|
|
|
8000
8266
|
});
|
|
8001
8267
|
}
|
|
8002
8268
|
}
|
|
8269
|
+
parser.on("text", (content) => {
|
|
8270
|
+
this.lastRunText += content;
|
|
8271
|
+
});
|
|
8003
8272
|
parser.on("parse_error", (msg) => {
|
|
8004
8273
|
log.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
|
|
8005
8274
|
runLog?.stream.write(`
|
|
@@ -8093,6 +8362,10 @@ class Worker {
|
|
|
8093
8362
|
try {
|
|
8094
8363
|
for await (const ev of stream) {
|
|
8095
8364
|
this.progressTracker?.ingest(ev);
|
|
8365
|
+
if (ev.kind === "assistant_text") {
|
|
8366
|
+
this.lastRunText += `${ev.payload.text}
|
|
8367
|
+
`;
|
|
8368
|
+
}
|
|
8096
8369
|
if (ev.source === "agent")
|
|
8097
8370
|
this.cliRunner?.record(ev);
|
|
8098
8371
|
if (ev.kind === "error") {
|
|
@@ -8180,6 +8453,8 @@ var init_worker = __esm(() => {
|
|
|
8180
8453
|
init_process_group();
|
|
8181
8454
|
init_progress_tracker();
|
|
8182
8455
|
init_prompt();
|
|
8456
|
+
init_review_completion();
|
|
8457
|
+
init_review_knowledge();
|
|
8183
8458
|
init_run_log();
|
|
8184
8459
|
init_sdk_agent_runner();
|
|
8185
8460
|
init_stage_advance();
|
|
@@ -8653,6 +8928,71 @@ var init_recovery = __esm(() => {
|
|
|
8653
8928
|
init_worktree();
|
|
8654
8929
|
});
|
|
8655
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
|
+
|
|
8656
8996
|
// src/reconcile.ts
|
|
8657
8997
|
class Reconciler {
|
|
8658
8998
|
client;
|
|
@@ -8667,6 +9007,7 @@ class Reconciler {
|
|
|
8667
9007
|
agentConfig;
|
|
8668
9008
|
timer = null;
|
|
8669
9009
|
lastTickAt = null;
|
|
9010
|
+
gitProvider = null;
|
|
8670
9011
|
get lastTick() {
|
|
8671
9012
|
return this.lastTickAt;
|
|
8672
9013
|
}
|
|
@@ -8694,7 +9035,7 @@ class Reconciler {
|
|
|
8694
9035
|
clearInterval(this.timer);
|
|
8695
9036
|
this.timer = null;
|
|
8696
9037
|
}
|
|
8697
|
-
log.info(
|
|
9038
|
+
log.info(TAG34, "Heartbeat stopped");
|
|
8698
9039
|
}
|
|
8699
9040
|
async recoverStaleRuns() {
|
|
8700
9041
|
if (!this.stateStore || !this.agentConfig)
|
|
@@ -8711,7 +9052,7 @@ class Reconciler {
|
|
|
8711
9052
|
if (!daemonDead && !(heartbeatStale && ourZombie))
|
|
8712
9053
|
continue;
|
|
8713
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`;
|
|
8714
|
-
log.warn(
|
|
9055
|
+
log.warn(TAG34, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
|
|
8715
9056
|
await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
|
|
8716
9057
|
runId: run.runId,
|
|
8717
9058
|
cardId: run.cardId,
|
|
@@ -8738,14 +9079,38 @@ class Reconciler {
|
|
|
8738
9079
|
const stalledAt = Date.parse(card.updated_at ?? "");
|
|
8739
9080
|
if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
|
|
8740
9081
|
continue;
|
|
8741
|
-
log.warn(
|
|
9082
|
+
log.warn(TAG34, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
|
|
8742
9083
|
try {
|
|
8743
9084
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
8744
9085
|
} catch (err) {
|
|
8745
|
-
log.error(
|
|
9086
|
+
log.error(TAG34, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
8746
9087
|
}
|
|
8747
9088
|
}
|
|
8748
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;
|
|
9098
|
+
}
|
|
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
|
+
});
|
|
9113
|
+
}
|
|
8749
9114
|
async releaseStalledApprovals(cards, columns, knownCardIds) {
|
|
8750
9115
|
const planning = this.agentConfig?.planning;
|
|
8751
9116
|
if (!planning?.enabled || planning.mode !== "gated" || planning.approvalTtlHours <= 0) {
|
|
@@ -8765,11 +9130,11 @@ class Reconciler {
|
|
|
8765
9130
|
const parkedAt = Date.parse(card.updated_at ?? "");
|
|
8766
9131
|
if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
|
|
8767
9132
|
continue;
|
|
8768
|
-
log.warn(
|
|
9133
|
+
log.warn(TAG34, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
|
|
8769
9134
|
try {
|
|
8770
9135
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
8771
9136
|
} catch (err) {
|
|
8772
|
-
log.error(
|
|
9137
|
+
log.error(TAG34, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
8773
9138
|
}
|
|
8774
9139
|
}
|
|
8775
9140
|
}
|
|
@@ -8812,21 +9177,21 @@ class Reconciler {
|
|
|
8812
9177
|
const subtasks = card.subtasks ?? [];
|
|
8813
9178
|
const mode = route.mode;
|
|
8814
9179
|
if (route.stage) {
|
|
8815
|
-
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`);
|
|
8816
9181
|
}
|
|
8817
9182
|
if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
|
|
8818
|
-
log.debug(
|
|
9183
|
+
log.debug(TAG34, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
|
|
8819
9184
|
continue;
|
|
8820
9185
|
}
|
|
8821
9186
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
8822
|
-
log.debug(
|
|
9187
|
+
log.debug(TAG34, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
|
|
8823
9188
|
continue;
|
|
8824
9189
|
}
|
|
8825
9190
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
8826
|
-
log.debug(
|
|
9191
|
+
log.debug(TAG34, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
|
|
8827
9192
|
continue;
|
|
8828
9193
|
}
|
|
8829
|
-
log.info(
|
|
9194
|
+
log.info(TAG34, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
|
|
8830
9195
|
await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
8831
9196
|
}
|
|
8832
9197
|
}
|
|
@@ -8834,25 +9199,28 @@ class Reconciler {
|
|
|
8834
9199
|
await this.recoverStaleRuns();
|
|
8835
9200
|
}
|
|
8836
9201
|
await this.recoverStrandedInProgress(cards, columns, knownCardIds);
|
|
9202
|
+
await this.recoverStrandedReview(cards, columns, labelMap, knownCardIds);
|
|
8837
9203
|
for (const knownId of knownCardIds) {
|
|
8838
9204
|
if (!allAgentCardIds.has(knownId)) {
|
|
8839
|
-
log.info(
|
|
9205
|
+
log.info(TAG34, `Missed unassign: ${knownId} — removing`);
|
|
8840
9206
|
await this.pool.removeCard(knownId);
|
|
8841
9207
|
}
|
|
8842
9208
|
}
|
|
8843
9209
|
await this.releaseStalledApprovals(cards, columns, knownCardIds);
|
|
8844
|
-
log.debug(
|
|
9210
|
+
log.debug(TAG34, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
|
|
8845
9211
|
} catch (err) {
|
|
8846
|
-
log.error(
|
|
9212
|
+
log.error(TAG34, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
|
|
8847
9213
|
}
|
|
8848
9214
|
}
|
|
8849
9215
|
}
|
|
8850
|
-
var
|
|
9216
|
+
var TAG34 = "reconcile";
|
|
8851
9217
|
var init_reconcile = __esm(() => {
|
|
8852
9218
|
init_board_helpers();
|
|
9219
|
+
init_git_pr();
|
|
8853
9220
|
init_log();
|
|
8854
9221
|
init_recovery();
|
|
8855
9222
|
init_review_worktree();
|
|
9223
|
+
init_strand_recovery();
|
|
8856
9224
|
init_types();
|
|
8857
9225
|
});
|
|
8858
9226
|
|
|
@@ -8885,7 +9253,7 @@ function prettyBanner(config, version) {
|
|
|
8885
9253
|
checks.push({ kind: "ok", message });
|
|
8886
9254
|
},
|
|
8887
9255
|
warn(message) {
|
|
8888
|
-
log.warn(
|
|
9256
|
+
log.warn(TAG35, message);
|
|
8889
9257
|
checks.push({ kind: "warn", message: message.split(`
|
|
8890
9258
|
`, 1)[0] });
|
|
8891
9259
|
},
|
|
@@ -8910,25 +9278,25 @@ function prettyBanner(config, version) {
|
|
|
8910
9278
|
};
|
|
8911
9279
|
}
|
|
8912
9280
|
function jsonBanner(config, version) {
|
|
8913
|
-
log.info(
|
|
8914
|
-
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(", ")}`);
|
|
8915
9283
|
if (config.agent.review.enabled) {
|
|
8916
|
-
log.info(
|
|
9284
|
+
log.info(TAG35, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
|
|
8917
9285
|
}
|
|
8918
9286
|
let failed = false;
|
|
8919
9287
|
return {
|
|
8920
9288
|
setProjectName(_name) {},
|
|
8921
9289
|
setGitProvider(provider) {
|
|
8922
|
-
log.info(
|
|
9290
|
+
log.info(TAG35, `Git provider: ${provider}`);
|
|
8923
9291
|
},
|
|
8924
9292
|
setHttpPort(port) {
|
|
8925
|
-
log.info(
|
|
9293
|
+
log.info(TAG35, `HTTP server on port ${port}`);
|
|
8926
9294
|
},
|
|
8927
9295
|
check(message) {
|
|
8928
|
-
log.info(
|
|
9296
|
+
log.info(TAG35, message);
|
|
8929
9297
|
},
|
|
8930
9298
|
warn(message) {
|
|
8931
|
-
log.warn(
|
|
9299
|
+
log.warn(TAG35, message);
|
|
8932
9300
|
},
|
|
8933
9301
|
fail() {
|
|
8934
9302
|
failed = true;
|
|
@@ -8936,7 +9304,7 @@ function jsonBanner(config, version) {
|
|
|
8936
9304
|
async ready(message) {
|
|
8937
9305
|
if (failed)
|
|
8938
9306
|
return;
|
|
8939
|
-
log.info(
|
|
9307
|
+
log.info(TAG35, message);
|
|
8940
9308
|
}
|
|
8941
9309
|
};
|
|
8942
9310
|
}
|
|
@@ -9017,7 +9385,7 @@ function cyan(s) {
|
|
|
9017
9385
|
function yellow(s) {
|
|
9018
9386
|
return `${ANSI.yellow}${s}${ANSI.reset}`;
|
|
9019
9387
|
}
|
|
9020
|
-
var
|
|
9388
|
+
var TAG35 = "daemon", RULE_WIDTH = 70, ANSI;
|
|
9021
9389
|
var init_startup_banner = __esm(() => {
|
|
9022
9390
|
init_log();
|
|
9023
9391
|
ANSI = {
|
|
@@ -9168,13 +9536,13 @@ class Watcher {
|
|
|
9168
9536
|
}
|
|
9169
9537
|
async start() {
|
|
9170
9538
|
if (!isPretty()) {
|
|
9171
|
-
log.info(
|
|
9539
|
+
log.info(TAG36, "Connecting to Supabase realtime (broadcast)...");
|
|
9172
9540
|
}
|
|
9173
9541
|
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
9174
9542
|
const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
|
|
9175
9543
|
this.subscribeBroadcast();
|
|
9176
9544
|
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
9177
|
-
log.debug(
|
|
9545
|
+
log.debug(TAG36, "Presence sync");
|
|
9178
9546
|
}).subscribe(async (status) => {
|
|
9179
9547
|
if (status === "SUBSCRIBED") {
|
|
9180
9548
|
await presenceChannel.track({
|
|
@@ -9187,7 +9555,7 @@ class Watcher {
|
|
|
9187
9555
|
agentName: this.identity.agentName
|
|
9188
9556
|
});
|
|
9189
9557
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
9190
|
-
log.info(
|
|
9558
|
+
log.info(TAG36, "Presence tracked on board-presence channel");
|
|
9191
9559
|
}
|
|
9192
9560
|
this.presenceTracked = true;
|
|
9193
9561
|
this.maybeResolveReady();
|
|
@@ -9200,13 +9568,13 @@ class Watcher {
|
|
|
9200
9568
|
return;
|
|
9201
9569
|
const gen = ++this.broadcastGen;
|
|
9202
9570
|
this.channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
|
|
9203
|
-
log.debug(
|
|
9571
|
+
log.debug(TAG36, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
|
|
9204
9572
|
this.onCardBroadcast({
|
|
9205
9573
|
event: "card_update",
|
|
9206
9574
|
payload: msg.payload ?? {}
|
|
9207
9575
|
});
|
|
9208
9576
|
}).on("broadcast", { event: "card_created" }, (msg) => {
|
|
9209
|
-
log.debug(
|
|
9577
|
+
log.debug(TAG36, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
|
|
9210
9578
|
this.onCardBroadcast({
|
|
9211
9579
|
event: "card_created",
|
|
9212
9580
|
payload: msg.payload ?? {}
|
|
@@ -9216,7 +9584,7 @@ class Watcher {
|
|
|
9216
9584
|
const cardId = payload.card_id;
|
|
9217
9585
|
const command = payload.command;
|
|
9218
9586
|
if (cardId && command) {
|
|
9219
|
-
log.info(
|
|
9587
|
+
log.info(TAG36, `Broadcast: agent_command ${command} for ${cardId}`);
|
|
9220
9588
|
this.onAgentCommand?.({ cardId, command });
|
|
9221
9589
|
}
|
|
9222
9590
|
}).subscribe((status) => {
|
|
@@ -9226,13 +9594,13 @@ class Watcher {
|
|
|
9226
9594
|
this.connected = true;
|
|
9227
9595
|
this.reconnectAttempts = 0;
|
|
9228
9596
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
9229
|
-
log.info(
|
|
9597
|
+
log.info(TAG36, "Broadcast subscription active");
|
|
9230
9598
|
}
|
|
9231
9599
|
this.maybeResolveReady();
|
|
9232
9600
|
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
|
|
9233
9601
|
this.connected = false;
|
|
9234
9602
|
if (!this.stopping) {
|
|
9235
|
-
log.warn(
|
|
9603
|
+
log.warn(TAG36, `Broadcast subscription ${status} — scheduling reconnect`);
|
|
9236
9604
|
this.scheduleReconnect();
|
|
9237
9605
|
}
|
|
9238
9606
|
}
|
|
@@ -9251,7 +9619,7 @@ class Watcher {
|
|
|
9251
9619
|
async reconnectBroadcast() {
|
|
9252
9620
|
if (this.stopping || !this.supabase)
|
|
9253
9621
|
return;
|
|
9254
|
-
log.warn(
|
|
9622
|
+
log.warn(TAG36, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
|
|
9255
9623
|
if (this.channel) {
|
|
9256
9624
|
const old = this.channel;
|
|
9257
9625
|
this.channel = null;
|
|
@@ -9281,10 +9649,10 @@ class Watcher {
|
|
|
9281
9649
|
this.supabase = null;
|
|
9282
9650
|
}
|
|
9283
9651
|
this.connected = false;
|
|
9284
|
-
log.info(
|
|
9652
|
+
log.info(TAG36, "Broadcast subscription stopped");
|
|
9285
9653
|
}
|
|
9286
9654
|
}
|
|
9287
|
-
var
|
|
9655
|
+
var TAG36 = "watcher";
|
|
9288
9656
|
var init_watcher = __esm(() => {
|
|
9289
9657
|
init_log();
|
|
9290
9658
|
});
|
|
@@ -9371,10 +9739,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
|
|
|
9371
9739
|
});
|
|
9372
9740
|
} catch {}
|
|
9373
9741
|
if (result.removed.length > 0) {
|
|
9374
|
-
log.info(
|
|
9742
|
+
log.info(TAG37, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
|
|
9375
9743
|
}
|
|
9376
9744
|
if (result.errors.length > 0) {
|
|
9377
|
-
log.warn(
|
|
9745
|
+
log.warn(TAG37, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
|
|
9378
9746
|
}
|
|
9379
9747
|
return result;
|
|
9380
9748
|
}
|
|
@@ -9404,7 +9772,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
9404
9772
|
} catch (err) {
|
|
9405
9773
|
const detail = gitErrorDetail2(err);
|
|
9406
9774
|
if (isTransientGitNetworkError(detail)) {
|
|
9407
|
-
log.debug(
|
|
9775
|
+
log.debug(TAG37, `Remote branch GC skipped — remote unreachable: ${detail}`);
|
|
9408
9776
|
return result;
|
|
9409
9777
|
}
|
|
9410
9778
|
result.errors.push({ ref: "fetch", error: detail });
|
|
@@ -9443,7 +9811,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
9443
9811
|
continue;
|
|
9444
9812
|
}
|
|
9445
9813
|
if (clock() > sweepDeadline) {
|
|
9446
|
-
log.debug(
|
|
9814
|
+
log.debug(TAG37, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
|
|
9447
9815
|
break;
|
|
9448
9816
|
}
|
|
9449
9817
|
try {
|
|
@@ -9456,17 +9824,17 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
9456
9824
|
} catch (err) {
|
|
9457
9825
|
const detail = gitErrorDetail2(err);
|
|
9458
9826
|
if (isTransientGitNetworkError(detail)) {
|
|
9459
|
-
log.debug(
|
|
9827
|
+
log.debug(TAG37, `Remote branch GC interrupted — remote unreachable: ${detail}`);
|
|
9460
9828
|
break;
|
|
9461
9829
|
}
|
|
9462
9830
|
result.errors.push({ ref, error: detail });
|
|
9463
9831
|
}
|
|
9464
9832
|
}
|
|
9465
9833
|
if (result.removed.length > 0) {
|
|
9466
|
-
log.info(
|
|
9834
|
+
log.info(TAG37, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
|
|
9467
9835
|
}
|
|
9468
9836
|
if (result.errors.length > 0) {
|
|
9469
|
-
log.warn(
|
|
9837
|
+
log.warn(TAG37, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
|
|
9470
9838
|
}
|
|
9471
9839
|
return result;
|
|
9472
9840
|
}
|
|
@@ -9497,13 +9865,13 @@ class WorktreeGc {
|
|
|
9497
9865
|
try {
|
|
9498
9866
|
runWorktreeGc(this.basePath, this.store);
|
|
9499
9867
|
} catch (err) {
|
|
9500
|
-
log.warn(
|
|
9868
|
+
log.warn(TAG37, `GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
9501
9869
|
}
|
|
9502
9870
|
if (this.remoteOpts) {
|
|
9503
9871
|
try {
|
|
9504
9872
|
pruneFailedRemoteBranches(this.remoteOpts);
|
|
9505
9873
|
} catch (err) {
|
|
9506
|
-
log.warn(
|
|
9874
|
+
log.warn(TAG37, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
9507
9875
|
}
|
|
9508
9876
|
}
|
|
9509
9877
|
}
|
|
@@ -9517,7 +9885,7 @@ function getRepoRoot2() {
|
|
|
9517
9885
|
return null;
|
|
9518
9886
|
}
|
|
9519
9887
|
}
|
|
9520
|
-
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;
|
|
9521
9889
|
var init_worktree_gc = __esm(() => {
|
|
9522
9890
|
init_log();
|
|
9523
9891
|
init_worktree();
|
|
@@ -9621,7 +9989,7 @@ async function main() {
|
|
|
9621
9989
|
} catch (err) {
|
|
9622
9990
|
if (err instanceof ConfigValidationError) {
|
|
9623
9991
|
banner.fail();
|
|
9624
|
-
log.error(
|
|
9992
|
+
log.error(TAG38, err.message);
|
|
9625
9993
|
process.exit(1);
|
|
9626
9994
|
}
|
|
9627
9995
|
throw err;
|
|
@@ -9731,7 +10099,7 @@ async function main() {
|
|
|
9731
10099
|
if (shuttingDown)
|
|
9732
10100
|
return;
|
|
9733
10101
|
shuttingDown = true;
|
|
9734
|
-
log.info(
|
|
10102
|
+
log.info(TAG38, `Received ${signal}, shutting down gracefully...`);
|
|
9735
10103
|
reconciler.stop();
|
|
9736
10104
|
mergeMonitor?.stop();
|
|
9737
10105
|
worktreeGc.stop();
|
|
@@ -9741,18 +10109,18 @@ async function main() {
|
|
|
9741
10109
|
}
|
|
9742
10110
|
await watcher.stop();
|
|
9743
10111
|
await pool.shutdown();
|
|
9744
|
-
log.info(
|
|
10112
|
+
log.info(TAG38, "Daemon stopped.");
|
|
9745
10113
|
process.exit(exitCode);
|
|
9746
10114
|
};
|
|
9747
10115
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
9748
10116
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
9749
10117
|
process.on("uncaughtException", (err) => {
|
|
9750
|
-
log.error(
|
|
10118
|
+
log.error(TAG38, `Uncaught exception: ${err.message}`);
|
|
9751
10119
|
exitCode = 1;
|
|
9752
10120
|
shutdown("uncaughtException");
|
|
9753
10121
|
});
|
|
9754
10122
|
process.on("unhandledRejection", (reason) => {
|
|
9755
|
-
log.error(
|
|
10123
|
+
log.error(TAG38, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
9756
10124
|
exitCode = 1;
|
|
9757
10125
|
shutdown("unhandledRejection");
|
|
9758
10126
|
});
|
|
@@ -9805,29 +10173,29 @@ async function handleBroadcast(event, client, pool, config, agentId) {
|
|
|
9805
10173
|
if (assignedAgentId === undefined)
|
|
9806
10174
|
return;
|
|
9807
10175
|
if (assignedAgentId === agentId) {
|
|
9808
|
-
log.info(
|
|
10176
|
+
log.info(TAG38, `Broadcast: card ${cardId} assigned to agent`);
|
|
9809
10177
|
try {
|
|
9810
10178
|
await pool.resetAttemptsForReassign(cardId);
|
|
9811
10179
|
await tryEnqueueCard(cardId, client, pool, config, agentId);
|
|
9812
10180
|
} catch (err) {
|
|
9813
|
-
log.error(
|
|
10181
|
+
log.error(TAG38, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
|
|
9814
10182
|
}
|
|
9815
10183
|
} else if (pool.isCardKnown(cardId)) {
|
|
9816
|
-
log.info(
|
|
10184
|
+
log.info(TAG38, `Broadcast: card ${cardId} unassigned from agent`);
|
|
9817
10185
|
await pool.removeCard(cardId);
|
|
9818
10186
|
}
|
|
9819
10187
|
}
|
|
9820
10188
|
async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
9821
10189
|
const { card } = await client.getCard(cardId);
|
|
9822
10190
|
if (card.assigned_agent_id !== agentId) {
|
|
9823
|
-
log.debug(
|
|
10191
|
+
log.debug(TAG38, `Card ${cardId} no longer assigned to agent — skipping`);
|
|
9824
10192
|
return;
|
|
9825
10193
|
}
|
|
9826
10194
|
const board = await client.getBoard(config.projectId, { summary: true });
|
|
9827
10195
|
const columns = board.columns;
|
|
9828
10196
|
const column = columns.find((c) => c.id === card.column_id);
|
|
9829
10197
|
if (!column) {
|
|
9830
|
-
log.warn(
|
|
10198
|
+
log.warn(TAG38, `Column not found for card ${cardId}`);
|
|
9831
10199
|
return;
|
|
9832
10200
|
}
|
|
9833
10201
|
const route = classifyPickup(card, column.name, {
|
|
@@ -9836,27 +10204,27 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
9836
10204
|
playbooks: config.agent.playbooks
|
|
9837
10205
|
});
|
|
9838
10206
|
if (!route) {
|
|
9839
|
-
log.info(
|
|
10207
|
+
log.info(TAG38, `Card #${card.short_id} is in "${column.name}", not a pickup/review/stage column — skipping`);
|
|
9840
10208
|
return;
|
|
9841
10209
|
}
|
|
9842
10210
|
if (route.stage) {
|
|
9843
|
-
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`);
|
|
9844
10212
|
}
|
|
9845
10213
|
const mode = route.mode;
|
|
9846
10214
|
const labelMap = buildLabelMap(board.labels ?? []);
|
|
9847
10215
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
9848
10216
|
const subtasks = card.subtasks ?? [];
|
|
9849
10217
|
if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
|
|
9850
|
-
log.debug(
|
|
10218
|
+
log.debug(TAG38, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
|
|
9851
10219
|
return;
|
|
9852
10220
|
}
|
|
9853
10221
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
9854
|
-
log.info(
|
|
10222
|
+
log.info(TAG38, `Card #${card.short_id} has no branch reference — skipping auto-review`);
|
|
9855
10223
|
return;
|
|
9856
10224
|
}
|
|
9857
10225
|
await pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
9858
10226
|
}
|
|
9859
|
-
var
|
|
10227
|
+
var TAG38 = "daemon", PKG_VERSION;
|
|
9860
10228
|
var init_src = __esm(() => {
|
|
9861
10229
|
init_board_helpers();
|
|
9862
10230
|
init_config();
|
|
@@ -9890,6 +10258,7 @@ Usage:
|
|
|
9890
10258
|
harmony-agent health Exit 0 if daemon is healthy, 1 otherwise
|
|
9891
10259
|
harmony-agent doctor Run preflight checks (don't start)
|
|
9892
10260
|
harmony-agent gc One-shot worktree garbage collection
|
|
10261
|
+
harmony-agent recover One-shot recovery of stranded Review cards
|
|
9893
10262
|
harmony-agent help Show this help
|
|
9894
10263
|
|
|
9895
10264
|
Flags:
|
|
@@ -10028,6 +10397,53 @@ async function gcCommand() {
|
|
|
10028
10397
|
}
|
|
10029
10398
|
return 0;
|
|
10030
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
|
+
}
|
|
10031
10447
|
async function dispatch(argv) {
|
|
10032
10448
|
const args = argv.filter((a) => a !== "--pretty" && a !== "--json");
|
|
10033
10449
|
const cmd = args[0];
|
|
@@ -10045,6 +10461,8 @@ async function dispatch(argv) {
|
|
|
10045
10461
|
return doctorCommand();
|
|
10046
10462
|
case "gc":
|
|
10047
10463
|
return gcCommand();
|
|
10464
|
+
case "recover":
|
|
10465
|
+
return recoverCommand();
|
|
10048
10466
|
case "help":
|
|
10049
10467
|
case "--help":
|
|
10050
10468
|
case "-h":
|