@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
@@ -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,88 +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
- dlq: boolean;
32
- dlqReason?: string;
33
- lastAttemptAt: number | null;
34
- lastOutcome: "success" | "failure" | null;
35
- failureHistory?: FailureSummaryRecord[];
36
- }
37
- export declare function newRunId(): string;
38
- export declare function defaultStatePath(): string;
39
- /**
40
- * Durable run-and-card state for the agent daemon. One writer (this daemon),
41
- * so in-process serialization via a promise chain is sufficient — no file locks.
42
- * Persists to JSON via write-to-tmp + rename, which is atomic on POSIX.
43
- */
44
- export declare class StateStore {
45
- private path;
46
- private state;
47
- private writeQueue;
48
- constructor(path: string);
49
- static open(path?: string): StateStore;
50
- private load;
51
- private persist;
52
- /** Await any pending writes. Useful for tests and shutdown. */
53
- flush(): Promise<void>;
54
- setDaemon(daemonId: string, pid: number): Promise<void>;
55
- getDaemon(): {
56
- daemonId: string | null;
57
- daemonPid: number | null;
58
- daemonStartedAt: number | null;
59
- };
60
- insertRun(run: RunRecord): Promise<void>;
61
- updateRun(runId: string, patch: Partial<RunRecord>): Promise<void>;
62
- heartbeat(runId: string): Promise<void>;
63
- endRun(runId: string, status: RunStatus, errorMessage?: string): Promise<void>;
64
- getRun(runId: string): RunRecord | null;
65
- getActiveRuns(): RunRecord[];
66
- getRunsForCard(cardId: string): RunRecord[];
67
- /** Trim completed runs older than `beforeTs` to keep the file small. */
68
- purgeOldRuns(beforeTs: number): Promise<void>;
69
- private ensureCard;
70
- getCard(cardId: string): CardRecord | null;
71
- incrementAttempt(cardId: string): Promise<number>;
72
- recordOutcome(cardId: string, outcome: "success" | "failure"): Promise<void>;
73
- /**
74
- * Push a failure summary onto the card's bounded history (most-recent first,
75
- * capped at 5). Read back by DLQ comment formatting and the Agent History
76
- * UI section to give users a post-mortem trail across attempts.
77
- */
78
- recordFailureSummary(cardId: string, entry: Omit<FailureSummaryRecord, "ts"> & {
79
- ts?: number;
80
- }): Promise<void>;
81
- getRecentFailures(cardId: string, limit?: number): FailureSummaryRecord[];
82
- addCost(cardId: string, cents: number): Promise<void>;
83
- getDailyCostCents(date?: string): number;
84
- markDlq(cardId: string, reason: string): Promise<void>;
85
- clearDlq(cardId: string): Promise<void>;
86
- isDlq(cardId: string): boolean;
87
- listDlq(): CardRecord[];
88
- }
@@ -1,239 +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
- dlq: false,
149
- lastAttemptAt: null,
150
- lastOutcome: null,
151
- };
152
- this.state.cards.push(rec);
153
- }
154
- return rec;
155
- }
156
- getCard(cardId) {
157
- return this.state.cards.find((c) => c.cardId === cardId) ?? null;
158
- }
159
- async incrementAttempt(cardId) {
160
- const rec = this.ensureCard(cardId);
161
- rec.attempts += 1;
162
- rec.lastAttemptAt = Date.now();
163
- await this.persist();
164
- return rec.attempts;
165
- }
166
- async recordOutcome(cardId, outcome) {
167
- const rec = this.ensureCard(cardId);
168
- rec.lastOutcome = outcome;
169
- if (outcome === "success")
170
- rec.attempts = 0;
171
- await this.persist();
172
- }
173
- /**
174
- * Push a failure summary onto the card's bounded history (most-recent first,
175
- * capped at 5). Read back by DLQ comment formatting and the Agent History
176
- * UI section to give users a post-mortem trail across attempts.
177
- */
178
- async recordFailureSummary(cardId, entry) {
179
- const rec = this.ensureCard(cardId);
180
- const next = {
181
- summary: entry.summary.slice(0, 500),
182
- ts: entry.ts ?? Date.now(),
183
- reason: entry.reason,
184
- recoveryBranch: entry.recoveryBranch,
185
- };
186
- const existing = rec.failureHistory ?? [];
187
- rec.failureHistory = [next, ...existing].slice(0, 5);
188
- await this.persist();
189
- }
190
- getRecentFailures(cardId, limit = 3) {
191
- const rec = this.getCard(cardId);
192
- if (!rec?.failureHistory)
193
- return [];
194
- return rec.failureHistory.slice(0, limit);
195
- }
196
- async addCost(cardId, cents) {
197
- if (cents <= 0)
198
- return;
199
- const rec = this.ensureCard(cardId);
200
- rec.totalCostCents += cents;
201
- const today = todayUtc();
202
- let daily = this.state.daily.find((d) => d.date === today);
203
- if (!daily) {
204
- daily = { date: today, costCents: 0 };
205
- this.state.daily.push(daily);
206
- }
207
- daily.costCents += cents;
208
- const cutoff = new Date(Date.now() - 30 * 86_400_000)
209
- .toISOString()
210
- .slice(0, 10);
211
- this.state.daily = this.state.daily.filter((d) => d.date >= cutoff);
212
- await this.persist();
213
- }
214
- getDailyCostCents(date) {
215
- const key = date ?? todayUtc();
216
- return this.state.daily.find((d) => d.date === key)?.costCents ?? 0;
217
- }
218
- // ---------- DLQ ----------
219
- async markDlq(cardId, reason) {
220
- const rec = this.ensureCard(cardId);
221
- rec.dlq = true;
222
- rec.dlqReason = reason;
223
- await this.persist();
224
- }
225
- async clearDlq(cardId) {
226
- const rec = this.getCard(cardId);
227
- if (!rec)
228
- return;
229
- rec.dlq = false;
230
- delete rec.dlqReason;
231
- await this.persist();
232
- }
233
- isDlq(cardId) {
234
- return this.getCard(cardId)?.dlq === true;
235
- }
236
- listDlq() {
237
- return this.state.cards.filter((c) => c.dlq);
238
- }
239
- }
@@ -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;