@gethmy/agent 1.7.1 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +6376 -141
- package/dist/index.js +6206 -333
- package/package.json +2 -2
- package/dist/board-helpers.d.ts +0 -31
- package/dist/board-helpers.js +0 -150
- package/dist/budget.d.ts +0 -39
- package/dist/budget.js +0 -73
- package/dist/cli.d.ts +0 -14
- package/dist/completion.d.ts +0 -36
- package/dist/completion.js +0 -322
- package/dist/config-validation.d.ts +0 -23
- package/dist/config-validation.js +0 -77
- package/dist/config.d.ts +0 -23
- package/dist/config.js +0 -103
- package/dist/episode-writer.d.ts +0 -116
- package/dist/episode-writer.js +0 -349
- package/dist/git-diff-stat.d.ts +0 -24
- package/dist/git-diff-stat.js +0 -56
- package/dist/git-pr.d.ts +0 -38
- package/dist/git-pr.js +0 -399
- package/dist/http-server.d.ts +0 -66
- package/dist/http-server.js +0 -96
- package/dist/index.d.ts +0 -5
- package/dist/log.d.ts +0 -34
- package/dist/log.js +0 -100
- package/dist/merge-monitor.d.ts +0 -23
- package/dist/merge-monitor.js +0 -169
- package/dist/pm.d.ts +0 -14
- package/dist/pm.js +0 -63
- package/dist/pool.d.ts +0 -71
- package/dist/pool.js +0 -259
- package/dist/process-group.d.ts +0 -26
- package/dist/process-group.js +0 -72
- package/dist/progress-tracker.d.ts +0 -82
- package/dist/progress-tracker.js +0 -457
- package/dist/prompt.d.ts +0 -23
- package/dist/prompt.js +0 -160
- package/dist/queue.d.ts +0 -39
- package/dist/queue.js +0 -100
- package/dist/reconcile.d.ts +0 -35
- package/dist/reconcile.js +0 -174
- package/dist/recovery.d.ts +0 -30
- package/dist/recovery.js +0 -141
- package/dist/review-completion.d.ts +0 -35
- package/dist/review-completion.js +0 -475
- package/dist/review-knowledge.d.ts +0 -14
- package/dist/review-knowledge.js +0 -89
- package/dist/review-prompt.d.ts +0 -12
- package/dist/review-prompt.js +0 -103
- package/dist/review-worker.d.ts +0 -56
- package/dist/review-worker.js +0 -638
- package/dist/review-worktree.d.ts +0 -12
- package/dist/review-worktree.js +0 -95
- package/dist/run-log.d.ts +0 -6
- package/dist/run-log.js +0 -19
- package/dist/startup-banner.d.ts +0 -29
- package/dist/startup-banner.js +0 -143
- package/dist/state-store.d.ts +0 -89
- package/dist/state-store.js +0 -230
- package/dist/stream-parser-selftest.d.ts +0 -9
- package/dist/stream-parser-selftest.js +0 -97
- package/dist/stream-parser.d.ts +0 -43
- package/dist/stream-parser.js +0 -174
- package/dist/transitions.d.ts +0 -57
- package/dist/transitions.js +0 -131
- package/dist/types.d.ts +0 -167
- package/dist/types.js +0 -76
- package/dist/verification.d.ts +0 -39
- package/dist/verification.js +0 -317
- package/dist/watcher.d.ts +0 -53
- package/dist/watcher.js +0 -153
- package/dist/worker.d.ts +0 -54
- package/dist/worker.js +0 -507
- package/dist/worktree-gc.d.ts +0 -67
- package/dist/worktree-gc.js +0 -245
- package/dist/worktree.d.ts +0 -18
- package/dist/worktree.js +0 -177
package/dist/completion.js
DELETED
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { moveCardToColumn } from "./board-helpers.js";
|
|
3
|
-
import { writeEpisode } from "./episode-writer.js";
|
|
4
|
-
import { captureDiffStat } from "./git-diff-stat.js";
|
|
5
|
-
import { createPullRequest, detectGitProvider, getBranchWebUrl, pushBranch, } from "./git-pr.js";
|
|
6
|
-
import { log } from "./log.js";
|
|
7
|
-
import { AGENT_NAME, agentIdentifier } from "./types.js";
|
|
8
|
-
import { attemptAutoFix, reportFindings, runVerification, } from "./verification.js";
|
|
9
|
-
import { cleanupWorktree } from "./worktree.js";
|
|
10
|
-
const TAG = "completion";
|
|
11
|
-
function formatTokenCount(tokens) {
|
|
12
|
-
if (tokens >= 1_000_000)
|
|
13
|
-
return `${(tokens / 1_000_000).toFixed(1)}M`;
|
|
14
|
-
if (tokens >= 1_000)
|
|
15
|
-
return `${(tokens / 1_000).toFixed(1)}k`;
|
|
16
|
-
return String(tokens);
|
|
17
|
-
}
|
|
18
|
-
export function buildTokenPayload(stats) {
|
|
19
|
-
if (!stats?.cost)
|
|
20
|
-
return {};
|
|
21
|
-
return {
|
|
22
|
-
costCents: Math.round(stats.cost.totalCostUsd * 100),
|
|
23
|
-
inputTokens: stats.cost.totalInputTokens,
|
|
24
|
-
outputTokens: stats.cost.totalOutputTokens,
|
|
25
|
-
cacheCreationInputTokens: stats.cost.totalCacheCreationInputTokens,
|
|
26
|
-
cacheReadInputTokens: stats.cost.totalCacheReadInputTokens,
|
|
27
|
-
modelName: stats.cost.modelName,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Post-work pipeline: push branch, create PR, move card, post summary.
|
|
32
|
-
*/
|
|
33
|
-
export async function runCompletion(client, card, branchName, worktreePath, config, workerId, sessionStats, workspaceId, agentSessionId, stateStore) {
|
|
34
|
-
// Hoisted so the episode write hook can read final verification state.
|
|
35
|
-
let verificationResult = {
|
|
36
|
-
passed: true,
|
|
37
|
-
buildErrors: [],
|
|
38
|
-
lintWarnings: [],
|
|
39
|
-
reviewFindings: [],
|
|
40
|
-
};
|
|
41
|
-
// Check if there are any commits on the branch
|
|
42
|
-
const hasCommits = checkHasCommits(worktreePath, config.worktree.baseBranch);
|
|
43
|
-
if (!hasCommits) {
|
|
44
|
-
log.warn(TAG, `No commits on branch ${branchName} — skipping completion`);
|
|
45
|
-
await client.endAgentSession(card.id, {
|
|
46
|
-
status: "completed",
|
|
47
|
-
progressPercent: 100,
|
|
48
|
-
...buildTokenPayload(sessionStats),
|
|
49
|
-
});
|
|
50
|
-
cleanupWorktree(worktreePath, branchName);
|
|
51
|
-
return true; // nothing to verify — not a failed attempt
|
|
52
|
-
}
|
|
53
|
-
// 1. Push branch FIRST so commits are durable on origin regardless of
|
|
54
|
-
// verification outcome. A failed verify (below) then preserves the work
|
|
55
|
-
// under `agent-attempts/*` for `failedAttemptRetentionDays`. Without this
|
|
56
|
-
// ordering, verify failures used to orphan commits in a deleted worktree —
|
|
57
|
-
// recoverable only via `git reflog`.
|
|
58
|
-
//
|
|
59
|
-
// `lastPushedSha` tracks the most recent commit we successfully pushed so
|
|
60
|
-
// later push points (post-fix, post-fail) can skip when HEAD hasn't moved.
|
|
61
|
-
log.info(TAG, `Pushing branch ${branchName} (pre-verify)...`);
|
|
62
|
-
let lastPushedSha = null;
|
|
63
|
-
try {
|
|
64
|
-
pushBranch(branchName, worktreePath);
|
|
65
|
-
lastPushedSha = readHeadSha(worktreePath);
|
|
66
|
-
}
|
|
67
|
-
catch (err) {
|
|
68
|
-
// Push failure shouldn't prevent verification from running, but the
|
|
69
|
-
// safety guarantee is gone — surface it loudly so the operator notices.
|
|
70
|
-
log.error(TAG, `pre-verify push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
71
|
-
}
|
|
72
|
-
// Only surface a recovery URL when the push actually landed — otherwise
|
|
73
|
-
// the link points at a ref that doesn't exist on origin.
|
|
74
|
-
const recoveryUrl = lastPushedSha
|
|
75
|
-
? getBranchWebUrl(branchName, worktreePath)
|
|
76
|
-
: null;
|
|
77
|
-
// 2. Verification gate
|
|
78
|
-
if (config.verification.enabled) {
|
|
79
|
-
await client.updateAgentProgress(card.id, {
|
|
80
|
-
agentIdentifier: agentIdentifier(workerId),
|
|
81
|
-
agentName: AGENT_NAME,
|
|
82
|
-
status: "working",
|
|
83
|
-
currentTask: "Verifying build...",
|
|
84
|
-
progressPercent: 80,
|
|
85
|
-
});
|
|
86
|
-
let result = await runVerification(worktreePath, config, workerId);
|
|
87
|
-
let autoFixAttempts = 0;
|
|
88
|
-
if (!result.passed && config.verification.autoFix) {
|
|
89
|
-
for (let attempt = 0; attempt < config.verification.maxFixAttempts; attempt++) {
|
|
90
|
-
log.info(TAG, `Auto-fix attempt ${attempt + 1}/${config.verification.maxFixAttempts}`);
|
|
91
|
-
await client.updateAgentProgress(card.id, {
|
|
92
|
-
agentIdentifier: agentIdentifier(workerId),
|
|
93
|
-
agentName: AGENT_NAME,
|
|
94
|
-
status: "working",
|
|
95
|
-
currentTask: `Fixing issues (attempt ${attempt + 1})...`,
|
|
96
|
-
progressPercent: 85,
|
|
97
|
-
});
|
|
98
|
-
const allErrors = [...result.buildErrors, ...result.lintWarnings];
|
|
99
|
-
await attemptAutoFix(worktreePath, config, allErrors);
|
|
100
|
-
result = await runVerification(worktreePath, config, workerId);
|
|
101
|
-
autoFixAttempts = attempt + 1;
|
|
102
|
-
if (result.passed) {
|
|
103
|
-
log.info(TAG, `Auto-fix succeeded on attempt ${attempt + 1}`);
|
|
104
|
-
// Push again so any auto-fix commits are durable on origin. Skip
|
|
105
|
-
// when HEAD hasn't moved since the last successful push — autofix
|
|
106
|
-
// can pass without producing new commits.
|
|
107
|
-
const sha = readHeadSha(worktreePath);
|
|
108
|
-
if (sha && sha !== lastPushedSha) {
|
|
109
|
-
try {
|
|
110
|
-
pushBranch(branchName, worktreePath);
|
|
111
|
-
lastPushedSha = sha;
|
|
112
|
-
}
|
|
113
|
-
catch (err) {
|
|
114
|
-
log.warn(TAG, `post-fix push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
break;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
verificationResult = result;
|
|
122
|
-
if (!result.passed) {
|
|
123
|
-
log.warn(TAG, `Verification failed for #${card.short_id} — reporting findings`);
|
|
124
|
-
// Push the latest tip (including any auto-fix attempts) so the
|
|
125
|
-
// failed branch on origin reflects what verify saw. Skip when HEAD
|
|
126
|
-
// hasn't advanced since the last successful push — common when
|
|
127
|
-
// every auto-fix attempt produced no new commits.
|
|
128
|
-
const failSha = readHeadSha(worktreePath);
|
|
129
|
-
if (failSha && failSha !== lastPushedSha) {
|
|
130
|
-
try {
|
|
131
|
-
pushBranch(branchName, worktreePath);
|
|
132
|
-
lastPushedSha = failSha;
|
|
133
|
-
}
|
|
134
|
-
catch (err) {
|
|
135
|
-
log.warn(TAG, `post-fail push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
const failureSummary = buildVerificationFailureSummary(result, autoFixAttempts);
|
|
139
|
-
try {
|
|
140
|
-
await stateStore.recordFailureSummary(card.id, {
|
|
141
|
-
summary: failureSummary,
|
|
142
|
-
reason: "verification",
|
|
143
|
-
recoveryBranch: branchName,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
catch (err) {
|
|
147
|
-
log.debug(TAG, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
|
|
148
|
-
}
|
|
149
|
-
// Only attach recovery instructions when the branch actually exists
|
|
150
|
-
// on origin — otherwise the `git fetch && git checkout` command we'd
|
|
151
|
-
// surface would fail for the user.
|
|
152
|
-
await reportFindings(client, card.id, result, lastPushedSha ? { branchName, branchUrl: recoveryUrl } : null);
|
|
153
|
-
await moveCardToColumn(client, card, config.verification.failColumn);
|
|
154
|
-
await client.endAgentSession(card.id, {
|
|
155
|
-
status: "failed",
|
|
156
|
-
failureReason: "verification",
|
|
157
|
-
failureSummary,
|
|
158
|
-
recoveryBranch: branchName,
|
|
159
|
-
...buildTokenPayload(sessionStats),
|
|
160
|
-
});
|
|
161
|
-
// Local-only cleanup. The remote ref under `agent-attempts/*` stays
|
|
162
|
-
// up; the GC sweep (worktree-gc.ts) prunes it after retention.
|
|
163
|
-
cleanupWorktree(worktreePath, branchName);
|
|
164
|
-
return false; // verification failed — counts as a failed attempt
|
|
165
|
-
}
|
|
166
|
-
log.info(TAG, `Verification passed for #${card.short_id}`);
|
|
167
|
-
}
|
|
168
|
-
// 3. Create PR
|
|
169
|
-
let prUrl = null;
|
|
170
|
-
if (config.completion.createPR) {
|
|
171
|
-
const provider = detectGitProvider(worktreePath);
|
|
172
|
-
prUrl = createPullRequest(card, branchName, worktreePath, config, provider);
|
|
173
|
-
}
|
|
174
|
-
// 4. Move card to Review column
|
|
175
|
-
if (config.completion.moveToColumn) {
|
|
176
|
-
await moveCardToColumn(client, card, config.completion.moveToColumn);
|
|
177
|
-
}
|
|
178
|
-
// 5. Post summary — always includes branch, optionally PR link
|
|
179
|
-
if (config.completion.postSummary) {
|
|
180
|
-
await postSummary(client, card, branchName, worktreePath, prUrl, config.worktree.baseBranch, sessionStats);
|
|
181
|
-
}
|
|
182
|
-
// 6. End agent session (include final token/cost snapshot)
|
|
183
|
-
await client.endAgentSession(card.id, {
|
|
184
|
-
status: "completed",
|
|
185
|
-
progressPercent: 100,
|
|
186
|
-
...buildTokenPayload(sessionStats),
|
|
187
|
-
});
|
|
188
|
-
// 6a. Episode write hook (Phase 1.5): completed implement runs accumulate
|
|
189
|
-
// into project-scoped episodic memory. Best-effort — failures never block
|
|
190
|
-
// the completion path (plan §"Write hook" + D8).
|
|
191
|
-
//
|
|
192
|
-
// Outcome is constant "success" here: verification failures return early
|
|
193
|
-
// above with status=paused, and D8 mandates paused/orphaned runs skip the
|
|
194
|
-
// episode write entirely. A failure-outcome episode would require routing
|
|
195
|
-
// a separate write hook into the pre-return path, which D8 intentionally
|
|
196
|
-
// omits ("daemon crashes ≠ task outcome").
|
|
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 ?? []);
|
|
206
|
-
await writeEpisode(client, {
|
|
207
|
-
kind: "implement",
|
|
208
|
-
card,
|
|
209
|
-
workspaceId,
|
|
210
|
-
outcome: "success",
|
|
211
|
-
approachSummary: sessionStats?.lastAssistantText ?? "",
|
|
212
|
-
approachBlocks: sessionStats?.assistantTextBlocks,
|
|
213
|
-
result: verificationResult,
|
|
214
|
-
cost: sessionStats?.cost ?? null,
|
|
215
|
-
filesEdited: sessionStats?.filesEdited ?? 0,
|
|
216
|
-
changedFiles,
|
|
217
|
-
churn: diffStat
|
|
218
|
-
? {
|
|
219
|
-
insertions: diffStat.insertions,
|
|
220
|
-
deletions: diffStat.deletions,
|
|
221
|
-
}
|
|
222
|
-
: undefined,
|
|
223
|
-
agentSessionId: agentSessionId ?? null,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
// 7. Cleanup worktree
|
|
227
|
-
cleanupWorktree(worktreePath, branchName);
|
|
228
|
-
log.info(TAG, `Completion done for #${card.short_id}${prUrl ? ` — PR: ${prUrl}` : ""}`);
|
|
229
|
-
return true;
|
|
230
|
-
}
|
|
231
|
-
function buildVerificationFailureSummary(result, autoFixAttempts) {
|
|
232
|
-
const counts = [];
|
|
233
|
-
if (result.buildErrors.length > 0) {
|
|
234
|
-
counts.push(`${result.buildErrors.length} build error(s)`);
|
|
235
|
-
}
|
|
236
|
-
if (result.lintWarnings.length > 0) {
|
|
237
|
-
counts.push(`${result.lintWarnings.length} lint issue(s)`);
|
|
238
|
-
}
|
|
239
|
-
if (result.reviewFindings.length > 0) {
|
|
240
|
-
counts.push(`${result.reviewFindings.length} review finding(s)`);
|
|
241
|
-
}
|
|
242
|
-
const head = counts.length > 0 ? counts.join(", ") : "verification failed";
|
|
243
|
-
const tail = autoFixAttempts > 0 ? ` after ${autoFixAttempts} auto-fix attempt(s)` : "";
|
|
244
|
-
return `${head}${tail}`;
|
|
245
|
-
}
|
|
246
|
-
function readHeadSha(worktreePath) {
|
|
247
|
-
try {
|
|
248
|
-
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
249
|
-
cwd: worktreePath,
|
|
250
|
-
encoding: "utf-8",
|
|
251
|
-
}).trim();
|
|
252
|
-
}
|
|
253
|
-
catch {
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
function checkHasCommits(worktreePath, baseBranch) {
|
|
258
|
-
try {
|
|
259
|
-
const count = execFileSync("git", ["rev-list", "--count", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
260
|
-
return parseInt(count, 10) > 0;
|
|
261
|
-
}
|
|
262
|
-
catch {
|
|
263
|
-
return false;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
async function postSummary(client, card, branchName, worktreePath, prUrl, baseBranch, sessionStats) {
|
|
267
|
-
// Build commit summary
|
|
268
|
-
let commitLog = "";
|
|
269
|
-
try {
|
|
270
|
-
commitLog = execFileSync("git", ["log", "--oneline", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
|
|
271
|
-
}
|
|
272
|
-
catch {
|
|
273
|
-
// best-effort
|
|
274
|
-
}
|
|
275
|
-
// Build description note
|
|
276
|
-
const SUMMARY_MARKER = "---\n**Agent completed**";
|
|
277
|
-
const parts = [`\n\n${SUMMARY_MARKER}`];
|
|
278
|
-
if (prUrl) {
|
|
279
|
-
parts.push(`PR: ${prUrl}`);
|
|
280
|
-
}
|
|
281
|
-
parts.push(`Branch: \`${branchName}\``);
|
|
282
|
-
if (sessionStats) {
|
|
283
|
-
const statParts = [];
|
|
284
|
-
statParts.push(`${sessionStats.toolCalls} tool calls`);
|
|
285
|
-
statParts.push(`${sessionStats.filesEdited} files edited`);
|
|
286
|
-
statParts.push(`${sessionStats.filesRead} files read`);
|
|
287
|
-
if (sessionStats.cost) {
|
|
288
|
-
statParts.push(`$${sessionStats.cost.totalCostUsd.toFixed(2)} cost`);
|
|
289
|
-
statParts.push(`${sessionStats.cost.numTurns} turns`);
|
|
290
|
-
const totalInput = sessionStats.cost.totalInputTokens +
|
|
291
|
-
sessionStats.cost.totalCacheCreationInputTokens +
|
|
292
|
-
sessionStats.cost.totalCacheReadInputTokens;
|
|
293
|
-
const totalTokens = totalInput + sessionStats.cost.totalOutputTokens;
|
|
294
|
-
if (totalTokens > 0) {
|
|
295
|
-
statParts.push(`${formatTokenCount(totalTokens)} tokens`);
|
|
296
|
-
}
|
|
297
|
-
const cacheRead = sessionStats.cost.totalCacheReadInputTokens;
|
|
298
|
-
if (totalInput > 0 && cacheRead > 0) {
|
|
299
|
-
const hitPct = Math.round((cacheRead / totalInput) * 100);
|
|
300
|
-
statParts.push(`${hitPct}% cache hit`);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
parts.push(`Stats: ${statParts.join(" · ")}`);
|
|
304
|
-
}
|
|
305
|
-
if (commitLog) {
|
|
306
|
-
parts.push(`\n\`\`\`\n${commitLog}\n\`\`\``);
|
|
307
|
-
}
|
|
308
|
-
try {
|
|
309
|
-
// Strip any previous agent summary block before appending (rework)
|
|
310
|
-
const existingDesc = card.description || "";
|
|
311
|
-
const baseDesc = existingDesc.includes(SUMMARY_MARKER)
|
|
312
|
-
? existingDesc.slice(0, existingDesc.indexOf(SUMMARY_MARKER)).trimEnd()
|
|
313
|
-
: existingDesc;
|
|
314
|
-
await client.updateCard(card.id, {
|
|
315
|
-
description: baseDesc + parts.join("\n"),
|
|
316
|
-
});
|
|
317
|
-
log.info(TAG, `Posted completion summary to #${card.short_id}`);
|
|
318
|
-
}
|
|
319
|
-
catch (err) {
|
|
320
|
-
log.error(TAG, `Failed to post summary: ${err instanceof Error ? err.message : err}`);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
|
|
2
|
-
import type { AgentConfig } from "./types.js";
|
|
3
|
-
export declare class ConfigValidationError extends Error {
|
|
4
|
-
readonly issues: string[];
|
|
5
|
-
constructor(message: string, issues: string[]);
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Validate that every column referenced in the agent config actually
|
|
9
|
-
* exists on the board. Labels that the daemon needs to create (agent,
|
|
10
|
-
* Ready to Merge, Merged, Need Review, agent-recovered) are not
|
|
11
|
-
* required to pre-exist — the daemon creates them on demand. Columns
|
|
12
|
-
* must already exist because moving a card to a non-existent column
|
|
13
|
-
* silently fails and leaves cards stuck.
|
|
14
|
-
*
|
|
15
|
-
* Fail-fast on any mismatch: the operator fixes their config OR the
|
|
16
|
-
* board before the daemon is useful.
|
|
17
|
-
*/
|
|
18
|
-
export declare function validateColumnReferences(client: HarmonyApiClient, projectId: string, config: AgentConfig): Promise<void>;
|
|
19
|
-
/**
|
|
20
|
-
* Same checks as validateColumnReferences but returns the unique list of
|
|
21
|
-
* validated column names (used by the startup banner). Throws on mismatch.
|
|
22
|
-
*/
|
|
23
|
-
export declare function validateAndListColumns(client: HarmonyApiClient, projectId: string, config: AgentConfig): Promise<string[]>;
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
export class ConfigValidationError extends Error {
|
|
2
|
-
issues;
|
|
3
|
-
constructor(message, issues) {
|
|
4
|
-
super(message);
|
|
5
|
-
this.issues = issues;
|
|
6
|
-
this.name = "ConfigValidationError";
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
function columnNames(board) {
|
|
10
|
-
return board.columns.map((c) => c.name);
|
|
11
|
-
}
|
|
12
|
-
function findColumn(board, name) {
|
|
13
|
-
const target = name.toLowerCase();
|
|
14
|
-
return board.columns.some((c) => c.name.toLowerCase() === target);
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Validate that every column referenced in the agent config actually
|
|
18
|
-
* exists on the board. Labels that the daemon needs to create (agent,
|
|
19
|
-
* Ready to Merge, Merged, Need Review, agent-recovered) are not
|
|
20
|
-
* required to pre-exist — the daemon creates them on demand. Columns
|
|
21
|
-
* must already exist because moving a card to a non-existent column
|
|
22
|
-
* silently fails and leaves cards stuck.
|
|
23
|
-
*
|
|
24
|
-
* Fail-fast on any mismatch: the operator fixes their config OR the
|
|
25
|
-
* board before the daemon is useful.
|
|
26
|
-
*/
|
|
27
|
-
export async function validateColumnReferences(client, projectId, config) {
|
|
28
|
-
const board = (await client.getBoard(projectId, {
|
|
29
|
-
summary: true,
|
|
30
|
-
}));
|
|
31
|
-
const known = columnNames(board);
|
|
32
|
-
const issues = [];
|
|
33
|
-
const required = [
|
|
34
|
-
...config.pickupColumns.map((c) => ({ value: c, where: "pickupColumns" })),
|
|
35
|
-
{
|
|
36
|
-
value: config.completion.moveToColumn,
|
|
37
|
-
where: "completion.moveToColumn",
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
value: config.verification.failColumn,
|
|
41
|
-
where: "verification.failColumn",
|
|
42
|
-
},
|
|
43
|
-
];
|
|
44
|
-
if (config.review.enabled) {
|
|
45
|
-
for (const c of config.review.pickupColumns) {
|
|
46
|
-
required.push({ value: c, where: "review.pickupColumns" });
|
|
47
|
-
}
|
|
48
|
-
required.push({ value: config.review.moveToColumn, where: "review.moveToColumn" }, { value: config.review.failColumn, where: "review.failColumn" });
|
|
49
|
-
}
|
|
50
|
-
for (const { value, where } of required) {
|
|
51
|
-
if (!value)
|
|
52
|
-
continue;
|
|
53
|
-
if (!findColumn(board, value)) {
|
|
54
|
-
issues.push(`${where}: column "${value}" not found on board`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (issues.length > 0) {
|
|
58
|
-
const help = `Available columns: ${known.join(", ")}`;
|
|
59
|
-
throw new ConfigValidationError(`Invalid agent config — the following columns are missing:\n - ${issues.join("\n - ")}\n${help}`, issues);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Same checks as validateColumnReferences but returns the unique list of
|
|
64
|
-
* validated column names (used by the startup banner). Throws on mismatch.
|
|
65
|
-
*/
|
|
66
|
-
export async function validateAndListColumns(client, projectId, config) {
|
|
67
|
-
await validateColumnReferences(client, projectId, config);
|
|
68
|
-
const names = [
|
|
69
|
-
...config.pickupColumns,
|
|
70
|
-
config.completion.moveToColumn,
|
|
71
|
-
config.verification.failColumn,
|
|
72
|
-
];
|
|
73
|
-
if (config.review.enabled) {
|
|
74
|
-
names.push(...config.review.pickupColumns, config.review.moveToColumn, config.review.failColumn);
|
|
75
|
-
}
|
|
76
|
-
return Array.from(new Set(names.filter(Boolean)));
|
|
77
|
-
}
|
package/dist/config.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
|
|
2
|
-
import type { AgentConfig, RealtimeCredentials } from "./types.js";
|
|
3
|
-
export interface DaemonConfig {
|
|
4
|
-
apiKey: string;
|
|
5
|
-
apiUrl: string;
|
|
6
|
-
workspaceId: string;
|
|
7
|
-
projectId: string;
|
|
8
|
-
userEmail: string;
|
|
9
|
-
agent: AgentConfig;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Load daemon configuration from ~/.harmony-mcp/config.json,
|
|
13
|
-
* merging default agent settings with any user overrides.
|
|
14
|
-
*/
|
|
15
|
-
export declare function loadDaemonConfig(): DaemonConfig;
|
|
16
|
-
/**
|
|
17
|
-
* Fetch Supabase realtime credentials from the Harmony API.
|
|
18
|
-
*/
|
|
19
|
-
export declare function fetchRealtimeCredentials(client: HarmonyApiClient): Promise<RealtimeCredentials>;
|
|
20
|
-
/**
|
|
21
|
-
* Create an API client from daemon config.
|
|
22
|
-
*/
|
|
23
|
-
export declare function createApiClient(config: DaemonConfig): HarmonyApiClient;
|
package/dist/config.js
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
|
|
6
|
-
import { getActiveProjectId, getActiveWorkspaceId, getApiKey, getApiUrl, getUserEmail, } from "@gethmy/mcp/src/config.js";
|
|
7
|
-
import { DEFAULT_AGENT_CONFIG } from "./types.js";
|
|
8
|
-
/**
|
|
9
|
-
* Resolve the git repo root so local config lookup always
|
|
10
|
-
* finds .harmony-mcp.json regardless of cwd.
|
|
11
|
-
*/
|
|
12
|
-
function getRepoRoot() {
|
|
13
|
-
return execSync("git rev-parse --show-toplevel", {
|
|
14
|
-
encoding: "utf-8",
|
|
15
|
-
}).trim();
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Load daemon configuration from ~/.harmony-mcp/config.json,
|
|
19
|
-
* merging default agent settings with any user overrides.
|
|
20
|
-
*/
|
|
21
|
-
export function loadDaemonConfig() {
|
|
22
|
-
const repoRoot = getRepoRoot();
|
|
23
|
-
const apiKey = getApiKey();
|
|
24
|
-
const apiUrl = getApiUrl();
|
|
25
|
-
const workspaceId = getActiveWorkspaceId(repoRoot);
|
|
26
|
-
const projectId = getActiveProjectId(repoRoot);
|
|
27
|
-
const userEmail = getUserEmail();
|
|
28
|
-
if (!workspaceId) {
|
|
29
|
-
throw new Error("No active workspace configured. Run `npx @gethmy/mcp setup` first.");
|
|
30
|
-
}
|
|
31
|
-
if (!projectId) {
|
|
32
|
-
throw new Error("No active project configured. Run `npx @gethmy/mcp setup` first.");
|
|
33
|
-
}
|
|
34
|
-
if (!userEmail) {
|
|
35
|
-
throw new Error("No user email configured. Run `npx @gethmy/mcp setup` first.");
|
|
36
|
-
}
|
|
37
|
-
// Try to load agent-specific overrides from config file
|
|
38
|
-
let agentOverrides = {};
|
|
39
|
-
try {
|
|
40
|
-
const configPath = join(homedir(), ".harmony-mcp", "config.json");
|
|
41
|
-
const raw = readFileSync(configPath, "utf-8");
|
|
42
|
-
const parsed = JSON.parse(raw);
|
|
43
|
-
if (parsed.agent) {
|
|
44
|
-
agentOverrides = parsed.agent;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
// No overrides, use defaults
|
|
49
|
-
}
|
|
50
|
-
const agent = {
|
|
51
|
-
...DEFAULT_AGENT_CONFIG,
|
|
52
|
-
...agentOverrides,
|
|
53
|
-
completion: {
|
|
54
|
-
...DEFAULT_AGENT_CONFIG.completion,
|
|
55
|
-
...(agentOverrides.completion ?? {}),
|
|
56
|
-
},
|
|
57
|
-
claude: {
|
|
58
|
-
...DEFAULT_AGENT_CONFIG.claude,
|
|
59
|
-
...(agentOverrides.claude ?? {}),
|
|
60
|
-
},
|
|
61
|
-
worktree: {
|
|
62
|
-
...DEFAULT_AGENT_CONFIG.worktree,
|
|
63
|
-
...(agentOverrides.worktree ?? {}),
|
|
64
|
-
},
|
|
65
|
-
verification: {
|
|
66
|
-
...DEFAULT_AGENT_CONFIG.verification,
|
|
67
|
-
...(agentOverrides.verification ?? {}),
|
|
68
|
-
},
|
|
69
|
-
review: {
|
|
70
|
-
...DEFAULT_AGENT_CONFIG.review,
|
|
71
|
-
...(agentOverrides.review ?? {}),
|
|
72
|
-
},
|
|
73
|
-
budget: {
|
|
74
|
-
...DEFAULT_AGENT_CONFIG.budget,
|
|
75
|
-
...(agentOverrides.budget ?? {}),
|
|
76
|
-
},
|
|
77
|
-
http: {
|
|
78
|
-
...DEFAULT_AGENT_CONFIG.http,
|
|
79
|
-
...(agentOverrides.http ?? {}),
|
|
80
|
-
},
|
|
81
|
-
timing: {
|
|
82
|
-
...DEFAULT_AGENT_CONFIG.timing,
|
|
83
|
-
...(agentOverrides.timing ?? {}),
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
return { apiKey, apiUrl, workspaceId, projectId, userEmail, agent };
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Fetch Supabase realtime credentials from the Harmony API.
|
|
90
|
-
*/
|
|
91
|
-
export async function fetchRealtimeCredentials(client) {
|
|
92
|
-
const result = (await client.request("GET", "/config/realtime"));
|
|
93
|
-
if (!result.supabaseUrl || !result.supabaseAnonKey) {
|
|
94
|
-
throw new Error("Invalid realtime credentials response from API");
|
|
95
|
-
}
|
|
96
|
-
return result;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Create an API client from daemon config.
|
|
100
|
-
*/
|
|
101
|
-
export function createApiClient(config) {
|
|
102
|
-
return new HarmonyApiClient({ apiKey: config.apiKey, apiUrl: config.apiUrl });
|
|
103
|
-
}
|
package/dist/episode-writer.d.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import type { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
|
|
2
|
-
import type { Card } from "@harmony/shared";
|
|
3
|
-
import type { CostUpdate } from "./stream-parser.js";
|
|
4
|
-
import type { EpisodeMeta, EpisodeOutcome } from "./types.js";
|
|
5
|
-
import type { VerificationResult } from "./verification.js";
|
|
6
|
-
interface ImplementEpisodeInput {
|
|
7
|
-
kind: "implement";
|
|
8
|
-
card: Card;
|
|
9
|
-
workspaceId: string;
|
|
10
|
-
outcome: EpisodeOutcome;
|
|
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[];
|
|
18
|
-
result: VerificationResult;
|
|
19
|
-
cost: CostUpdate | null;
|
|
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
|
-
};
|
|
28
|
-
errorMessage?: string;
|
|
29
|
-
agentSessionId?: string | null;
|
|
30
|
-
}
|
|
31
|
-
interface ReviewEpisodeInput {
|
|
32
|
-
kind: "review";
|
|
33
|
-
card: Card;
|
|
34
|
-
workspaceId: string;
|
|
35
|
-
verdict: "approved" | "rejected";
|
|
36
|
-
summary: string;
|
|
37
|
-
cost: CostUpdate | null;
|
|
38
|
-
agentSessionId?: string | null;
|
|
39
|
-
reviewSessionId?: string | null;
|
|
40
|
-
originalEpisodeId?: string | null;
|
|
41
|
-
}
|
|
42
|
-
export type EpisodeInput = ImplementEpisodeInput | ReviewEpisodeInput;
|
|
43
|
-
/**
|
|
44
|
-
* Rule-derived quality score (0..1) for an implement run. Failures default to 0.
|
|
45
|
-
* Plan §"Quality score": +0.4 if build passed, +0.2 if lint passed, +0.2 if no
|
|
46
|
-
* error thrown, +0.2 if run completed cleanly.
|
|
47
|
-
*/
|
|
48
|
-
export declare function computeQualityScore(result: VerificationResult, opts: {
|
|
49
|
-
errorThrown: boolean;
|
|
50
|
-
runCompletedCleanly: boolean;
|
|
51
|
-
}): number;
|
|
52
|
-
/**
|
|
53
|
-
* Trim a free-form summary down to the documented 400-char cap. v1 uses a
|
|
54
|
-
* last-turn trim rather than an LLM rewrite (plan §"Write hook"). Empty or
|
|
55
|
-
* whitespace-only input collapses to a marker so the episode still surfaces
|
|
56
|
-
* as a recallable hit (rather than an empty bullet) in future prompts.
|
|
57
|
-
*/
|
|
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;
|
|
78
|
-
/**
|
|
79
|
-
* Build the entity payload for one episode. Pure — returned object can be
|
|
80
|
-
* snapshotted in tests without hitting the network.
|
|
81
|
-
*/
|
|
82
|
-
export declare function buildEpisodePayload(input: EpisodeInput, projectId: string): {
|
|
83
|
-
workspace_id: string;
|
|
84
|
-
project_id?: string;
|
|
85
|
-
type: string;
|
|
86
|
-
memory_tier: string;
|
|
87
|
-
scope: string;
|
|
88
|
-
title: string;
|
|
89
|
-
content: string;
|
|
90
|
-
metadata: EpisodeMeta;
|
|
91
|
-
importance: number;
|
|
92
|
-
confidence: number;
|
|
93
|
-
tags: string[];
|
|
94
|
-
agent_identifier: string;
|
|
95
|
-
};
|
|
96
|
-
/**
|
|
97
|
-
* Write one episode entity. Best-effort: any failure is logged and swallowed
|
|
98
|
-
* so the calling pipeline can complete (plan D8: episode writes never block
|
|
99
|
-
* run completion).
|
|
100
|
-
*
|
|
101
|
-
* Returns the entity id on success, or null on swallowed failure.
|
|
102
|
-
*/
|
|
103
|
-
export declare function writeEpisode(client: HarmonyApiClient, input: EpisodeInput): Promise<string | null>;
|
|
104
|
-
/**
|
|
105
|
-
* Find the most recent implement episode for a given card so the review
|
|
106
|
-
* pipeline can back-fill its verdict. Returns null when none exists or the
|
|
107
|
-
* lookup throws — back-fill is best-effort.
|
|
108
|
-
*/
|
|
109
|
-
export declare function findLatestImplementEpisode(client: HarmonyApiClient, workspaceId: string, projectId: string, cardShortId: number): Promise<string | null>;
|
|
110
|
-
/**
|
|
111
|
-
* Apply the review verdict to an earlier implement episode (plan §"Read hook"
|
|
112
|
-
* back-fill block). Approved nudges the original episode's confidence up;
|
|
113
|
-
* rejected tombstones it via superseded_by.
|
|
114
|
-
*/
|
|
115
|
-
export declare function backfillReviewVerdict(client: HarmonyApiClient, originalEpisodeId: string, verdict: "approved" | "rejected", reviewEpisodeId: string | null): Promise<void>;
|
|
116
|
-
export {};
|