@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/index.js
CHANGED
|
@@ -101,6 +101,16 @@ var init_log = __esm(() => {
|
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
// src/board-helpers.ts
|
|
104
|
+
var exports_board_helpers = {};
|
|
105
|
+
__export(exports_board_helpers, {
|
|
106
|
+
resolveCardLabels: () => resolveCardLabels,
|
|
107
|
+
moveCardToColumn: () => moveCardToColumn,
|
|
108
|
+
moveCardAndAddLabel: () => moveCardAndAddLabel,
|
|
109
|
+
hasLabel: () => hasLabel,
|
|
110
|
+
findOrCreateLabel: () => findOrCreateLabel,
|
|
111
|
+
buildLabelMap: () => buildLabelMap,
|
|
112
|
+
addLabelByName: () => addLabelByName
|
|
113
|
+
});
|
|
104
114
|
function buildLabelMap(boardLabels) {
|
|
105
115
|
const map = new Map;
|
|
106
116
|
for (const label of boardLabels) {
|
|
@@ -694,6 +704,22 @@ var init_config_validation = __esm(() => {
|
|
|
694
704
|
});
|
|
695
705
|
|
|
696
706
|
// src/git-pr.ts
|
|
707
|
+
var exports_git_pr = {};
|
|
708
|
+
__export(exports_git_pr, {
|
|
709
|
+
validateGitProviderCli: () => validateGitProviderCli,
|
|
710
|
+
updateExistingPr: () => updateExistingPr,
|
|
711
|
+
resolvePrUrl: () => resolvePrUrl,
|
|
712
|
+
renameRemoteBranch: () => renameRemoteBranch,
|
|
713
|
+
remoteBranchExists: () => remoteBranchExists,
|
|
714
|
+
pushBranch: () => pushBranch,
|
|
715
|
+
getBranchWebUrl: () => getBranchWebUrl,
|
|
716
|
+
findExistingPr: () => findExistingPr,
|
|
717
|
+
extractPrUrl: () => extractPrUrl,
|
|
718
|
+
detectGitProvider: () => detectGitProvider,
|
|
719
|
+
createPullRequest: () => createPullRequest,
|
|
720
|
+
checkPrMergeStatus: () => checkPrMergeStatus,
|
|
721
|
+
buildPrBody: () => buildPrBody
|
|
722
|
+
});
|
|
697
723
|
import { execFile, execFileSync } from "node:child_process";
|
|
698
724
|
import { promisify } from "node:util";
|
|
699
725
|
function detectGitProvider(cwd) {
|
|
@@ -815,6 +841,14 @@ function extractPrUrl(description) {
|
|
|
815
841
|
return null;
|
|
816
842
|
}
|
|
817
843
|
}
|
|
844
|
+
function resolvePrUrl(description, branchName, cwd, provider) {
|
|
845
|
+
const fromDesc = extractPrUrl(description);
|
|
846
|
+
if (fromDesc)
|
|
847
|
+
return fromDesc;
|
|
848
|
+
if (!branchName)
|
|
849
|
+
return null;
|
|
850
|
+
return findExistingPr(branchName, cwd, provider) || null;
|
|
851
|
+
}
|
|
818
852
|
function remoteBranchExists(branchName, cwd) {
|
|
819
853
|
try {
|
|
820
854
|
execFileSync("git", ["ls-remote", "--exit-code", "origin", `refs/heads/${branchName}`], { cwd, stdio: "pipe" });
|
|
@@ -1601,6 +1635,48 @@ var init_logger = () => {};
|
|
|
1601
1635
|
var init_playbookCatalog = () => {};
|
|
1602
1636
|
|
|
1603
1637
|
// ../harmony-shared/dist/playbookStage.js
|
|
1638
|
+
function normalizeLoopDef(raw) {
|
|
1639
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
1640
|
+
return null;
|
|
1641
|
+
const obj = raw;
|
|
1642
|
+
if (obj.mode !== "converge" && obj.mode !== "fanout")
|
|
1643
|
+
return null;
|
|
1644
|
+
const mode = obj.mode;
|
|
1645
|
+
const rawMax = obj.max_iterations;
|
|
1646
|
+
const maxInt = typeof rawMax === "number" && Number.isFinite(rawMax) && rawMax >= 1 ? Math.floor(rawMax) : DEFAULT_LOOP_MAX_ITERATIONS;
|
|
1647
|
+
const exitGate = obj.exit_gate && typeof obj.exit_gate === "object" && !Array.isArray(obj.exit_gate) ? obj.exit_gate : null;
|
|
1648
|
+
const def = { mode, max_iterations: maxInt };
|
|
1649
|
+
if (exitGate)
|
|
1650
|
+
def.exit_gate = exitGate;
|
|
1651
|
+
if (obj.item_source && typeof obj.item_source === "object" && !Array.isArray(obj.item_source)) {
|
|
1652
|
+
def.item_source = obj.item_source;
|
|
1653
|
+
}
|
|
1654
|
+
if (typeof obj.concurrency === "number" && obj.concurrency >= 1) {
|
|
1655
|
+
def.concurrency = Math.floor(obj.concurrency);
|
|
1656
|
+
}
|
|
1657
|
+
if (obj.on_item_fail === "continue" || obj.on_item_fail === "halt") {
|
|
1658
|
+
def.on_item_fail = obj.on_item_fail;
|
|
1659
|
+
}
|
|
1660
|
+
return def;
|
|
1661
|
+
}
|
|
1662
|
+
function getStageLoop(stage) {
|
|
1663
|
+
return normalizeLoopDef(stage.loop);
|
|
1664
|
+
}
|
|
1665
|
+
function isConvergeLoop(loop) {
|
|
1666
|
+
return loop !== null && loop.mode === "converge";
|
|
1667
|
+
}
|
|
1668
|
+
function decideLoopContinuation(args) {
|
|
1669
|
+
const { loop, gatePassed, hasExitGate, completedIterations } = args;
|
|
1670
|
+
const max = Math.max(1, Math.floor(loop.max_iterations) || 1);
|
|
1671
|
+
if (!hasExitGate) {
|
|
1672
|
+
return completedIterations >= max ? "exit" : "iterate";
|
|
1673
|
+
}
|
|
1674
|
+
if (gatePassed)
|
|
1675
|
+
return "exit";
|
|
1676
|
+
if (completedIterations >= max)
|
|
1677
|
+
return "exhausted";
|
|
1678
|
+
return "iterate";
|
|
1679
|
+
}
|
|
1604
1680
|
function readStageDefs(def) {
|
|
1605
1681
|
if (def.steps_version !== 2)
|
|
1606
1682
|
return [];
|
|
@@ -1637,7 +1713,7 @@ function entryActionAllowlist(entryAction) {
|
|
|
1637
1713
|
return `mcp__${entryAction}`;
|
|
1638
1714
|
return null;
|
|
1639
1715
|
}
|
|
1640
|
-
var SKILL_TOOL_ALLOWLIST, HARMONY_TOOL_RE;
|
|
1716
|
+
var DEFAULT_LOOP_MAX_ITERATIONS = 5, SKILL_TOOL_ALLOWLIST, HARMONY_TOOL_RE;
|
|
1641
1717
|
var init_playbookStage = __esm(() => {
|
|
1642
1718
|
SKILL_TOOL_ALLOWLIST = {
|
|
1643
1719
|
hmy: "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*",
|
|
@@ -2186,6 +2262,10 @@ var init_review_worktree = __esm(() => {
|
|
|
2186
2262
|
});
|
|
2187
2263
|
|
|
2188
2264
|
// src/merge-monitor.ts
|
|
2265
|
+
var exports_merge_monitor = {};
|
|
2266
|
+
__export(exports_merge_monitor, {
|
|
2267
|
+
MergeMonitor: () => MergeMonitor
|
|
2268
|
+
});
|
|
2189
2269
|
import { execFile as execFile2 } from "node:child_process";
|
|
2190
2270
|
import { promisify as promisify2 } from "node:util";
|
|
2191
2271
|
|
|
@@ -2219,6 +2299,9 @@ class MergeMonitor {
|
|
|
2219
2299
|
}
|
|
2220
2300
|
log.info(TAG7, "Merge monitor stopped");
|
|
2221
2301
|
}
|
|
2302
|
+
async runOnce() {
|
|
2303
|
+
await this.tick();
|
|
2304
|
+
}
|
|
2222
2305
|
async scheduleNext(delayMs) {
|
|
2223
2306
|
await new Promise((resolve3) => {
|
|
2224
2307
|
this.timer = setTimeout(() => resolve3(), delayMs);
|
|
@@ -2256,9 +2339,10 @@ class MergeMonitor {
|
|
|
2256
2339
|
const batch = candidatesWithLabels.slice(0, 5);
|
|
2257
2340
|
log.debug(TAG7, `Checking ${batch.length} Ready to Merge card(s)`);
|
|
2258
2341
|
const results = await Promise.allSettled(batch.map(async ({ card, labels }) => {
|
|
2259
|
-
const
|
|
2342
|
+
const branchName = extractBranchFromDescription(card.description);
|
|
2343
|
+
const prUrl = resolvePrUrl(card.description ?? null, branchName, this.cwd, this.provider);
|
|
2260
2344
|
if (!prUrl) {
|
|
2261
|
-
log.debug(TAG7, `#${card.short_id} has no PR
|
|
2345
|
+
log.debug(TAG7, `#${card.short_id} has no resolvable PR — skipping`);
|
|
2262
2346
|
return;
|
|
2263
2347
|
}
|
|
2264
2348
|
const state = await checkPrMergeStatus(prUrl, this.cwd, this.provider);
|
|
@@ -5574,6 +5658,30 @@ class StateStore {
|
|
|
5574
5658
|
rec.attempts = 0;
|
|
5575
5659
|
await this.persist();
|
|
5576
5660
|
}
|
|
5661
|
+
getLoopIterations(cardId, stageId) {
|
|
5662
|
+
const rec = this.getCard(cardId);
|
|
5663
|
+
if (!rec || rec.loopStageId !== stageId)
|
|
5664
|
+
return 0;
|
|
5665
|
+
return rec.loopIterations ?? 0;
|
|
5666
|
+
}
|
|
5667
|
+
async incrementLoopIteration(cardId, stageId) {
|
|
5668
|
+
const rec = this.ensureCard(cardId);
|
|
5669
|
+
if (rec.loopStageId !== stageId) {
|
|
5670
|
+
rec.loopStageId = stageId;
|
|
5671
|
+
rec.loopIterations = 0;
|
|
5672
|
+
}
|
|
5673
|
+
rec.loopIterations = (rec.loopIterations ?? 0) + 1;
|
|
5674
|
+
await this.persist();
|
|
5675
|
+
return rec.loopIterations;
|
|
5676
|
+
}
|
|
5677
|
+
async resetLoopIterations(cardId) {
|
|
5678
|
+
const rec = this.getCard(cardId);
|
|
5679
|
+
if (!rec || rec.loopIterations == null && rec.loopStageId == null)
|
|
5680
|
+
return;
|
|
5681
|
+
rec.loopStageId = null;
|
|
5682
|
+
rec.loopIterations = 0;
|
|
5683
|
+
await this.persist();
|
|
5684
|
+
}
|
|
5577
5685
|
async resetAttempts(cardId) {
|
|
5578
5686
|
const rec = this.getCard(cardId);
|
|
5579
5687
|
if (!rec || rec.attempts === 0)
|
|
@@ -6707,6 +6815,26 @@ class CliAgentRunner {
|
|
|
6707
6815
|
this.enqueue({ kind: "playbook_advanced", source: "system", payload });
|
|
6708
6816
|
this.startTimer();
|
|
6709
6817
|
}
|
|
6818
|
+
recordLoopIterationStarted(payload) {
|
|
6819
|
+
this.enqueue({ kind: "loop_iteration_started", source: "system", payload });
|
|
6820
|
+
this.startTimer();
|
|
6821
|
+
}
|
|
6822
|
+
recordLoopIterationEvaluated(payload) {
|
|
6823
|
+
this.enqueue({
|
|
6824
|
+
kind: "loop_iteration_evaluated",
|
|
6825
|
+
source: "system",
|
|
6826
|
+
payload: { ...payload, evidence: truncateOutput(payload.evidence) }
|
|
6827
|
+
});
|
|
6828
|
+
this.startTimer();
|
|
6829
|
+
}
|
|
6830
|
+
recordLoopCompleted(payload) {
|
|
6831
|
+
this.enqueue({ kind: "loop_completed", source: "system", payload });
|
|
6832
|
+
this.startTimer();
|
|
6833
|
+
}
|
|
6834
|
+
recordLoopExhausted(payload) {
|
|
6835
|
+
this.enqueue({ kind: "loop_exhausted", source: "system", payload });
|
|
6836
|
+
this.startTimer();
|
|
6837
|
+
}
|
|
6710
6838
|
record(body) {
|
|
6711
6839
|
this.enqueue(body);
|
|
6712
6840
|
this.startTimer();
|
|
@@ -6802,7 +6930,7 @@ When finished, call harmony_end_agent_session with status="completed".`
|
|
|
6802
6930
|
}
|
|
6803
6931
|
async function renderCommentsSection(client, cardId) {
|
|
6804
6932
|
try {
|
|
6805
|
-
const { comments } = await client.
|
|
6933
|
+
const { comments } = await client.request("GET", `/cards/${encodeURIComponent(cardId)}/comments?limit=200&order=desc`);
|
|
6806
6934
|
if (!Array.isArray(comments) || comments.length === 0)
|
|
6807
6935
|
return "";
|
|
6808
6936
|
const section = serializeCommentThread(comments, {
|
|
@@ -6934,6 +7062,121 @@ async function resolveStageColumnName(client, card, stage) {
|
|
|
6934
7062
|
async function persistStagePointer(client, card, body) {
|
|
6935
7063
|
await client.request("POST", `/cards/${encodeURIComponent(card.id)}/advance-stage`, body);
|
|
6936
7064
|
}
|
|
7065
|
+
async function advanceStageRun(card, stage, stageIndex, def, evaluation, deps) {
|
|
7066
|
+
const loop = getStageLoop(stage);
|
|
7067
|
+
if (!isConvergeLoop(loop)) {
|
|
7068
|
+
if (!evaluation)
|
|
7069
|
+
return { kind: "no_advance" };
|
|
7070
|
+
return advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps);
|
|
7071
|
+
}
|
|
7072
|
+
return advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loop, deps);
|
|
7073
|
+
}
|
|
7074
|
+
function firstErrorMessage(evaluation) {
|
|
7075
|
+
const e = evaluation?.findings.find((f) => f.level === "error");
|
|
7076
|
+
return e ? e.message : null;
|
|
7077
|
+
}
|
|
7078
|
+
async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loop, deps) {
|
|
7079
|
+
const hasExitGate = loop.exit_gate != null;
|
|
7080
|
+
const gatePassed = hasExitGate ? evaluation?.passed ?? false : false;
|
|
7081
|
+
const gateResult = !hasExitGate ? "skipped" : gatePassed ? "passed" : "failed";
|
|
7082
|
+
const maxIterations = Math.max(1, Math.floor(loop.max_iterations) || 1);
|
|
7083
|
+
const iteration = await deps.stateStore.incrementLoopIteration(card.id, stage.id);
|
|
7084
|
+
const decision = decideLoopContinuation({
|
|
7085
|
+
loop,
|
|
7086
|
+
gatePassed,
|
|
7087
|
+
hasExitGate,
|
|
7088
|
+
completedIterations: iteration
|
|
7089
|
+
});
|
|
7090
|
+
const evidence = evaluation ? evaluation.findings.map((f) => `[${f.level}] ${f.message}`).join(`
|
|
7091
|
+
`) : undefined;
|
|
7092
|
+
const verdictGloss = !hasExitGate ? "count-only" : gatePassed ? "gate passed" : firstErrorMessage(evaluation) ?? "gate unmet";
|
|
7093
|
+
const summary = `iteration ${iteration}/${maxIterations} — ${verdictGloss}${decision === "iterate" ? " → retrying" : ""}`;
|
|
7094
|
+
deps.sink?.recordLoopIterationEvaluated?.({
|
|
7095
|
+
stageId: stage.id,
|
|
7096
|
+
iteration,
|
|
7097
|
+
maxIterations,
|
|
7098
|
+
gateResult,
|
|
7099
|
+
evidence,
|
|
7100
|
+
summary
|
|
7101
|
+
});
|
|
7102
|
+
log.info(TAG28, `#${card.short_id} converge loop "${stage.name}": ${summary} → ${decision}`);
|
|
7103
|
+
if (decision === "exit") {
|
|
7104
|
+
await deps.stateStore.resetLoopIterations(card.id).catch(() => {});
|
|
7105
|
+
deps.sink?.recordLoopCompleted?.({
|
|
7106
|
+
stageId: stage.id,
|
|
7107
|
+
iterations: iteration,
|
|
7108
|
+
maxIterations,
|
|
7109
|
+
reason: hasExitGate ? "exit gate passed" : `count-only loop completed ${iteration} iteration(s)`
|
|
7110
|
+
});
|
|
7111
|
+
const exitEval = evaluation?.passed ? evaluation : {
|
|
7112
|
+
passed: true,
|
|
7113
|
+
findings: [
|
|
7114
|
+
{
|
|
7115
|
+
level: "info",
|
|
7116
|
+
message: `Converge loop "${stage.name}" complete after ${iteration} iteration(s).`
|
|
7117
|
+
}
|
|
7118
|
+
],
|
|
7119
|
+
structured: evaluation?.structured ?? {}
|
|
7120
|
+
};
|
|
7121
|
+
return advanceStageOnGate(card, stage, stageIndex, def, exitEval, deps);
|
|
7122
|
+
}
|
|
7123
|
+
if (decision === "exhausted") {
|
|
7124
|
+
await deps.stateStore.resetLoopIterations(card.id).catch(() => {});
|
|
7125
|
+
await deps.stateStore.decrementAttempt(card.id).catch(() => {});
|
|
7126
|
+
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.`;
|
|
7127
|
+
deps.sink?.recordLoopExhausted?.({
|
|
7128
|
+
stageId: stage.id,
|
|
7129
|
+
iterations: iteration,
|
|
7130
|
+
maxIterations,
|
|
7131
|
+
reason
|
|
7132
|
+
});
|
|
7133
|
+
try {
|
|
7134
|
+
await deps.client.updateAgentProgress(card.id, {
|
|
7135
|
+
agentIdentifier: "claude-code-stage",
|
|
7136
|
+
agentName: "Harmony Agent",
|
|
7137
|
+
status: "waiting",
|
|
7138
|
+
currentTask: reason
|
|
7139
|
+
});
|
|
7140
|
+
} catch {}
|
|
7141
|
+
await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, {
|
|
7142
|
+
keepAttempts: true
|
|
7143
|
+
});
|
|
7144
|
+
log.info(TAG28, `#${card.short_id} LoopExhausted: ${reason}`);
|
|
7145
|
+
return { kind: "held_gate_unmet", reason };
|
|
7146
|
+
}
|
|
7147
|
+
await deps.stateStore.decrementAttempt(card.id).catch(() => {});
|
|
7148
|
+
await writeIterationHandoff(card, stage, iteration, maxIterations, evaluation, deps);
|
|
7149
|
+
const toColumn = await resolveStageColumnName(deps.client, card, stage) ?? deps.fallbackColumn;
|
|
7150
|
+
try {
|
|
7151
|
+
await deps.client.addComment(card.id, `Converge loop — ${summary}. Re-running "${stage.name}".`, { commentType: "progress" });
|
|
7152
|
+
} catch {}
|
|
7153
|
+
await runTransition(deps.client, card, {
|
|
7154
|
+
move: { columnName: toColumn },
|
|
7155
|
+
addLabels: [{ name: AGENT_LABEL }],
|
|
7156
|
+
...isAgentRunnableOwner(stage.owner) ? { assignAgent: deps.agentId } : {}
|
|
7157
|
+
}, { store: deps.stateStore, runId: deps.runId });
|
|
7158
|
+
log.info(TAG28, `#${card.short_id} converge loop "${stage.name}" — requeued to "${toColumn}" for iteration ${iteration + 1}/${maxIterations}`);
|
|
7159
|
+
return { kind: "requeued_gate_unmet", toColumn };
|
|
7160
|
+
}
|
|
7161
|
+
async function writeIterationHandoff(card, stage, iteration, maxIterations, evaluation, deps) {
|
|
7162
|
+
try {
|
|
7163
|
+
const findings = evaluation?.findings.filter((f) => f.level !== "info") ?? [];
|
|
7164
|
+
const produced = findings.length > 0 ? `Iteration ${iteration}/${maxIterations} of the "${stage.name}" converge loop did not pass its exit gate. Findings:
|
|
7165
|
+
${findings.map((f) => `- [${f.level}] ${f.message}`).join(`
|
|
7166
|
+
`)}` : `Iteration ${iteration}/${maxIterations} of the "${stage.name}" converge loop completed; the loop continues.`;
|
|
7167
|
+
const body = buildHandoffCommentBody({
|
|
7168
|
+
stageId: stage.id,
|
|
7169
|
+
stageName: stage.name,
|
|
7170
|
+
artifactType: stage.artifact_type,
|
|
7171
|
+
produced,
|
|
7172
|
+
decisions: [],
|
|
7173
|
+
nextStageNeeds: "Address the findings above on the same branch and re-run; this stage repeats until its exit gate passes."
|
|
7174
|
+
});
|
|
7175
|
+
await deps.client.addComment(card.id, body, { commentType: "decision" });
|
|
7176
|
+
} catch (err) {
|
|
7177
|
+
log.warn(TAG28, `iteration-handoff write failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
7178
|
+
}
|
|
7179
|
+
}
|
|
6937
7180
|
async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps) {
|
|
6938
7181
|
const summary = gateSummary(stage, evaluation);
|
|
6939
7182
|
deps.sink?.recordStageGateEvaluated({
|
|
@@ -7292,7 +7535,9 @@ class Worker {
|
|
|
7292
7535
|
const basePrompt = await buildPrompt(enriched, this.branchName, this.worktreePath, this.client, this.workspaceId, this.projectId);
|
|
7293
7536
|
let prompt = basePrompt;
|
|
7294
7537
|
if (stageCtx.kind === "run") {
|
|
7295
|
-
const
|
|
7538
|
+
const loop = getStageLoop(stageCtx.stage);
|
|
7539
|
+
const isLoop = isConvergeLoop(loop);
|
|
7540
|
+
const inherited = await this.loadInheritedHandoffSection(card.id, stageCtx.stage.id, { includeOwnStage: isLoop });
|
|
7296
7541
|
prompt = [buildStagePreamble(stageCtx.stage), inherited, basePrompt].filter(Boolean).join(`
|
|
7297
7542
|
|
|
7298
7543
|
`);
|
|
@@ -7301,6 +7546,16 @@ class Worker {
|
|
|
7301
7546
|
stageName: stageCtx.stage.name,
|
|
7302
7547
|
owner: stageCtx.stage.owner
|
|
7303
7548
|
});
|
|
7549
|
+
if (isLoop && loop) {
|
|
7550
|
+
const priorIterations = this.stateStore.getLoopIterations(card.id, stageCtx.stage.id);
|
|
7551
|
+
this.cliRunner?.recordLoopIterationStarted({
|
|
7552
|
+
stageId: stageCtx.stage.id,
|
|
7553
|
+
stageName: stageCtx.stage.name,
|
|
7554
|
+
iteration: priorIterations + 1,
|
|
7555
|
+
maxIterations: Math.max(1, Math.floor(loop.max_iterations) || 1),
|
|
7556
|
+
mode: loop.mode
|
|
7557
|
+
});
|
|
7558
|
+
}
|
|
7304
7559
|
}
|
|
7305
7560
|
await this.client.updateAgentProgress(card.id, {
|
|
7306
7561
|
agentIdentifier: agentIdentifier(this.id),
|
|
@@ -7462,15 +7717,19 @@ class Worker {
|
|
|
7462
7717
|
} catch {}
|
|
7463
7718
|
await this.recordOutcome(card.id, "failure");
|
|
7464
7719
|
} else if (this.runId && this.aborted) {
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7720
|
+
if (!this.completionStarted) {
|
|
7721
|
+
try {
|
|
7722
|
+
await this.client.updateCard(card.id, { assignedAgentId: null });
|
|
7723
|
+
} catch (err) {
|
|
7724
|
+
log.warn(this.tag, `failed to release card after stop: ${err instanceof Error ? err.message : err}`);
|
|
7725
|
+
}
|
|
7726
|
+
try {
|
|
7727
|
+
await runTransition(this.client, card, { removeLabels: ["agent"] });
|
|
7728
|
+
} catch (tErr) {
|
|
7729
|
+
log.warn(this.tag, `stop label cleanup failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
|
|
7730
|
+
}
|
|
7731
|
+
} else {
|
|
7732
|
+
log.info(this.tag, `cancel arrived after completion on #${card.short_id} — keeping assignment so review picks it up (#585)`);
|
|
7474
7733
|
}
|
|
7475
7734
|
try {
|
|
7476
7735
|
await this.stateStore.endRun(this.runId, "paused", {
|
|
@@ -7612,15 +7871,13 @@ class Worker {
|
|
|
7612
7871
|
} catch {}
|
|
7613
7872
|
}
|
|
7614
7873
|
}
|
|
7615
|
-
async loadInheritedHandoffSection(cardId, currentStageId) {
|
|
7874
|
+
async loadInheritedHandoffSection(cardId, currentStageId, opts = {}) {
|
|
7616
7875
|
try {
|
|
7617
|
-
const { comments } = await this.client.
|
|
7618
|
-
limit: 200
|
|
7619
|
-
});
|
|
7876
|
+
const { comments } = await this.client.request("GET", `/cards/${encodeURIComponent(cardId)}/comments?limit=200&order=desc&comment_type=decision`);
|
|
7620
7877
|
if (!Array.isArray(comments) || comments.length === 0)
|
|
7621
7878
|
return "";
|
|
7622
7879
|
const handoff = extractLatestHandoff(comments, {
|
|
7623
|
-
excludeStageId: currentStageId
|
|
7880
|
+
excludeStageId: opts.includeOwnStage ? undefined : currentStageId
|
|
7624
7881
|
});
|
|
7625
7882
|
return handoff ? renderInheritedHandoffSection(handoff) : "";
|
|
7626
7883
|
} catch (err) {
|
|
@@ -7648,7 +7905,9 @@ class Worker {
|
|
|
7648
7905
|
}
|
|
7649
7906
|
async collectStageGateEvidence(card, stage, worktreePath, subtasks) {
|
|
7650
7907
|
try {
|
|
7651
|
-
const
|
|
7908
|
+
const loop = getStageLoop(stage);
|
|
7909
|
+
const gateSource = isConvergeLoop(loop) ? loop?.exit_gate ?? null : stage.gate;
|
|
7910
|
+
const gate = normalizeGateSpec(gateSource);
|
|
7652
7911
|
if (!gate) {
|
|
7653
7912
|
return null;
|
|
7654
7913
|
}
|
|
@@ -7691,11 +7950,8 @@ class Worker {
|
|
|
7691
7950
|
}
|
|
7692
7951
|
}
|
|
7693
7952
|
async advanceFromGateEvaluation(card, stage, stageIndex, def, evaluation) {
|
|
7694
|
-
if (!evaluation) {
|
|
7695
|
-
return { kind: "no_advance" };
|
|
7696
|
-
}
|
|
7697
7953
|
try {
|
|
7698
|
-
return await
|
|
7954
|
+
return await advanceStageRun(card, stage, stageIndex, def, evaluation, {
|
|
7699
7955
|
client: this.client,
|
|
7700
7956
|
stateStore: this.stateStore,
|
|
7701
7957
|
agentId: this.agentId,
|
|
@@ -8671,6 +8927,71 @@ var init_recovery = __esm(() => {
|
|
|
8671
8927
|
init_worktree();
|
|
8672
8928
|
});
|
|
8673
8929
|
|
|
8930
|
+
// src/strand-recovery.ts
|
|
8931
|
+
var exports_strand_recovery = {};
|
|
8932
|
+
__export(exports_strand_recovery, {
|
|
8933
|
+
reclaimPreReviewStrands: () => reclaimPreReviewStrands
|
|
8934
|
+
});
|
|
8935
|
+
async function reclaimPreReviewStrands(opts) {
|
|
8936
|
+
const {
|
|
8937
|
+
client,
|
|
8938
|
+
agentId,
|
|
8939
|
+
cards,
|
|
8940
|
+
columns,
|
|
8941
|
+
labelMap,
|
|
8942
|
+
reviewColumns,
|
|
8943
|
+
approvedLabel,
|
|
8944
|
+
graceMs,
|
|
8945
|
+
knownCardIds,
|
|
8946
|
+
cwd,
|
|
8947
|
+
provider
|
|
8948
|
+
} = opts;
|
|
8949
|
+
const now = opts.now ?? Date.now();
|
|
8950
|
+
const maxPerSweep = opts.maxPerSweep ?? 5;
|
|
8951
|
+
const reviewColIds = new Set(columns.filter((c) => reviewColumns.some((n) => n.toLowerCase() === c.name.toLowerCase())).map((c) => c.id));
|
|
8952
|
+
if (reviewColIds.size === 0)
|
|
8953
|
+
return [];
|
|
8954
|
+
const reclaimed = [];
|
|
8955
|
+
let checked = 0;
|
|
8956
|
+
for (const card of cards) {
|
|
8957
|
+
if (checked >= maxPerSweep)
|
|
8958
|
+
break;
|
|
8959
|
+
if (card.archived_at || !reviewColIds.has(card.column_id) || card.assigned_agent_id != null || card.assignee_id != null || knownCardIds.has(card.id)) {
|
|
8960
|
+
continue;
|
|
8961
|
+
}
|
|
8962
|
+
const branch = extractBranchFromDescription(card.description);
|
|
8963
|
+
if (!branch)
|
|
8964
|
+
continue;
|
|
8965
|
+
const labels = resolveCardLabels(card, labelMap);
|
|
8966
|
+
if (hasLabel(labels, approvedLabel) || hasLabel(labels, NEED_REVIEW_LABEL)) {
|
|
8967
|
+
continue;
|
|
8968
|
+
}
|
|
8969
|
+
const stalledAt = Date.parse(card.updated_at ?? "");
|
|
8970
|
+
if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
|
|
8971
|
+
continue;
|
|
8972
|
+
checked++;
|
|
8973
|
+
const prUrl = resolvePrUrl(card.description ?? null, branch, cwd, provider);
|
|
8974
|
+
if (prUrl)
|
|
8975
|
+
continue;
|
|
8976
|
+
log.warn(TAG33, `#${card.short_id} stranded in review (branch pushed, no PR, unowned) — re-asserting daemon assignment`);
|
|
8977
|
+
try {
|
|
8978
|
+
await client.updateCard(card.id, { assignedAgentId: agentId });
|
|
8979
|
+
reclaimed.push(card.id);
|
|
8980
|
+
} catch (err) {
|
|
8981
|
+
log.error(TAG33, `review re-claim failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
8982
|
+
}
|
|
8983
|
+
}
|
|
8984
|
+
return reclaimed;
|
|
8985
|
+
}
|
|
8986
|
+
var TAG33 = "strand-recovery";
|
|
8987
|
+
var init_strand_recovery = __esm(() => {
|
|
8988
|
+
init_board_helpers();
|
|
8989
|
+
init_git_pr();
|
|
8990
|
+
init_log();
|
|
8991
|
+
init_review_worktree();
|
|
8992
|
+
init_types();
|
|
8993
|
+
});
|
|
8994
|
+
|
|
8674
8995
|
// src/reconcile.ts
|
|
8675
8996
|
class Reconciler {
|
|
8676
8997
|
client;
|
|
@@ -8685,6 +9006,7 @@ class Reconciler {
|
|
|
8685
9006
|
agentConfig;
|
|
8686
9007
|
timer = null;
|
|
8687
9008
|
lastTickAt = null;
|
|
9009
|
+
gitProvider = null;
|
|
8688
9010
|
get lastTick() {
|
|
8689
9011
|
return this.lastTickAt;
|
|
8690
9012
|
}
|
|
@@ -8712,7 +9034,7 @@ class Reconciler {
|
|
|
8712
9034
|
clearInterval(this.timer);
|
|
8713
9035
|
this.timer = null;
|
|
8714
9036
|
}
|
|
8715
|
-
log.info(
|
|
9037
|
+
log.info(TAG34, "Heartbeat stopped");
|
|
8716
9038
|
}
|
|
8717
9039
|
async recoverStaleRuns() {
|
|
8718
9040
|
if (!this.stateStore || !this.agentConfig)
|
|
@@ -8729,7 +9051,7 @@ class Reconciler {
|
|
|
8729
9051
|
if (!daemonDead && !(heartbeatStale && ourZombie))
|
|
8730
9052
|
continue;
|
|
8731
9053
|
const reason = daemonDead ? `foreign daemon ${run.daemonPid} is dead` : `our worker lost card ${run.cardId} with ${Math.round((now - run.lastHeartbeatAt) / 1000)}s stale heartbeat`;
|
|
8732
|
-
log.warn(
|
|
9054
|
+
log.warn(TAG34, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
|
|
8733
9055
|
await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
|
|
8734
9056
|
runId: run.runId,
|
|
8735
9057
|
cardId: run.cardId,
|
|
@@ -8756,14 +9078,38 @@ class Reconciler {
|
|
|
8756
9078
|
const stalledAt = Date.parse(card.updated_at ?? "");
|
|
8757
9079
|
if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
|
|
8758
9080
|
continue;
|
|
8759
|
-
log.warn(
|
|
9081
|
+
log.warn(TAG34, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
|
|
8760
9082
|
try {
|
|
8761
9083
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
8762
9084
|
} catch (err) {
|
|
8763
|
-
log.error(
|
|
9085
|
+
log.error(TAG34, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
8764
9086
|
}
|
|
8765
9087
|
}
|
|
8766
9088
|
}
|
|
9089
|
+
async recoverStrandedReview(cards, columns, labelMap, knownCardIds) {
|
|
9090
|
+
if (this.reviewColumns.length === 0)
|
|
9091
|
+
return;
|
|
9092
|
+
if (!this.gitProvider) {
|
|
9093
|
+
try {
|
|
9094
|
+
this.gitProvider = detectGitProvider();
|
|
9095
|
+
} catch {
|
|
9096
|
+
return;
|
|
9097
|
+
}
|
|
9098
|
+
}
|
|
9099
|
+
await reclaimPreReviewStrands({
|
|
9100
|
+
client: this.client,
|
|
9101
|
+
agentId: this.agentId,
|
|
9102
|
+
cards,
|
|
9103
|
+
columns,
|
|
9104
|
+
labelMap,
|
|
9105
|
+
reviewColumns: this.reviewColumns,
|
|
9106
|
+
approvedLabel: this.approvedLabel,
|
|
9107
|
+
graceMs: this.agentConfig?.timing.staleHeartbeatMs ?? 120000,
|
|
9108
|
+
knownCardIds,
|
|
9109
|
+
cwd: process.cwd(),
|
|
9110
|
+
provider: this.gitProvider
|
|
9111
|
+
});
|
|
9112
|
+
}
|
|
8767
9113
|
async releaseStalledApprovals(cards, columns, knownCardIds) {
|
|
8768
9114
|
const planning = this.agentConfig?.planning;
|
|
8769
9115
|
if (!planning?.enabled || planning.mode !== "gated" || planning.approvalTtlHours <= 0) {
|
|
@@ -8783,11 +9129,11 @@ class Reconciler {
|
|
|
8783
9129
|
const parkedAt = Date.parse(card.updated_at ?? "");
|
|
8784
9130
|
if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
|
|
8785
9131
|
continue;
|
|
8786
|
-
log.warn(
|
|
9132
|
+
log.warn(TAG34, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
|
|
8787
9133
|
try {
|
|
8788
9134
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
8789
9135
|
} catch (err) {
|
|
8790
|
-
log.error(
|
|
9136
|
+
log.error(TAG34, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
8791
9137
|
}
|
|
8792
9138
|
}
|
|
8793
9139
|
}
|
|
@@ -8830,21 +9176,21 @@ class Reconciler {
|
|
|
8830
9176
|
const subtasks = card.subtasks ?? [];
|
|
8831
9177
|
const mode = route.mode;
|
|
8832
9178
|
if (route.stage) {
|
|
8833
|
-
log.info(
|
|
9179
|
+
log.info(TAG34, `Stage card #${card.short_id} (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement) regardless of column`);
|
|
8834
9180
|
}
|
|
8835
9181
|
if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
|
|
8836
|
-
log.debug(
|
|
9182
|
+
log.debug(TAG34, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
|
|
8837
9183
|
continue;
|
|
8838
9184
|
}
|
|
8839
9185
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
8840
|
-
log.debug(
|
|
9186
|
+
log.debug(TAG34, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
|
|
8841
9187
|
continue;
|
|
8842
9188
|
}
|
|
8843
9189
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
8844
|
-
log.debug(
|
|
9190
|
+
log.debug(TAG34, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
|
|
8845
9191
|
continue;
|
|
8846
9192
|
}
|
|
8847
|
-
log.info(
|
|
9193
|
+
log.info(TAG34, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
|
|
8848
9194
|
await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
8849
9195
|
}
|
|
8850
9196
|
}
|
|
@@ -8852,25 +9198,28 @@ class Reconciler {
|
|
|
8852
9198
|
await this.recoverStaleRuns();
|
|
8853
9199
|
}
|
|
8854
9200
|
await this.recoverStrandedInProgress(cards, columns, knownCardIds);
|
|
9201
|
+
await this.recoverStrandedReview(cards, columns, labelMap, knownCardIds);
|
|
8855
9202
|
for (const knownId of knownCardIds) {
|
|
8856
9203
|
if (!allAgentCardIds.has(knownId)) {
|
|
8857
|
-
log.info(
|
|
9204
|
+
log.info(TAG34, `Missed unassign: ${knownId} — removing`);
|
|
8858
9205
|
await this.pool.removeCard(knownId);
|
|
8859
9206
|
}
|
|
8860
9207
|
}
|
|
8861
9208
|
await this.releaseStalledApprovals(cards, columns, knownCardIds);
|
|
8862
|
-
log.debug(
|
|
9209
|
+
log.debug(TAG34, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
|
|
8863
9210
|
} catch (err) {
|
|
8864
|
-
log.error(
|
|
9211
|
+
log.error(TAG34, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
|
|
8865
9212
|
}
|
|
8866
9213
|
}
|
|
8867
9214
|
}
|
|
8868
|
-
var
|
|
9215
|
+
var TAG34 = "reconcile";
|
|
8869
9216
|
var init_reconcile = __esm(() => {
|
|
8870
9217
|
init_board_helpers();
|
|
9218
|
+
init_git_pr();
|
|
8871
9219
|
init_log();
|
|
8872
9220
|
init_recovery();
|
|
8873
9221
|
init_review_worktree();
|
|
9222
|
+
init_strand_recovery();
|
|
8874
9223
|
init_types();
|
|
8875
9224
|
});
|
|
8876
9225
|
|
|
@@ -8903,7 +9252,7 @@ function prettyBanner(config, version) {
|
|
|
8903
9252
|
checks.push({ kind: "ok", message });
|
|
8904
9253
|
},
|
|
8905
9254
|
warn(message) {
|
|
8906
|
-
log.warn(
|
|
9255
|
+
log.warn(TAG35, message);
|
|
8907
9256
|
checks.push({ kind: "warn", message: message.split(`
|
|
8908
9257
|
`, 1)[0] });
|
|
8909
9258
|
},
|
|
@@ -8928,25 +9277,25 @@ function prettyBanner(config, version) {
|
|
|
8928
9277
|
};
|
|
8929
9278
|
}
|
|
8930
9279
|
function jsonBanner(config, version) {
|
|
8931
|
-
log.info(
|
|
8932
|
-
log.info(
|
|
9280
|
+
log.info(TAG35, `Harmony Agent Daemon v${version} starting...`);
|
|
9281
|
+
log.info(TAG35, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
|
|
8933
9282
|
if (config.agent.review.enabled) {
|
|
8934
|
-
log.info(
|
|
9283
|
+
log.info(TAG35, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
|
|
8935
9284
|
}
|
|
8936
9285
|
let failed = false;
|
|
8937
9286
|
return {
|
|
8938
9287
|
setProjectName(_name) {},
|
|
8939
9288
|
setGitProvider(provider) {
|
|
8940
|
-
log.info(
|
|
9289
|
+
log.info(TAG35, `Git provider: ${provider}`);
|
|
8941
9290
|
},
|
|
8942
9291
|
setHttpPort(port) {
|
|
8943
|
-
log.info(
|
|
9292
|
+
log.info(TAG35, `HTTP server on port ${port}`);
|
|
8944
9293
|
},
|
|
8945
9294
|
check(message) {
|
|
8946
|
-
log.info(
|
|
9295
|
+
log.info(TAG35, message);
|
|
8947
9296
|
},
|
|
8948
9297
|
warn(message) {
|
|
8949
|
-
log.warn(
|
|
9298
|
+
log.warn(TAG35, message);
|
|
8950
9299
|
},
|
|
8951
9300
|
fail() {
|
|
8952
9301
|
failed = true;
|
|
@@ -8954,7 +9303,7 @@ function jsonBanner(config, version) {
|
|
|
8954
9303
|
async ready(message) {
|
|
8955
9304
|
if (failed)
|
|
8956
9305
|
return;
|
|
8957
|
-
log.info(
|
|
9306
|
+
log.info(TAG35, message);
|
|
8958
9307
|
}
|
|
8959
9308
|
};
|
|
8960
9309
|
}
|
|
@@ -9035,7 +9384,7 @@ function cyan(s) {
|
|
|
9035
9384
|
function yellow(s) {
|
|
9036
9385
|
return `${ANSI.yellow}${s}${ANSI.reset}`;
|
|
9037
9386
|
}
|
|
9038
|
-
var
|
|
9387
|
+
var TAG35 = "daemon", RULE_WIDTH = 70, ANSI;
|
|
9039
9388
|
var init_startup_banner = __esm(() => {
|
|
9040
9389
|
init_log();
|
|
9041
9390
|
ANSI = {
|
|
@@ -9186,13 +9535,13 @@ class Watcher {
|
|
|
9186
9535
|
}
|
|
9187
9536
|
async start() {
|
|
9188
9537
|
if (!isPretty()) {
|
|
9189
|
-
log.info(
|
|
9538
|
+
log.info(TAG36, "Connecting to Supabase realtime (broadcast)...");
|
|
9190
9539
|
}
|
|
9191
9540
|
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
9192
9541
|
const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
|
|
9193
9542
|
this.subscribeBroadcast();
|
|
9194
9543
|
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
9195
|
-
log.debug(
|
|
9544
|
+
log.debug(TAG36, "Presence sync");
|
|
9196
9545
|
}).subscribe(async (status) => {
|
|
9197
9546
|
if (status === "SUBSCRIBED") {
|
|
9198
9547
|
await presenceChannel.track({
|
|
@@ -9205,7 +9554,7 @@ class Watcher {
|
|
|
9205
9554
|
agentName: this.identity.agentName
|
|
9206
9555
|
});
|
|
9207
9556
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
9208
|
-
log.info(
|
|
9557
|
+
log.info(TAG36, "Presence tracked on board-presence channel");
|
|
9209
9558
|
}
|
|
9210
9559
|
this.presenceTracked = true;
|
|
9211
9560
|
this.maybeResolveReady();
|
|
@@ -9218,13 +9567,13 @@ class Watcher {
|
|
|
9218
9567
|
return;
|
|
9219
9568
|
const gen = ++this.broadcastGen;
|
|
9220
9569
|
this.channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
|
|
9221
|
-
log.debug(
|
|
9570
|
+
log.debug(TAG36, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
|
|
9222
9571
|
this.onCardBroadcast({
|
|
9223
9572
|
event: "card_update",
|
|
9224
9573
|
payload: msg.payload ?? {}
|
|
9225
9574
|
});
|
|
9226
9575
|
}).on("broadcast", { event: "card_created" }, (msg) => {
|
|
9227
|
-
log.debug(
|
|
9576
|
+
log.debug(TAG36, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
|
|
9228
9577
|
this.onCardBroadcast({
|
|
9229
9578
|
event: "card_created",
|
|
9230
9579
|
payload: msg.payload ?? {}
|
|
@@ -9234,7 +9583,7 @@ class Watcher {
|
|
|
9234
9583
|
const cardId = payload.card_id;
|
|
9235
9584
|
const command = payload.command;
|
|
9236
9585
|
if (cardId && command) {
|
|
9237
|
-
log.info(
|
|
9586
|
+
log.info(TAG36, `Broadcast: agent_command ${command} for ${cardId}`);
|
|
9238
9587
|
this.onAgentCommand?.({ cardId, command });
|
|
9239
9588
|
}
|
|
9240
9589
|
}).subscribe((status) => {
|
|
@@ -9244,13 +9593,13 @@ class Watcher {
|
|
|
9244
9593
|
this.connected = true;
|
|
9245
9594
|
this.reconnectAttempts = 0;
|
|
9246
9595
|
if (!isPretty() || !this.suppressStartupLogs) {
|
|
9247
|
-
log.info(
|
|
9596
|
+
log.info(TAG36, "Broadcast subscription active");
|
|
9248
9597
|
}
|
|
9249
9598
|
this.maybeResolveReady();
|
|
9250
9599
|
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
|
|
9251
9600
|
this.connected = false;
|
|
9252
9601
|
if (!this.stopping) {
|
|
9253
|
-
log.warn(
|
|
9602
|
+
log.warn(TAG36, `Broadcast subscription ${status} — scheduling reconnect`);
|
|
9254
9603
|
this.scheduleReconnect();
|
|
9255
9604
|
}
|
|
9256
9605
|
}
|
|
@@ -9269,7 +9618,7 @@ class Watcher {
|
|
|
9269
9618
|
async reconnectBroadcast() {
|
|
9270
9619
|
if (this.stopping || !this.supabase)
|
|
9271
9620
|
return;
|
|
9272
|
-
log.warn(
|
|
9621
|
+
log.warn(TAG36, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
|
|
9273
9622
|
if (this.channel) {
|
|
9274
9623
|
const old = this.channel;
|
|
9275
9624
|
this.channel = null;
|
|
@@ -9299,10 +9648,10 @@ class Watcher {
|
|
|
9299
9648
|
this.supabase = null;
|
|
9300
9649
|
}
|
|
9301
9650
|
this.connected = false;
|
|
9302
|
-
log.info(
|
|
9651
|
+
log.info(TAG36, "Broadcast subscription stopped");
|
|
9303
9652
|
}
|
|
9304
9653
|
}
|
|
9305
|
-
var
|
|
9654
|
+
var TAG36 = "watcher";
|
|
9306
9655
|
var init_watcher = __esm(() => {
|
|
9307
9656
|
init_log();
|
|
9308
9657
|
});
|
|
@@ -9389,10 +9738,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
|
|
|
9389
9738
|
});
|
|
9390
9739
|
} catch {}
|
|
9391
9740
|
if (result.removed.length > 0) {
|
|
9392
|
-
log.info(
|
|
9741
|
+
log.info(TAG37, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
|
|
9393
9742
|
}
|
|
9394
9743
|
if (result.errors.length > 0) {
|
|
9395
|
-
log.warn(
|
|
9744
|
+
log.warn(TAG37, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
|
|
9396
9745
|
}
|
|
9397
9746
|
return result;
|
|
9398
9747
|
}
|
|
@@ -9422,7 +9771,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
9422
9771
|
} catch (err) {
|
|
9423
9772
|
const detail = gitErrorDetail2(err);
|
|
9424
9773
|
if (isTransientGitNetworkError(detail)) {
|
|
9425
|
-
log.debug(
|
|
9774
|
+
log.debug(TAG37, `Remote branch GC skipped — remote unreachable: ${detail}`);
|
|
9426
9775
|
return result;
|
|
9427
9776
|
}
|
|
9428
9777
|
result.errors.push({ ref: "fetch", error: detail });
|
|
@@ -9461,7 +9810,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
9461
9810
|
continue;
|
|
9462
9811
|
}
|
|
9463
9812
|
if (clock() > sweepDeadline) {
|
|
9464
|
-
log.debug(
|
|
9813
|
+
log.debug(TAG37, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
|
|
9465
9814
|
break;
|
|
9466
9815
|
}
|
|
9467
9816
|
try {
|
|
@@ -9474,17 +9823,17 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
9474
9823
|
} catch (err) {
|
|
9475
9824
|
const detail = gitErrorDetail2(err);
|
|
9476
9825
|
if (isTransientGitNetworkError(detail)) {
|
|
9477
|
-
log.debug(
|
|
9826
|
+
log.debug(TAG37, `Remote branch GC interrupted — remote unreachable: ${detail}`);
|
|
9478
9827
|
break;
|
|
9479
9828
|
}
|
|
9480
9829
|
result.errors.push({ ref, error: detail });
|
|
9481
9830
|
}
|
|
9482
9831
|
}
|
|
9483
9832
|
if (result.removed.length > 0) {
|
|
9484
|
-
log.info(
|
|
9833
|
+
log.info(TAG37, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
|
|
9485
9834
|
}
|
|
9486
9835
|
if (result.errors.length > 0) {
|
|
9487
|
-
log.warn(
|
|
9836
|
+
log.warn(TAG37, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
|
|
9488
9837
|
}
|
|
9489
9838
|
return result;
|
|
9490
9839
|
}
|
|
@@ -9515,13 +9864,13 @@ class WorktreeGc {
|
|
|
9515
9864
|
try {
|
|
9516
9865
|
runWorktreeGc(this.basePath, this.store);
|
|
9517
9866
|
} catch (err) {
|
|
9518
|
-
log.warn(
|
|
9867
|
+
log.warn(TAG37, `GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
9519
9868
|
}
|
|
9520
9869
|
if (this.remoteOpts) {
|
|
9521
9870
|
try {
|
|
9522
9871
|
pruneFailedRemoteBranches(this.remoteOpts);
|
|
9523
9872
|
} catch (err) {
|
|
9524
|
-
log.warn(
|
|
9873
|
+
log.warn(TAG37, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
9525
9874
|
}
|
|
9526
9875
|
}
|
|
9527
9876
|
}
|
|
@@ -9535,7 +9884,7 @@ function getRepoRoot2() {
|
|
|
9535
9884
|
return null;
|
|
9536
9885
|
}
|
|
9537
9886
|
}
|
|
9538
|
-
var
|
|
9887
|
+
var TAG37 = "worktree-gc", GIT_NETWORK_TIMEOUT_MS = 30000, GIT_SSH_CONNECT_TIMEOUT_SECS = 10, GIT_PRUNE_SWEEP_BUDGET_MS = 60000, GIT_NETWORK_EXEC, TRANSIENT_GIT_NETWORK_ERROR;
|
|
9539
9888
|
var init_worktree_gc = __esm(() => {
|
|
9540
9889
|
init_log();
|
|
9541
9890
|
init_worktree();
|
|
@@ -9639,7 +9988,7 @@ async function main() {
|
|
|
9639
9988
|
} catch (err) {
|
|
9640
9989
|
if (err instanceof ConfigValidationError) {
|
|
9641
9990
|
banner.fail();
|
|
9642
|
-
log.error(
|
|
9991
|
+
log.error(TAG38, err.message);
|
|
9643
9992
|
process.exit(1);
|
|
9644
9993
|
}
|
|
9645
9994
|
throw err;
|
|
@@ -9749,7 +10098,7 @@ async function main() {
|
|
|
9749
10098
|
if (shuttingDown)
|
|
9750
10099
|
return;
|
|
9751
10100
|
shuttingDown = true;
|
|
9752
|
-
log.info(
|
|
10101
|
+
log.info(TAG38, `Received ${signal}, shutting down gracefully...`);
|
|
9753
10102
|
reconciler.stop();
|
|
9754
10103
|
mergeMonitor?.stop();
|
|
9755
10104
|
worktreeGc.stop();
|
|
@@ -9759,18 +10108,18 @@ async function main() {
|
|
|
9759
10108
|
}
|
|
9760
10109
|
await watcher.stop();
|
|
9761
10110
|
await pool.shutdown();
|
|
9762
|
-
log.info(
|
|
10111
|
+
log.info(TAG38, "Daemon stopped.");
|
|
9763
10112
|
process.exit(exitCode);
|
|
9764
10113
|
};
|
|
9765
10114
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
9766
10115
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
9767
10116
|
process.on("uncaughtException", (err) => {
|
|
9768
|
-
log.error(
|
|
10117
|
+
log.error(TAG38, `Uncaught exception: ${err.message}`);
|
|
9769
10118
|
exitCode = 1;
|
|
9770
10119
|
shutdown("uncaughtException");
|
|
9771
10120
|
});
|
|
9772
10121
|
process.on("unhandledRejection", (reason) => {
|
|
9773
|
-
log.error(
|
|
10122
|
+
log.error(TAG38, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
9774
10123
|
exitCode = 1;
|
|
9775
10124
|
shutdown("unhandledRejection");
|
|
9776
10125
|
});
|
|
@@ -9823,29 +10172,29 @@ async function handleBroadcast(event, client, pool, config, agentId) {
|
|
|
9823
10172
|
if (assignedAgentId === undefined)
|
|
9824
10173
|
return;
|
|
9825
10174
|
if (assignedAgentId === agentId) {
|
|
9826
|
-
log.info(
|
|
10175
|
+
log.info(TAG38, `Broadcast: card ${cardId} assigned to agent`);
|
|
9827
10176
|
try {
|
|
9828
10177
|
await pool.resetAttemptsForReassign(cardId);
|
|
9829
10178
|
await tryEnqueueCard(cardId, client, pool, config, agentId);
|
|
9830
10179
|
} catch (err) {
|
|
9831
|
-
log.error(
|
|
10180
|
+
log.error(TAG38, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
|
|
9832
10181
|
}
|
|
9833
10182
|
} else if (pool.isCardKnown(cardId)) {
|
|
9834
|
-
log.info(
|
|
10183
|
+
log.info(TAG38, `Broadcast: card ${cardId} unassigned from agent`);
|
|
9835
10184
|
await pool.removeCard(cardId);
|
|
9836
10185
|
}
|
|
9837
10186
|
}
|
|
9838
10187
|
async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
9839
10188
|
const { card } = await client.getCard(cardId);
|
|
9840
10189
|
if (card.assigned_agent_id !== agentId) {
|
|
9841
|
-
log.debug(
|
|
10190
|
+
log.debug(TAG38, `Card ${cardId} no longer assigned to agent — skipping`);
|
|
9842
10191
|
return;
|
|
9843
10192
|
}
|
|
9844
10193
|
const board = await client.getBoard(config.projectId, { summary: true });
|
|
9845
10194
|
const columns = board.columns;
|
|
9846
10195
|
const column = columns.find((c) => c.id === card.column_id);
|
|
9847
10196
|
if (!column) {
|
|
9848
|
-
log.warn(
|
|
10197
|
+
log.warn(TAG38, `Column not found for card ${cardId}`);
|
|
9849
10198
|
return;
|
|
9850
10199
|
}
|
|
9851
10200
|
const route = classifyPickup(card, column.name, {
|
|
@@ -9854,27 +10203,27 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
9854
10203
|
playbooks: config.agent.playbooks
|
|
9855
10204
|
});
|
|
9856
10205
|
if (!route) {
|
|
9857
|
-
log.info(
|
|
10206
|
+
log.info(TAG38, `Card #${card.short_id} is in "${column.name}", not a pickup/review/stage column — skipping`);
|
|
9858
10207
|
return;
|
|
9859
10208
|
}
|
|
9860
10209
|
if (route.stage) {
|
|
9861
|
-
log.info(
|
|
10210
|
+
log.info(TAG38, `Card #${card.short_id} is a playbook stage card (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement pool) regardless of column`);
|
|
9862
10211
|
}
|
|
9863
10212
|
const mode = route.mode;
|
|
9864
10213
|
const labelMap = buildLabelMap(board.labels ?? []);
|
|
9865
10214
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
9866
10215
|
const subtasks = card.subtasks ?? [];
|
|
9867
10216
|
if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
|
|
9868
|
-
log.debug(
|
|
10217
|
+
log.debug(TAG38, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
|
|
9869
10218
|
return;
|
|
9870
10219
|
}
|
|
9871
10220
|
if (mode === "review" && !extractBranchFromDescription(card.description)) {
|
|
9872
|
-
log.info(
|
|
10221
|
+
log.info(TAG38, `Card #${card.short_id} has no branch reference — skipping auto-review`);
|
|
9873
10222
|
return;
|
|
9874
10223
|
}
|
|
9875
10224
|
await pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
9876
10225
|
}
|
|
9877
|
-
var
|
|
10226
|
+
var TAG38 = "daemon", PKG_VERSION;
|
|
9878
10227
|
var init_src = __esm(() => {
|
|
9879
10228
|
init_board_helpers();
|
|
9880
10229
|
init_config();
|