@gethmy/agent 1.7.0 → 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 (76) hide show
  1. package/README.md +8 -1
  2. package/dist/cli.js +6376 -205
  3. package/dist/index.js +6206 -341
  4. package/package.json +2 -2
  5. package/dist/board-helpers.d.ts +0 -31
  6. package/dist/board-helpers.js +0 -150
  7. package/dist/budget.d.ts +0 -47
  8. package/dist/budget.js +0 -161
  9. package/dist/cli.d.ts +0 -16
  10. package/dist/completion.d.ts +0 -32
  11. package/dist/completion.js +0 -304
  12. package/dist/config-validation.d.ts +0 -23
  13. package/dist/config-validation.js +0 -77
  14. package/dist/config.d.ts +0 -23
  15. package/dist/config.js +0 -103
  16. package/dist/episode-writer.d.ts +0 -84
  17. package/dist/episode-writer.js +0 -232
  18. package/dist/git-pr.d.ts +0 -38
  19. package/dist/git-pr.js +0 -399
  20. package/dist/http-server.d.ts +0 -79
  21. package/dist/http-server.js +0 -114
  22. package/dist/index.d.ts +0 -5
  23. package/dist/log.d.ts +0 -34
  24. package/dist/log.js +0 -100
  25. package/dist/merge-monitor.d.ts +0 -23
  26. package/dist/merge-monitor.js +0 -169
  27. package/dist/pm.d.ts +0 -14
  28. package/dist/pm.js +0 -63
  29. package/dist/pool.d.ts +0 -70
  30. package/dist/pool.js +0 -258
  31. package/dist/process-group.d.ts +0 -26
  32. package/dist/process-group.js +0 -72
  33. package/dist/progress-tracker.d.ts +0 -79
  34. package/dist/progress-tracker.js +0 -442
  35. package/dist/prompt.d.ts +0 -18
  36. package/dist/prompt.js +0 -117
  37. package/dist/queue.d.ts +0 -39
  38. package/dist/queue.js +0 -100
  39. package/dist/reconcile.d.ts +0 -35
  40. package/dist/reconcile.js +0 -174
  41. package/dist/recovery.d.ts +0 -30
  42. package/dist/recovery.js +0 -141
  43. package/dist/review-completion.d.ts +0 -40
  44. package/dist/review-completion.js +0 -474
  45. package/dist/review-knowledge.d.ts +0 -14
  46. package/dist/review-knowledge.js +0 -89
  47. package/dist/review-prompt.d.ts +0 -12
  48. package/dist/review-prompt.js +0 -103
  49. package/dist/review-worker.d.ts +0 -56
  50. package/dist/review-worker.js +0 -638
  51. package/dist/review-worktree.d.ts +0 -12
  52. package/dist/review-worktree.js +0 -95
  53. package/dist/run-log.d.ts +0 -6
  54. package/dist/run-log.js +0 -19
  55. package/dist/startup-banner.d.ts +0 -29
  56. package/dist/startup-banner.js +0 -143
  57. package/dist/state-store.d.ts +0 -88
  58. package/dist/state-store.js +0 -239
  59. package/dist/stream-parser-selftest.d.ts +0 -9
  60. package/dist/stream-parser-selftest.js +0 -97
  61. package/dist/stream-parser.d.ts +0 -43
  62. package/dist/stream-parser.js +0 -174
  63. package/dist/transitions.d.ts +0 -57
  64. package/dist/transitions.js +0 -131
  65. package/dist/types.d.ts +0 -140
  66. package/dist/types.js +0 -79
  67. package/dist/verification.d.ts +0 -39
  68. package/dist/verification.js +0 -317
  69. package/dist/watcher.d.ts +0 -53
  70. package/dist/watcher.js +0 -153
  71. package/dist/worker.d.ts +0 -53
  72. package/dist/worker.js +0 -464
  73. package/dist/worktree-gc.d.ts +0 -67
  74. package/dist/worktree-gc.js +0 -245
  75. package/dist/worktree.d.ts +0 -18
  76. package/dist/worktree.js +0 -177
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/agent",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "description": "Push-based agent daemon for Harmony — watches board assignments and spawns Claude CLI workers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -38,7 +38,7 @@
38
38
  "scripts": {
39
39
  "start": "node dist/index.js",
40
40
  "prebuild": "cd ../harmony-shared && bun run build && cd ../memory && bun run build",
41
- "build": "rm -rf dist && tsc -p tsconfig.build.json",
41
+ "build": "rm -rf dist && bun build src/index.ts src/cli.ts --outdir dist --target node --external @supabase/supabase-js --external @gethmy/mcp --external \"@gethmy/mcp/*\"",
42
42
  "typecheck": "tsc --noEmit",
43
43
  "prepublishOnly": "npm run build",
44
44
  "test": "vitest run"
@@ -1,31 +0,0 @@
1
- import type { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
2
- import type { Card, Label } from "@harmony/shared";
3
- /**
4
- * Build a label lookup map from board-level label definitions.
5
- */
6
- export declare function buildLabelMap(boardLabels: Label[]): Map<string, Label>;
7
- /**
8
- * Resolve a card's `labelIds` to full Label objects using a label map.
9
- */
10
- export declare function resolveCardLabels(card: Card, labelMap: Map<string, Label>): Label[];
11
- /**
12
- * Check if a card has a label by name (case-insensitive).
13
- */
14
- export declare function hasLabel(cardLabels: Label[], labelName: string): boolean;
15
- /**
16
- * Move a card to a column by name. Fetches the board to resolve column ID.
17
- */
18
- export declare function moveCardToColumn(client: HarmonyApiClient, card: Card, targetColumnName: string): Promise<void>;
19
- /**
20
- * Find a label by name (case-insensitive) or create it if missing.
21
- */
22
- export declare function findOrCreateLabel(client: HarmonyApiClient, projectId: string, labelName: string, color?: string): Promise<string | null>;
23
- /**
24
- * Add a label to a card by name, creating the label if it doesn't exist.
25
- */
26
- export declare function addLabelByName(client: HarmonyApiClient, card: Card, labelName: string, color?: string): Promise<void>;
27
- /**
28
- * Move a card to a column and add a label in one pass, fetching the board only once.
29
- * Returns true if the move succeeded, false otherwise.
30
- */
31
- export declare function moveCardAndAddLabel(client: HarmonyApiClient, card: Card, targetColumnName: string, labelName: string, labelColor?: string): Promise<boolean>;
@@ -1,150 +0,0 @@
1
- import { log } from "./log.js";
2
- const TAG = "board";
3
- /**
4
- * Build a label lookup map from board-level label definitions.
5
- */
6
- export function buildLabelMap(boardLabels) {
7
- const map = new Map();
8
- for (const label of boardLabels) {
9
- map.set(label.id, label);
10
- }
11
- return map;
12
- }
13
- /**
14
- * Resolve a card's `labelIds` to full Label objects using a label map.
15
- */
16
- export function resolveCardLabels(card, labelMap) {
17
- const ids = card.labelIds ?? [];
18
- return ids
19
- .map((id) => labelMap.get(id))
20
- .filter((l) => l !== undefined);
21
- }
22
- /**
23
- * Check if a card has a label by name (case-insensitive).
24
- */
25
- export function hasLabel(cardLabels, labelName) {
26
- return cardLabels.some((l) => l.name.toLowerCase() === labelName.toLowerCase());
27
- }
28
- /**
29
- * Move a card to a column by name. Fetches the board to resolve column ID.
30
- */
31
- export async function moveCardToColumn(client, card, targetColumnName) {
32
- try {
33
- const board = await client.getBoard(card.project_id);
34
- const targetColumn = board.columns.find((c) => c.name.toLowerCase() === targetColumnName.toLowerCase());
35
- if (!targetColumn) {
36
- log.warn(TAG, `Column "${targetColumnName}" not found, skipping move`);
37
- return;
38
- }
39
- if (card.column_id === targetColumn.id) {
40
- log.debug(TAG, `Card already in "${targetColumnName}"`);
41
- return;
42
- }
43
- await client.moveCard(card.id, targetColumn.id);
44
- log.info(TAG, `Moved #${card.short_id} to "${targetColumnName}"`);
45
- }
46
- catch (err) {
47
- log.error(TAG, `Failed to move card: ${err instanceof Error ? err.message : err}`);
48
- }
49
- }
50
- /**
51
- * Find a label by name (case-insensitive) or create it if missing.
52
- */
53
- export async function findOrCreateLabel(client, projectId, labelName, color = "#22c55e") {
54
- try {
55
- const board = await client.getBoard(projectId);
56
- const labels = board.labels ?? [];
57
- const existing = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
58
- if (existing)
59
- return existing.id;
60
- const result = await client.createLabel(projectId, {
61
- name: labelName,
62
- color,
63
- });
64
- const labelId = result?.label?.id;
65
- if (labelId) {
66
- log.info(TAG, `Created label "${labelName}"`);
67
- return labelId;
68
- }
69
- log.warn(TAG, `createLabel succeeded but returned no id`);
70
- return null;
71
- }
72
- catch (err) {
73
- log.error(TAG, `Failed to find/create label "${labelName}": ${err instanceof Error ? err.message : err}`);
74
- return null;
75
- }
76
- }
77
- /**
78
- * Add a label to a card by name, creating the label if it doesn't exist.
79
- */
80
- export async function addLabelByName(client, card, labelName, color) {
81
- const labelId = await findOrCreateLabel(client, card.project_id, labelName, color);
82
- if (!labelId)
83
- return;
84
- try {
85
- await client.addLabelToCard(card.id, labelId);
86
- log.info(TAG, `Added label "${labelName}" to #${card.short_id}`);
87
- }
88
- catch (err) {
89
- log.error(TAG, `Failed to add label to card: ${err instanceof Error ? err.message : err}`);
90
- }
91
- }
92
- /**
93
- * Move a card to a column and add a label in one pass, fetching the board only once.
94
- * Returns true if the move succeeded, false otherwise.
95
- */
96
- export async function moveCardAndAddLabel(client, card, targetColumnName, labelName, labelColor = "#8b5cf6") {
97
- let moved = false;
98
- try {
99
- const board = await client.getBoard(card.project_id);
100
- const columns = board.columns;
101
- const labels = board.labels ?? [];
102
- // Move card
103
- const targetColumn = columns.find((c) => c.name.toLowerCase() === targetColumnName.toLowerCase());
104
- if (!targetColumn) {
105
- log.warn(TAG, `Column "${targetColumnName}" not found on board (available: ${columns.map((c) => c.name).join(", ")})`);
106
- }
107
- else if (card.column_id === targetColumn.id) {
108
- log.debug(TAG, `Card #${card.short_id} already in "${targetColumnName}"`);
109
- moved = true;
110
- }
111
- else {
112
- try {
113
- await client.moveCard(card.id, targetColumn.id);
114
- log.info(TAG, `Moved #${card.short_id} to "${targetColumnName}"`);
115
- moved = true;
116
- }
117
- catch (err) {
118
- log.error(TAG, `Failed to move #${card.short_id} to "${targetColumnName}": ${err instanceof Error ? err.message : err}`);
119
- }
120
- }
121
- // Resolve label ID (find or create)
122
- let labelId;
123
- const existing = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
124
- if (existing) {
125
- labelId = existing.id;
126
- }
127
- else {
128
- const result = await client.createLabel(card.project_id, {
129
- name: labelName,
130
- color: labelColor,
131
- });
132
- labelId = result?.label?.id;
133
- if (labelId)
134
- log.info(TAG, `Created label "${labelName}"`);
135
- }
136
- if (labelId) {
137
- try {
138
- await client.addLabelToCard(card.id, labelId);
139
- log.info(TAG, `Added label "${labelName}" to #${card.short_id}`);
140
- }
141
- catch (err) {
142
- log.error(TAG, `Failed to add label "${labelName}" to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
143
- }
144
- }
145
- }
146
- catch (err) {
147
- log.error(TAG, `Failed to prepare move/label for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
148
- }
149
- return moved;
150
- }
package/dist/budget.d.ts DELETED
@@ -1,47 +0,0 @@
1
- import type { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
2
- import type { Card } from "@harmony/shared";
3
- import type { StateStore } from "./state-store.js";
4
- import type { AgentConfig } from "./types.js";
5
- export type GuardDecision = {
6
- allow: true;
7
- } | {
8
- allow: false;
9
- reason: GuardReason;
10
- detail: string;
11
- };
12
- export type GuardReason = "dlq" | "max_attempts" | "card_cost_cap" | "daily_budget";
13
- /**
14
- * BudgetGuard is consulted on every pickup and on every run start.
15
- * It protects the daemon from three failure modes:
16
- * 1. Cards that can never succeed (DLQ after N failed attempts).
17
- * 2. Cards that burn unbounded tokens on a single attempt.
18
- * 3. Runaway daily spend across the entire daemon.
19
- *
20
- * The guard is advisory for the hot path (returns a decision); the
21
- * caller is responsible for marking DLQ and skipping the enqueue.
22
- */
23
- export declare class BudgetGuard {
24
- private config;
25
- private store;
26
- constructor(config: AgentConfig["budget"], store: StateStore);
27
- /**
28
- * Inspect a card before we commit to picking it up. If any threshold
29
- * is already exceeded, return a skip decision — the caller should
30
- * apply the DLQ label (for `dlq`/`max_attempts`/`card_cost_cap`) or
31
- * simply hold until the daily budget resets (`daily_budget`).
32
- */
33
- 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
- }
package/dist/budget.js DELETED
@@ -1,161 +0,0 @@
1
- import { log } from "./log.js";
2
- import { runTransition } from "./transitions.js";
3
- const TAG = "budget";
4
- /**
5
- * BudgetGuard is consulted on every pickup and on every run start.
6
- * It protects the daemon from three failure modes:
7
- * 1. Cards that can never succeed (DLQ after N failed attempts).
8
- * 2. Cards that burn unbounded tokens on a single attempt.
9
- * 3. Runaway daily spend across the entire daemon.
10
- *
11
- * The guard is advisory for the hot path (returns a decision); the
12
- * caller is responsible for marking DLQ and skipping the enqueue.
13
- */
14
- export class BudgetGuard {
15
- config;
16
- store;
17
- constructor(config, store) {
18
- this.config = config;
19
- this.store = store;
20
- }
21
- /**
22
- * Inspect a card before we commit to picking it up. If any threshold
23
- * is already exceeded, return a skip decision — the caller should
24
- * apply the DLQ label (for `dlq`/`max_attempts`/`card_cost_cap`) or
25
- * simply hold until the daily budget resets (`daily_budget`).
26
- */
27
- check(cardId) {
28
- if (this.store.isDlq(cardId)) {
29
- const rec = this.store.getCard(cardId);
30
- return {
31
- allow: false,
32
- reason: "dlq",
33
- detail: rec?.dlqReason ?? "previously marked DLQ",
34
- };
35
- }
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
- const dailySpent = this.store.getDailyCostCents();
54
- if (dailySpent >= this.config.dailyBudgetCents) {
55
- return {
56
- allow: false,
57
- reason: "daily_budget",
58
- detail: `daily budget ${formatCents(dailySpent)}/${formatCents(this.config.dailyBudgetCents)} exhausted`,
59
- };
60
- }
61
- return { allow: true };
62
- }
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
- }
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**";
111
- function buildDlqDescriptionBlock(reason, detail, failures) {
112
- const lines = [
113
- DLQ_FENCE_START,
114
- "---",
115
- "**Agent DLQ**",
116
- `Cap hit: ${reason} — ${detail}`,
117
- ];
118
- if (failures.length > 0) {
119
- lines.push("", "Recent failures:");
120
- for (const f of failures) {
121
- const when = new Date(f.ts).toISOString().replace("T", " ").slice(0, 16);
122
- const tag = f.reason ? ` [${f.reason}]` : "";
123
- const branch = f.recoveryBranch
124
- ? `\n recover: \`git fetch && git checkout ${f.recoveryBranch}\``
125
- : "";
126
- lines.push(`- ${when} UTC${tag} — ${f.summary}${branch}`);
127
- }
128
- }
129
- else {
130
- lines.push("", "_No prior failure summaries recorded._");
131
- }
132
- lines.push(DLQ_FENCE_END);
133
- return lines.join("\n");
134
- }
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
- function formatCents(cents) {
160
- return `$${(cents / 100).toFixed(2)}`;
161
- }
package/dist/cli.d.ts DELETED
@@ -1,16 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Harmony Agent CLI entry point.
4
- *
5
- * Subcommands:
6
- * (no args) — run the daemon (same as `run`)
7
- * run — run the daemon
8
- * status — GET /status from the running daemon, pretty-print
9
- * health — GET /health, exit 0 if healthy, 1 otherwise
10
- * doctor — run preflight checks without starting the daemon
11
- * gc — one-shot worktree garbage collection
12
- * dlq list — print DLQ entries
13
- * dlq clear <id> — clear a card's DLQ mark
14
- * help — show usage
15
- */
16
- export {};
@@ -1,32 +0,0 @@
1
- import type { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
2
- import type { Card } from "@harmony/shared";
3
- import type { StateStore } from "./state-store.js";
4
- import type { CostUpdate } from "./stream-parser.js";
5
- import { type AgentConfig } from "./types.js";
6
- export interface SessionStats {
7
- filesEdited: number;
8
- filesRead: number;
9
- toolCalls: number;
10
- cost: CostUpdate | null;
11
- /** Trimmed last assistant text — feeds the episode write hook (Phase 1.5). */
12
- lastAssistantText?: string;
13
- }
14
- export declare function buildTokenPayload(stats?: SessionStats | null): {
15
- costCents?: undefined;
16
- inputTokens?: undefined;
17
- outputTokens?: undefined;
18
- cacheCreationInputTokens?: undefined;
19
- cacheReadInputTokens?: undefined;
20
- modelName?: undefined;
21
- } | {
22
- costCents: number;
23
- inputTokens: number;
24
- outputTokens: number;
25
- cacheCreationInputTokens: number;
26
- cacheReadInputTokens: number;
27
- modelName: string | undefined;
28
- };
29
- /**
30
- * Post-work pipeline: push branch, create PR, move card, post summary.
31
- */
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>;