@gethmy/agent 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/budget.js +34 -6
- package/dist/completion.d.ts +1 -1
- package/dist/completion.js +51 -19
- package/dist/git-pr.js +22 -1
- package/dist/index.js +3 -1
- package/dist/pool.js +9 -5
- package/dist/review-completion.d.ts +1 -1
- package/dist/review-completion.js +1 -1
- package/dist/startup-banner.js +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,11 +36,12 @@ npx @gethmy/mcp setup
|
|
|
36
36
|
```jsonc
|
|
37
37
|
{
|
|
38
38
|
"agent": {
|
|
39
|
-
"poolSize":
|
|
39
|
+
"poolSize": 3,
|
|
40
40
|
"pickupColumns": ["To Do"],
|
|
41
41
|
"claude": { "model": "opus", "reviewModel": "sonnet" },
|
|
42
42
|
"review": {
|
|
43
43
|
"enabled": true,
|
|
44
|
+
"poolSize": 2,
|
|
44
45
|
"pickupColumns": ["Review"],
|
|
45
46
|
"moveToColumn": "Done",
|
|
46
47
|
"failColumn": "To Do"
|
package/dist/budget.js
CHANGED
|
@@ -103,9 +103,18 @@ export class BudgetGuard {
|
|
|
103
103
|
log.warn(TAG, `#${card.short_id} DLQ'd — ${reason}: ${detail}`);
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
-
const
|
|
106
|
+
const DLQ_FENCE_START = "<!-- agent-dlq:start -->";
|
|
107
|
+
const DLQ_FENCE_END = "<!-- agent-dlq:end -->";
|
|
108
|
+
// Legacy marker — pre-fence DLQ blocks written before 2026-05-23. Strip path
|
|
109
|
+
// only; new blocks always emit the fenced form.
|
|
110
|
+
const LEGACY_DLQ_MARKER = "---\n**Agent DLQ**";
|
|
107
111
|
function buildDlqDescriptionBlock(reason, detail, failures) {
|
|
108
|
-
const lines = [
|
|
112
|
+
const lines = [
|
|
113
|
+
DLQ_FENCE_START,
|
|
114
|
+
"---",
|
|
115
|
+
"**Agent DLQ**",
|
|
116
|
+
`Cap hit: ${reason} — ${detail}`,
|
|
117
|
+
];
|
|
109
118
|
if (failures.length > 0) {
|
|
110
119
|
lines.push("", "Recent failures:");
|
|
111
120
|
for (const f of failures) {
|
|
@@ -120,13 +129,32 @@ function buildDlqDescriptionBlock(reason, detail, failures) {
|
|
|
120
129
|
else {
|
|
121
130
|
lines.push("", "_No prior failure summaries recorded._");
|
|
122
131
|
}
|
|
132
|
+
lines.push(DLQ_FENCE_END);
|
|
123
133
|
return lines.join("\n");
|
|
124
134
|
}
|
|
125
135
|
function stripDlqBlock(description) {
|
|
126
|
-
const
|
|
127
|
-
if (
|
|
128
|
-
|
|
129
|
-
|
|
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();
|
|
130
158
|
}
|
|
131
159
|
function formatCents(cents) {
|
|
132
160
|
return `$${(cents / 100).toFixed(2)}`;
|
package/dist/completion.d.ts
CHANGED
|
@@ -29,4 +29,4 @@ export declare function buildTokenPayload(stats?: SessionStats | null): {
|
|
|
29
29
|
/**
|
|
30
30
|
* Post-work pipeline: push branch, create PR, move card, post summary.
|
|
31
31
|
*/
|
|
32
|
-
export declare function runCompletion(client: HarmonyApiClient, card: Card, branchName: string, worktreePath: string, config: AgentConfig, workerId
|
|
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<void>;
|
package/dist/completion.js
CHANGED
|
@@ -29,7 +29,7 @@ export function buildTokenPayload(stats) {
|
|
|
29
29
|
/**
|
|
30
30
|
* Post-work pipeline: push branch, create PR, move card, post summary.
|
|
31
31
|
*/
|
|
32
|
-
export async function runCompletion(client, card, branchName, worktreePath, config, workerId
|
|
32
|
+
export async function runCompletion(client, card, branchName, worktreePath, config, workerId, sessionStats, workspaceId, agentSessionId, stateStore) {
|
|
33
33
|
// Hoisted so the episode write hook can read final verification state.
|
|
34
34
|
let verificationResult = {
|
|
35
35
|
passed: true,
|
|
@@ -54,16 +54,25 @@ export async function runCompletion(client, card, branchName, worktreePath, conf
|
|
|
54
54
|
// under `agent-attempts/*` for `failedAttemptRetentionDays`. Without this
|
|
55
55
|
// ordering, verify failures used to orphan commits in a deleted worktree —
|
|
56
56
|
// recoverable only via `git reflog`.
|
|
57
|
+
//
|
|
58
|
+
// `lastPushedSha` tracks the most recent commit we successfully pushed so
|
|
59
|
+
// later push points (post-fix, post-fail) can skip when HEAD hasn't moved.
|
|
57
60
|
log.info(TAG, `Pushing branch ${branchName} (pre-verify)...`);
|
|
61
|
+
let lastPushedSha = null;
|
|
58
62
|
try {
|
|
59
63
|
pushBranch(branchName, worktreePath);
|
|
64
|
+
lastPushedSha = readHeadSha(worktreePath);
|
|
60
65
|
}
|
|
61
66
|
catch (err) {
|
|
62
67
|
// Push failure shouldn't prevent verification from running, but the
|
|
63
68
|
// safety guarantee is gone — surface it loudly so the operator notices.
|
|
64
69
|
log.error(TAG, `pre-verify push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
65
70
|
}
|
|
66
|
-
|
|
71
|
+
// Only surface a recovery URL when the push actually landed — otherwise
|
|
72
|
+
// the link points at a ref that doesn't exist on origin.
|
|
73
|
+
const recoveryUrl = lastPushedSha
|
|
74
|
+
? getBranchWebUrl(branchName, worktreePath)
|
|
75
|
+
: null;
|
|
67
76
|
// 2. Verification gate
|
|
68
77
|
if (config.verification.enabled) {
|
|
69
78
|
await client.updateAgentProgress(card.id, {
|
|
@@ -91,12 +100,18 @@ export async function runCompletion(client, card, branchName, worktreePath, conf
|
|
|
91
100
|
autoFixAttempts = attempt + 1;
|
|
92
101
|
if (result.passed) {
|
|
93
102
|
log.info(TAG, `Auto-fix succeeded on attempt ${attempt + 1}`);
|
|
94
|
-
// Push again so
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
// Push again so any auto-fix commits are durable on origin. Skip
|
|
104
|
+
// when HEAD hasn't moved since the last successful push — autofix
|
|
105
|
+
// can pass without producing new commits.
|
|
106
|
+
const sha = readHeadSha(worktreePath);
|
|
107
|
+
if (sha && sha !== lastPushedSha) {
|
|
108
|
+
try {
|
|
109
|
+
pushBranch(branchName, worktreePath);
|
|
110
|
+
lastPushedSha = sha;
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
log.warn(TAG, `post-fix push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
114
|
+
}
|
|
100
115
|
}
|
|
101
116
|
break;
|
|
102
117
|
}
|
|
@@ -106,16 +121,22 @@ export async function runCompletion(client, card, branchName, worktreePath, conf
|
|
|
106
121
|
if (!result.passed) {
|
|
107
122
|
log.warn(TAG, `Verification failed for #${card.short_id} — reporting findings`);
|
|
108
123
|
// Push the latest tip (including any auto-fix attempts) so the
|
|
109
|
-
// failed branch on origin reflects what verify saw.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
// failed branch on origin reflects what verify saw. Skip when HEAD
|
|
125
|
+
// hasn't advanced since the last successful push — common when
|
|
126
|
+
// every auto-fix attempt produced no new commits.
|
|
127
|
+
const failSha = readHeadSha(worktreePath);
|
|
128
|
+
if (failSha && failSha !== lastPushedSha) {
|
|
129
|
+
try {
|
|
130
|
+
pushBranch(branchName, worktreePath);
|
|
131
|
+
lastPushedSha = failSha;
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
log.warn(TAG, `post-fail push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
135
|
+
}
|
|
115
136
|
}
|
|
116
137
|
const failureSummary = buildVerificationFailureSummary(result, autoFixAttempts);
|
|
117
138
|
try {
|
|
118
|
-
await stateStore
|
|
139
|
+
await stateStore.recordFailureSummary(card.id, {
|
|
119
140
|
summary: failureSummary,
|
|
120
141
|
reason: "verification",
|
|
121
142
|
recoveryBranch: branchName,
|
|
@@ -124,10 +145,10 @@ export async function runCompletion(client, card, branchName, worktreePath, conf
|
|
|
124
145
|
catch (err) {
|
|
125
146
|
log.debug(TAG, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
|
|
126
147
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
});
|
|
148
|
+
// Only attach recovery instructions when the branch actually exists
|
|
149
|
+
// on origin — otherwise the `git fetch && git checkout` command we'd
|
|
150
|
+
// surface would fail for the user.
|
|
151
|
+
await reportFindings(client, card.id, result, lastPushedSha ? { branchName, branchUrl: recoveryUrl } : null);
|
|
131
152
|
await moveCardToColumn(client, card, config.verification.failColumn);
|
|
132
153
|
await client.endAgentSession(card.id, {
|
|
133
154
|
status: "failed",
|
|
@@ -204,6 +225,17 @@ function buildVerificationFailureSummary(result, autoFixAttempts) {
|
|
|
204
225
|
const tail = autoFixAttempts > 0 ? ` after ${autoFixAttempts} auto-fix attempt(s)` : "";
|
|
205
226
|
return `${head}${tail}`;
|
|
206
227
|
}
|
|
228
|
+
function readHeadSha(worktreePath) {
|
|
229
|
+
try {
|
|
230
|
+
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
231
|
+
cwd: worktreePath,
|
|
232
|
+
encoding: "utf-8",
|
|
233
|
+
}).trim();
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
207
239
|
function checkHasCommits(worktreePath, baseBranch) {
|
|
208
240
|
try {
|
|
209
241
|
const count = execFileSync("git", ["rev-list", "--count", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
package/dist/git-pr.js
CHANGED
|
@@ -158,7 +158,28 @@ export function remoteBranchExists(branchName, cwd) {
|
|
|
158
158
|
export function pushBranch(branchName, cwd) {
|
|
159
159
|
if (remoteBranchExists(branchName, cwd)) {
|
|
160
160
|
log.info(TAG, `Remote branch ${branchName} exists (rework), force-pushing`);
|
|
161
|
-
|
|
161
|
+
// Resolve the remote tip explicitly so the lease anchors to a known SHA.
|
|
162
|
+
// Bare `--force-with-lease` only checks against the local tracking ref,
|
|
163
|
+
// which can be stale if it was never fetched in this worktree — that
|
|
164
|
+
// lets a concurrent update slip through. Fetch + pin the expected SHA.
|
|
165
|
+
let expectedSha = null;
|
|
166
|
+
try {
|
|
167
|
+
execFileSync("git", ["fetch", "origin", branchName], {
|
|
168
|
+
cwd,
|
|
169
|
+
stdio: "pipe",
|
|
170
|
+
});
|
|
171
|
+
expectedSha = execFileSync("git", ["rev-parse", `refs/remotes/origin/${branchName}`], { cwd, encoding: "utf-8" }).trim();
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
log.warn(TAG, `could not resolve remote tip for ${branchName}, falling back to weak lease: ${err instanceof Error ? err.message : err}`);
|
|
175
|
+
}
|
|
176
|
+
const lease = expectedSha
|
|
177
|
+
? `--force-with-lease=refs/heads/${branchName}:${expectedSha}`
|
|
178
|
+
: "--force-with-lease";
|
|
179
|
+
execFileSync("git", ["push", lease, "-u", "origin", branchName], {
|
|
180
|
+
cwd,
|
|
181
|
+
stdio: "pipe",
|
|
182
|
+
});
|
|
162
183
|
}
|
|
163
184
|
else {
|
|
164
185
|
execFileSync("git", ["push", "-u", "origin", branchName], {
|
package/dist/index.js
CHANGED
|
@@ -267,7 +267,9 @@ export async function main() {
|
|
|
267
267
|
}
|
|
268
268
|
// Banner check lines for service intervals + pool. Compose into one
|
|
269
269
|
// line each so the banner stays compact.
|
|
270
|
-
const reviewCount = config.agent.review.enabled
|
|
270
|
+
const reviewCount = config.agent.review.enabled
|
|
271
|
+
? config.agent.review.poolSize
|
|
272
|
+
: 0;
|
|
271
273
|
banner.check(`Pool: ${config.agent.poolSize} impl${reviewCount > 0 ? ` + ${reviewCount} review` : ""}`);
|
|
272
274
|
const services = [
|
|
273
275
|
`Heartbeat ${config.agent.timing.reconcileIntervalMs / 1000}s`,
|
package/dist/pool.js
CHANGED
|
@@ -23,12 +23,16 @@ export class Pool {
|
|
|
23
23
|
this.tryDispatchFor(this.implWorkers, this.implQueue, "impl");
|
|
24
24
|
}, workspaceId, projectId, stateStore));
|
|
25
25
|
}
|
|
26
|
-
// Create review
|
|
26
|
+
// Create review workers. IDs offset by `config.poolSize` so impl and
|
|
27
|
+
// review worker IDs never collide (verification + review ports both
|
|
28
|
+
// derive from the worker ID, so collision would mean port collision).
|
|
27
29
|
if (config.review.enabled) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
this.
|
|
31
|
-
|
|
30
|
+
for (let i = 0; i < config.review.poolSize; i++) {
|
|
31
|
+
const reviewWorkerId = config.poolSize + i;
|
|
32
|
+
this.reviewWorkers.push(new ReviewWorker(reviewWorkerId, config, client, userEmail, () => {
|
|
33
|
+
this.tryDispatchFor(this.reviewWorkers, this.reviewQueue, "review");
|
|
34
|
+
}, stateStore, workspaceId, projectId));
|
|
35
|
+
}
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
/**
|
|
@@ -37,4 +37,4 @@ export declare function parseReviewOutput(stdout: string): ReviewResult;
|
|
|
37
37
|
* Handles approved/rejected verdicts, creates subtasks for findings,
|
|
38
38
|
* and moves the card to the appropriate column.
|
|
39
39
|
*/
|
|
40
|
-
export declare function runReviewCompletion(client: HarmonyApiClient, card: Card, result: ReviewResult, config: AgentConfig, worktreePath: string, branchName: string | null, sessionStats
|
|
40
|
+
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>;
|
|
@@ -425,7 +425,7 @@ export async function runReviewCompletion(client, card, result, config, worktree
|
|
|
425
425
|
? getBranchWebUrl(branchName, worktreePath)
|
|
426
426
|
: null;
|
|
427
427
|
try {
|
|
428
|
-
await stateStore
|
|
428
|
+
await stateStore.recordFailureSummary(card.id, {
|
|
429
429
|
summary: failureSummary,
|
|
430
430
|
reason: "review",
|
|
431
431
|
recoveryBranch,
|
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/types.d.ts
CHANGED
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",
|