@gethmy/agent 1.0.0 → 1.0.1
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 +5 -5
- package/dist/board-helpers.d.ts +23 -0
- package/dist/board-helpers.js +131 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +2 -11761
- package/dist/completion.d.ts +7 -0
- package/dist/completion.js +132 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.js +91 -0
- package/dist/git-pr.d.ts +25 -0
- package/dist/git-pr.js +305 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +165 -11730
- package/dist/log.d.ts +10 -0
- package/dist/log.js +35 -0
- package/dist/merge-monitor.d.ts +23 -0
- package/dist/merge-monitor.js +155 -0
- package/dist/pm.d.ts +14 -0
- package/dist/pm.js +63 -0
- package/dist/pool.d.ts +36 -0
- package/dist/pool.js +134 -0
- package/dist/progress-tracker.d.ts +39 -0
- package/dist/progress-tracker.js +189 -0
- package/dist/prompt.d.ts +5 -0
- package/dist/prompt.js +40 -0
- package/dist/queue.d.ts +37 -0
- package/dist/queue.js +96 -0
- package/dist/reconcile.d.ts +21 -0
- package/dist/reconcile.js +107 -0
- package/dist/review-completion.d.ts +31 -0
- package/dist/review-completion.js +247 -0
- package/dist/review-knowledge.d.ts +14 -0
- package/dist/review-knowledge.js +89 -0
- package/dist/review-prompt.d.ts +12 -0
- package/dist/review-prompt.js +100 -0
- package/dist/review-worker.d.ts +35 -0
- package/dist/review-worker.js +302 -0
- package/dist/review-worktree.d.ts +12 -0
- package/dist/review-worktree.js +83 -0
- package/dist/stream-parser.d.ts +22 -0
- package/dist/stream-parser.js +81 -0
- package/dist/types.d.ts +74 -0
- package/dist/types.js +53 -0
- package/dist/verification.d.ts +16 -0
- package/dist/verification.js +251 -0
- package/dist/watcher.d.ts +21 -0
- package/dist/watcher.js +62 -0
- package/dist/worker.d.ts +34 -0
- package/dist/worker.js +268 -0
- package/dist/worktree.d.ts +13 -0
- package/dist/worktree.js +115 -0
- package/package.json +6 -5
package/dist/log.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logging for the agent daemon.
|
|
3
|
+
* Always logs to stderr so stdout stays clean for potential piping.
|
|
4
|
+
*/
|
|
5
|
+
export declare const log: {
|
|
6
|
+
info(tag: string, msg: string): void;
|
|
7
|
+
warn(tag: string, msg: string): void;
|
|
8
|
+
error(tag: string, msg: string): void;
|
|
9
|
+
debug(tag: string, msg: string): void;
|
|
10
|
+
};
|
package/dist/log.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logging for the agent daemon.
|
|
3
|
+
* Always logs to stderr so stdout stays clean for potential piping.
|
|
4
|
+
*/
|
|
5
|
+
const COLORS = {
|
|
6
|
+
reset: "\x1b[0m",
|
|
7
|
+
dim: "\x1b[2m",
|
|
8
|
+
red: "\x1b[31m",
|
|
9
|
+
green: "\x1b[32m",
|
|
10
|
+
yellow: "\x1b[33m",
|
|
11
|
+
blue: "\x1b[34m",
|
|
12
|
+
cyan: "\x1b[36m",
|
|
13
|
+
};
|
|
14
|
+
function timestamp() {
|
|
15
|
+
return new Date().toISOString().slice(11, 23);
|
|
16
|
+
}
|
|
17
|
+
function fmt(level, color, tag, msg) {
|
|
18
|
+
return `${COLORS.dim}${timestamp()}${COLORS.reset} ${color}${level}${COLORS.reset} ${COLORS.cyan}[${tag}]${COLORS.reset} ${msg}`;
|
|
19
|
+
}
|
|
20
|
+
export const log = {
|
|
21
|
+
info(tag, msg) {
|
|
22
|
+
console.error(fmt("INFO ", COLORS.green, tag, msg));
|
|
23
|
+
},
|
|
24
|
+
warn(tag, msg) {
|
|
25
|
+
console.error(fmt("WARN ", COLORS.yellow, tag, msg));
|
|
26
|
+
},
|
|
27
|
+
error(tag, msg) {
|
|
28
|
+
console.error(fmt("ERROR", COLORS.red, tag, msg));
|
|
29
|
+
},
|
|
30
|
+
debug(tag, msg) {
|
|
31
|
+
if (process.env.DEBUG) {
|
|
32
|
+
console.error(fmt("DEBUG", COLORS.dim, tag, msg));
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { addLabelByName, hasApprovedLabel, moveCardToColumn, } 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
|
+
log.info(TAG, `Merge monitor every ${this.intervalMs / 1000}s`);
|
|
33
|
+
void this.scheduleNext(0);
|
|
34
|
+
}
|
|
35
|
+
stop() {
|
|
36
|
+
this.running = false;
|
|
37
|
+
if (this.timer) {
|
|
38
|
+
clearTimeout(this.timer);
|
|
39
|
+
this.timer = null;
|
|
40
|
+
}
|
|
41
|
+
log.info(TAG, "Merge monitor stopped");
|
|
42
|
+
}
|
|
43
|
+
/** Schedule the next tick after `delayMs`, avoiding overlapping ticks. */
|
|
44
|
+
async scheduleNext(delayMs) {
|
|
45
|
+
await new Promise((resolve) => {
|
|
46
|
+
this.timer = setTimeout(() => resolve(), delayMs);
|
|
47
|
+
});
|
|
48
|
+
if (!this.running)
|
|
49
|
+
return; // stopped while waiting
|
|
50
|
+
await this.tick();
|
|
51
|
+
if (this.running) {
|
|
52
|
+
void this.scheduleNext(this.intervalMs);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async tick() {
|
|
56
|
+
try {
|
|
57
|
+
const board = await this.client.getBoard(this.projectId);
|
|
58
|
+
const cards = (board.cards ?? []);
|
|
59
|
+
const columns = (board.columns ?? []);
|
|
60
|
+
// Find review column IDs
|
|
61
|
+
const reviewColumnIds = new Set(columns
|
|
62
|
+
.filter((c) => this.config.review.pickupColumns.some((name) => name.toLowerCase() === c.name.toLowerCase()))
|
|
63
|
+
.map((c) => c.id));
|
|
64
|
+
// Filter cards in review columns that have the "Ready to Merge" label
|
|
65
|
+
const approvedLabel = this.config.review.approvedLabel;
|
|
66
|
+
const candidates = cards.filter((c) => !c.archived_at &&
|
|
67
|
+
reviewColumnIds.has(c.column_id) &&
|
|
68
|
+
hasApprovedLabel((c.labels ?? []), approvedLabel));
|
|
69
|
+
if (candidates.length === 0) {
|
|
70
|
+
log.debug(TAG, "No Ready to Merge cards found");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Process max 5 per tick to respect rate limits
|
|
74
|
+
const batch = candidates.slice(0, 5);
|
|
75
|
+
log.debug(TAG, `Checking ${batch.length} Ready to Merge card(s)`);
|
|
76
|
+
// Check PR states concurrently (async, non-blocking)
|
|
77
|
+
const results = await Promise.allSettled(batch.map(async (card) => {
|
|
78
|
+
const prUrl = extractPrUrl(card.description ?? null);
|
|
79
|
+
if (!prUrl) {
|
|
80
|
+
log.debug(TAG, `#${card.short_id} has no PR URL — skipping`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const state = await checkPrMergeStatus(prUrl, this.cwd, this.provider);
|
|
84
|
+
if (state === "merged") {
|
|
85
|
+
log.info(TAG, `#${card.short_id} PR merged — completing`);
|
|
86
|
+
await this.completeMergedCard(card);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
log.debug(TAG, `#${card.short_id} PR state: ${state}`);
|
|
90
|
+
}
|
|
91
|
+
}));
|
|
92
|
+
for (const r of results) {
|
|
93
|
+
if (r.status === "rejected") {
|
|
94
|
+
log.warn(TAG, `Card processing failed: ${r.reason}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
log.error(TAG, `Tick failed: ${err instanceof Error ? err.message : err}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async completeMergedCard(card) {
|
|
103
|
+
// 1. Move to Done — bail if this fails since subsequent steps assume card is in Done
|
|
104
|
+
try {
|
|
105
|
+
await moveCardToColumn(this.client, card, this.config.review.moveToColumn);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
log.error(TAG, `Failed to move #${card.short_id} to Done: ${err instanceof Error ? err.message : err}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// 2. Add "Merged" label
|
|
112
|
+
await addLabelByName(this.client, card, this.config.review.mergedLabel, this.config.review.mergedLabelColor);
|
|
113
|
+
// 3. Remove "Ready to Merge" label
|
|
114
|
+
const approvedLabelName = this.config.review.approvedLabel.toLowerCase();
|
|
115
|
+
const cardLabels = (card.labels ?? []);
|
|
116
|
+
const approvedLabelObj = cardLabels.find((l) => l.name.toLowerCase() === approvedLabelName);
|
|
117
|
+
if (approvedLabelObj) {
|
|
118
|
+
try {
|
|
119
|
+
await this.client.removeLabelFromCard(card.id, approvedLabelObj.id);
|
|
120
|
+
log.info(TAG, `Removed "${this.config.review.approvedLabel}" from #${card.short_id}`);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
log.warn(TAG, `Failed to remove label: ${err instanceof Error ? err.message : err}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// 4. Append merge timestamp to description (idempotent)
|
|
127
|
+
const existing = card.description || "";
|
|
128
|
+
if (!existing.includes("Merged at")) {
|
|
129
|
+
try {
|
|
130
|
+
const timestamp = new Date().toISOString();
|
|
131
|
+
const separator = existing ? "\n" : "";
|
|
132
|
+
await this.client.updateCard(card.id, {
|
|
133
|
+
description: `${existing}${separator}Merged at ${timestamp}`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
log.warn(TAG, `Failed to update description: ${err instanceof Error ? err.message : err}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// 5. Best-effort: clean up local branch (uses shared extraction with validation)
|
|
141
|
+
const branchName = extractBranchFromDescription(card.description);
|
|
142
|
+
if (branchName) {
|
|
143
|
+
try {
|
|
144
|
+
await execFileAsync("git", ["branch", "-D", "--", branchName], {
|
|
145
|
+
cwd: this.cwd,
|
|
146
|
+
});
|
|
147
|
+
log.info(TAG, `Deleted local branch ${branchName}`);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// best-effort
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
log.info(TAG, `#${card.short_id} completed (merged)`);
|
|
154
|
+
}
|
|
155
|
+
}
|
package/dist/pm.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
|
|
2
|
+
import type { Card, Column, Label, Subtask } from "@harmony/shared";
|
|
3
|
+
import type { AgentConfig, WorkMode } from "./types.js";
|
|
4
|
+
export declare class Pool {
|
|
5
|
+
private implWorkers;
|
|
6
|
+
private reviewWorkers;
|
|
7
|
+
private implQueue;
|
|
8
|
+
private reviewQueue;
|
|
9
|
+
constructor(config: AgentConfig, client: HarmonyApiClient, userEmail: string);
|
|
10
|
+
/**
|
|
11
|
+
* Enqueue a card for processing with the given mode.
|
|
12
|
+
*/
|
|
13
|
+
enqueue(card: Card, column: Column, labels: Label[], subtasks: Subtask[], mode?: WorkMode): void;
|
|
14
|
+
/**
|
|
15
|
+
* Remove a card from any queue or cancel an active worker.
|
|
16
|
+
*/
|
|
17
|
+
removeCard(cardId: string): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Check if a card is currently being worked on by any worker.
|
|
20
|
+
*/
|
|
21
|
+
isCardActive(cardId: string): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Check if a card is known to the pool (queued or active).
|
|
24
|
+
*/
|
|
25
|
+
isCardKnown(cardId: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Get all card IDs that are either queued or active.
|
|
28
|
+
*/
|
|
29
|
+
knownCardIds(): Set<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Gracefully shutdown all workers.
|
|
32
|
+
*/
|
|
33
|
+
shutdown(): Promise<void>;
|
|
34
|
+
private cardDataCache;
|
|
35
|
+
private tryDispatchFor;
|
|
36
|
+
}
|
package/dist/pool.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { log } from "./log.js";
|
|
2
|
+
import { PriorityQueue } from "./queue.js";
|
|
3
|
+
import { ReviewWorker } from "./review-worker.js";
|
|
4
|
+
import { Worker } from "./worker.js";
|
|
5
|
+
const TAG = "pool";
|
|
6
|
+
export class Pool {
|
|
7
|
+
implWorkers = [];
|
|
8
|
+
reviewWorkers = [];
|
|
9
|
+
implQueue;
|
|
10
|
+
reviewQueue;
|
|
11
|
+
constructor(config, client, userEmail) {
|
|
12
|
+
this.implQueue = new PriorityQueue(config);
|
|
13
|
+
this.reviewQueue = new PriorityQueue(config);
|
|
14
|
+
// Create implementation workers
|
|
15
|
+
for (let i = 0; i < config.poolSize; i++) {
|
|
16
|
+
this.implWorkers.push(new Worker(i, config, client, userEmail, () => {
|
|
17
|
+
this.tryDispatchFor(this.implWorkers, this.implQueue, "impl");
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
// Create review worker(s) — 1 review worker per pool
|
|
21
|
+
if (config.review.enabled) {
|
|
22
|
+
const reviewWorkerId = config.poolSize; // offset to avoid ID collision
|
|
23
|
+
this.reviewWorkers.push(new ReviewWorker(reviewWorkerId, config, client, userEmail, () => {
|
|
24
|
+
this.tryDispatchFor(this.reviewWorkers, this.reviewQueue, "review");
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
const reviewCount = this.reviewWorkers.length;
|
|
28
|
+
log.info(TAG, `Pool initialized: ${config.poolSize} impl worker(s), ${reviewCount} review worker(s)`);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Enqueue a card for processing with the given mode.
|
|
32
|
+
*/
|
|
33
|
+
enqueue(card, column, labels, subtasks, mode = "implement") {
|
|
34
|
+
// Don't enqueue if already in any queue or actively being worked on
|
|
35
|
+
if (this.implQueue.has(card.id) ||
|
|
36
|
+
this.reviewQueue.has(card.id) ||
|
|
37
|
+
this.isCardActive(card.id)) {
|
|
38
|
+
log.debug(TAG, `Card ${card.id} already queued or active, skipping`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const queue = mode === "review" ? this.reviewQueue : this.implQueue;
|
|
42
|
+
queue.enqueue(card, column, labels, mode);
|
|
43
|
+
// Store card data for when it gets dispatched
|
|
44
|
+
this.cardDataCache.set(card.id, { card, column, labels, subtasks, mode });
|
|
45
|
+
const workers = mode === "review" ? this.reviewWorkers : this.implWorkers;
|
|
46
|
+
this.tryDispatchFor(workers, queue, mode);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Remove a card from any queue or cancel an active worker.
|
|
50
|
+
*/
|
|
51
|
+
async removeCard(cardId) {
|
|
52
|
+
// Try both queues
|
|
53
|
+
for (const queue of [this.implQueue, this.reviewQueue]) {
|
|
54
|
+
const removed = queue.remove(cardId);
|
|
55
|
+
if (removed) {
|
|
56
|
+
this.cardDataCache.delete(cardId);
|
|
57
|
+
log.info(TAG, `Removed #${removed.shortId} from ${removed.mode} queue`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Cancel active worker (check impl first, then review)
|
|
62
|
+
const worker = this.implWorkers.find((w) => w.cardId === cardId) ??
|
|
63
|
+
this.reviewWorkers.find((w) => w.cardId === cardId);
|
|
64
|
+
if (worker) {
|
|
65
|
+
log.info(TAG, `Cancelling worker ${worker.id} for card ${cardId}`);
|
|
66
|
+
await worker.cancel();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Check if a card is currently being worked on by any worker.
|
|
71
|
+
*/
|
|
72
|
+
isCardActive(cardId) {
|
|
73
|
+
return (this.implWorkers.some((w) => w.cardId === cardId && w.isActive) ||
|
|
74
|
+
this.reviewWorkers.some((w) => w.cardId === cardId && w.isActive));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Check if a card is known to the pool (queued or active).
|
|
78
|
+
*/
|
|
79
|
+
isCardKnown(cardId) {
|
|
80
|
+
return (this.implQueue.has(cardId) ||
|
|
81
|
+
this.reviewQueue.has(cardId) ||
|
|
82
|
+
this.isCardActive(cardId));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get all card IDs that are either queued or active.
|
|
86
|
+
*/
|
|
87
|
+
knownCardIds() {
|
|
88
|
+
const ids = new Set([
|
|
89
|
+
...this.implQueue.cardIds(),
|
|
90
|
+
...this.reviewQueue.cardIds(),
|
|
91
|
+
]);
|
|
92
|
+
for (const w of this.implWorkers) {
|
|
93
|
+
if (w.cardId)
|
|
94
|
+
ids.add(w.cardId);
|
|
95
|
+
}
|
|
96
|
+
for (const w of this.reviewWorkers) {
|
|
97
|
+
if (w.cardId)
|
|
98
|
+
ids.add(w.cardId);
|
|
99
|
+
}
|
|
100
|
+
return ids;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Gracefully shutdown all workers.
|
|
104
|
+
*/
|
|
105
|
+
async shutdown() {
|
|
106
|
+
log.info(TAG, "Shutting down pool...");
|
|
107
|
+
const active = [
|
|
108
|
+
...this.implWorkers.filter((w) => w.isActive),
|
|
109
|
+
...this.reviewWorkers.filter((w) => w.isActive),
|
|
110
|
+
];
|
|
111
|
+
await Promise.all(active.map((w) => w.cancel()));
|
|
112
|
+
log.info(TAG, "Pool shutdown complete");
|
|
113
|
+
}
|
|
114
|
+
// --- Private ---
|
|
115
|
+
cardDataCache = new Map();
|
|
116
|
+
tryDispatchFor(workers, queue, label) {
|
|
117
|
+
const idle = workers.find((w) => w.isIdle);
|
|
118
|
+
if (!idle) {
|
|
119
|
+
log.debug(TAG, `No idle ${label} workers (queue: ${queue.length})`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const next = queue.dequeue();
|
|
123
|
+
if (!next)
|
|
124
|
+
return;
|
|
125
|
+
const data = this.cardDataCache.get(next.cardId);
|
|
126
|
+
if (!data) {
|
|
127
|
+
log.warn(TAG, `No cached data for card ${next.cardId}, skipping`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
this.cardDataCache.delete(next.cardId);
|
|
131
|
+
log.info(TAG, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
|
|
132
|
+
idle.run(data.card, data.column, data.labels, data.subtasks);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { HarmonyApiClient } from "@gethmy/mcp/src/api-client.js";
|
|
2
|
+
import type { StreamParser } from "./stream-parser.js";
|
|
3
|
+
export declare class ProgressTracker {
|
|
4
|
+
private client;
|
|
5
|
+
private cardId;
|
|
6
|
+
private workerId;
|
|
7
|
+
private phase;
|
|
8
|
+
private progress;
|
|
9
|
+
private toolCallCount;
|
|
10
|
+
private hasEdited;
|
|
11
|
+
private lastUpdateAt;
|
|
12
|
+
private pendingUpdate;
|
|
13
|
+
private heartbeatTimer;
|
|
14
|
+
private stopped;
|
|
15
|
+
private subtaskTotal;
|
|
16
|
+
private subtaskCompleted;
|
|
17
|
+
private subtaskMode;
|
|
18
|
+
constructor(client: HarmonyApiClient, cardId: string, workerId: number, subtasks: {
|
|
19
|
+
completed: boolean;
|
|
20
|
+
}[]);
|
|
21
|
+
/**
|
|
22
|
+
* Wire up the parser events and start the heartbeat.
|
|
23
|
+
*/
|
|
24
|
+
attach(parser: StreamParser): void;
|
|
25
|
+
/**
|
|
26
|
+
* Stop all timers and flush any pending update.
|
|
27
|
+
*/
|
|
28
|
+
stop(): void;
|
|
29
|
+
private onToolStart;
|
|
30
|
+
private onToolEnd;
|
|
31
|
+
private transitionTo;
|
|
32
|
+
private incrementProgress;
|
|
33
|
+
private currentTaskLabel;
|
|
34
|
+
private scheduleUpdate;
|
|
35
|
+
private sendUpdate;
|
|
36
|
+
private startHeartbeat;
|
|
37
|
+
private phaseOrder;
|
|
38
|
+
private extractBashCommand;
|
|
39
|
+
}
|