ai-agentmanager 1.0.1 โ 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +76 -4
- package/bin/agentmgr.js +10 -3
- package/docs/architecture.md +1 -0
- package/docs/images/agents.jpg +0 -0
- package/docs/images/conflict-radar.jpg +0 -0
- package/package.json +1 -1
- package/public/app.js +518 -11
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/icon.svg +1 -0
- package/public/index.html +97 -0
- package/public/manifest.webmanifest +16 -0
- package/public/style.css +69 -3
- package/src/cleanup.js +185 -0
- package/src/cli.js +181 -0
- package/src/config.js +4 -0
- package/src/doctor.js +113 -0
- package/src/engines/antigravity.js +1 -1
- package/src/engines/claude.js +22 -5
- package/src/engines/codex.js +1 -1
- package/src/engines/index.js +8 -0
- package/src/judge.js +12 -0
- package/src/manager.js +65 -12
- package/src/server.js +44 -2
- package/src/teams.js +984 -0
- package/src/tracker.js +11 -1
package/src/cli.js
CHANGED
|
@@ -626,6 +626,133 @@ export async function main(argv) {
|
|
|
626
626
|
console.log(`${green('๐')} winner: ${r.rows.find((x) => x.id === r.winner)?.variant?.label ?? r.winner}`);
|
|
627
627
|
});
|
|
628
628
|
|
|
629
|
+
const teamCmd = program.command('team').description('Agents in roles solve one problem: analyze, critique, implement, test, review');
|
|
630
|
+
teamCmd
|
|
631
|
+
.command('start')
|
|
632
|
+
.description('Start a team on a problem')
|
|
633
|
+
.argument('<problem>')
|
|
634
|
+
.option('-r, --repo <url>', 'git repository URL')
|
|
635
|
+
.option('-d, --dir <path>', 'git folder (the team gets a worktree), or a new folder for a new project')
|
|
636
|
+
.option('--project <name>', 'project shortcut or name')
|
|
637
|
+
.option('-b, --base <ref>', 'branch the team starts from')
|
|
638
|
+
.option('-c, --critics <n>', 'critics who challenge the analysis (0-2)', '1')
|
|
639
|
+
.option('-t, --test <command>', 'test command, a hint for the tester')
|
|
640
|
+
.option('-m, --model <model>', 'model for every role')
|
|
641
|
+
.option('--role-model <role=model...>', 'model for one role, e.g. --role-model analyst=opus reviewer=opus')
|
|
642
|
+
.option('--max-rounds <n>', 'analysis rounds before it asks you (default 3)')
|
|
643
|
+
.option('--max-loops <n>', 'fix rounds per request before it asks you (default 3)')
|
|
644
|
+
.option('--gap-rounds <n>', 'gap rounds (for what the reviewer says is still missing) it runs without asking you (default 0: it asks first)')
|
|
645
|
+
.option('--separate', 'one agent per role instead of one lead session with sub-agents')
|
|
646
|
+
.action(async (problem, opts) => {
|
|
647
|
+
await daemon();
|
|
648
|
+
const models = Object.fromEntries((opts.roleModel ?? []).map((p) => p.split('=')).filter(([k, v]) => k && v));
|
|
649
|
+
const t = await api('POST', '/teams', {
|
|
650
|
+
intent: problem, repo: opts.repo, dir: opts.dir, project: opts.project, base: opts.base, critics: Number(opts.critics),
|
|
651
|
+
testCommand: opts.test, model: opts.model, models, maxRounds: opts.maxRounds, maxLoops: opts.maxLoops, gapRounds: opts.gapRounds,
|
|
652
|
+
...(opts.separate ? { mode: 'agents' } : {}),
|
|
653
|
+
});
|
|
654
|
+
console.log(`${green('๐ฅ')} team ${bold(t.id)}: ${t.mode === 'lead' ? `lead ${t.members[0].id}, with ${t.subagents.map((r) => r.role).join(', ')} as sub-agents` : t.members.map((m) => `${m.role} (${m.id})`).join(', ')}`);
|
|
655
|
+
if (t.newProject) console.log(dim(` new project: ${t.newProject}`));
|
|
656
|
+
console.log(dim(` follow it: agentmgr team show ${t.id}`));
|
|
657
|
+
});
|
|
658
|
+
teamCmd
|
|
659
|
+
.command('show')
|
|
660
|
+
.description('Where a team stands and what each member handed off (or list teams)')
|
|
661
|
+
.argument('[team]')
|
|
662
|
+
.action(async (id) => {
|
|
663
|
+
await daemon();
|
|
664
|
+
if (!id) {
|
|
665
|
+
const list = await api('GET', '/teams');
|
|
666
|
+
if (!list.length) return console.log(dim('No teams yet: agentmgr team start "<problem>" -r <repo>'));
|
|
667
|
+
return table(list.map((t) => [t.id, t.name, t.stage, `${t.loop}`, t.intent.split('\n')[0].slice(0, 50)]), ['TEAM', 'NAME', 'STAGE', 'ROUND', 'PROBLEM']);
|
|
668
|
+
}
|
|
669
|
+
const t = await api('GET', `/teams/${id}`);
|
|
670
|
+
console.log(`${bold(t.name)} ${t.mode === 'lead' || t.quest > 1 ? `quest ${t.quest}${t.gapRound ? ` (gap round ${t.gapRound})` : ''} ยท ` : ''}${t.stage}${t.blocked ? `: ${t.blocked.reason}` : ''}`);
|
|
671
|
+
console.log(dim(` ${(t.request ?? t.intent).split('\n')[0]}`));
|
|
672
|
+
for (const q of t.quests ?? []) console.log(dim(` quest ${q.n}: ${q.request.split('\n')[0].slice(0, 60)} (${q.outcome})`));
|
|
673
|
+
console.log(dim(` folder ${t.dir}${t.branch ? ` ยท branch ${t.branch}` : ''}`));
|
|
674
|
+
table(t.members.map((m) => [m.role + (m.n && t.settings.critics > 1 ? ` ${m.n}` : ''), m.id, m.status, m.turn ? 'its turn' : '', m.costUsd ? `$${m.costUsd.toFixed(2)}` : '']), ['ROLE', 'AGENT', 'STATUS', '', 'COST']);
|
|
675
|
+
if (t.history.length) {
|
|
676
|
+
console.log('');
|
|
677
|
+
table(t.history.map((h) => [h.role, h.verdict, h.file, h.summary.slice(0, 70)]), ['FROM', 'VERDICT', 'FILE', 'SUMMARY']);
|
|
678
|
+
}
|
|
679
|
+
if (t.stage === 'ready') console.log(`\n${green('โ')} approved: agentmgr team finalize ${t.id} ${t.remote ? '[--keep]' : '--merge | --publish | --keep'}, or agentmgr team send-back ${t.id} "<what to change>"${t.gaps ? `, or agentmgr team fix-gaps ${t.id}` : ''}`);
|
|
680
|
+
else if (t.stage === 'done' && t.gaps) console.log(`\n${yellow('!')} the last review lists gaps: agentmgr team fix-gaps ${t.id} ["note"]`);
|
|
681
|
+
if (t.stage === 'blocked') console.log(`\n${yellow('!')} decide: agentmgr team continue ${t.id} ["note"], or agentmgr team skip ${t.id}`);
|
|
682
|
+
});
|
|
683
|
+
teamCmd
|
|
684
|
+
.command('finalize')
|
|
685
|
+
.description('Accept the approved work: the implementer opens a PR, or --merge / --publish / --keep')
|
|
686
|
+
.argument('<team>')
|
|
687
|
+
.option('--merge', 'fast-forward the local main branch to the team branch')
|
|
688
|
+
.option('--publish', 'no GitHub repository yet: create a private one, push and open a PR')
|
|
689
|
+
.option('--keep', 'keep the branch as it is')
|
|
690
|
+
.option('--no-pr', 'same as --keep')
|
|
691
|
+
.action(async (id, opts) => {
|
|
692
|
+
await daemon();
|
|
693
|
+
const mode = opts.merge ? 'merge' : opts.publish ? 'publish' : opts.keep || opts.pr === false ? 'keep' : 'pr';
|
|
694
|
+
const t = await api('POST', `/teams/${id}/finalize`, { mode });
|
|
695
|
+
console.log(`${green('โ')} ${{ merged: 'merged into main', kept: 'done; the branch is kept' }[t.outcome] ?? (mode === 'publish' ? 'the implementer is publishing it to GitHub' : 'the implementer is opening the pull request')}`);
|
|
696
|
+
});
|
|
697
|
+
teamCmd
|
|
698
|
+
.command('send-back')
|
|
699
|
+
.description('Send the approved work back with what should change')
|
|
700
|
+
.argument('<team>')
|
|
701
|
+
.argument('<note>')
|
|
702
|
+
.action(async (id, note) => {
|
|
703
|
+
await daemon();
|
|
704
|
+
await api('POST', `/teams/${id}/send-back`, { note });
|
|
705
|
+
console.log(`${green('โฉ')} sent back to the implementer`);
|
|
706
|
+
});
|
|
707
|
+
teamCmd
|
|
708
|
+
.command('fix-gaps')
|
|
709
|
+
.description('Another round, from analysis to review, for what the last review says is missing')
|
|
710
|
+
.argument('<team>')
|
|
711
|
+
.argument('[note]', "what's missing, if the review lists nothing")
|
|
712
|
+
.action(async (id, note) => {
|
|
713
|
+
await daemon();
|
|
714
|
+
await api('POST', `/teams/${id}/fix-gaps`, { note });
|
|
715
|
+
console.log(`${green('โฆ')} gap round started`);
|
|
716
|
+
});
|
|
717
|
+
teamCmd
|
|
718
|
+
.command('continue')
|
|
719
|
+
.description('After a disagreement: one more round, with an optional note')
|
|
720
|
+
.argument('<team>')
|
|
721
|
+
.argument('[note]')
|
|
722
|
+
.action(async (id, note) => {
|
|
723
|
+
await daemon();
|
|
724
|
+
await api('POST', `/teams/${id}/resume`, { action: 'continue', note });
|
|
725
|
+
console.log(`${green('โฆ')} another round started`);
|
|
726
|
+
});
|
|
727
|
+
teamCmd
|
|
728
|
+
.command('skip')
|
|
729
|
+
.description('After a disagreement: implement the latest analysis, or accept the work as is')
|
|
730
|
+
.argument('<team>')
|
|
731
|
+
.action(async (id) => {
|
|
732
|
+
await daemon();
|
|
733
|
+
const t = await api('POST', `/teams/${id}/resume`, { action: 'skip' });
|
|
734
|
+
console.log(`${green('โ')} ${t.stage === 'ready' ? 'accepted; finalize when ready' : 'implementing the latest analysis'}`);
|
|
735
|
+
});
|
|
736
|
+
teamCmd
|
|
737
|
+
.command('ask')
|
|
738
|
+
.description("Give a team's lead new work: it runs it through the team as the next quest")
|
|
739
|
+
.argument('<team>')
|
|
740
|
+
.argument('<request>')
|
|
741
|
+
.action(async (id, request) => {
|
|
742
|
+
await daemon();
|
|
743
|
+
await api('POST', `/teams/${id}/ask`, { text: request });
|
|
744
|
+
console.log(`${green('โ')} given to the lead`);
|
|
745
|
+
});
|
|
746
|
+
teamCmd
|
|
747
|
+
.command('stop')
|
|
748
|
+
.description("Stop a team: its agents close; nothing is deleted")
|
|
749
|
+
.argument('<team>')
|
|
750
|
+
.action(async (id) => {
|
|
751
|
+
await daemon();
|
|
752
|
+
await api('POST', `/teams/${id}/stop`);
|
|
753
|
+
console.log(`${green('โ ')} team stopped`);
|
|
754
|
+
});
|
|
755
|
+
|
|
629
756
|
const projCmd = program.command('project').description('Project shortcuts: short names like "cc" for repos you work on');
|
|
630
757
|
projCmd
|
|
631
758
|
.command('ls')
|
|
@@ -879,6 +1006,60 @@ export async function main(argv) {
|
|
|
879
1006
|
}
|
|
880
1007
|
});
|
|
881
1008
|
|
|
1009
|
+
program
|
|
1010
|
+
.command('cleanup')
|
|
1011
|
+
.description('Close agents idle for a while and delete ones closed long after that (see what it would do, run it, or pick the strategy)')
|
|
1012
|
+
.option('--run', 'close and delete what is due now')
|
|
1013
|
+
.option('--close-only', 'with --run: only close sessions, delete nothing')
|
|
1014
|
+
.option('--strategy <s>', 'close-delete (both automatically), close (close automatically, delete by hand), manual, or off')
|
|
1015
|
+
.option('--close-after <days>', 'close sessions idle this many days')
|
|
1016
|
+
.option('--delete-after <days>', 'delete agents closed this many days')
|
|
1017
|
+
.action(async (opts) => {
|
|
1018
|
+
await daemon();
|
|
1019
|
+
const patch = {
|
|
1020
|
+
...(opts.strategy ? { strategy: opts.strategy } : {}),
|
|
1021
|
+
...(opts.closeAfter ? { closeAfterDays: Number(opts.closeAfter) } : {}),
|
|
1022
|
+
...(opts.deleteAfter ? { deleteAfterDays: Number(opts.deleteAfter) } : {}),
|
|
1023
|
+
};
|
|
1024
|
+
if (Object.keys(patch).length) await api('PUT', '/cleanup', patch);
|
|
1025
|
+
if (opts.run) {
|
|
1026
|
+
const r = await api('POST', '/cleanup/run', { delete: !opts.closeOnly });
|
|
1027
|
+
for (const x of r.closed) console.log(`${yellow('โ ')} closed ${x.name} (idle ${x.idleDays} days)`);
|
|
1028
|
+
for (const x of r.deleted) console.log(`${red('ร')} deleted ${x.name} (closed ${x.closedDays} days)`);
|
|
1029
|
+
for (const x of r.failed) console.log(`${red('!')} ${x.name}: ${x.reason}`);
|
|
1030
|
+
if (!r.closed.length && !r.deleted.length) console.log(dim('Nothing to clean up.'));
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const c = await api('GET', '/cleanup');
|
|
1034
|
+
const what = { 'close-delete': 'closes and deletes automatically', close: 'closes automatically; delete with --run', manual: 'nothing automatic; use --run', off: 'off' }[c.strategy];
|
|
1035
|
+
console.log(`${bold('Clean up')}: ${c.strategy} (${what}) ยท close after ${c.closeAfterDays} days idle ยท delete ${c.deleteAfterDays} days after closing`);
|
|
1036
|
+
const rows = [
|
|
1037
|
+
...c.close.map((x) => [yellow('close'), x.name, `idle ${x.idleDays}d`, '']),
|
|
1038
|
+
...c.delete.map((x) => [red('delete'), x.name, `closed ${x.closedDays}d`, '']),
|
|
1039
|
+
...c.kept.map((x) => [dim('keep'), x.name, x.closedDays != null ? `closed ${x.closedDays}d` : `idle ${x.idleDays}d`, x.reason]),
|
|
1040
|
+
];
|
|
1041
|
+
if (rows.length) table(rows, ['DUE', 'AGENT', 'AGE', 'KEPT BECAUSE']);
|
|
1042
|
+
else console.log(dim('Nothing is due.'));
|
|
1043
|
+
if (c.lastRun) console.log(dim(`\nLast run ${new Date(c.lastRun.at).toLocaleString()} (${c.lastRun.by}): closed ${c.lastRun.closed.length}, deleted ${c.lastRun.deleted.length}`));
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
program
|
|
1047
|
+
.command('doctor')
|
|
1048
|
+
.description('Check what AI Agent Manager needs on this machine (Node.js, Claude Code, git, gh, the daemon) and how to fix it')
|
|
1049
|
+
.action(async () => {
|
|
1050
|
+
const { doctor } = await import('./doctor.js');
|
|
1051
|
+
const checks = await doctor();
|
|
1052
|
+
const mark = { ok: green('โ'), warn: yellow('!'), fail: red('โ'), info: dim('ยท') };
|
|
1053
|
+
const width = Math.max(...checks.map((c) => c.name.length));
|
|
1054
|
+
for (const c of checks) {
|
|
1055
|
+
console.log(`${mark[c.status]} ${c.name.padEnd(width)} ${c.status === 'fail' ? red(c.detail) : c.detail}`);
|
|
1056
|
+
if (c.fix) console.log(` ${' '.repeat(width)} ${dim('โ')} ${c.fix}`);
|
|
1057
|
+
}
|
|
1058
|
+
const failed = checks.filter((c) => c.status === 'fail').length;
|
|
1059
|
+
console.log(failed ? red(`\n${failed} problem${failed === 1 ? '' : 's'} to fix`) : green('\nReady: start with `agentmgr ui`'));
|
|
1060
|
+
if (failed) process.exitCode = 1;
|
|
1061
|
+
});
|
|
1062
|
+
|
|
882
1063
|
const remoteCmd = program.command('remote').description('Open AI Agent Manager from a phone or another computer on the same network');
|
|
883
1064
|
const printRemote = async (st, { qr = true } = {}) => {
|
|
884
1065
|
if (!st.enabled || !st.running) return console.log(`remote access ${dim('off')} (turn on: agentmgr remote on)`);
|
package/src/config.js
CHANGED
|
@@ -24,6 +24,8 @@ export const paths = {
|
|
|
24
24
|
scrollback: path.join(HOME, 'scrollback'),
|
|
25
25
|
// Files attached from the UI (dropped or pasted into a terminal), per agent.
|
|
26
26
|
uploads: path.join(HOME, 'uploads'),
|
|
27
|
+
// Agent teams' handoff files (analysis, critiques, test reports, reviews), per team.
|
|
28
|
+
teams: path.join(HOME, 'teams'),
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
const DEFAULTS = {
|
|
@@ -65,6 +67,8 @@ const DEFAULTS = {
|
|
|
65
67
|
handoffIdleSec: 5,
|
|
66
68
|
// The AI judge (stuck, idle screens, reviews); see JUDGE_DEFAULTS in judge.js for every field.
|
|
67
69
|
judge: { enabled: true },
|
|
70
|
+
// Clean up old agents: "close-delete", "close" (delete by hand), "manual" or "off"; see cleanup.js.
|
|
71
|
+
cleanup: { strategy: 'close', closeAfterDays: 14, deleteAfterDays: 14 },
|
|
68
72
|
// Shell used for the ancillary terminal in an agent's folder.
|
|
69
73
|
shell: process.env.SHELL || '/bin/zsh',
|
|
70
74
|
};
|
package/src/doctor.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// `agentmgr doctor`: checks what AI Agent Manager needs on this machine and says how to fix
|
|
2
|
+
// what's missing. Each check is { name, status: 'ok' | 'warn' | 'fail' | 'info', detail, fix }.
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import net from 'node:net';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { HOST, PORT, loadConfig, paths } from './config.js';
|
|
9
|
+
import { api, BASE, isRunning } from './client.js';
|
|
10
|
+
import { ENGINES, installed, notInstalled } from './engines/index.js';
|
|
11
|
+
import { findBrowser } from './livebrowser.js';
|
|
12
|
+
|
|
13
|
+
const run = (bin, args, timeout = 8000) =>
|
|
14
|
+
new Promise((resolve) => {
|
|
15
|
+
execFile(bin, args, { timeout }, (err, stdout, stderr) => resolve({ ok: !err, out: `${stdout}${stderr}`.trim(), err }));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const firstLine = (s) => s.split('\n')[0].trim();
|
|
19
|
+
|
|
20
|
+
function portFree(port) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const srv = net.createServer();
|
|
23
|
+
srv.once('error', () => resolve(false));
|
|
24
|
+
srv.listen(port, HOST, () => srv.close(() => resolve(true)));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Whether Claude Code has credentials, without reading them: an API key, or a signed-in account. */
|
|
29
|
+
function claudeSignedIn() {
|
|
30
|
+
if (process.env.ANTHROPIC_API_KEY) return 'an API key (ANTHROPIC_API_KEY)';
|
|
31
|
+
try {
|
|
32
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
|
|
33
|
+
if (cfg.oauthAccount) return 'a Claude account';
|
|
34
|
+
} catch {}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function ptyWorks() {
|
|
39
|
+
try {
|
|
40
|
+
const pty = (await import('node-pty')).default;
|
|
41
|
+
return await new Promise((resolve) => {
|
|
42
|
+
const p = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : '/bin/sh', process.platform === 'win32' ? ['/c', 'exit 0'] : ['-c', 'exit 0'], {});
|
|
43
|
+
const t = setTimeout(() => resolve('started but did not finish'), 5000);
|
|
44
|
+
p.onExit(() => {
|
|
45
|
+
clearTimeout(t);
|
|
46
|
+
resolve(null);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
} catch (err) {
|
|
50
|
+
return err.message;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function doctor() {
|
|
55
|
+
const config = loadConfig();
|
|
56
|
+
const checks = [];
|
|
57
|
+
const add = (name, status, detail, fix) => checks.push({ name, status, detail, ...(fix ? { fix } : {}) });
|
|
58
|
+
|
|
59
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
60
|
+
add('Node.js', major >= 20 ? 'ok' : 'fail', `${process.versions.node}`, major >= 20 ? null : 'Install Node.js 20 or newer (https://nodejs.org)');
|
|
61
|
+
|
|
62
|
+
const ptyErr = await ptyWorks();
|
|
63
|
+
add('Terminals (node-pty)', ptyErr ? 'fail' : 'ok', ptyErr ? `can't start a terminal: ${ptyErr}` : 'can start terminals',
|
|
64
|
+
ptyErr ? 'Reinstall: npm install -g ai-agentmanager (or npm rebuild node-pty in a git checkout)' : null);
|
|
65
|
+
|
|
66
|
+
// The daemon's view wins: a login service may have a different PATH than this shell.
|
|
67
|
+
const running = await isRunning();
|
|
68
|
+
const daemonEngines = running ? await api('GET', '/engines').catch(() => null) : null;
|
|
69
|
+
for (const engine of Object.values(ENGINES)) {
|
|
70
|
+
const has = daemonEngines ? daemonEngines.find((e) => e.id === engine.id)?.installed : installed(engine, config);
|
|
71
|
+
const main = engine.id === (config.engine ?? 'claude');
|
|
72
|
+
if (!has) {
|
|
73
|
+
add(engine.label, main ? 'fail' : 'info', main ? 'not found' : 'not installed (optional)', main ? notInstalled(engine, config) : null);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const v = await run(engine.bin(config), ['--version']);
|
|
77
|
+
add(engine.label, 'ok', v.ok ? firstLine(v.out) : 'installed');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const signedIn = claudeSignedIn();
|
|
81
|
+
add('Claude sign-in', signedIn ? 'ok' : 'warn', signedIn ? `uses ${signedIn}` : "couldn't tell whether Claude Code is signed in",
|
|
82
|
+
signedIn ? null : 'Run `claude` in a terminal once and sign in');
|
|
83
|
+
|
|
84
|
+
const git = await run('git', ['--version']);
|
|
85
|
+
add('git', git.ok ? 'ok' : 'fail', git.ok ? firstLine(git.out) : 'not found', git.ok ? null : 'Install git (agents work in git worktrees)');
|
|
86
|
+
|
|
87
|
+
const gh = await run('gh', ['auth', 'status']);
|
|
88
|
+
if (gh.err?.code === 'ENOENT') add('GitHub CLI (gh)', 'warn', 'not installed: PRs and checks are not tracked', 'Install it (https://cli.github.com) and run `gh auth login`');
|
|
89
|
+
else add('GitHub CLI (gh)', gh.ok ? 'ok' : 'warn', gh.ok ? 'signed in' : 'not signed in: PRs and checks are not tracked', gh.ok ? null : 'Run `gh auth login`');
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
fs.mkdirSync(paths.home, { recursive: true });
|
|
93
|
+
fs.accessSync(paths.home, fs.constants.W_OK);
|
|
94
|
+
add('Data folder', 'ok', paths.home);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
add('Data folder', 'fail', `${paths.home}: ${err.message}`, 'Make it writable, or point AGENTMGR_HOME at another folder');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (running) {
|
|
100
|
+
const a = await api('GET', '/about').catch(() => null);
|
|
101
|
+
add('Daemon', 'ok', `running at ${BASE}${a ? ` (version ${a.version}, pid ${a.daemon.pid})` : ''}`);
|
|
102
|
+
} else if (await portFree(PORT)) {
|
|
103
|
+
add('Daemon', 'info', `not running; port ${PORT} is free`, 'Start it with `agentmgr ui`');
|
|
104
|
+
} else {
|
|
105
|
+
add('Daemon', 'fail', `port ${PORT} is used by another program`, 'Stop that program, or set AGENTMGR_PORT to another port');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const chrome = findBrowser(config);
|
|
109
|
+
add('Chrome (live browser tabs)', chrome ? 'ok' : 'warn', chrome ?? 'not found: live browser tabs are unavailable (Lite mode still works)',
|
|
110
|
+
chrome ? null : 'Install Chrome, Chromium, Brave or Edge, or set "browserBin" in config.json');
|
|
111
|
+
|
|
112
|
+
return checks;
|
|
113
|
+
}
|
|
@@ -109,7 +109,7 @@ export default {
|
|
|
109
109
|
launchArgs({ extra = [], permissionMode, model, writableDir, instructions, sessionId, resume, prompt }) {
|
|
110
110
|
const opts = [...extra, ...(MODES[permissionMode] ?? MODES.default)];
|
|
111
111
|
if (model) opts.push('--model', model);
|
|
112
|
-
|
|
112
|
+
for (const d of [writableDir ?? []].flat()) opts.push('--add-dir', d);
|
|
113
113
|
// "--flag=value" so a message starting with "-" isn't read as a flag.
|
|
114
114
|
if (resume) return ['--conversation', sessionId, ...opts, ...(prompt ? [`--prompt-interactive=${prompt}`] : [])];
|
|
115
115
|
const first = [instructions, prompt && (instructions ? `Your task:\n${prompt}` : prompt)].filter(Boolean).join('\n\n---\n\n');
|
package/src/engines/claude.js
CHANGED
|
@@ -11,25 +11,28 @@ export default {
|
|
|
11
11
|
id: 'claude',
|
|
12
12
|
label: 'Claude Code',
|
|
13
13
|
// What the UI may offer for this engine's agents.
|
|
14
|
-
supports: { chooseSessionId: true, cost: true, planHandoff: true, fork: true, oneShot: true },
|
|
14
|
+
supports: { chooseSessionId: true, cost: true, planHandoff: true, fork: true, oneShot: true, subagents: true },
|
|
15
15
|
permissionModes: ['default', 'acceptEdits', 'plan', 'auto', 'bypassPermissions'],
|
|
16
16
|
|
|
17
17
|
bin: (config) => config.engines?.claude?.bin ?? config.claudeBin ?? 'claude',
|
|
18
18
|
extraArgs: (config) => [...(config.engines?.claude?.args ?? config.claudeArgs ?? [])],
|
|
19
19
|
|
|
20
20
|
/** Options for every launch: permissions, model, a folder it may write to, extra instructions. */
|
|
21
|
-
optionArgs({ permissionMode, model, writableDir, instructions }) {
|
|
21
|
+
optionArgs({ permissionMode, model, writableDir, instructions, agents }) {
|
|
22
22
|
const args = [];
|
|
23
23
|
if (permissionMode && permissionMode !== 'default') args.push('--permission-mode', permissionMode);
|
|
24
24
|
if (model) args.push('--model', model);
|
|
25
|
-
|
|
25
|
+
const dirs = [writableDir ?? []].flat();
|
|
26
|
+
if (dirs.length) args.push('--add-dir', ...dirs, '--allowedTools', ...dirs.flatMap((d) => [`Write(/${d}/**)`, `Edit(/${d}/**)`]));
|
|
26
27
|
if (instructions) args.push('--append-system-prompt', instructions);
|
|
28
|
+
// Sub-agents for this session only (a team lead's roles).
|
|
29
|
+
if (agents) args.push('--agents', JSON.stringify(agents));
|
|
27
30
|
return args;
|
|
28
31
|
},
|
|
29
32
|
|
|
30
33
|
/** Full arguments for a launch: options, then the session. */
|
|
31
|
-
launchArgs({ extra = [], permissionMode, model, writableDir, instructions, sessionId, resume, name, prompt }) {
|
|
32
|
-
return [...extra, ...this.optionArgs({ permissionMode, model, writableDir, instructions }), ...this.sessionArgs({ sessionId, resume, name, prompt })];
|
|
34
|
+
launchArgs({ extra = [], permissionMode, model, writableDir, instructions, agents, sessionId, resume, name, prompt }) {
|
|
35
|
+
return [...extra, ...this.optionArgs({ permissionMode, model, writableDir, instructions, agents }), ...this.sessionArgs({ sessionId, resume, name, prompt })];
|
|
33
36
|
},
|
|
34
37
|
|
|
35
38
|
/** A new session under the ID we chose, or a resumed one; prompt is the first (or next) message. */
|
|
@@ -84,6 +87,20 @@ export default {
|
|
|
84
87
|
],
|
|
85
88
|
promptChoices: (screen) => promptChoices(screen),
|
|
86
89
|
|
|
90
|
+
/** Lines of Claude Code's own frame (input box, mode and status bar): never a question to the person. */
|
|
91
|
+
screenChrome: [
|
|
92
|
+
/\b(accept edits|bypass permissions|plan mode|auto mode|auto-accept edits) on\b/i,
|
|
93
|
+
/shift\+tab to cycle/i,
|
|
94
|
+
/\? for shortcuts/i,
|
|
95
|
+
/esc to interrupt/i,
|
|
96
|
+
/ctrl\+[a-z] to (expand|edit|show|hide)/i,
|
|
97
|
+
/update available/i,
|
|
98
|
+
/^[โโโ-]{3,}/,
|
|
99
|
+
/^\/\w+$/,
|
|
100
|
+
/^[โฏ>]\s*$/,
|
|
101
|
+
],
|
|
102
|
+
install: 'Install Claude Code (https://docs.claude.com/en/docs/claude-code) and run `claude` once to sign in',
|
|
103
|
+
|
|
87
104
|
trust: (dir) => trustFolder(dir),
|
|
88
105
|
|
|
89
106
|
/** A one-shot, tool-less call; the prompt goes on stdin and the answer comes back as text. */
|
package/src/engines/codex.js
CHANGED
|
@@ -158,7 +158,7 @@ export default {
|
|
|
158
158
|
// Inline mode keeps the conversation in the terminal's scrollback, which AI Agent Manager replays.
|
|
159
159
|
const opts = [...extra, '--no-alt-screen', ...(MODES[permissionMode] ?? MODES.default)];
|
|
160
160
|
if (model) opts.push('--model', model);
|
|
161
|
-
|
|
161
|
+
for (const d of [writableDir ?? []].flat()) opts.push('--add-dir', d);
|
|
162
162
|
if (resume) return ['resume', ...opts, sessionId, ...(prompt ? [prompt] : [])];
|
|
163
163
|
const first = [instructions, prompt && (instructions ? `Your task:\n${prompt}` : prompt)].filter(Boolean).join('\n\n---\n\n');
|
|
164
164
|
return [...opts, ...(first ? [first] : [])];
|
package/src/engines/index.js
CHANGED
|
@@ -23,6 +23,14 @@ export function installed(engine, config) {
|
|
|
23
23
|
return (process.env.PATH ?? '').split(path.delimiter).some((d) => d && fs.existsSync(path.join(d, bin)));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** What to tell the person when an engine's command isn't on this machine. */
|
|
27
|
+
export function notInstalled(engine, config) {
|
|
28
|
+
const bin = engine.bin(config);
|
|
29
|
+
const how = engine.install ?? `Install ${engine.label}`;
|
|
30
|
+
const where = path.isAbsolute(bin) ? `${bin} doesn't exist` : `\`${bin}\` isn't on the daemon's PATH`;
|
|
31
|
+
return `${engine.label} not found: ${where}. ${how}, then run \`agentmgr restart\`. If it's installed somewhere else, set its path in config.json${engine.id === 'claude' ? ' ("claudeBin") or AGENTMGR_CLAUDE_BIN' : ` ("engines": {"${engine.id}": {"bin": "โฆ"}})`}.`;
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
/** Every engine, with whether it can run here, for pickers and the hub. */
|
|
27
35
|
export function listEngines(config) {
|
|
28
36
|
return Object.values(ENGINES).map((e) => ({ id: e.id, label: e.label, installed: installed(e, config), permissionModes: e.permissionModes, supports: e.supports }));
|
package/src/judge.js
CHANGED
|
@@ -78,6 +78,16 @@ export function stuckPrompt(ctx, finding, events, past = []) {
|
|
|
78
78
|
].join('\n');
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** The screen without the engine's own frame (input box, mode indicators, status bar). */
|
|
82
|
+
export function stripChrome(screen, patterns = []) {
|
|
83
|
+
if (!patterns.length) return screen;
|
|
84
|
+
return screen
|
|
85
|
+
.split('\n')
|
|
86
|
+
.filter((line) => !patterns.some((re) => re.test(line.trim())))
|
|
87
|
+
.join('\n')
|
|
88
|
+
.trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
export function screenPrompt(ctx, screen, past = []) {
|
|
82
92
|
return [
|
|
83
93
|
'You supervise an AI coding agent running in a terminal. It has stopped producing output.',
|
|
@@ -87,6 +97,8 @@ export function screenPrompt(ctx, screen, past = []) {
|
|
|
87
97
|
'- "error": it crashed, hit an error it cannot recover from, or a limit (rate limit, auth, quota)',
|
|
88
98
|
'- "idle": finished a step and is simply waiting for the next instruction, nothing specific to answer',
|
|
89
99
|
'For "question", copy the question in plain words and list the choices it offered, if any.',
|
|
100
|
+
'The tool\'s own input box, mode indicators and status bar (for example "accept edits on", "plan mode on",',
|
|
101
|
+
'"shift+tab to cycle", "? for shortcuts", "update available") are not questions: only the agent\'s own words count.',
|
|
90
102
|
RULES,
|
|
91
103
|
'Shape: {"state": "question|done|error|idle", "confidence": 0.0, "summary": "one line for an inbox", "question": "or empty", "choices": ["..."]}',
|
|
92
104
|
'',
|