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