@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
package/dist/http-server.d.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
export interface HealthSnapshot {
|
|
2
|
-
healthy: boolean;
|
|
3
|
-
checks: Record<string, boolean>;
|
|
4
|
-
}
|
|
5
|
-
export interface WorkerSnapshot {
|
|
6
|
-
id: number;
|
|
7
|
-
pipeline: "implement" | "review";
|
|
8
|
-
state: string;
|
|
9
|
-
cardId: string | null;
|
|
10
|
-
cardShortId: number | null;
|
|
11
|
-
phase: string | null;
|
|
12
|
-
startedAt: number | null;
|
|
13
|
-
branchName: string | null;
|
|
14
|
-
}
|
|
15
|
-
export interface StatusSnapshot {
|
|
16
|
-
daemonId: string;
|
|
17
|
-
daemonPid: number;
|
|
18
|
-
startedAt: number;
|
|
19
|
-
uptimeMs: number;
|
|
20
|
-
workers: WorkerSnapshot[];
|
|
21
|
-
implQueue: Array<{
|
|
22
|
-
cardId: string;
|
|
23
|
-
shortId: number;
|
|
24
|
-
priority: number;
|
|
25
|
-
enqueuedAt: number;
|
|
26
|
-
}>;
|
|
27
|
-
reviewQueue: Array<{
|
|
28
|
-
cardId: string;
|
|
29
|
-
shortId: number;
|
|
30
|
-
priority: number;
|
|
31
|
-
enqueuedAt: number;
|
|
32
|
-
}>;
|
|
33
|
-
dlq: Array<{
|
|
34
|
-
cardId: string;
|
|
35
|
-
reason: string;
|
|
36
|
-
attempts: number;
|
|
37
|
-
totalCostCents: number;
|
|
38
|
-
}>;
|
|
39
|
-
budget: {
|
|
40
|
-
todayCents: number;
|
|
41
|
-
dailyCapCents: number;
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
export type Command = "pause" | "resume" | "stop";
|
|
45
|
-
export interface HttpServerOptions {
|
|
46
|
-
port: number;
|
|
47
|
-
bindAddr: string;
|
|
48
|
-
getStatus: () => StatusSnapshot;
|
|
49
|
-
getHealth: () => HealthSnapshot;
|
|
50
|
-
handleCommand: (cmd: Command, cardId: string) => Promise<void>;
|
|
51
|
-
/**
|
|
52
|
-
* Clear a card's DLQ marker. Routed through the daemon (not CLI
|
|
53
|
-
* direct-write) so we never have two processes racing to persist
|
|
54
|
-
* the same state-store file.
|
|
55
|
-
*/
|
|
56
|
-
clearDlq: (cardId: string) => Promise<void>;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Tiny introspection + control surface for the running daemon.
|
|
60
|
-
* Binds to 127.0.0.1 by default so it's not network-reachable.
|
|
61
|
-
*
|
|
62
|
-
* GET /health — 200 when healthy, 503 otherwise. Simple liveness check
|
|
63
|
-
* suitable for process supervisors (systemd, pm2, docker healthcheck).
|
|
64
|
-
* GET /status — full JSON snapshot: workers, queues, DLQ, budget.
|
|
65
|
-
* POST /pause/:cardId, /resume/:cardId, /stop/:cardId — command path
|
|
66
|
-
* so an operator can nudge a stuck worker without killing the daemon.
|
|
67
|
-
*/
|
|
68
|
-
export declare class HttpServer {
|
|
69
|
-
private opts;
|
|
70
|
-
private server;
|
|
71
|
-
constructor(opts: HttpServerOptions);
|
|
72
|
-
start(): Promise<void>;
|
|
73
|
-
stop(): Promise<void>;
|
|
74
|
-
private route;
|
|
75
|
-
private respondDlqClear;
|
|
76
|
-
private respondHealth;
|
|
77
|
-
private respondStatus;
|
|
78
|
-
private respondCommand;
|
|
79
|
-
}
|
package/dist/http-server.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { createServer, } from "node:http";
|
|
2
|
-
import { log } from "./log.js";
|
|
3
|
-
const TAG = "http";
|
|
4
|
-
/**
|
|
5
|
-
* Tiny introspection + control surface for the running daemon.
|
|
6
|
-
* Binds to 127.0.0.1 by default so it's not network-reachable.
|
|
7
|
-
*
|
|
8
|
-
* GET /health — 200 when healthy, 503 otherwise. Simple liveness check
|
|
9
|
-
* suitable for process supervisors (systemd, pm2, docker healthcheck).
|
|
10
|
-
* GET /status — full JSON snapshot: workers, queues, DLQ, budget.
|
|
11
|
-
* POST /pause/:cardId, /resume/:cardId, /stop/:cardId — command path
|
|
12
|
-
* so an operator can nudge a stuck worker without killing the daemon.
|
|
13
|
-
*/
|
|
14
|
-
export class HttpServer {
|
|
15
|
-
opts;
|
|
16
|
-
server = null;
|
|
17
|
-
constructor(opts) {
|
|
18
|
-
this.opts = opts;
|
|
19
|
-
}
|
|
20
|
-
async start() {
|
|
21
|
-
return new Promise((resolve, reject) => {
|
|
22
|
-
this.server = createServer((req, res) => {
|
|
23
|
-
this.route(req, res).catch((err) => {
|
|
24
|
-
log.error(TAG, `unhandled: ${err instanceof Error ? err.message : err}`);
|
|
25
|
-
if (!res.headersSent) {
|
|
26
|
-
res.writeHead(500, { "content-type": "application/json" });
|
|
27
|
-
res.end(JSON.stringify({ error: "internal_error" }));
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
this.server.once("error", reject);
|
|
32
|
-
this.server.listen(this.opts.port, this.opts.bindAddr, () => {
|
|
33
|
-
resolve();
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
async stop() {
|
|
38
|
-
if (!this.server)
|
|
39
|
-
return;
|
|
40
|
-
await new Promise((resolve) => {
|
|
41
|
-
this.server?.close(() => resolve());
|
|
42
|
-
});
|
|
43
|
-
this.server = null;
|
|
44
|
-
}
|
|
45
|
-
async route(req, res) {
|
|
46
|
-
const url = new URL(req.url ?? "/", "http://localhost");
|
|
47
|
-
const method = (req.method ?? "GET").toUpperCase();
|
|
48
|
-
const path = url.pathname;
|
|
49
|
-
if (method === "GET" && path === "/health") {
|
|
50
|
-
return this.respondHealth(res);
|
|
51
|
-
}
|
|
52
|
-
if (method === "GET" && path === "/status") {
|
|
53
|
-
return this.respondStatus(res);
|
|
54
|
-
}
|
|
55
|
-
if (method === "POST") {
|
|
56
|
-
const dlq = path.match(/^\/dlq\/clear\/([^/]+)$/);
|
|
57
|
-
if (dlq) {
|
|
58
|
-
return this.respondDlqClear(res, decodeURIComponent(dlq[1]));
|
|
59
|
-
}
|
|
60
|
-
const cmd = parseCommand(path);
|
|
61
|
-
if (cmd) {
|
|
62
|
-
return this.respondCommand(res, cmd.command, cmd.cardId);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
res.writeHead(404, { "content-type": "application/json" });
|
|
66
|
-
res.end(JSON.stringify({ error: "not_found", path }));
|
|
67
|
-
}
|
|
68
|
-
async respondDlqClear(res, cardId) {
|
|
69
|
-
try {
|
|
70
|
-
await this.opts.clearDlq(cardId);
|
|
71
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
72
|
-
res.end(JSON.stringify({ ok: true, cardId }));
|
|
73
|
-
}
|
|
74
|
-
catch (err) {
|
|
75
|
-
res.writeHead(500, { "content-type": "application/json" });
|
|
76
|
-
res.end(JSON.stringify({
|
|
77
|
-
error: "clear_dlq_failed",
|
|
78
|
-
detail: err instanceof Error ? err.message : String(err),
|
|
79
|
-
}));
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
respondHealth(res) {
|
|
83
|
-
const health = this.opts.getHealth();
|
|
84
|
-
res.writeHead(health.healthy ? 200 : 503, {
|
|
85
|
-
"content-type": "application/json",
|
|
86
|
-
});
|
|
87
|
-
res.end(JSON.stringify(health));
|
|
88
|
-
}
|
|
89
|
-
respondStatus(res) {
|
|
90
|
-
const snapshot = this.opts.getStatus();
|
|
91
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
92
|
-
res.end(JSON.stringify(snapshot));
|
|
93
|
-
}
|
|
94
|
-
async respondCommand(res, command, cardId) {
|
|
95
|
-
try {
|
|
96
|
-
await this.opts.handleCommand(command, cardId);
|
|
97
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
98
|
-
res.end(JSON.stringify({ ok: true, command, cardId }));
|
|
99
|
-
}
|
|
100
|
-
catch (err) {
|
|
101
|
-
res.writeHead(500, { "content-type": "application/json" });
|
|
102
|
-
res.end(JSON.stringify({
|
|
103
|
-
error: "command_failed",
|
|
104
|
-
detail: err instanceof Error ? err.message : String(err),
|
|
105
|
-
}));
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
function parseCommand(path) {
|
|
110
|
-
const match = path.match(/^\/(pause|resume|stop)\/([^/]+)$/);
|
|
111
|
-
if (!match)
|
|
112
|
-
return null;
|
|
113
|
-
return { command: match[1], cardId: decodeURIComponent(match[2]) };
|
|
114
|
-
}
|
package/dist/index.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { type DaemonConfig } from "./config.js";
|
|
2
|
-
import { type StartupBanner } from "./startup-banner.js";
|
|
3
|
-
declare function validatePrerequisites(config: DaemonConfig, banner: StartupBanner): Promise<string>;
|
|
4
|
-
export { validatePrerequisites };
|
|
5
|
-
export declare function main(): Promise<void>;
|
package/dist/log.d.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Structured logging for the agent daemon.
|
|
3
|
-
*
|
|
4
|
-
* When stderr is a TTY, output is ANSI-coloured single-line text — the
|
|
5
|
-
* readable default for humans running the daemon interactively. When
|
|
6
|
-
* stderr is piped/redirected (files, logshippers, systemd), output
|
|
7
|
-
* falls back to one JSON object per line — trivially `jq`-able,
|
|
8
|
-
* indexable, and machine-readable.
|
|
9
|
-
*
|
|
10
|
-
* Overrides: `--pretty` / HARMONY_AGENT_PRETTY=1 force pretty;
|
|
11
|
-
* `--json` / HARMONY_AGENT_JSON=1 force JSON. Explicit flags beat
|
|
12
|
-
* TTY detection either way.
|
|
13
|
-
*
|
|
14
|
-
* Stdout is left clean so consumers can pipe structured data if they
|
|
15
|
-
* want to (future use).
|
|
16
|
-
*/
|
|
17
|
-
export interface LogContext {
|
|
18
|
-
event?: string;
|
|
19
|
-
runId?: string;
|
|
20
|
-
cardId?: string;
|
|
21
|
-
[key: string]: unknown;
|
|
22
|
-
}
|
|
23
|
-
export declare const log: {
|
|
24
|
-
info(tag: string, msg: string, ctx?: LogContext): void;
|
|
25
|
-
warn(tag: string, msg: string, ctx?: LogContext): void;
|
|
26
|
-
error(tag: string, msg: string, ctx?: LogContext): void;
|
|
27
|
-
debug(tag: string, msg: string, ctx?: LogContext): void;
|
|
28
|
-
/**
|
|
29
|
-
* Emit a named event. Semantically the same as `info` but signals to
|
|
30
|
-
* downstream tooling that this is a structured event worth indexing.
|
|
31
|
-
*/
|
|
32
|
-
event(tag: string, event: string, ctx?: LogContext): void;
|
|
33
|
-
};
|
|
34
|
-
export declare function isPretty(): boolean;
|
package/dist/log.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Structured logging for the agent daemon.
|
|
3
|
-
*
|
|
4
|
-
* When stderr is a TTY, output is ANSI-coloured single-line text — the
|
|
5
|
-
* readable default for humans running the daemon interactively. When
|
|
6
|
-
* stderr is piped/redirected (files, logshippers, systemd), output
|
|
7
|
-
* falls back to one JSON object per line — trivially `jq`-able,
|
|
8
|
-
* indexable, and machine-readable.
|
|
9
|
-
*
|
|
10
|
-
* Overrides: `--pretty` / HARMONY_AGENT_PRETTY=1 force pretty;
|
|
11
|
-
* `--json` / HARMONY_AGENT_JSON=1 force JSON. Explicit flags beat
|
|
12
|
-
* TTY detection either way.
|
|
13
|
-
*
|
|
14
|
-
* Stdout is left clean so consumers can pipe structured data if they
|
|
15
|
-
* want to (future use).
|
|
16
|
-
*/
|
|
17
|
-
const COLORS = {
|
|
18
|
-
reset: "\x1b[0m",
|
|
19
|
-
dim: "\x1b[2m",
|
|
20
|
-
red: "\x1b[31m",
|
|
21
|
-
green: "\x1b[32m",
|
|
22
|
-
yellow: "\x1b[33m",
|
|
23
|
-
blue: "\x1b[34m",
|
|
24
|
-
cyan: "\x1b[36m",
|
|
25
|
-
};
|
|
26
|
-
const LEVEL_COLOR = {
|
|
27
|
-
debug: COLORS.dim,
|
|
28
|
-
info: COLORS.green,
|
|
29
|
-
warn: COLORS.yellow,
|
|
30
|
-
error: COLORS.red,
|
|
31
|
-
};
|
|
32
|
-
function pretty() {
|
|
33
|
-
if (process.env.HARMONY_AGENT_JSON === "1" ||
|
|
34
|
-
process.argv.includes("--json")) {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
if (process.env.HARMONY_AGENT_PRETTY === "1")
|
|
38
|
-
return true;
|
|
39
|
-
if (process.argv.includes("--pretty"))
|
|
40
|
-
return true;
|
|
41
|
-
return Boolean(process.stderr.isTTY);
|
|
42
|
-
}
|
|
43
|
-
function shortTime(iso) {
|
|
44
|
-
return iso.slice(11, 23);
|
|
45
|
-
}
|
|
46
|
-
function emit(rec) {
|
|
47
|
-
if (rec.level === "debug" && !process.env.DEBUG)
|
|
48
|
-
return;
|
|
49
|
-
if (pretty()) {
|
|
50
|
-
const color = LEVEL_COLOR[rec.level];
|
|
51
|
-
const label = rec.level.toUpperCase().padEnd(5, " ");
|
|
52
|
-
const ctx = [];
|
|
53
|
-
if (rec.event)
|
|
54
|
-
ctx.push(`event=${rec.event}`);
|
|
55
|
-
if (rec.runId)
|
|
56
|
-
ctx.push(`run=${rec.runId}`);
|
|
57
|
-
if (rec.cardId)
|
|
58
|
-
ctx.push(`card=${rec.cardId}`);
|
|
59
|
-
const tail = ctx.length
|
|
60
|
-
? ` ${COLORS.dim}(${ctx.join(" ")})${COLORS.reset}`
|
|
61
|
-
: "";
|
|
62
|
-
process.stderr.write(`${COLORS.dim}${shortTime(rec.ts)}${COLORS.reset} ${color}${label}${COLORS.reset} ${COLORS.cyan}[${rec.tag}]${COLORS.reset} ${rec.msg}${tail}\n`);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
process.stderr.write(`${JSON.stringify(rec)}\n`);
|
|
66
|
-
}
|
|
67
|
-
function record(level, tag, msg, ctx) {
|
|
68
|
-
const rec = {
|
|
69
|
-
ts: new Date().toISOString(),
|
|
70
|
-
level,
|
|
71
|
-
tag,
|
|
72
|
-
msg,
|
|
73
|
-
...(ctx ?? {}),
|
|
74
|
-
};
|
|
75
|
-
emit(rec);
|
|
76
|
-
}
|
|
77
|
-
export const log = {
|
|
78
|
-
info(tag, msg, ctx) {
|
|
79
|
-
record("info", tag, msg, ctx);
|
|
80
|
-
},
|
|
81
|
-
warn(tag, msg, ctx) {
|
|
82
|
-
record("warn", tag, msg, ctx);
|
|
83
|
-
},
|
|
84
|
-
error(tag, msg, ctx) {
|
|
85
|
-
record("error", tag, msg, ctx);
|
|
86
|
-
},
|
|
87
|
-
debug(tag, msg, ctx) {
|
|
88
|
-
record("debug", tag, msg, ctx);
|
|
89
|
-
},
|
|
90
|
-
/**
|
|
91
|
-
* Emit a named event. Semantically the same as `info` but signals to
|
|
92
|
-
* downstream tooling that this is a structured event worth indexing.
|
|
93
|
-
*/
|
|
94
|
-
event(tag, event, ctx) {
|
|
95
|
-
record("info", tag, event, { ...ctx, event });
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
export function isPretty() {
|
|
99
|
-
return pretty();
|
|
100
|
-
}
|
package/dist/merge-monitor.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
|
|
2
|
-
import type { AgentConfig } from "./types.js";
|
|
3
|
-
/**
|
|
4
|
-
* Polls "Ready to Merge" cards and detects when their PRs get merged,
|
|
5
|
-
* then moves the card to Done and swaps labels.
|
|
6
|
-
*/
|
|
7
|
-
export declare class MergeMonitor {
|
|
8
|
-
private client;
|
|
9
|
-
private projectId;
|
|
10
|
-
private config;
|
|
11
|
-
private intervalMs;
|
|
12
|
-
private timer;
|
|
13
|
-
private running;
|
|
14
|
-
private readonly provider;
|
|
15
|
-
private readonly cwd;
|
|
16
|
-
constructor(client: HarmonyApiClient, projectId: string, config: AgentConfig, intervalMs?: number);
|
|
17
|
-
start(): void;
|
|
18
|
-
stop(): void;
|
|
19
|
-
/** Schedule the next tick after `delayMs`, avoiding overlapping ticks. */
|
|
20
|
-
private scheduleNext;
|
|
21
|
-
private tick;
|
|
22
|
-
private completeMergedCard;
|
|
23
|
-
}
|
package/dist/merge-monitor.js
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
import { addLabelByName, buildLabelMap, hasLabel, moveCardToColumn, resolveCardLabels, } from "./board-helpers.js";
|
|
4
|
-
import { checkPrMergeStatus, detectGitProvider, extractPrUrl, } from "./git-pr.js";
|
|
5
|
-
import { log } from "./log.js";
|
|
6
|
-
import { extractBranchFromDescription } from "./review-worktree.js";
|
|
7
|
-
const TAG = "merge-monitor";
|
|
8
|
-
const execFileAsync = promisify(execFile);
|
|
9
|
-
/**
|
|
10
|
-
* Polls "Ready to Merge" cards and detects when their PRs get merged,
|
|
11
|
-
* then moves the card to Done and swaps labels.
|
|
12
|
-
*/
|
|
13
|
-
export class MergeMonitor {
|
|
14
|
-
client;
|
|
15
|
-
projectId;
|
|
16
|
-
config;
|
|
17
|
-
intervalMs;
|
|
18
|
-
timer = null;
|
|
19
|
-
running = false;
|
|
20
|
-
provider;
|
|
21
|
-
cwd;
|
|
22
|
-
constructor(client, projectId, config, intervalMs = 60_000) {
|
|
23
|
-
this.client = client;
|
|
24
|
-
this.projectId = projectId;
|
|
25
|
-
this.config = config;
|
|
26
|
-
this.intervalMs = intervalMs;
|
|
27
|
-
this.provider = detectGitProvider();
|
|
28
|
-
this.cwd = process.cwd();
|
|
29
|
-
}
|
|
30
|
-
start() {
|
|
31
|
-
this.running = true;
|
|
32
|
-
void this.scheduleNext(0);
|
|
33
|
-
}
|
|
34
|
-
stop() {
|
|
35
|
-
this.running = false;
|
|
36
|
-
if (this.timer) {
|
|
37
|
-
clearTimeout(this.timer);
|
|
38
|
-
this.timer = null;
|
|
39
|
-
}
|
|
40
|
-
log.info(TAG, "Merge monitor stopped");
|
|
41
|
-
}
|
|
42
|
-
/** Schedule the next tick after `delayMs`, avoiding overlapping ticks. */
|
|
43
|
-
async scheduleNext(delayMs) {
|
|
44
|
-
await new Promise((resolve) => {
|
|
45
|
-
this.timer = setTimeout(() => resolve(), delayMs);
|
|
46
|
-
});
|
|
47
|
-
if (!this.running)
|
|
48
|
-
return; // stopped while waiting
|
|
49
|
-
await this.tick();
|
|
50
|
-
if (this.running) {
|
|
51
|
-
void this.scheduleNext(this.intervalMs);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
async tick() {
|
|
55
|
-
try {
|
|
56
|
-
const board = await this.client.getBoard(this.projectId, {
|
|
57
|
-
labelName: this.config.review.approvedLabel,
|
|
58
|
-
});
|
|
59
|
-
const cards = (board.cards ?? []);
|
|
60
|
-
const columns = (board.columns ?? []);
|
|
61
|
-
// Build label lookup (id → Label) to resolve card.labelIds
|
|
62
|
-
const labelMap = buildLabelMap((board.labels ?? []));
|
|
63
|
-
// Find review column IDs
|
|
64
|
-
const reviewColumnIds = new Set(columns
|
|
65
|
-
.filter((c) => this.config.review.pickupColumns.some((name) => name.toLowerCase() === c.name.toLowerCase()))
|
|
66
|
-
.map((c) => c.id));
|
|
67
|
-
// Cards are already pre-filtered by the approved label server-side via
|
|
68
|
-
// the `labelName` param. The column + label checks below are kept as
|
|
69
|
-
// defense-in-depth against API changes or stale cache.
|
|
70
|
-
const approvedLabel = this.config.review.approvedLabel;
|
|
71
|
-
const candidatesWithLabels = [];
|
|
72
|
-
for (const c of cards) {
|
|
73
|
-
if (c.archived_at || !reviewColumnIds.has(c.column_id))
|
|
74
|
-
continue;
|
|
75
|
-
const labels = resolveCardLabels(c, labelMap);
|
|
76
|
-
if (hasLabel(labels, approvedLabel)) {
|
|
77
|
-
candidatesWithLabels.push({ card: c, labels });
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (candidatesWithLabels.length === 0) {
|
|
81
|
-
log.debug(TAG, "No Ready to Merge cards found");
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
// Process max 5 per tick to respect rate limits
|
|
85
|
-
const batch = candidatesWithLabels.slice(0, 5);
|
|
86
|
-
log.debug(TAG, `Checking ${batch.length} Ready to Merge card(s)`);
|
|
87
|
-
// Check PR states concurrently (async, non-blocking)
|
|
88
|
-
const results = await Promise.allSettled(batch.map(async ({ card, labels }) => {
|
|
89
|
-
const prUrl = extractPrUrl(card.description ?? null);
|
|
90
|
-
if (!prUrl) {
|
|
91
|
-
log.debug(TAG, `#${card.short_id} has no PR URL — skipping`);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const state = await checkPrMergeStatus(prUrl, this.cwd, this.provider);
|
|
95
|
-
if (state === "merged") {
|
|
96
|
-
log.info(TAG, `#${card.short_id} PR merged — completing`);
|
|
97
|
-
await this.completeMergedCard(card, labels);
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
log.debug(TAG, `#${card.short_id} PR state: ${state}`);
|
|
101
|
-
}
|
|
102
|
-
}));
|
|
103
|
-
for (const r of results) {
|
|
104
|
-
if (r.status === "rejected") {
|
|
105
|
-
log.warn(TAG, `Card processing failed: ${r.reason}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
log.error(TAG, `Tick failed: ${err instanceof Error ? err.message : err}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
async completeMergedCard(card, resolvedLabels) {
|
|
114
|
-
// 1. Move to Done — bail if this fails since subsequent steps assume card is in Done
|
|
115
|
-
try {
|
|
116
|
-
await moveCardToColumn(this.client, card, this.config.review.moveToColumn);
|
|
117
|
-
}
|
|
118
|
-
catch (err) {
|
|
119
|
-
log.error(TAG, `Failed to move #${card.short_id} to Done: ${err instanceof Error ? err.message : err}`);
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
// 2. Add "Merged" label
|
|
123
|
-
await addLabelByName(this.client, card, this.config.review.mergedLabel, this.config.review.mergedLabelColor);
|
|
124
|
-
// 3. Remove "Ready to Merge" label
|
|
125
|
-
const approvedLabelName = this.config.review.approvedLabel.toLowerCase();
|
|
126
|
-
const approvedLabelObj = resolvedLabels.find((l) => l.name.toLowerCase() === approvedLabelName);
|
|
127
|
-
if (approvedLabelObj) {
|
|
128
|
-
try {
|
|
129
|
-
await this.client.removeLabelFromCard(card.id, approvedLabelObj.id);
|
|
130
|
-
log.info(TAG, `Removed "${this.config.review.approvedLabel}" from #${card.short_id}`);
|
|
131
|
-
}
|
|
132
|
-
catch (err) {
|
|
133
|
-
log.warn(TAG, `Failed to remove label: ${err instanceof Error ? err.message : err}`);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
// 4. Append merge timestamp to description (idempotent).
|
|
137
|
-
// Do NOT force done=true here — the column's `mark_cards_done` flag is the
|
|
138
|
-
// user's source of truth, and the backend `moveCard` in step 1 already
|
|
139
|
-
// applied it. Overriding here would ignore the user's column setting (#129).
|
|
140
|
-
const existing = card.description || "";
|
|
141
|
-
const alreadyStamped = existing.includes("Merged at");
|
|
142
|
-
if (!alreadyStamped) {
|
|
143
|
-
try {
|
|
144
|
-
const timestamp = new Date().toISOString();
|
|
145
|
-
const separator = existing ? "\n" : "";
|
|
146
|
-
await this.client.updateCard(card.id, {
|
|
147
|
-
description: `${existing}${separator}Merged at ${timestamp}`,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
catch (err) {
|
|
151
|
-
log.warn(TAG, `Failed to update card: ${err instanceof Error ? err.message : err}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
// 5. Best-effort: clean up local branch (uses shared extraction with validation)
|
|
155
|
-
const branchName = extractBranchFromDescription(card.description);
|
|
156
|
-
if (branchName) {
|
|
157
|
-
try {
|
|
158
|
-
await execFileAsync("git", ["branch", "-D", "--", branchName], {
|
|
159
|
-
cwd: this.cwd,
|
|
160
|
-
});
|
|
161
|
-
log.info(TAG, `Deleted local branch ${branchName}`);
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
// best-effort
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
log.info(TAG, `#${card.short_id} completed (merged)`);
|
|
168
|
-
}
|
|
169
|
-
}
|
package/dist/pm.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export type PackageManager = "bun" | "npm" | "pnpm" | "yarn";
|
|
2
|
-
/**
|
|
3
|
-
* Detect the package manager based on lockfiles in the repo root.
|
|
4
|
-
*/
|
|
5
|
-
export declare function detectPackageManager(): PackageManager;
|
|
6
|
-
/**
|
|
7
|
-
* Return the install command string for the detected package manager.
|
|
8
|
-
*/
|
|
9
|
-
export declare function installCommand(): string;
|
|
10
|
-
/**
|
|
11
|
-
* Return [cmd, args[]] suitable for spawn() or execFileSync() to run a package script.
|
|
12
|
-
* Adds `--` separator for npm and pnpm when extra args are present.
|
|
13
|
-
*/
|
|
14
|
-
export declare function spawnRunArgs(script: string, ...extra: string[]): [string, string[]];
|
package/dist/pm.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { log } from "./log.js";
|
|
4
|
-
const TAG = "pm";
|
|
5
|
-
let cached = null;
|
|
6
|
-
/**
|
|
7
|
-
* Detect the package manager based on lockfiles in the repo root.
|
|
8
|
-
*/
|
|
9
|
-
export function detectPackageManager() {
|
|
10
|
-
if (cached)
|
|
11
|
-
return cached;
|
|
12
|
-
let repoRoot;
|
|
13
|
-
try {
|
|
14
|
-
repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
15
|
-
encoding: "utf-8",
|
|
16
|
-
}).trim();
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
repoRoot = process.cwd();
|
|
20
|
-
}
|
|
21
|
-
if (existsSync(`${repoRoot}/bun.lock`) ||
|
|
22
|
-
existsSync(`${repoRoot}/bun.lockb`)) {
|
|
23
|
-
cached = "bun";
|
|
24
|
-
}
|
|
25
|
-
else if (existsSync(`${repoRoot}/pnpm-lock.yaml`)) {
|
|
26
|
-
cached = "pnpm";
|
|
27
|
-
}
|
|
28
|
-
else if (existsSync(`${repoRoot}/yarn.lock`)) {
|
|
29
|
-
cached = "yarn";
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
cached = "npm";
|
|
33
|
-
}
|
|
34
|
-
log.info(TAG, `Detected package manager: ${cached}`);
|
|
35
|
-
return cached;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Return the install command string for the detected package manager.
|
|
39
|
-
*/
|
|
40
|
-
export function installCommand() {
|
|
41
|
-
const pm = detectPackageManager();
|
|
42
|
-
switch (pm) {
|
|
43
|
-
case "bun":
|
|
44
|
-
return "bun install --frozen-lockfile";
|
|
45
|
-
case "pnpm":
|
|
46
|
-
return "pnpm install --frozen-lockfile";
|
|
47
|
-
case "yarn":
|
|
48
|
-
return "yarn install --frozen-lockfile";
|
|
49
|
-
case "npm":
|
|
50
|
-
return "npm ci";
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Return [cmd, args[]] suitable for spawn() or execFileSync() to run a package script.
|
|
55
|
-
* Adds `--` separator for npm and pnpm when extra args are present.
|
|
56
|
-
*/
|
|
57
|
-
export function spawnRunArgs(script, ...extra) {
|
|
58
|
-
const pm = detectPackageManager();
|
|
59
|
-
if (extra.length > 0 && (pm === "npm" || pm === "pnpm")) {
|
|
60
|
-
return [pm, ["run", script, "--", ...extra]];
|
|
61
|
-
}
|
|
62
|
-
return [pm, ["run", script, ...extra]];
|
|
63
|
-
}
|
package/dist/pool.d.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import type { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
|
|
2
|
-
import type { Card, Column, Label, Subtask } from "@harmony/shared";
|
|
3
|
-
import { PriorityQueue } from "./queue.js";
|
|
4
|
-
import type { StateStore } from "./state-store.js";
|
|
5
|
-
import { type AgentConfig, type WorkMode } from "./types.js";
|
|
6
|
-
export declare class Pool {
|
|
7
|
-
private client;
|
|
8
|
-
private implWorkers;
|
|
9
|
-
private reviewWorkers;
|
|
10
|
-
private implQueue;
|
|
11
|
-
private reviewQueue;
|
|
12
|
-
private budget;
|
|
13
|
-
constructor(config: AgentConfig, client: HarmonyApiClient, userEmail: string, workspaceId: string, projectId: string, stateStore: StateStore);
|
|
14
|
-
/**
|
|
15
|
-
* Enqueue a card for processing with the given mode.
|
|
16
|
-
*
|
|
17
|
-
* Returns async so callers can await the DLQ side-effects on skip.
|
|
18
|
-
* Budget/DLQ checks happen here so the reconciler, realtime watcher,
|
|
19
|
-
* and manual API calls all go through the same gate.
|
|
20
|
-
*/
|
|
21
|
-
enqueue(card: Card, column: Column, labels: Label[], subtasks: Subtask[], mode?: WorkMode): Promise<void>;
|
|
22
|
-
/**
|
|
23
|
-
* Best-effort waiting-state emit. Failures are swallowed because we don't
|
|
24
|
-
* want a board-API hiccup to drop the queue/budget event in pool.ts.
|
|
25
|
-
*/
|
|
26
|
-
private emitWaiting;
|
|
27
|
-
/**
|
|
28
|
-
* Remove a card from any queue or cancel an active worker.
|
|
29
|
-
*/
|
|
30
|
-
removeCard(cardId: string): Promise<void>;
|
|
31
|
-
/**
|
|
32
|
-
* Check if a card is currently being worked on by any worker.
|
|
33
|
-
*/
|
|
34
|
-
isCardActive(cardId: string): boolean;
|
|
35
|
-
/**
|
|
36
|
-
* Check if a card is known to the pool (queued or active).
|
|
37
|
-
*/
|
|
38
|
-
isCardKnown(cardId: string): boolean;
|
|
39
|
-
/**
|
|
40
|
-
* Get all card IDs that are either queued or active.
|
|
41
|
-
*/
|
|
42
|
-
knownCardIds(): Set<string>;
|
|
43
|
-
/**
|
|
44
|
-
* Handle an agent command (pause/resume/stop) for a specific card.
|
|
45
|
-
*/
|
|
46
|
-
handleAgentCommand(cardId: string, command: "pause" | "resume" | "stop"): Promise<void>;
|
|
47
|
-
/**
|
|
48
|
-
* Point-in-time snapshot for the HTTP /status endpoint. Safe to call
|
|
49
|
-
* from anywhere — reads in-memory state only.
|
|
50
|
-
*/
|
|
51
|
-
snapshotWorkers(): Array<{
|
|
52
|
-
id: number;
|
|
53
|
-
pipeline: "implement" | "review";
|
|
54
|
-
state: string;
|
|
55
|
-
cardId: string | null;
|
|
56
|
-
cardShortId: number | null;
|
|
57
|
-
startedAt: number | null;
|
|
58
|
-
branchName: string | null;
|
|
59
|
-
}>;
|
|
60
|
-
snapshotQueues(): {
|
|
61
|
-
impl: ReturnType<PriorityQueue["snapshot"]>;
|
|
62
|
-
review: ReturnType<PriorityQueue["snapshot"]>;
|
|
63
|
-
};
|
|
64
|
-
/**
|
|
65
|
-
* Gracefully shutdown all workers.
|
|
66
|
-
*/
|
|
67
|
-
shutdown(): Promise<void>;
|
|
68
|
-
private cardDataCache;
|
|
69
|
-
private tryDispatchFor;
|
|
70
|
-
}
|