@gethmy/agent 1.15.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +95 -17
- package/dist/index.js +95 -17
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1666,6 +1666,9 @@ function getStageLoop(stage) {
|
|
|
1666
1666
|
function isConvergeLoop(loop) {
|
|
1667
1667
|
return loop !== null && loop.mode === "converge";
|
|
1668
1668
|
}
|
|
1669
|
+
function resolveLoopExitGate(stage, loop) {
|
|
1670
|
+
return loop.exit_gate ?? stage.gate ?? null;
|
|
1671
|
+
}
|
|
1669
1672
|
function decideLoopContinuation(args) {
|
|
1670
1673
|
const { loop, gatePassed, hasExitGate, completedIterations } = args;
|
|
1671
1674
|
const max = Math.max(1, Math.floor(loop.max_iterations) || 1);
|
|
@@ -1714,7 +1717,10 @@ function entryActionAllowlist(entryAction) {
|
|
|
1714
1717
|
return `mcp__${entryAction}`;
|
|
1715
1718
|
return null;
|
|
1716
1719
|
}
|
|
1717
|
-
|
|
1720
|
+
function stageDisallowedTools() {
|
|
1721
|
+
return STAGE_DAEMON_OWNED_TOOLS.length > 0 ? STAGE_DAEMON_OWNED_TOOLS.join(",") : null;
|
|
1722
|
+
}
|
|
1723
|
+
var DEFAULT_LOOP_MAX_ITERATIONS = 5, SKILL_TOOL_ALLOWLIST, HARMONY_TOOL_RE, STAGE_DAEMON_OWNED_TOOLS;
|
|
1718
1724
|
var init_playbookStage = __esm(() => {
|
|
1719
1725
|
SKILL_TOOL_ALLOWLIST = {
|
|
1720
1726
|
hmy: "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*",
|
|
@@ -1725,6 +1731,11 @@ var init_playbookStage = __esm(() => {
|
|
|
1725
1731
|
"hmy-standup": "Read,Grep,Glob,mcp__harmony__*"
|
|
1726
1732
|
};
|
|
1727
1733
|
HARMONY_TOOL_RE = /^harmony_[a-z_]+$/;
|
|
1734
|
+
STAGE_DAEMON_OWNED_TOOLS = [
|
|
1735
|
+
"mcp__harmony__harmony_end_agent_session",
|
|
1736
|
+
"mcp__harmony__harmony_start_agent_session",
|
|
1737
|
+
"mcp__harmony__harmony_move_card"
|
|
1738
|
+
];
|
|
1728
1739
|
});
|
|
1729
1740
|
|
|
1730
1741
|
// ../harmony-shared/dist/projectTemplates.js
|
|
@@ -2159,6 +2170,58 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
2159
2170
|
} catch {}
|
|
2160
2171
|
}
|
|
2161
2172
|
}
|
|
2173
|
+
function resolveRepoRoot() {
|
|
2174
|
+
return execFileSync3("git", ["rev-parse", "--show-toplevel"], {
|
|
2175
|
+
encoding: "utf-8"
|
|
2176
|
+
}).trim();
|
|
2177
|
+
}
|
|
2178
|
+
function branchAheadOfItsRemote(branchName, repoRoot = resolveRepoRoot()) {
|
|
2179
|
+
try {
|
|
2180
|
+
const out = execFileSync3("git", ["rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8" }).trim();
|
|
2181
|
+
return out.length > 0;
|
|
2182
|
+
} catch {
|
|
2183
|
+
return false;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
async function rescueUnpushedBranch(client, cardId, branchName, repoRoot = resolveRepoRoot()) {
|
|
2187
|
+
const { getBranchWebUrl: getBranchWebUrl2, pushBranch: pushBranch2 } = await Promise.resolve().then(() => (init_git_pr(), exports_git_pr));
|
|
2188
|
+
try {
|
|
2189
|
+
pushBranch2(branchName, repoRoot);
|
|
2190
|
+
} catch (err) {
|
|
2191
|
+
log.error(TAG5, `push-rescue failed for ${branchName} — leaving local branch ref intact (recoverable via git reflog / the local branch): ${err instanceof Error ? err.message : err}`);
|
|
2192
|
+
return false;
|
|
2193
|
+
}
|
|
2194
|
+
log.warn(TAG5, `push-rescued unpushed branch ${branchName} to origin before teardown`);
|
|
2195
|
+
try {
|
|
2196
|
+
const url = getBranchWebUrl2(branchName, repoRoot);
|
|
2197
|
+
const recover = url ? `View it at ${url} or recover locally: \`git fetch && git checkout ${branchName}\`` : `Recover it locally: \`git fetch && git checkout ${branchName}\``;
|
|
2198
|
+
const body = `⚠ Run ended before completion. Committed work was push-rescued to ` + `\`origin/${branchName}\` so it isn't lost. ${recover}`;
|
|
2199
|
+
await client.addComment(cardId, body, { commentType: "message" });
|
|
2200
|
+
} catch (err) {
|
|
2201
|
+
log.warn(TAG5, `push-rescue comment failed for ${branchName} (work is still safe on origin): ${err instanceof Error ? err.message : err}`);
|
|
2202
|
+
}
|
|
2203
|
+
return true;
|
|
2204
|
+
}
|
|
2205
|
+
async function teardownWorktree(client, cardId, worktreePath, branchName) {
|
|
2206
|
+
let skipBranchDelete = false;
|
|
2207
|
+
if (branchName && cardId) {
|
|
2208
|
+
let repoRoot;
|
|
2209
|
+
try {
|
|
2210
|
+
repoRoot = resolveRepoRoot();
|
|
2211
|
+
} catch {
|
|
2212
|
+
cleanupWorktree(worktreePath, branchName);
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
if (branchAheadOfItsRemote(branchName, repoRoot)) {
|
|
2216
|
+
const ok = await rescueUnpushedBranch(client, cardId, branchName, repoRoot);
|
|
2217
|
+
if (!ok) {
|
|
2218
|
+
skipBranchDelete = true;
|
|
2219
|
+
log.error(TAG5, `Keeping local branch ${branchName} (push-rescue failed) to avoid orphaning its commit`);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
cleanupWorktree(worktreePath, skipBranchDelete ? undefined : branchName);
|
|
2224
|
+
}
|
|
2162
2225
|
function makeBranchName(shortId, title, prefix = "agent-attempts/") {
|
|
2163
2226
|
const slug = title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
2164
2227
|
return `${prefix}${shortId}-${slug || "task"}`;
|
|
@@ -3509,7 +3572,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3509
3572
|
failureSummary,
|
|
3510
3573
|
...buildTokenPayload(sessionStats)
|
|
3511
3574
|
});
|
|
3512
|
-
|
|
3575
|
+
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
3513
3576
|
return false;
|
|
3514
3577
|
}
|
|
3515
3578
|
log.info(TAG14, `Pushing branch ${branchName} (pre-verify)...`);
|
|
@@ -3591,7 +3654,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3591
3654
|
recoveryBranch: branchName,
|
|
3592
3655
|
...buildTokenPayload(sessionStats)
|
|
3593
3656
|
});
|
|
3594
|
-
|
|
3657
|
+
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
3595
3658
|
return false;
|
|
3596
3659
|
}
|
|
3597
3660
|
log.info(TAG14, `Verification passed for #${card.short_id}`);
|
|
@@ -3647,7 +3710,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3647
3710
|
log.warn(TAG14, `onBeforeWorktreeCleanup hook failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
3648
3711
|
}
|
|
3649
3712
|
}
|
|
3650
|
-
|
|
3713
|
+
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
3651
3714
|
log.info(TAG14, `Completion done for #${card.short_id}${prUrl ? ` — PR: ${prUrl}` : ""}`);
|
|
3652
3715
|
return true;
|
|
3653
3716
|
}
|
|
@@ -3967,6 +4030,7 @@ class SdkAgentRunner {
|
|
|
3967
4030
|
cwd: input.cwd,
|
|
3968
4031
|
model: input.model ?? this.cfg.model,
|
|
3969
4032
|
allowedTools: allowed,
|
|
4033
|
+
...this.cfg.disallowedTools && this.cfg.disallowedTools.length > 0 ? { disallowedTools: this.cfg.disallowedTools } : {},
|
|
3970
4034
|
tools: builtinTools,
|
|
3971
4035
|
permissionMode: "dontAsk",
|
|
3972
4036
|
maxTurns: this.cfg.maxTurns,
|
|
@@ -6918,7 +6982,7 @@ async function buildPrompt(enriched, branchName, worktreePath, client, workspace
|
|
|
6918
6982
|
variant: "execute",
|
|
6919
6983
|
customConstraints: `You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
|
|
6920
6984
|
Do NOT push to main. All your work stays on \`${branchName}\`.
|
|
6921
|
-
|
|
6985
|
+
The daemon owns the run lifecycle: once your work is committed it ends the agent session, pushes the branch, and moves the card to Review for you. Do NOT call harmony_end_agent_session, do NOT start a new session, and do NOT move the card or change its column yourself. If the skill driving this work tells you to move the card or end the session as a final step, SKIP it — it is handled for you (those tools are disabled for this run). Finish the implementation, commit, and stop.`
|
|
6922
6986
|
});
|
|
6923
6987
|
log.info(TAG27, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
|
|
6924
6988
|
return result.prompt + pastEpisodesSection;
|
|
@@ -7028,7 +7092,7 @@ ${subtaskStr}
|
|
|
7028
7092
|
Include a brief currentTask description.
|
|
7029
7093
|
3. Implement the changes on branch \`${branchName}\`
|
|
7030
7094
|
4. Commit your work with clear, descriptive commit messages
|
|
7031
|
-
5. When
|
|
7095
|
+
5. When the work is committed, STOP. The daemon owns the run lifecycle: it ends the agent session, pushes the branch, and moves the card to Review for you. Do NOT call harmony_end_agent_session, do NOT start a new session, and do NOT move the card or change its column yourself — those tools are disabled for this run.
|
|
7032
7096
|
|
|
7033
7097
|
You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
|
|
7034
7098
|
Do NOT push to main. All your work stays on \`${branchName}\`.`;
|
|
@@ -7077,7 +7141,7 @@ function firstErrorMessage(evaluation) {
|
|
|
7077
7141
|
return e ? e.message : null;
|
|
7078
7142
|
}
|
|
7079
7143
|
async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loop, deps) {
|
|
7080
|
-
const hasExitGate = loop
|
|
7144
|
+
const hasExitGate = resolveLoopExitGate(stage, loop) != null;
|
|
7081
7145
|
const gatePassed = hasExitGate ? evaluation?.passed ?? false : false;
|
|
7082
7146
|
const gateResult = !hasExitGate ? "skipped" : gatePassed ? "passed" : "failed";
|
|
7083
7147
|
const maxIterations = Math.max(1, Math.floor(loop.max_iterations) || 1);
|
|
@@ -7330,7 +7394,7 @@ function buildStagePreamble(stage) {
|
|
|
7330
7394
|
lines.push(`Hand off when done: ${summary.trim()}`);
|
|
7331
7395
|
}
|
|
7332
7396
|
}
|
|
7333
|
-
lines.push("Do only this stage's work.
|
|
7397
|
+
lines.push("Do only this stage's work, then stop. The daemon owns the run lifecycle here: it ends the agent session and performs every card move and stage advancement automatically once your stage work is done. Do NOT call `harmony_end_agent_session`, do NOT start a new session, and do NOT move the card or change its column yourself. If the skill driving this stage tells you to move the card (e.g. to Review) or end your session as a final step, SKIP that step — it is handled for you. Those tools are disabled for this run, so attempting them only wastes turns. Finish the stage's work and stop.");
|
|
7334
7398
|
if (normalizeGateSpec(stage.gate)?.kind === "review_passed") {
|
|
7335
7399
|
lines.push("", "**This stage's gate is `review_passed` — your deliverable is a review verdict, not a prose handoff.** Do NOT end the session until you have actually reviewed the change and emitted EXACTLY one JSON block as the LAST thing you output (nothing after it):", "```json", REVIEW_VERDICT_SCHEMA, "```", "Decision rules:", REVIEW_DECISION_RULES, "The board reads this verdict to decide advancement: `approved` advances the card, `rejected` sends it back. A missing or unparseable verdict blocks the card. Do NOT modify any code — this is a read-only review.");
|
|
7336
7400
|
}
|
|
@@ -7343,6 +7407,13 @@ function buildSteeringPrompt(messages) {
|
|
|
7343
7407
|
return messages.map((m, i) => `${i + 1}. ${m}`).join(`
|
|
7344
7408
|
`);
|
|
7345
7409
|
}
|
|
7410
|
+
function computeRunSpawnGating(stageAllowedTools) {
|
|
7411
|
+
const denylist = stageDisallowedTools();
|
|
7412
|
+
return {
|
|
7413
|
+
...stageAllowedTools ? { allowedTools: stageAllowedTools } : {},
|
|
7414
|
+
...denylist ? { disallowedTools: denylist } : {}
|
|
7415
|
+
};
|
|
7416
|
+
}
|
|
7346
7417
|
|
|
7347
7418
|
class Worker {
|
|
7348
7419
|
config;
|
|
@@ -7371,6 +7442,7 @@ class Worker {
|
|
|
7371
7442
|
timedOut = false;
|
|
7372
7443
|
verificationFailed = false;
|
|
7373
7444
|
held = false;
|
|
7445
|
+
activeRunSpawnOpts = null;
|
|
7374
7446
|
completionStarted = false;
|
|
7375
7447
|
sessionId = null;
|
|
7376
7448
|
runId = null;
|
|
@@ -7450,6 +7522,7 @@ class Worker {
|
|
|
7450
7522
|
this.lastRunText = "";
|
|
7451
7523
|
this.cliSessionId = null;
|
|
7452
7524
|
this.lastDrainedSeq = 0;
|
|
7525
|
+
this.activeRunSpawnOpts = null;
|
|
7453
7526
|
this.cardId = card.id;
|
|
7454
7527
|
this.startedAt = Date.now();
|
|
7455
7528
|
this.runId = newRunId();
|
|
@@ -7570,9 +7643,10 @@ class Worker {
|
|
|
7570
7643
|
this.timedOut = true;
|
|
7571
7644
|
this.cancel();
|
|
7572
7645
|
}, this.config.maxTimeout);
|
|
7646
|
+
this.activeRunSpawnOpts = computeRunSpawnGating(stageCtx.kind === "run" ? stageCtx.allowedTools : null);
|
|
7573
7647
|
await this.spawnClaude(prompt, card, subtasks, {
|
|
7574
7648
|
model: this.selectImplementModel(card),
|
|
7575
|
-
...
|
|
7649
|
+
...this.activeRunSpawnOpts ?? {}
|
|
7576
7650
|
});
|
|
7577
7651
|
if (this.aborted)
|
|
7578
7652
|
return;
|
|
@@ -7647,7 +7721,7 @@ class Worker {
|
|
|
7647
7721
|
}
|
|
7648
7722
|
if (this.worktreePath) {
|
|
7649
7723
|
try {
|
|
7650
|
-
|
|
7724
|
+
await teardownWorktree(this.client, card.id, this.worktreePath, this.branchName ?? undefined);
|
|
7651
7725
|
} catch {
|
|
7652
7726
|
log.warn(this.tag, "Failed to cleanup worktree before requeue");
|
|
7653
7727
|
}
|
|
@@ -7691,7 +7765,7 @@ class Worker {
|
|
|
7691
7765
|
} else if (this.runId && this.timedOut) {
|
|
7692
7766
|
if (this.worktreePath) {
|
|
7693
7767
|
try {
|
|
7694
|
-
|
|
7768
|
+
await teardownWorktree(this.client, card.id, this.worktreePath, this.branchName ?? undefined);
|
|
7695
7769
|
} catch {
|
|
7696
7770
|
log.warn(this.tag, "Failed to cleanup worktree before requeue");
|
|
7697
7771
|
}
|
|
@@ -7781,7 +7855,7 @@ class Worker {
|
|
|
7781
7855
|
await this.cliRunner.flushFinal();
|
|
7782
7856
|
} catch {}
|
|
7783
7857
|
}
|
|
7784
|
-
this.cleanup();
|
|
7858
|
+
await this.cleanup();
|
|
7785
7859
|
this.state = "idle";
|
|
7786
7860
|
this.onDone(this);
|
|
7787
7861
|
}
|
|
@@ -7907,7 +7981,7 @@ class Worker {
|
|
|
7907
7981
|
async collectStageGateEvidence(card, stage, worktreePath, subtasks) {
|
|
7908
7982
|
try {
|
|
7909
7983
|
const loop = getStageLoop(stage);
|
|
7910
|
-
const gateSource = isConvergeLoop(loop)
|
|
7984
|
+
const gateSource = isConvergeLoop(loop) && loop ? resolveLoopExitGate(stage, loop) : stage.gate;
|
|
7911
7985
|
const gate = normalizeGateSpec(gateSource);
|
|
7912
7986
|
if (!gate) {
|
|
7913
7987
|
return null;
|
|
@@ -8205,7 +8279,8 @@ class Worker {
|
|
|
8205
8279
|
await this.spawnClaude(buildSteeringPrompt(messages.map((m) => m.text)), card, subtasks, {
|
|
8206
8280
|
model: this.selectImplementModel(card),
|
|
8207
8281
|
maxTurns: STEERING_MAX_TURNS,
|
|
8208
|
-
resumeSessionId: this.cliSessionId
|
|
8282
|
+
resumeSessionId: this.cliSessionId,
|
|
8283
|
+
...this.activeRunSpawnOpts ?? {}
|
|
8209
8284
|
});
|
|
8210
8285
|
} catch (err) {
|
|
8211
8286
|
log.warn(this.tag, `Steering resume failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
@@ -8233,6 +8308,7 @@ class Worker {
|
|
|
8233
8308
|
String(maxTurns),
|
|
8234
8309
|
"--allowedTools",
|
|
8235
8310
|
allowedTools,
|
|
8311
|
+
...opts.disallowedTools ? ["--disallowedTools", opts.disallowedTools] : [],
|
|
8236
8312
|
...opts.resumeSessionId ? ["--resume", opts.resumeSessionId] : [],
|
|
8237
8313
|
...this.config.claude.additionalArgs,
|
|
8238
8314
|
"--",
|
|
@@ -8320,6 +8396,7 @@ class Worker {
|
|
|
8320
8396
|
const model = opts.model ?? this.config.claude.model;
|
|
8321
8397
|
const maxTurns = opts.maxTurns ?? this.config.claude.maxTurns;
|
|
8322
8398
|
const allowedTools = (opts.allowedTools ?? IMPLEMENT_ALLOWED_TOOLS).split(",").map((t) => t.trim()).filter(Boolean);
|
|
8399
|
+
const disallowedTools = opts.disallowedTools ? opts.disallowedTools.split(",").map((t) => t.trim()).filter(Boolean) : undefined;
|
|
8323
8400
|
const initialPhase = opts.initialPhase ?? "exploring";
|
|
8324
8401
|
const sdkCfg = this.config.sdk;
|
|
8325
8402
|
log.info(this.tag, `Spawning Agent SDK runner (model=${model}, maxTurns=${maxTurns}${opts.resumeSessionId ? ", resume" : ""})`);
|
|
@@ -8339,6 +8416,7 @@ class Worker {
|
|
|
8339
8416
|
model,
|
|
8340
8417
|
maxTurns,
|
|
8341
8418
|
allowedTools,
|
|
8419
|
+
...disallowedTools ? { disallowedTools } : {},
|
|
8342
8420
|
maxBudgetUsd: sdkCfg?.maxBudgetUsd,
|
|
8343
8421
|
settingSources: sdkCfg?.settingSources,
|
|
8344
8422
|
mcpServers: sdkCfg?.mcpServers,
|
|
@@ -8405,7 +8483,7 @@ class Worker {
|
|
|
8405
8483
|
throw err;
|
|
8406
8484
|
}
|
|
8407
8485
|
}
|
|
8408
|
-
cleanup() {
|
|
8486
|
+
async cleanup() {
|
|
8409
8487
|
if (this.progressTracker) {
|
|
8410
8488
|
this.progressTracker.stop();
|
|
8411
8489
|
this.progressTracker = null;
|
|
@@ -8423,7 +8501,7 @@ class Worker {
|
|
|
8423
8501
|
}
|
|
8424
8502
|
if (this.worktreePath && (this.state === "error" || this.timedOut || this.aborted)) {
|
|
8425
8503
|
try {
|
|
8426
|
-
|
|
8504
|
+
await teardownWorktree(this.client, this.cardId, this.worktreePath, this.branchName ?? undefined);
|
|
8427
8505
|
} catch {
|
|
8428
8506
|
log.warn(this.tag, "Failed to cleanup worktree");
|
|
8429
8507
|
}
|
|
@@ -8904,7 +8982,7 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
8904
8982
|
}
|
|
8905
8983
|
if (run.worktreePath) {
|
|
8906
8984
|
try {
|
|
8907
|
-
|
|
8985
|
+
await teardownWorktree(client, run.cardId, run.worktreePath, run.branchName ?? undefined);
|
|
8908
8986
|
outcome.actions.push("cleaned up worktree");
|
|
8909
8987
|
} catch (err) {
|
|
8910
8988
|
const msg = err instanceof Error ? err.message : String(err);
|
package/dist/index.js
CHANGED
|
@@ -1665,6 +1665,9 @@ function getStageLoop(stage) {
|
|
|
1665
1665
|
function isConvergeLoop(loop) {
|
|
1666
1666
|
return loop !== null && loop.mode === "converge";
|
|
1667
1667
|
}
|
|
1668
|
+
function resolveLoopExitGate(stage, loop) {
|
|
1669
|
+
return loop.exit_gate ?? stage.gate ?? null;
|
|
1670
|
+
}
|
|
1668
1671
|
function decideLoopContinuation(args) {
|
|
1669
1672
|
const { loop, gatePassed, hasExitGate, completedIterations } = args;
|
|
1670
1673
|
const max = Math.max(1, Math.floor(loop.max_iterations) || 1);
|
|
@@ -1713,7 +1716,10 @@ function entryActionAllowlist(entryAction) {
|
|
|
1713
1716
|
return `mcp__${entryAction}`;
|
|
1714
1717
|
return null;
|
|
1715
1718
|
}
|
|
1716
|
-
|
|
1719
|
+
function stageDisallowedTools() {
|
|
1720
|
+
return STAGE_DAEMON_OWNED_TOOLS.length > 0 ? STAGE_DAEMON_OWNED_TOOLS.join(",") : null;
|
|
1721
|
+
}
|
|
1722
|
+
var DEFAULT_LOOP_MAX_ITERATIONS = 5, SKILL_TOOL_ALLOWLIST, HARMONY_TOOL_RE, STAGE_DAEMON_OWNED_TOOLS;
|
|
1717
1723
|
var init_playbookStage = __esm(() => {
|
|
1718
1724
|
SKILL_TOOL_ALLOWLIST = {
|
|
1719
1725
|
hmy: "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*",
|
|
@@ -1724,6 +1730,11 @@ var init_playbookStage = __esm(() => {
|
|
|
1724
1730
|
"hmy-standup": "Read,Grep,Glob,mcp__harmony__*"
|
|
1725
1731
|
};
|
|
1726
1732
|
HARMONY_TOOL_RE = /^harmony_[a-z_]+$/;
|
|
1733
|
+
STAGE_DAEMON_OWNED_TOOLS = [
|
|
1734
|
+
"mcp__harmony__harmony_end_agent_session",
|
|
1735
|
+
"mcp__harmony__harmony_start_agent_session",
|
|
1736
|
+
"mcp__harmony__harmony_move_card"
|
|
1737
|
+
];
|
|
1727
1738
|
});
|
|
1728
1739
|
|
|
1729
1740
|
// ../harmony-shared/dist/projectTemplates.js
|
|
@@ -2158,6 +2169,58 @@ function cleanupWorktree(worktreePath, branchName) {
|
|
|
2158
2169
|
} catch {}
|
|
2159
2170
|
}
|
|
2160
2171
|
}
|
|
2172
|
+
function resolveRepoRoot() {
|
|
2173
|
+
return execFileSync3("git", ["rev-parse", "--show-toplevel"], {
|
|
2174
|
+
encoding: "utf-8"
|
|
2175
|
+
}).trim();
|
|
2176
|
+
}
|
|
2177
|
+
function branchAheadOfItsRemote(branchName, repoRoot = resolveRepoRoot()) {
|
|
2178
|
+
try {
|
|
2179
|
+
const out = execFileSync3("git", ["rev-list", branchName, "--not", "--remotes=origin"], { cwd: repoRoot, encoding: "utf-8" }).trim();
|
|
2180
|
+
return out.length > 0;
|
|
2181
|
+
} catch {
|
|
2182
|
+
return false;
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
async function rescueUnpushedBranch(client, cardId, branchName, repoRoot = resolveRepoRoot()) {
|
|
2186
|
+
const { getBranchWebUrl: getBranchWebUrl2, pushBranch: pushBranch2 } = await Promise.resolve().then(() => (init_git_pr(), exports_git_pr));
|
|
2187
|
+
try {
|
|
2188
|
+
pushBranch2(branchName, repoRoot);
|
|
2189
|
+
} catch (err) {
|
|
2190
|
+
log.error(TAG5, `push-rescue failed for ${branchName} — leaving local branch ref intact (recoverable via git reflog / the local branch): ${err instanceof Error ? err.message : err}`);
|
|
2191
|
+
return false;
|
|
2192
|
+
}
|
|
2193
|
+
log.warn(TAG5, `push-rescued unpushed branch ${branchName} to origin before teardown`);
|
|
2194
|
+
try {
|
|
2195
|
+
const url = getBranchWebUrl2(branchName, repoRoot);
|
|
2196
|
+
const recover = url ? `View it at ${url} or recover locally: \`git fetch && git checkout ${branchName}\`` : `Recover it locally: \`git fetch && git checkout ${branchName}\``;
|
|
2197
|
+
const body = `⚠ Run ended before completion. Committed work was push-rescued to ` + `\`origin/${branchName}\` so it isn't lost. ${recover}`;
|
|
2198
|
+
await client.addComment(cardId, body, { commentType: "message" });
|
|
2199
|
+
} catch (err) {
|
|
2200
|
+
log.warn(TAG5, `push-rescue comment failed for ${branchName} (work is still safe on origin): ${err instanceof Error ? err.message : err}`);
|
|
2201
|
+
}
|
|
2202
|
+
return true;
|
|
2203
|
+
}
|
|
2204
|
+
async function teardownWorktree(client, cardId, worktreePath, branchName) {
|
|
2205
|
+
let skipBranchDelete = false;
|
|
2206
|
+
if (branchName && cardId) {
|
|
2207
|
+
let repoRoot;
|
|
2208
|
+
try {
|
|
2209
|
+
repoRoot = resolveRepoRoot();
|
|
2210
|
+
} catch {
|
|
2211
|
+
cleanupWorktree(worktreePath, branchName);
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
if (branchAheadOfItsRemote(branchName, repoRoot)) {
|
|
2215
|
+
const ok = await rescueUnpushedBranch(client, cardId, branchName, repoRoot);
|
|
2216
|
+
if (!ok) {
|
|
2217
|
+
skipBranchDelete = true;
|
|
2218
|
+
log.error(TAG5, `Keeping local branch ${branchName} (push-rescue failed) to avoid orphaning its commit`);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
cleanupWorktree(worktreePath, skipBranchDelete ? undefined : branchName);
|
|
2223
|
+
}
|
|
2161
2224
|
function makeBranchName(shortId, title, prefix = "agent-attempts/") {
|
|
2162
2225
|
const slug = title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
2163
2226
|
return `${prefix}${shortId}-${slug || "task"}`;
|
|
@@ -3508,7 +3571,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3508
3571
|
failureSummary,
|
|
3509
3572
|
...buildTokenPayload(sessionStats)
|
|
3510
3573
|
});
|
|
3511
|
-
|
|
3574
|
+
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
3512
3575
|
return false;
|
|
3513
3576
|
}
|
|
3514
3577
|
log.info(TAG14, `Pushing branch ${branchName} (pre-verify)...`);
|
|
@@ -3590,7 +3653,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3590
3653
|
recoveryBranch: branchName,
|
|
3591
3654
|
...buildTokenPayload(sessionStats)
|
|
3592
3655
|
});
|
|
3593
|
-
|
|
3656
|
+
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
3594
3657
|
return false;
|
|
3595
3658
|
}
|
|
3596
3659
|
log.info(TAG14, `Verification passed for #${card.short_id}`);
|
|
@@ -3646,7 +3709,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
3646
3709
|
log.warn(TAG14, `onBeforeWorktreeCleanup hook failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
3647
3710
|
}
|
|
3648
3711
|
}
|
|
3649
|
-
|
|
3712
|
+
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
3650
3713
|
log.info(TAG14, `Completion done for #${card.short_id}${prUrl ? ` — PR: ${prUrl}` : ""}`);
|
|
3651
3714
|
return true;
|
|
3652
3715
|
}
|
|
@@ -3966,6 +4029,7 @@ class SdkAgentRunner {
|
|
|
3966
4029
|
cwd: input.cwd,
|
|
3967
4030
|
model: input.model ?? this.cfg.model,
|
|
3968
4031
|
allowedTools: allowed,
|
|
4032
|
+
...this.cfg.disallowedTools && this.cfg.disallowedTools.length > 0 ? { disallowedTools: this.cfg.disallowedTools } : {},
|
|
3969
4033
|
tools: builtinTools,
|
|
3970
4034
|
permissionMode: "dontAsk",
|
|
3971
4035
|
maxTurns: this.cfg.maxTurns,
|
|
@@ -6917,7 +6981,7 @@ async function buildPrompt(enriched, branchName, worktreePath, client, workspace
|
|
|
6917
6981
|
variant: "execute",
|
|
6918
6982
|
customConstraints: `You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
|
|
6919
6983
|
Do NOT push to main. All your work stays on \`${branchName}\`.
|
|
6920
|
-
|
|
6984
|
+
The daemon owns the run lifecycle: once your work is committed it ends the agent session, pushes the branch, and moves the card to Review for you. Do NOT call harmony_end_agent_session, do NOT start a new session, and do NOT move the card or change its column yourself. If the skill driving this work tells you to move the card or end the session as a final step, SKIP it — it is handled for you (those tools are disabled for this run). Finish the implementation, commit, and stop.`
|
|
6921
6985
|
});
|
|
6922
6986
|
log.info(TAG27, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
|
|
6923
6987
|
return result.prompt + pastEpisodesSection;
|
|
@@ -7027,7 +7091,7 @@ ${subtaskStr}
|
|
|
7027
7091
|
Include a brief currentTask description.
|
|
7028
7092
|
3. Implement the changes on branch \`${branchName}\`
|
|
7029
7093
|
4. Commit your work with clear, descriptive commit messages
|
|
7030
|
-
5. When
|
|
7094
|
+
5. When the work is committed, STOP. The daemon owns the run lifecycle: it ends the agent session, pushes the branch, and moves the card to Review for you. Do NOT call harmony_end_agent_session, do NOT start a new session, and do NOT move the card or change its column yourself — those tools are disabled for this run.
|
|
7031
7095
|
|
|
7032
7096
|
You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
|
|
7033
7097
|
Do NOT push to main. All your work stays on \`${branchName}\`.`;
|
|
@@ -7076,7 +7140,7 @@ function firstErrorMessage(evaluation) {
|
|
|
7076
7140
|
return e ? e.message : null;
|
|
7077
7141
|
}
|
|
7078
7142
|
async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loop, deps) {
|
|
7079
|
-
const hasExitGate = loop
|
|
7143
|
+
const hasExitGate = resolveLoopExitGate(stage, loop) != null;
|
|
7080
7144
|
const gatePassed = hasExitGate ? evaluation?.passed ?? false : false;
|
|
7081
7145
|
const gateResult = !hasExitGate ? "skipped" : gatePassed ? "passed" : "failed";
|
|
7082
7146
|
const maxIterations = Math.max(1, Math.floor(loop.max_iterations) || 1);
|
|
@@ -7329,7 +7393,7 @@ function buildStagePreamble(stage) {
|
|
|
7329
7393
|
lines.push(`Hand off when done: ${summary.trim()}`);
|
|
7330
7394
|
}
|
|
7331
7395
|
}
|
|
7332
|
-
lines.push("Do only this stage's work.
|
|
7396
|
+
lines.push("Do only this stage's work, then stop. The daemon owns the run lifecycle here: it ends the agent session and performs every card move and stage advancement automatically once your stage work is done. Do NOT call `harmony_end_agent_session`, do NOT start a new session, and do NOT move the card or change its column yourself. If the skill driving this stage tells you to move the card (e.g. to Review) or end your session as a final step, SKIP that step — it is handled for you. Those tools are disabled for this run, so attempting them only wastes turns. Finish the stage's work and stop.");
|
|
7333
7397
|
if (normalizeGateSpec(stage.gate)?.kind === "review_passed") {
|
|
7334
7398
|
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
7399
|
}
|
|
@@ -7342,6 +7406,13 @@ function buildSteeringPrompt(messages) {
|
|
|
7342
7406
|
return messages.map((m, i) => `${i + 1}. ${m}`).join(`
|
|
7343
7407
|
`);
|
|
7344
7408
|
}
|
|
7409
|
+
function computeRunSpawnGating(stageAllowedTools) {
|
|
7410
|
+
const denylist = stageDisallowedTools();
|
|
7411
|
+
return {
|
|
7412
|
+
...stageAllowedTools ? { allowedTools: stageAllowedTools } : {},
|
|
7413
|
+
...denylist ? { disallowedTools: denylist } : {}
|
|
7414
|
+
};
|
|
7415
|
+
}
|
|
7345
7416
|
|
|
7346
7417
|
class Worker {
|
|
7347
7418
|
config;
|
|
@@ -7370,6 +7441,7 @@ class Worker {
|
|
|
7370
7441
|
timedOut = false;
|
|
7371
7442
|
verificationFailed = false;
|
|
7372
7443
|
held = false;
|
|
7444
|
+
activeRunSpawnOpts = null;
|
|
7373
7445
|
completionStarted = false;
|
|
7374
7446
|
sessionId = null;
|
|
7375
7447
|
runId = null;
|
|
@@ -7449,6 +7521,7 @@ class Worker {
|
|
|
7449
7521
|
this.lastRunText = "";
|
|
7450
7522
|
this.cliSessionId = null;
|
|
7451
7523
|
this.lastDrainedSeq = 0;
|
|
7524
|
+
this.activeRunSpawnOpts = null;
|
|
7452
7525
|
this.cardId = card.id;
|
|
7453
7526
|
this.startedAt = Date.now();
|
|
7454
7527
|
this.runId = newRunId();
|
|
@@ -7569,9 +7642,10 @@ class Worker {
|
|
|
7569
7642
|
this.timedOut = true;
|
|
7570
7643
|
this.cancel();
|
|
7571
7644
|
}, this.config.maxTimeout);
|
|
7645
|
+
this.activeRunSpawnOpts = computeRunSpawnGating(stageCtx.kind === "run" ? stageCtx.allowedTools : null);
|
|
7572
7646
|
await this.spawnClaude(prompt, card, subtasks, {
|
|
7573
7647
|
model: this.selectImplementModel(card),
|
|
7574
|
-
...
|
|
7648
|
+
...this.activeRunSpawnOpts ?? {}
|
|
7575
7649
|
});
|
|
7576
7650
|
if (this.aborted)
|
|
7577
7651
|
return;
|
|
@@ -7646,7 +7720,7 @@ class Worker {
|
|
|
7646
7720
|
}
|
|
7647
7721
|
if (this.worktreePath) {
|
|
7648
7722
|
try {
|
|
7649
|
-
|
|
7723
|
+
await teardownWorktree(this.client, card.id, this.worktreePath, this.branchName ?? undefined);
|
|
7650
7724
|
} catch {
|
|
7651
7725
|
log.warn(this.tag, "Failed to cleanup worktree before requeue");
|
|
7652
7726
|
}
|
|
@@ -7690,7 +7764,7 @@ class Worker {
|
|
|
7690
7764
|
} else if (this.runId && this.timedOut) {
|
|
7691
7765
|
if (this.worktreePath) {
|
|
7692
7766
|
try {
|
|
7693
|
-
|
|
7767
|
+
await teardownWorktree(this.client, card.id, this.worktreePath, this.branchName ?? undefined);
|
|
7694
7768
|
} catch {
|
|
7695
7769
|
log.warn(this.tag, "Failed to cleanup worktree before requeue");
|
|
7696
7770
|
}
|
|
@@ -7780,7 +7854,7 @@ class Worker {
|
|
|
7780
7854
|
await this.cliRunner.flushFinal();
|
|
7781
7855
|
} catch {}
|
|
7782
7856
|
}
|
|
7783
|
-
this.cleanup();
|
|
7857
|
+
await this.cleanup();
|
|
7784
7858
|
this.state = "idle";
|
|
7785
7859
|
this.onDone(this);
|
|
7786
7860
|
}
|
|
@@ -7906,7 +7980,7 @@ class Worker {
|
|
|
7906
7980
|
async collectStageGateEvidence(card, stage, worktreePath, subtasks) {
|
|
7907
7981
|
try {
|
|
7908
7982
|
const loop = getStageLoop(stage);
|
|
7909
|
-
const gateSource = isConvergeLoop(loop)
|
|
7983
|
+
const gateSource = isConvergeLoop(loop) && loop ? resolveLoopExitGate(stage, loop) : stage.gate;
|
|
7910
7984
|
const gate = normalizeGateSpec(gateSource);
|
|
7911
7985
|
if (!gate) {
|
|
7912
7986
|
return null;
|
|
@@ -8204,7 +8278,8 @@ class Worker {
|
|
|
8204
8278
|
await this.spawnClaude(buildSteeringPrompt(messages.map((m) => m.text)), card, subtasks, {
|
|
8205
8279
|
model: this.selectImplementModel(card),
|
|
8206
8280
|
maxTurns: STEERING_MAX_TURNS,
|
|
8207
|
-
resumeSessionId: this.cliSessionId
|
|
8281
|
+
resumeSessionId: this.cliSessionId,
|
|
8282
|
+
...this.activeRunSpawnOpts ?? {}
|
|
8208
8283
|
});
|
|
8209
8284
|
} catch (err) {
|
|
8210
8285
|
log.warn(this.tag, `Steering resume failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
@@ -8232,6 +8307,7 @@ class Worker {
|
|
|
8232
8307
|
String(maxTurns),
|
|
8233
8308
|
"--allowedTools",
|
|
8234
8309
|
allowedTools,
|
|
8310
|
+
...opts.disallowedTools ? ["--disallowedTools", opts.disallowedTools] : [],
|
|
8235
8311
|
...opts.resumeSessionId ? ["--resume", opts.resumeSessionId] : [],
|
|
8236
8312
|
...this.config.claude.additionalArgs,
|
|
8237
8313
|
"--",
|
|
@@ -8319,6 +8395,7 @@ class Worker {
|
|
|
8319
8395
|
const model = opts.model ?? this.config.claude.model;
|
|
8320
8396
|
const maxTurns = opts.maxTurns ?? this.config.claude.maxTurns;
|
|
8321
8397
|
const allowedTools = (opts.allowedTools ?? IMPLEMENT_ALLOWED_TOOLS).split(",").map((t) => t.trim()).filter(Boolean);
|
|
8398
|
+
const disallowedTools = opts.disallowedTools ? opts.disallowedTools.split(",").map((t) => t.trim()).filter(Boolean) : undefined;
|
|
8322
8399
|
const initialPhase = opts.initialPhase ?? "exploring";
|
|
8323
8400
|
const sdkCfg = this.config.sdk;
|
|
8324
8401
|
log.info(this.tag, `Spawning Agent SDK runner (model=${model}, maxTurns=${maxTurns}${opts.resumeSessionId ? ", resume" : ""})`);
|
|
@@ -8338,6 +8415,7 @@ class Worker {
|
|
|
8338
8415
|
model,
|
|
8339
8416
|
maxTurns,
|
|
8340
8417
|
allowedTools,
|
|
8418
|
+
...disallowedTools ? { disallowedTools } : {},
|
|
8341
8419
|
maxBudgetUsd: sdkCfg?.maxBudgetUsd,
|
|
8342
8420
|
settingSources: sdkCfg?.settingSources,
|
|
8343
8421
|
mcpServers: sdkCfg?.mcpServers,
|
|
@@ -8404,7 +8482,7 @@ class Worker {
|
|
|
8404
8482
|
throw err;
|
|
8405
8483
|
}
|
|
8406
8484
|
}
|
|
8407
|
-
cleanup() {
|
|
8485
|
+
async cleanup() {
|
|
8408
8486
|
if (this.progressTracker) {
|
|
8409
8487
|
this.progressTracker.stop();
|
|
8410
8488
|
this.progressTracker = null;
|
|
@@ -8422,7 +8500,7 @@ class Worker {
|
|
|
8422
8500
|
}
|
|
8423
8501
|
if (this.worktreePath && (this.state === "error" || this.timedOut || this.aborted)) {
|
|
8424
8502
|
try {
|
|
8425
|
-
|
|
8503
|
+
await teardownWorktree(this.client, this.cardId, this.worktreePath, this.branchName ?? undefined);
|
|
8426
8504
|
} catch {
|
|
8427
8505
|
log.warn(this.tag, "Failed to cleanup worktree");
|
|
8428
8506
|
}
|
|
@@ -8903,7 +8981,7 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
8903
8981
|
}
|
|
8904
8982
|
if (run.worktreePath) {
|
|
8905
8983
|
try {
|
|
8906
|
-
|
|
8984
|
+
await teardownWorktree(client, run.cardId, run.worktreePath, run.branchName ?? undefined);
|
|
8907
8985
|
outcome.actions.push("cleaned up worktree");
|
|
8908
8986
|
} catch (err) {
|
|
8909
8987
|
const msg = err instanceof Error ? err.message : String(err);
|