@gethmy/agent 1.6.1 → 1.7.1
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 +10 -2
- package/dist/budget.d.ts +20 -28
- package/dist/budget.js +24 -112
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -64
- package/dist/completion.d.ts +5 -1
- package/dist/completion.js +20 -2
- package/dist/episode-writer.d.ts +32 -0
- package/dist/episode-writer.js +120 -3
- package/dist/git-diff-stat.d.ts +24 -0
- package/dist/git-diff-stat.js +56 -0
- package/dist/http-server.d.ts +1 -14
- package/dist/http-server.js +1 -19
- package/dist/index.js +4 -10
- package/dist/pool.d.ts +4 -3
- package/dist/pool.js +28 -23
- package/dist/progress-tracker.d.ts +3 -0
- package/dist/progress-tracker.js +15 -0
- package/dist/prompt.d.ts +5 -0
- package/dist/prompt.js +44 -1
- package/dist/review-completion.d.ts +0 -5
- package/dist/review-completion.js +63 -62
- package/dist/startup-banner.js +1 -1
- package/dist/state-store.d.ts +8 -7
- package/dist/state-store.js +14 -23
- package/dist/types.d.ts +35 -6
- package/dist/types.js +2 -4
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +47 -4
- package/package.json +1 -1
|
@@ -32,9 +32,4 @@ export interface ReviewResult {
|
|
|
32
32
|
* bouncing it to To Do for a parse failure that isn't a code quality signal.
|
|
33
33
|
*/
|
|
34
34
|
export declare function parseReviewOutput(stdout: string): ReviewResult;
|
|
35
|
-
/**
|
|
36
|
-
* Post-review completion pipeline.
|
|
37
|
-
* Handles approved/rejected verdicts, creates subtasks for findings,
|
|
38
|
-
* and moves the card to the appropriate column.
|
|
39
|
-
*/
|
|
40
35
|
export declare function runReviewCompletion(client: HarmonyApiClient, card: Card, result: ReviewResult, config: AgentConfig, worktreePath: string, branchName: string | null, sessionStats: SessionStats | null | undefined, runLogPath: string | null | undefined, workspaceId: string | undefined, agentSessionId: string | null | undefined, stateStore: StateStore): Promise<void>;
|
|
@@ -183,6 +183,18 @@ function stripReviewSummary(description) {
|
|
|
183
183
|
* Handles approved/rejected verdicts, creates subtasks for findings,
|
|
184
184
|
* and moves the card to the appropriate column.
|
|
185
185
|
*/
|
|
186
|
+
/**
|
|
187
|
+
* Post a review verdict as a typed comment instead of mutating the card
|
|
188
|
+
* description (card-comments plan, Phase 3). Best-effort — never throws.
|
|
189
|
+
*/
|
|
190
|
+
async function postReviewComment(client, card, commentType, body) {
|
|
191
|
+
try {
|
|
192
|
+
await client.addComment(card.id, body, { commentType });
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
log.error(TAG, `Failed to post review comment to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
186
198
|
export async function runReviewCompletion(client, card, result, config, worktreePath, branchName, sessionStats, runLogPath, workspaceId, agentSessionId, stateStore) {
|
|
187
199
|
// Re-fetch card for fresh description (avoids stale data from enqueue time)
|
|
188
200
|
let freshDesc;
|
|
@@ -208,32 +220,28 @@ export async function runReviewCompletion(client, card, result, config, worktree
|
|
|
208
220
|
log.warn(TAG, `Failed to add "${NEED_REVIEW_LABEL}" label: ${err instanceof Error ? err.message : err}`);
|
|
209
221
|
}
|
|
210
222
|
if (config.review.postFindings) {
|
|
211
|
-
const baseDesc = stripReviewSummary(freshDesc);
|
|
212
223
|
const rawTail = runLogPath ? tailRunLog(runLogPath) : null;
|
|
213
224
|
// Log content routinely contains ```json fences from Claude's own
|
|
214
|
-
// output; embedding it inside a 3-backtick fence would break
|
|
215
|
-
//
|
|
225
|
+
// output; embedding it inside a 3-backtick fence would break markdown.
|
|
226
|
+
// Use a 4-backtick fence and downgrade any 4+-backtick runs.
|
|
216
227
|
const runLogTail = rawTail
|
|
217
|
-
? rawTail.replace(/`{4,}/g, (
|
|
228
|
+
? rawTail.replace(/`{4,}/g, () => "`".repeat(3))
|
|
218
229
|
: null;
|
|
219
230
|
const runLogHint = runLogPath
|
|
220
|
-
?
|
|
221
|
-
: "
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
'
|
|
231
|
+
? `Run log: \`${runLogPath}\``
|
|
232
|
+
: "Run log: (not captured)";
|
|
233
|
+
const body = [
|
|
234
|
+
"**Review — parse error.**",
|
|
235
|
+
'The review agent\'s output could not be parsed. Card stays in Review with the "Need Review" label — inspect the run log below to diagnose.',
|
|
225
236
|
runLogHint,
|
|
226
|
-
result.summary ?
|
|
237
|
+
result.summary ? `Raw output (truncated):\n${result.summary}` : "",
|
|
227
238
|
runLogTail
|
|
228
|
-
?
|
|
239
|
+
? `Run log tail (last ${RUN_LOG_TAIL_BYTES}B):\n\`\`\`\`\n${runLogTail}\n\`\`\`\``
|
|
229
240
|
: "",
|
|
230
|
-
]
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
catch (err) {
|
|
235
|
-
log.error(TAG, `Failed to update description: ${err instanceof Error ? err.message : err}`);
|
|
236
|
-
}
|
|
241
|
+
]
|
|
242
|
+
.filter(Boolean)
|
|
243
|
+
.join("\n\n");
|
|
244
|
+
await postReviewComment(client, card, "blocker", body);
|
|
237
245
|
}
|
|
238
246
|
await client.endAgentSession(card.id, {
|
|
239
247
|
status: "paused",
|
|
@@ -276,30 +284,23 @@ export async function runReviewCompletion(client, card, result, config, worktree
|
|
|
276
284
|
}
|
|
277
285
|
// Add "Ready to Merge" label
|
|
278
286
|
await addLabelByName(client, card, config.review.approvedLabel, config.review.approvedLabelColor);
|
|
279
|
-
// Post approval
|
|
287
|
+
// Post the approval verdict as a decision comment (card stays in Review).
|
|
280
288
|
if (config.review.postFindings) {
|
|
281
|
-
const baseDesc = stripReviewSummary(freshDesc).replace(/\n\nReview cycle:\s*\d+\/\d+/, "");
|
|
282
289
|
const scopeLine = result.scopeCheck
|
|
283
|
-
?
|
|
290
|
+
? `Scope: ${result.scopeCheck.status}${result.scopeCheck.notes ? ` — ${result.scopeCheck.notes}` : ""}`
|
|
284
291
|
: "";
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
result.summary
|
|
292
|
+
const body = [
|
|
293
|
+
"**Review — approved.**",
|
|
294
|
+
result.summary || "",
|
|
288
295
|
scopeLine,
|
|
289
296
|
result.findings.length > 0
|
|
290
|
-
?
|
|
297
|
+
? `${result.findings.length} minor finding(s) noted.`
|
|
291
298
|
: "",
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
await client.updateCard(card.id, { description: baseDesc + summary });
|
|
299
|
-
}
|
|
300
|
-
catch (err) {
|
|
301
|
-
log.error(TAG, `Failed to update description: ${err instanceof Error ? err.message : err}`);
|
|
302
|
-
}
|
|
299
|
+
prUrl ? `PR: ${prUrl}` : "",
|
|
300
|
+
]
|
|
301
|
+
.filter(Boolean)
|
|
302
|
+
.join("\n\n");
|
|
303
|
+
await postReviewComment(client, card, "decision", body);
|
|
303
304
|
}
|
|
304
305
|
await client.endAgentSession(card.id, {
|
|
305
306
|
status: "completed",
|
|
@@ -324,18 +325,14 @@ export async function runReviewCompletion(client, card, result, config, worktree
|
|
|
324
325
|
if (currentCycle >= maxCycles) {
|
|
325
326
|
log.warn(TAG, `#${card.short_id} reached max review cycles (${maxCycles}), moving to Done with note`);
|
|
326
327
|
await moveCardToColumn(client, card, config.review.moveToColumn);
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
catch (err) {
|
|
337
|
-
log.error(TAG, `Failed to update description: ${err instanceof Error ? err.message : err}`);
|
|
338
|
-
}
|
|
328
|
+
const body = [
|
|
329
|
+
"**Review — needs human review.**",
|
|
330
|
+
`Reached max review cycles (${maxCycles}). Please review manually.`,
|
|
331
|
+
result.summary || "",
|
|
332
|
+
]
|
|
333
|
+
.filter(Boolean)
|
|
334
|
+
.join("\n\n");
|
|
335
|
+
await postReviewComment(client, card, "blocker", body);
|
|
339
336
|
await client.endAgentSession(card.id, {
|
|
340
337
|
status: "completed",
|
|
341
338
|
...buildTokenPayload(sessionStats),
|
|
@@ -396,26 +393,30 @@ export async function runReviewCompletion(client, card, result, config, worktree
|
|
|
396
393
|
log.error(TAG, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
|
|
397
394
|
}
|
|
398
395
|
}));
|
|
399
|
-
//
|
|
396
|
+
// The review cycle counter stays in the description — it is functional
|
|
397
|
+
// state used to enforce maxReviewCycles, not narrative. (This also strips
|
|
398
|
+
// any legacy summary block from the description.) The review narrative
|
|
399
|
+
// goes to a summary comment instead.
|
|
400
400
|
const baseDesc = stripReviewSummary(freshDesc);
|
|
401
401
|
const updatedDesc = updateReviewCycleMarker(baseDesc, currentCycle, maxCycles);
|
|
402
|
-
const scopeLine = result.scopeCheck
|
|
403
|
-
? `\nScope: ${result.scopeCheck.status}${result.scopeCheck.notes ? ` — ${result.scopeCheck.notes}` : ""}`
|
|
404
|
-
: "";
|
|
405
|
-
const summary = [
|
|
406
|
-
`\n\n${REVIEW_MARKER} Rejected**`,
|
|
407
|
-
result.summary ? `\n${result.summary}` : "",
|
|
408
|
-
scopeLine,
|
|
409
|
-
`\n${criticalFindings.length} critical, ${majorFindings.length} major, ${minorFindings.length} minor finding(s).`,
|
|
410
|
-
].join("");
|
|
411
402
|
try {
|
|
412
|
-
await client.updateCard(card.id, {
|
|
413
|
-
description: updatedDesc + summary,
|
|
414
|
-
});
|
|
403
|
+
await client.updateCard(card.id, { description: updatedDesc });
|
|
415
404
|
}
|
|
416
405
|
catch (err) {
|
|
417
|
-
log.error(TAG, `Failed to update
|
|
406
|
+
log.error(TAG, `Failed to update review cycle marker: ${err instanceof Error ? err.message : err}`);
|
|
418
407
|
}
|
|
408
|
+
const scopeLine = result.scopeCheck
|
|
409
|
+
? `Scope: ${result.scopeCheck.status}${result.scopeCheck.notes ? ` — ${result.scopeCheck.notes}` : ""}`
|
|
410
|
+
: "";
|
|
411
|
+
const body = [
|
|
412
|
+
"**Review — rejected.**",
|
|
413
|
+
result.summary || "",
|
|
414
|
+
scopeLine,
|
|
415
|
+
`${criticalFindings.length} critical, ${majorFindings.length} major, ${minorFindings.length} minor finding(s).`,
|
|
416
|
+
]
|
|
417
|
+
.filter(Boolean)
|
|
418
|
+
.join("\n\n");
|
|
419
|
+
await postReviewComment(client, card, "summary", body);
|
|
419
420
|
}
|
|
420
421
|
// Move back to failColumn (To Do) for re-implementation
|
|
421
422
|
await moveCardToColumn(client, card, config.review.failColumn);
|
package/dist/startup-banner.js
CHANGED
|
@@ -104,7 +104,7 @@ function configRows(config, projectName, gitProvider) {
|
|
|
104
104
|
rows.push({ label: "User", value: config.userEmail });
|
|
105
105
|
const reviewEnabled = config.agent.review.enabled;
|
|
106
106
|
const poolDesc = reviewEnabled
|
|
107
|
-
? `Pool ${config.agent.poolSize} impl +
|
|
107
|
+
? `Pool ${config.agent.poolSize} impl + ${config.agent.review.poolSize} review`
|
|
108
108
|
: `Pool ${config.agent.poolSize} impl`;
|
|
109
109
|
const flowDesc = reviewEnabled
|
|
110
110
|
? `Pickup ${config.agent.pickupColumns[0]} → ${config.agent.completion.moveToColumn} → ${config.agent.review.moveToColumn}`
|
package/dist/state-store.d.ts
CHANGED
|
@@ -28,8 +28,6 @@ export interface CardRecord {
|
|
|
28
28
|
cardId: string;
|
|
29
29
|
attempts: number;
|
|
30
30
|
totalCostCents: number;
|
|
31
|
-
dlq: boolean;
|
|
32
|
-
dlqReason?: string;
|
|
33
31
|
lastAttemptAt: number | null;
|
|
34
32
|
lastOutcome: "success" | "failure" | null;
|
|
35
33
|
failureHistory?: FailureSummaryRecord[];
|
|
@@ -70,9 +68,16 @@ export declare class StateStore {
|
|
|
70
68
|
getCard(cardId: string): CardRecord | null;
|
|
71
69
|
incrementAttempt(cardId: string): Promise<number>;
|
|
72
70
|
recordOutcome(cardId: string, outcome: "success" | "failure"): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Reset a card's attempt counter so a card that previously gave up
|
|
73
|
+
* (attempts >= maxAttemptsPerCard) becomes eligible again. Called when a
|
|
74
|
+
* card is unassigned/reassigned — reassignment is the human's "try again".
|
|
75
|
+
* No-op for an unknown card.
|
|
76
|
+
*/
|
|
77
|
+
resetAttempts(cardId: string): Promise<void>;
|
|
73
78
|
/**
|
|
74
79
|
* Push a failure summary onto the card's bounded history (most-recent first,
|
|
75
|
-
* capped at 5). Read back by
|
|
80
|
+
* capped at 5). Read back by the give-up comment and the Agent History
|
|
76
81
|
* UI section to give users a post-mortem trail across attempts.
|
|
77
82
|
*/
|
|
78
83
|
recordFailureSummary(cardId: string, entry: Omit<FailureSummaryRecord, "ts"> & {
|
|
@@ -81,8 +86,4 @@ export declare class StateStore {
|
|
|
81
86
|
getRecentFailures(cardId: string, limit?: number): FailureSummaryRecord[];
|
|
82
87
|
addCost(cardId: string, cents: number): Promise<void>;
|
|
83
88
|
getDailyCostCents(date?: string): number;
|
|
84
|
-
markDlq(cardId: string, reason: string): Promise<void>;
|
|
85
|
-
clearDlq(cardId: string): Promise<void>;
|
|
86
|
-
isDlq(cardId: string): boolean;
|
|
87
|
-
listDlq(): CardRecord[];
|
|
88
89
|
}
|
package/dist/state-store.js
CHANGED
|
@@ -145,7 +145,6 @@ export class StateStore {
|
|
|
145
145
|
cardId,
|
|
146
146
|
attempts: 0,
|
|
147
147
|
totalCostCents: 0,
|
|
148
|
-
dlq: false,
|
|
149
148
|
lastAttemptAt: null,
|
|
150
149
|
lastOutcome: null,
|
|
151
150
|
};
|
|
@@ -170,9 +169,22 @@ export class StateStore {
|
|
|
170
169
|
rec.attempts = 0;
|
|
171
170
|
await this.persist();
|
|
172
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Reset a card's attempt counter so a card that previously gave up
|
|
174
|
+
* (attempts >= maxAttemptsPerCard) becomes eligible again. Called when a
|
|
175
|
+
* card is unassigned/reassigned — reassignment is the human's "try again".
|
|
176
|
+
* No-op for an unknown card.
|
|
177
|
+
*/
|
|
178
|
+
async resetAttempts(cardId) {
|
|
179
|
+
const rec = this.getCard(cardId);
|
|
180
|
+
if (!rec || rec.attempts === 0)
|
|
181
|
+
return;
|
|
182
|
+
rec.attempts = 0;
|
|
183
|
+
await this.persist();
|
|
184
|
+
}
|
|
173
185
|
/**
|
|
174
186
|
* Push a failure summary onto the card's bounded history (most-recent first,
|
|
175
|
-
* capped at 5). Read back by
|
|
187
|
+
* capped at 5). Read back by the give-up comment and the Agent History
|
|
176
188
|
* UI section to give users a post-mortem trail across attempts.
|
|
177
189
|
*/
|
|
178
190
|
async recordFailureSummary(cardId, entry) {
|
|
@@ -215,25 +227,4 @@ export class StateStore {
|
|
|
215
227
|
const key = date ?? todayUtc();
|
|
216
228
|
return this.state.daily.find((d) => d.date === key)?.costCents ?? 0;
|
|
217
229
|
}
|
|
218
|
-
// ---------- DLQ ----------
|
|
219
|
-
async markDlq(cardId, reason) {
|
|
220
|
-
const rec = this.ensureCard(cardId);
|
|
221
|
-
rec.dlq = true;
|
|
222
|
-
rec.dlqReason = reason;
|
|
223
|
-
await this.persist();
|
|
224
|
-
}
|
|
225
|
-
async clearDlq(cardId) {
|
|
226
|
-
const rec = this.getCard(cardId);
|
|
227
|
-
if (!rec)
|
|
228
|
-
return;
|
|
229
|
-
rec.dlq = false;
|
|
230
|
-
delete rec.dlqReason;
|
|
231
|
-
await this.persist();
|
|
232
|
-
}
|
|
233
|
-
isDlq(cardId) {
|
|
234
|
-
return this.getCard(cardId)?.dlq === true;
|
|
235
|
-
}
|
|
236
|
-
listDlq() {
|
|
237
|
-
return this.state.cards.filter((c) => c.dlq);
|
|
238
|
-
}
|
|
239
230
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface AgentConfig {
|
|
|
40
40
|
};
|
|
41
41
|
review: {
|
|
42
42
|
enabled: boolean;
|
|
43
|
+
/** Concurrent review workers. Each gets its own dev-server port slot. */
|
|
44
|
+
poolSize: number;
|
|
43
45
|
pickupColumns: string[];
|
|
44
46
|
moveToColumn: string;
|
|
45
47
|
failColumn: string;
|
|
@@ -55,15 +57,13 @@ export interface AgentConfig {
|
|
|
55
57
|
mergedLabelColor: string;
|
|
56
58
|
};
|
|
57
59
|
budget: {
|
|
58
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Max implement attempts per card before the daemon gives up (resets on
|
|
62
|
+
* success and on reassign). Repeated verification failures count.
|
|
63
|
+
*/
|
|
59
64
|
maxAttemptsPerCard: number;
|
|
60
|
-
/** Max cumulative spend per card, in cents, before DLQ. */
|
|
61
|
-
maxCentsPerCard: number;
|
|
62
65
|
/** Daily spend cap, in cents (UTC day). Exceeded → pause pickups. */
|
|
63
66
|
dailyBudgetCents: number;
|
|
64
|
-
/** Label applied to DLQ'd cards. */
|
|
65
|
-
dlqLabel: string;
|
|
66
|
-
dlqLabelColor: string;
|
|
67
67
|
};
|
|
68
68
|
http: {
|
|
69
69
|
/** Local HTTP status/control server. Bound to 127.0.0.1 by default. */
|
|
@@ -129,6 +129,35 @@ export interface EpisodeMeta {
|
|
|
129
129
|
files_touched: number;
|
|
130
130
|
num_turns: number;
|
|
131
131
|
error?: string;
|
|
132
|
+
/**
|
|
133
|
+
* Changed file paths for the run (#272). Capped (default first 30). Prefer
|
|
134
|
+
* the diff's authoritative list; falls back to ProgressTracker-tracked edit
|
|
135
|
+
* paths when a diff is unavailable.
|
|
136
|
+
*/
|
|
137
|
+
changed_files?: string[];
|
|
138
|
+
/** Line churn for the run (#272), best-effort from `git diff --numstat`. */
|
|
139
|
+
churn?: {
|
|
140
|
+
insertions: number;
|
|
141
|
+
deletions: number;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Cheap deterministic root-cause / gotcha line extracted from the run's
|
|
145
|
+
* assistant text (#272). No LLM call — left unset when nothing matches.
|
|
146
|
+
*/
|
|
147
|
+
key_insight?: string;
|
|
148
|
+
/**
|
|
149
|
+
* Provenance record (#273). Agent episodes are always `source: 'agent-run'`.
|
|
150
|
+
* Mirrors the `MemoryOrigin` shape stamped by `harmony_remember`; lives in
|
|
151
|
+
* metadata jsonb (no dedicated column). Input for provenance badges and
|
|
152
|
+
* auto-vs-curated hygiene filtering.
|
|
153
|
+
*/
|
|
154
|
+
origin?: {
|
|
155
|
+
source: "manual" | "assistant" | "agent-run" | "import";
|
|
156
|
+
source_card_id?: string;
|
|
157
|
+
source_session_id?: string;
|
|
158
|
+
author?: string;
|
|
159
|
+
source_trust?: string;
|
|
160
|
+
};
|
|
132
161
|
/** Provenance only — never used as memory scope. */
|
|
133
162
|
agent_session_id?: string;
|
|
134
163
|
/** Set on back-fill from review pipeline. */
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const DEFAULT_AGENT_CONFIG = {
|
|
2
|
-
poolSize:
|
|
2
|
+
poolSize: 3,
|
|
3
3
|
maxTimeout: 1_800_000, // 30 minutes
|
|
4
4
|
pickupColumns: ["To Do"],
|
|
5
5
|
priorityLabels: { urgent: 100, critical: 90, bug: 50 },
|
|
@@ -35,6 +35,7 @@ export const DEFAULT_AGENT_CONFIG = {
|
|
|
35
35
|
},
|
|
36
36
|
review: {
|
|
37
37
|
enabled: true,
|
|
38
|
+
poolSize: 2,
|
|
38
39
|
pickupColumns: ["Review"],
|
|
39
40
|
moveToColumn: "Done",
|
|
40
41
|
failColumn: "To Do",
|
|
@@ -51,10 +52,7 @@ export const DEFAULT_AGENT_CONFIG = {
|
|
|
51
52
|
},
|
|
52
53
|
budget: {
|
|
53
54
|
maxAttemptsPerCard: 3,
|
|
54
|
-
maxCentsPerCard: 500, // $5.00
|
|
55
55
|
dailyBudgetCents: 5000, // $50.00
|
|
56
|
-
dlqLabel: "dlq",
|
|
57
|
-
dlqLabelColor: "#dc2626",
|
|
58
56
|
},
|
|
59
57
|
http: {
|
|
60
58
|
enabled: true,
|
package/dist/worker.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export declare class Worker {
|
|
|
21
21
|
private progressTracker;
|
|
22
22
|
private lastSessionStats;
|
|
23
23
|
private aborted;
|
|
24
|
+
private verificationFailed;
|
|
24
25
|
private sessionId;
|
|
25
26
|
private runId;
|
|
26
27
|
constructor(id: number, config: AgentConfig, client: HarmonyApiClient, _userEmail: string, onDone: (worker: Worker) => void, workspaceId: string, projectId: string, stateStore: StateStore);
|
package/dist/worker.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { moveCardAndAddLabel } from "./board-helpers.js";
|
|
2
|
+
import { buildGaveUpComment } from "./budget.js";
|
|
2
3
|
import { buildTokenPayload, runCompletion, } from "./completion.js";
|
|
3
4
|
import { log } from "./log.js";
|
|
4
5
|
import { signalGroup, spawnInGroup, terminateGroup } from "./process-group.js";
|
|
@@ -32,6 +33,7 @@ export class Worker {
|
|
|
32
33
|
progressTracker = null;
|
|
33
34
|
lastSessionStats;
|
|
34
35
|
aborted = false;
|
|
36
|
+
verificationFailed = false;
|
|
35
37
|
sessionId = null;
|
|
36
38
|
runId = null;
|
|
37
39
|
constructor(id, config, client, _userEmail, onDone, workspaceId, projectId, stateStore) {
|
|
@@ -96,6 +98,7 @@ export class Worker {
|
|
|
96
98
|
*/
|
|
97
99
|
async run(card, column, labels, subtasks) {
|
|
98
100
|
this.aborted = false;
|
|
101
|
+
this.verificationFailed = false;
|
|
99
102
|
this.cardId = card.id;
|
|
100
103
|
this.startedAt = Date.now();
|
|
101
104
|
this.runId = newRunId();
|
|
@@ -104,7 +107,7 @@ export class Worker {
|
|
|
104
107
|
this.state = "preparing";
|
|
105
108
|
this.branchName = makeBranchName(card.short_id, card.title, this.config.worktree.failedBranchPrefix);
|
|
106
109
|
log.info(this.tag, `Preparing #${card.short_id} "${card.title}"`);
|
|
107
|
-
// Per-card attempt counter resets on success;
|
|
110
|
+
// Per-card attempt counter resets on success; give-up triggers off it.
|
|
108
111
|
await this.stateStore.incrementAttempt(card.id);
|
|
109
112
|
// Start the heartbeat loop so the reconciler knows this run is
|
|
110
113
|
// still alive even if no phase transitions happen for a while
|
|
@@ -195,7 +198,12 @@ export class Worker {
|
|
|
195
198
|
});
|
|
196
199
|
this.state = "completing";
|
|
197
200
|
await this.recordPhase("completing");
|
|
198
|
-
await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore);
|
|
201
|
+
const completed = await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore);
|
|
202
|
+
// A verification failure is a failed attempt, not a success — even
|
|
203
|
+
// though runCompletion returns normally (it has already moved the card
|
|
204
|
+
// to the fail column and ended the session). Flag it so the finally
|
|
205
|
+
// block records a failure and counts it toward the give-up budget.
|
|
206
|
+
this.verificationFailed = !completed;
|
|
199
207
|
}
|
|
200
208
|
catch (err) {
|
|
201
209
|
this.state = "error";
|
|
@@ -228,8 +236,11 @@ export class Worker {
|
|
|
228
236
|
// Only bookkeep success when we actually succeeded. "cancelling"
|
|
229
237
|
// and aborted runs are failures/user-initiated stops, not wins —
|
|
230
238
|
// counting them as success would reset attempts and mask real
|
|
231
|
-
// failure loops.
|
|
232
|
-
const succeeded = this.runId &&
|
|
239
|
+
// failure loops. A verification failure is likewise a failed attempt.
|
|
240
|
+
const succeeded = this.runId &&
|
|
241
|
+
this.state !== "error" &&
|
|
242
|
+
!this.aborted &&
|
|
243
|
+
!this.verificationFailed;
|
|
233
244
|
if (succeeded) {
|
|
234
245
|
try {
|
|
235
246
|
await this.stateStore.endRun(this.runId, "completed");
|
|
@@ -249,6 +260,18 @@ export class Worker {
|
|
|
249
260
|
// best-effort
|
|
250
261
|
}
|
|
251
262
|
}
|
|
263
|
+
else if (this.runId && this.verificationFailed) {
|
|
264
|
+
// Verification failed (no exception was thrown). Count it as a
|
|
265
|
+
// failed attempt so repeated failures eventually trip the give-up
|
|
266
|
+
// budget — runCompletion already moved the card + ended the session.
|
|
267
|
+
try {
|
|
268
|
+
await this.stateStore.endRun(this.runId, "paused", "verification");
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// best-effort
|
|
272
|
+
}
|
|
273
|
+
await this.recordOutcome(card.id, "failure");
|
|
274
|
+
}
|
|
252
275
|
this.cleanup();
|
|
253
276
|
this.state = "idle";
|
|
254
277
|
this.onDone(this);
|
|
@@ -262,6 +285,26 @@ export class Worker {
|
|
|
262
285
|
await this.stateStore.addCost(cardId, cents);
|
|
263
286
|
}
|
|
264
287
|
await this.stateStore.recordOutcome(cardId, outcome);
|
|
288
|
+
// Give-up: if this failure exhausted the attempt budget, post a single
|
|
289
|
+
// human-facing comment here at the crossing. The pool guard then skips
|
|
290
|
+
// the card quietly until it is reassigned (which resets attempts), so
|
|
291
|
+
// this fires exactly once — no permanent DLQ, no label, no manual clear.
|
|
292
|
+
if (outcome === "failure") {
|
|
293
|
+
const max = this.config.budget.maxAttemptsPerCard;
|
|
294
|
+
const attempts = this.stateStore.getCard(cardId)?.attempts ?? 0;
|
|
295
|
+
if (attempts >= max) {
|
|
296
|
+
try {
|
|
297
|
+
const body = buildGaveUpComment(max, this.stateStore.getRecentFailures(cardId, 3));
|
|
298
|
+
await this.client.addComment(cardId, body, {
|
|
299
|
+
commentType: "blocker",
|
|
300
|
+
});
|
|
301
|
+
log.warn(this.tag, `gave up on ${cardId} after ${attempts} attempts`);
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
log.warn(this.tag, `failed to post give-up comment for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
265
308
|
}
|
|
266
309
|
catch (err) {
|
|
267
310
|
log.warn(this.tag, `recordOutcome(${outcome}) failed: ${err instanceof Error ? err.message : err}`);
|