@gethmy/agent 1.7.1 → 1.7.3

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 +6386 -141
  2. package/dist/index.js +6216 -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,95 +0,0 @@
1
- import { execFileSync, execSync } from "node:child_process";
2
- import { existsSync } from "node:fs";
3
- import { resolve } from "node:path";
4
- import { log } from "./log.js";
5
- import { installCommand } from "./pm.js";
6
- import { cleanupWorktree } from "./worktree.js";
7
- const TAG = "review-worktree";
8
- /**
9
- * Checkout an existing remote branch into a worktree for review.
10
- * Unlike createWorktree() which creates a new branch, this checks out
11
- * an existing branch that was pushed by the implementation worker.
12
- */
13
- export function checkoutExistingBranch(basePath, branchName) {
14
- const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
15
- encoding: "utf-8",
16
- }).trim();
17
- const worktreeDir = resolve(repoRoot, basePath, `review-${branchName}`);
18
- if (existsSync(worktreeDir)) {
19
- log.warn(TAG, `Review worktree already exists at ${worktreeDir}, cleaning up`);
20
- cleanupWorktree(worktreeDir);
21
- }
22
- // Force-prune orphaned worktree metadata (dir removed externally but git
23
- // still registers the branch as checked out). Default expiry is 3 months,
24
- // which leaves fresh stale entries in place.
25
- try {
26
- execFileSync("git", ["worktree", "prune", "--expire=now"], {
27
- cwd: repoRoot,
28
- stdio: "pipe",
29
- });
30
- }
31
- catch {
32
- // non-fatal
33
- }
34
- // Fetch latest to ensure we have the remote branch
35
- try {
36
- execFileSync("git", ["fetch", "origin", branchName], {
37
- cwd: repoRoot,
38
- stdio: "pipe",
39
- });
40
- }
41
- catch {
42
- throw new Error(`Failed to fetch remote branch: ${branchName}`);
43
- }
44
- // Delete stale local branch if it exists
45
- try {
46
- execFileSync("git", ["branch", "-D", branchName], {
47
- cwd: repoRoot,
48
- stdio: "pipe",
49
- });
50
- }
51
- catch {
52
- // Branch doesn't exist locally — that's fine
53
- }
54
- // Create worktree from the remote branch
55
- log.info(TAG, `Creating review worktree: ${worktreeDir} (branch: ${branchName})`);
56
- execFileSync("git", [
57
- "worktree",
58
- "add",
59
- "--track",
60
- "-b",
61
- branchName,
62
- worktreeDir,
63
- `origin/${branchName}`,
64
- ], { cwd: repoRoot, stdio: "pipe" });
65
- // Install dependencies
66
- log.info(TAG, "Installing dependencies in review worktree...");
67
- try {
68
- execSync(installCommand(), {
69
- cwd: worktreeDir,
70
- stdio: "pipe",
71
- timeout: 60_000,
72
- });
73
- }
74
- catch {
75
- log.warn(TAG, "Install failed (may be fine if deps are hoisted)");
76
- }
77
- return worktreeDir;
78
- }
79
- /**
80
- * Extract branch name from card description's completion summary.
81
- * Looks for: Branch: `agent/...`
82
- * Validates that the extracted name contains only safe characters.
83
- */
84
- export function extractBranchFromDescription(description) {
85
- if (!description)
86
- return null;
87
- const match = description.match(/Branch:\s*`([^`]+)`/);
88
- const branch = match?.[1] ?? null;
89
- // Validate branch name contains only safe git ref characters
90
- if (branch && !/^[a-zA-Z0-9/_.-]+$/.test(branch)) {
91
- log.warn(TAG, `Extracted branch name contains unsafe characters: ${branch}`);
92
- return null;
93
- }
94
- return branch;
95
- }
package/dist/run-log.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import { type WriteStream } from "node:fs";
2
- export interface RunLog {
3
- path: string;
4
- stream: WriteStream;
5
- }
6
- export declare function openRunLog(tag: string, runId: string | null, shortId: number): RunLog | null;
package/dist/run-log.js DELETED
@@ -1,19 +0,0 @@
1
- import { createWriteStream, mkdirSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import { log } from "./log.js";
5
- export function openRunLog(tag, runId, shortId) {
6
- if (!runId)
7
- return null;
8
- try {
9
- const dir = join(homedir(), ".harmony-mcp", "runs");
10
- mkdirSync(dir, { recursive: true });
11
- const path = join(dir, `${runId}-card-${shortId}.log`);
12
- const stream = createWriteStream(path, { flags: "a" });
13
- return { path, stream };
14
- }
15
- catch (err) {
16
- log.warn(tag, `Failed to open run log: ${err instanceof Error ? err.message : err}`);
17
- return null;
18
- }
19
- }
@@ -1,29 +0,0 @@
1
- /**
2
- * Startup banner — collapses the ~21 individually-timestamped INFO lines
3
- * the daemon used to emit at startup into a single grouped block in
4
- * pretty/TTY mode. JSON mode is unchanged (each `check()` falls through
5
- * to `log.info("daemon", msg)` so log shippers see the same records).
6
- *
7
- * Usage:
8
- * const banner = createStartupBanner(config, PKG_VERSION);
9
- * banner.check("Claude CLI 2.1.126");
10
- * banner.setProjectName("Harmony");
11
- * banner.check("API key validated");
12
- * ...
13
- * await banner.ready("watching for card assignments");
14
- *
15
- * Pretty rendering is deferred until `ready()` so the whole block lands
16
- * atomically with one stderr.write — nothing can interleave inside it.
17
- * If a startup error occurs and a warn/error needs to fire, call
18
- * `banner.fail()` first so the banner suppresses itself and the warning
19
- * isn't hidden under the block.
20
- */
21
- import type { DaemonConfig } from "./config.js";
22
- export interface StartupBanner {
23
- setProjectName(name: string): void;
24
- setGitProvider(provider: string): void;
25
- check(message: string): void;
26
- ready(message: string): Promise<void>;
27
- fail(): void;
28
- }
29
- export declare function createStartupBanner(config: DaemonConfig, version: string): StartupBanner;
@@ -1,143 +0,0 @@
1
- import { isPretty, log } from "./log.js";
2
- const TAG = "daemon";
3
- const RULE_WIDTH = 70;
4
- const ANSI = {
5
- reset: "\x1b[0m",
6
- dim: "\x1b[2m",
7
- cyan: "\x1b[36m",
8
- };
9
- export function createStartupBanner(config, version) {
10
- return isPretty()
11
- ? prettyBanner(config, version)
12
- : jsonBanner(config, version);
13
- }
14
- function prettyBanner(config, version) {
15
- const checks = [];
16
- let projectName;
17
- let gitProvider;
18
- let failed = false;
19
- let rendered = false;
20
- return {
21
- setProjectName(name) {
22
- projectName = name;
23
- },
24
- setGitProvider(provider) {
25
- gitProvider = provider;
26
- },
27
- check(message) {
28
- checks.push(message);
29
- },
30
- fail() {
31
- failed = true;
32
- },
33
- async ready(message) {
34
- if (failed || rendered)
35
- return;
36
- rendered = true;
37
- const block = renderPretty({
38
- version,
39
- config,
40
- projectName,
41
- gitProvider,
42
- checks,
43
- readyMessage: message,
44
- });
45
- process.stderr.write(block);
46
- },
47
- };
48
- }
49
- function jsonBanner(config, version) {
50
- // Mirror today's structured output: one log.info per startup event.
51
- // Emit the version line eagerly so JSON consumers still see it first.
52
- log.info(TAG, `Harmony Agent Daemon v${version} starting...`);
53
- log.info(TAG, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
54
- if (config.agent.review.enabled) {
55
- log.info(TAG, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
56
- }
57
- let failed = false;
58
- return {
59
- setProjectName(_name) {
60
- // No-op in JSON mode — the explicit `Project access (X)` check
61
- // line that follows already conveys this and avoids duplication.
62
- },
63
- setGitProvider(provider) {
64
- log.info(TAG, `Git provider: ${provider}`);
65
- },
66
- check(message) {
67
- log.info(TAG, message);
68
- },
69
- fail() {
70
- failed = true;
71
- },
72
- async ready(message) {
73
- if (failed)
74
- return;
75
- log.info(TAG, message);
76
- },
77
- };
78
- }
79
- function renderPretty(input) {
80
- const { version, config, projectName, gitProvider, checks, readyMessage } = input;
81
- const lines = [];
82
- lines.push("");
83
- lines.push(titleRule(`Harmony Agent Daemon v${version}`));
84
- lines.push("");
85
- for (const row of configRows(config, projectName, gitProvider)) {
86
- lines.push(` ${dim(row.label.padEnd(9))} ${row.value}`);
87
- }
88
- lines.push("");
89
- for (const msg of checks) {
90
- lines.push(` ${cyan("✓")} ${msg}`);
91
- }
92
- lines.push("");
93
- lines.push(`${cyan("▶")} ${cyan("Ready")} — ${readyMessage}`);
94
- lines.push(dim("─".repeat(RULE_WIDTH)));
95
- lines.push("");
96
- return lines.join("\n");
97
- }
98
- function configRows(config, projectName, gitProvider) {
99
- const rows = [];
100
- const projectLabel = projectName
101
- ? `${projectName} (${shortenId(config.projectId)})`
102
- : shortenId(config.projectId);
103
- rows.push({ label: "Project", value: projectLabel });
104
- rows.push({ label: "User", value: config.userEmail });
105
- const reviewEnabled = config.agent.review.enabled;
106
- const poolDesc = reviewEnabled
107
- ? `Pool ${config.agent.poolSize} impl + ${config.agent.review.poolSize} review`
108
- : `Pool ${config.agent.poolSize} impl`;
109
- const flowDesc = reviewEnabled
110
- ? `Pickup ${config.agent.pickupColumns[0]} → ${config.agent.completion.moveToColumn} → ${config.agent.review.moveToColumn}`
111
- : `Pickup ${config.agent.pickupColumns.join(", ")}`;
112
- rows.push({
113
- label: "Model",
114
- value: `${config.agent.claude.model} · ${poolDesc} · ${flowDesc}`,
115
- });
116
- const tail = [];
117
- if (gitProvider)
118
- tail.push(gitProvider);
119
- if (config.agent.http.enabled) {
120
- tail.push(`HTTP http://${config.agent.http.bindAddr}:${config.agent.http.port}`);
121
- }
122
- if (tail.length > 0) {
123
- rows.push({ label: "Git", value: tail.join(" · ") });
124
- }
125
- return rows;
126
- }
127
- function titleRule(title) {
128
- const prefix = "─── ";
129
- const surround = ` `;
130
- const suffix = "─".repeat(Math.max(3, RULE_WIDTH - prefix.length - title.length - surround.length));
131
- return dim(`${prefix}${title}${surround}${suffix}`);
132
- }
133
- function shortenId(id) {
134
- if (id.length <= 8)
135
- return id;
136
- return `${id.slice(0, 8)}…`;
137
- }
138
- function dim(s) {
139
- return `${ANSI.dim}${s}${ANSI.reset}`;
140
- }
141
- function cyan(s) {
142
- return `${ANSI.cyan}${s}${ANSI.reset}`;
143
- }
@@ -1,89 +0,0 @@
1
- export type RunPipeline = "implement" | "review";
2
- export type RunStatus = "active" | "completed" | "paused" | "orphaned";
3
- export interface RunRecord {
4
- runId: string;
5
- cardId: string;
6
- cardShortId: number;
7
- pipeline: RunPipeline;
8
- workerId: number;
9
- sessionId: string | null;
10
- worktreePath: string | null;
11
- branchName: string | null;
12
- daemonPid: number;
13
- phase: string;
14
- startedAt: number;
15
- lastHeartbeatAt: number;
16
- endedAt: number | null;
17
- status: RunStatus;
18
- costCents: number;
19
- errorMessage?: string;
20
- }
21
- export interface FailureSummaryRecord {
22
- summary: string;
23
- ts: number;
24
- reason?: "verification" | "review" | "daemon_restart" | "budget" | "other";
25
- recoveryBranch?: string;
26
- }
27
- export interface CardRecord {
28
- cardId: string;
29
- attempts: number;
30
- totalCostCents: number;
31
- lastAttemptAt: number | null;
32
- lastOutcome: "success" | "failure" | null;
33
- failureHistory?: FailureSummaryRecord[];
34
- }
35
- export declare function newRunId(): string;
36
- export declare function defaultStatePath(): string;
37
- /**
38
- * Durable run-and-card state for the agent daemon. One writer (this daemon),
39
- * so in-process serialization via a promise chain is sufficient — no file locks.
40
- * Persists to JSON via write-to-tmp + rename, which is atomic on POSIX.
41
- */
42
- export declare class StateStore {
43
- private path;
44
- private state;
45
- private writeQueue;
46
- constructor(path: string);
47
- static open(path?: string): StateStore;
48
- private load;
49
- private persist;
50
- /** Await any pending writes. Useful for tests and shutdown. */
51
- flush(): Promise<void>;
52
- setDaemon(daemonId: string, pid: number): Promise<void>;
53
- getDaemon(): {
54
- daemonId: string | null;
55
- daemonPid: number | null;
56
- daemonStartedAt: number | null;
57
- };
58
- insertRun(run: RunRecord): Promise<void>;
59
- updateRun(runId: string, patch: Partial<RunRecord>): Promise<void>;
60
- heartbeat(runId: string): Promise<void>;
61
- endRun(runId: string, status: RunStatus, errorMessage?: string): Promise<void>;
62
- getRun(runId: string): RunRecord | null;
63
- getActiveRuns(): RunRecord[];
64
- getRunsForCard(cardId: string): RunRecord[];
65
- /** Trim completed runs older than `beforeTs` to keep the file small. */
66
- purgeOldRuns(beforeTs: number): Promise<void>;
67
- private ensureCard;
68
- getCard(cardId: string): CardRecord | null;
69
- incrementAttempt(cardId: string): Promise<number>;
70
- recordOutcome(cardId: string, outcome: "success" | "failure"): Promise<void>;
71
- /**
72
- * Reset a card's attempt counter so a card that previously gave up
73
- * (attempts >= maxAttemptsPerCard) becomes eligible again. Called when a
74
- * card is unassigned/reassigned — reassignment is the human's "try again".
75
- * No-op for an unknown card.
76
- */
77
- resetAttempts(cardId: string): Promise<void>;
78
- /**
79
- * Push a failure summary onto the card's bounded history (most-recent first,
80
- * capped at 5). Read back by the give-up comment and the Agent History
81
- * UI section to give users a post-mortem trail across attempts.
82
- */
83
- recordFailureSummary(cardId: string, entry: Omit<FailureSummaryRecord, "ts"> & {
84
- ts?: number;
85
- }): Promise<void>;
86
- getRecentFailures(cardId: string, limit?: number): FailureSummaryRecord[];
87
- addCost(cardId: string, cents: number): Promise<void>;
88
- getDailyCostCents(date?: string): number;
89
- }
@@ -1,230 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
4
- import { log } from "./log.js";
5
- const TAG = "state-store";
6
- const SCHEMA_VERSION = 1;
7
- function emptyState() {
8
- return {
9
- version: SCHEMA_VERSION,
10
- daemonId: null,
11
- daemonPid: null,
12
- daemonStartedAt: null,
13
- runs: [],
14
- cards: [],
15
- daily: [],
16
- };
17
- }
18
- function todayUtc() {
19
- return new Date().toISOString().slice(0, 10);
20
- }
21
- export function newRunId() {
22
- const ts = Date.now().toString(36);
23
- const rand = Math.random().toString(36).slice(2, 10);
24
- return `r_${ts}_${rand}`;
25
- }
26
- export function defaultStatePath() {
27
- return join(homedir(), ".harmony-mcp", "agent-state.json");
28
- }
29
- /**
30
- * Durable run-and-card state for the agent daemon. One writer (this daemon),
31
- * so in-process serialization via a promise chain is sufficient — no file locks.
32
- * Persists to JSON via write-to-tmp + rename, which is atomic on POSIX.
33
- */
34
- export class StateStore {
35
- path;
36
- state;
37
- writeQueue = Promise.resolve();
38
- constructor(path) {
39
- this.path = path;
40
- this.state = this.load();
41
- }
42
- static open(path) {
43
- const resolved = path ?? defaultStatePath();
44
- const dir = dirname(resolved);
45
- if (!existsSync(dir))
46
- mkdirSync(dir, { recursive: true });
47
- return new StateStore(resolved);
48
- }
49
- load() {
50
- if (!existsSync(this.path))
51
- return emptyState();
52
- try {
53
- const raw = readFileSync(this.path, "utf-8");
54
- const parsed = JSON.parse(raw);
55
- if (parsed?.version !== SCHEMA_VERSION) {
56
- log.warn(TAG, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — starting fresh`);
57
- return emptyState();
58
- }
59
- return {
60
- version: SCHEMA_VERSION,
61
- daemonId: parsed.daemonId ?? null,
62
- daemonPid: parsed.daemonPid ?? null,
63
- daemonStartedAt: parsed.daemonStartedAt ?? null,
64
- runs: parsed.runs ?? [],
65
- cards: parsed.cards ?? [],
66
- daily: parsed.daily ?? [],
67
- };
68
- }
69
- catch (err) {
70
- log.error(TAG, `failed to read state file: ${err instanceof Error ? err.message : err}`);
71
- return emptyState();
72
- }
73
- }
74
- persist() {
75
- const fire = async () => {
76
- const tmp = `${this.path}.tmp`;
77
- const data = JSON.stringify(this.state, null, 2);
78
- writeFileSync(tmp, data, "utf-8");
79
- renameSync(tmp, this.path);
80
- };
81
- this.writeQueue = this.writeQueue.then(fire, fire);
82
- return this.writeQueue;
83
- }
84
- /** Await any pending writes. Useful for tests and shutdown. */
85
- flush() {
86
- return this.writeQueue;
87
- }
88
- // ---------- Daemon metadata ----------
89
- setDaemon(daemonId, pid) {
90
- this.state.daemonId = daemonId;
91
- this.state.daemonPid = pid;
92
- this.state.daemonStartedAt = Date.now();
93
- return this.persist();
94
- }
95
- getDaemon() {
96
- return {
97
- daemonId: this.state.daemonId,
98
- daemonPid: this.state.daemonPid,
99
- daemonStartedAt: this.state.daemonStartedAt,
100
- };
101
- }
102
- // ---------- Runs ----------
103
- insertRun(run) {
104
- this.state.runs.push(run);
105
- return this.persist();
106
- }
107
- updateRun(runId, patch) {
108
- const idx = this.state.runs.findIndex((r) => r.runId === runId);
109
- if (idx === -1)
110
- return Promise.resolve();
111
- this.state.runs[idx] = { ...this.state.runs[idx], ...patch };
112
- return this.persist();
113
- }
114
- heartbeat(runId) {
115
- return this.updateRun(runId, { lastHeartbeatAt: Date.now() });
116
- }
117
- endRun(runId, status, errorMessage) {
118
- const patch = {
119
- status,
120
- endedAt: Date.now(),
121
- };
122
- if (errorMessage !== undefined)
123
- patch.errorMessage = errorMessage;
124
- return this.updateRun(runId, patch);
125
- }
126
- getRun(runId) {
127
- return this.state.runs.find((r) => r.runId === runId) ?? null;
128
- }
129
- getActiveRuns() {
130
- return this.state.runs.filter((r) => r.endedAt === null);
131
- }
132
- getRunsForCard(cardId) {
133
- return this.state.runs.filter((r) => r.cardId === cardId);
134
- }
135
- /** Trim completed runs older than `beforeTs` to keep the file small. */
136
- purgeOldRuns(beforeTs) {
137
- this.state.runs = this.state.runs.filter((r) => r.endedAt === null || r.endedAt >= beforeTs);
138
- return this.persist();
139
- }
140
- // ---------- Cards ----------
141
- ensureCard(cardId) {
142
- let rec = this.state.cards.find((c) => c.cardId === cardId);
143
- if (!rec) {
144
- rec = {
145
- cardId,
146
- attempts: 0,
147
- totalCostCents: 0,
148
- lastAttemptAt: null,
149
- lastOutcome: null,
150
- };
151
- this.state.cards.push(rec);
152
- }
153
- return rec;
154
- }
155
- getCard(cardId) {
156
- return this.state.cards.find((c) => c.cardId === cardId) ?? null;
157
- }
158
- async incrementAttempt(cardId) {
159
- const rec = this.ensureCard(cardId);
160
- rec.attempts += 1;
161
- rec.lastAttemptAt = Date.now();
162
- await this.persist();
163
- return rec.attempts;
164
- }
165
- async recordOutcome(cardId, outcome) {
166
- const rec = this.ensureCard(cardId);
167
- rec.lastOutcome = outcome;
168
- if (outcome === "success")
169
- rec.attempts = 0;
170
- await this.persist();
171
- }
172
- /**
173
- * Reset a card's attempt counter so a card that previously gave up
174
- * (attempts >= maxAttemptsPerCard) becomes eligible again. Called when a
175
- * card is unassigned/reassigned — reassignment is the human's "try again".
176
- * No-op for an unknown card.
177
- */
178
- async resetAttempts(cardId) {
179
- const rec = this.getCard(cardId);
180
- if (!rec || rec.attempts === 0)
181
- return;
182
- rec.attempts = 0;
183
- await this.persist();
184
- }
185
- /**
186
- * Push a failure summary onto the card's bounded history (most-recent first,
187
- * capped at 5). Read back by the give-up comment and the Agent History
188
- * UI section to give users a post-mortem trail across attempts.
189
- */
190
- async recordFailureSummary(cardId, entry) {
191
- const rec = this.ensureCard(cardId);
192
- const next = {
193
- summary: entry.summary.slice(0, 500),
194
- ts: entry.ts ?? Date.now(),
195
- reason: entry.reason,
196
- recoveryBranch: entry.recoveryBranch,
197
- };
198
- const existing = rec.failureHistory ?? [];
199
- rec.failureHistory = [next, ...existing].slice(0, 5);
200
- await this.persist();
201
- }
202
- getRecentFailures(cardId, limit = 3) {
203
- const rec = this.getCard(cardId);
204
- if (!rec?.failureHistory)
205
- return [];
206
- return rec.failureHistory.slice(0, limit);
207
- }
208
- async addCost(cardId, cents) {
209
- if (cents <= 0)
210
- return;
211
- const rec = this.ensureCard(cardId);
212
- rec.totalCostCents += cents;
213
- const today = todayUtc();
214
- let daily = this.state.daily.find((d) => d.date === today);
215
- if (!daily) {
216
- daily = { date: today, costCents: 0 };
217
- this.state.daily.push(daily);
218
- }
219
- daily.costCents += cents;
220
- const cutoff = new Date(Date.now() - 30 * 86_400_000)
221
- .toISOString()
222
- .slice(0, 10);
223
- this.state.daily = this.state.daily.filter((d) => d.date >= cutoff);
224
- await this.persist();
225
- }
226
- getDailyCostCents(date) {
227
- const key = date ?? todayUtc();
228
- return this.state.daily.find((d) => d.date === key)?.costCents ?? 0;
229
- }
230
- }
@@ -1,9 +0,0 @@
1
- /**
2
- * Feed a minimal Claude-CLI stream-json fixture through StreamParser and
3
- * assert the expected events fire. Intended as a startup canary: if the CLI
4
- * ever changes its envelope shape, the daemon fails loud on boot instead of
5
- * silently parking every card with a parse error (see card #128).
6
- *
7
- * Throws on any missing event; returns silently on success.
8
- */
9
- export declare function verifyStreamParserFormat(): void;