@simonfestl/husky-cli 1.33.1 → 1.35.0

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.
@@ -215,5 +215,100 @@ program
215
215
  process.exit(1);
216
216
  }
217
217
  });
218
+ /**
219
+ * Wait for plan approval (worker command)
220
+ *
221
+ * Exit codes:
222
+ * 0 = approved
223
+ * 1 = rejected
224
+ * 2 = revision_requested
225
+ * 3 = timeout
226
+ */
227
+ program
228
+ .command("await")
229
+ .description("Wait for supervisor to approve/reject a plan (worker)")
230
+ .argument("<id>", "Plan ID to wait for")
231
+ .option("--timeout <seconds>", "Timeout in seconds (default: 300)", "300")
232
+ .option("--interval <seconds>", "Poll interval in seconds (default: 5)", "5")
233
+ .option("--json", "Output result as JSON")
234
+ .action(async (id, options) => {
235
+ const timeout = parseInt(options.timeout, 10) * 1000;
236
+ const interval = parseInt(options.interval, 10) * 1000;
237
+ const startTime = Date.now();
238
+ const api = getApiClient();
239
+ if (!options.json) {
240
+ console.log(chalk.yellow(`⏳ Waiting for plan ${id} approval...`));
241
+ console.log(chalk.dim(` Timeout: ${options.timeout}s, Poll interval: ${options.interval}s`));
242
+ }
243
+ while (true) {
244
+ try {
245
+ const result = await api.get(`/api/plans/${id}`);
246
+ const { plan } = result;
247
+ if (plan.status === "approved") {
248
+ if (options.json) {
249
+ console.log(JSON.stringify({ status: "approved", notes: plan.supervisorNotes }));
250
+ }
251
+ else {
252
+ console.log(chalk.green("\n✓ Plan approved!"));
253
+ if (plan.supervisorNotes) {
254
+ console.log(chalk.dim(` Supervisor notes: ${plan.supervisorNotes}`));
255
+ }
256
+ console.log(chalk.dim(" You may proceed with implementation."));
257
+ }
258
+ process.exit(0);
259
+ }
260
+ if (plan.status === "rejected") {
261
+ if (options.json) {
262
+ console.log(JSON.stringify({ status: "rejected", notes: plan.supervisorNotes }));
263
+ }
264
+ else {
265
+ console.log(chalk.red("\n✗ Plan rejected"));
266
+ if (plan.supervisorNotes) {
267
+ console.log(chalk.dim(` Reason: ${plan.supervisorNotes}`));
268
+ }
269
+ }
270
+ process.exit(1);
271
+ }
272
+ if (plan.status === "revision_requested") {
273
+ if (options.json) {
274
+ console.log(JSON.stringify({ status: "revision_requested", notes: plan.supervisorNotes }));
275
+ }
276
+ else {
277
+ console.log(chalk.blue("\n🔄 Revision requested"));
278
+ if (plan.supervisorNotes) {
279
+ console.log(chalk.dim(` Feedback: ${plan.supervisorNotes}`));
280
+ }
281
+ console.log(chalk.dim(" Please update and resubmit the plan."));
282
+ }
283
+ process.exit(2);
284
+ }
285
+ // Still pending - check timeout
286
+ if (Date.now() - startTime >= timeout) {
287
+ if (options.json) {
288
+ console.log(JSON.stringify({ status: "timeout", elapsed: Math.round((Date.now() - startTime) / 1000) }));
289
+ }
290
+ else {
291
+ console.log(chalk.yellow("\n⏱️ Timeout reached"));
292
+ console.log(chalk.dim(` Plan is still pending after ${options.timeout}s`));
293
+ }
294
+ process.exit(3);
295
+ }
296
+ // Wait before next poll
297
+ if (!options.json) {
298
+ process.stdout.write(".");
299
+ }
300
+ await new Promise((resolve) => setTimeout(resolve, interval));
301
+ }
302
+ catch (error) {
303
+ if (options.json) {
304
+ console.log(JSON.stringify({ status: "error", message: error instanceof Error ? error.message : String(error) }));
305
+ }
306
+ else {
307
+ console.error(chalk.red("\n✗ Error checking plan status:"), error instanceof Error ? error.message : error);
308
+ }
309
+ process.exit(1);
310
+ }
311
+ }
312
+ });
218
313
  export const planCommand = program;
219
314
  export default program;
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const supervisorCommand: Command;
@@ -0,0 +1,528 @@
1
+ import { Command } from "commander";
2
+ import { getConfig } from "./config.js";
3
+ import { spawn, execSync } from "child_process";
4
+ import { promisify } from "util";
5
+ import { exec } from "child_process";
6
+ const execAsync = promisify(exec);
7
+ export const supervisorCommand = new Command("supervisor")
8
+ .description("Manage Husky Supervisor Agent");
9
+ // supervisor start
10
+ supervisorCommand
11
+ .command("start")
12
+ .description("Start the supervisor agent")
13
+ .option("--session <name>", "Tmux session name", "supervisor")
14
+ .option("--id <id>", "Agent ID", "supervisor")
15
+ .option("--name <name>", "Agent display name", "Husky Supervisor")
16
+ .option("--vm <name>", "VM name (auto-detected if not provided)")
17
+ .action(async (options) => {
18
+ const config = getConfig();
19
+ if (!config.apiUrl) {
20
+ console.error("Error: API URL not configured. Run: husky config set api-url <url>");
21
+ process.exit(1);
22
+ }
23
+ console.log("🐕 Starting Husky Supervisor Agent...");
24
+ // Set environment variables
25
+ const env = {
26
+ ...process.env,
27
+ HUSKY_API_URL: config.apiUrl,
28
+ HUSKY_API_KEY: config.apiKey,
29
+ HUSKY_AGENT_ID: options.id,
30
+ HUSKY_SUPERVISOR_SESSION: options.session,
31
+ };
32
+ // Get the supervisor script path
33
+ const scriptPath = new URL("../scripts/supervisor-start.sh", import.meta.url).pathname;
34
+ try {
35
+ // Run the supervisor script
36
+ const child = spawn(scriptPath, [], {
37
+ env,
38
+ stdio: "inherit",
39
+ });
40
+ child.on("close", (code) => {
41
+ if (code !== 0) {
42
+ console.error(`Supervisor script exited with code ${code}`);
43
+ process.exit(code);
44
+ }
45
+ });
46
+ }
47
+ catch (error) {
48
+ console.error("Error starting supervisor:", error);
49
+ process.exit(1);
50
+ }
51
+ });
52
+ // supervisor status
53
+ supervisorCommand
54
+ .command("status")
55
+ .description("Check supervisor status")
56
+ .option("--session <name>", "Tmux session name", "supervisor")
57
+ .action(async (options) => {
58
+ try {
59
+ const { stdout } = await execAsync(`tmux has-session -t "${options.session}"`);
60
+ console.log("✅ Supervisor is running");
61
+ console.log(`Session: ${options.session}`);
62
+ // List windows
63
+ const { stdout: windows } = await execAsync(`tmux list-windows -t "${options.session}"`);
64
+ console.log("\nActive windows:");
65
+ console.log(windows);
66
+ }
67
+ catch (error) {
68
+ console.log("❌ Supervisor is not running");
69
+ process.exit(1);
70
+ }
71
+ });
72
+ // supervisor stop
73
+ supervisorCommand
74
+ .command("stop")
75
+ .description("Stop the supervisor agent")
76
+ .option("--session <name>", "Tmux session name", "supervisor")
77
+ .action(async (options) => {
78
+ try {
79
+ await execAsync(`tmux kill-session -t "${options.session}"`);
80
+ console.log("✅ Supervisor stopped successfully");
81
+ }
82
+ catch (error) {
83
+ console.log("❌ Supervisor is not running or failed to stop");
84
+ process.exit(1);
85
+ }
86
+ });
87
+ // supervisor restart
88
+ supervisorCommand
89
+ .command("restart")
90
+ .description("Restart the supervisor agent")
91
+ .option("--session <name>", "Tmux session name", "supervisor")
92
+ .action(async (options) => {
93
+ try {
94
+ console.log("🔄 Restarting supervisor...");
95
+ await execAsync(`tmux kill-session -t "${options.session}"`);
96
+ console.log("✅ Old supervisor stopped");
97
+ // Start new supervisor
98
+ const scriptPath = new URL("../scripts/supervisor-start.sh", import.meta.url).pathname;
99
+ const config = getConfig();
100
+ const child = spawn(scriptPath, [], {
101
+ env: {
102
+ ...process.env,
103
+ HUSKY_API_URL: config.apiUrl,
104
+ HUSKY_API_KEY: config.apiKey,
105
+ },
106
+ stdio: "inherit",
107
+ });
108
+ child.on("close", (code) => {
109
+ if (code !== 0) {
110
+ console.error(`Supervisor restart failed with code ${code}`);
111
+ process.exit(code);
112
+ }
113
+ });
114
+ }
115
+ catch (error) {
116
+ console.error("Error restarting supervisor:", error);
117
+ process.exit(1);
118
+ }
119
+ });
120
+ // supervisor attach
121
+ supervisorCommand
122
+ .command("attach")
123
+ .description("Attach to the supervisor tmux session")
124
+ .option("--session <name>", "Tmux session name", "supervisor")
125
+ .option("--window <name>", "Window name or number (default: supervisor)", "supervisor")
126
+ .action(async (options) => {
127
+ try {
128
+ const { stdout } = await execAsync(`tmux has-session -t "${options.session}"`);
129
+ // Attach to specific window
130
+ spawn("tmux", ["attach", "-t", `${options.session}:${options.window}`], {
131
+ stdio: "inherit",
132
+ });
133
+ }
134
+ catch (error) {
135
+ console.error("❌ Supervisor is not running");
136
+ console.log("Start it with: husky supervisor start");
137
+ process.exit(1);
138
+ }
139
+ });
140
+ // supervisor logs
141
+ supervisorCommand
142
+ .command("logs")
143
+ .description("Show logs from supervisor windows")
144
+ .option("--session <name>", "Tmux session name", "supervisor")
145
+ .option("--window <name>", "Window name (supervisor, messages, tasks, vms, heartbeat)")
146
+ .option("--lines <n>", "Number of lines to show", "50")
147
+ .action(async (options) => {
148
+ try {
149
+ await execAsync(`tmux has-session -t "${options.session}"`);
150
+ const window = options.window || "supervisor";
151
+ const lines = options.lines;
152
+ console.log(`📋 Showing last ${lines} lines from '${window}' window:\n`);
153
+ // Capture output from tmux pane
154
+ const { stdout } = await execAsync(`tmux capture-pane -t "${options.session}:${window}" -p | tail -n ${lines}`);
155
+ console.log(stdout);
156
+ }
157
+ catch (error) {
158
+ console.error("❌ Failed to get logs");
159
+ if (error instanceof Error && error.message.includes("session")) {
160
+ console.error("Supervisor is not running");
161
+ }
162
+ process.exit(1);
163
+ }
164
+ });
165
+ // supervisor message
166
+ supervisorCommand
167
+ .command("message")
168
+ .description("Send a message to the supervisor interface")
169
+ .argument("<message>", "Message to send")
170
+ .option("--session <name>", "Tmux session name", "supervisor")
171
+ .action(async (message, options) => {
172
+ try {
173
+ await execAsync(`tmux has-session -t "${options.session}"`);
174
+ // Send message to supervisor window
175
+ const escapedMessage = message
176
+ .replace(/\\/g, "\\\\")
177
+ .replace(/"/g, '\\"')
178
+ .replace(/\$/g, "\\$");
179
+ await execAsync(`tmux send-keys -t "${options.session}:supervisor" "${escapedMessage}" Enter`);
180
+ console.log("✅ Message sent to supervisor");
181
+ }
182
+ catch (error) {
183
+ console.error("❌ Failed to send message");
184
+ if (error instanceof Error && error.message.includes("session")) {
185
+ console.error("Supervisor is not running");
186
+ }
187
+ process.exit(1);
188
+ }
189
+ });
190
+ // supervisor execute
191
+ supervisorCommand
192
+ .command("execute")
193
+ .description("Execute a command in the supervisor interface")
194
+ .argument("<command>", "Command to execute")
195
+ .option("--session <name>", "Tmux session name", "supervisor")
196
+ .option("--window <name>", "Window to execute in", "supervisor")
197
+ .action(async (command, options) => {
198
+ try {
199
+ await execAsync(`tmux has-session -t "${options.session}"`);
200
+ // Send command to specified window
201
+ const escapedCommand = command
202
+ .replace(/\\/g, "\\\\")
203
+ .replace(/"/g, '\\"')
204
+ .replace(/\$/g, "\\$");
205
+ await execAsync(`tmux send-keys -t "${options.session}:${options.window}" "${escapedCommand}" Enter`);
206
+ console.log(`✅ Command sent to ${options.window} window`);
207
+ }
208
+ catch (error) {
209
+ console.error("❌ Failed to execute command");
210
+ if (error instanceof Error && error.message.includes("session")) {
211
+ console.error("Supervisor is not running");
212
+ }
213
+ process.exit(1);
214
+ }
215
+ });
216
+ // supervisor workflow
217
+ supervisorCommand
218
+ .command("workflow")
219
+ .description("Show supervisor workflow status and next actions")
220
+ .option("--json", "Output as JSON")
221
+ .action(async (options) => {
222
+ const config = getConfig();
223
+ if (!config.apiUrl) {
224
+ console.error("Error: API URL not configured. Run: husky config set api-url <url>");
225
+ process.exit(1);
226
+ }
227
+ try {
228
+ // Get task backlog
229
+ const tasksRes = await fetch(`${config.apiUrl}/api/tasks?status=backlog&assignee=llm&limit=10`, {
230
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
231
+ });
232
+ // Get VM status
233
+ const vmCommand = "gcloud compute instances list --filter='name~husky-' --format='json(name,status,zone,machineType)'";
234
+ const { stdout: vmOutput } = await execAsync(vmCommand);
235
+ const vms = JSON.parse(vmOutput || "[]");
236
+ // Get registered agents
237
+ const agentsRes = await fetch(`${config.apiUrl}/api/agents`, {
238
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
239
+ });
240
+ const tasks = tasksRes.ok ? await tasksRes.json() : { tasks: [] };
241
+ const agents = agentsRes.ok ? await agentsRes.json() : { agents: [] };
242
+ if (options.json) {
243
+ console.log(JSON.stringify({
244
+ tasks: tasks.tasks || [],
245
+ vms,
246
+ agents: agents.agents || [],
247
+ timestamp: new Date().toISOString(),
248
+ }, null, 2));
249
+ return;
250
+ }
251
+ console.log("\n🐕 Husky Supervisor Workflow Status");
252
+ console.log("=".repeat(50));
253
+ // Task status
254
+ console.log("\n📋 Task Backlog:");
255
+ const backlogTasks = tasks.tasks || [];
256
+ if (backlogTasks.length === 0) {
257
+ console.log(" No tasks in backlog");
258
+ }
259
+ else {
260
+ backlogTasks.forEach((task, i) => {
261
+ console.log(` ${i + 1}. ${task.title} (${task.id})`);
262
+ console.log(` Priority: ${task.priority} | Assignee: ${task.assignee}`);
263
+ });
264
+ }
265
+ // VM status
266
+ console.log("\n🖥️ VM Fleet Status:");
267
+ const runningWorkers = vms.filter((vm) => vm.name.includes("worker") && vm.status === "RUNNING");
268
+ const suspendedWorkers = vms.filter((vm) => vm.name.includes("worker") && vm.status === "SUSPENDED");
269
+ const terminatedWorkers = vms.filter((vm) => vm.name.includes("worker") && vm.status === "TERMINATED");
270
+ console.log(` Running workers: ${runningWorkers.length}`);
271
+ console.log(` Suspended workers: ${suspendedWorkers.length}`);
272
+ console.log(` Terminated workers: ${terminatedWorkers.length}`);
273
+ if (vms.find((vm) => vm.name.includes("supervisor"))) {
274
+ const supervisorVm = vms.find((vm) => vm.name.includes("supervisor"));
275
+ console.log(` Supervisor VM: ${supervisorVm.status}`);
276
+ }
277
+ // Agent status
278
+ console.log("\n🤖 Registered Agents:");
279
+ const agentList = agents.agents || [];
280
+ const onlineAgents = agentList.filter((agent) => agent.status === "online");
281
+ const busyAgents = agentList.filter((agent) => agent.status === "busy");
282
+ const offlineAgents = agentList.filter((agent) => agent.status === "offline");
283
+ console.log(` Online: ${onlineAgents.length}, Busy: ${busyAgents.length}, Offline: ${offlineAgents.length}`);
284
+ // Next actions
285
+ console.log("\n🎯 Suggested Actions:");
286
+ if (backlogTasks.length > 0 && runningWorkers.length === 0) {
287
+ console.log(" • Start worker VM(s) to handle task backlog");
288
+ }
289
+ if (terminatedWorkers.length > 0 && backlogTasks.length > 0) {
290
+ console.log(" • Resume terminated worker VMs");
291
+ }
292
+ if (backlogTasks.length > 0) {
293
+ console.log(" • Assign tasks to available workers");
294
+ }
295
+ if (offlineAgents.length > 0) {
296
+ console.log(" • Check on offline agents");
297
+ }
298
+ if (backlogTasks.length === 0 && runningWorkers.length > 0) {
299
+ console.log(" • Suspend idle worker VMs to save costs");
300
+ }
301
+ console.log("\nQuick commands:");
302
+ console.log(" husky supervisor start - Start supervisor");
303
+ console.log(" husky supervisor attach - Attach to supervisor");
304
+ console.log(" husk supervisor status - Check status");
305
+ console.log("");
306
+ }
307
+ catch (error) {
308
+ console.error("Error getting workflow status:", error);
309
+ process.exit(1);
310
+ }
311
+ });
312
+ // supervisor cycle
313
+ supervisorCommand
314
+ .command("cycle")
315
+ .description("Run the main supervisor loop (for automation)")
316
+ .option("--interval <seconds>", "Loop interval in seconds", "300") // 5 minutes default
317
+ .option("--once", "Run once and exit")
318
+ .action(async (options) => {
319
+ const config = getConfig();
320
+ if (!config.apiUrl) {
321
+ console.error("Error: API URL not configured. Run: husky config set api-url <url>");
322
+ process.exit(1);
323
+ }
324
+ const interval = parseInt(options.interval) * 1000;
325
+ console.log(`🐕 Starting supervisor cycle${options.once ? ' (once)' : ` (every ${options.interval}s)`}...`);
326
+ const runCycle = async () => {
327
+ try {
328
+ console.log(`\n[${new Date().toLocaleTimeString()}] Running supervisor cycle...`);
329
+ // Step 1: Check for new tasks
330
+ const tasksRes = await fetch(`${config.apiUrl}/api/tasks?status=backlog&assignee=llm&limit=5`, {
331
+ headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
332
+ });
333
+ if (tasksRes.ok) {
334
+ const tasksData = await tasksRes.json();
335
+ const tasks = tasksData.tasks || [];
336
+ if (tasks.length > 0) {
337
+ console.log(` Found ${tasks.length} tasks in backlog`);
338
+ // Step 2: Check available worker VMs
339
+ const { stdout: vmOutput } = await execAsync("gcloud compute instances list --filter='name~husky-worker' --format='json(name,status,zone)'");
340
+ const vms = JSON.parse(vmOutput || "[]");
341
+ const runningWorkers = vms.filter((vm) => vm.name.includes("worker") && vm.status === "RUNNING");
342
+ if (runningWorkers.length === 0) {
343
+ console.log(" No worker VMs running, starting one...");
344
+ // This would need to be implemented based on your GCP setup
345
+ console.log(" TODO: Implement VM startup logic");
346
+ }
347
+ // Step 3: Assign tasks to workers
348
+ tasks.forEach((task) => {
349
+ console.log(` Task ${task.id}: ${task.title}`);
350
+ });
351
+ }
352
+ else {
353
+ console.log(" No tasks in backlog");
354
+ }
355
+ }
356
+ console.log(`[${new Date().toLocaleTimeString()}] Cycle completed`);
357
+ }
358
+ catch (error) {
359
+ console.error(`[${new Date().toLocaleTimeString()}] Cycle error:`, error);
360
+ }
361
+ };
362
+ if (options.once) {
363
+ await runCycle();
364
+ }
365
+ else {
366
+ await runCycle();
367
+ setInterval(runCycle, interval);
368
+ console.log(`\nSupervisor cycle running. Press Ctrl+C to stop.`);
369
+ // Keep process alive
370
+ process.on("SIGINT", () => {
371
+ console.log("\n👋 Stopping supervisor cycle...");
372
+ process.exit(0);
373
+ });
374
+ await new Promise(() => { });
375
+ }
376
+ });
377
+ function checkGitHubPR(branch) {
378
+ try {
379
+ const result = execSync(`gh pr view "${branch}" --json url,mergedAt,state -q '{"url":.url,"mergedAt":.mergedAt,"state":.state}'`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
380
+ const data = JSON.parse(result.trim());
381
+ return {
382
+ exists: true,
383
+ merged: data.mergedAt !== null && data.mergedAt !== "",
384
+ url: data.url,
385
+ state: data.state,
386
+ };
387
+ }
388
+ catch {
389
+ return { exists: false, merged: false };
390
+ }
391
+ }
392
+ supervisorCommand
393
+ .command("auto-track")
394
+ .description("Automatic task status tracking based on GitHub PR state")
395
+ .option("--interval <minutes>", "Check interval in minutes", "5")
396
+ .option("--dry-run", "Show what would be updated without making changes")
397
+ .option("--once", "Run once and exit")
398
+ .option("--stale-hours <hours>", "Hours before task is considered stale", "24")
399
+ .option("--alert", "Send alerts for stale tasks to Google Chat")
400
+ .action(async (options) => {
401
+ const config = getConfig();
402
+ if (!config.apiUrl) {
403
+ console.error("Error: API URL not configured. Run: husky config set api-url <url>");
404
+ process.exit(1);
405
+ }
406
+ const intervalMs = parseInt(options.interval) * 60 * 1000;
407
+ const staleMs = parseInt(options.staleHours) * 60 * 60 * 1000;
408
+ const headers = {};
409
+ if (config.apiKey)
410
+ headers["x-api-key"] = config.apiKey;
411
+ console.log(`🔄 Starting auto-tracking loop (interval: ${options.interval}m, stale: ${options.staleHours}h)`);
412
+ if (options.dryRun)
413
+ console.log(" DRY RUN MODE - no changes will be made\n");
414
+ async function checkAndUpdate() {
415
+ const timestamp = new Date().toLocaleTimeString();
416
+ console.log(`\n[${timestamp}] Checking task statuses...`);
417
+ try {
418
+ const res = await fetch(`${config.apiUrl}/api/tasks?status=in_progress,review&limit=50`, { headers });
419
+ if (!res.ok) {
420
+ console.error(` ❌ Failed to fetch tasks: ${res.status}`);
421
+ return;
422
+ }
423
+ const data = await res.json();
424
+ const tasks = data.tasks || [];
425
+ if (tasks.length === 0) {
426
+ console.log(" No in_progress or review tasks found");
427
+ return;
428
+ }
429
+ console.log(` Found ${tasks.length} active tasks`);
430
+ const staleTasks = [];
431
+ const now = Date.now();
432
+ for (const task of tasks) {
433
+ const taskAge = now - new Date(task.updatedAt).getTime();
434
+ const isStale = taskAge > staleMs;
435
+ if (isStale) {
436
+ staleTasks.push(task);
437
+ }
438
+ if (!task.worktreeBranch) {
439
+ console.log(` 📌 ${task.id}: No branch linked (${task.status})${isStale ? " ⚠️ STALE" : ""}`);
440
+ continue;
441
+ }
442
+ const prStatus = checkGitHubPR(task.worktreeBranch);
443
+ if (prStatus.merged && task.status !== "done") {
444
+ console.log(` ✅ ${task.id}: PR merged → marking done`);
445
+ if (!options.dryRun) {
446
+ const updateRes = await fetch(`${config.apiUrl}/api/tasks/${task.id}`, {
447
+ method: "PATCH",
448
+ headers: { ...headers, "Content-Type": "application/json" },
449
+ body: JSON.stringify({
450
+ status: "done",
451
+ statusMessage: "PR merged - auto-tracked by supervisor",
452
+ }),
453
+ });
454
+ if (!updateRes.ok) {
455
+ console.error(` ❌ Failed to update task: ${updateRes.status}`);
456
+ }
457
+ }
458
+ }
459
+ else if (prStatus.exists && task.status === "in_progress") {
460
+ console.log(` 📝 ${task.id}: PR exists (${prStatus.state}) → marking review`);
461
+ if (!options.dryRun) {
462
+ const updateRes = await fetch(`${config.apiUrl}/api/tasks/${task.id}`, {
463
+ method: "PATCH",
464
+ headers: { ...headers, "Content-Type": "application/json" },
465
+ body: JSON.stringify({
466
+ status: "review",
467
+ prUrl: prStatus.url,
468
+ statusMessage: "PR opened - auto-tracked by supervisor",
469
+ }),
470
+ });
471
+ if (!updateRes.ok) {
472
+ console.error(` ❌ Failed to update task: ${updateRes.status}`);
473
+ }
474
+ }
475
+ }
476
+ else if (!prStatus.exists && task.status === "in_progress") {
477
+ console.log(` 🔨 ${task.id}: In progress, no PR yet${isStale ? " ⚠️ STALE" : ""}`);
478
+ }
479
+ else {
480
+ console.log(` ℹ️ ${task.id}: ${task.status} (PR: ${prStatus.exists ? prStatus.state : "none"})${isStale ? " ⚠️ STALE" : ""}`);
481
+ }
482
+ }
483
+ if (staleTasks.length > 0) {
484
+ console.log(`\n ⚠️ Found ${staleTasks.length} stale tasks (>${options.staleHours}h inactive):`);
485
+ staleTasks.forEach((t) => {
486
+ const hours = Math.round((now - new Date(t.updatedAt).getTime()) / (1000 * 60 * 60));
487
+ console.log(` - ${t.title} (${t.id}) - ${hours}h inactive`);
488
+ });
489
+ if (options.alert && !options.dryRun) {
490
+ const alertMessage = `⚠️ **Stale Task Alert**\n\n${staleTasks.length} tasks inactive for >${options.staleHours}h:\n\n${staleTasks.map((t) => `- ${t.title} (${t.id})`).join("\n")}\n\nPlease review and update status.`;
491
+ try {
492
+ const chatRes = await fetch(`${config.apiUrl}/api/google-chat/send`, {
493
+ method: "POST",
494
+ headers: { ...headers, "Content-Type": "application/json" },
495
+ body: JSON.stringify({
496
+ text: alertMessage,
497
+ spaceName: process.env.GOOGLE_CHAT_DM_SPACE,
498
+ }),
499
+ });
500
+ if (chatRes.ok) {
501
+ console.log(" 📣 Stale task alert sent to Google Chat");
502
+ }
503
+ }
504
+ catch (error) {
505
+ console.error(" ❌ Failed to send alert:", error);
506
+ }
507
+ }
508
+ }
509
+ console.log(`[${new Date().toLocaleTimeString()}] Check completed`);
510
+ }
511
+ catch (error) {
512
+ console.error(`[${timestamp}] Error:`, error);
513
+ }
514
+ }
515
+ if (options.once) {
516
+ await checkAndUpdate();
517
+ }
518
+ else {
519
+ await checkAndUpdate();
520
+ setInterval(checkAndUpdate, intervalMs);
521
+ console.log(`\nAuto-tracking running. Press Ctrl+C to stop.`);
522
+ process.on("SIGINT", () => {
523
+ console.log("\n👋 Stopping auto-tracking...");
524
+ process.exit(0);
525
+ });
526
+ await new Promise(() => { });
527
+ }
528
+ });
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ import { authCommand } from "./commands/auth.js";
38
38
  import { businessCommand } from "./commands/business.js";
39
39
  import { planCommand } from "./commands/plan.js";
40
40
  import { diagramsCommand } from "./commands/diagrams.js";
41
+ import { supervisorCommand } from "./commands/supervisor.js";
41
42
  import { checkVersion } from "./lib/version-check.js";
42
43
  // Read version from package.json
43
44
  const require = createRequire(import.meta.url);
@@ -84,6 +85,7 @@ program.addCommand(authCommand);
84
85
  program.addCommand(businessCommand);
85
86
  program.addCommand(planCommand);
86
87
  program.addCommand(diagramsCommand);
88
+ program.addCommand(supervisorCommand);
87
89
  // Handle --llm flag specially
88
90
  if (process.argv.includes("--llm")) {
89
91
  printLLMContext();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonfestl/husky-cli",
3
- "version": "1.33.1",
3
+ "version": "1.35.0",
4
4
  "description": "CLI for Huskyv0 Task Orchestration with Claude Agent SDK",
5
5
  "type": "module",
6
6
  "bin": {