@gethmy/agent 1.7.0 → 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 +8 -1
- 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 +1 -9
- package/dist/pool.d.ts +4 -3
- package/dist/pool.js +19 -18
- 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/state-store.d.ts +8 -7
- package/dist/state-store.js +14 -23
- package/dist/types.d.ts +33 -6
- package/dist/types.js +0 -3
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +47 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,13 +16,20 @@ Built for **failsafe auto mode**: crashed daemons recover on restart, misconfigu
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
# Run directly (works with any package manager)
|
|
19
|
-
npx @gethmy/agent
|
|
19
|
+
npx @gethmy/agent@latest
|
|
20
20
|
|
|
21
21
|
# Or install globally
|
|
22
22
|
npm install -g @gethmy/agent
|
|
23
23
|
harmony-agent
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
> **Always pin `@latest`.** A bare `npx @gethmy/agent` reuses any previously
|
|
27
|
+
> cached version that satisfies the spec — so an old install in `~/.npm/_npx`
|
|
28
|
+
> can shadow the current release and you'll get stale startup logs and CLI
|
|
29
|
+
> behavior. `npx @gethmy/agent@latest` re-resolves to the newest published
|
|
30
|
+
> version. If you ever suspect a stale run, clear the cache with
|
|
31
|
+
> `rm -rf ~/.npm/_npx` (or install globally to skip npx caching entirely).
|
|
32
|
+
|
|
26
33
|
## Configuration
|
|
27
34
|
|
|
28
35
|
1. Set up the MCP server first:
|
package/dist/budget.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { Card } from "@harmony/shared";
|
|
3
|
-
import type { StateStore } from "./state-store.js";
|
|
1
|
+
import type { FailureSummaryRecord, StateStore } from "./state-store.js";
|
|
4
2
|
import type { AgentConfig } from "./types.js";
|
|
5
3
|
export type GuardDecision = {
|
|
6
4
|
allow: true;
|
|
@@ -9,39 +7,33 @@ export type GuardDecision = {
|
|
|
9
7
|
reason: GuardReason;
|
|
10
8
|
detail: string;
|
|
11
9
|
};
|
|
12
|
-
export type GuardReason = "
|
|
10
|
+
export type GuardReason = "max_attempts" | "daily_budget";
|
|
13
11
|
/**
|
|
14
|
-
* BudgetGuard is consulted on every pickup
|
|
15
|
-
*
|
|
16
|
-
* 1. Cards that can never succeed
|
|
17
|
-
*
|
|
18
|
-
*
|
|
12
|
+
* BudgetGuard is consulted on every implement pickup. It protects the
|
|
13
|
+
* daemon from two failure modes:
|
|
14
|
+
* 1. Cards that can never succeed — after N failed attempts the daemon
|
|
15
|
+
* gives up quietly (`max_attempts`) and pings once via a comment.
|
|
16
|
+
* 2. Runaway daily spend across the entire daemon (`daily_budget`).
|
|
19
17
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* Both are recoverable: `max_attempts` resets when the card is reassigned
|
|
19
|
+
* (see `StateStore.resetAttempts`), and `daily_budget` resets at UTC
|
|
20
|
+
* midnight. There is no per-card cost cap and no permanent dead-letter
|
|
21
|
+
* quarantine — the guard never blocks a card forever.
|
|
22
22
|
*/
|
|
23
23
|
export declare class BudgetGuard {
|
|
24
24
|
private config;
|
|
25
25
|
private store;
|
|
26
26
|
constructor(config: AgentConfig["budget"], store: StateStore);
|
|
27
27
|
/**
|
|
28
|
-
* Inspect a card before we commit to picking it up.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* simply hold until the daily budget resets (`daily_budget`).
|
|
28
|
+
* Inspect a card before we commit to picking it up. `max_attempts` means
|
|
29
|
+
* the daemon has given up (the worker has already posted a comment);
|
|
30
|
+
* `daily_budget` is a soft pause until the UTC day rolls over.
|
|
32
31
|
*/
|
|
33
32
|
check(cardId: string): GuardDecision;
|
|
34
|
-
/**
|
|
35
|
-
* Does the guard's decision warrant a permanent DLQ marker? The daily
|
|
36
|
-
* budget is *not* permanent — it resets at UTC midnight — so we only
|
|
37
|
-
* DLQ for terminal states.
|
|
38
|
-
*/
|
|
39
|
-
isTerminal(reason: GuardReason): boolean;
|
|
40
|
-
/**
|
|
41
|
-
* Apply the DLQ label to a card, persist the reason, and append a
|
|
42
|
-
* post-mortem block to the card description listing the last 3 failure
|
|
43
|
-
* summaries. Safe to call repeatedly — labels are idempotent and the
|
|
44
|
-
* description block is delimited so reruns replace rather than stack.
|
|
45
|
-
*/
|
|
46
|
-
markDlq(client: HarmonyApiClient, card: Card, reason: GuardReason, detail: string): Promise<void>;
|
|
47
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Build the one-shot "agent gave up" comment posted when a card exhausts
|
|
36
|
+
* its attempt budget. Lists the recent failure summaries so a human has a
|
|
37
|
+
* post-mortem trail and a recovery branch to check out.
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildGaveUpComment(maxAttempts: number, failures: FailureSummaryRecord[]): string;
|
package/dist/budget.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { log } from "./log.js";
|
|
2
|
-
import { runTransition } from "./transitions.js";
|
|
3
|
-
const TAG = "budget";
|
|
4
1
|
/**
|
|
5
|
-
* BudgetGuard is consulted on every pickup
|
|
6
|
-
*
|
|
7
|
-
* 1. Cards that can never succeed
|
|
8
|
-
*
|
|
9
|
-
*
|
|
2
|
+
* BudgetGuard is consulted on every implement pickup. It protects the
|
|
3
|
+
* daemon from two failure modes:
|
|
4
|
+
* 1. Cards that can never succeed — after N failed attempts the daemon
|
|
5
|
+
* gives up quietly (`max_attempts`) and pings once via a comment.
|
|
6
|
+
* 2. Runaway daily spend across the entire daemon (`daily_budget`).
|
|
10
7
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* Both are recoverable: `max_attempts` resets when the card is reassigned
|
|
9
|
+
* (see `StateStore.resetAttempts`), and `daily_budget` resets at UTC
|
|
10
|
+
* midnight. There is no per-card cost cap and no permanent dead-letter
|
|
11
|
+
* quarantine — the guard never blocks a card forever.
|
|
13
12
|
*/
|
|
14
13
|
export class BudgetGuard {
|
|
15
14
|
config;
|
|
@@ -19,37 +18,19 @@ export class BudgetGuard {
|
|
|
19
18
|
this.store = store;
|
|
20
19
|
}
|
|
21
20
|
/**
|
|
22
|
-
* Inspect a card before we commit to picking it up.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* simply hold until the daily budget resets (`daily_budget`).
|
|
21
|
+
* Inspect a card before we commit to picking it up. `max_attempts` means
|
|
22
|
+
* the daemon has given up (the worker has already posted a comment);
|
|
23
|
+
* `daily_budget` is a soft pause until the UTC day rolls over.
|
|
26
24
|
*/
|
|
27
25
|
check(cardId) {
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
const card = this.store.getCard(cardId);
|
|
27
|
+
if (card && card.attempts >= this.config.maxAttemptsPerCard) {
|
|
30
28
|
return {
|
|
31
29
|
allow: false,
|
|
32
|
-
reason: "
|
|
33
|
-
detail:
|
|
30
|
+
reason: "max_attempts",
|
|
31
|
+
detail: `${card.attempts} of ${this.config.maxAttemptsPerCard} attempts exhausted`,
|
|
34
32
|
};
|
|
35
33
|
}
|
|
36
|
-
const card = this.store.getCard(cardId);
|
|
37
|
-
if (card) {
|
|
38
|
-
if (card.attempts >= this.config.maxAttemptsPerCard) {
|
|
39
|
-
return {
|
|
40
|
-
allow: false,
|
|
41
|
-
reason: "max_attempts",
|
|
42
|
-
detail: `${card.attempts} of ${this.config.maxAttemptsPerCard} attempts exhausted`,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
if (card.totalCostCents >= this.config.maxCentsPerCard) {
|
|
46
|
-
return {
|
|
47
|
-
allow: false,
|
|
48
|
-
reason: "card_cost_cap",
|
|
49
|
-
detail: `spent ${formatCents(card.totalCostCents)} of ${formatCents(this.config.maxCentsPerCard)} per-card cap`,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
34
|
const dailySpent = this.store.getDailyCostCents();
|
|
54
35
|
if (dailySpent >= this.config.dailyBudgetCents) {
|
|
55
36
|
return {
|
|
@@ -60,60 +41,16 @@ export class BudgetGuard {
|
|
|
60
41
|
}
|
|
61
42
|
return { allow: true };
|
|
62
43
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Does the guard's decision warrant a permanent DLQ marker? The daily
|
|
65
|
-
* budget is *not* permanent — it resets at UTC midnight — so we only
|
|
66
|
-
* DLQ for terminal states.
|
|
67
|
-
*/
|
|
68
|
-
isTerminal(reason) {
|
|
69
|
-
return (reason === "dlq" ||
|
|
70
|
-
reason === "max_attempts" ||
|
|
71
|
-
reason === "card_cost_cap");
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Apply the DLQ label to a card, persist the reason, and append a
|
|
75
|
-
* post-mortem block to the card description listing the last 3 failure
|
|
76
|
-
* summaries. Safe to call repeatedly — labels are idempotent and the
|
|
77
|
-
* description block is delimited so reruns replace rather than stack.
|
|
78
|
-
*/
|
|
79
|
-
async markDlq(client, card, reason, detail) {
|
|
80
|
-
await this.store.markDlq(card.id, `${reason}: ${detail}`);
|
|
81
|
-
try {
|
|
82
|
-
await runTransition(client, card, {
|
|
83
|
-
addLabels: [
|
|
84
|
-
{ name: this.config.dlqLabel, color: this.config.dlqLabelColor },
|
|
85
|
-
],
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
catch (err) {
|
|
89
|
-
log.warn(TAG, `failed to add dlq label to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
90
|
-
}
|
|
91
|
-
try {
|
|
92
|
-
const recent = this.store.getRecentFailures(card.id, 3);
|
|
93
|
-
const block = buildDlqDescriptionBlock(reason, detail, recent);
|
|
94
|
-
const existing = card.description ?? "";
|
|
95
|
-
const stripped = stripDlqBlock(existing);
|
|
96
|
-
await client.updateCard(card.id, {
|
|
97
|
-
description: `${stripped}${stripped ? "\n\n" : ""}${block}`,
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
catch (err) {
|
|
101
|
-
log.warn(TAG, `failed to post DLQ summary to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
102
|
-
}
|
|
103
|
-
log.warn(TAG, `#${card.short_id} DLQ'd — ${reason}: ${detail}`);
|
|
104
|
-
}
|
|
105
44
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
function
|
|
45
|
+
/**
|
|
46
|
+
* Build the one-shot "agent gave up" comment posted when a card exhausts
|
|
47
|
+
* its attempt budget. Lists the recent failure summaries so a human has a
|
|
48
|
+
* post-mortem trail and a recovery branch to check out.
|
|
49
|
+
*/
|
|
50
|
+
export function buildGaveUpComment(maxAttempts, failures) {
|
|
112
51
|
const lines = [
|
|
113
|
-
|
|
114
|
-
"
|
|
115
|
-
"**Agent DLQ**",
|
|
116
|
-
`Cap hit: ${reason} — ${detail}`,
|
|
52
|
+
"**Agent gave up — needs a human.**",
|
|
53
|
+
`Stopped after ${maxAttempts} failed attempt${maxAttempts === 1 ? "" : "s"}. Reassign the card to try again.`,
|
|
117
54
|
];
|
|
118
55
|
if (failures.length > 0) {
|
|
119
56
|
lines.push("", "Recent failures:");
|
|
@@ -129,33 +66,8 @@ function buildDlqDescriptionBlock(reason, detail, failures) {
|
|
|
129
66
|
else {
|
|
130
67
|
lines.push("", "_No prior failure summaries recorded._");
|
|
131
68
|
}
|
|
132
|
-
lines.push(DLQ_FENCE_END);
|
|
133
69
|
return lines.join("\n");
|
|
134
70
|
}
|
|
135
|
-
function stripDlqBlock(description) {
|
|
136
|
-
const start = description.indexOf(DLQ_FENCE_START);
|
|
137
|
-
if (start >= 0) {
|
|
138
|
-
const end = description.indexOf(DLQ_FENCE_END, start);
|
|
139
|
-
if (end < 0) {
|
|
140
|
-
// Malformed: opening fence with no closer. Treat the rest of the
|
|
141
|
-
// description as the block — safer than preserving an orphan fence.
|
|
142
|
-
return description.slice(0, start).trimEnd();
|
|
143
|
-
}
|
|
144
|
-
const prefix = description.slice(0, start).trimEnd();
|
|
145
|
-
const suffix = description
|
|
146
|
-
.slice(end + DLQ_FENCE_END.length)
|
|
147
|
-
.replace(/^\s+/, "");
|
|
148
|
-
if (prefix && suffix)
|
|
149
|
-
return `${prefix}\n\n${suffix}`;
|
|
150
|
-
return prefix || suffix;
|
|
151
|
-
}
|
|
152
|
-
// Legacy unfenced block — match the original behavior (no suffix to
|
|
153
|
-
// preserve, since the legacy emitter always wrote to end-of-description).
|
|
154
|
-
const legacy = description.indexOf(LEGACY_DLQ_MARKER);
|
|
155
|
-
if (legacy >= 0)
|
|
156
|
-
return description.slice(0, legacy).trimEnd();
|
|
157
|
-
return description.trimEnd();
|
|
158
|
-
}
|
|
159
71
|
function formatCents(cents) {
|
|
160
72
|
return `$${(cents / 100).toFixed(2)}`;
|
|
161
73
|
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
* health — GET /health, exit 0 if healthy, 1 otherwise
|
|
10
10
|
* doctor — run preflight checks without starting the daemon
|
|
11
11
|
* gc — one-shot worktree garbage collection
|
|
12
|
-
* dlq list — print DLQ entries
|
|
13
|
-
* dlq clear <id> — clear a card's DLQ mark
|
|
14
12
|
* help — show usage
|
|
15
13
|
*/
|
|
16
14
|
export {};
|
package/dist/cli.js
CHANGED
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
* health — GET /health, exit 0 if healthy, 1 otherwise
|
|
10
10
|
* doctor — run preflight checks without starting the daemon
|
|
11
11
|
* gc — one-shot worktree garbage collection
|
|
12
|
-
* dlq list — print DLQ entries
|
|
13
|
-
* dlq clear <id> — clear a card's DLQ mark
|
|
14
12
|
* help — show usage
|
|
15
13
|
*/
|
|
16
14
|
import { log } from "./log.js";
|
|
@@ -23,8 +21,6 @@ Usage:
|
|
|
23
21
|
harmony-agent health Exit 0 if daemon is healthy, 1 otherwise
|
|
24
22
|
harmony-agent doctor Run preflight checks (don't start)
|
|
25
23
|
harmony-agent gc One-shot worktree garbage collection
|
|
26
|
-
harmony-agent dlq list List dead-lettered cards
|
|
27
|
-
harmony-agent dlq clear <cardId> Clear a card's DLQ marker
|
|
28
24
|
harmony-agent help Show this help
|
|
29
25
|
|
|
30
26
|
Flags:
|
|
@@ -45,12 +41,6 @@ async function httpCall(path, init) {
|
|
|
45
41
|
const url = `http://${cfg.agent.http.bindAddr}:${cfg.agent.http.port}${path}`;
|
|
46
42
|
return fetch(url, init);
|
|
47
43
|
}
|
|
48
|
-
/** True if the error looks like "daemon is not running" (ECONNREFUSED). */
|
|
49
|
-
function isDaemonDown(err) {
|
|
50
|
-
const code = err?.cause?.code;
|
|
51
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
52
|
-
return (code === "ECONNREFUSED" || /ECONNREFUSED|fetch failed|connect/i.test(msg));
|
|
53
|
-
}
|
|
54
44
|
function printStatus(body) {
|
|
55
45
|
const out = process.stdout;
|
|
56
46
|
const uptime = formatDuration(body.uptimeMs);
|
|
@@ -70,10 +60,6 @@ function printStatus(body) {
|
|
|
70
60
|
for (const q of body.reviewQueue) {
|
|
71
61
|
out.write(` #${q.shortId} priority=${q.priority}\n`);
|
|
72
62
|
}
|
|
73
|
-
out.write(`dlq (${body.dlq.length})\n`);
|
|
74
|
-
for (const d of body.dlq) {
|
|
75
|
-
out.write(` ${d.cardId} attempts=${d.attempts} cost=$${(d.totalCostCents / 100).toFixed(2)} reason=${d.reason}\n`);
|
|
76
|
-
}
|
|
77
63
|
}
|
|
78
64
|
function formatDuration(ms) {
|
|
79
65
|
const s = Math.floor(ms / 1000);
|
|
@@ -155,54 +141,6 @@ async function gcCommand() {
|
|
|
155
141
|
}
|
|
156
142
|
return 0;
|
|
157
143
|
}
|
|
158
|
-
async function dlqCommand(args) {
|
|
159
|
-
const sub = args[0];
|
|
160
|
-
const { StateStore } = await import("./state-store.js");
|
|
161
|
-
const store = StateStore.open();
|
|
162
|
-
if (!sub || sub === "list") {
|
|
163
|
-
const entries = store.listDlq();
|
|
164
|
-
if (entries.length === 0) {
|
|
165
|
-
process.stdout.write("DLQ is empty\n");
|
|
166
|
-
return 0;
|
|
167
|
-
}
|
|
168
|
-
for (const c of entries) {
|
|
169
|
-
process.stdout.write(`${c.cardId} attempts=${c.attempts} cost=$${(c.totalCostCents / 100).toFixed(2)} reason=${c.dlqReason ?? "(unknown)"}\n`);
|
|
170
|
-
}
|
|
171
|
-
return 0;
|
|
172
|
-
}
|
|
173
|
-
if (sub === "clear") {
|
|
174
|
-
const cardId = args[1];
|
|
175
|
-
if (!cardId) {
|
|
176
|
-
process.stderr.write("usage: harmony-agent dlq clear <cardId>\n");
|
|
177
|
-
return 2;
|
|
178
|
-
}
|
|
179
|
-
// Prefer the running daemon if present — direct file writes race
|
|
180
|
-
// the daemon's own in-memory state-store and silently lose data.
|
|
181
|
-
try {
|
|
182
|
-
const res = await httpCall(`/dlq/clear/${encodeURIComponent(cardId)}`, {
|
|
183
|
-
method: "POST",
|
|
184
|
-
});
|
|
185
|
-
if (res.ok) {
|
|
186
|
-
process.stdout.write(`cleared DLQ for ${cardId} (via daemon)\n`);
|
|
187
|
-
return 0;
|
|
188
|
-
}
|
|
189
|
-
process.stderr.write(`daemon returned ${res.status} ${res.statusText}\n`);
|
|
190
|
-
return 1;
|
|
191
|
-
}
|
|
192
|
-
catch (err) {
|
|
193
|
-
if (!isDaemonDown(err)) {
|
|
194
|
-
process.stderr.write(`daemon HTTP error: ${err instanceof Error ? err.message : err}\n`);
|
|
195
|
-
return 1;
|
|
196
|
-
}
|
|
197
|
-
// Daemon offline → safe to write directly.
|
|
198
|
-
await store.clearDlq(cardId);
|
|
199
|
-
process.stdout.write(`cleared DLQ for ${cardId} (daemon offline, wrote directly)\n`);
|
|
200
|
-
return 0;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
process.stderr.write(`unknown dlq subcommand: ${sub}\n`);
|
|
204
|
-
return 2;
|
|
205
|
-
}
|
|
206
144
|
async function dispatch(argv) {
|
|
207
145
|
// Strip node, script, and any global flags we own.
|
|
208
146
|
const args = argv.filter((a) => a !== "--pretty" && a !== "--json");
|
|
@@ -221,8 +159,6 @@ async function dispatch(argv) {
|
|
|
221
159
|
return doctorCommand();
|
|
222
160
|
case "gc":
|
|
223
161
|
return gcCommand();
|
|
224
|
-
case "dlq":
|
|
225
|
-
return dlqCommand(args.slice(1));
|
|
226
162
|
case "help":
|
|
227
163
|
case "--help":
|
|
228
164
|
case "-h":
|
package/dist/completion.d.ts
CHANGED
|
@@ -5,11 +5,15 @@ import type { CostUpdate } from "./stream-parser.js";
|
|
|
5
5
|
import { type AgentConfig } from "./types.js";
|
|
6
6
|
export interface SessionStats {
|
|
7
7
|
filesEdited: number;
|
|
8
|
+
/** Edited file paths tracked by the ProgressTracker (#272). */
|
|
9
|
+
filesEditedPaths?: string[];
|
|
8
10
|
filesRead: number;
|
|
9
11
|
toolCalls: number;
|
|
10
12
|
cost: CostUpdate | null;
|
|
11
13
|
/** Trimmed last assistant text — feeds the episode write hook (Phase 1.5). */
|
|
12
14
|
lastAssistantText?: string;
|
|
15
|
+
/** All non-trivial assistant text blocks — richer summary source (#272). */
|
|
16
|
+
assistantTextBlocks?: string[];
|
|
13
17
|
}
|
|
14
18
|
export declare function buildTokenPayload(stats?: SessionStats | null): {
|
|
15
19
|
costCents?: undefined;
|
|
@@ -29,4 +33,4 @@ export declare function buildTokenPayload(stats?: SessionStats | null): {
|
|
|
29
33
|
/**
|
|
30
34
|
* Post-work pipeline: push branch, create PR, move card, post summary.
|
|
31
35
|
*/
|
|
32
|
-
export declare function runCompletion(client: HarmonyApiClient, card: Card, branchName: string, worktreePath: string, config: AgentConfig, workerId: number, sessionStats: SessionStats | undefined, workspaceId: string | undefined, agentSessionId: string | null | undefined, stateStore: StateStore): Promise<
|
|
36
|
+
export declare function runCompletion(client: HarmonyApiClient, card: Card, branchName: string, worktreePath: string, config: AgentConfig, workerId: number, sessionStats: SessionStats | undefined, workspaceId: string | undefined, agentSessionId: string | null | undefined, stateStore: StateStore): Promise<boolean>;
|
package/dist/completion.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { moveCardToColumn } from "./board-helpers.js";
|
|
3
3
|
import { writeEpisode } from "./episode-writer.js";
|
|
4
|
+
import { captureDiffStat } from "./git-diff-stat.js";
|
|
4
5
|
import { createPullRequest, detectGitProvider, getBranchWebUrl, pushBranch, } from "./git-pr.js";
|
|
5
6
|
import { log } from "./log.js";
|
|
6
7
|
import { AGENT_NAME, agentIdentifier } from "./types.js";
|
|
@@ -47,7 +48,7 @@ export async function runCompletion(client, card, branchName, worktreePath, conf
|
|
|
47
48
|
...buildTokenPayload(sessionStats),
|
|
48
49
|
});
|
|
49
50
|
cleanupWorktree(worktreePath, branchName);
|
|
50
|
-
return;
|
|
51
|
+
return true; // nothing to verify — not a failed attempt
|
|
51
52
|
}
|
|
52
53
|
// 1. Push branch FIRST so commits are durable on origin regardless of
|
|
53
54
|
// verification outcome. A failed verify (below) then preserves the work
|
|
@@ -160,7 +161,7 @@ export async function runCompletion(client, card, branchName, worktreePath, conf
|
|
|
160
161
|
// Local-only cleanup. The remote ref under `agent-attempts/*` stays
|
|
161
162
|
// up; the GC sweep (worktree-gc.ts) prunes it after retention.
|
|
162
163
|
cleanupWorktree(worktreePath, branchName);
|
|
163
|
-
return;
|
|
164
|
+
return false; // verification failed — counts as a failed attempt
|
|
164
165
|
}
|
|
165
166
|
log.info(TAG, `Verification passed for #${card.short_id}`);
|
|
166
167
|
}
|
|
@@ -194,21 +195,38 @@ export async function runCompletion(client, card, branchName, worktreePath, conf
|
|
|
194
195
|
// a separate write hook into the pre-return path, which D8 intentionally
|
|
195
196
|
// omits ("daemon crashes ≠ task outcome").
|
|
196
197
|
if (workspaceId) {
|
|
198
|
+
// Capture changed files + churn from the diff (#272). Best-effort + guarded:
|
|
199
|
+
// a null result just falls back to the ProgressTracker-tracked edit paths.
|
|
200
|
+
// The diff's file list is authoritative (it reflects what actually landed,
|
|
201
|
+
// including renames/deletes the tracker can't see) so prefer it.
|
|
202
|
+
const diffStat = captureDiffStat(worktreePath, config.worktree.baseBranch);
|
|
203
|
+
const changedFiles = diffStat && diffStat.files.length > 0
|
|
204
|
+
? diffStat.files
|
|
205
|
+
: (sessionStats?.filesEditedPaths ?? []);
|
|
197
206
|
await writeEpisode(client, {
|
|
198
207
|
kind: "implement",
|
|
199
208
|
card,
|
|
200
209
|
workspaceId,
|
|
201
210
|
outcome: "success",
|
|
202
211
|
approachSummary: sessionStats?.lastAssistantText ?? "",
|
|
212
|
+
approachBlocks: sessionStats?.assistantTextBlocks,
|
|
203
213
|
result: verificationResult,
|
|
204
214
|
cost: sessionStats?.cost ?? null,
|
|
205
215
|
filesEdited: sessionStats?.filesEdited ?? 0,
|
|
216
|
+
changedFiles,
|
|
217
|
+
churn: diffStat
|
|
218
|
+
? {
|
|
219
|
+
insertions: diffStat.insertions,
|
|
220
|
+
deletions: diffStat.deletions,
|
|
221
|
+
}
|
|
222
|
+
: undefined,
|
|
206
223
|
agentSessionId: agentSessionId ?? null,
|
|
207
224
|
});
|
|
208
225
|
}
|
|
209
226
|
// 7. Cleanup worktree
|
|
210
227
|
cleanupWorktree(worktreePath, branchName);
|
|
211
228
|
log.info(TAG, `Completion done for #${card.short_id}${prUrl ? ` — PR: ${prUrl}` : ""}`);
|
|
229
|
+
return true;
|
|
212
230
|
}
|
|
213
231
|
function buildVerificationFailureSummary(result, autoFixAttempts) {
|
|
214
232
|
const counts = [];
|
package/dist/episode-writer.d.ts
CHANGED
|
@@ -9,9 +9,22 @@ interface ImplementEpisodeInput {
|
|
|
9
9
|
workspaceId: string;
|
|
10
10
|
outcome: EpisodeOutcome;
|
|
11
11
|
approachSummary: string;
|
|
12
|
+
/**
|
|
13
|
+
* All non-trivial assistant text blocks from the run (#272). When present,
|
|
14
|
+
* used to assemble a richer approach summary + extract a key insight. Falls
|
|
15
|
+
* back to `approachSummary` (last turn) when empty.
|
|
16
|
+
*/
|
|
17
|
+
approachBlocks?: string[];
|
|
12
18
|
result: VerificationResult;
|
|
13
19
|
cost: CostUpdate | null;
|
|
14
20
|
filesEdited: number;
|
|
21
|
+
/** Changed file paths (#272): diff list when available, else tracked paths. */
|
|
22
|
+
changedFiles?: string[];
|
|
23
|
+
/** Line churn (#272), best-effort from `git diff --numstat`. */
|
|
24
|
+
churn?: {
|
|
25
|
+
insertions: number;
|
|
26
|
+
deletions: number;
|
|
27
|
+
};
|
|
15
28
|
errorMessage?: string;
|
|
16
29
|
agentSessionId?: string | null;
|
|
17
30
|
}
|
|
@@ -43,6 +56,25 @@ export declare function computeQualityScore(result: VerificationResult, opts: {
|
|
|
43
56
|
* as a recallable hit (rather than an empty bullet) in future prompts.
|
|
44
57
|
*/
|
|
45
58
|
export declare function trimApproachSummary(text: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Cap the review rationale ("why approved / rejected") richly (#272 task 5)
|
|
61
|
+
* rather than re-trimming to the 400-char implement bound. Empty input
|
|
62
|
+
* collapses to a marker so the episode still surfaces as a recallable hit.
|
|
63
|
+
*/
|
|
64
|
+
export declare function trimReviewRationale(text: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* Assemble a richer approach summary from the collected assistant text blocks
|
|
67
|
+
* (#272). Joins the trailing blocks (most relevant context lives near the end
|
|
68
|
+
* of a run) up to a longer bounded cap than the single-turn trim. Falls back to
|
|
69
|
+
* the last-turn `fallback` text when no blocks were collected. No LLM call.
|
|
70
|
+
*/
|
|
71
|
+
export declare function buildRichApproachSummary(blocks: string[] | undefined, fallback: string): string;
|
|
72
|
+
/**
|
|
73
|
+
* Extract a cheap deterministic "key insight" line from the run's assistant
|
|
74
|
+
* text (#272 task 3). Scans for a sentence/line matching a root-cause / gotcha
|
|
75
|
+
* pattern. Returns undefined when nothing matches — never fabricated, no LLM.
|
|
76
|
+
*/
|
|
77
|
+
export declare function extractKeyInsight(blocks: string[] | undefined, fallback: string): string | undefined;
|
|
46
78
|
/**
|
|
47
79
|
* Build the entity payload for one episode. Pure — returned object can be
|
|
48
80
|
* snapshotted in tests without hitting the network.
|