@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
package/dist/queue.js DELETED
@@ -1,100 +0,0 @@
1
- import { log } from "./log.js";
2
- const TAG = "queue";
3
- /**
4
- * Priority queue for cards waiting to be worked on.
5
- * Sorted by: label priority boost > column position boost > enqueue time (FIFO).
6
- */
7
- export class PriorityQueue {
8
- config;
9
- items = [];
10
- constructor(config) {
11
- this.config = config;
12
- }
13
- /**
14
- * Calculate priority score for a card.
15
- */
16
- scoreCard(_card, column, labels) {
17
- let score = 0;
18
- // Label boost: highest matching label wins
19
- for (const label of labels) {
20
- const boost = this.config.priorityLabels[label.name.toLowerCase()] ?? 0;
21
- if (boost > score)
22
- score = boost;
23
- }
24
- // Column position boost: leftmost columns get higher priority
25
- if (this.config.columnBoost) {
26
- score += Math.max(0, 100 - column.position * 10);
27
- }
28
- return score;
29
- }
30
- /**
31
- * Add a card to the queue. If already present, update its priority.
32
- */
33
- enqueue(card, column, labels, mode = "implement") {
34
- const existing = this.items.findIndex((i) => i.cardId === card.id);
35
- if (existing !== -1) {
36
- log.debug(TAG, `Card #${card.short_id} already queued, updating priority`);
37
- this.items.splice(existing, 1);
38
- }
39
- const priority = this.scoreCard(card, column, labels);
40
- const item = {
41
- cardId: card.id,
42
- shortId: card.short_id,
43
- title: card.title,
44
- priority,
45
- enqueuedAt: Date.now(),
46
- mode,
47
- };
48
- // Insert in sorted position (highest priority first, FIFO tiebreak)
49
- let insertIdx = this.items.length;
50
- for (let i = 0; i < this.items.length; i++) {
51
- if (priority > this.items[i].priority ||
52
- (priority === this.items[i].priority &&
53
- item.enqueuedAt < this.items[i].enqueuedAt)) {
54
- insertIdx = i;
55
- break;
56
- }
57
- }
58
- this.items.splice(insertIdx, 0, item);
59
- log.info(TAG, `Enqueued #${card.short_id} "${card.title}" (priority=${priority}, pos=${insertIdx}, queue=${this.items.length})`);
60
- }
61
- /**
62
- * Remove and return the highest-priority item.
63
- */
64
- dequeue() {
65
- return this.items.shift() ?? null;
66
- }
67
- /**
68
- * Remove a specific card from the queue.
69
- */
70
- remove(cardId) {
71
- const idx = this.items.findIndex((i) => i.cardId === cardId);
72
- if (idx === -1)
73
- return null;
74
- const [item] = this.items.splice(idx, 1);
75
- log.info(TAG, `Removed #${item.shortId} from queue`);
76
- return item;
77
- }
78
- /**
79
- * Check if a card is in the queue.
80
- */
81
- has(cardId) {
82
- return this.items.some((i) => i.cardId === cardId);
83
- }
84
- /**
85
- * Get all queued card IDs.
86
- */
87
- cardIds() {
88
- return this.items.map((i) => i.cardId);
89
- }
90
- get length() {
91
- return this.items.length;
92
- }
93
- peek() {
94
- return this.items[0] ?? null;
95
- }
96
- /** Copy of the queue in priority order (for introspection). */
97
- snapshot() {
98
- return this.items.slice();
99
- }
100
- }
@@ -1,35 +0,0 @@
1
- import type { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
2
- import type { Pool } from "./pool.js";
3
- import type { StateStore } from "./state-store.js";
4
- import { type AgentConfig } from "./types.js";
5
- /**
6
- * Reconciliation heartbeat: polls the board every `intervalMs` to catch
7
- * missed realtime events and sync state.
8
- */
9
- export declare class Reconciler {
10
- private client;
11
- private pool;
12
- private projectId;
13
- private agentUserId;
14
- private pickupColumns;
15
- private reviewColumns;
16
- private approvedLabel;
17
- private intervalMs;
18
- private stateStore?;
19
- private agentConfig?;
20
- private timer;
21
- private lastTickAt;
22
- get lastTick(): number | null;
23
- get isRunning(): boolean;
24
- constructor(client: HarmonyApiClient, pool: Pool, projectId: string, agentUserId: string, pickupColumns: string[], reviewColumns: string[], approvedLabel: string, intervalMs?: number, stateStore?: StateStore | undefined, agentConfig?: AgentConfig | undefined);
25
- start(): void;
26
- stop(): void;
27
- /**
28
- * Walk the state store for runs marked active whose owning daemon is
29
- * dead OR whose heartbeat is stale. Each such run gets the same
30
- * recovery treatment as startup orphans: session ended, card returned
31
- * to pickup column with agent-recovered label, worktree cleaned up.
32
- */
33
- private recoverStaleRuns;
34
- private tick;
35
- }
package/dist/reconcile.js DELETED
@@ -1,174 +0,0 @@
1
- import { buildLabelMap, hasLabel, resolveCardLabels } from "./board-helpers.js";
2
- import { log } from "./log.js";
3
- import { isProcessAlive, recoverRun } from "./recovery.js";
4
- import { extractBranchFromDescription } from "./review-worktree.js";
5
- import { NEED_REVIEW_LABEL } from "./types.js";
6
- const TAG = "reconcile";
7
- /**
8
- * Reconciliation heartbeat: polls the board every `intervalMs` to catch
9
- * missed realtime events and sync state.
10
- */
11
- export class Reconciler {
12
- client;
13
- pool;
14
- projectId;
15
- agentUserId;
16
- pickupColumns;
17
- reviewColumns;
18
- approvedLabel;
19
- intervalMs;
20
- stateStore;
21
- agentConfig;
22
- timer = null;
23
- lastTickAt = null;
24
- get lastTick() {
25
- return this.lastTickAt;
26
- }
27
- get isRunning() {
28
- return this.timer !== null;
29
- }
30
- constructor(client, pool, projectId, agentUserId, pickupColumns, reviewColumns, approvedLabel, intervalMs = 60_000, stateStore, agentConfig) {
31
- this.client = client;
32
- this.pool = pool;
33
- this.projectId = projectId;
34
- this.agentUserId = agentUserId;
35
- this.pickupColumns = pickupColumns;
36
- this.reviewColumns = reviewColumns;
37
- this.approvedLabel = approvedLabel;
38
- this.intervalMs = intervalMs;
39
- this.stateStore = stateStore;
40
- this.agentConfig = agentConfig;
41
- }
42
- start() {
43
- // Run immediately, then on interval
44
- this.tick();
45
- this.timer = setInterval(() => this.tick(), this.intervalMs);
46
- }
47
- stop() {
48
- if (this.timer) {
49
- clearInterval(this.timer);
50
- this.timer = null;
51
- }
52
- log.info(TAG, "Heartbeat stopped");
53
- }
54
- /**
55
- * Walk the state store for runs marked active whose owning daemon is
56
- * dead OR whose heartbeat is stale. Each such run gets the same
57
- * recovery treatment as startup orphans: session ended, card returned
58
- * to pickup column with agent-recovered label, worktree cleaned up.
59
- */
60
- async recoverStaleRuns() {
61
- if (!this.stateStore || !this.agentConfig)
62
- return;
63
- const now = Date.now();
64
- const stale = this.agentConfig.timing.staleHeartbeatMs;
65
- const active = this.stateStore.getActiveRuns();
66
- const pool = this.pool;
67
- for (const run of active) {
68
- const foreignDaemon = run.daemonPid !== process.pid;
69
- const daemonDead = foreignDaemon && !isProcessAlive(run.daemonPid, process.pid);
70
- const heartbeatStale = now - run.lastHeartbeatAt > stale;
71
- const ourZombie = !foreignDaemon && !pool.isCardActive(run.cardId);
72
- if (!daemonDead && !(heartbeatStale && ourZombie))
73
- continue;
74
- const reason = daemonDead
75
- ? `foreign daemon ${run.daemonPid} is dead`
76
- : `our worker lost card ${run.cardId} with ${Math.round((now - run.lastHeartbeatAt) / 1000)}s stale heartbeat`;
77
- log.warn(TAG, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
78
- await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
79
- runId: run.runId,
80
- cardId: run.cardId,
81
- cardShortId: run.cardShortId,
82
- pipeline: run.pipeline,
83
- actions: [],
84
- errors: [],
85
- });
86
- }
87
- }
88
- async tick() {
89
- this.lastTickAt = Date.now();
90
- try {
91
- const board = await this.client.getBoard(this.projectId);
92
- const cards = (board.cards ?? []);
93
- const columns = (board.columns ?? []);
94
- // Build label lookup (id → Label) to resolve card.labelIds
95
- const labelMap = buildLabelMap((board.labels ?? []));
96
- // Build a lookup of columns by ID
97
- const columnMap = new Map();
98
- for (const col of columns) {
99
- columnMap.set(col.id, col);
100
- }
101
- // Build column ID sets for both modes
102
- const pickupColumnIds = new Set(columns
103
- .filter((c) => this.pickupColumns.some((name) => name.toLowerCase() === c.name.toLowerCase()))
104
- .map((c) => c.id));
105
- const reviewColumnIds = new Set(columns
106
- .filter((c) => this.reviewColumns.some((name) => name.toLowerCase() === c.name.toLowerCase()))
107
- .map((c) => c.id));
108
- // Find cards assigned to our agent in either pickup or review columns
109
- const assignedCards = cards.filter((c) => c.assignee_id === this.agentUserId &&
110
- !c.archived_at &&
111
- (pickupColumnIds.has(c.column_id) ||
112
- reviewColumnIds.has(c.column_id)));
113
- const knownCardIds = this.pool.knownCardIds();
114
- // All cards still assigned to the agent (any column) — used to detect
115
- // genuine unassigns without false-positiving on cards the worker moved
116
- // to "In Progress" or other non-pickup columns.
117
- const allAgentCardIds = new Set(cards
118
- .filter((c) => c.assignee_id === this.agentUserId && !c.archived_at)
119
- .map((c) => c.id));
120
- // Cards assigned but NOT in queue/active → enqueue (missed event)
121
- for (const card of assignedCards) {
122
- if (!knownCardIds.has(card.id)) {
123
- const column = columnMap.get(card.column_id);
124
- if (!column)
125
- continue;
126
- const cardLabels = resolveCardLabels(card, labelMap);
127
- const subtasks = card.subtasks ?? [];
128
- // Determine mode based on which column set the card is in
129
- const mode = reviewColumnIds.has(card.column_id)
130
- ? "review"
131
- : "implement";
132
- // Skip already-approved cards in review mode
133
- if (mode === "review" &&
134
- this.approvedLabel &&
135
- hasLabel(cardLabels, this.approvedLabel)) {
136
- log.debug(TAG, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
137
- continue;
138
- }
139
- // Skip cards with "Need Review" label (awaiting human review)
140
- if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
141
- log.debug(TAG, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
142
- continue;
143
- }
144
- // Skip review for cards without a branch reference — not qualified for auto-review
145
- if (mode === "review" &&
146
- !extractBranchFromDescription(card.description)) {
147
- log.debug(TAG, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
148
- continue;
149
- }
150
- log.info(TAG, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
151
- await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
152
- }
153
- }
154
- // Detect zombie runs: state-store says active, but either:
155
- // (a) another daemon's PID is dead, or
156
- // (b) our daemon holds the run but no worker is on the card, or
157
- // (c) heartbeat is older than staleHeartbeatMs.
158
- if (this.stateStore && this.agentConfig) {
159
- await this.recoverStaleRuns();
160
- }
161
- // Cards in queue/active but no longer assigned to agent → cancel/remove
162
- for (const knownId of knownCardIds) {
163
- if (!allAgentCardIds.has(knownId)) {
164
- log.info(TAG, `Missed unassign: ${knownId} — removing`);
165
- await this.pool.removeCard(knownId);
166
- }
167
- }
168
- log.debug(TAG, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
169
- }
170
- catch (err) {
171
- log.error(TAG, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
172
- }
173
- }
174
- }
@@ -1,30 +0,0 @@
1
- import type { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
2
- import type { RunRecord, StateStore } from "./state-store.js";
3
- import type { AgentConfig } from "./types.js";
4
- export interface RecoveryOutcome {
5
- runId: string;
6
- cardId: string;
7
- cardShortId: number;
8
- pipeline: "implement" | "review";
9
- actions: string[];
10
- errors: string[];
11
- }
12
- /**
13
- * Check if a process is still alive. A crashed daemon's PID is unlikely
14
- * to be reused within a reboot window; if it is, we still treat it as
15
- * orphaned because our current process is the new daemon.
16
- */
17
- export declare function isProcessAlive(pid: number, currentPid: number): boolean;
18
- /**
19
- * Reconcile orphaned runs from a previous daemon life.
20
- *
21
- * For each active run in the state store:
22
- * - If the daemon PID is alive (should not happen for a fresh process),
23
- * skip it — it's another instance.
24
- * - Otherwise: end the Harmony session, return the card to its pickup
25
- * column with the `agent-recovered` label, and cleanup the worktree.
26
- *
27
- * This runs once at daemon startup, before the pool accepts work.
28
- */
29
- export declare function recoverOrphans(store: StateStore, client: HarmonyApiClient, config: AgentConfig): Promise<RecoveryOutcome[]>;
30
- export declare function recoverRun(run: RunRecord, store: StateStore, client: HarmonyApiClient, config: AgentConfig, outcome: RecoveryOutcome): Promise<void>;
package/dist/recovery.js DELETED
@@ -1,141 +0,0 @@
1
- import { addLabelByName, moveCardToColumn } from "./board-helpers.js";
2
- import { log } from "./log.js";
3
- import { cleanupWorktree } from "./worktree.js";
4
- const TAG = "recovery";
5
- const RECOVERED_LABEL = "agent-recovered";
6
- const RECOVERED_LABEL_COLOR = "#f59e0b";
7
- /**
8
- * Check if a process is still alive. A crashed daemon's PID is unlikely
9
- * to be reused within a reboot window; if it is, we still treat it as
10
- * orphaned because our current process is the new daemon.
11
- */
12
- export function isProcessAlive(pid, currentPid) {
13
- if (pid === currentPid)
14
- return true;
15
- try {
16
- process.kill(pid, 0);
17
- return true;
18
- }
19
- catch {
20
- return false;
21
- }
22
- }
23
- async function fetchCardSafely(client, cardId) {
24
- try {
25
- const { card } = (await client.getCard(cardId));
26
- return card;
27
- }
28
- catch (err) {
29
- log.warn(TAG, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
30
- return null;
31
- }
32
- }
33
- /**
34
- * Reconcile orphaned runs from a previous daemon life.
35
- *
36
- * For each active run in the state store:
37
- * - If the daemon PID is alive (should not happen for a fresh process),
38
- * skip it — it's another instance.
39
- * - Otherwise: end the Harmony session, return the card to its pickup
40
- * column with the `agent-recovered` label, and cleanup the worktree.
41
- *
42
- * This runs once at daemon startup, before the pool accepts work.
43
- */
44
- export async function recoverOrphans(store, client, config) {
45
- const active = store.getActiveRuns();
46
- if (active.length === 0) {
47
- return [];
48
- }
49
- const outcomes = [];
50
- log.info(TAG, `recovering ${active.length} orphan run(s) from prior daemon`);
51
- for (const run of active) {
52
- const outcome = {
53
- runId: run.runId,
54
- cardId: run.cardId,
55
- cardShortId: run.cardShortId,
56
- pipeline: run.pipeline,
57
- actions: [],
58
- errors: [],
59
- };
60
- outcomes.push(outcome);
61
- if (isProcessAlive(run.daemonPid, process.pid)) {
62
- log.warn(TAG, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
63
- outcome.actions.push("skipped: daemon pid still alive");
64
- continue;
65
- }
66
- log.info(TAG, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
67
- await recoverRun(run, store, client, config, outcome);
68
- }
69
- return outcomes;
70
- }
71
- export async function recoverRun(run, store, client, config, outcome) {
72
- // 1. End the agent session so the card stops showing the progress ring.
73
- // Mark as failed (not paused) — daemon crash is an outcome, not a
74
- // user-initiated pause. UI renders the destructive tint + recovery branch
75
- // button if the run had pushed any commits.
76
- try {
77
- await client.endAgentSession(run.cardId, {
78
- status: "failed",
79
- failureReason: "daemon_restart",
80
- failureSummary: `Daemon restarted mid-${run.pipeline} (phase: ${run.phase})`,
81
- recoveryBranch: run.branchName ?? undefined,
82
- progressPercent: run.phase === "completing" ? 95 : undefined,
83
- });
84
- outcome.actions.push("ended agent session (failed: daemon_restart)");
85
- }
86
- catch (err) {
87
- const msg = err instanceof Error ? err.message : String(err);
88
- outcome.errors.push(`endAgentSession: ${msg}`);
89
- log.warn(TAG, `endAgentSession failed for ${run.cardId}: ${msg}`);
90
- }
91
- // 2. Move card back to a safe column and add the recovered label.
92
- // - implement pipeline → pickup column (usually "To Do")
93
- // - review pipeline → leave in place (reviewer will re-pick)
94
- const card = await fetchCardSafely(client, run.cardId);
95
- if (card) {
96
- if (run.pipeline === "implement") {
97
- const target = config.pickupColumns[0];
98
- if (target) {
99
- try {
100
- await moveCardToColumn(client, card, target);
101
- outcome.actions.push(`moved to "${target}"`);
102
- }
103
- catch (err) {
104
- const msg = err instanceof Error ? err.message : String(err);
105
- outcome.errors.push(`moveCardToColumn: ${msg}`);
106
- }
107
- }
108
- }
109
- try {
110
- await addLabelByName(client, card, RECOVERED_LABEL, RECOVERED_LABEL_COLOR);
111
- outcome.actions.push(`labeled "${RECOVERED_LABEL}"`);
112
- }
113
- catch (err) {
114
- const msg = err instanceof Error ? err.message : String(err);
115
- outcome.errors.push(`addLabel: ${msg}`);
116
- }
117
- }
118
- else {
119
- outcome.actions.push("card not reachable — local cleanup only");
120
- }
121
- // 3. Cleanup local worktree so it doesn't collide with future runs.
122
- if (run.worktreePath) {
123
- try {
124
- cleanupWorktree(run.worktreePath, run.branchName ?? undefined);
125
- outcome.actions.push("cleaned up worktree");
126
- }
127
- catch (err) {
128
- const msg = err instanceof Error ? err.message : String(err);
129
- outcome.errors.push(`cleanupWorktree: ${msg}`);
130
- }
131
- }
132
- // 4. Mark the run as orphaned in the store.
133
- try {
134
- await store.endRun(run.runId, "orphaned", "recovered after daemon restart");
135
- }
136
- catch (err) {
137
- const msg = err instanceof Error ? err.message : String(err);
138
- outcome.errors.push(`endRun: ${msg}`);
139
- }
140
- log.info(TAG, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
141
- }
@@ -1,35 +0,0 @@
1
- import type { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
2
- import type { Card } from "@harmony/shared";
3
- import { type SessionStats } from "./completion.js";
4
- import type { StateStore } from "./state-store.js";
5
- import { type AgentConfig } from "./types.js";
6
- export interface ReviewFinding {
7
- severity: "critical" | "major" | "minor";
8
- title: string;
9
- description: string;
10
- category?: string;
11
- location?: string;
12
- }
13
- export interface ScopeCheck {
14
- status: "clean" | "drift" | "missing";
15
- notes?: string;
16
- }
17
- export interface ReviewResult {
18
- verdict: "approved" | "rejected" | "error";
19
- summary: string;
20
- scopeCheck?: ScopeCheck;
21
- findings: ReviewFinding[];
22
- }
23
- /**
24
- * Parse Claude's review output into a structured ReviewResult.
25
- *
26
- * Tries multiple extraction strategies in order:
27
- * 1. ```json ... ``` fenced block (what the prompt asks for)
28
- * 2. Any top-level JSON object containing a "verdict" key (last-wins)
29
- * 3. Regex for a bare `"verdict": "approved|rejected"` anywhere — lossy
30
- * but keeps the pipeline moving
31
- * 4. Falls back to verdict: "error" — keeps card in Review instead of
32
- * bouncing it to To Do for a parse failure that isn't a code quality signal.
33
- */
34
- export declare function parseReviewOutput(stdout: string): ReviewResult;
35
- export declare function runReviewCompletion(client: HarmonyApiClient, card: Card, result: ReviewResult, config: AgentConfig, worktreePath: string, branchName: string | null, sessionStats: SessionStats | null | undefined, runLogPath: string | null | undefined, workspaceId: string | undefined, agentSessionId: string | null | undefined, stateStore: StateStore): Promise<void>;