@matthugh1/conductor-cli 0.1.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.
package/dist/agent.js ADDED
@@ -0,0 +1,717 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ensureDatabaseUrl,
4
+ postToServer
5
+ } from "./chunk-MJKFQIYA.js";
6
+
7
+ // ../../src/cli/agent.ts
8
+ import { resolve } from "path";
9
+ function parseArgs(argv) {
10
+ const args = argv.slice(2);
11
+ let projectRoot = process.cwd();
12
+ let project;
13
+ let json = false;
14
+ let help = false;
15
+ let once = false;
16
+ let maxDeliverables;
17
+ let timeout = 120;
18
+ let consecutiveFailures = 3;
19
+ let skipPermissions = false;
20
+ let dryRun = false;
21
+ let command;
22
+ let subcommand;
23
+ for (let i = 0; i < args.length; i++) {
24
+ const arg = args[i];
25
+ if (arg === "--project-root" && i + 1 < args.length) {
26
+ projectRoot = resolve(args[++i]);
27
+ } else if (arg === "--project" && i + 1 < args.length) {
28
+ project = args[++i];
29
+ } else if ((arg === "--max-deliverables" || arg === "--max-tasks") && i + 1 < args.length) {
30
+ maxDeliverables = Number.parseInt(args[++i], 10);
31
+ } else if (arg === "--timeout" && i + 1 < args.length) {
32
+ timeout = Number.parseInt(args[++i], 10);
33
+ } else if (arg === "--consecutive-failures" && i + 1 < args.length) {
34
+ consecutiveFailures = Number.parseInt(args[++i], 10);
35
+ } else if (arg === "--skip-permissions") {
36
+ skipPermissions = true;
37
+ } else if (arg === "--dry-run") {
38
+ dryRun = true;
39
+ } else if (arg === "--json") {
40
+ json = true;
41
+ } else if (arg === "--help" || arg === "-h") {
42
+ help = true;
43
+ } else if (arg === "--once") {
44
+ once = true;
45
+ } else if (!arg.startsWith("-") && command === void 0) {
46
+ command = arg;
47
+ } else if (!arg.startsWith("-") && subcommand === void 0) {
48
+ subcommand = arg;
49
+ }
50
+ }
51
+ return { command, subcommand, projectRoot, project, json, help, once, maxDeliverables, timeout, consecutiveFailures, skipPermissions, dryRun };
52
+ }
53
+ var HELP_TEXT = `
54
+ Conductor \u2014 local agent CLI
55
+
56
+ Usage: conductor <command> [options]
57
+
58
+ Commands:
59
+ check Run health checks on the local environment
60
+ scan Detect worktrees on disk and sync with database
61
+ init Initialize a project for Conductor
62
+ hooks Check hook status or install hooks
63
+ watch Poll for cleanup tasks and execute them locally
64
+ run Autonomous runner \u2014 work through deliverable queue
65
+
66
+ Options:
67
+ --project-root <path> Project directory (default: cwd)
68
+ --project <name> Project name (for run command)
69
+ --json Output as JSON instead of plain English
70
+ --once Process one task and exit (watch only)
71
+ --help Show this help text
72
+
73
+ Run options:
74
+ --max-deliverables <n> Max deliverables per run (default: 10)
75
+ --timeout <minutes> Max minutes per deliverable (default: 120)
76
+ --consecutive-failures <n> Stop after N failures in a row (default: 3)
77
+ --skip-permissions Allow Claude to write files without prompting
78
+ --dry-run Show queue without spawning agents
79
+
80
+ Examples:
81
+ conductor check
82
+ conductor init --project-root /path/to/project
83
+ conductor hooks install
84
+ conductor scan --json
85
+ conductor watch
86
+ conductor run --project Conductor --skip-permissions
87
+ conductor run --project Conductor --dry-run
88
+ `.trim();
89
+ function writeOut(text) {
90
+ process.stdout.write(text + "\n");
91
+ }
92
+ function writeErr(text) {
93
+ process.stderr.write(text + "\n");
94
+ }
95
+ async function cmdCheck(projectRoot, jsonOutput) {
96
+ const { query: dbQuery } = await import("./db-U6Y3QJDD.js");
97
+ const projects = await dbQuery(
98
+ "SELECT id FROM projects WHERE path = $1 LIMIT 1",
99
+ [projectRoot]
100
+ );
101
+ if (projects.length === 0) {
102
+ if (jsonOutput) {
103
+ writeOut(
104
+ JSON.stringify({
105
+ error: "Project not registered",
106
+ fix: "Run: conductor init"
107
+ })
108
+ );
109
+ } else {
110
+ writeErr(
111
+ "Project is not registered in Conductor. Run: conductor init"
112
+ );
113
+ }
114
+ return 1;
115
+ }
116
+ const { getEnvironmentHealthReport } = await import("./health-CTND2ANA.js");
117
+ const report = await getEnvironmentHealthReport(projects[0].id);
118
+ if (jsonOutput) {
119
+ writeOut(JSON.stringify(report, null, 2));
120
+ } else {
121
+ writeOut("Environment Health");
122
+ writeOut("\u2500".repeat(40));
123
+ for (const check of report.checks) {
124
+ const icon = check.status === "pass" ? "\u2713" : check.status === "warn" ? "\u26A0" : "\u2717";
125
+ writeOut(`${icon} ${check.name.padEnd(14)} ${check.message}`);
126
+ }
127
+ }
128
+ try {
129
+ const { saveHealthSnapshot } = await import("./health-snapshots-6MUVHE3G.js");
130
+ await saveHealthSnapshot(projects[0].id, report);
131
+ } catch {
132
+ const posted = await postToServer("/api/environment/health", { report }, projectRoot);
133
+ if (!posted && !jsonOutput) {
134
+ writeErr("\u26A0 Could not sync results to the dashboard.");
135
+ }
136
+ }
137
+ try {
138
+ const { getGitBranchInfo, formatGitBranchLine, getStatusSummary, getRecentCommits } = await import("./git-wrapper-DVJ46TMA.js");
139
+ const { getHookStatus } = await import("./git-hooks-UZJ6AER4.js");
140
+ const { saveGitStatusSnapshot } = await import("./git-snapshots-N3FBS7T3.js");
141
+ const branchInfo = await getGitBranchInfo(projectRoot);
142
+ const branchLine = formatGitBranchLine(branchInfo);
143
+ const status = await getStatusSummary(projectRoot);
144
+ let recentCommits = [];
145
+ try {
146
+ recentCommits = await getRecentCommits(projectRoot);
147
+ } catch {
148
+ }
149
+ let hookStatus = null;
150
+ try {
151
+ hookStatus = await getHookStatus(projectRoot);
152
+ } catch {
153
+ }
154
+ const worktreeRows = await dbQuery(
155
+ "SELECT branch_name, worktree_path FROM worktrees WHERE project_id = $1 AND status = 'active'",
156
+ [projects[0].id]
157
+ );
158
+ const worktreeCommits = {};
159
+ const { runGit } = await import("./git-wrapper-DVJ46TMA.js");
160
+ for (const wt of worktreeRows) {
161
+ try {
162
+ const out = await runGit(wt.worktree_path, ["log", "-1", "--format=%cI"]);
163
+ const date = out.trim();
164
+ if (date) worktreeCommits[wt.branch_name] = date;
165
+ } catch {
166
+ }
167
+ }
168
+ await saveGitStatusSnapshot(projects[0].id, {
169
+ branch: branchInfo.branchName,
170
+ branchLine,
171
+ status,
172
+ recentCommits,
173
+ hookStatus,
174
+ worktreeCommits
175
+ });
176
+ if (!jsonOutput) {
177
+ writeOut("");
178
+ writeOut("Git Status");
179
+ writeOut("\u2500".repeat(40));
180
+ writeOut(`Branch: ${branchLine}`);
181
+ writeOut(`Status: ${status}`);
182
+ }
183
+ } catch (err) {
184
+ if (!jsonOutput) {
185
+ const msg = err instanceof Error ? err.message : "unknown error";
186
+ writeErr(`\u26A0 Git status snapshot failed: ${msg}`);
187
+ }
188
+ }
189
+ try {
190
+ const { getGitBranchInfo } = await import("./git-wrapper-DVJ46TMA.js");
191
+ const { getBranchOverview } = await import("./branch-overview-XVHTGFCJ.js");
192
+ const { saveBranchOverviewSnapshot } = await import("./git-snapshots-N3FBS7T3.js");
193
+ const branchInfo = await getGitBranchInfo(projectRoot);
194
+ let currentBranch = branchInfo.branchName;
195
+ if (currentBranch?.startsWith("detached (")) currentBranch = null;
196
+ const overview = await getBranchOverview(projectRoot, {
197
+ projectId: projects[0].id,
198
+ currentBranch
199
+ });
200
+ await saveBranchOverviewSnapshot(projects[0].id, overview);
201
+ if (!jsonOutput) {
202
+ writeOut(`Branches: ${overview.branches.length} found`);
203
+ }
204
+ } catch (err) {
205
+ if (!jsonOutput) {
206
+ const msg = err instanceof Error ? err.message : "unknown error";
207
+ writeErr(`\u26A0 Branch overview snapshot failed: ${msg}`);
208
+ }
209
+ }
210
+ const hasFail = report.checks.some((c) => c.status === "fail");
211
+ const hasWarn = report.checks.some((c) => c.status === "warn");
212
+ if (hasFail) return 1;
213
+ if (hasWarn) return 2;
214
+ return 0;
215
+ }
216
+ async function cmdScan(projectRoot, jsonOutput) {
217
+ const { scanWorktrees } = await import("./worktree-manager-QKRBTPVC.js");
218
+ const { query: dbQuery } = await import("./db-U6Y3QJDD.js");
219
+ const projects = await dbQuery(
220
+ "SELECT id FROM projects WHERE path = $1 LIMIT 1",
221
+ [projectRoot]
222
+ );
223
+ if (projects.length === 0) {
224
+ if (jsonOutput) {
225
+ writeOut(
226
+ JSON.stringify({
227
+ error: "Project not registered",
228
+ fix: "Run: conductor init"
229
+ })
230
+ );
231
+ } else {
232
+ writeErr(
233
+ "Project is not registered in Conductor. Run: conductor init"
234
+ );
235
+ }
236
+ return 1;
237
+ }
238
+ const projectId = projects[0].id;
239
+ const result = await scanWorktrees(projectId, projectRoot);
240
+ if (jsonOutput) {
241
+ writeOut(JSON.stringify(result, null, 2));
242
+ } else {
243
+ writeOut(
244
+ `Scan complete: ${result.newlyDetected} registered, ${result.registered} existing, ${result.removedFromDisk} removed from disk`
245
+ );
246
+ if (result.worktrees.length === 0) {
247
+ writeOut("No worktrees found.");
248
+ } else {
249
+ writeOut("");
250
+ for (const wt of result.worktrees) {
251
+ const initiative = wt.initiativeTitle ?? "no initiative";
252
+ const tag = wt.newlyRegistered ? " [new]" : wt.removedFromDisk ? " [removed from disk]" : "";
253
+ const commit = wt.lastCommitAt ? ` last commit: ${wt.lastCommitAt.slice(0, 10)}` : "";
254
+ writeOut(
255
+ ` ${wt.branchName} (${initiative}) [${wt.status}]${tag}${commit}`
256
+ );
257
+ }
258
+ }
259
+ }
260
+ return 0;
261
+ }
262
+ async function cmdInit(projectRoot, jsonOutput) {
263
+ const { resolve: resolvePath, basename, join } = await import("path");
264
+ const { realpath, mkdir, readFile, writeFile, access } = await import("fs/promises");
265
+ const { getServerBaseUrl } = await import("./cli-config-TDSTAXIA.js");
266
+ let root = resolvePath(projectRoot.trim());
267
+ try {
268
+ root = await realpath(root);
269
+ } catch {
270
+ }
271
+ try {
272
+ await access(root);
273
+ } catch {
274
+ writeErr(`That folder doesn't seem to exist: ${root}`);
275
+ return 1;
276
+ }
277
+ const created = [];
278
+ const conductorDir = join(root, ".conductor");
279
+ let alreadyExisted = false;
280
+ try {
281
+ await access(conductorDir);
282
+ alreadyExisted = true;
283
+ } catch {
284
+ await mkdir(conductorDir, { recursive: true });
285
+ created.push("Created .conductor/ folder");
286
+ }
287
+ const gitignorePath = join(root, ".gitignore");
288
+ let gitignoreContent = "";
289
+ try {
290
+ gitignoreContent = await readFile(gitignorePath, "utf8");
291
+ } catch {
292
+ }
293
+ const hasConductorEntry = gitignoreContent.split("\n").some((l) => l.trim() === ".conductor/" || l.trim() === ".conductor");
294
+ if (!hasConductorEntry) {
295
+ const sep = gitignoreContent.length > 0 && !gitignoreContent.endsWith("\n") ? "\n" : "";
296
+ const pre = gitignoreContent.length > 0 ? "\n" : "";
297
+ await writeFile(gitignorePath, `${gitignoreContent}${sep}${pre}# Conductor session data
298
+ .conductor/
299
+ `, "utf8");
300
+ created.push("Added .conductor/ to .gitignore");
301
+ }
302
+ let registeredRemotely = false;
303
+ let registrationError = null;
304
+ try {
305
+ const baseUrl = getServerBaseUrl();
306
+ const resp = await fetch(`${baseUrl}/projects/register`, {
307
+ method: "POST",
308
+ headers: { "Content-Type": "application/json" },
309
+ body: JSON.stringify({ name: basename(root), localPath: root }),
310
+ signal: AbortSignal.timeout(5e3)
311
+ });
312
+ if (resp.ok) {
313
+ registeredRemotely = true;
314
+ created.push("Registered with Conductor server");
315
+ } else {
316
+ const errBody = await resp.json().catch(() => ({}));
317
+ registrationError = errBody.error ?? `Server returned ${resp.status}`;
318
+ }
319
+ } catch {
320
+ registrationError = "Could not reach the Conductor server. Local setup still succeeded.";
321
+ }
322
+ if (created.length === 0) created.push("Everything was already set up");
323
+ const result = { success: true, projectRoot: root, created, alreadyExisted, registeredRemotely, registrationError };
324
+ if (jsonOutput) {
325
+ writeOut(JSON.stringify(result, null, 2));
326
+ } else {
327
+ if (alreadyExisted && !registeredRemotely && !registrationError) {
328
+ writeOut("Already initialized \u2014 no changes needed.");
329
+ } else {
330
+ writeOut("Project initialized.");
331
+ writeOut(` ${created.join(", ")}`);
332
+ }
333
+ if (registrationError) {
334
+ writeOut(` \u26A0 ${registrationError}`);
335
+ }
336
+ }
337
+ return 0;
338
+ }
339
+ async function cmdHooks(projectRoot, subcommand, jsonOutput) {
340
+ const { getHookStatus, installHooks } = await import("./git-hooks-UZJ6AER4.js");
341
+ if (subcommand === "install") {
342
+ const result = await installHooks(projectRoot);
343
+ if (jsonOutput) {
344
+ writeOut(JSON.stringify(result, null, 2));
345
+ } else {
346
+ if (result.installed.length > 0) {
347
+ writeOut(`Hooks installed: ${result.installed.join(", ")}`);
348
+ }
349
+ if (result.skipped.length > 0) {
350
+ writeOut(
351
+ `Skipped (custom hooks exist): ${result.skipped.join(", ")}`
352
+ );
353
+ }
354
+ if (result.installed.length === 0 && result.skipped.length === 0) {
355
+ writeOut("No hooks to install.");
356
+ }
357
+ }
358
+ return 0;
359
+ }
360
+ const status = await getHookStatus(projectRoot);
361
+ const presentCount = status.hooks.filter((h) => h.present).length;
362
+ const totalCount = status.hooks.length;
363
+ if (jsonOutput) {
364
+ writeOut(JSON.stringify(status, null, 2));
365
+ } else {
366
+ if (status.installed && !status.outdated) {
367
+ writeOut(`\u2713 Hooks installed (${presentCount}/${totalCount})`);
368
+ } else if (status.outdated) {
369
+ writeOut(
370
+ `\u26A0 Hooks outdated (${presentCount}/${totalCount}) \u2014 run: conductor hooks install`
371
+ );
372
+ } else {
373
+ writeOut(
374
+ "\u2717 Hooks not installed \u2014 run: conductor hooks install"
375
+ );
376
+ }
377
+ }
378
+ if (status.installed && !status.outdated) return 0;
379
+ return 2;
380
+ }
381
+ async function cmdWatch(projectRoot, once, jsonOutput) {
382
+ const { claimNextTask, executeTask, failStaleTasks } = await import("./cli-tasks-NW3BONXC.js");
383
+ const { query: dbQuery } = await import("./db-U6Y3QJDD.js");
384
+ const projects = await dbQuery(
385
+ "SELECT id FROM projects WHERE path = $1 LIMIT 1",
386
+ [projectRoot]
387
+ );
388
+ if (projects.length === 0) {
389
+ if (jsonOutput) {
390
+ writeOut(
391
+ JSON.stringify({
392
+ error: "Project not registered",
393
+ fix: "Run: conductor init"
394
+ })
395
+ );
396
+ } else {
397
+ writeErr(
398
+ "Project is not registered in Conductor. Run: conductor init"
399
+ );
400
+ }
401
+ return 1;
402
+ }
403
+ const projectId = projects[0].id;
404
+ const staleCount = await failStaleTasks(projectId);
405
+ if (staleCount > 0 && !jsonOutput) {
406
+ writeOut(`Auto-failed ${staleCount} stale task(s).`);
407
+ }
408
+ if (once) {
409
+ const task = await claimNextTask(projectId);
410
+ if (!task) {
411
+ if (jsonOutput) {
412
+ writeOut(JSON.stringify({ message: "No pending tasks." }));
413
+ } else {
414
+ writeOut("No pending tasks.");
415
+ }
416
+ return 0;
417
+ }
418
+ const summary = await executeTask(task);
419
+ if (jsonOutput) {
420
+ writeOut(JSON.stringify({ taskId: task.id, taskType: task.taskType, summary }));
421
+ } else {
422
+ writeOut(`[${task.taskType}] ${summary}`);
423
+ }
424
+ return 0;
425
+ }
426
+ if (!jsonOutput) {
427
+ writeOut("Watching for tasks... (Ctrl+C to stop)");
428
+ }
429
+ let running = true;
430
+ const shutdown = () => {
431
+ if (!jsonOutput) {
432
+ writeOut("\nShutting down gracefully...");
433
+ }
434
+ running = false;
435
+ };
436
+ process.on("SIGINT", shutdown);
437
+ process.on("SIGTERM", shutdown);
438
+ while (running) {
439
+ try {
440
+ const task = await claimNextTask(projectId);
441
+ if (task) {
442
+ const summary = await executeTask(task);
443
+ if (jsonOutput) {
444
+ writeOut(
445
+ JSON.stringify({ taskId: task.id, taskType: task.taskType, summary })
446
+ );
447
+ } else {
448
+ writeOut(`[${task.taskType}] ${summary}`);
449
+ }
450
+ }
451
+ await failStaleTasks(projectId);
452
+ } catch (err) {
453
+ const msg = err instanceof Error ? err.message : String(err);
454
+ if (!jsonOutput) {
455
+ writeErr(`Poll error: ${msg}`);
456
+ }
457
+ }
458
+ for (let i = 0; i < 5 && running; i++) {
459
+ await new Promise((resolve2) => setTimeout(resolve2, 1e3));
460
+ }
461
+ }
462
+ return 0;
463
+ }
464
+ async function cmdRun(opts) {
465
+ const { spawn } = await import("child_process");
466
+ const { readFileSync, existsSync } = await import("fs");
467
+ const { assembleAutonomousPrompt } = await import("./runner-prompt-2B6EXGN6.js");
468
+ const { computeWorkQueue } = await import("./work-queue-YE5P4S7R.js");
469
+ const { query: dbQuery } = await import("./db-U6Y3QJDD.js");
470
+ let project = opts.projectName ?? opts.projectRoot;
471
+ if (opts.projectName) {
472
+ const rows = await dbQuery(
473
+ "SELECT path FROM projects WHERE name = $1 LIMIT 1",
474
+ [opts.projectName]
475
+ );
476
+ if (rows.length === 0) {
477
+ writeErr(`Project "${opts.projectName}" not found in Conductor.`);
478
+ return 1;
479
+ }
480
+ if (rows[0].path === null) {
481
+ writeErr(`Project "${opts.projectName}" has no local path registered.`);
482
+ return 1;
483
+ }
484
+ project = rows[0].path;
485
+ }
486
+ function ts() {
487
+ return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
488
+ }
489
+ function log(msg) {
490
+ writeOut(`[${ts()}] ${msg}`);
491
+ }
492
+ function readClaudeMd(root) {
493
+ for (const p of [resolve(root, "CLAUDE.md"), resolve(root, ".claude", "CLAUDE.md")]) {
494
+ if (existsSync(p)) return readFileSync(p, "utf8");
495
+ }
496
+ return "";
497
+ }
498
+ function spawnAgent(prompt, timeoutMs) {
499
+ return new Promise((res) => {
500
+ let timedOut = false;
501
+ const args = [];
502
+ if (opts.skipPermissions) args.push("--dangerously-skip-permissions");
503
+ args.push("-p", prompt);
504
+ const child = spawn("claude", args, {
505
+ stdio: ["ignore", "inherit", "inherit"],
506
+ env: { ...process.env }
507
+ });
508
+ const timer = setTimeout(() => {
509
+ timedOut = true;
510
+ child.kill("SIGTERM");
511
+ setTimeout(() => {
512
+ if (!child.killed) child.kill("SIGKILL");
513
+ }, 1e4);
514
+ }, timeoutMs);
515
+ child.on("close", (code) => {
516
+ clearTimeout(timer);
517
+ res({ exitCode: code, timedOut });
518
+ });
519
+ child.on("error", (err) => {
520
+ clearTimeout(timer);
521
+ writeErr(`[${ts()}] ERROR: Failed to spawn agent: ${err.message}`);
522
+ res({ exitCode: 1, timedOut: false });
523
+ });
524
+ });
525
+ }
526
+ async function getNextItem(skipIds) {
527
+ const queue = await computeWorkQueue(project, 50, { autonomous: true });
528
+ for (const item of queue.queue) {
529
+ if (item.type === "deliverable" && (item.tier === "ready" || item.tier === "active") && !skipIds.has(item.entityId)) {
530
+ return item;
531
+ }
532
+ }
533
+ return null;
534
+ }
535
+ log(`Starting runner for project: ${project}`);
536
+ if (opts.skipPermissions) log("Permissions: --dangerously-skip-permissions will be passed to Claude");
537
+ const claudeMd = readClaudeMd(project);
538
+ if (claudeMd.length > 0) log("Loaded CLAUDE.md project rules");
539
+ const results = [];
540
+ const attemptedIds = /* @__PURE__ */ new Set();
541
+ const maxTasks = opts.maxDeliverables ?? 10;
542
+ let totalIterations = 0;
543
+ let consecutiveFailures = 0;
544
+ for (let taskNum = 0; taskNum < maxTasks; taskNum++) {
545
+ if (consecutiveFailures >= opts.consecutiveFailures) {
546
+ log(`Stopping: ${consecutiveFailures} consecutive failures reached limit.`);
547
+ break;
548
+ }
549
+ let item;
550
+ try {
551
+ item = await getNextItem(attemptedIds);
552
+ } catch (err) {
553
+ const msg = err instanceof Error ? err.message : String(err);
554
+ writeErr(`[${ts()}] ERROR: Failed to fetch work queue: ${msg}`);
555
+ consecutiveFailures++;
556
+ totalIterations++;
557
+ results.push({ title: "Queue fetch", outcome: "error", durationMs: 0, exitCode: null, errorMessage: msg });
558
+ continue;
559
+ }
560
+ if (item === null) {
561
+ log("Queue empty \u2014 no autonomous deliverables remaining.");
562
+ break;
563
+ }
564
+ attemptedIds.add(item.entityId);
565
+ log(`Working on: "${item.title}" (P${item.priority})${item.initiativeTitle ? ` [${item.initiativeTitle}]` : ""}`);
566
+ if (opts.dryRun) {
567
+ log(` [dry-run] Would spawn agent for deliverable ${item.entityId}`);
568
+ results.push({ title: item.title, outcome: "completed", durationMs: 0, exitCode: 0 });
569
+ totalIterations++;
570
+ continue;
571
+ }
572
+ let prompt;
573
+ try {
574
+ prompt = assembleAutonomousPrompt({ claudeMd, item, projectName: project });
575
+ } catch (err) {
576
+ const msg = err instanceof Error ? err.message : String(err);
577
+ writeErr(`[${ts()}] ERROR: Prompt assembly failed for "${item.title}": ${msg}`);
578
+ consecutiveFailures++;
579
+ totalIterations++;
580
+ results.push({ title: item.title, outcome: "error", durationMs: 0, exitCode: null, errorMessage: `Prompt: ${msg}` });
581
+ continue;
582
+ }
583
+ const timeoutMs = opts.timeout * 6e4;
584
+ const startTime = Date.now();
585
+ let exitCode;
586
+ let timedOut;
587
+ try {
588
+ const r = await spawnAgent(prompt, timeoutMs);
589
+ exitCode = r.exitCode;
590
+ timedOut = r.timedOut;
591
+ } catch (err) {
592
+ const msg = err instanceof Error ? err.message : String(err);
593
+ writeErr(`[${ts()}] ERROR: Agent spawn crashed for "${item.title}": ${msg}`);
594
+ consecutiveFailures++;
595
+ totalIterations++;
596
+ results.push({ title: item.title, outcome: "error", durationMs: Date.now() - startTime, exitCode: null, errorMessage: msg });
597
+ log(" Branch left intact for inspection.");
598
+ continue;
599
+ }
600
+ const durationMs = Date.now() - startTime;
601
+ const durationMin = Math.round(durationMs / 6e4);
602
+ totalIterations++;
603
+ if (timedOut) {
604
+ log(`Timed out after ${durationMin}m \u2014 "${item.title}". Branch left intact.`);
605
+ consecutiveFailures++;
606
+ results.push({ title: item.title, outcome: "timeout", durationMs, exitCode, errorMessage: `Timed out after ${durationMin}m` });
607
+ } else if (exitCode === 0) {
608
+ log(`Completed in ${durationMin}m \u2014 "${item.title}"`);
609
+ consecutiveFailures = 0;
610
+ results.push({ title: item.title, outcome: "completed", durationMs, exitCode });
611
+ } else {
612
+ log(`Failed (exit ${exitCode}) after ${durationMin}m \u2014 "${item.title}". Branch left intact.`);
613
+ consecutiveFailures++;
614
+ results.push({ title: item.title, outcome: "failed", durationMs, exitCode, errorMessage: `Exit code ${exitCode}` });
615
+ }
616
+ }
617
+ log("");
618
+ log("\u2550\u2550\u2550 Runner Summary \u2550\u2550\u2550");
619
+ log(`Deliverables attempted: ${results.length}`);
620
+ const cCount = results.filter((r) => r.outcome === "completed").length;
621
+ const fCount = results.filter((r) => r.outcome === "failed" || r.outcome === "error").length;
622
+ const tCount = results.filter((r) => r.outcome === "timeout").length;
623
+ if (cCount > 0) log(` Completed: ${cCount}`);
624
+ if (fCount > 0) log(` Failed: ${fCount}`);
625
+ if (tCount > 0) log(` Timed out: ${tCount}`);
626
+ if (consecutiveFailures >= opts.consecutiveFailures) {
627
+ log(` Stopped early: ${consecutiveFailures} consecutive failures.`);
628
+ }
629
+ const totalMin = Math.round(results.reduce((s, r) => s + r.durationMs, 0) / 6e4);
630
+ log(`Total time: ${totalMin}m`);
631
+ log(`Total iterations: ${totalIterations}`);
632
+ for (const r of results) {
633
+ const min = Math.round(r.durationMs / 6e4);
634
+ const icon = r.outcome === "completed" ? "OK" : r.outcome === "timeout" ? "TIMEOUT" : r.outcome === "error" ? "ERROR" : "FAIL";
635
+ const detail = r.errorMessage ? ` \u2014 ${r.errorMessage}` : "";
636
+ log(` [${icon}] ${r.title} (${min}m)${detail}`);
637
+ }
638
+ if (opts.jsonOutput) {
639
+ writeOut(JSON.stringify({ attempted: results.length, completed: cCount, failed: fCount, timedOut: tCount, results }, null, 2));
640
+ }
641
+ return fCount > 0 ? 1 : 0;
642
+ }
643
+ async function main() {
644
+ const parsed = parseArgs(process.argv);
645
+ if (parsed.help || parsed.command === void 0) {
646
+ writeOut(HELP_TEXT);
647
+ process.exit(parsed.help ? 0 : 1);
648
+ }
649
+ const hasDb = await ensureDatabaseUrl();
650
+ if (!hasDb) {
651
+ writeErr(
652
+ "Could not find a database connection. Make sure the MCP server is running (npm run mcp) and try again."
653
+ );
654
+ process.exit(1);
655
+ }
656
+ try {
657
+ let exitCode;
658
+ switch (parsed.command) {
659
+ case "check":
660
+ exitCode = await cmdCheck(parsed.projectRoot, parsed.json);
661
+ break;
662
+ case "scan":
663
+ exitCode = await cmdScan(parsed.projectRoot, parsed.json);
664
+ break;
665
+ case "init":
666
+ exitCode = await cmdInit(parsed.projectRoot, parsed.json);
667
+ break;
668
+ case "hooks":
669
+ exitCode = await cmdHooks(
670
+ parsed.projectRoot,
671
+ parsed.subcommand,
672
+ parsed.json
673
+ );
674
+ break;
675
+ case "watch":
676
+ exitCode = await cmdWatch(
677
+ parsed.projectRoot,
678
+ parsed.once,
679
+ parsed.json
680
+ );
681
+ break;
682
+ case "run":
683
+ exitCode = await cmdRun({
684
+ projectRoot: parsed.projectRoot,
685
+ projectName: parsed.project,
686
+ maxDeliverables: parsed.maxDeliverables,
687
+ timeout: parsed.timeout,
688
+ consecutiveFailures: parsed.consecutiveFailures,
689
+ skipPermissions: parsed.skipPermissions,
690
+ dryRun: parsed.dryRun,
691
+ jsonOutput: parsed.json
692
+ });
693
+ break;
694
+ default:
695
+ writeErr(`Unknown command: ${parsed.command}`);
696
+ writeErr("Run conductor --help for usage.");
697
+ exitCode = 1;
698
+ }
699
+ process.exit(exitCode);
700
+ } catch (err) {
701
+ const message = err instanceof Error ? err.message : String(err);
702
+ if (parsed.json) {
703
+ writeOut(JSON.stringify({ error: message }, null, 2));
704
+ } else {
705
+ writeErr(`Error: ${message}`);
706
+ if (message.includes("ECONNREFUSED") || message.includes("Could not reach")) {
707
+ writeErr("Fix: Make sure the MCP server is running (npm run mcp) or set DATABASE_URL");
708
+ } else if (message.includes("not a git repository")) {
709
+ writeErr("Fix: Run this from inside a git repository");
710
+ } else if (message.includes("Git is not installed")) {
711
+ writeErr("Fix: Install git \u2014 https://git-scm.com/downloads");
712
+ }
713
+ }
714
+ process.exit(1);
715
+ }
716
+ }
717
+ main();