@gethmy/agent 1.49.5 → 1.49.6
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/README.md +1 -1
- package/dist/cli.js +153 -16
- package/dist/index.js +151 -16
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -121,7 +121,7 @@ harmony-agent help # Show usage
|
|
|
121
121
|
## What the daemon does
|
|
122
122
|
|
|
123
123
|
1. **Watches** the board in real time via Supabase Realtime.
|
|
124
|
-
2. **Picks up** cards assigned to
|
|
124
|
+
2. **Picks up** cards assigned to the daemon's virtual agent from configured columns. Moving a card to a pickup column is not enough; with `sweep.enabled` the daemon also claims unassigned cards from those columns itself.
|
|
125
125
|
3. **Implements** changes in an isolated git worktree using a full-tool Claude run.
|
|
126
126
|
4. **Verifies** the build + lint pass. Failures trigger an auto-fix attempt. Persistent failures move the card back to the `verification.failColumn` (default: `To Do`).
|
|
127
127
|
5. **Pushes** the branch and moves the card to `Review`.
|
package/dist/cli.js
CHANGED
|
@@ -735,6 +735,18 @@ function formatDailyCap(cents) {
|
|
|
735
735
|
function formatTurnCap(turns) {
|
|
736
736
|
return turns < 0 ? "no cap" : `${turns} turns`;
|
|
737
737
|
}
|
|
738
|
+
function describeGuardDenial(reason, detail) {
|
|
739
|
+
switch (reason) {
|
|
740
|
+
case "max_attempts":
|
|
741
|
+
return `Stopped — ${detail} (\`budget.maxAttemptsPerCard\`). Reassign the card to grant a fresh attempt, or raise the limit in the daemon config.`;
|
|
742
|
+
case "card_turns":
|
|
743
|
+
return `Stopped — ${detail} (\`budget.maxTurnsPerCard\`). Reassign the card to clear the count, or raise the limit in the daemon config (-1 removes it).`;
|
|
744
|
+
case "card_runs":
|
|
745
|
+
return `Stopped — ${detail} (\`budget.maxRunsPerCard\`). Reassign the card to clear the count, or raise the limit in the daemon config (-1 removes it).`;
|
|
746
|
+
case "daily_budget":
|
|
747
|
+
return `Daily budget reached — waiting for the UTC reset (${detail}, \`budget.dailyBudgetCents\`). Raise the limit in the daemon config to continue today (-1 removes it).`;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
738
750
|
|
|
739
751
|
class BudgetGuard {
|
|
740
752
|
config;
|
|
@@ -2313,6 +2325,7 @@ var init_plan_phase = __esm(() => {
|
|
|
2313
2325
|
mode: "advisory",
|
|
2314
2326
|
model: "sonnet",
|
|
2315
2327
|
maxTurns: 40,
|
|
2328
|
+
timeoutMs: 10 * 60000,
|
|
2316
2329
|
postComment: true,
|
|
2317
2330
|
awaitingApprovalColumn: "To Do",
|
|
2318
2331
|
minComplexityScore: 3,
|
|
@@ -2373,6 +2386,9 @@ var init_types = __esm(() => {
|
|
|
2373
2386
|
},
|
|
2374
2387
|
scoreBands: DEFAULT_SCORE_BANDS,
|
|
2375
2388
|
sizingModel: "",
|
|
2389
|
+
sizingMaxTurns: 25,
|
|
2390
|
+
sizingMaxBudgetUsd: 0.75,
|
|
2391
|
+
sizingTimeoutMs: 240000,
|
|
2376
2392
|
researchRule: "shadow",
|
|
2377
2393
|
reviewModel: "sonnet",
|
|
2378
2394
|
deepReviewModel: "sonnet",
|
|
@@ -2475,6 +2491,10 @@ var init_types = __esm(() => {
|
|
|
2475
2491
|
runRecordDays: 14,
|
|
2476
2492
|
runLogDays: 30
|
|
2477
2493
|
},
|
|
2494
|
+
steering: {
|
|
2495
|
+
maxTurns: 15,
|
|
2496
|
+
maxIterations: 10
|
|
2497
|
+
},
|
|
2478
2498
|
planning: DEFAULT_PLANNING_CONFIG,
|
|
2479
2499
|
contractFirst: DEFAULT_CONTRACT_CONFIG,
|
|
2480
2500
|
boardReview: DEFAULT_BOARD_REVIEW_CONFIG,
|
|
@@ -2495,6 +2515,7 @@ __export(exports_config_validation, {
|
|
|
2495
2515
|
validateVerificationConfig: () => validateVerificationConfig,
|
|
2496
2516
|
validateSweepConfig: () => validateSweepConfig,
|
|
2497
2517
|
validateSizingConfig: () => validateSizingConfig,
|
|
2518
|
+
validateRunLimitsConfig: () => validateRunLimitsConfig,
|
|
2498
2519
|
validateRetentionConfig: () => validateRetentionConfig,
|
|
2499
2520
|
validateRankingConfig: () => validateRankingConfig,
|
|
2500
2521
|
validateHttpConfig: () => validateHttpConfig,
|
|
@@ -2826,6 +2847,41 @@ function validateRetentionConfig(config) {
|
|
|
2826
2847
|
` + ` Set a positive number of days, or a negative value to keep this store forever.`, [issue]);
|
|
2827
2848
|
}
|
|
2828
2849
|
}
|
|
2850
|
+
function validateRunLimitsConfig(config) {
|
|
2851
|
+
const wholeOrOff = [
|
|
2852
|
+
["planning.timeoutMs", config.planning.timeoutMs],
|
|
2853
|
+
["steering.maxTurns", config.steering.maxTurns],
|
|
2854
|
+
["steering.maxIterations", config.steering.maxIterations],
|
|
2855
|
+
["claude.sizingMaxTurns", config.claude.sizingMaxTurns],
|
|
2856
|
+
["claude.sizingTimeoutMs", config.claude.sizingTimeoutMs]
|
|
2857
|
+
];
|
|
2858
|
+
const issues = [];
|
|
2859
|
+
for (const [key, value] of wholeOrOff) {
|
|
2860
|
+
if (value === -1)
|
|
2861
|
+
continue;
|
|
2862
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
2863
|
+
issues.push(`${key}: ${value === 0 ? '0 is ambiguous — it could mean "none allowed" or "no limit"' : `${JSON.stringify(value)} is not a positive whole number`}`);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
const budget = config.claude.sizingMaxBudgetUsd;
|
|
2867
|
+
if (budget !== -1 && !(Number.isFinite(budget) && budget > 0)) {
|
|
2868
|
+
issues.push(`claude.sizingMaxBudgetUsd: ${budget === 0 ? '0 is ambiguous — it could mean "no spend allowed" or "no limit"' : `${JSON.stringify(budget)} is not a positive number`}`);
|
|
2869
|
+
}
|
|
2870
|
+
for (const [key, value] of [
|
|
2871
|
+
["claude.maxTurns", config.claude.maxTurns],
|
|
2872
|
+
["claude.reviewMaxTurns", config.claude.reviewMaxTurns]
|
|
2873
|
+
]) {
|
|
2874
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
2875
|
+
issues.push(`${key}: ${JSON.stringify(value)} is not a positive whole number`);
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
if (issues.length > 0) {
|
|
2879
|
+
throw new ConfigValidationError(`Invalid agent config — run limits:
|
|
2880
|
+
- ${issues.join(`
|
|
2881
|
+
- `)}
|
|
2882
|
+
` + ` Set a positive value, or -1 to remove the limit (not available for claude.maxTurns and claude.reviewMaxTurns).`, issues);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2829
2885
|
function validateRankingConfig(config) {
|
|
2830
2886
|
const issues = [];
|
|
2831
2887
|
const entries = Object.entries(config.ranking);
|
|
@@ -7184,9 +7240,11 @@ ${runLogTail}
|
|
|
7184
7240
|
} else {
|
|
7185
7241
|
await breakReviewLoopIfNeeded();
|
|
7186
7242
|
}
|
|
7243
|
+
const movedTo = handbackVerdict.proceed ? `I moved the card to "${config.review.moveToColumn}" instead of sending it back to "${config.review.failColumn}" again.` : "I left the card where it is instead of sending it back again.";
|
|
7187
7244
|
const body = [
|
|
7188
7245
|
"**Review — needs human review.**",
|
|
7189
|
-
`
|
|
7246
|
+
`This card reached the limit of ${maxCycles} review cycles (\`review.maxReviewCycles\`). ${movedTo}`,
|
|
7247
|
+
"Next: read the earlier review comments and fix or merge the branch by hand. To allow more automatic rounds, raise `review.maxReviewCycles` in the daemon config.",
|
|
7190
7248
|
result.summary || ""
|
|
7191
7249
|
].filter(Boolean).join(`
|
|
7192
7250
|
|
|
@@ -9902,6 +9960,7 @@ var init_workspace_model_config = __esm(() => {
|
|
|
9902
9960
|
// src/worker.ts
|
|
9903
9961
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
9904
9962
|
import {
|
|
9963
|
+
boundedTimerMs,
|
|
9905
9964
|
chooseImplementModel,
|
|
9906
9965
|
classifyRunError as classifyRunError2,
|
|
9907
9966
|
createWorktree,
|
|
@@ -10792,7 +10851,10 @@ ${prompt}`;
|
|
|
10792
10851
|
title: card.title,
|
|
10793
10852
|
description: card.description,
|
|
10794
10853
|
model,
|
|
10795
|
-
scoreBands: claude.scoreBands
|
|
10854
|
+
scoreBands: claude.scoreBands,
|
|
10855
|
+
maxTurns: claude.sizingMaxTurns < 0 ? null : claude.sizingMaxTurns,
|
|
10856
|
+
maxBudgetUsd: claude.sizingMaxBudgetUsd < 0 ? null : claude.sizingMaxBudgetUsd,
|
|
10857
|
+
timeoutMs: claude.sizingTimeoutMs < 0 ? null : claude.sizingTimeoutMs
|
|
10796
10858
|
});
|
|
10797
10859
|
if (outcome.status === "sized") {
|
|
10798
10860
|
log33.info(this.tag, `Sized #${card.short_id}: complexity ${outcome.sizing.complexity}/10 -> ${outcome.sizing.tier}`);
|
|
@@ -11031,6 +11093,10 @@ ${prompt}`;
|
|
|
11031
11093
|
}
|
|
11032
11094
|
}
|
|
11033
11095
|
}
|
|
11096
|
+
armPhaseTimer(onExpiry) {
|
|
11097
|
+
const { timeoutMs } = this.config.planning;
|
|
11098
|
+
return setTimeout(onExpiry, boundedTimerMs(timeoutMs < 0 ? this.config.maxTimeout : timeoutMs));
|
|
11099
|
+
}
|
|
11034
11100
|
async runPlanningPhase(enriched) {
|
|
11035
11101
|
const planning = this.config.planning;
|
|
11036
11102
|
const { card } = enriched;
|
|
@@ -11045,7 +11111,7 @@ ${prompt}`;
|
|
|
11045
11111
|
}).catch(() => {});
|
|
11046
11112
|
const planPrompt = buildPlanPrompt(enriched, this.worktreePath);
|
|
11047
11113
|
let planTimedOut = false;
|
|
11048
|
-
const planTimeout =
|
|
11114
|
+
const planTimeout = this.armPhaseTimer(() => {
|
|
11049
11115
|
planTimedOut = true;
|
|
11050
11116
|
log33.warn(this.tag, "Planning pass exceeded timeout — abandoning, implementing directly");
|
|
11051
11117
|
if (this.sdkRunner) {
|
|
@@ -11056,7 +11122,7 @@ ${prompt}`;
|
|
|
11056
11122
|
sigtermTimeoutMs: 5000
|
|
11057
11123
|
}).catch(() => {});
|
|
11058
11124
|
}
|
|
11059
|
-
}
|
|
11125
|
+
});
|
|
11060
11126
|
try {
|
|
11061
11127
|
await this.spawnClaude(planPrompt, card, [], {
|
|
11062
11128
|
model: planning.model,
|
|
@@ -11156,7 +11222,7 @@ ${prompt}`;
|
|
|
11156
11222
|
}).catch(() => {});
|
|
11157
11223
|
const contractPrompt = buildContractPrompt(enriched, this.worktreePath);
|
|
11158
11224
|
let contractTimedOut = false;
|
|
11159
|
-
const contractTimeout =
|
|
11225
|
+
const contractTimeout = this.armPhaseTimer(() => {
|
|
11160
11226
|
contractTimedOut = true;
|
|
11161
11227
|
log33.warn(this.tag, "Contract pass exceeded timeout — abandoning, implementing directly");
|
|
11162
11228
|
if (this.sdkRunner) {
|
|
@@ -11167,7 +11233,7 @@ ${prompt}`;
|
|
|
11167
11233
|
sigtermTimeoutMs: 5000
|
|
11168
11234
|
}).catch(() => {});
|
|
11169
11235
|
}
|
|
11170
|
-
}
|
|
11236
|
+
});
|
|
11171
11237
|
try {
|
|
11172
11238
|
await this.spawnClaude(contractPrompt, card, [], {
|
|
11173
11239
|
model: contractCfg.model,
|
|
@@ -11219,7 +11285,8 @@ ${prompt}`;
|
|
|
11219
11285
|
async drainSteeringMessages(card, subtasks) {
|
|
11220
11286
|
if (!this.cliSessionId || !this.sessionId || !this.cardId)
|
|
11221
11287
|
return;
|
|
11222
|
-
|
|
11288
|
+
const { maxIterations, maxTurns } = this.config.steering;
|
|
11289
|
+
for (let i = 0;(maxIterations < 0 || i < maxIterations) && !this.aborted; i++) {
|
|
11223
11290
|
let messages;
|
|
11224
11291
|
try {
|
|
11225
11292
|
const res = await this.client.getPendingUserMessages(this.cardId, this.sessionId, this.lastDrainedSeq);
|
|
@@ -11237,7 +11304,7 @@ ${prompt}`;
|
|
|
11237
11304
|
try {
|
|
11238
11305
|
await this.spawnClaude(buildSteeringPrompt(messages.map((m) => frameSteeringQuestion(m.text, m.questionCommentId))), card, subtasks, {
|
|
11239
11306
|
model: this.selectImplementModel(card),
|
|
11240
|
-
maxTurns:
|
|
11307
|
+
maxTurns: maxTurns < 0 ? undefined : maxTurns,
|
|
11241
11308
|
resumeSessionId: this.cliSessionId,
|
|
11242
11309
|
noCommitNotice: true,
|
|
11243
11310
|
...this.activeRunSpawnOpts ?? {}
|
|
@@ -11543,7 +11610,7 @@ ${prompt}`;
|
|
|
11543
11610
|
this.chargedTokens = emptyTokens();
|
|
11544
11611
|
}
|
|
11545
11612
|
}
|
|
11546
|
-
var TAG30 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4,
|
|
11613
|
+
var TAG30 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4, PLAN_ALLOWED_TOOLS = "Read,Grep,Glob,mcp__harmony__*", IMPLEMENT_ALLOWED_TOOLS = "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*";
|
|
11547
11614
|
var init_worker = __esm(() => {
|
|
11548
11615
|
init_src();
|
|
11549
11616
|
init_board_helpers();
|
|
@@ -11563,7 +11630,6 @@ var init_worker = __esm(() => {
|
|
|
11563
11630
|
init_transitions();
|
|
11564
11631
|
init_types();
|
|
11565
11632
|
init_workspace_model_config();
|
|
11566
|
-
PLAN_PHASE_TIMEOUT = 10 * 60000;
|
|
11567
11633
|
});
|
|
11568
11634
|
|
|
11569
11635
|
// src/workspace-model-cache.ts
|
|
@@ -11725,13 +11791,13 @@ class Pool {
|
|
|
11725
11791
|
if (mode === "implement") {
|
|
11726
11792
|
if (this.authPaused) {
|
|
11727
11793
|
log35.debug(TAG31, `#${card.short_id} held — agent paused (auth error)`);
|
|
11728
|
-
await this.emitWaiting(card.id, "Agent paused — Anthropic auth error
|
|
11794
|
+
await this.emitWaiting(card.id, "Agent paused — Anthropic auth error. Fix the API credentials, then restart the daemon.");
|
|
11729
11795
|
return;
|
|
11730
11796
|
}
|
|
11731
11797
|
const cooldownMs = this.apiCooldownRemainingMs();
|
|
11732
11798
|
if (cooldownMs > 0) {
|
|
11733
11799
|
log35.debug(TAG31, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
|
|
11734
|
-
await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying
|
|
11800
|
+
await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying after ${new Date(this.apiCooldownUntil).toISOString().slice(11, 16)} UTC. Nothing to change: the daemon resumes by itself.`);
|
|
11735
11801
|
return;
|
|
11736
11802
|
}
|
|
11737
11803
|
const decision = this.budget.check(card.id);
|
|
@@ -11740,6 +11806,7 @@ class Pool {
|
|
|
11740
11806
|
await this.denyDailyBudget(card, decision.detail);
|
|
11741
11807
|
} else {
|
|
11742
11808
|
log35.debug(TAG31, `#${card.short_id} skipped (${decision.reason}): ${decision.detail}`);
|
|
11809
|
+
await this.emitWaiting(card.id, describeGuardDenial(decision.reason, decision.detail));
|
|
11743
11810
|
}
|
|
11744
11811
|
return;
|
|
11745
11812
|
}
|
|
@@ -11747,6 +11814,7 @@ class Pool {
|
|
|
11747
11814
|
chainSuccessors = successors;
|
|
11748
11815
|
if (blockers === null) {
|
|
11749
11816
|
log35.warn(TAG31, `#${card.short_id} blocker check failed — deferring to next tick`);
|
|
11817
|
+
await this.emitWaiting(card.id, "Waiting — could not read this card's links to check for blockers. The daemon retries on the next tick.");
|
|
11750
11818
|
return;
|
|
11751
11819
|
}
|
|
11752
11820
|
if (blockers.length > 0) {
|
|
@@ -11780,7 +11848,7 @@ class Pool {
|
|
|
11780
11848
|
}
|
|
11781
11849
|
}
|
|
11782
11850
|
async denyDailyBudget(card, detail) {
|
|
11783
|
-
const reason =
|
|
11851
|
+
const reason = describeGuardDenial("daily_budget", detail);
|
|
11784
11852
|
const line = `#${card.short_id} skipped (daily_budget): ${detail}`;
|
|
11785
11853
|
if (this.lastWaitingEmit.get(card.id) === reason)
|
|
11786
11854
|
log35.debug(TAG31, line);
|
|
@@ -11788,6 +11856,11 @@ class Pool {
|
|
|
11788
11856
|
log35.warn(TAG31, line);
|
|
11789
11857
|
await this.emitWaiting(card.id, reason);
|
|
11790
11858
|
}
|
|
11859
|
+
async noteSkip(cardId, reason) {
|
|
11860
|
+
if (this.isCardKnown(cardId))
|
|
11861
|
+
return;
|
|
11862
|
+
await this.emitWaiting(cardId, reason);
|
|
11863
|
+
}
|
|
11791
11864
|
lastWaitingEmit = new Map;
|
|
11792
11865
|
async emitWaiting(cardId, currentTask) {
|
|
11793
11866
|
if (this.lastWaitingEmit.get(cardId) === currentTask)
|
|
@@ -13902,6 +13975,7 @@ var init_startup_model = __esm(() => {
|
|
|
13902
13975
|
// src/startup-banner.ts
|
|
13903
13976
|
var exports_startup_banner = {};
|
|
13904
13977
|
__export(exports_startup_banner, {
|
|
13978
|
+
limitsLine: () => limitsLine,
|
|
13905
13979
|
createStartupBanner: () => createStartupBanner,
|
|
13906
13980
|
contextSourceLine: () => contextSourceLine
|
|
13907
13981
|
});
|
|
@@ -13963,6 +14037,7 @@ function jsonBanner(config, version) {
|
|
|
13963
14037
|
log44.info(TAG40, `Harmony Agent Daemon v${version} starting...`);
|
|
13964
14038
|
log44.info(TAG40, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
|
|
13965
14039
|
log44.info(TAG40, `Config: ${contextSourceLine(config)}`);
|
|
14040
|
+
log44.info(TAG40, `Limits: ${limitsLine(config)}`);
|
|
13966
14041
|
if (config.agent.review.enabled) {
|
|
13967
14042
|
log44.info(TAG40, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
|
|
13968
14043
|
}
|
|
@@ -14042,6 +14117,7 @@ function configRows(config, projectName, gitProvider, httpPort, model) {
|
|
|
14042
14117
|
value: `${modelDesc} · ${poolDesc} · ${flowDesc}`
|
|
14043
14118
|
});
|
|
14044
14119
|
rows.push({ label: "Runner", value: runnerDesc(config.agent.runner) });
|
|
14120
|
+
rows.push({ label: "Limits", value: limitsLine(config) });
|
|
14045
14121
|
const tail = [];
|
|
14046
14122
|
if (gitProvider)
|
|
14047
14123
|
tail.push(gitProvider);
|
|
@@ -14064,6 +14140,21 @@ function contextSourceLine(config) {
|
|
|
14064
14140
|
const pinLabel = config.repoAgentPaths.size > 0 ? "repo pin, +auto-merge" : "repo pin";
|
|
14065
14141
|
return `project+workspace ${path} (${pinLabel}) · rest ${globalPath}`;
|
|
14066
14142
|
}
|
|
14143
|
+
function limitsLine(config) {
|
|
14144
|
+
const { budget, review, worktree } = config.agent;
|
|
14145
|
+
const cap = (value, unit) => value < 0 ? "unlimited" : `${value} ${unit}`;
|
|
14146
|
+
const parts = [
|
|
14147
|
+
`${budget.maxAttemptsPerCard} attempts/card (budget.maxAttemptsPerCard)`,
|
|
14148
|
+
`${cap(budget.maxTurnsPerCard, "turns")}/card (budget.maxTurnsPerCard)`,
|
|
14149
|
+
`${cap(budget.maxRunsPerCard, "runs")}/card (budget.maxRunsPerCard)`,
|
|
14150
|
+
`${budget.dailyBudgetCents < 0 ? "unlimited" : `$${(budget.dailyBudgetCents / 100).toFixed(2)}`}/day (budget.dailyBudgetCents)`
|
|
14151
|
+
];
|
|
14152
|
+
if (review.enabled) {
|
|
14153
|
+
parts.push(`${review.maxReviewCycles} review cycles (review.maxReviewCycles)`);
|
|
14154
|
+
}
|
|
14155
|
+
parts.push(`failed branches ${worktree.failedAttemptRetentionDays > 0 ? `${worktree.failedAttemptRetentionDays}d` : "kept"} (worktree.failedAttemptRetentionDays)`);
|
|
14156
|
+
return parts.join(" · ");
|
|
14157
|
+
}
|
|
14067
14158
|
function titleRule(title) {
|
|
14068
14159
|
const prefix = "─── ";
|
|
14069
14160
|
const surround = ` `;
|
|
@@ -15020,6 +15111,7 @@ var exports_worktree_gc = {};
|
|
|
15020
15111
|
__export(exports_worktree_gc, {
|
|
15021
15112
|
runWorktreeGc: () => runWorktreeGc,
|
|
15022
15113
|
pruneFailedRemoteBranches: () => pruneFailedRemoteBranches,
|
|
15114
|
+
noteExpiringBranches: () => noteExpiringBranches,
|
|
15023
15115
|
isTransientGitNetworkError: () => isTransientGitNetworkError,
|
|
15024
15116
|
WorktreeGc: () => WorktreeGc
|
|
15025
15117
|
});
|
|
@@ -15151,6 +15243,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
15151
15243
|
scanned: 0,
|
|
15152
15244
|
removed: [],
|
|
15153
15245
|
skipped: [],
|
|
15246
|
+
expiring: [],
|
|
15154
15247
|
errors: []
|
|
15155
15248
|
};
|
|
15156
15249
|
if (!opts.prefix)
|
|
@@ -15209,6 +15302,9 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
15209
15302
|
result.scanned++;
|
|
15210
15303
|
if (!Number.isFinite(ts) || ts >= cutoffSecs) {
|
|
15211
15304
|
result.skipped.push(ref);
|
|
15305
|
+
if (Number.isFinite(ts) && ts < cutoffSecs + EXPIRY_NOTICE_SECS) {
|
|
15306
|
+
result.expiring.push(ref);
|
|
15307
|
+
}
|
|
15212
15308
|
continue;
|
|
15213
15309
|
}
|
|
15214
15310
|
if (clock() > sweepDeadline) {
|
|
@@ -15272,7 +15368,10 @@ class WorktreeGc {
|
|
|
15272
15368
|
}
|
|
15273
15369
|
if (this.remoteOpts) {
|
|
15274
15370
|
try {
|
|
15275
|
-
pruneFailedRemoteBranches(this.remoteOpts);
|
|
15371
|
+
const pruned = pruneFailedRemoteBranches(this.remoteOpts);
|
|
15372
|
+
if (pruned.expiring.length > 0) {
|
|
15373
|
+
this.remoteOpts.onExpiring?.(pruned.expiring);
|
|
15374
|
+
}
|
|
15276
15375
|
} catch (err) {
|
|
15277
15376
|
log48.warn(TAG44, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
15278
15377
|
}
|
|
@@ -15284,6 +15383,28 @@ class WorktreeGc {
|
|
|
15284
15383
|
}
|
|
15285
15384
|
}
|
|
15286
15385
|
}
|
|
15386
|
+
async function noteExpiringBranches(store, client, refs, retentionDays, notified) {
|
|
15387
|
+
for (const ref of refs) {
|
|
15388
|
+
if (notified.has(ref))
|
|
15389
|
+
continue;
|
|
15390
|
+
const owner = store.listCards().find((c) => c.daemonBranch === ref || c.failureHistory?.some((f) => f.recoveryBranch === ref));
|
|
15391
|
+
if (!owner)
|
|
15392
|
+
continue;
|
|
15393
|
+
notified.add(ref);
|
|
15394
|
+
try {
|
|
15395
|
+
await client.addComment(owner.cardId, [
|
|
15396
|
+
"**Failed-attempt branch expires within a day.**",
|
|
15397
|
+
`\`${ref}\` holds this card's unfinished commits. The daemon deletes failed-attempt branches after ${retentionDays} days (\`worktree.failedAttemptRetentionDays\`), and the next sweep removes this one.`,
|
|
15398
|
+
"To keep the work, check it out or open a PR from it now. To keep such branches longer, raise `worktree.failedAttemptRetentionDays` in the daemon config (0 turns the sweep off)."
|
|
15399
|
+
].join(`
|
|
15400
|
+
|
|
15401
|
+
`), { commentType: "summary" });
|
|
15402
|
+
} catch (err) {
|
|
15403
|
+
notified.delete(ref);
|
|
15404
|
+
log48.warn(TAG44, `Failed to post the expiring-branch note for ${ref}: ${err instanceof Error ? err.message : err}`);
|
|
15405
|
+
}
|
|
15406
|
+
}
|
|
15407
|
+
}
|
|
15287
15408
|
function getRepoRoot2() {
|
|
15288
15409
|
try {
|
|
15289
15410
|
return execFileSync8("git", [...GIT_NO_HOOKS9, "rev-parse", "--show-toplevel"], {
|
|
@@ -15293,7 +15414,7 @@ function getRepoRoot2() {
|
|
|
15293
15414
|
return null;
|
|
15294
15415
|
}
|
|
15295
15416
|
}
|
|
15296
|
-
var TAG44 = "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;
|
|
15417
|
+
var TAG44 = "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, EXPIRY_NOTICE_SECS;
|
|
15297
15418
|
var init_worktree_gc = __esm(() => {
|
|
15298
15419
|
init_run_gc();
|
|
15299
15420
|
GIT_NETWORK_EXEC = {
|
|
@@ -15318,6 +15439,7 @@ var init_worktree_gc = __esm(() => {
|
|
|
15318
15439
|
"enotfound",
|
|
15319
15440
|
"enetunreach"
|
|
15320
15441
|
].join("|"), "i");
|
|
15442
|
+
EXPIRY_NOTICE_SECS = 24 * 60 * 60;
|
|
15321
15443
|
});
|
|
15322
15444
|
|
|
15323
15445
|
// src/index.ts
|
|
@@ -15421,6 +15543,7 @@ async function main() {
|
|
|
15421
15543
|
validateSweepConfig(config.agent);
|
|
15422
15544
|
validateRankingConfig(config.agent);
|
|
15423
15545
|
validateRetentionConfig(config.agent);
|
|
15546
|
+
validateRunLimitsConfig(config.agent);
|
|
15424
15547
|
validateVerificationConfig(config.agent);
|
|
15425
15548
|
validateSizingConfig(config.agent);
|
|
15426
15549
|
validateHttpConfig(config.agent);
|
|
@@ -15518,9 +15641,15 @@ async function main() {
|
|
|
15518
15641
|
await settlePlanCriteria(client, "merge-monitor", completedCard);
|
|
15519
15642
|
};
|
|
15520
15643
|
}
|
|
15644
|
+
const expiringNotified = new Set;
|
|
15521
15645
|
const worktreeGc = new WorktreeGc(config.agent.worktree.basePath, stateStore, config.agent.timing.worktreeGcIntervalMs, config.agent.worktree.failedAttemptRetentionDays > 0 ? {
|
|
15522
15646
|
prefix: config.agent.worktree.failedBranchPrefix,
|
|
15523
|
-
retentionDays: config.agent.worktree.failedAttemptRetentionDays
|
|
15647
|
+
retentionDays: config.agent.worktree.failedAttemptRetentionDays,
|
|
15648
|
+
onExpiring: (refs) => {
|
|
15649
|
+
noteExpiringBranches(stateStore, client, refs, config.agent.worktree.failedAttemptRetentionDays, expiringNotified).catch((err) => {
|
|
15650
|
+
log49.warn(TAG45, `Expiring-branch notes failed: ${err}`);
|
|
15651
|
+
});
|
|
15652
|
+
}
|
|
15524
15653
|
} : undefined, config.agent.retention);
|
|
15525
15654
|
let boardReviewer = null;
|
|
15526
15655
|
if (config.agent.boardReview.enabled) {
|
|
@@ -15715,6 +15844,7 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
15715
15844
|
const column = columns.find((c) => c.id === card.column_id);
|
|
15716
15845
|
if (!column) {
|
|
15717
15846
|
log49.warn(TAG45, `Column not found for card ${cardId}`);
|
|
15847
|
+
await pool.noteSkip(cardId, "Not picked up — the card's column was not found on the board. Check the card is in a column of this project.");
|
|
15718
15848
|
return;
|
|
15719
15849
|
}
|
|
15720
15850
|
const route = classifyPickup(column.name, {
|
|
@@ -15723,6 +15853,8 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
15723
15853
|
});
|
|
15724
15854
|
if (!route) {
|
|
15725
15855
|
log49.info(TAG45, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
|
|
15856
|
+
const reviewColumns = config.agent.review.enabled ? config.agent.review.pickupColumns : [];
|
|
15857
|
+
await pool.noteSkip(card.id, `Not picked up — "${column.name}" is not a pickup column. Pickup: ${config.agent.pickupColumns.join(", ")} (\`pickupColumns\`)${reviewColumns.length > 0 ? `; review: ${reviewColumns.join(", ")} (\`review.pickupColumns\`)` : ""}. Move the card into one of them.`);
|
|
15726
15858
|
return;
|
|
15727
15859
|
}
|
|
15728
15860
|
const mode = route.mode;
|
|
@@ -15731,14 +15863,17 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
15731
15863
|
const subtasks = card.subtasks ?? [];
|
|
15732
15864
|
if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
|
|
15733
15865
|
log49.debug(TAG45, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
|
|
15866
|
+
await pool.noteSkip(card.id, `Not reviewed — the card already has the "${config.agent.review.approvedLabel}" label (\`review.approvedLabel\`). Remove the label to queue a review.`);
|
|
15734
15867
|
return;
|
|
15735
15868
|
}
|
|
15736
15869
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
15737
15870
|
log49.debug(TAG45, `Card #${card.short_id} has "${NEED_REVIEW_LABEL}" label (needs human) — skipping review`);
|
|
15871
|
+
await pool.noteSkip(card.id, `Not reviewed — the card has the "${NEED_REVIEW_LABEL}" label, so a person owns the next step. Remove the label to queue another review.`);
|
|
15738
15872
|
return;
|
|
15739
15873
|
}
|
|
15740
15874
|
if (mode === "review" && !qualifiesForAutoReview(card.description)) {
|
|
15741
15875
|
log49.info(TAG45, `Card #${card.short_id} has no branch or PR reference — skipping auto-review`);
|
|
15876
|
+
await pool.noteSkip(card.id, "Not reviewed — the card description has no `Branch:` line and no PR link, so there is nothing to review. Add one to queue a review.");
|
|
15742
15877
|
return;
|
|
15743
15878
|
}
|
|
15744
15879
|
await pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
@@ -16737,6 +16872,7 @@ async function doctorCommand() {
|
|
|
16737
16872
|
validateHttpConfig: validateHttpConfig2,
|
|
16738
16873
|
validateRankingConfig: validateRankingConfig2,
|
|
16739
16874
|
validateRetentionConfig: validateRetentionConfig2,
|
|
16875
|
+
validateRunLimitsConfig: validateRunLimitsConfig2,
|
|
16740
16876
|
validateSizingConfig: validateSizingConfig2,
|
|
16741
16877
|
validateSweepConfig: validateSweepConfig2,
|
|
16742
16878
|
validateVerificationConfig: validateVerificationConfig2
|
|
@@ -16754,6 +16890,7 @@ async function doctorCommand() {
|
|
|
16754
16890
|
validateSweepConfig2(config.agent);
|
|
16755
16891
|
validateRankingConfig2(config.agent);
|
|
16756
16892
|
validateRetentionConfig2(config.agent);
|
|
16893
|
+
validateRunLimitsConfig2(config.agent);
|
|
16757
16894
|
validateVerificationConfig2(config.agent);
|
|
16758
16895
|
validateSizingConfig2(config.agent);
|
|
16759
16896
|
validateHttpConfig2(config.agent);
|
package/dist/index.js
CHANGED
|
@@ -734,6 +734,18 @@ function formatDailyCap(cents) {
|
|
|
734
734
|
function formatTurnCap(turns) {
|
|
735
735
|
return turns < 0 ? "no cap" : `${turns} turns`;
|
|
736
736
|
}
|
|
737
|
+
function describeGuardDenial(reason, detail) {
|
|
738
|
+
switch (reason) {
|
|
739
|
+
case "max_attempts":
|
|
740
|
+
return `Stopped — ${detail} (\`budget.maxAttemptsPerCard\`). Reassign the card to grant a fresh attempt, or raise the limit in the daemon config.`;
|
|
741
|
+
case "card_turns":
|
|
742
|
+
return `Stopped — ${detail} (\`budget.maxTurnsPerCard\`). Reassign the card to clear the count, or raise the limit in the daemon config (-1 removes it).`;
|
|
743
|
+
case "card_runs":
|
|
744
|
+
return `Stopped — ${detail} (\`budget.maxRunsPerCard\`). Reassign the card to clear the count, or raise the limit in the daemon config (-1 removes it).`;
|
|
745
|
+
case "daily_budget":
|
|
746
|
+
return `Daily budget reached — waiting for the UTC reset (${detail}, \`budget.dailyBudgetCents\`). Raise the limit in the daemon config to continue today (-1 removes it).`;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
737
749
|
|
|
738
750
|
class BudgetGuard {
|
|
739
751
|
config;
|
|
@@ -2312,6 +2324,7 @@ var init_plan_phase = __esm(() => {
|
|
|
2312
2324
|
mode: "advisory",
|
|
2313
2325
|
model: "sonnet",
|
|
2314
2326
|
maxTurns: 40,
|
|
2327
|
+
timeoutMs: 10 * 60000,
|
|
2315
2328
|
postComment: true,
|
|
2316
2329
|
awaitingApprovalColumn: "To Do",
|
|
2317
2330
|
minComplexityScore: 3,
|
|
@@ -2372,6 +2385,9 @@ var init_types = __esm(() => {
|
|
|
2372
2385
|
},
|
|
2373
2386
|
scoreBands: DEFAULT_SCORE_BANDS,
|
|
2374
2387
|
sizingModel: "",
|
|
2388
|
+
sizingMaxTurns: 25,
|
|
2389
|
+
sizingMaxBudgetUsd: 0.75,
|
|
2390
|
+
sizingTimeoutMs: 240000,
|
|
2375
2391
|
researchRule: "shadow",
|
|
2376
2392
|
reviewModel: "sonnet",
|
|
2377
2393
|
deepReviewModel: "sonnet",
|
|
@@ -2474,6 +2490,10 @@ var init_types = __esm(() => {
|
|
|
2474
2490
|
runRecordDays: 14,
|
|
2475
2491
|
runLogDays: 30
|
|
2476
2492
|
},
|
|
2493
|
+
steering: {
|
|
2494
|
+
maxTurns: 15,
|
|
2495
|
+
maxIterations: 10
|
|
2496
|
+
},
|
|
2477
2497
|
planning: DEFAULT_PLANNING_CONFIG,
|
|
2478
2498
|
contractFirst: DEFAULT_CONTRACT_CONFIG,
|
|
2479
2499
|
boardReview: DEFAULT_BOARD_REVIEW_CONFIG,
|
|
@@ -2494,6 +2514,7 @@ __export(exports_config_validation, {
|
|
|
2494
2514
|
validateVerificationConfig: () => validateVerificationConfig,
|
|
2495
2515
|
validateSweepConfig: () => validateSweepConfig,
|
|
2496
2516
|
validateSizingConfig: () => validateSizingConfig,
|
|
2517
|
+
validateRunLimitsConfig: () => validateRunLimitsConfig,
|
|
2497
2518
|
validateRetentionConfig: () => validateRetentionConfig,
|
|
2498
2519
|
validateRankingConfig: () => validateRankingConfig,
|
|
2499
2520
|
validateHttpConfig: () => validateHttpConfig,
|
|
@@ -2825,6 +2846,41 @@ function validateRetentionConfig(config) {
|
|
|
2825
2846
|
` + ` Set a positive number of days, or a negative value to keep this store forever.`, [issue]);
|
|
2826
2847
|
}
|
|
2827
2848
|
}
|
|
2849
|
+
function validateRunLimitsConfig(config) {
|
|
2850
|
+
const wholeOrOff = [
|
|
2851
|
+
["planning.timeoutMs", config.planning.timeoutMs],
|
|
2852
|
+
["steering.maxTurns", config.steering.maxTurns],
|
|
2853
|
+
["steering.maxIterations", config.steering.maxIterations],
|
|
2854
|
+
["claude.sizingMaxTurns", config.claude.sizingMaxTurns],
|
|
2855
|
+
["claude.sizingTimeoutMs", config.claude.sizingTimeoutMs]
|
|
2856
|
+
];
|
|
2857
|
+
const issues = [];
|
|
2858
|
+
for (const [key, value] of wholeOrOff) {
|
|
2859
|
+
if (value === -1)
|
|
2860
|
+
continue;
|
|
2861
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
2862
|
+
issues.push(`${key}: ${value === 0 ? '0 is ambiguous — it could mean "none allowed" or "no limit"' : `${JSON.stringify(value)} is not a positive whole number`}`);
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
const budget = config.claude.sizingMaxBudgetUsd;
|
|
2866
|
+
if (budget !== -1 && !(Number.isFinite(budget) && budget > 0)) {
|
|
2867
|
+
issues.push(`claude.sizingMaxBudgetUsd: ${budget === 0 ? '0 is ambiguous — it could mean "no spend allowed" or "no limit"' : `${JSON.stringify(budget)} is not a positive number`}`);
|
|
2868
|
+
}
|
|
2869
|
+
for (const [key, value] of [
|
|
2870
|
+
["claude.maxTurns", config.claude.maxTurns],
|
|
2871
|
+
["claude.reviewMaxTurns", config.claude.reviewMaxTurns]
|
|
2872
|
+
]) {
|
|
2873
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
2874
|
+
issues.push(`${key}: ${JSON.stringify(value)} is not a positive whole number`);
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
if (issues.length > 0) {
|
|
2878
|
+
throw new ConfigValidationError(`Invalid agent config — run limits:
|
|
2879
|
+
- ${issues.join(`
|
|
2880
|
+
- `)}
|
|
2881
|
+
` + ` Set a positive value, or -1 to remove the limit (not available for claude.maxTurns and claude.reviewMaxTurns).`, issues);
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2828
2884
|
function validateRankingConfig(config) {
|
|
2829
2885
|
const issues = [];
|
|
2830
2886
|
const entries = Object.entries(config.ranking);
|
|
@@ -7183,9 +7239,11 @@ ${runLogTail}
|
|
|
7183
7239
|
} else {
|
|
7184
7240
|
await breakReviewLoopIfNeeded();
|
|
7185
7241
|
}
|
|
7242
|
+
const movedTo = handbackVerdict.proceed ? `I moved the card to "${config.review.moveToColumn}" instead of sending it back to "${config.review.failColumn}" again.` : "I left the card where it is instead of sending it back again.";
|
|
7186
7243
|
const body = [
|
|
7187
7244
|
"**Review — needs human review.**",
|
|
7188
|
-
`
|
|
7245
|
+
`This card reached the limit of ${maxCycles} review cycles (\`review.maxReviewCycles\`). ${movedTo}`,
|
|
7246
|
+
"Next: read the earlier review comments and fix or merge the branch by hand. To allow more automatic rounds, raise `review.maxReviewCycles` in the daemon config.",
|
|
7189
7247
|
result.summary || ""
|
|
7190
7248
|
].filter(Boolean).join(`
|
|
7191
7249
|
|
|
@@ -9901,6 +9959,7 @@ var init_workspace_model_config = __esm(() => {
|
|
|
9901
9959
|
// src/worker.ts
|
|
9902
9960
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
9903
9961
|
import {
|
|
9962
|
+
boundedTimerMs,
|
|
9904
9963
|
chooseImplementModel,
|
|
9905
9964
|
classifyRunError as classifyRunError2,
|
|
9906
9965
|
createWorktree,
|
|
@@ -10791,7 +10850,10 @@ ${prompt}`;
|
|
|
10791
10850
|
title: card.title,
|
|
10792
10851
|
description: card.description,
|
|
10793
10852
|
model,
|
|
10794
|
-
scoreBands: claude.scoreBands
|
|
10853
|
+
scoreBands: claude.scoreBands,
|
|
10854
|
+
maxTurns: claude.sizingMaxTurns < 0 ? null : claude.sizingMaxTurns,
|
|
10855
|
+
maxBudgetUsd: claude.sizingMaxBudgetUsd < 0 ? null : claude.sizingMaxBudgetUsd,
|
|
10856
|
+
timeoutMs: claude.sizingTimeoutMs < 0 ? null : claude.sizingTimeoutMs
|
|
10795
10857
|
});
|
|
10796
10858
|
if (outcome.status === "sized") {
|
|
10797
10859
|
log33.info(this.tag, `Sized #${card.short_id}: complexity ${outcome.sizing.complexity}/10 -> ${outcome.sizing.tier}`);
|
|
@@ -11030,6 +11092,10 @@ ${prompt}`;
|
|
|
11030
11092
|
}
|
|
11031
11093
|
}
|
|
11032
11094
|
}
|
|
11095
|
+
armPhaseTimer(onExpiry) {
|
|
11096
|
+
const { timeoutMs } = this.config.planning;
|
|
11097
|
+
return setTimeout(onExpiry, boundedTimerMs(timeoutMs < 0 ? this.config.maxTimeout : timeoutMs));
|
|
11098
|
+
}
|
|
11033
11099
|
async runPlanningPhase(enriched) {
|
|
11034
11100
|
const planning = this.config.planning;
|
|
11035
11101
|
const { card } = enriched;
|
|
@@ -11044,7 +11110,7 @@ ${prompt}`;
|
|
|
11044
11110
|
}).catch(() => {});
|
|
11045
11111
|
const planPrompt = buildPlanPrompt(enriched, this.worktreePath);
|
|
11046
11112
|
let planTimedOut = false;
|
|
11047
|
-
const planTimeout =
|
|
11113
|
+
const planTimeout = this.armPhaseTimer(() => {
|
|
11048
11114
|
planTimedOut = true;
|
|
11049
11115
|
log33.warn(this.tag, "Planning pass exceeded timeout — abandoning, implementing directly");
|
|
11050
11116
|
if (this.sdkRunner) {
|
|
@@ -11055,7 +11121,7 @@ ${prompt}`;
|
|
|
11055
11121
|
sigtermTimeoutMs: 5000
|
|
11056
11122
|
}).catch(() => {});
|
|
11057
11123
|
}
|
|
11058
|
-
}
|
|
11124
|
+
});
|
|
11059
11125
|
try {
|
|
11060
11126
|
await this.spawnClaude(planPrompt, card, [], {
|
|
11061
11127
|
model: planning.model,
|
|
@@ -11155,7 +11221,7 @@ ${prompt}`;
|
|
|
11155
11221
|
}).catch(() => {});
|
|
11156
11222
|
const contractPrompt = buildContractPrompt(enriched, this.worktreePath);
|
|
11157
11223
|
let contractTimedOut = false;
|
|
11158
|
-
const contractTimeout =
|
|
11224
|
+
const contractTimeout = this.armPhaseTimer(() => {
|
|
11159
11225
|
contractTimedOut = true;
|
|
11160
11226
|
log33.warn(this.tag, "Contract pass exceeded timeout — abandoning, implementing directly");
|
|
11161
11227
|
if (this.sdkRunner) {
|
|
@@ -11166,7 +11232,7 @@ ${prompt}`;
|
|
|
11166
11232
|
sigtermTimeoutMs: 5000
|
|
11167
11233
|
}).catch(() => {});
|
|
11168
11234
|
}
|
|
11169
|
-
}
|
|
11235
|
+
});
|
|
11170
11236
|
try {
|
|
11171
11237
|
await this.spawnClaude(contractPrompt, card, [], {
|
|
11172
11238
|
model: contractCfg.model,
|
|
@@ -11218,7 +11284,8 @@ ${prompt}`;
|
|
|
11218
11284
|
async drainSteeringMessages(card, subtasks) {
|
|
11219
11285
|
if (!this.cliSessionId || !this.sessionId || !this.cardId)
|
|
11220
11286
|
return;
|
|
11221
|
-
|
|
11287
|
+
const { maxIterations, maxTurns } = this.config.steering;
|
|
11288
|
+
for (let i = 0;(maxIterations < 0 || i < maxIterations) && !this.aborted; i++) {
|
|
11222
11289
|
let messages;
|
|
11223
11290
|
try {
|
|
11224
11291
|
const res = await this.client.getPendingUserMessages(this.cardId, this.sessionId, this.lastDrainedSeq);
|
|
@@ -11236,7 +11303,7 @@ ${prompt}`;
|
|
|
11236
11303
|
try {
|
|
11237
11304
|
await this.spawnClaude(buildSteeringPrompt(messages.map((m) => frameSteeringQuestion(m.text, m.questionCommentId))), card, subtasks, {
|
|
11238
11305
|
model: this.selectImplementModel(card),
|
|
11239
|
-
maxTurns:
|
|
11306
|
+
maxTurns: maxTurns < 0 ? undefined : maxTurns,
|
|
11240
11307
|
resumeSessionId: this.cliSessionId,
|
|
11241
11308
|
noCommitNotice: true,
|
|
11242
11309
|
...this.activeRunSpawnOpts ?? {}
|
|
@@ -11542,7 +11609,7 @@ ${prompt}`;
|
|
|
11542
11609
|
this.chargedTokens = emptyTokens();
|
|
11543
11610
|
}
|
|
11544
11611
|
}
|
|
11545
|
-
var TAG30 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4,
|
|
11612
|
+
var TAG30 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4, PLAN_ALLOWED_TOOLS = "Read,Grep,Glob,mcp__harmony__*", IMPLEMENT_ALLOWED_TOOLS = "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*";
|
|
11546
11613
|
var init_worker = __esm(() => {
|
|
11547
11614
|
init_src();
|
|
11548
11615
|
init_board_helpers();
|
|
@@ -11562,7 +11629,6 @@ var init_worker = __esm(() => {
|
|
|
11562
11629
|
init_transitions();
|
|
11563
11630
|
init_types();
|
|
11564
11631
|
init_workspace_model_config();
|
|
11565
|
-
PLAN_PHASE_TIMEOUT = 10 * 60000;
|
|
11566
11632
|
});
|
|
11567
11633
|
|
|
11568
11634
|
// src/workspace-model-cache.ts
|
|
@@ -11724,13 +11790,13 @@ class Pool {
|
|
|
11724
11790
|
if (mode === "implement") {
|
|
11725
11791
|
if (this.authPaused) {
|
|
11726
11792
|
log35.debug(TAG31, `#${card.short_id} held — agent paused (auth error)`);
|
|
11727
|
-
await this.emitWaiting(card.id, "Agent paused — Anthropic auth error
|
|
11793
|
+
await this.emitWaiting(card.id, "Agent paused — Anthropic auth error. Fix the API credentials, then restart the daemon.");
|
|
11728
11794
|
return;
|
|
11729
11795
|
}
|
|
11730
11796
|
const cooldownMs = this.apiCooldownRemainingMs();
|
|
11731
11797
|
if (cooldownMs > 0) {
|
|
11732
11798
|
log35.debug(TAG31, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
|
|
11733
|
-
await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying
|
|
11799
|
+
await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying after ${new Date(this.apiCooldownUntil).toISOString().slice(11, 16)} UTC. Nothing to change: the daemon resumes by itself.`);
|
|
11734
11800
|
return;
|
|
11735
11801
|
}
|
|
11736
11802
|
const decision = this.budget.check(card.id);
|
|
@@ -11739,6 +11805,7 @@ class Pool {
|
|
|
11739
11805
|
await this.denyDailyBudget(card, decision.detail);
|
|
11740
11806
|
} else {
|
|
11741
11807
|
log35.debug(TAG31, `#${card.short_id} skipped (${decision.reason}): ${decision.detail}`);
|
|
11808
|
+
await this.emitWaiting(card.id, describeGuardDenial(decision.reason, decision.detail));
|
|
11742
11809
|
}
|
|
11743
11810
|
return;
|
|
11744
11811
|
}
|
|
@@ -11746,6 +11813,7 @@ class Pool {
|
|
|
11746
11813
|
chainSuccessors = successors;
|
|
11747
11814
|
if (blockers === null) {
|
|
11748
11815
|
log35.warn(TAG31, `#${card.short_id} blocker check failed — deferring to next tick`);
|
|
11816
|
+
await this.emitWaiting(card.id, "Waiting — could not read this card's links to check for blockers. The daemon retries on the next tick.");
|
|
11749
11817
|
return;
|
|
11750
11818
|
}
|
|
11751
11819
|
if (blockers.length > 0) {
|
|
@@ -11779,7 +11847,7 @@ class Pool {
|
|
|
11779
11847
|
}
|
|
11780
11848
|
}
|
|
11781
11849
|
async denyDailyBudget(card, detail) {
|
|
11782
|
-
const reason =
|
|
11850
|
+
const reason = describeGuardDenial("daily_budget", detail);
|
|
11783
11851
|
const line = `#${card.short_id} skipped (daily_budget): ${detail}`;
|
|
11784
11852
|
if (this.lastWaitingEmit.get(card.id) === reason)
|
|
11785
11853
|
log35.debug(TAG31, line);
|
|
@@ -11787,6 +11855,11 @@ class Pool {
|
|
|
11787
11855
|
log35.warn(TAG31, line);
|
|
11788
11856
|
await this.emitWaiting(card.id, reason);
|
|
11789
11857
|
}
|
|
11858
|
+
async noteSkip(cardId, reason) {
|
|
11859
|
+
if (this.isCardKnown(cardId))
|
|
11860
|
+
return;
|
|
11861
|
+
await this.emitWaiting(cardId, reason);
|
|
11862
|
+
}
|
|
11790
11863
|
lastWaitingEmit = new Map;
|
|
11791
11864
|
async emitWaiting(cardId, currentTask) {
|
|
11792
11865
|
if (this.lastWaitingEmit.get(cardId) === currentTask)
|
|
@@ -13901,6 +13974,7 @@ var init_startup_model = __esm(() => {
|
|
|
13901
13974
|
// src/startup-banner.ts
|
|
13902
13975
|
var exports_startup_banner = {};
|
|
13903
13976
|
__export(exports_startup_banner, {
|
|
13977
|
+
limitsLine: () => limitsLine,
|
|
13904
13978
|
createStartupBanner: () => createStartupBanner,
|
|
13905
13979
|
contextSourceLine: () => contextSourceLine
|
|
13906
13980
|
});
|
|
@@ -13962,6 +14036,7 @@ function jsonBanner(config, version) {
|
|
|
13962
14036
|
log44.info(TAG40, `Harmony Agent Daemon v${version} starting...`);
|
|
13963
14037
|
log44.info(TAG40, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
|
|
13964
14038
|
log44.info(TAG40, `Config: ${contextSourceLine(config)}`);
|
|
14039
|
+
log44.info(TAG40, `Limits: ${limitsLine(config)}`);
|
|
13965
14040
|
if (config.agent.review.enabled) {
|
|
13966
14041
|
log44.info(TAG40, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
|
|
13967
14042
|
}
|
|
@@ -14041,6 +14116,7 @@ function configRows(config, projectName, gitProvider, httpPort, model) {
|
|
|
14041
14116
|
value: `${modelDesc} · ${poolDesc} · ${flowDesc}`
|
|
14042
14117
|
});
|
|
14043
14118
|
rows.push({ label: "Runner", value: runnerDesc(config.agent.runner) });
|
|
14119
|
+
rows.push({ label: "Limits", value: limitsLine(config) });
|
|
14044
14120
|
const tail = [];
|
|
14045
14121
|
if (gitProvider)
|
|
14046
14122
|
tail.push(gitProvider);
|
|
@@ -14063,6 +14139,21 @@ function contextSourceLine(config) {
|
|
|
14063
14139
|
const pinLabel = config.repoAgentPaths.size > 0 ? "repo pin, +auto-merge" : "repo pin";
|
|
14064
14140
|
return `project+workspace ${path} (${pinLabel}) · rest ${globalPath}`;
|
|
14065
14141
|
}
|
|
14142
|
+
function limitsLine(config) {
|
|
14143
|
+
const { budget, review, worktree } = config.agent;
|
|
14144
|
+
const cap = (value, unit) => value < 0 ? "unlimited" : `${value} ${unit}`;
|
|
14145
|
+
const parts = [
|
|
14146
|
+
`${budget.maxAttemptsPerCard} attempts/card (budget.maxAttemptsPerCard)`,
|
|
14147
|
+
`${cap(budget.maxTurnsPerCard, "turns")}/card (budget.maxTurnsPerCard)`,
|
|
14148
|
+
`${cap(budget.maxRunsPerCard, "runs")}/card (budget.maxRunsPerCard)`,
|
|
14149
|
+
`${budget.dailyBudgetCents < 0 ? "unlimited" : `$${(budget.dailyBudgetCents / 100).toFixed(2)}`}/day (budget.dailyBudgetCents)`
|
|
14150
|
+
];
|
|
14151
|
+
if (review.enabled) {
|
|
14152
|
+
parts.push(`${review.maxReviewCycles} review cycles (review.maxReviewCycles)`);
|
|
14153
|
+
}
|
|
14154
|
+
parts.push(`failed branches ${worktree.failedAttemptRetentionDays > 0 ? `${worktree.failedAttemptRetentionDays}d` : "kept"} (worktree.failedAttemptRetentionDays)`);
|
|
14155
|
+
return parts.join(" · ");
|
|
14156
|
+
}
|
|
14066
14157
|
function titleRule(title) {
|
|
14067
14158
|
const prefix = "─── ";
|
|
14068
14159
|
const surround = ` `;
|
|
@@ -15019,6 +15110,7 @@ var exports_worktree_gc = {};
|
|
|
15019
15110
|
__export(exports_worktree_gc, {
|
|
15020
15111
|
runWorktreeGc: () => runWorktreeGc,
|
|
15021
15112
|
pruneFailedRemoteBranches: () => pruneFailedRemoteBranches,
|
|
15113
|
+
noteExpiringBranches: () => noteExpiringBranches,
|
|
15022
15114
|
isTransientGitNetworkError: () => isTransientGitNetworkError,
|
|
15023
15115
|
WorktreeGc: () => WorktreeGc
|
|
15024
15116
|
});
|
|
@@ -15150,6 +15242,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
15150
15242
|
scanned: 0,
|
|
15151
15243
|
removed: [],
|
|
15152
15244
|
skipped: [],
|
|
15245
|
+
expiring: [],
|
|
15153
15246
|
errors: []
|
|
15154
15247
|
};
|
|
15155
15248
|
if (!opts.prefix)
|
|
@@ -15208,6 +15301,9 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
15208
15301
|
result.scanned++;
|
|
15209
15302
|
if (!Number.isFinite(ts) || ts >= cutoffSecs) {
|
|
15210
15303
|
result.skipped.push(ref);
|
|
15304
|
+
if (Number.isFinite(ts) && ts < cutoffSecs + EXPIRY_NOTICE_SECS) {
|
|
15305
|
+
result.expiring.push(ref);
|
|
15306
|
+
}
|
|
15211
15307
|
continue;
|
|
15212
15308
|
}
|
|
15213
15309
|
if (clock() > sweepDeadline) {
|
|
@@ -15271,7 +15367,10 @@ class WorktreeGc {
|
|
|
15271
15367
|
}
|
|
15272
15368
|
if (this.remoteOpts) {
|
|
15273
15369
|
try {
|
|
15274
|
-
pruneFailedRemoteBranches(this.remoteOpts);
|
|
15370
|
+
const pruned = pruneFailedRemoteBranches(this.remoteOpts);
|
|
15371
|
+
if (pruned.expiring.length > 0) {
|
|
15372
|
+
this.remoteOpts.onExpiring?.(pruned.expiring);
|
|
15373
|
+
}
|
|
15275
15374
|
} catch (err) {
|
|
15276
15375
|
log48.warn(TAG44, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
15277
15376
|
}
|
|
@@ -15283,6 +15382,28 @@ class WorktreeGc {
|
|
|
15283
15382
|
}
|
|
15284
15383
|
}
|
|
15285
15384
|
}
|
|
15385
|
+
async function noteExpiringBranches(store, client, refs, retentionDays, notified) {
|
|
15386
|
+
for (const ref of refs) {
|
|
15387
|
+
if (notified.has(ref))
|
|
15388
|
+
continue;
|
|
15389
|
+
const owner = store.listCards().find((c) => c.daemonBranch === ref || c.failureHistory?.some((f) => f.recoveryBranch === ref));
|
|
15390
|
+
if (!owner)
|
|
15391
|
+
continue;
|
|
15392
|
+
notified.add(ref);
|
|
15393
|
+
try {
|
|
15394
|
+
await client.addComment(owner.cardId, [
|
|
15395
|
+
"**Failed-attempt branch expires within a day.**",
|
|
15396
|
+
`\`${ref}\` holds this card's unfinished commits. The daemon deletes failed-attempt branches after ${retentionDays} days (\`worktree.failedAttemptRetentionDays\`), and the next sweep removes this one.`,
|
|
15397
|
+
"To keep the work, check it out or open a PR from it now. To keep such branches longer, raise `worktree.failedAttemptRetentionDays` in the daemon config (0 turns the sweep off)."
|
|
15398
|
+
].join(`
|
|
15399
|
+
|
|
15400
|
+
`), { commentType: "summary" });
|
|
15401
|
+
} catch (err) {
|
|
15402
|
+
notified.delete(ref);
|
|
15403
|
+
log48.warn(TAG44, `Failed to post the expiring-branch note for ${ref}: ${err instanceof Error ? err.message : err}`);
|
|
15404
|
+
}
|
|
15405
|
+
}
|
|
15406
|
+
}
|
|
15286
15407
|
function getRepoRoot2() {
|
|
15287
15408
|
try {
|
|
15288
15409
|
return execFileSync8("git", [...GIT_NO_HOOKS9, "rev-parse", "--show-toplevel"], {
|
|
@@ -15292,7 +15413,7 @@ function getRepoRoot2() {
|
|
|
15292
15413
|
return null;
|
|
15293
15414
|
}
|
|
15294
15415
|
}
|
|
15295
|
-
var TAG44 = "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;
|
|
15416
|
+
var TAG44 = "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, EXPIRY_NOTICE_SECS;
|
|
15296
15417
|
var init_worktree_gc = __esm(() => {
|
|
15297
15418
|
init_run_gc();
|
|
15298
15419
|
GIT_NETWORK_EXEC = {
|
|
@@ -15317,6 +15438,7 @@ var init_worktree_gc = __esm(() => {
|
|
|
15317
15438
|
"enotfound",
|
|
15318
15439
|
"enetunreach"
|
|
15319
15440
|
].join("|"), "i");
|
|
15441
|
+
EXPIRY_NOTICE_SECS = 24 * 60 * 60;
|
|
15320
15442
|
});
|
|
15321
15443
|
|
|
15322
15444
|
// src/index.ts
|
|
@@ -15420,6 +15542,7 @@ async function main() {
|
|
|
15420
15542
|
validateSweepConfig(config.agent);
|
|
15421
15543
|
validateRankingConfig(config.agent);
|
|
15422
15544
|
validateRetentionConfig(config.agent);
|
|
15545
|
+
validateRunLimitsConfig(config.agent);
|
|
15423
15546
|
validateVerificationConfig(config.agent);
|
|
15424
15547
|
validateSizingConfig(config.agent);
|
|
15425
15548
|
validateHttpConfig(config.agent);
|
|
@@ -15517,9 +15640,15 @@ async function main() {
|
|
|
15517
15640
|
await settlePlanCriteria(client, "merge-monitor", completedCard);
|
|
15518
15641
|
};
|
|
15519
15642
|
}
|
|
15643
|
+
const expiringNotified = new Set;
|
|
15520
15644
|
const worktreeGc = new WorktreeGc(config.agent.worktree.basePath, stateStore, config.agent.timing.worktreeGcIntervalMs, config.agent.worktree.failedAttemptRetentionDays > 0 ? {
|
|
15521
15645
|
prefix: config.agent.worktree.failedBranchPrefix,
|
|
15522
|
-
retentionDays: config.agent.worktree.failedAttemptRetentionDays
|
|
15646
|
+
retentionDays: config.agent.worktree.failedAttemptRetentionDays,
|
|
15647
|
+
onExpiring: (refs) => {
|
|
15648
|
+
noteExpiringBranches(stateStore, client, refs, config.agent.worktree.failedAttemptRetentionDays, expiringNotified).catch((err) => {
|
|
15649
|
+
log49.warn(TAG45, `Expiring-branch notes failed: ${err}`);
|
|
15650
|
+
});
|
|
15651
|
+
}
|
|
15523
15652
|
} : undefined, config.agent.retention);
|
|
15524
15653
|
let boardReviewer = null;
|
|
15525
15654
|
if (config.agent.boardReview.enabled) {
|
|
@@ -15714,6 +15843,7 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
15714
15843
|
const column = columns.find((c) => c.id === card.column_id);
|
|
15715
15844
|
if (!column) {
|
|
15716
15845
|
log49.warn(TAG45, `Column not found for card ${cardId}`);
|
|
15846
|
+
await pool.noteSkip(cardId, "Not picked up — the card's column was not found on the board. Check the card is in a column of this project.");
|
|
15717
15847
|
return;
|
|
15718
15848
|
}
|
|
15719
15849
|
const route = classifyPickup(column.name, {
|
|
@@ -15722,6 +15852,8 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
15722
15852
|
});
|
|
15723
15853
|
if (!route) {
|
|
15724
15854
|
log49.info(TAG45, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
|
|
15855
|
+
const reviewColumns = config.agent.review.enabled ? config.agent.review.pickupColumns : [];
|
|
15856
|
+
await pool.noteSkip(card.id, `Not picked up — "${column.name}" is not a pickup column. Pickup: ${config.agent.pickupColumns.join(", ")} (\`pickupColumns\`)${reviewColumns.length > 0 ? `; review: ${reviewColumns.join(", ")} (\`review.pickupColumns\`)` : ""}. Move the card into one of them.`);
|
|
15725
15857
|
return;
|
|
15726
15858
|
}
|
|
15727
15859
|
const mode = route.mode;
|
|
@@ -15730,14 +15862,17 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
15730
15862
|
const subtasks = card.subtasks ?? [];
|
|
15731
15863
|
if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
|
|
15732
15864
|
log49.debug(TAG45, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
|
|
15865
|
+
await pool.noteSkip(card.id, `Not reviewed — the card already has the "${config.agent.review.approvedLabel}" label (\`review.approvedLabel\`). Remove the label to queue a review.`);
|
|
15733
15866
|
return;
|
|
15734
15867
|
}
|
|
15735
15868
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
15736
15869
|
log49.debug(TAG45, `Card #${card.short_id} has "${NEED_REVIEW_LABEL}" label (needs human) — skipping review`);
|
|
15870
|
+
await pool.noteSkip(card.id, `Not reviewed — the card has the "${NEED_REVIEW_LABEL}" label, so a person owns the next step. Remove the label to queue another review.`);
|
|
15737
15871
|
return;
|
|
15738
15872
|
}
|
|
15739
15873
|
if (mode === "review" && !qualifiesForAutoReview(card.description)) {
|
|
15740
15874
|
log49.info(TAG45, `Card #${card.short_id} has no branch or PR reference — skipping auto-review`);
|
|
15875
|
+
await pool.noteSkip(card.id, "Not reviewed — the card description has no `Branch:` line and no PR link, so there is nothing to review. Add one to queue a review.");
|
|
15741
15876
|
return;
|
|
15742
15877
|
}
|
|
15743
15878
|
await pool.enqueue(card, column, cardLabels, subtasks, mode);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/agent",
|
|
3
|
-
"version": "1.49.
|
|
3
|
+
"version": "1.49.6",
|
|
4
4
|
"description": "Push-based agent daemon for Harmony — picks up assigned cards, builds them in isolated git worktrees, and opens PRs. It runs where you put it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@anthropic-ai/claude-agent-sdk": "^0.3.278",
|
|
48
|
-
"@gethmy/harness": "1.19.
|
|
48
|
+
"@gethmy/harness": "1.19.13",
|
|
49
49
|
"@gethmy/mcp": "3.23.1",
|
|
50
50
|
"@supabase/supabase-js": "2.95.3"
|
|
51
51
|
},
|