copilot-agent 0.2.0 → 0.3.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 +315 -571
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
@@ -148,6 +143,9 @@ import { execSync } from "child_process";
|
|
|
148
143
|
var RED = "\x1B[31m";
|
|
149
144
|
var GREEN = "\x1B[32m";
|
|
150
145
|
var YELLOW = "\x1B[33m";
|
|
146
|
+
var CYAN = "\x1B[36m";
|
|
147
|
+
var DIM = "\x1B[2m";
|
|
148
|
+
var BOLD = "\x1B[1m";
|
|
151
149
|
var RESET = "\x1B[0m";
|
|
152
150
|
|
|
153
151
|
// src/lib/logger.ts
|
|
@@ -186,6 +184,11 @@ function fail(msg) {
|
|
|
186
184
|
console.error(out);
|
|
187
185
|
writeToFile(`\u2716 ${msg}`);
|
|
188
186
|
}
|
|
187
|
+
function info(msg) {
|
|
188
|
+
const out = `${CYAN}\u2139 ${msg}${RESET}`;
|
|
189
|
+
console.log(out);
|
|
190
|
+
writeToFile(`\u2139 ${msg}`);
|
|
191
|
+
}
|
|
189
192
|
function notify(message, title = "copilot-agent") {
|
|
190
193
|
try {
|
|
191
194
|
if (process.platform === "darwin") {
|
|
@@ -226,16 +229,20 @@ function findCopilotProcesses() {
|
|
|
226
229
|
try {
|
|
227
230
|
const output = execSync2("ps -eo pid,command", { encoding: "utf-8" });
|
|
228
231
|
const results = [];
|
|
232
|
+
const myPid = process.pid;
|
|
233
|
+
const parentPid = process.ppid;
|
|
229
234
|
for (const line of output.split("\n")) {
|
|
230
235
|
const trimmed = line.trim();
|
|
231
236
|
if (!trimmed) continue;
|
|
232
237
|
if ((trimmed.includes("copilot") || trimmed.includes("@githubnext/copilot")) && !trimmed.includes("ps -eo") && !trimmed.includes("copilot-agent") && !trimmed.includes("grep")) {
|
|
233
238
|
const match = trimmed.match(/^(\d+)\s+(.+)$/);
|
|
234
239
|
if (match) {
|
|
240
|
+
const pid = parseInt(match[1], 10);
|
|
241
|
+
if (pid === myPid || pid === parentPid) continue;
|
|
235
242
|
const cmd = match[2];
|
|
236
243
|
const sidMatch = cmd.match(/resume[= ]+([a-f0-9-]{36})/);
|
|
237
244
|
results.push({
|
|
238
|
-
pid
|
|
245
|
+
pid,
|
|
239
246
|
command: cmd,
|
|
240
247
|
sessionId: sidMatch?.[1]
|
|
241
248
|
});
|
|
@@ -252,6 +259,30 @@ function findPidForSession(sid) {
|
|
|
252
259
|
const matching = procs.filter((p) => p.command.includes(sid)).sort((a, b) => b.pid - a.pid);
|
|
253
260
|
return matching[0]?.pid ?? null;
|
|
254
261
|
}
|
|
262
|
+
async function waitForAllCopilotToFinish(timeoutMs = 144e5, pollMs = 1e4) {
|
|
263
|
+
const start = Date.now();
|
|
264
|
+
let warned = false;
|
|
265
|
+
while (Date.now() - start < timeoutMs) {
|
|
266
|
+
const procs = findCopilotProcesses();
|
|
267
|
+
if (procs.length === 0) return;
|
|
268
|
+
if (!warned) {
|
|
269
|
+
warn(`Waiting for ${procs.length} copilot process(es) to finish before starting...`);
|
|
270
|
+
for (const p of procs) {
|
|
271
|
+
log(` PID ${p.pid}: ${p.command.slice(0, 80)}`);
|
|
272
|
+
}
|
|
273
|
+
warned = true;
|
|
274
|
+
}
|
|
275
|
+
await sleep(pollMs);
|
|
276
|
+
}
|
|
277
|
+
warn("Timeout waiting for copilot processes to finish");
|
|
278
|
+
}
|
|
279
|
+
function assertSessionNotRunning(sid) {
|
|
280
|
+
const pid = findPidForSession(sid);
|
|
281
|
+
if (pid) {
|
|
282
|
+
fail(`Session ${sid.slice(0, 8)}\u2026 already has copilot running (PID ${pid}). Cannot resume \u2014 would corrupt the session.`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
255
286
|
async function waitForExit(pid, timeoutMs = 144e5) {
|
|
256
287
|
const start = Date.now();
|
|
257
288
|
while (Date.now() - start < timeoutMs) {
|
|
@@ -264,7 +295,8 @@ async function waitForExit(pid, timeoutMs = 144e5) {
|
|
|
264
295
|
}
|
|
265
296
|
return false;
|
|
266
297
|
}
|
|
267
|
-
function runCopilot(args, options) {
|
|
298
|
+
async function runCopilot(args, options) {
|
|
299
|
+
await waitForAllCopilotToFinish();
|
|
268
300
|
return new Promise((resolve4) => {
|
|
269
301
|
const child = spawn("copilot", args, {
|
|
270
302
|
cwd: options?.cwd,
|
|
@@ -287,6 +319,7 @@ function runCopilot(args, options) {
|
|
|
287
319
|
});
|
|
288
320
|
}
|
|
289
321
|
function runCopilotResume(sid, steps, message, cwd) {
|
|
322
|
+
assertSessionNotRunning(sid);
|
|
290
323
|
const args = [
|
|
291
324
|
`--resume=${sid}`,
|
|
292
325
|
"--autopilot",
|
|
@@ -314,61 +347,77 @@ function sleep(ms) {
|
|
|
314
347
|
}
|
|
315
348
|
|
|
316
349
|
// src/commands/status.ts
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
350
|
+
function registerStatusCommand(program2) {
|
|
351
|
+
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) => {
|
|
352
|
+
if (opts.active) {
|
|
353
|
+
showActive();
|
|
354
|
+
} else {
|
|
355
|
+
showRecent(parseInt(opts.limit, 10), opts.incomplete ?? false);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
322
358
|
}
|
|
323
|
-
|
|
324
|
-
const procs =
|
|
359
|
+
function showActive() {
|
|
360
|
+
const procs = findCopilotProcesses();
|
|
325
361
|
if (procs.length === 0) {
|
|
326
|
-
log(
|
|
362
|
+
log(`${DIM}No active copilot processes.${RESET}`);
|
|
327
363
|
return;
|
|
328
364
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
${"PID".padEnd(8)} ${"Session".padEnd(40)} ${"Command".slice(0, 60)}`
|
|
333
|
-
)
|
|
334
|
-
);
|
|
335
|
-
console.log("\u2500".repeat(108));
|
|
365
|
+
log(`
|
|
366
|
+
${BOLD}${"PID".padEnd(8)} ${"Session".padEnd(40)} Command${RESET}`);
|
|
367
|
+
log("\u2500".repeat(108));
|
|
336
368
|
for (const p of procs) {
|
|
337
|
-
|
|
338
|
-
`${String(p.pid).padEnd(8)} ${(p.sessionId ?? "\u2014").padEnd(40)} ${(p.command
|
|
369
|
+
log(
|
|
370
|
+
`${CYAN}${String(p.pid).padEnd(8)}${RESET} ${(p.sessionId ?? "\u2014").padEnd(40)} ${truncate(p.command, 58)}`
|
|
339
371
|
);
|
|
340
372
|
}
|
|
341
|
-
|
|
373
|
+
log("");
|
|
342
374
|
}
|
|
343
|
-
function showRecent(limit) {
|
|
344
|
-
|
|
375
|
+
function showRecent(limit, incompleteOnly) {
|
|
376
|
+
let sessions = listSessions(limit);
|
|
377
|
+
if (incompleteOnly) {
|
|
378
|
+
sessions = sessions.filter((s) => !s.complete);
|
|
379
|
+
}
|
|
345
380
|
if (sessions.length === 0) {
|
|
346
|
-
log(
|
|
381
|
+
log(`${DIM}No sessions found.${RESET}`);
|
|
347
382
|
return;
|
|
348
383
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
${"Status".padEnd(10)} ${"Premium".padEnd(10)} ${"Last Event".padEnd(25)} ${"Summary".padEnd(40)} ${"ID"}`
|
|
353
|
-
)
|
|
384
|
+
log(
|
|
385
|
+
`
|
|
386
|
+
${BOLD}${"Status".padEnd(10)} ${"Premium".padEnd(10)} ${"Last Event".padEnd(25)} ${"Summary".padEnd(40)} ID${RESET}`
|
|
354
387
|
);
|
|
355
|
-
|
|
388
|
+
log("\u2500".repeat(120));
|
|
356
389
|
for (const s of sessions) {
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
console.log(
|
|
363
|
-
`${status.padEnd(20)} ${premium.padEnd(10)} ${lastEvt.padEnd(25)} ${summary.padEnd(40)} ${chalk.dim(s.id)}`
|
|
390
|
+
const status = s.complete ? `${GREEN}\u2714 done${RESET}` : `${YELLOW}\u23F8 stop${RESET}`;
|
|
391
|
+
const premium = String(s.premiumRequests);
|
|
392
|
+
const summary = truncate(s.summary || "\u2014", 38);
|
|
393
|
+
log(
|
|
394
|
+
`${status.padEnd(10 + 9)} ${premium.padEnd(10)} ${s.lastEvent.padEnd(25)} ${summary.padEnd(40)} ${DIM}${s.id}${RESET}`
|
|
364
395
|
);
|
|
365
396
|
}
|
|
366
|
-
|
|
397
|
+
log(`
|
|
398
|
+
${DIM}Total: ${sessions.length} session(s)${RESET}`);
|
|
399
|
+
}
|
|
400
|
+
function truncate(s, max) {
|
|
401
|
+
if (s.length <= max) return s;
|
|
402
|
+
return s.substring(0, max - 1) + "\u2026";
|
|
367
403
|
}
|
|
368
404
|
|
|
369
405
|
// src/commands/watch.ts
|
|
370
|
-
|
|
371
|
-
|
|
406
|
+
function registerWatchCommand(program2) {
|
|
407
|
+
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) => {
|
|
408
|
+
try {
|
|
409
|
+
await watchCommand(sid, {
|
|
410
|
+
steps: parseInt(opts.steps, 10),
|
|
411
|
+
maxResumes: parseInt(opts.maxResumes, 10),
|
|
412
|
+
cooldown: parseInt(opts.cooldown, 10),
|
|
413
|
+
message: opts.message
|
|
414
|
+
});
|
|
415
|
+
} catch (err) {
|
|
416
|
+
fail(`Watch error: ${err instanceof Error ? err.message : err}`);
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
372
421
|
async function watchCommand(sid, opts) {
|
|
373
422
|
assertCopilot();
|
|
374
423
|
if (!sid) {
|
|
@@ -377,7 +426,7 @@ async function watchCommand(sid, opts) {
|
|
|
377
426
|
fail("No incomplete session found.");
|
|
378
427
|
process.exit(1);
|
|
379
428
|
}
|
|
380
|
-
|
|
429
|
+
info(`Auto-detected incomplete session: ${CYAN}${sid}${RESET}`);
|
|
381
430
|
}
|
|
382
431
|
if (!validateSession(sid)) {
|
|
383
432
|
fail(`Invalid session: ${sid}`);
|
|
@@ -389,13 +438,10 @@ async function watchCommand(sid, opts) {
|
|
|
389
438
|
}
|
|
390
439
|
let resumes = 0;
|
|
391
440
|
while (resumes < opts.maxResumes) {
|
|
392
|
-
const pid =
|
|
441
|
+
const pid = findPidForSession(sid);
|
|
393
442
|
if (pid) {
|
|
394
|
-
|
|
395
|
-
`Watching PID ${pid} for session ${chalk2.cyan(sid.slice(0, 8))}\u2026`
|
|
396
|
-
).start();
|
|
443
|
+
info(`Watching PID ${pid} for session ${CYAN}${sid.slice(0, 8)}${RESET}\u2026`);
|
|
397
444
|
const exited = await waitForExit(pid);
|
|
398
|
-
spinner.stop();
|
|
399
445
|
if (!exited) {
|
|
400
446
|
warn("Timeout waiting for process exit.");
|
|
401
447
|
break;
|
|
@@ -403,19 +449,25 @@ async function watchCommand(sid, opts) {
|
|
|
403
449
|
}
|
|
404
450
|
await sleep2(3e3);
|
|
405
451
|
if (hasTaskComplete(sid)) {
|
|
406
|
-
ok(
|
|
407
|
-
`Task complete! Summary: ${getSessionSummary(sid) || "none"}`
|
|
408
|
-
);
|
|
452
|
+
ok(`Task complete! Summary: ${getSessionSummary(sid) || "none"}`);
|
|
409
453
|
notify("Task completed!", `Session ${sid.slice(0, 8)}`);
|
|
410
454
|
return;
|
|
411
455
|
}
|
|
412
456
|
resumes++;
|
|
413
|
-
log(
|
|
414
|
-
|
|
457
|
+
log(`Session interrupted (${getLastEvent(sid)}). Resume ${resumes}/${opts.maxResumes}\u2026`);
|
|
458
|
+
if (opts.cooldown > 0 && resumes > 1) {
|
|
459
|
+
info(`Cooldown ${opts.cooldown}s...`);
|
|
460
|
+
await sleep2(opts.cooldown * 1e3);
|
|
461
|
+
}
|
|
462
|
+
const cwd = getSessionCwd(sid) || void 0;
|
|
463
|
+
const result = await runCopilotResume(
|
|
464
|
+
sid,
|
|
465
|
+
opts.steps,
|
|
466
|
+
opts.message ?? "Continue remaining work. Pick up where you left off and complete the task.",
|
|
467
|
+
cwd
|
|
415
468
|
);
|
|
416
|
-
const result = await runCopilotResume(sid, opts.steps, opts.message);
|
|
417
469
|
if (result.sessionId && result.sessionId !== sid) {
|
|
418
|
-
|
|
470
|
+
info(`New session created: ${CYAN}${result.sessionId}${RESET}`);
|
|
419
471
|
sid = result.sessionId;
|
|
420
472
|
}
|
|
421
473
|
}
|
|
@@ -426,10 +478,6 @@ function sleep2(ms) {
|
|
|
426
478
|
return new Promise((r) => setTimeout(r, ms));
|
|
427
479
|
}
|
|
428
480
|
|
|
429
|
-
// src/commands/run.ts
|
|
430
|
-
import chalk3 from "chalk";
|
|
431
|
-
import ora2 from "ora";
|
|
432
|
-
|
|
433
481
|
// src/lib/detect.ts
|
|
434
482
|
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
435
483
|
import { join as join2, basename, resolve as resolve2 } from "path";
|
|
@@ -479,6 +527,27 @@ function detectProjectName(dir) {
|
|
|
479
527
|
}
|
|
480
528
|
return basename(resolve2(dir));
|
|
481
529
|
}
|
|
530
|
+
function detectMainBranch(dir) {
|
|
531
|
+
try {
|
|
532
|
+
const ref = execSync3("git symbolic-ref refs/remotes/origin/HEAD", {
|
|
533
|
+
cwd: dir,
|
|
534
|
+
encoding: "utf-8",
|
|
535
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
536
|
+
}).trim();
|
|
537
|
+
return ref.split("/").pop() ?? "main";
|
|
538
|
+
} catch {
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
const branch = execSync3("git branch --show-current", {
|
|
542
|
+
cwd: dir,
|
|
543
|
+
encoding: "utf-8",
|
|
544
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
545
|
+
}).trim();
|
|
546
|
+
if (branch) return branch;
|
|
547
|
+
} catch {
|
|
548
|
+
}
|
|
549
|
+
return "main";
|
|
550
|
+
}
|
|
482
551
|
|
|
483
552
|
// src/lib/tasks.ts
|
|
484
553
|
var COMMON_TASKS = [
|
|
@@ -659,16 +728,41 @@ function gitStash(dir) {
|
|
|
659
728
|
function gitCheckout(dir, branch) {
|
|
660
729
|
return gitExec(dir, `git checkout ${branch} -q`) !== null;
|
|
661
730
|
}
|
|
731
|
+
function gitCreateBranch(dir, branch) {
|
|
732
|
+
return gitExec(dir, `git checkout -b ${branch}`) !== null;
|
|
733
|
+
}
|
|
734
|
+
function gitCountCommits(dir, from, to) {
|
|
735
|
+
const result = gitExec(dir, `git log ${from}..${to} --oneline`);
|
|
736
|
+
if (!result) return 0;
|
|
737
|
+
return result.split("\n").filter((l) => l.trim()).length;
|
|
738
|
+
}
|
|
662
739
|
function gitStatus(dir) {
|
|
663
740
|
return gitExec(dir, "git status --porcelain") ?? "";
|
|
664
741
|
}
|
|
665
742
|
|
|
666
743
|
// src/commands/run.ts
|
|
744
|
+
function registerRunCommand(program2) {
|
|
745
|
+
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) => {
|
|
746
|
+
try {
|
|
747
|
+
await runCommand(dir ?? process.cwd(), {
|
|
748
|
+
steps: parseInt(opts.steps, 10),
|
|
749
|
+
maxTasks: parseInt(opts.maxTasks, 10),
|
|
750
|
+
maxPremium: parseInt(opts.maxPremium, 10),
|
|
751
|
+
dryRun: opts.dryRun ?? false
|
|
752
|
+
});
|
|
753
|
+
} catch (err) {
|
|
754
|
+
fail(`Run error: ${err instanceof Error ? err.message : err}`);
|
|
755
|
+
process.exit(1);
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
}
|
|
667
759
|
async function runCommand(dir, opts) {
|
|
668
760
|
assertCopilot();
|
|
669
761
|
const projectType = detectProjectType(dir);
|
|
670
762
|
const name = detectProjectName(dir);
|
|
671
|
-
|
|
763
|
+
const mainBranch = isGitRepo(dir) ? detectMainBranch(dir) : null;
|
|
764
|
+
info(`Project: ${CYAN}${name}${RESET} (${projectType})`);
|
|
765
|
+
if (mainBranch) info(`Main branch: ${mainBranch}`);
|
|
672
766
|
const tasks = getTasksForProject(projectType).slice(0, opts.maxTasks);
|
|
673
767
|
if (tasks.length === 0) {
|
|
674
768
|
warn("No tasks found for this project type.");
|
|
@@ -676,594 +770,244 @@ async function runCommand(dir, opts) {
|
|
|
676
770
|
}
|
|
677
771
|
log(`Found ${tasks.length} tasks:`);
|
|
678
772
|
for (const t of tasks) {
|
|
679
|
-
|
|
773
|
+
log(` ${DIM}\u2022${RESET} ${t.title}`);
|
|
680
774
|
}
|
|
681
775
|
if (opts.dryRun) {
|
|
682
|
-
log(
|
|
776
|
+
log(`${DIM}(dry-run \u2014 not executing)${RESET}`);
|
|
683
777
|
return;
|
|
684
778
|
}
|
|
685
779
|
const originalBranch = isGitRepo(dir) ? gitCurrentBranch(dir) : null;
|
|
686
780
|
let completed = 0;
|
|
687
781
|
let premiumTotal = 0;
|
|
688
782
|
for (const task of tasks) {
|
|
783
|
+
if (premiumTotal >= opts.maxPremium) {
|
|
784
|
+
warn(`Premium request limit reached (${premiumTotal}/${opts.maxPremium}).`);
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
689
787
|
log(`
|
|
690
788
|
${"\u2550".repeat(60)}`);
|
|
691
|
-
log(
|
|
789
|
+
log(`${BOLD}${CYAN}Task: ${task.title}${RESET}`);
|
|
692
790
|
log(`${"\u2550".repeat(60)}`);
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
791
|
+
const timestamp = Date.now().toString(36);
|
|
792
|
+
const random = Math.random().toString(36).substring(2, 6);
|
|
793
|
+
const branchName = `agent/fix-${completed + 1}-${timestamp}-${random}`;
|
|
794
|
+
if (mainBranch && isGitRepo(dir)) {
|
|
795
|
+
if (gitStatus(dir)) gitStash(dir);
|
|
796
|
+
gitCheckout(dir, mainBranch);
|
|
797
|
+
if (!gitCreateBranch(dir, branchName)) {
|
|
798
|
+
warn(`Could not create branch ${branchName}, continuing on current.`);
|
|
700
799
|
}
|
|
701
800
|
}
|
|
702
|
-
|
|
801
|
+
info(`Running: ${task.title}\u2026`);
|
|
703
802
|
const result = await withLock(
|
|
704
803
|
"copilot-run",
|
|
705
|
-
() => runCopilotTask(task.prompt, opts.steps)
|
|
804
|
+
() => runCopilotTask(task.prompt, opts.steps, dir)
|
|
706
805
|
);
|
|
707
|
-
|
|
806
|
+
const commits = mainBranch ? gitCountCommits(dir, mainBranch, "HEAD") : 0;
|
|
708
807
|
premiumTotal += result.premium;
|
|
709
808
|
completed++;
|
|
710
|
-
ok(`${task.title} \u2014
|
|
809
|
+
ok(`${task.title} \u2014 ${commits} commit(s), ${result.premium} premium`);
|
|
711
810
|
if (originalBranch && isGitRepo(dir)) {
|
|
712
|
-
|
|
713
|
-
gitCheckout(dir, originalBranch);
|
|
714
|
-
} catch {
|
|
715
|
-
}
|
|
811
|
+
gitCheckout(dir, mainBranch ?? originalBranch);
|
|
716
812
|
}
|
|
717
813
|
}
|
|
718
814
|
log(`
|
|
719
|
-
|
|
815
|
+
${BOLD}\u2550\u2550\u2550 Run Summary \u2550\u2550\u2550${RESET}`);
|
|
816
|
+
log(`Completed ${completed}/${tasks.length} tasks. Total premium: ${premiumTotal}`);
|
|
720
817
|
notify(`Completed ${completed} tasks`, name);
|
|
721
818
|
}
|
|
722
819
|
|
|
723
820
|
// src/commands/overnight.ts
|
|
724
|
-
import chalk4 from "chalk";
|
|
725
|
-
import ora3 from "ora";
|
|
726
821
|
import { join as join5 } from "path";
|
|
727
822
|
import { homedir as homedir3 } from "os";
|
|
823
|
+
function registerOvernightCommand(program2) {
|
|
824
|
+
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) => {
|
|
825
|
+
try {
|
|
826
|
+
await overnightCommand(dir ?? process.cwd(), {
|
|
827
|
+
until: parseInt(opts.until, 10),
|
|
828
|
+
steps: parseInt(opts.steps, 10),
|
|
829
|
+
cooldown: parseInt(opts.cooldown, 10),
|
|
830
|
+
maxPremium: parseInt(opts.maxPremium, 10),
|
|
831
|
+
dryRun: opts.dryRun ?? false
|
|
832
|
+
});
|
|
833
|
+
} catch (err) {
|
|
834
|
+
fail(`Overnight error: ${err instanceof Error ? err.message : err}`);
|
|
835
|
+
process.exit(1);
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
function isPastDeadline(untilHour) {
|
|
840
|
+
const hour = (/* @__PURE__ */ new Date()).getHours();
|
|
841
|
+
return hour >= untilHour && hour < 20;
|
|
842
|
+
}
|
|
728
843
|
async function overnightCommand(dir, opts) {
|
|
729
844
|
assertCopilot();
|
|
730
845
|
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
|
-
);
|
|
846
|
+
const logPath = join5(homedir3(), ".copilot", "auto-resume-logs", `overnight-${ts}.log`);
|
|
737
847
|
setLogFile(logPath);
|
|
738
|
-
const deadline = parseDeadline(opts.until);
|
|
739
848
|
const name = detectProjectName(dir);
|
|
740
849
|
const projectType = detectProjectType(dir);
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
850
|
+
const mainBranch = isGitRepo(dir) ? detectMainBranch(dir) : null;
|
|
851
|
+
info(`Overnight runner for ${CYAN}${name}${RESET} (${projectType})`);
|
|
852
|
+
info(`Deadline: ${String(opts.until).padStart(2, "0")}:00`);
|
|
853
|
+
info(`Max premium: ${opts.maxPremium}, Steps: ${opts.steps}`);
|
|
854
|
+
info(`Log: ${logPath}`);
|
|
855
|
+
const tasks = getTasksForProject(projectType);
|
|
745
856
|
if (opts.dryRun) {
|
|
746
|
-
const tasks2 = getTasksForProject(projectType);
|
|
747
857
|
log(`
|
|
748
|
-
Would run ${
|
|
749
|
-
for (const t of
|
|
858
|
+
Would run ${tasks.length} tasks:`);
|
|
859
|
+
for (const t of tasks) log(` ${DIM}\u2022${RESET} ${t.title}`);
|
|
750
860
|
return;
|
|
751
861
|
}
|
|
752
|
-
const
|
|
862
|
+
const existingSession = findLatestIncomplete();
|
|
863
|
+
if (existingSession && validateSession(existingSession)) {
|
|
864
|
+
info(`Found incomplete session: ${existingSession}`);
|
|
865
|
+
const pid = findPidForSession(existingSession);
|
|
866
|
+
if (pid) {
|
|
867
|
+
info(`Waiting for running copilot (PID ${pid})...`);
|
|
868
|
+
await waitForExit(pid);
|
|
869
|
+
}
|
|
870
|
+
if (!hasTaskComplete(existingSession) && !isPastDeadline(opts.until)) {
|
|
871
|
+
info("Resuming incomplete session...");
|
|
872
|
+
const cwd = getSessionCwd(existingSession) || dir;
|
|
873
|
+
await runCopilotResume(
|
|
874
|
+
existingSession,
|
|
875
|
+
opts.steps,
|
|
876
|
+
"Continue remaining work. Complete the task.",
|
|
877
|
+
cwd
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
const originalBranch = isGitRepo(dir) ? gitCurrentBranch(dir) : null;
|
|
753
882
|
let taskIdx = 0;
|
|
754
883
|
let totalPremium = 0;
|
|
755
|
-
let
|
|
756
|
-
|
|
757
|
-
while (Date.now() < deadline) {
|
|
884
|
+
let totalCommits = 0;
|
|
885
|
+
while (!isPastDeadline(opts.until) && taskIdx < tasks.length) {
|
|
758
886
|
if (totalPremium >= opts.maxPremium) {
|
|
759
887
|
warn(`Premium budget exhausted: ${totalPremium}/${opts.maxPremium}`);
|
|
760
888
|
break;
|
|
761
889
|
}
|
|
762
890
|
const task = tasks[taskIdx % tasks.length];
|
|
763
|
-
cycle++;
|
|
764
891
|
taskIdx++;
|
|
765
892
|
log(`
|
|
766
893
|
${"\u2550".repeat(60)}`);
|
|
767
|
-
log(
|
|
768
|
-
|
|
769
|
-
);
|
|
770
|
-
log(`Time remaining: ${msToHuman(deadline - Date.now())}`);
|
|
894
|
+
log(`${BOLD}${CYAN}[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Task ${taskIdx}: ${task.title}${RESET}`);
|
|
895
|
+
log(`${DIM}Premium: ${totalPremium}/${opts.maxPremium}${RESET}`);
|
|
771
896
|
log(`${"\u2550".repeat(60)}`);
|
|
772
|
-
const
|
|
897
|
+
const timestamp = Date.now().toString(36);
|
|
898
|
+
const random = Math.random().toString(36).substring(2, 6);
|
|
899
|
+
const branchName = `agent/overnight-${taskIdx}-${timestamp}-${random}`;
|
|
900
|
+
if (mainBranch && isGitRepo(dir)) {
|
|
901
|
+
gitStash(dir);
|
|
902
|
+
gitCheckout(dir, mainBranch);
|
|
903
|
+
gitCreateBranch(dir, branchName);
|
|
904
|
+
}
|
|
905
|
+
info(`Running: ${task.title}\u2026`);
|
|
773
906
|
try {
|
|
774
907
|
const result = await withLock(
|
|
775
908
|
"copilot-overnight",
|
|
776
|
-
() => runCopilotTask(task.prompt, opts.steps)
|
|
909
|
+
() => runCopilotTask(task.prompt, opts.steps, dir)
|
|
777
910
|
);
|
|
778
|
-
|
|
911
|
+
const commits = mainBranch ? gitCountCommits(dir, mainBranch, "HEAD") : 0;
|
|
779
912
|
totalPremium += result.premium;
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
`${
|
|
783
|
-
|
|
913
|
+
totalCommits += commits;
|
|
914
|
+
if (commits > 0) {
|
|
915
|
+
ok(`${commits} commit(s) on ${branchName}`);
|
|
916
|
+
} else {
|
|
917
|
+
log(`${DIM}No commits on ${branchName}${RESET}`);
|
|
918
|
+
}
|
|
784
919
|
} catch (err) {
|
|
785
|
-
spinner.stop();
|
|
786
920
|
fail(`Task failed: ${err}`);
|
|
787
921
|
}
|
|
788
|
-
if (
|
|
789
|
-
|
|
790
|
-
|
|
922
|
+
if (mainBranch && isGitRepo(dir)) {
|
|
923
|
+
gitCheckout(dir, mainBranch);
|
|
924
|
+
}
|
|
925
|
+
if (!isPastDeadline(opts.until)) {
|
|
926
|
+
info(`Cooldown ${opts.cooldown}s\u2026`);
|
|
927
|
+
await sleep3(opts.cooldown * 1e3);
|
|
791
928
|
}
|
|
792
929
|
}
|
|
793
|
-
|
|
794
|
-
|
|
930
|
+
if (originalBranch && isGitRepo(dir)) {
|
|
931
|
+
gitCheckout(dir, originalBranch);
|
|
932
|
+
}
|
|
933
|
+
const summary = `Overnight done \u2014 ${taskIdx} tasks, ${totalCommits} commits, ${totalPremium} premium.`;
|
|
934
|
+
ok(summary);
|
|
795
935
|
notify(summary, name);
|
|
796
936
|
}
|
|
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
937
|
function sleep3(ms) {
|
|
811
938
|
return new Promise((r) => setTimeout(r, ms));
|
|
812
939
|
}
|
|
813
940
|
|
|
814
941
|
// src/commands/research.ts
|
|
815
|
-
import
|
|
816
|
-
|
|
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";
|
|
942
|
+
import { existsSync as existsSync5, copyFileSync, mkdirSync as mkdirSync3 } from "fs";
|
|
943
|
+
import { join as join6, resolve as resolve3 } from "path";
|
|
873
944
|
import { homedir as homedir4 } from "os";
|
|
874
|
-
|
|
875
|
-
|
|
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);
|
|
945
|
+
function registerResearchCommand(program2) {
|
|
946
|
+
program2.command("research [project]").description("Research improvements or a specific topic").option("-s, --steps <n>", "Max autopilot continues", "50").action(async (project, opts) => {
|
|
996
947
|
try {
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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
|
-
}
|
|
948
|
+
await researchCommand(project ?? process.cwd(), {
|
|
949
|
+
steps: parseInt(opts.steps, 10)
|
|
950
|
+
});
|
|
1033
951
|
} catch (err) {
|
|
1034
|
-
fail(`
|
|
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`);
|
|
952
|
+
fail(`Research error: ${err instanceof Error ? err.message : err}`);
|
|
1063
953
|
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
954
|
}
|
|
1092
|
-
projects.push(resolved);
|
|
1093
|
-
writeProjects(projects);
|
|
1094
|
-
ok(`Added: ${resolved}`);
|
|
1095
955
|
});
|
|
1096
956
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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();
|
|
957
|
+
function buildResearchPrompt(projectType, projectName) {
|
|
958
|
+
return `You are a senior software architect. Analyze this ${projectType} project "${projectName}" thoroughly.
|
|
959
|
+
|
|
960
|
+
Research and produce a file called RESEARCH-PROPOSALS.md with:
|
|
961
|
+
|
|
962
|
+
1. **Architecture Assessment** \u2014 Current architecture, patterns used, strengths and weaknesses
|
|
963
|
+
2. **Code Quality Report** \u2014 Common issues, anti-patterns, technical debt areas
|
|
964
|
+
3. **Security Audit** \u2014 Potential vulnerabilities, dependency risks, configuration issues
|
|
965
|
+
4. **Performance Analysis** \u2014 Bottlenecks, optimization opportunities, resource usage
|
|
966
|
+
5. **Testing Gap Analysis** \u2014 Untested areas, test quality, coverage recommendations
|
|
967
|
+
6. **Improvement Proposals** \u2014 Prioritized list of actionable improvements with effort estimates
|
|
968
|
+
|
|
969
|
+
For each proposal, include:
|
|
970
|
+
- Priority (P0/P1/P2)
|
|
971
|
+
- Estimated effort (hours)
|
|
972
|
+
- Impact description
|
|
973
|
+
- Suggested implementation approach
|
|
974
|
+
|
|
975
|
+
Write RESEARCH-PROPOSALS.md in the project root.`;
|
|
1128
976
|
}
|
|
1129
|
-
async function
|
|
977
|
+
async function researchCommand(dir, opts) {
|
|
1130
978
|
assertCopilot();
|
|
1131
|
-
const
|
|
1132
|
-
|
|
1133
|
-
const
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
-
}
|
|
979
|
+
const projectDir = resolve3(dir);
|
|
980
|
+
const projectType = detectProjectType(projectDir);
|
|
981
|
+
const projectName = detectProjectName(projectDir);
|
|
982
|
+
info(`Researching: ${CYAN}${projectName}${RESET} (${projectType})`);
|
|
983
|
+
const prompt = buildResearchPrompt(projectType, projectName);
|
|
984
|
+
const result = await withLock(
|
|
985
|
+
"copilot-research",
|
|
986
|
+
() => runCopilotTask(prompt, opts.steps, projectDir)
|
|
987
|
+
);
|
|
988
|
+
log(`Copilot exited with code ${result.exitCode}`);
|
|
989
|
+
const proposalsFile = join6(projectDir, "RESEARCH-PROPOSALS.md");
|
|
990
|
+
if (existsSync5(proposalsFile)) {
|
|
991
|
+
ok("RESEARCH-PROPOSALS.md generated.");
|
|
992
|
+
const backupDir = join6(homedir4(), ".copilot", "research-reports");
|
|
993
|
+
mkdirSync3(backupDir, { recursive: true });
|
|
994
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
995
|
+
const backupFile = join6(backupDir, `${projectName}-${timestamp}.md`);
|
|
996
|
+
copyFileSync(proposalsFile, backupFile);
|
|
997
|
+
ok(`Backup saved: ${backupFile}`);
|
|
998
|
+
} else {
|
|
999
|
+
warn("RESEARCH-PROPOSALS.md was not generated. Check copilot output.");
|
|
1201
1000
|
}
|
|
1202
|
-
|
|
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");
|
|
1001
|
+
notify("Research complete", projectName);
|
|
1208
1002
|
}
|
|
1209
1003
|
|
|
1210
1004
|
// src/index.ts
|
|
1211
1005
|
var program = new Command();
|
|
1212
|
-
program.name("copilot-agent").description("Autonomous GitHub Copilot CLI agent \u2014 auto-resume, task discovery, overnight
|
|
1213
|
-
program
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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
|
-
});
|
|
1006
|
+
program.name("copilot-agent").version("0.3.0").description("Autonomous GitHub Copilot CLI agent \u2014 auto-resume, task discovery, overnight runs");
|
|
1007
|
+
registerStatusCommand(program);
|
|
1008
|
+
registerWatchCommand(program);
|
|
1009
|
+
registerRunCommand(program);
|
|
1010
|
+
registerOvernightCommand(program);
|
|
1011
|
+
registerResearchCommand(program);
|
|
1268
1012
|
program.parse();
|
|
1269
1013
|
//# sourceMappingURL=index.js.map
|