@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.
- package/README.md +8 -1
- package/dist/cli.js +6376 -205
- package/dist/index.js +6206 -341
- package/package.json +2 -2
- package/dist/board-helpers.d.ts +0 -31
- package/dist/board-helpers.js +0 -150
- package/dist/budget.d.ts +0 -47
- package/dist/budget.js +0 -161
- package/dist/cli.d.ts +0 -16
- package/dist/completion.d.ts +0 -32
- package/dist/completion.js +0 -304
- package/dist/config-validation.d.ts +0 -23
- package/dist/config-validation.js +0 -77
- package/dist/config.d.ts +0 -23
- package/dist/config.js +0 -103
- package/dist/episode-writer.d.ts +0 -84
- package/dist/episode-writer.js +0 -232
- package/dist/git-pr.d.ts +0 -38
- package/dist/git-pr.js +0 -399
- package/dist/http-server.d.ts +0 -79
- package/dist/http-server.js +0 -114
- package/dist/index.d.ts +0 -5
- package/dist/log.d.ts +0 -34
- package/dist/log.js +0 -100
- package/dist/merge-monitor.d.ts +0 -23
- package/dist/merge-monitor.js +0 -169
- package/dist/pm.d.ts +0 -14
- package/dist/pm.js +0 -63
- package/dist/pool.d.ts +0 -70
- package/dist/pool.js +0 -258
- package/dist/process-group.d.ts +0 -26
- package/dist/process-group.js +0 -72
- package/dist/progress-tracker.d.ts +0 -79
- package/dist/progress-tracker.js +0 -442
- package/dist/prompt.d.ts +0 -18
- package/dist/prompt.js +0 -117
- package/dist/queue.d.ts +0 -39
- package/dist/queue.js +0 -100
- package/dist/reconcile.d.ts +0 -35
- package/dist/reconcile.js +0 -174
- package/dist/recovery.d.ts +0 -30
- package/dist/recovery.js +0 -141
- package/dist/review-completion.d.ts +0 -40
- package/dist/review-completion.js +0 -474
- package/dist/review-knowledge.d.ts +0 -14
- package/dist/review-knowledge.js +0 -89
- package/dist/review-prompt.d.ts +0 -12
- package/dist/review-prompt.js +0 -103
- package/dist/review-worker.d.ts +0 -56
- package/dist/review-worker.js +0 -638
- package/dist/review-worktree.d.ts +0 -12
- package/dist/review-worktree.js +0 -95
- package/dist/run-log.d.ts +0 -6
- package/dist/run-log.js +0 -19
- package/dist/startup-banner.d.ts +0 -29
- package/dist/startup-banner.js +0 -143
- package/dist/state-store.d.ts +0 -88
- package/dist/state-store.js +0 -239
- package/dist/stream-parser-selftest.d.ts +0 -9
- package/dist/stream-parser-selftest.js +0 -97
- package/dist/stream-parser.d.ts +0 -43
- package/dist/stream-parser.js +0 -174
- package/dist/transitions.d.ts +0 -57
- package/dist/transitions.js +0 -131
- package/dist/types.d.ts +0 -140
- package/dist/types.js +0 -79
- package/dist/verification.d.ts +0 -39
- package/dist/verification.js +0 -317
- package/dist/watcher.d.ts +0 -53
- package/dist/watcher.js +0 -153
- package/dist/worker.d.ts +0 -53
- package/dist/worker.js +0 -464
- package/dist/worktree-gc.d.ts +0 -67
- package/dist/worktree-gc.js +0 -245
- package/dist/worktree.d.ts +0 -18
- package/dist/worktree.js +0 -177
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { StreamParser } from "./stream-parser.js";
|
|
2
|
-
/**
|
|
3
|
-
* Feed a minimal Claude-CLI stream-json fixture through StreamParser and
|
|
4
|
-
* assert the expected events fire. Intended as a startup canary: if the CLI
|
|
5
|
-
* ever changes its envelope shape, the daemon fails loud on boot instead of
|
|
6
|
-
* silently parking every card with a parse error (see card #128).
|
|
7
|
-
*
|
|
8
|
-
* Throws on any missing event; returns silently on success.
|
|
9
|
-
*/
|
|
10
|
-
export function verifyStreamParserFormat() {
|
|
11
|
-
const parser = new StreamParser();
|
|
12
|
-
let textSeen = null;
|
|
13
|
-
let toolStart = null;
|
|
14
|
-
let toolEnd = null;
|
|
15
|
-
let resultSeen = null;
|
|
16
|
-
let costSeen = false;
|
|
17
|
-
parser.on("text", (c) => {
|
|
18
|
-
textSeen = c;
|
|
19
|
-
});
|
|
20
|
-
parser.on("tool_start", (name, input) => {
|
|
21
|
-
toolStart = { name, input };
|
|
22
|
-
});
|
|
23
|
-
parser.on("tool_end", (name, id, content) => {
|
|
24
|
-
toolEnd = { name, id, content };
|
|
25
|
-
});
|
|
26
|
-
parser.on("result", (stop) => {
|
|
27
|
-
resultSeen = stop;
|
|
28
|
-
});
|
|
29
|
-
parser.on("cost_update", () => {
|
|
30
|
-
costSeen = true;
|
|
31
|
-
});
|
|
32
|
-
const fixture = [
|
|
33
|
-
{
|
|
34
|
-
type: "assistant",
|
|
35
|
-
message: {
|
|
36
|
-
content: [{ type: "text", text: "canary-text" }],
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
type: "assistant",
|
|
41
|
-
message: {
|
|
42
|
-
content: [
|
|
43
|
-
{ type: "tool_use", id: "tu_canary", name: "Read", input: { x: 1 } },
|
|
44
|
-
],
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
type: "user",
|
|
49
|
-
message: {
|
|
50
|
-
content: [
|
|
51
|
-
{
|
|
52
|
-
type: "tool_result",
|
|
53
|
-
tool_use_id: "tu_canary",
|
|
54
|
-
content: "ok",
|
|
55
|
-
},
|
|
56
|
-
],
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
type: "result",
|
|
61
|
-
subtype: "success",
|
|
62
|
-
result: "done",
|
|
63
|
-
stop_reason: "end_turn",
|
|
64
|
-
total_cost_usd: 0.001,
|
|
65
|
-
usage: { input_tokens: 1, output_tokens: 1 },
|
|
66
|
-
},
|
|
67
|
-
];
|
|
68
|
-
for (const line of fixture) {
|
|
69
|
-
parser.feed(`${JSON.stringify(line)}\n`);
|
|
70
|
-
}
|
|
71
|
-
const failures = [];
|
|
72
|
-
if (textSeen !== "canary-text") {
|
|
73
|
-
failures.push(`text event missing or wrong (got ${JSON.stringify(textSeen)})`);
|
|
74
|
-
}
|
|
75
|
-
const ts = toolStart;
|
|
76
|
-
if (!ts || ts.name !== "Read") {
|
|
77
|
-
failures.push("tool_start event missing or wrong");
|
|
78
|
-
}
|
|
79
|
-
const te = toolEnd;
|
|
80
|
-
if (!te ||
|
|
81
|
-
te.name !== "Read" ||
|
|
82
|
-
te.id !== "tu_canary" ||
|
|
83
|
-
te.content !== "ok") {
|
|
84
|
-
failures.push("tool_end event missing or wrong");
|
|
85
|
-
}
|
|
86
|
-
if (resultSeen !== "end_turn") {
|
|
87
|
-
failures.push(`result event missing or wrong (got ${JSON.stringify(resultSeen)})`);
|
|
88
|
-
}
|
|
89
|
-
if (!costSeen) {
|
|
90
|
-
failures.push("cost_update event missing");
|
|
91
|
-
}
|
|
92
|
-
if (failures.length > 0) {
|
|
93
|
-
throw new Error("StreamParser canary failed — Claude CLI stream-json format may have drifted. " +
|
|
94
|
-
"Review pipeline will silently park every card until this is fixed.\n" +
|
|
95
|
-
failures.map((f) => ` - ${f}`).join("\n"));
|
|
96
|
-
}
|
|
97
|
-
}
|
package/dist/stream-parser.d.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from "node:events";
|
|
2
|
-
import type { Readable } from "node:stream";
|
|
3
|
-
export interface CostUpdate {
|
|
4
|
-
totalCostUsd: number;
|
|
5
|
-
/** Fresh input tokens only — does NOT include cache_read or cache_creation. */
|
|
6
|
-
totalInputTokens: number;
|
|
7
|
-
totalOutputTokens: number;
|
|
8
|
-
totalCacheCreationInputTokens: number;
|
|
9
|
-
totalCacheReadInputTokens: number;
|
|
10
|
-
durationMs: number;
|
|
11
|
-
durationApiMs: number;
|
|
12
|
-
numTurns: number;
|
|
13
|
-
modelName?: string;
|
|
14
|
-
}
|
|
15
|
-
export interface StreamParserEvents {
|
|
16
|
-
tool_start: [name: string, input: unknown];
|
|
17
|
-
tool_end: [name: string, toolUseId: string, content: string | undefined];
|
|
18
|
-
text: [content: string];
|
|
19
|
-
result: [stopReason: string];
|
|
20
|
-
cost_update: [cost: CostUpdate];
|
|
21
|
-
parse_error: [msg: string];
|
|
22
|
-
}
|
|
23
|
-
export declare class StreamParser extends EventEmitter<StreamParserEvents> {
|
|
24
|
-
private buffer;
|
|
25
|
-
private attached;
|
|
26
|
-
private toolNames;
|
|
27
|
-
private hasEmittedText;
|
|
28
|
-
private observedModel?;
|
|
29
|
-
/**
|
|
30
|
-
* Attach a readable stream (Claude CLI stdout) to the parser.
|
|
31
|
-
* Parses NDJSON lines and emits typed events.
|
|
32
|
-
* Each instance must only be attached once.
|
|
33
|
-
*/
|
|
34
|
-
attach(stream: Readable): void;
|
|
35
|
-
/**
|
|
36
|
-
* Feed a raw NDJSON chunk directly. Exposed for tests and any caller
|
|
37
|
-
* that doesn't have a Readable stream handy.
|
|
38
|
-
*/
|
|
39
|
-
feed(chunk: string): void;
|
|
40
|
-
private flush;
|
|
41
|
-
private parseLine;
|
|
42
|
-
private handleMessage;
|
|
43
|
-
}
|
package/dist/stream-parser.js
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from "node:events";
|
|
2
|
-
import { log } from "./log.js";
|
|
3
|
-
const TAG = "stream-parser";
|
|
4
|
-
export class StreamParser extends EventEmitter {
|
|
5
|
-
buffer = "";
|
|
6
|
-
attached = false;
|
|
7
|
-
toolNames = new Map();
|
|
8
|
-
hasEmittedText = false;
|
|
9
|
-
observedModel;
|
|
10
|
-
/**
|
|
11
|
-
* Attach a readable stream (Claude CLI stdout) to the parser.
|
|
12
|
-
* Parses NDJSON lines and emits typed events.
|
|
13
|
-
* Each instance must only be attached once.
|
|
14
|
-
*/
|
|
15
|
-
attach(stream) {
|
|
16
|
-
if (this.attached) {
|
|
17
|
-
throw new Error("StreamParser already attached to a stream");
|
|
18
|
-
}
|
|
19
|
-
this.attached = true;
|
|
20
|
-
stream.on("data", (chunk) => {
|
|
21
|
-
this.buffer += chunk.toString();
|
|
22
|
-
this.flush();
|
|
23
|
-
});
|
|
24
|
-
stream.on("end", () => {
|
|
25
|
-
// Process any remaining buffer
|
|
26
|
-
if (this.buffer.trim()) {
|
|
27
|
-
this.flush();
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Feed a raw NDJSON chunk directly. Exposed for tests and any caller
|
|
33
|
-
* that doesn't have a Readable stream handy.
|
|
34
|
-
*/
|
|
35
|
-
feed(chunk) {
|
|
36
|
-
this.buffer += chunk;
|
|
37
|
-
this.flush();
|
|
38
|
-
}
|
|
39
|
-
flush() {
|
|
40
|
-
const lines = this.buffer.split("\n");
|
|
41
|
-
// Keep incomplete last line in buffer
|
|
42
|
-
this.buffer = lines.pop() ?? "";
|
|
43
|
-
for (const line of lines) {
|
|
44
|
-
const trimmed = line.trim();
|
|
45
|
-
if (!trimmed)
|
|
46
|
-
continue;
|
|
47
|
-
this.parseLine(trimmed);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
parseLine(line) {
|
|
51
|
-
let msg;
|
|
52
|
-
try {
|
|
53
|
-
msg = JSON.parse(line);
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
// Not valid JSON — skip silently (could be stray output)
|
|
57
|
-
log.debug(TAG, `Non-JSON line: ${line.slice(0, 100)}`);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
try {
|
|
61
|
-
this.handleMessage(msg);
|
|
62
|
-
}
|
|
63
|
-
catch (err) {
|
|
64
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
65
|
-
log.warn(TAG, `Error handling stream event: ${errMsg}`);
|
|
66
|
-
this.emit("parse_error", errMsg);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
handleMessage(msg) {
|
|
70
|
-
// Capture model from any envelope that carries it. The Claude CLI exposes
|
|
71
|
-
// `model` on the top-level `system` envelope and inside each assistant
|
|
72
|
-
// envelope's message, but the final `result` envelope does not.
|
|
73
|
-
if (!this.observedModel) {
|
|
74
|
-
if (typeof msg.model === "string") {
|
|
75
|
-
this.observedModel = msg.model;
|
|
76
|
-
}
|
|
77
|
-
else if (typeof msg.message?.model === "string") {
|
|
78
|
-
this.observedModel = msg.message.model;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
switch (msg.type) {
|
|
82
|
-
case "assistant": {
|
|
83
|
-
const blocks = msg.message?.content;
|
|
84
|
-
if (!Array.isArray(blocks))
|
|
85
|
-
break;
|
|
86
|
-
for (const block of blocks) {
|
|
87
|
-
if (block.type === "text" && typeof block.text === "string") {
|
|
88
|
-
this.emit("text", block.text);
|
|
89
|
-
this.hasEmittedText = true;
|
|
90
|
-
}
|
|
91
|
-
else if (block.type === "tool_use" &&
|
|
92
|
-
typeof block.name === "string") {
|
|
93
|
-
if (typeof block.id === "string") {
|
|
94
|
-
this.toolNames.set(block.id, block.name);
|
|
95
|
-
}
|
|
96
|
-
this.emit("tool_start", block.name, block.input);
|
|
97
|
-
}
|
|
98
|
-
// thinking blocks, redacted_thinking, etc. — ignored
|
|
99
|
-
}
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
case "user": {
|
|
103
|
-
const blocks = msg.message?.content;
|
|
104
|
-
if (!Array.isArray(blocks))
|
|
105
|
-
break;
|
|
106
|
-
for (const block of blocks) {
|
|
107
|
-
if (block.type === "tool_result" &&
|
|
108
|
-
typeof block.tool_use_id === "string") {
|
|
109
|
-
const name = this.toolNames.get(block.tool_use_id) ?? "";
|
|
110
|
-
this.toolNames.delete(block.tool_use_id);
|
|
111
|
-
const content = normalizeToolResultContent(block.content);
|
|
112
|
-
this.emit("tool_end", name, block.tool_use_id, content);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
case "result": {
|
|
118
|
-
// Fallback: if the CLI ended without streaming any assistant text
|
|
119
|
-
// (e.g. hit max-turns), the final `result` field still carries the
|
|
120
|
-
// last assistant message. Emit it so the caller can parse the verdict.
|
|
121
|
-
if (!this.hasEmittedText &&
|
|
122
|
-
typeof msg.result === "string" &&
|
|
123
|
-
msg.result.length > 0) {
|
|
124
|
-
this.emit("text", msg.result);
|
|
125
|
-
this.hasEmittedText = true;
|
|
126
|
-
}
|
|
127
|
-
if (typeof msg.total_cost_usd === "number") {
|
|
128
|
-
const usage = msg.usage;
|
|
129
|
-
this.emit("cost_update", {
|
|
130
|
-
totalCostUsd: msg.total_cost_usd,
|
|
131
|
-
totalInputTokens: usage?.input_tokens ?? 0,
|
|
132
|
-
totalOutputTokens: usage?.output_tokens ?? 0,
|
|
133
|
-
totalCacheCreationInputTokens: usage?.cache_creation_input_tokens ?? 0,
|
|
134
|
-
totalCacheReadInputTokens: usage?.cache_read_input_tokens ?? 0,
|
|
135
|
-
durationMs: msg.duration_ms ?? 0,
|
|
136
|
-
durationApiMs: msg.duration_api_ms ?? 0,
|
|
137
|
-
numTurns: msg.num_turns ?? 0,
|
|
138
|
-
modelName: this.observedModel ??
|
|
139
|
-
(typeof msg.model === "string" ? msg.model : undefined),
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
this.emit("result", msg.stop_reason ?? msg.subtype ?? "unknown");
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
// Ignore system, ping, etc.
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
function normalizeToolResultContent(raw) {
|
|
150
|
-
if (raw == null)
|
|
151
|
-
return undefined;
|
|
152
|
-
if (typeof raw === "string")
|
|
153
|
-
return raw;
|
|
154
|
-
if (Array.isArray(raw)) {
|
|
155
|
-
// Anthropic sometimes returns content as a list of typed blocks
|
|
156
|
-
// ({ type: "text", text: "..." }); flatten text blocks, fall back to JSON.
|
|
157
|
-
const parts = [];
|
|
158
|
-
for (const block of raw) {
|
|
159
|
-
if (block &&
|
|
160
|
-
typeof block === "object" &&
|
|
161
|
-
"text" in block &&
|
|
162
|
-
typeof block.text === "string") {
|
|
163
|
-
parts.push(block.text);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return parts.length > 0 ? parts.join("") : JSON.stringify(raw);
|
|
167
|
-
}
|
|
168
|
-
try {
|
|
169
|
-
return JSON.stringify(raw);
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
return String(raw);
|
|
173
|
-
}
|
|
174
|
-
}
|
package/dist/transitions.d.ts
DELETED
|
@@ -1,57 +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
|
-
export type TransitionStep = "move" | "addLabel" | "removeLabel" | "updateCard" | "endSession";
|
|
5
|
-
export declare class TransitionError extends Error {
|
|
6
|
-
readonly step: TransitionStep;
|
|
7
|
-
readonly attempts: number;
|
|
8
|
-
readonly detail: string;
|
|
9
|
-
constructor(step: TransitionStep, attempts: number, detail: string, options?: {
|
|
10
|
-
cause?: unknown;
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
export interface EndSessionArgs {
|
|
14
|
-
status: "completed" | "paused";
|
|
15
|
-
progressPercent?: number;
|
|
16
|
-
costCents?: number;
|
|
17
|
-
inputTokens?: number;
|
|
18
|
-
outputTokens?: number;
|
|
19
|
-
}
|
|
20
|
-
export interface TransitionPlan {
|
|
21
|
-
/** Target column name. No-op if the card is already there. */
|
|
22
|
-
move?: {
|
|
23
|
-
columnName: string;
|
|
24
|
-
};
|
|
25
|
-
/** Labels to add (idempotent — existing labels skipped). */
|
|
26
|
-
addLabels?: Array<{
|
|
27
|
-
name: string;
|
|
28
|
-
color?: string;
|
|
29
|
-
}>;
|
|
30
|
-
/** Labels to remove (idempotent — missing labels skipped). */
|
|
31
|
-
removeLabels?: string[];
|
|
32
|
-
/** Arbitrary card field updates. */
|
|
33
|
-
updateCard?: {
|
|
34
|
-
description?: string;
|
|
35
|
-
title?: string;
|
|
36
|
-
};
|
|
37
|
-
/** End the active agent session. */
|
|
38
|
-
endSession?: EndSessionArgs;
|
|
39
|
-
}
|
|
40
|
-
export interface TransitionOptions {
|
|
41
|
-
retries?: number;
|
|
42
|
-
backoffMs?: number;
|
|
43
|
-
store?: StateStore;
|
|
44
|
-
runId?: string;
|
|
45
|
-
/** If true, a missing column throws instead of being a no-op. */
|
|
46
|
-
strictColumn?: boolean;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Execute a board transition as an ordered, retriable sequence of steps.
|
|
50
|
-
*
|
|
51
|
-
* Replaces the scattered `try { ... } catch { /* best-effort * / }` pattern
|
|
52
|
-
* so every partial failure surfaces with structured context. Operations
|
|
53
|
-
* are individually idempotent (move-to-same-column is a no-op, add/remove
|
|
54
|
-
* label checks existing state) so re-running a partially-succeeded
|
|
55
|
-
* transition is safe.
|
|
56
|
-
*/
|
|
57
|
-
export declare function runTransition(client: HarmonyApiClient, card: Card, plan: TransitionPlan, opts?: TransitionOptions): Promise<void>;
|
package/dist/transitions.js
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { log } from "./log.js";
|
|
2
|
-
const TAG = "transition";
|
|
3
|
-
export class TransitionError extends Error {
|
|
4
|
-
step;
|
|
5
|
-
attempts;
|
|
6
|
-
detail;
|
|
7
|
-
constructor(step, attempts, detail, options) {
|
|
8
|
-
super(`${step} failed after ${attempts} attempt(s): ${detail}`, options);
|
|
9
|
-
this.step = step;
|
|
10
|
-
this.attempts = attempts;
|
|
11
|
-
this.detail = detail;
|
|
12
|
-
this.name = "TransitionError";
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Retry a flaky step with exponential backoff. Only the last error is
|
|
17
|
-
* thrown — intermediate errors are logged as warnings so operators can
|
|
18
|
-
* see that retries happened without losing the final cause.
|
|
19
|
-
*/
|
|
20
|
-
async function withRetry(step, cardShortId, op, attempts, backoffMs) {
|
|
21
|
-
let lastErr;
|
|
22
|
-
for (let i = 0; i < attempts; i++) {
|
|
23
|
-
try {
|
|
24
|
-
return await op();
|
|
25
|
-
}
|
|
26
|
-
catch (err) {
|
|
27
|
-
lastErr = err;
|
|
28
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
29
|
-
if (i < attempts - 1) {
|
|
30
|
-
const wait = backoffMs * 2 ** i;
|
|
31
|
-
log.warn(TAG, `${step} failed for #${cardShortId} (attempt ${i + 1}/${attempts}): ${msg} — retrying in ${wait}ms`);
|
|
32
|
-
await new Promise((r) => setTimeout(r, wait));
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
const msg = lastErr instanceof Error ? lastErr.message : String(lastErr);
|
|
37
|
-
throw new TransitionError(step, attempts, msg, { cause: lastErr });
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Execute a board transition as an ordered, retriable sequence of steps.
|
|
41
|
-
*
|
|
42
|
-
* Replaces the scattered `try { ... } catch { /* best-effort * / }` pattern
|
|
43
|
-
* so every partial failure surfaces with structured context. Operations
|
|
44
|
-
* are individually idempotent (move-to-same-column is a no-op, add/remove
|
|
45
|
-
* label checks existing state) so re-running a partially-succeeded
|
|
46
|
-
* transition is safe.
|
|
47
|
-
*/
|
|
48
|
-
export async function runTransition(client, card, plan, opts = {}) {
|
|
49
|
-
const attempts = opts.retries ?? 3;
|
|
50
|
-
const backoffMs = opts.backoffMs ?? 500;
|
|
51
|
-
const shortId = card.short_id;
|
|
52
|
-
// Fetch the board once so we can resolve columns and labels together.
|
|
53
|
-
// Mutations to card state (move, label add/remove) all invalidate the
|
|
54
|
-
// same board-level data anyway, so one read is enough.
|
|
55
|
-
const board = (await withRetry("move", shortId, () => client.getBoard(card.project_id), attempts, backoffMs));
|
|
56
|
-
const columns = board.columns;
|
|
57
|
-
const labels = board.labels ?? [];
|
|
58
|
-
// --- 1. MOVE ---
|
|
59
|
-
if (plan.move) {
|
|
60
|
-
const target = columns.find((c) => c.name.toLowerCase() === plan.move.columnName.toLowerCase());
|
|
61
|
-
if (!target) {
|
|
62
|
-
const msg = `column "${plan.move.columnName}" not found`;
|
|
63
|
-
if (opts.strictColumn) {
|
|
64
|
-
throw new TransitionError("move", 1, msg);
|
|
65
|
-
}
|
|
66
|
-
log.warn(TAG, `#${shortId}: ${msg} — skipping move`);
|
|
67
|
-
}
|
|
68
|
-
else if (card.column_id !== target.id) {
|
|
69
|
-
await withRetry("move", shortId, () => client.moveCard(card.id, target.id), attempts, backoffMs);
|
|
70
|
-
log.info(TAG, `#${shortId} → "${target.name}"`);
|
|
71
|
-
card.column_id = target.id;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
// --- 2. ADD LABELS ---
|
|
75
|
-
if (plan.addLabels?.length) {
|
|
76
|
-
const existing = new Set(card.labelIds ?? []);
|
|
77
|
-
for (const { name, color } of plan.addLabels) {
|
|
78
|
-
const match = labels.find((l) => l.name.toLowerCase() === name.toLowerCase());
|
|
79
|
-
const labelId = match?.id ??
|
|
80
|
-
(await ensureLabel(client, card.project_id, name, color, attempts, backoffMs));
|
|
81
|
-
if (!labelId || existing.has(labelId))
|
|
82
|
-
continue;
|
|
83
|
-
await withRetry("addLabel", shortId, () => client.addLabelToCard(card.id, labelId), attempts, backoffMs);
|
|
84
|
-
existing.add(labelId);
|
|
85
|
-
log.info(TAG, `#${shortId} +label "${name}"`);
|
|
86
|
-
}
|
|
87
|
-
card.labelIds = Array.from(existing);
|
|
88
|
-
}
|
|
89
|
-
// --- 3. REMOVE LABELS ---
|
|
90
|
-
if (plan.removeLabels?.length) {
|
|
91
|
-
const existing = new Set(card.labelIds ?? []);
|
|
92
|
-
for (const name of plan.removeLabels) {
|
|
93
|
-
const match = labels.find((l) => l.name.toLowerCase() === name.toLowerCase());
|
|
94
|
-
if (!match || !existing.has(match.id))
|
|
95
|
-
continue;
|
|
96
|
-
await withRetry("removeLabel", shortId, () => client.removeLabelFromCard(card.id, match.id), attempts, backoffMs);
|
|
97
|
-
existing.delete(match.id);
|
|
98
|
-
log.info(TAG, `#${shortId} -label "${name}"`);
|
|
99
|
-
}
|
|
100
|
-
card.labelIds = Array.from(existing);
|
|
101
|
-
}
|
|
102
|
-
// --- 4. UPDATE CARD ---
|
|
103
|
-
if (plan.updateCard) {
|
|
104
|
-
await withRetry("updateCard", shortId, () => client.updateCard(card.id, plan.updateCard), attempts, backoffMs);
|
|
105
|
-
log.info(TAG, `#${shortId} updated`);
|
|
106
|
-
}
|
|
107
|
-
// --- 5. END SESSION ---
|
|
108
|
-
if (plan.endSession) {
|
|
109
|
-
await withRetry("endSession", shortId, () => client.endAgentSession(card.id, plan.endSession), attempts, backoffMs);
|
|
110
|
-
log.info(TAG, `#${shortId} session ended (${plan.endSession.status})`);
|
|
111
|
-
}
|
|
112
|
-
// --- Audit trail ---
|
|
113
|
-
if (opts.store && opts.runId) {
|
|
114
|
-
try {
|
|
115
|
-
await opts.store.heartbeat(opts.runId);
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
// non-fatal
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
async function ensureLabel(client, projectId, name, color, attempts, backoffMs) {
|
|
123
|
-
try {
|
|
124
|
-
const result = await withRetry("addLabel", 0, () => client.createLabel(projectId, { name, color: color ?? "#8b5cf6" }), attempts, backoffMs);
|
|
125
|
-
return result?.label?.id ?? null;
|
|
126
|
-
}
|
|
127
|
-
catch (err) {
|
|
128
|
-
log.warn(TAG, `ensureLabel "${name}" failed: ${err instanceof Error ? err.message : err}`);
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
}
|
package/dist/types.d.ts
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import type { Card, Column, Label, Subtask } from "@harmony/shared";
|
|
2
|
-
export type WorkMode = "implement" | "review";
|
|
3
|
-
export interface AgentConfig {
|
|
4
|
-
poolSize: number;
|
|
5
|
-
maxTimeout: number;
|
|
6
|
-
pickupColumns: string[];
|
|
7
|
-
priorityLabels: Record<string, number>;
|
|
8
|
-
columnBoost: boolean;
|
|
9
|
-
completion: {
|
|
10
|
-
createPR: boolean;
|
|
11
|
-
moveToColumn: string;
|
|
12
|
-
postSummary: boolean;
|
|
13
|
-
};
|
|
14
|
-
claude: {
|
|
15
|
-
model: string;
|
|
16
|
-
reviewModel: string;
|
|
17
|
-
maxTurns: number;
|
|
18
|
-
additionalArgs: string[];
|
|
19
|
-
};
|
|
20
|
-
worktree: {
|
|
21
|
-
basePath: string;
|
|
22
|
-
baseBranch: string;
|
|
23
|
-
/** Remote-branch prefix while an attempt is still in-flight or failed. */
|
|
24
|
-
failedBranchPrefix: string;
|
|
25
|
-
/** Remote-branch prefix after a successful run reaches Review. */
|
|
26
|
-
approvedBranchPrefix: string;
|
|
27
|
-
/** Days to keep failed-attempt branches on origin before GC removes them. */
|
|
28
|
-
failedAttemptRetentionDays: number;
|
|
29
|
-
};
|
|
30
|
-
verification: {
|
|
31
|
-
enabled: boolean;
|
|
32
|
-
build: boolean;
|
|
33
|
-
lint: boolean;
|
|
34
|
-
autoFix: boolean;
|
|
35
|
-
maxFixAttempts: number;
|
|
36
|
-
deepReview: boolean;
|
|
37
|
-
devServerBasePort: number;
|
|
38
|
-
timeout: number;
|
|
39
|
-
failColumn: string;
|
|
40
|
-
};
|
|
41
|
-
review: {
|
|
42
|
-
enabled: boolean;
|
|
43
|
-
/** Concurrent review workers. Each gets its own dev-server port slot. */
|
|
44
|
-
poolSize: number;
|
|
45
|
-
pickupColumns: string[];
|
|
46
|
-
moveToColumn: string;
|
|
47
|
-
failColumn: string;
|
|
48
|
-
devServerPort: number;
|
|
49
|
-
maxTimeout: number;
|
|
50
|
-
postFindings: boolean;
|
|
51
|
-
maxReviewCycles: number;
|
|
52
|
-
createPR: boolean;
|
|
53
|
-
approvedLabel: string;
|
|
54
|
-
approvedLabelColor: string;
|
|
55
|
-
mergeMonitor: boolean;
|
|
56
|
-
mergedLabel: string;
|
|
57
|
-
mergedLabelColor: string;
|
|
58
|
-
};
|
|
59
|
-
budget: {
|
|
60
|
-
/** Max implement attempts per card before DLQ (reset on success). */
|
|
61
|
-
maxAttemptsPerCard: number;
|
|
62
|
-
/** Max cumulative spend per card, in cents, before DLQ. */
|
|
63
|
-
maxCentsPerCard: number;
|
|
64
|
-
/** Daily spend cap, in cents (UTC day). Exceeded → pause pickups. */
|
|
65
|
-
dailyBudgetCents: number;
|
|
66
|
-
/** Label applied to DLQ'd cards. */
|
|
67
|
-
dlqLabel: string;
|
|
68
|
-
dlqLabelColor: string;
|
|
69
|
-
};
|
|
70
|
-
http: {
|
|
71
|
-
/** Local HTTP status/control server. Bound to 127.0.0.1 by default. */
|
|
72
|
-
enabled: boolean;
|
|
73
|
-
port: number;
|
|
74
|
-
bindAddr: string;
|
|
75
|
-
};
|
|
76
|
-
timing: {
|
|
77
|
-
heartbeatMs: number;
|
|
78
|
-
staleHeartbeatMs: number;
|
|
79
|
-
reconcileIntervalMs: number;
|
|
80
|
-
worktreeGcIntervalMs: number;
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
export declare const DEFAULT_AGENT_CONFIG: AgentConfig;
|
|
84
|
-
export declare const NEED_REVIEW_LABEL = "Need Review";
|
|
85
|
-
export declare const NEED_REVIEW_LABEL_COLOR = "#f59e0b";
|
|
86
|
-
export declare const AGENT_NAME = "Harmony Agent";
|
|
87
|
-
export declare function agentIdentifier(workerId: number): string;
|
|
88
|
-
export type ProgressPhase = "exploring" | "implementing" | "testing" | "committing" | "finishing";
|
|
89
|
-
export type WorkerState = "idle" | "preparing" | "running" | "completing" | "verifying" | "cancelling" | "error";
|
|
90
|
-
export interface QueueItem {
|
|
91
|
-
cardId: string;
|
|
92
|
-
shortId: number;
|
|
93
|
-
title: string;
|
|
94
|
-
priority: number;
|
|
95
|
-
enqueuedAt: number;
|
|
96
|
-
mode: WorkMode;
|
|
97
|
-
}
|
|
98
|
-
export interface EnrichedCard {
|
|
99
|
-
card: Card;
|
|
100
|
-
column: Column;
|
|
101
|
-
labels: Label[];
|
|
102
|
-
subtasks: Subtask[];
|
|
103
|
-
mode: WorkMode;
|
|
104
|
-
}
|
|
105
|
-
export interface RealtimeCredentials {
|
|
106
|
-
supabaseUrl: string;
|
|
107
|
-
supabaseAnonKey: string;
|
|
108
|
-
}
|
|
109
|
-
/** Pipeline that produced an episode. */
|
|
110
|
-
export type EpisodeKind = "implement" | "review";
|
|
111
|
-
/** Outcome of an implement run; review verdict maps to its own type. */
|
|
112
|
-
export type EpisodeOutcome = "success" | "failure";
|
|
113
|
-
/**
|
|
114
|
-
* Structured metadata persisted alongside every episode entity in
|
|
115
|
-
* `knowledge_entities.metadata`. Read by the recall path to render the
|
|
116
|
-
* "Similar past tasks" section in subsequent agent prompts.
|
|
117
|
-
*/
|
|
118
|
-
export interface EpisodeMeta {
|
|
119
|
-
episode_kind: EpisodeKind;
|
|
120
|
-
card_short_id: number;
|
|
121
|
-
card_title: string;
|
|
122
|
-
approach_summary: string;
|
|
123
|
-
outcome: EpisodeOutcome;
|
|
124
|
-
quality_score: number;
|
|
125
|
-
duration_ms: number;
|
|
126
|
-
token_cost: {
|
|
127
|
-
input: number;
|
|
128
|
-
output: number;
|
|
129
|
-
usd: number;
|
|
130
|
-
};
|
|
131
|
-
files_touched: number;
|
|
132
|
-
num_turns: number;
|
|
133
|
-
error?: string;
|
|
134
|
-
/** Provenance only — never used as memory scope. */
|
|
135
|
-
agent_session_id?: string;
|
|
136
|
-
/** Set on back-fill from review pipeline. */
|
|
137
|
-
review_session_id?: string;
|
|
138
|
-
/** Set on review-decision entities so back-fill can find the original. */
|
|
139
|
-
original_episode_id?: string;
|
|
140
|
-
}
|