@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.
Files changed (77) hide show
  1. package/dist/cli.js +6376 -141
  2. package/dist/index.js +6206 -333
  3. package/package.json +2 -2
  4. package/dist/board-helpers.d.ts +0 -31
  5. package/dist/board-helpers.js +0 -150
  6. package/dist/budget.d.ts +0 -39
  7. package/dist/budget.js +0 -73
  8. package/dist/cli.d.ts +0 -14
  9. package/dist/completion.d.ts +0 -36
  10. package/dist/completion.js +0 -322
  11. package/dist/config-validation.d.ts +0 -23
  12. package/dist/config-validation.js +0 -77
  13. package/dist/config.d.ts +0 -23
  14. package/dist/config.js +0 -103
  15. package/dist/episode-writer.d.ts +0 -116
  16. package/dist/episode-writer.js +0 -349
  17. package/dist/git-diff-stat.d.ts +0 -24
  18. package/dist/git-diff-stat.js +0 -56
  19. package/dist/git-pr.d.ts +0 -38
  20. package/dist/git-pr.js +0 -399
  21. package/dist/http-server.d.ts +0 -66
  22. package/dist/http-server.js +0 -96
  23. package/dist/index.d.ts +0 -5
  24. package/dist/log.d.ts +0 -34
  25. package/dist/log.js +0 -100
  26. package/dist/merge-monitor.d.ts +0 -23
  27. package/dist/merge-monitor.js +0 -169
  28. package/dist/pm.d.ts +0 -14
  29. package/dist/pm.js +0 -63
  30. package/dist/pool.d.ts +0 -71
  31. package/dist/pool.js +0 -259
  32. package/dist/process-group.d.ts +0 -26
  33. package/dist/process-group.js +0 -72
  34. package/dist/progress-tracker.d.ts +0 -82
  35. package/dist/progress-tracker.js +0 -457
  36. package/dist/prompt.d.ts +0 -23
  37. package/dist/prompt.js +0 -160
  38. package/dist/queue.d.ts +0 -39
  39. package/dist/queue.js +0 -100
  40. package/dist/reconcile.d.ts +0 -35
  41. package/dist/reconcile.js +0 -174
  42. package/dist/recovery.d.ts +0 -30
  43. package/dist/recovery.js +0 -141
  44. package/dist/review-completion.d.ts +0 -35
  45. package/dist/review-completion.js +0 -475
  46. package/dist/review-knowledge.d.ts +0 -14
  47. package/dist/review-knowledge.js +0 -89
  48. package/dist/review-prompt.d.ts +0 -12
  49. package/dist/review-prompt.js +0 -103
  50. package/dist/review-worker.d.ts +0 -56
  51. package/dist/review-worker.js +0 -638
  52. package/dist/review-worktree.d.ts +0 -12
  53. package/dist/review-worktree.js +0 -95
  54. package/dist/run-log.d.ts +0 -6
  55. package/dist/run-log.js +0 -19
  56. package/dist/startup-banner.d.ts +0 -29
  57. package/dist/startup-banner.js +0 -143
  58. package/dist/state-store.d.ts +0 -89
  59. package/dist/state-store.js +0 -230
  60. package/dist/stream-parser-selftest.d.ts +0 -9
  61. package/dist/stream-parser-selftest.js +0 -97
  62. package/dist/stream-parser.d.ts +0 -43
  63. package/dist/stream-parser.js +0 -174
  64. package/dist/transitions.d.ts +0 -57
  65. package/dist/transitions.js +0 -131
  66. package/dist/types.d.ts +0 -167
  67. package/dist/types.js +0 -76
  68. package/dist/verification.d.ts +0 -39
  69. package/dist/verification.js +0 -317
  70. package/dist/watcher.d.ts +0 -53
  71. package/dist/watcher.js +0 -153
  72. package/dist/worker.d.ts +0 -54
  73. package/dist/worker.js +0 -507
  74. package/dist/worktree-gc.d.ts +0 -67
  75. package/dist/worktree-gc.js +0 -245
  76. package/dist/worktree.d.ts +0 -18
  77. package/dist/worktree.js +0 -177
@@ -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
- }
@@ -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 {};