copilot-agent 0.2.0 → 0.4.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/index.js CHANGED
@@ -1,11 +1,6 @@
1
- #!/usr/bin/env node
2
-
3
1
  // src/index.ts
4
2
  import { Command } from "commander";
5
3
 
6
- // src/commands/status.ts
7
- import chalk from "chalk";
8
-
9
4
  // src/lib/session.ts
10
5
  import {
11
6
  existsSync,
@@ -138,6 +133,7 @@ function findLatestIncomplete() {
138
133
 
139
134
  // src/lib/process.ts
140
135
  import { execSync as execSync2, spawn } from "child_process";
136
+ import { resolve as resolve2 } from "path";
141
137
 
142
138
  // src/lib/logger.ts
143
139
  import { appendFileSync, mkdirSync } from "fs";
@@ -148,6 +144,9 @@ import { execSync } from "child_process";
148
144
  var RED = "\x1B[31m";
149
145
  var GREEN = "\x1B[32m";
150
146
  var YELLOW = "\x1B[33m";
147
+ var CYAN = "\x1B[36m";
148
+ var DIM = "\x1B[2m";
149
+ var BOLD = "\x1B[1m";
151
150
  var RESET = "\x1B[0m";
152
151
 
153
152
  // src/lib/logger.ts
@@ -186,6 +185,11 @@ function fail(msg) {
186
185
  console.error(out);
187
186
  writeToFile(`\u2716 ${msg}`);
188
187
  }
188
+ function info(msg) {
189
+ const out = `${CYAN}\u2139 ${msg}${RESET}`;
190
+ console.log(out);
191
+ writeToFile(`\u2139 ${msg}`);
192
+ }
189
193
  function notify(message, title = "copilot-agent") {
190
194
  try {
191
195
  if (process.platform === "darwin") {
@@ -226,19 +230,31 @@ function findCopilotProcesses() {
226
230
  try {
227
231
  const output = execSync2("ps -eo pid,command", { encoding: "utf-8" });
228
232
  const results = [];
233
+ const myPid = process.pid;
234
+ const parentPid = process.ppid;
229
235
  for (const line of output.split("\n")) {
230
236
  const trimmed = line.trim();
231
237
  if (!trimmed) continue;
232
238
  if ((trimmed.includes("copilot") || trimmed.includes("@githubnext/copilot")) && !trimmed.includes("ps -eo") && !trimmed.includes("copilot-agent") && !trimmed.includes("grep")) {
233
239
  const match = trimmed.match(/^(\d+)\s+(.+)$/);
234
240
  if (match) {
241
+ const pid = parseInt(match[1], 10);
242
+ if (pid === myPid || pid === parentPid) continue;
235
243
  const cmd = match[2];
236
244
  const sidMatch = cmd.match(/resume[= ]+([a-f0-9-]{36})/);
237
- results.push({
238
- pid: parseInt(match[1], 10),
239
- command: cmd,
240
- sessionId: sidMatch?.[1]
241
- });
245
+ let cwd;
246
+ try {
247
+ cwd = execSync2(`lsof -p ${pid} -Fn 2>/dev/null | grep '^n/' | head -1`, {
248
+ encoding: "utf-8",
249
+ stdio: ["pipe", "pipe", "pipe"]
250
+ }).trim().slice(1) || void 0;
251
+ } catch {
252
+ }
253
+ const sid = sidMatch?.[1];
254
+ if (!cwd && sid) {
255
+ cwd = getSessionCwd(sid) || void 0;
256
+ }
257
+ results.push({ pid, command: cmd, sessionId: sid, cwd });
242
258
  }
243
259
  }
244
260
  }
@@ -252,6 +268,35 @@ function findPidForSession(sid) {
252
268
  const matching = procs.filter((p) => p.command.includes(sid)).sort((a, b) => b.pid - a.pid);
253
269
  return matching[0]?.pid ?? null;
254
270
  }
271
+ async function waitForCopilotInDir(dir, timeoutMs = 144e5, pollMs = 1e4) {
272
+ const targetDir = resolve2(dir);
273
+ const start = Date.now();
274
+ let warned = false;
275
+ while (Date.now() - start < timeoutMs) {
276
+ const procs = findCopilotProcesses();
277
+ const conflicting = procs.filter((p) => {
278
+ if (!p.cwd) return false;
279
+ return resolve2(p.cwd) === targetDir;
280
+ });
281
+ if (conflicting.length === 0) return;
282
+ if (!warned) {
283
+ warn(`Waiting for copilot in ${targetDir} to finish...`);
284
+ for (const p of conflicting) {
285
+ log(` PID ${p.pid}: ${p.command.slice(0, 80)}`);
286
+ }
287
+ warned = true;
288
+ }
289
+ await sleep(pollMs);
290
+ }
291
+ warn("Timeout waiting for copilot to finish in directory");
292
+ }
293
+ function assertSessionNotRunning(sid) {
294
+ const pid = findPidForSession(sid);
295
+ if (pid) {
296
+ fail(`Session ${sid.slice(0, 8)}\u2026 already has copilot running (PID ${pid}). Cannot resume \u2014 would corrupt the session.`);
297
+ process.exit(1);
298
+ }
299
+ }
255
300
  async function waitForExit(pid, timeoutMs = 144e5) {
256
301
  const start = Date.now();
257
302
  while (Date.now() - start < timeoutMs) {
@@ -264,8 +309,10 @@ async function waitForExit(pid, timeoutMs = 144e5) {
264
309
  }
265
310
  return false;
266
311
  }
267
- function runCopilot(args, options) {
268
- return new Promise((resolve4) => {
312
+ async function runCopilot(args, options) {
313
+ const dir = options?.cwd ?? process.cwd();
314
+ await waitForCopilotInDir(dir);
315
+ return new Promise((resolve6) => {
269
316
  const child = spawn("copilot", args, {
270
317
  cwd: options?.cwd,
271
318
  stdio: "inherit",
@@ -275,18 +322,19 @@ function runCopilot(args, options) {
275
322
  await sleep(3e3);
276
323
  const sid = getLatestSessionId();
277
324
  const premium = sid ? getSessionPremium(sid) : 0;
278
- resolve4({
325
+ resolve6({
279
326
  exitCode: code ?? 1,
280
327
  sessionId: sid,
281
328
  premium
282
329
  });
283
330
  });
284
331
  child.on("error", () => {
285
- resolve4({ exitCode: 1, sessionId: null, premium: 0 });
332
+ resolve6({ exitCode: 1, sessionId: null, premium: 0 });
286
333
  });
287
334
  });
288
335
  }
289
336
  function runCopilotResume(sid, steps, message, cwd) {
337
+ assertSessionNotRunning(sid);
290
338
  const args = [
291
339
  `--resume=${sid}`,
292
340
  "--autopilot",
@@ -314,61 +362,77 @@ function sleep(ms) {
314
362
  }
315
363
 
316
364
  // src/commands/status.ts
317
- async function statusCommand(opts) {
318
- if (opts.active) {
319
- return showActive();
320
- }
321
- showRecent(opts.limit);
365
+ function registerStatusCommand(program2) {
366
+ program2.command("status").description("Show copilot session status").option("-l, --limit <n>", "Number of sessions to show", "10").option("-a, --active", "Show only active (running) processes").option("-i, --incomplete", "Only show incomplete sessions").action((opts) => {
367
+ if (opts.active) {
368
+ showActive();
369
+ } else {
370
+ showRecent(parseInt(opts.limit, 10), opts.incomplete ?? false);
371
+ }
372
+ });
322
373
  }
323
- async function showActive() {
324
- const procs = await findCopilotProcesses();
374
+ function showActive() {
375
+ const procs = findCopilotProcesses();
325
376
  if (procs.length === 0) {
326
- log("No active copilot processes.");
377
+ log(`${DIM}No active copilot processes.${RESET}`);
327
378
  return;
328
379
  }
329
- console.log(
330
- chalk.bold(
331
- `
332
- ${"PID".padEnd(8)} ${"Session".padEnd(40)} ${"Command".slice(0, 60)}`
333
- )
334
- );
335
- console.log("\u2500".repeat(108));
380
+ log(`
381
+ ${BOLD}${"PID".padEnd(8)} ${"Session".padEnd(40)} Command${RESET}`);
382
+ log("\u2500".repeat(108));
336
383
  for (const p of procs) {
337
- console.log(
338
- `${String(p.pid).padEnd(8)} ${(p.sessionId ?? "\u2014").padEnd(40)} ${(p.command ?? "").slice(0, 60)}`
384
+ log(
385
+ `${CYAN}${String(p.pid).padEnd(8)}${RESET} ${(p.sessionId ?? "\u2014").padEnd(40)} ${truncate(p.command, 58)}`
339
386
  );
340
387
  }
341
- console.log();
388
+ log("");
342
389
  }
343
- function showRecent(limit) {
344
- const sessions = listSessions(limit);
390
+ function showRecent(limit, incompleteOnly) {
391
+ let sessions = listSessions(limit);
392
+ if (incompleteOnly) {
393
+ sessions = sessions.filter((s) => !s.complete);
394
+ }
345
395
  if (sessions.length === 0) {
346
- log("No sessions found.");
396
+ log(`${DIM}No sessions found.${RESET}`);
347
397
  return;
348
398
  }
349
- console.log(
350
- chalk.bold(
351
- `
352
- ${"Status".padEnd(10)} ${"Premium".padEnd(10)} ${"Last Event".padEnd(25)} ${"Summary".padEnd(40)} ${"ID"}`
353
- )
399
+ log(
400
+ `
401
+ ${BOLD}${"Status".padEnd(10)} ${"Premium".padEnd(10)} ${"Last Event".padEnd(25)} ${"Summary".padEnd(40)} ID${RESET}`
354
402
  );
355
- console.log("\u2500".repeat(120));
403
+ log("\u2500".repeat(120));
356
404
  for (const s of sessions) {
357
- const done = hasTaskComplete(s.id);
358
- const status = done ? chalk.green("\u2705 done") : chalk.yellow("\u23F8\uFE0F stopped");
359
- const premium = String(getSessionPremium(s.id));
360
- const lastEvt = getLastEvent(s.id);
361
- const summary = (s.summary ?? "\u2014").slice(0, 38);
362
- console.log(
363
- `${status.padEnd(20)} ${premium.padEnd(10)} ${lastEvt.padEnd(25)} ${summary.padEnd(40)} ${chalk.dim(s.id)}`
405
+ const status = s.complete ? `${GREEN}\u2714 done${RESET}` : `${YELLOW}\u23F8 stop${RESET}`;
406
+ const premium = String(s.premiumRequests);
407
+ const summary = truncate(s.summary || "\u2014", 38);
408
+ log(
409
+ `${status.padEnd(10 + 9)} ${premium.padEnd(10)} ${s.lastEvent.padEnd(25)} ${summary.padEnd(40)} ${DIM}${s.id}${RESET}`
364
410
  );
365
411
  }
366
- console.log();
412
+ log(`
413
+ ${DIM}Total: ${sessions.length} session(s)${RESET}`);
414
+ }
415
+ function truncate(s, max) {
416
+ if (s.length <= max) return s;
417
+ return s.substring(0, max - 1) + "\u2026";
367
418
  }
368
419
 
369
420
  // src/commands/watch.ts
370
- import chalk2 from "chalk";
371
- import ora from "ora";
421
+ function registerWatchCommand(program2) {
422
+ program2.command("watch [session-id]").description("Watch a session and auto-resume when it stops").option("-s, --steps <n>", "Max autopilot continues per resume", "30").option("-r, --max-resumes <n>", "Max number of resumes", "10").option("-c, --cooldown <n>", "Seconds between resumes", "10").option("-m, --message <msg>", "Message to send on resume").action(async (sid, opts) => {
423
+ try {
424
+ await watchCommand(sid, {
425
+ steps: parseInt(opts.steps, 10),
426
+ maxResumes: parseInt(opts.maxResumes, 10),
427
+ cooldown: parseInt(opts.cooldown, 10),
428
+ message: opts.message
429
+ });
430
+ } catch (err) {
431
+ fail(`Watch error: ${err instanceof Error ? err.message : err}`);
432
+ process.exit(1);
433
+ }
434
+ });
435
+ }
372
436
  async function watchCommand(sid, opts) {
373
437
  assertCopilot();
374
438
  if (!sid) {
@@ -377,7 +441,7 @@ async function watchCommand(sid, opts) {
377
441
  fail("No incomplete session found.");
378
442
  process.exit(1);
379
443
  }
380
- log(`Auto-detected incomplete session: ${chalk2.cyan(sid)}`);
444
+ info(`Auto-detected incomplete session: ${CYAN}${sid}${RESET}`);
381
445
  }
382
446
  if (!validateSession(sid)) {
383
447
  fail(`Invalid session: ${sid}`);
@@ -389,13 +453,10 @@ async function watchCommand(sid, opts) {
389
453
  }
390
454
  let resumes = 0;
391
455
  while (resumes < opts.maxResumes) {
392
- const pid = await findPidForSession(sid);
456
+ const pid = findPidForSession(sid);
393
457
  if (pid) {
394
- const spinner = ora(
395
- `Watching PID ${pid} for session ${chalk2.cyan(sid.slice(0, 8))}\u2026`
396
- ).start();
458
+ info(`Watching PID ${pid} for session ${CYAN}${sid.slice(0, 8)}${RESET}\u2026`);
397
459
  const exited = await waitForExit(pid);
398
- spinner.stop();
399
460
  if (!exited) {
400
461
  warn("Timeout waiting for process exit.");
401
462
  break;
@@ -403,19 +464,25 @@ async function watchCommand(sid, opts) {
403
464
  }
404
465
  await sleep2(3e3);
405
466
  if (hasTaskComplete(sid)) {
406
- ok(
407
- `Task complete! Summary: ${getSessionSummary(sid) || "none"}`
408
- );
467
+ ok(`Task complete! Summary: ${getSessionSummary(sid) || "none"}`);
409
468
  notify("Task completed!", `Session ${sid.slice(0, 8)}`);
410
469
  return;
411
470
  }
412
471
  resumes++;
413
- log(
414
- `Session interrupted (${getLastEvent(sid)}). Resume ${resumes}/${opts.maxResumes}\u2026`
472
+ log(`Session interrupted (${getLastEvent(sid)}). Resume ${resumes}/${opts.maxResumes}\u2026`);
473
+ if (opts.cooldown > 0 && resumes > 1) {
474
+ info(`Cooldown ${opts.cooldown}s...`);
475
+ await sleep2(opts.cooldown * 1e3);
476
+ }
477
+ const cwd = getSessionCwd(sid) || void 0;
478
+ const result = await runCopilotResume(
479
+ sid,
480
+ opts.steps,
481
+ opts.message ?? "Continue remaining work. Pick up where you left off and complete the task.",
482
+ cwd
415
483
  );
416
- const result = await runCopilotResume(sid, opts.steps, opts.message);
417
484
  if (result.sessionId && result.sessionId !== sid) {
418
- log(`New session created: ${chalk2.cyan(result.sessionId)}`);
485
+ info(`New session created: ${CYAN}${result.sessionId}${RESET}`);
419
486
  sid = result.sessionId;
420
487
  }
421
488
  }
@@ -426,13 +493,9 @@ function sleep2(ms) {
426
493
  return new Promise((r) => setTimeout(r, ms));
427
494
  }
428
495
 
429
- // src/commands/run.ts
430
- import chalk3 from "chalk";
431
- import ora2 from "ora";
432
-
433
496
  // src/lib/detect.ts
434
497
  import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
435
- import { join as join2, basename, resolve as resolve2 } from "path";
498
+ import { join as join2, basename, resolve as resolve3 } from "path";
436
499
  import { execSync as execSync3 } from "child_process";
437
500
  function detectProjectType(dir) {
438
501
  const exists = (f) => existsSync2(join2(dir, f));
@@ -477,7 +540,28 @@ function detectProjectName(dir) {
477
540
  if (pkg.name) return pkg.name;
478
541
  } catch {
479
542
  }
480
- return basename(resolve2(dir));
543
+ return basename(resolve3(dir));
544
+ }
545
+ function detectMainBranch(dir) {
546
+ try {
547
+ const ref = execSync3("git symbolic-ref refs/remotes/origin/HEAD", {
548
+ cwd: dir,
549
+ encoding: "utf-8",
550
+ stdio: ["pipe", "pipe", "pipe"]
551
+ }).trim();
552
+ return ref.split("/").pop() ?? "main";
553
+ } catch {
554
+ }
555
+ try {
556
+ const branch = execSync3("git branch --show-current", {
557
+ cwd: dir,
558
+ encoding: "utf-8",
559
+ stdio: ["pipe", "pipe", "pipe"]
560
+ }).trim();
561
+ if (branch) return branch;
562
+ } catch {
563
+ }
564
+ return "main";
481
565
  }
482
566
 
483
567
  // src/lib/tasks.ts
@@ -634,7 +718,7 @@ async function withLock(name, fn) {
634
718
 
635
719
  // src/lib/git.ts
636
720
  import { existsSync as existsSync4 } from "fs";
637
- import { join as join4 } from "path";
721
+ import { join as join4, resolve as resolve4 } from "path";
638
722
  import { execSync as execSync4 } from "child_process";
639
723
  function gitExec(dir, cmd) {
640
724
  try {
@@ -659,16 +743,41 @@ function gitStash(dir) {
659
743
  function gitCheckout(dir, branch) {
660
744
  return gitExec(dir, `git checkout ${branch} -q`) !== null;
661
745
  }
746
+ function gitCreateBranch(dir, branch) {
747
+ return gitExec(dir, `git checkout -b ${branch}`) !== null;
748
+ }
749
+ function gitCountCommits(dir, from, to) {
750
+ const result = gitExec(dir, `git log ${from}..${to} --oneline`);
751
+ if (!result) return 0;
752
+ return result.split("\n").filter((l) => l.trim()).length;
753
+ }
662
754
  function gitStatus(dir) {
663
755
  return gitExec(dir, "git status --porcelain") ?? "";
664
756
  }
665
757
 
666
758
  // src/commands/run.ts
759
+ function registerRunCommand(program2) {
760
+ program2.command("run [dir]").description("Discover and fix issues in a project").option("-s, --steps <n>", "Max autopilot continues per task", "30").option("-t, --max-tasks <n>", "Max number of tasks to run", "5").option("-p, --max-premium <n>", "Max total premium requests", "50").option("--dry-run", "Show tasks without executing").action(async (dir, opts) => {
761
+ try {
762
+ await runCommand(dir ?? process.cwd(), {
763
+ steps: parseInt(opts.steps, 10),
764
+ maxTasks: parseInt(opts.maxTasks, 10),
765
+ maxPremium: parseInt(opts.maxPremium, 10),
766
+ dryRun: opts.dryRun ?? false
767
+ });
768
+ } catch (err) {
769
+ fail(`Run error: ${err instanceof Error ? err.message : err}`);
770
+ process.exit(1);
771
+ }
772
+ });
773
+ }
667
774
  async function runCommand(dir, opts) {
668
775
  assertCopilot();
669
776
  const projectType = detectProjectType(dir);
670
777
  const name = detectProjectName(dir);
671
- log(`Project: ${chalk3.cyan(name)} (${projectType})`);
778
+ const mainBranch = isGitRepo(dir) ? detectMainBranch(dir) : null;
779
+ info(`Project: ${CYAN}${name}${RESET} (${projectType})`);
780
+ if (mainBranch) info(`Main branch: ${mainBranch}`);
672
781
  const tasks = getTasksForProject(projectType).slice(0, opts.maxTasks);
673
782
  if (tasks.length === 0) {
674
783
  warn("No tasks found for this project type.");
@@ -676,594 +785,244 @@ async function runCommand(dir, opts) {
676
785
  }
677
786
  log(`Found ${tasks.length} tasks:`);
678
787
  for (const t of tasks) {
679
- console.log(` ${chalk3.dim("\u2022")} ${t.title}`);
788
+ log(` ${DIM}\u2022${RESET} ${t.title}`);
680
789
  }
681
790
  if (opts.dryRun) {
682
- log(chalk3.dim("(dry-run \u2014 not executing)"));
791
+ log(`${DIM}(dry-run \u2014 not executing)${RESET}`);
683
792
  return;
684
793
  }
685
794
  const originalBranch = isGitRepo(dir) ? gitCurrentBranch(dir) : null;
686
795
  let completed = 0;
687
796
  let premiumTotal = 0;
688
797
  for (const task of tasks) {
798
+ if (premiumTotal >= opts.maxPremium) {
799
+ warn(`Premium request limit reached (${premiumTotal}/${opts.maxPremium}).`);
800
+ break;
801
+ }
689
802
  log(`
690
803
  ${"\u2550".repeat(60)}`);
691
- log(`Task: ${chalk3.bold(task.title)}`);
804
+ log(`${BOLD}${CYAN}Task: ${task.title}${RESET}`);
692
805
  log(`${"\u2550".repeat(60)}`);
693
- if (originalBranch && isGitRepo(dir)) {
694
- const branch = `copilot-agent/${task.title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
695
- try {
696
- if (gitStatus(dir)) gitStash(dir);
697
- gitCheckout(dir, branch);
698
- } catch (e) {
699
- warn(`Could not create branch ${branch}, continuing on current.`);
806
+ const timestamp = Date.now().toString(36);
807
+ const random = Math.random().toString(36).substring(2, 6);
808
+ const branchName = `agent/fix-${completed + 1}-${timestamp}-${random}`;
809
+ if (mainBranch && isGitRepo(dir)) {
810
+ if (gitStatus(dir)) gitStash(dir);
811
+ gitCheckout(dir, mainBranch);
812
+ if (!gitCreateBranch(dir, branchName)) {
813
+ warn(`Could not create branch ${branchName}, continuing on current.`);
700
814
  }
701
815
  }
702
- const spinner = ora2(`Running: ${task.title}\u2026`).start();
816
+ info(`Running: ${task.title}\u2026`);
703
817
  const result = await withLock(
704
818
  "copilot-run",
705
- () => runCopilotTask(task.prompt, opts.steps)
819
+ () => runCopilotTask(task.prompt, opts.steps, dir)
706
820
  );
707
- spinner.stop();
821
+ const commits = mainBranch ? gitCountCommits(dir, mainBranch, "HEAD") : 0;
708
822
  premiumTotal += result.premium;
709
823
  completed++;
710
- ok(`${task.title} \u2014 exit ${result.exitCode}, premium: ${result.premium}`);
824
+ ok(`${task.title} \u2014 ${commits} commit(s), ${result.premium} premium`);
711
825
  if (originalBranch && isGitRepo(dir)) {
712
- try {
713
- gitCheckout(dir, originalBranch);
714
- } catch {
715
- }
826
+ gitCheckout(dir, mainBranch ?? originalBranch);
716
827
  }
717
828
  }
718
829
  log(`
719
- Completed ${completed}/${tasks.length} tasks. Total premium: ${premiumTotal}`);
830
+ ${BOLD}\u2550\u2550\u2550 Run Summary \u2550\u2550\u2550${RESET}`);
831
+ log(`Completed ${completed}/${tasks.length} tasks. Total premium: ${premiumTotal}`);
720
832
  notify(`Completed ${completed} tasks`, name);
721
833
  }
722
834
 
723
835
  // src/commands/overnight.ts
724
- import chalk4 from "chalk";
725
- import ora3 from "ora";
726
836
  import { join as join5 } from "path";
727
837
  import { homedir as homedir3 } from "os";
838
+ function registerOvernightCommand(program2) {
839
+ program2.command("overnight [dir]").description("Run tasks continuously until a deadline").option("-u, --until <HH>", "Stop at this hour (24h format)", "07").option("-s, --steps <n>", "Max autopilot continues per task", "50").option("-c, --cooldown <n>", "Seconds between tasks", "15").option("-p, --max-premium <n>", "Max premium requests budget", "300").option("--dry-run", "Show plan without executing").action(async (dir, opts) => {
840
+ try {
841
+ await overnightCommand(dir ?? process.cwd(), {
842
+ until: parseInt(opts.until, 10),
843
+ steps: parseInt(opts.steps, 10),
844
+ cooldown: parseInt(opts.cooldown, 10),
845
+ maxPremium: parseInt(opts.maxPremium, 10),
846
+ dryRun: opts.dryRun ?? false
847
+ });
848
+ } catch (err) {
849
+ fail(`Overnight error: ${err instanceof Error ? err.message : err}`);
850
+ process.exit(1);
851
+ }
852
+ });
853
+ }
854
+ function isPastDeadline(untilHour) {
855
+ const hour = (/* @__PURE__ */ new Date()).getHours();
856
+ return hour >= untilHour && hour < 20;
857
+ }
728
858
  async function overnightCommand(dir, opts) {
729
859
  assertCopilot();
730
860
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").slice(0, 15);
731
- const logPath = join5(
732
- homedir3(),
733
- ".copilot",
734
- "auto-resume-logs",
735
- `overnight-${ts}.log`
736
- );
861
+ const logPath = join5(homedir3(), ".copilot", "auto-resume-logs", `overnight-${ts}.log`);
737
862
  setLogFile(logPath);
738
- const deadline = parseDeadline(opts.until);
739
863
  const name = detectProjectName(dir);
740
864
  const projectType = detectProjectType(dir);
741
- log(`Overnight runner for ${chalk4.cyan(name)} (${projectType})`);
742
- log(`Deadline: ${opts.until} (${msToHuman(deadline - Date.now())} from now)`);
743
- log(`Max premium: ${opts.maxPremium}, Steps: ${opts.steps}`);
744
- log(`Log: ${logPath}`);
865
+ const mainBranch = isGitRepo(dir) ? detectMainBranch(dir) : null;
866
+ info(`Overnight runner for ${CYAN}${name}${RESET} (${projectType})`);
867
+ info(`Deadline: ${String(opts.until).padStart(2, "0")}:00`);
868
+ info(`Max premium: ${opts.maxPremium}, Steps: ${opts.steps}`);
869
+ info(`Log: ${logPath}`);
870
+ const tasks = getTasksForProject(projectType);
745
871
  if (opts.dryRun) {
746
- const tasks2 = getTasksForProject(projectType);
747
872
  log(`
748
- Would run ${tasks2.length} tasks:`);
749
- for (const t of tasks2) console.log(` ${chalk4.dim("\u2022")} ${t.title}`);
873
+ Would run ${tasks.length} tasks:`);
874
+ for (const t of tasks) log(` ${DIM}\u2022${RESET} ${t.title}`);
750
875
  return;
751
876
  }
752
- const tasks = getTasksForProject(projectType);
877
+ const existingSession = findLatestIncomplete();
878
+ if (existingSession && validateSession(existingSession)) {
879
+ info(`Found incomplete session: ${existingSession}`);
880
+ const pid = findPidForSession(existingSession);
881
+ if (pid) {
882
+ info(`Waiting for running copilot (PID ${pid})...`);
883
+ await waitForExit(pid);
884
+ }
885
+ if (!hasTaskComplete(existingSession) && !isPastDeadline(opts.until)) {
886
+ info("Resuming incomplete session...");
887
+ const cwd = getSessionCwd(existingSession) || dir;
888
+ await runCopilotResume(
889
+ existingSession,
890
+ opts.steps,
891
+ "Continue remaining work. Complete the task.",
892
+ cwd
893
+ );
894
+ }
895
+ }
896
+ const originalBranch = isGitRepo(dir) ? gitCurrentBranch(dir) : null;
753
897
  let taskIdx = 0;
754
898
  let totalPremium = 0;
755
- let completedTasks = 0;
756
- let cycle = 0;
757
- while (Date.now() < deadline) {
899
+ let totalCommits = 0;
900
+ while (!isPastDeadline(opts.until) && taskIdx < tasks.length) {
758
901
  if (totalPremium >= opts.maxPremium) {
759
902
  warn(`Premium budget exhausted: ${totalPremium}/${opts.maxPremium}`);
760
903
  break;
761
904
  }
762
905
  const task = tasks[taskIdx % tasks.length];
763
- cycle++;
764
906
  taskIdx++;
765
907
  log(`
766
908
  ${"\u2550".repeat(60)}`);
767
- log(
768
- `Cycle ${cycle} | Task: ${chalk4.bold(task.title)} | Premium: ${totalPremium}/${opts.maxPremium}`
769
- );
770
- log(`Time remaining: ${msToHuman(deadline - Date.now())}`);
909
+ log(`${BOLD}${CYAN}[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Task ${taskIdx}: ${task.title}${RESET}`);
910
+ log(`${DIM}Premium: ${totalPremium}/${opts.maxPremium}${RESET}`);
771
911
  log(`${"\u2550".repeat(60)}`);
772
- const spinner = ora3(`Running: ${task.title}\u2026`).start();
912
+ const timestamp = Date.now().toString(36);
913
+ const random = Math.random().toString(36).substring(2, 6);
914
+ const branchName = `agent/overnight-${taskIdx}-${timestamp}-${random}`;
915
+ if (mainBranch && isGitRepo(dir)) {
916
+ gitStash(dir);
917
+ gitCheckout(dir, mainBranch);
918
+ gitCreateBranch(dir, branchName);
919
+ }
920
+ info(`Running: ${task.title}\u2026`);
773
921
  try {
774
922
  const result = await withLock(
775
923
  "copilot-overnight",
776
- () => runCopilotTask(task.prompt, opts.steps)
924
+ () => runCopilotTask(task.prompt, opts.steps, dir)
777
925
  );
778
- spinner.stop();
926
+ const commits = mainBranch ? gitCountCommits(dir, mainBranch, "HEAD") : 0;
779
927
  totalPremium += result.premium;
780
- completedTasks++;
781
- ok(
782
- `${task.title} \u2014 exit ${result.exitCode}, premium: ${result.premium}`
783
- );
928
+ totalCommits += commits;
929
+ if (commits > 0) {
930
+ ok(`${commits} commit(s) on ${branchName}`);
931
+ } else {
932
+ log(`${DIM}No commits on ${branchName}${RESET}`);
933
+ }
784
934
  } catch (err) {
785
- spinner.stop();
786
935
  fail(`Task failed: ${err}`);
787
936
  }
788
- if (Date.now() < deadline) {
789
- log("Cooldown 30s\u2026");
790
- await sleep3(3e4);
937
+ if (mainBranch && isGitRepo(dir)) {
938
+ gitCheckout(dir, mainBranch);
791
939
  }
940
+ if (!isPastDeadline(opts.until)) {
941
+ info(`Cooldown ${opts.cooldown}s\u2026`);
942
+ await sleep3(opts.cooldown * 1e3);
943
+ }
944
+ }
945
+ if (originalBranch && isGitRepo(dir)) {
946
+ gitCheckout(dir, originalBranch);
792
947
  }
793
- const summary = `Overnight done. ${completedTasks} tasks, ${totalPremium} premium.`;
794
- log(summary);
948
+ const summary = `Overnight done \u2014 ${taskIdx} tasks, ${totalCommits} commits, ${totalPremium} premium.`;
949
+ ok(summary);
795
950
  notify(summary, name);
796
951
  }
797
- function parseDeadline(hhmm) {
798
- const [h, m] = hhmm.split(":").map(Number);
799
- const now = /* @__PURE__ */ new Date();
800
- const target = new Date(now);
801
- target.setHours(h, m, 0, 0);
802
- if (target <= now) target.setDate(target.getDate() + 1);
803
- return target.getTime();
804
- }
805
- function msToHuman(ms) {
806
- const h = Math.floor(ms / 36e5);
807
- const m = Math.floor(ms % 36e5 / 6e4);
808
- return `${h}h ${m}m`;
809
- }
810
952
  function sleep3(ms) {
811
953
  return new Promise((r) => setTimeout(r, ms));
812
954
  }
813
955
 
814
956
  // src/commands/research.ts
815
- import chalk5 from "chalk";
816
- var RESEARCH_PROMPTS = [
817
- {
818
- title: "Dependency updates",
819
- prompt: "Research the latest versions of all dependencies. Check changelogs for breaking changes. Create a summary of what can be safely updated."
820
- },
821
- {
822
- title: "Performance review",
823
- prompt: "Profile the application for performance bottlenecks. Check startup time, memory usage, and hot paths. Suggest optimizations with benchmarks."
824
- },
825
- {
826
- title: "Architecture review",
827
- prompt: "Review the project architecture. Check for code smells, circular dependencies, and coupling issues. Suggest improvements following clean architecture."
828
- },
829
- {
830
- title: "Accessibility audit",
831
- prompt: "Audit the UI for accessibility issues. Check color contrast, screen reader support, and keyboard navigation. Create a report."
832
- },
833
- {
834
- title: "Best practices",
835
- prompt: "Compare the codebase against current best practices for this framework/language. Identify gaps and suggest improvements."
836
- }
837
- ];
838
- async function researchCommand(topic, opts) {
839
- assertCopilot();
840
- if (topic) {
841
- log(`Research topic: ${chalk5.cyan(topic)}`);
842
- const result = await withLock(
843
- "copilot-research",
844
- () => runCopilotTask(
845
- `Research the following topic and create a detailed report: ${topic}`,
846
- opts.steps
847
- )
848
- );
849
- ok(`Research complete \u2014 premium: ${result.premium}`);
850
- notify("Research complete", topic.slice(0, 30));
851
- return;
852
- }
853
- log("Running predefined research tasks\u2026");
854
- for (const r of RESEARCH_PROMPTS) {
855
- log(`
856
- ${chalk5.bold(r.title)}`);
857
- try {
858
- const result = await withLock(
859
- "copilot-research",
860
- () => runCopilotTask(r.prompt, opts.steps)
861
- );
862
- ok(`${r.title} \u2014 premium: ${result.premium}`);
863
- } catch (err) {
864
- fail(`${r.title} failed: ${err}`);
865
- }
866
- }
867
- notify("All research tasks complete");
868
- }
869
-
870
- // src/commands/daemon.ts
871
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync2, unlinkSync, statSync as statSync2 } from "fs";
872
- import { join as join6 } from "path";
957
+ import { existsSync as existsSync5, copyFileSync, mkdirSync as mkdirSync3 } from "fs";
958
+ import { join as join6, resolve as resolve5 } from "path";
873
959
  import { homedir as homedir4 } from "os";
874
- import { spawn as spawn2 } from "child_process";
875
- var PID_FILE = join6(homedir4(), ".copilot", "watchdog.pid");
876
- var LOG_FILE = join6(homedir4(), ".copilot", "auto-resume-logs", "watchdog.log");
877
- var SESSION_DIR2 = join6(homedir4(), ".copilot", "session-state");
878
- async function daemonCommand(action, opts) {
879
- switch (action) {
880
- case "start":
881
- return startDaemon(opts);
882
- case "stop":
883
- return stopDaemon();
884
- case "status":
885
- return statusDaemon();
886
- case "logs":
887
- return showLogs();
888
- default:
889
- fail(`Unknown action: ${action}. Use: start, stop, status, logs`);
890
- process.exit(1);
891
- }
892
- }
893
- function isDaemonRunning() {
894
- if (!existsSync5(PID_FILE)) return { running: false };
895
- try {
896
- const pid = parseInt(readFileSync4(PID_FILE, "utf-8").trim());
897
- process.kill(pid, 0);
898
- return { running: true, pid };
899
- } catch {
900
- return { running: false };
901
- }
902
- }
903
- async function startDaemon(opts) {
904
- assertCopilot();
905
- const { running, pid } = isDaemonRunning();
906
- if (running) {
907
- fail(`Watchdog already running (PID: ${pid}). Use 'copilot-agent daemon stop' first.`);
908
- process.exit(1);
909
- }
910
- if (existsSync5(PID_FILE)) {
911
- unlinkSync(PID_FILE);
912
- }
913
- log(`\u{1F415} Starting watchdog daemon (poll: ${opts.poll}s, auto-resume: ${opts.resume})`);
914
- log(` Log: ${LOG_FILE}`);
915
- const child = spawn2(
916
- process.execPath,
917
- [
918
- process.argv[1],
919
- "daemon",
920
- "_loop",
921
- "--poll",
922
- String(opts.poll),
923
- "--idle",
924
- String(opts.idle),
925
- "--steps",
926
- String(opts.steps),
927
- ...opts.resume ? ["--resume"] : []
928
- ],
929
- {
930
- detached: true,
931
- stdio: "ignore"
932
- }
933
- );
934
- child.unref();
935
- if (child.pid) {
936
- writeFileSync2(PID_FILE, String(child.pid));
937
- ok(`Watchdog started (PID: ${child.pid})`);
938
- } else {
939
- fail("Failed to start watchdog");
940
- }
941
- }
942
- async function stopDaemon() {
943
- const { running, pid } = isDaemonRunning();
944
- if (!running) {
945
- warn("Watchdog is not running.");
946
- return;
947
- }
948
- try {
949
- process.kill(pid, "SIGTERM");
950
- ok(`Watchdog stopped (PID: ${pid})`);
951
- } catch {
952
- fail(`Could not stop PID ${pid}`);
953
- }
954
- try {
955
- unlinkSync(PID_FILE);
956
- } catch {
957
- }
958
- }
959
- async function statusDaemon() {
960
- const { running, pid } = isDaemonRunning();
961
- if (running) {
962
- ok(`Watchdog running (PID: ${pid})`);
963
- if (existsSync5(LOG_FILE)) {
964
- const stat = statSync2(LOG_FILE);
965
- log(`Log: ${LOG_FILE} (${(stat.size / 1024).toFixed(1)} KB)`);
966
- }
967
- } else {
968
- log("Watchdog is not running.");
969
- }
970
- }
971
- async function showLogs() {
972
- if (!existsSync5(LOG_FILE)) {
973
- log("No log file found.");
974
- return;
975
- }
976
- const lines = readFileSync4(LOG_FILE, "utf-8").trimEnd().split("\n");
977
- const tail = lines.slice(-30);
978
- for (const line of tail) console.log(line);
979
- }
980
- async function daemonLoop(opts) {
981
- setLogFile(LOG_FILE);
982
- log(`\u{1F415} Watchdog daemon loop started (PID: ${process.pid})`);
983
- let lastAlertedSid = "";
984
- const shutdown = () => {
985
- log("Watchdog shutting down");
986
- try {
987
- unlinkSync(PID_FILE);
988
- } catch {
989
- }
990
- process.exit(0);
991
- };
992
- process.on("SIGTERM", shutdown);
993
- process.on("SIGINT", shutdown);
994
- while (true) {
995
- await sleep4(opts.poll * 1e3);
960
+ function registerResearchCommand(program2) {
961
+ program2.command("research [project]").description("Research improvements or a specific topic").option("-s, --steps <n>", "Max autopilot continues", "50").action(async (project, opts) => {
996
962
  try {
997
- const sid = getLatestSessionId();
998
- if (!sid || !validateSession(sid)) continue;
999
- if (sid === lastAlertedSid && hasTaskComplete(sid)) continue;
1000
- const pid = await findPidForSession(sid);
1001
- if (pid) continue;
1002
- if (hasTaskComplete(sid)) {
1003
- if (sid !== lastAlertedSid) {
1004
- log(`\u2705 Session ${sid.slice(0, 8)} completed: ${getSessionSummary(sid).slice(0, 60)}`);
1005
- notify("Task completed", `Session ${sid.slice(0, 8)}`);
1006
- lastAlertedSid = sid;
1007
- }
1008
- continue;
1009
- }
1010
- const eventsPath = join6(SESSION_DIR2, sid, "events.jsonl");
1011
- const mtime = statSync2(eventsPath).mtimeMs;
1012
- const idleMinutes = (Date.now() - mtime) / 6e4;
1013
- if (idleMinutes < opts.idle) continue;
1014
- log(`\u23F8\uFE0F Session ${sid.slice(0, 8)} idle ${idleMinutes.toFixed(0)}m (last: ${getLastEvent(sid)})`);
1015
- if (opts.resume) {
1016
- log(`\u{1F504} Auto-resuming session ${sid.slice(0, 8)}\u2026`);
1017
- lastAlertedSid = sid;
1018
- try {
1019
- await withLock("watchdog-resume", async () => {
1020
- const result = await runCopilotResume(sid, opts.steps);
1021
- ok(`Resume done \u2014 exit ${result.exitCode}, premium: ${result.premium}`);
1022
- });
1023
- } catch (err) {
1024
- fail(`Resume failed: ${err}`);
1025
- }
1026
- } else {
1027
- if (sid !== lastAlertedSid) {
1028
- warn(`Session ${sid.slice(0, 8)} needs attention (idle ${idleMinutes.toFixed(0)}m)`);
1029
- notify("Session interrupted", `${sid.slice(0, 8)} idle ${idleMinutes.toFixed(0)}m`);
1030
- lastAlertedSid = sid;
1031
- }
1032
- }
963
+ await researchCommand(project ?? process.cwd(), {
964
+ steps: parseInt(opts.steps, 10)
965
+ });
1033
966
  } catch (err) {
1034
- fail(`Watchdog error: ${err}`);
1035
- }
1036
- }
1037
- }
1038
- function sleep4(ms) {
1039
- return new Promise((r) => setTimeout(r, ms));
1040
- }
1041
-
1042
- // src/commands/multi.ts
1043
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1044
- import { join as join7, resolve as resolve3, basename as basename2 } from "path";
1045
- import { homedir as homedir5 } from "os";
1046
- import chalk6 from "chalk";
1047
- var PROJECTS_FILE = join7(homedir5(), ".copilot", "autonomous-projects.txt");
1048
- var LOG_DIR = join7(homedir5(), ".copilot", "auto-resume-logs");
1049
- async function multiCommand(action, args, opts) {
1050
- ensureFiles();
1051
- switch (action) {
1052
- case "add":
1053
- return addProject(args[0]);
1054
- case "remove":
1055
- return removeProject(args[0]);
1056
- case "list":
1057
- return listProjects();
1058
- case "health":
1059
- case "research":
1060
- return runAll(action, opts);
1061
- default:
1062
- fail(`Unknown action: ${action}. Use: add, remove, list, health, research`);
967
+ fail(`Research error: ${err instanceof Error ? err.message : err}`);
1063
968
  process.exit(1);
1064
- }
1065
- }
1066
- function ensureFiles() {
1067
- mkdirSync3(LOG_DIR, { recursive: true });
1068
- if (!existsSync6(PROJECTS_FILE)) writeFileSync3(PROJECTS_FILE, "");
1069
- }
1070
- function readProjects() {
1071
- return readFileSync5(PROJECTS_FILE, "utf-8").split("\n").map((l) => l.trim()).filter(Boolean);
1072
- }
1073
- function writeProjects(projects) {
1074
- writeFileSync3(PROJECTS_FILE, projects.join("\n") + "\n");
1075
- }
1076
- async function addProject(path) {
1077
- if (!path) {
1078
- fail("Usage: copilot-agent multi add <path>");
1079
- process.exit(1);
1080
- }
1081
- const resolved = resolve3(path);
1082
- if (!existsSync6(resolved)) {
1083
- fail(`Not found: ${resolved}`);
1084
- process.exit(1);
1085
- }
1086
- await withLock("projects-file", async () => {
1087
- const projects = readProjects();
1088
- if (projects.includes(resolved)) {
1089
- warn(`Already registered: ${resolved}`);
1090
- return;
1091
- }
1092
- projects.push(resolved);
1093
- writeProjects(projects);
1094
- ok(`Added: ${resolved}`);
1095
- });
1096
- }
1097
- async function removeProject(path) {
1098
- if (!path) {
1099
- fail("Usage: copilot-agent multi remove <path>");
1100
- process.exit(1);
1101
- }
1102
- const resolved = resolve3(path);
1103
- await withLock("projects-file", async () => {
1104
- const projects = readProjects();
1105
- const filtered = projects.filter((p) => p !== resolved);
1106
- if (filtered.length === projects.length) {
1107
- warn(`Not registered: ${resolved}`);
1108
- return;
1109
969
  }
1110
- writeProjects(filtered);
1111
- ok(`Removed: ${resolved}`);
1112
970
  });
1113
971
  }
1114
- function listProjects() {
1115
- const projects = readProjects();
1116
- if (projects.length === 0) {
1117
- log("No projects registered. Add: copilot-agent multi add <path>");
1118
- return;
1119
- }
1120
- console.log(chalk6.bold("\nRegistered projects:"));
1121
- for (let i = 0; i < projects.length; i++) {
1122
- const exists = existsSync6(projects[i]);
1123
- const icon = exists ? chalk6.green("\u2705") : chalk6.red("\u274C");
1124
- const type = exists ? detectProjectType(projects[i]) : "?";
1125
- console.log(` ${i + 1}. ${icon} ${projects[i]} ${chalk6.dim(`(${type})`)}`);
1126
- }
1127
- console.log();
972
+ function buildResearchPrompt(projectType, projectName) {
973
+ return `You are a senior software architect. Analyze this ${projectType} project "${projectName}" thoroughly.
974
+
975
+ Research and produce a file called RESEARCH-PROPOSALS.md with:
976
+
977
+ 1. **Architecture Assessment** \u2014 Current architecture, patterns used, strengths and weaknesses
978
+ 2. **Code Quality Report** \u2014 Common issues, anti-patterns, technical debt areas
979
+ 3. **Security Audit** \u2014 Potential vulnerabilities, dependency risks, configuration issues
980
+ 4. **Performance Analysis** \u2014 Bottlenecks, optimization opportunities, resource usage
981
+ 5. **Testing Gap Analysis** \u2014 Untested areas, test quality, coverage recommendations
982
+ 6. **Improvement Proposals** \u2014 Prioritized list of actionable improvements with effort estimates
983
+
984
+ For each proposal, include:
985
+ - Priority (P0/P1/P2)
986
+ - Estimated effort (hours)
987
+ - Impact description
988
+ - Suggested implementation approach
989
+
990
+ Write RESEARCH-PROPOSALS.md in the project root.`;
1128
991
  }
1129
- async function runAll(mode, opts) {
992
+ async function researchCommand(dir, opts) {
1130
993
  assertCopilot();
1131
- const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").slice(0, 15);
1132
- setLogFile(join7(LOG_DIR, `multi-${mode}-${ts}.log`));
1133
- const projects = readProjects();
1134
- if (projects.length === 0) {
1135
- fail("No projects registered. Add: copilot-agent multi add <path>");
1136
- process.exit(1);
1137
- }
1138
- log(`\u{1F3ED} Multi-project ${mode} \u2014 ${projects.length} projects`);
1139
- if (opts.dryRun) {
1140
- for (const p of projects) {
1141
- const type = existsSync6(p) ? detectProjectType(p) : "unknown";
1142
- const tasks = getTasksForProject(type);
1143
- console.log(`
1144
- ${chalk6.bold(basename2(p))} (${type})`);
1145
- for (const t of tasks.slice(0, 3)) {
1146
- console.log(` ${chalk6.dim("\u2022")} ${t.title}`);
1147
- }
1148
- }
1149
- log(chalk6.dim("\n(dry-run \u2014 not executing)"));
1150
- return;
1151
- }
1152
- let total = 0;
1153
- let success = 0;
1154
- let failed = 0;
1155
- let skipped = 0;
1156
- const report = [];
1157
- for (const project of projects) {
1158
- total++;
1159
- if (!existsSync6(project)) {
1160
- warn(`Skipping (not found): ${project}`);
1161
- report.push(`\u23ED ${basename2(project)} (not found)`);
1162
- skipped++;
1163
- continue;
1164
- }
1165
- const name = detectProjectName(project);
1166
- const type = detectProjectType(project);
1167
- log(`
1168
- ${"\u2550".repeat(50)}`);
1169
- log(`${chalk6.bold(name)} (${type}) \u2014 ${total}/${projects.length}`);
1170
- log(`${"\u2550".repeat(50)}`);
1171
- const tasks = mode === "research" ? [{ title: "Research", prompt: "Research latest best practices, dependency updates, and architecture improvements. Create a report.", priority: 1 }] : getTasksForProject(type).slice(0, 3);
1172
- let projectSuccess = true;
1173
- for (const task of tasks) {
1174
- try {
1175
- const result = await withLock(
1176
- "copilot-multi",
1177
- () => runCopilotTask(
1178
- `Project: ${project}
1179
-
1180
- ${task.prompt}`,
1181
- opts.steps
1182
- )
1183
- );
1184
- ok(`${task.title} \u2014 exit ${result.exitCode}, premium: ${result.premium}`);
1185
- } catch (err) {
1186
- fail(`${task.title} failed: ${err}`);
1187
- projectSuccess = false;
1188
- }
1189
- }
1190
- if (projectSuccess) {
1191
- success++;
1192
- report.push(`\u2705 ${name}`);
1193
- } else {
1194
- failed++;
1195
- report.push(`\u274C ${name}`);
1196
- }
1197
- if (total < projects.length) {
1198
- log(`Cooldown ${opts.cooldown}s\u2026`);
1199
- await new Promise((r) => setTimeout(r, opts.cooldown * 1e3));
1200
- }
994
+ const projectDir = resolve5(dir);
995
+ const projectType = detectProjectType(projectDir);
996
+ const projectName = detectProjectName(projectDir);
997
+ info(`Researching: ${CYAN}${projectName}${RESET} (${projectType})`);
998
+ const prompt = buildResearchPrompt(projectType, projectName);
999
+ const result = await withLock(
1000
+ "copilot-research",
1001
+ () => runCopilotTask(prompt, opts.steps, projectDir)
1002
+ );
1003
+ log(`Copilot exited with code ${result.exitCode}`);
1004
+ const proposalsFile = join6(projectDir, "RESEARCH-PROPOSALS.md");
1005
+ if (existsSync5(proposalsFile)) {
1006
+ ok("RESEARCH-PROPOSALS.md generated.");
1007
+ const backupDir = join6(homedir4(), ".copilot", "research-reports");
1008
+ mkdirSync3(backupDir, { recursive: true });
1009
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1010
+ const backupFile = join6(backupDir, `${projectName}-${timestamp}.md`);
1011
+ copyFileSync(proposalsFile, backupFile);
1012
+ ok(`Backup saved: ${backupFile}`);
1013
+ } else {
1014
+ warn("RESEARCH-PROPOSALS.md was not generated. Check copilot output.");
1201
1015
  }
1202
- console.log(`
1203
- ${"\u2550".repeat(50)}`);
1204
- log(`\u{1F4CA} Summary: ${success}/${total} succeeded, ${failed} failed, ${skipped} skipped`);
1205
- for (const line of report) console.log(` ${line}`);
1206
- console.log();
1207
- notify(`Multi-${mode}: ${success}/${total} succeeded`, "copilot-agent");
1016
+ notify("Research complete", projectName);
1208
1017
  }
1209
1018
 
1210
1019
  // src/index.ts
1211
1020
  var program = new Command();
1212
- program.name("copilot-agent").description("Autonomous GitHub Copilot CLI agent \u2014 auto-resume, task discovery, overnight runner").version("0.2.0");
1213
- program.command("status").description("Show copilot session status").option("-l, --limit <n>", "Number of sessions to show", "10").option("-a, --active", "Show only active (running) sessions").action(async (opts) => {
1214
- await statusCommand({
1215
- limit: parseInt(opts.limit),
1216
- active: opts.active ?? false
1217
- });
1218
- });
1219
- program.command("watch [session-id]").description("Watch a session and auto-resume when it stops").option("-s, --steps <n>", "Max autopilot continues per resume", "50").option("-r, --max-resumes <n>", "Max number of resumes", "20").option("-m, --message <msg>", "Message to send on resume").action(async (sid, opts) => {
1220
- await watchCommand(sid, {
1221
- steps: parseInt(opts.steps),
1222
- maxResumes: parseInt(opts.maxResumes),
1223
- message: opts.message
1224
- });
1225
- });
1226
- program.command("run [dir]").description("Discover and fix issues in a project").option("-s, --steps <n>", "Max autopilot continues per task", "30").option("-t, --max-tasks <n>", "Max number of tasks to run", "5").option("--dry-run", "Show tasks without executing").action(async (dir, opts) => {
1227
- await runCommand(dir ?? process.cwd(), {
1228
- steps: parseInt(opts.steps),
1229
- maxTasks: parseInt(opts.maxTasks),
1230
- dryRun: opts.dryRun ?? false
1231
- });
1232
- });
1233
- program.command("overnight [dir]").description("Run tasks continuously until a deadline").option("-u, --until <HH:MM>", "Deadline time (24h format)", "07:00").option("-s, --steps <n>", "Max autopilot continues per task", "50").option("-p, --max-premium <n>", "Max premium requests budget", "300").option("--dry-run", "Show plan without executing").action(async (dir, opts) => {
1234
- await overnightCommand(dir ?? process.cwd(), {
1235
- until: opts.until,
1236
- steps: parseInt(opts.steps),
1237
- maxPremium: parseInt(opts.maxPremium),
1238
- dryRun: opts.dryRun ?? false
1239
- });
1240
- });
1241
- program.command("research [topic]").description("Research improvements or a specific topic").option("-s, --steps <n>", "Max autopilot continues", "30").action(async (topic, opts) => {
1242
- await researchCommand(topic, {
1243
- steps: parseInt(opts.steps)
1244
- });
1245
- });
1246
- var daemon = program.command("daemon <action>").description("Background watchdog daemon (start, stop, status, logs)").option("--poll <n>", "Poll interval in seconds", "20").option("--idle <n>", "Minutes before considering session idle", "5").option("--resume", "Auto-resume interrupted sessions").option("-s, --steps <n>", "Max autopilot continues per resume", "50").action(async (action, opts) => {
1247
- const parsed = {
1248
- poll: parseInt(opts.poll),
1249
- idle: parseInt(opts.idle),
1250
- resume: opts.resume ?? false,
1251
- steps: parseInt(opts.steps)
1252
- };
1253
- if (action === "_loop") {
1254
- await daemonLoop(parsed);
1255
- } else {
1256
- await daemonCommand(action, parsed);
1257
- }
1258
- });
1259
- var multi = program.command("multi <action> [args...]").description("Multi-project orchestrator (add, remove, list, health, research)").option("-s, --steps <n>", "Max autopilot continues per task", "30").option("-c, --cooldown <n>", "Seconds between projects", "60").option("-p, --max-premium <n>", "Max premium per project", "30").option("--dry-run", "Show plan without executing").action(async (action, args, opts) => {
1260
- await multiCommand(action, args, {
1261
- mode: action,
1262
- cooldown: parseInt(opts.cooldown),
1263
- steps: parseInt(opts.steps),
1264
- maxPremium: parseInt(opts.maxPremium),
1265
- dryRun: opts.dryRun ?? false
1266
- });
1267
- });
1021
+ program.name("copilot-agent").version("0.4.0").description("Autonomous GitHub Copilot CLI agent \u2014 auto-resume, task discovery, overnight runs");
1022
+ registerStatusCommand(program);
1023
+ registerWatchCommand(program);
1024
+ registerRunCommand(program);
1025
+ registerOvernightCommand(program);
1026
+ registerResearchCommand(program);
1268
1027
  program.parse();
1269
1028
  //# sourceMappingURL=index.js.map