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 +340 -581
- 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,
|
|
@@ -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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
325
|
+
resolve6({
|
|
279
326
|
exitCode: code ?? 1,
|
|
280
327
|
sessionId: sid,
|
|
281
328
|
premium
|
|
282
329
|
});
|
|
283
330
|
});
|
|
284
331
|
child.on("error", () => {
|
|
285
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
324
|
-
const procs =
|
|
374
|
+
function showActive() {
|
|
375
|
+
const procs = findCopilotProcesses();
|
|
325
376
|
if (procs.length === 0) {
|
|
326
|
-
log(
|
|
377
|
+
log(`${DIM}No active copilot processes.${RESET}`);
|
|
327
378
|
return;
|
|
328
379
|
}
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
338
|
-
`${String(p.pid).padEnd(8)} ${(p.sessionId ?? "\u2014").padEnd(40)} ${(p.command
|
|
384
|
+
log(
|
|
385
|
+
`${CYAN}${String(p.pid).padEnd(8)}${RESET} ${(p.sessionId ?? "\u2014").padEnd(40)} ${truncate(p.command, 58)}`
|
|
339
386
|
);
|
|
340
387
|
}
|
|
341
|
-
|
|
388
|
+
log("");
|
|
342
389
|
}
|
|
343
|
-
function showRecent(limit) {
|
|
344
|
-
|
|
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(
|
|
396
|
+
log(`${DIM}No sessions found.${RESET}`);
|
|
347
397
|
return;
|
|
348
398
|
}
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
403
|
+
log("\u2500".repeat(120));
|
|
356
404
|
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)}`
|
|
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
|
-
|
|
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
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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 =
|
|
456
|
+
const pid = findPidForSession(sid);
|
|
393
457
|
if (pid) {
|
|
394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
788
|
+
log(` ${DIM}\u2022${RESET} ${t.title}`);
|
|
680
789
|
}
|
|
681
790
|
if (opts.dryRun) {
|
|
682
|
-
log(
|
|
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(
|
|
804
|
+
log(`${BOLD}${CYAN}Task: ${task.title}${RESET}`);
|
|
692
805
|
log(`${"\u2550".repeat(60)}`);
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
|
|
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
|
-
|
|
821
|
+
const commits = mainBranch ? gitCountCommits(dir, mainBranch, "HEAD") : 0;
|
|
708
822
|
premiumTotal += result.premium;
|
|
709
823
|
completed++;
|
|
710
|
-
ok(`${task.title} \u2014
|
|
824
|
+
ok(`${task.title} \u2014 ${commits} commit(s), ${result.premium} premium`);
|
|
711
825
|
if (originalBranch && isGitRepo(dir)) {
|
|
712
|
-
|
|
713
|
-
gitCheckout(dir, originalBranch);
|
|
714
|
-
} catch {
|
|
715
|
-
}
|
|
826
|
+
gitCheckout(dir, mainBranch ?? originalBranch);
|
|
716
827
|
}
|
|
717
828
|
}
|
|
718
829
|
log(`
|
|
719
|
-
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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 ${
|
|
749
|
-
for (const t of
|
|
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
|
|
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
|
|
756
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
926
|
+
const commits = mainBranch ? gitCountCommits(dir, mainBranch, "HEAD") : 0;
|
|
779
927
|
totalPremium += result.premium;
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
`${
|
|
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 (
|
|
789
|
-
|
|
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
|
|
794
|
-
|
|
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
|
|
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";
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
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
|
-
}
|
|
963
|
+
await researchCommand(project ?? process.cwd(), {
|
|
964
|
+
steps: parseInt(opts.steps, 10)
|
|
965
|
+
});
|
|
1033
966
|
} 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`);
|
|
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
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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
|
|
992
|
+
async function researchCommand(dir, opts) {
|
|
1130
993
|
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
|
-
}
|
|
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
|
-
|
|
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
|
|
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
|
-
});
|
|
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
|