ai-agentmanager 1.0.2 → 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 +13 -0
- package/README.md +58 -3
- package/docs/architecture.md +1 -0
- package/package.json +1 -1
- package/public/app.js +398 -7
- package/public/index.html +90 -0
- package/public/style.css +42 -1
- package/src/cleanup.js +185 -0
- package/src/cli.js +164 -0
- package/src/config.js +4 -0
- package/src/engines/antigravity.js +1 -1
- package/src/engines/claude.js +8 -5
- package/src/engines/codex.js +1 -1
- package/src/manager.js +39 -8
- package/src/server.js +44 -2
- package/src/teams.js +984 -0
- package/src/tracker.js +6 -0
package/public/index.html
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
</div>
|
|
23
23
|
<div class="top-actions">
|
|
24
24
|
<input id="filter" type="search" placeholder="Filter agents" aria-label="Filter agents" />
|
|
25
|
+
<button id="cleanup-btn" title="Close agents idle for a while, and delete old closed ones">Clean up</button>
|
|
25
26
|
<button id="alerts-btn" class="local-only" title="Slack alerts when an agent needs you">Alerts</button>
|
|
26
27
|
<button id="remote-btn" class="local-only" title="Open AI Agent Manager from your phone">Remote access</button>
|
|
27
28
|
<button id="ask-btn" title="A general Claude session with no repo or folder">Ask Claude</button>
|
|
@@ -38,6 +39,13 @@
|
|
|
38
39
|
</div>
|
|
39
40
|
<div id="agents-view">
|
|
40
41
|
<div class="counts" id="counts"></div>
|
|
42
|
+
<div class="recent-bar" id="recent-bar" hidden>
|
|
43
|
+
<label class="recent-switch" title="Hide agents created before then (they're still there; turn this off to see them)">
|
|
44
|
+
<input type="checkbox" id="recent-on"> Created in the last
|
|
45
|
+
</label>
|
|
46
|
+
<input type="number" id="recent-days" min="1" max="3650" value="7" aria-label="Days"> days
|
|
47
|
+
<span class="spacer"></span><span id="recent-hidden"></span>
|
|
48
|
+
</div>
|
|
41
49
|
<ul id="agent-list" class="agent-list" aria-label="Agents"></ul>
|
|
42
50
|
</div>
|
|
43
51
|
<div id="backlog-view" hidden></div>
|
|
@@ -192,6 +200,7 @@
|
|
|
192
200
|
<label class="check"><input type="radio" name="when" value="now" checked /> Start now</label>
|
|
193
201
|
<label class="check"><input type="radio" name="when" value="backlog" /> Queue in backlog</label>
|
|
194
202
|
<label class="check" title="Several agents try the same intent; compare and keep the best"><input type="radio" name="when" value="race" /> Race variants</label>
|
|
203
|
+
<label class="check" title="Agents in roles solve one problem: analyze, critique, implement, test, review"><input type="radio" name="when" value="team" /> Team</label>
|
|
195
204
|
<label class="check">Priority
|
|
196
205
|
<select name="priority" aria-label="Priority">
|
|
197
206
|
<option value="0">P0 urgent</option>
|
|
@@ -211,6 +220,48 @@
|
|
|
211
220
|
<input name="testCommand" placeholder="npm test" />
|
|
212
221
|
</label>
|
|
213
222
|
</div>
|
|
223
|
+
<div class="team-only" hidden>
|
|
224
|
+
<p class="hint">An analyst writes the analysis and critics challenge it until they agree; then an implementer builds it, a tester tests it and a reviewer approves it or sends it back. The reviewer also lists what the request asks for that isn't built yet; the team then asks you whether to run a gap round for it. The team stops for you when the reviewer approves, or if they can't agree. No repository yet? Leave the repository and folder empty (or give a new folder) and the team starts a new project. Tip: pick <b>Accept edits</b> or <b>Auto</b> as the permission mode, or the agents stop to ask before each edit or command.</p>
|
|
225
|
+
<label>
|
|
226
|
+
How the team runs
|
|
227
|
+
<select name="teamMode">
|
|
228
|
+
<option value="lead" selected>One lead session; the roles are its sub-agents</option>
|
|
229
|
+
<option value="agents">Separate agents, one per role (any engine)</option>
|
|
230
|
+
</select>
|
|
231
|
+
</label>
|
|
232
|
+
<div class="row">
|
|
233
|
+
<label>
|
|
234
|
+
Critics
|
|
235
|
+
<select name="critics">
|
|
236
|
+
<option value="0">None</option>
|
|
237
|
+
<option value="1" selected>1</option>
|
|
238
|
+
<option value="2">2</option>
|
|
239
|
+
</select>
|
|
240
|
+
</label>
|
|
241
|
+
<label>
|
|
242
|
+
Test command <span class="hint">optional; a hint for the tester</span>
|
|
243
|
+
<input name="teamTest" placeholder="npm test" />
|
|
244
|
+
</label>
|
|
245
|
+
</div>
|
|
246
|
+
<div class="row three">
|
|
247
|
+
<label>
|
|
248
|
+
Analysis rounds <span class="hint">before it stops for you</span>
|
|
249
|
+
<input name="maxRounds" type="number" min="1" max="10" placeholder="3" />
|
|
250
|
+
</label>
|
|
251
|
+
<label>
|
|
252
|
+
Build rounds <span class="hint">fix rounds per request</span>
|
|
253
|
+
<input name="maxLoops" type="number" min="1" max="10" placeholder="3" />
|
|
254
|
+
</label>
|
|
255
|
+
<label>
|
|
256
|
+
Gap rounds without asking <span class="hint">0: it asks you first</span>
|
|
257
|
+
<input name="gapRounds" type="number" min="0" max="10" placeholder="0" />
|
|
258
|
+
</label>
|
|
259
|
+
</div>
|
|
260
|
+
<label>
|
|
261
|
+
Models by role <span class="hint">optional, e.g. <code>analyst=opus reviewer=opus</code>; others use the model above</span>
|
|
262
|
+
<input name="roleModels" placeholder="analyst=opus reviewer=opus" />
|
|
263
|
+
</label>
|
|
264
|
+
</div>
|
|
214
265
|
<label class="check now-only">
|
|
215
266
|
<input type="checkbox" name="start" checked />
|
|
216
267
|
Launch immediately
|
|
@@ -292,6 +343,17 @@
|
|
|
292
343
|
</div>
|
|
293
344
|
</dialog>
|
|
294
345
|
|
|
346
|
+
<dialog id="team-dialog" class="doc-dialog">
|
|
347
|
+
<div class="doc-head">
|
|
348
|
+
<h2 id="team-title">Team</h2>
|
|
349
|
+
<span class="spacer"></span>
|
|
350
|
+
<button type="button" id="team-stop" class="small ghost" title="Close the team's agents; nothing is deleted">Stop team</button>
|
|
351
|
+
<button type="button" id="team-delete" class="small ghost" title="Forget this team">Forget team</button>
|
|
352
|
+
<button type="button" id="team-close" class="small ghost">Close</button>
|
|
353
|
+
</div>
|
|
354
|
+
<div id="team-body" class="race-body team-body"></div>
|
|
355
|
+
</dialog>
|
|
356
|
+
|
|
295
357
|
<dialog id="race-dialog" class="doc-dialog">
|
|
296
358
|
<div class="doc-head">
|
|
297
359
|
<h2 id="race-title">Race</h2>
|
|
@@ -337,6 +399,34 @@
|
|
|
337
399
|
</form>
|
|
338
400
|
</dialog>
|
|
339
401
|
|
|
402
|
+
<dialog id="cleanup-dialog">
|
|
403
|
+
<form id="cleanup-form" method="dialog" class="alerts-body">
|
|
404
|
+
<h2>Clean up old agents</h2>
|
|
405
|
+
<p class="hint">Closing an agent ends its session; resume it any time. Deleting removes it from your list, with the worktree AI Agent Manager created (Claude's own transcripts stay). Never touched: agents marked <b>Keep</b>, agents working or setting up, and members of a team that is still going. Not deleted: agents with an open pull request, or with uncommitted changes or commits that aren't on any other branch.</p>
|
|
406
|
+
<label>Strategy
|
|
407
|
+
<select name="strategy">
|
|
408
|
+
<option value="close-delete">Close, then delete, automatically</option>
|
|
409
|
+
<option value="close">Close automatically; delete when I say</option>
|
|
410
|
+
<option value="manual">Nothing automatic; only when I run it</option>
|
|
411
|
+
<option value="off">Off</option>
|
|
412
|
+
</select>
|
|
413
|
+
</label>
|
|
414
|
+
<div class="row">
|
|
415
|
+
<label>Close after <span class="hint">days with no activity</span><input name="closeAfterDays" type="number" min="1" /></label>
|
|
416
|
+
<label>Delete after <span class="hint">days closed</span><input name="deleteAfterDays" type="number" min="1" /></label>
|
|
417
|
+
</div>
|
|
418
|
+
<div id="cleanup-plan" class="cleanup-plan"></div>
|
|
419
|
+
<p id="cleanup-error" class="error" hidden></p>
|
|
420
|
+
<div class="dialog-actions">
|
|
421
|
+
<button type="button" id="cleanup-run-close" title="Close the sessions that are due now">Close due now</button>
|
|
422
|
+
<button type="button" id="cleanup-run" class="danger" title="Close and delete what is due now">Close and delete due now</button>
|
|
423
|
+
<span class="spacer"></span>
|
|
424
|
+
<button type="button" id="cleanup-close" class="ghost">Close</button>
|
|
425
|
+
<button class="primary">Save</button>
|
|
426
|
+
</div>
|
|
427
|
+
</form>
|
|
428
|
+
</dialog>
|
|
429
|
+
|
|
340
430
|
<dialog id="remote-dialog">
|
|
341
431
|
<div class="remote-body">
|
|
342
432
|
<h2>Remote access</h2>
|
package/public/style.css
CHANGED
|
@@ -142,6 +142,11 @@ body.term-max .layout { grid-template-columns: 1fr; }
|
|
|
142
142
|
.row.when { align-items: center; }
|
|
143
143
|
.row.when select { width: auto; margin-left: 6px; padding: 2px 6px; }
|
|
144
144
|
.counts { display: flex; gap: 12px; padding: 10px 14px; font-size: 12px; color: var(--muted); border-bottom: 1px solid var(--border); }
|
|
145
|
+
.recent-bar { display: flex; align-items: center; gap: 6px; padding: 6px 14px; font-size: 12px; color: var(--muted); border-bottom: 1px solid var(--border); flex-wrap: wrap; }
|
|
146
|
+
.recent-bar .spacer { flex: 1; }
|
|
147
|
+
.recent-switch { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; white-space: nowrap; margin: 0; font-weight: normal; }
|
|
148
|
+
.recent-switch input { width: auto; margin: 0; }
|
|
149
|
+
.recent-bar input[type="number"] { width: 56px; flex: none; padding: 2px 6px; font-size: 12px; }
|
|
145
150
|
.agent-list { list-style: none; margin: 0; padding: 0; }
|
|
146
151
|
.agent-list li.project { border-bottom: 1px solid var(--border); }
|
|
147
152
|
.project-head {
|
|
@@ -154,6 +159,8 @@ body.term-max .layout { grid-template-columns: 1fr; }
|
|
|
154
159
|
.project-head .caret { width: 12px; color: var(--muted); flex: none; }
|
|
155
160
|
.project-head .pname { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
156
161
|
.project-head .pcounts { display: flex; gap: 6px; align-items: center; color: var(--muted); font-weight: 500; font-size: 12px; }
|
|
162
|
+
.project-head .prun { color: var(--green); }
|
|
163
|
+
.project-head .pcount { white-space: nowrap; }
|
|
157
164
|
.project-head .pill { padding: 0 6px; }
|
|
158
165
|
.project-agents { list-style: none; margin: 0; padding: 0; }
|
|
159
166
|
.agent-list li.agent {
|
|
@@ -377,6 +384,10 @@ body.remote .local-only { display: none; }
|
|
|
377
384
|
.alerts-body .check { display: flex; gap: 8px; align-items: center; font-size: 13px; }
|
|
378
385
|
.alerts-body .check input[type=checkbox] { width: auto; }
|
|
379
386
|
.alerts-body .num { width: 64px; padding: 2px 6px; }
|
|
387
|
+
.cleanup-plan { font-size: 13px; max-height: 260px; overflow: auto; }
|
|
388
|
+
.cleanup-plan h4 { margin: 8px 0 4px; font-size: 13px; }
|
|
389
|
+
.cleanup-plan ul { margin: 0; padding-left: 18px; }
|
|
390
|
+
.cleanup-plan li .hint { margin-left: 6px; }
|
|
380
391
|
#login-form { padding: 20px; display: grid; gap: 12px; }
|
|
381
392
|
#login-form h2 { margin: 0; font-size: 17px; }
|
|
382
393
|
|
|
@@ -394,6 +405,8 @@ dialog::backdrop { background: rgba(0, 0, 0, .45); }
|
|
|
394
405
|
#new-form label.check input { width: auto; }
|
|
395
406
|
.hint { color: var(--muted); font-weight: 400; font-size: 12px; }
|
|
396
407
|
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
408
|
+
.row.three { grid-template-columns: 1fr 1fr 1fr; }
|
|
409
|
+
.team-gaps { max-height: 220px; overflow: auto; margin: 6px 0; font-size: 13px; }
|
|
397
410
|
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
398
411
|
.error { color: var(--red); margin: 0; font-size: 13px; }
|
|
399
412
|
|
|
@@ -423,7 +436,7 @@ dialog::backdrop { background: rgba(0, 0, 0, .45); }
|
|
|
423
436
|
.splitter, .desk-only { display: none !important; }
|
|
424
437
|
.sidebar { max-height: 38vh; border-right: 0; border-bottom: 1px solid var(--border); }
|
|
425
438
|
.top-actions input { width: 130px; }
|
|
426
|
-
.row { grid-template-columns: 1fr; }
|
|
439
|
+
.row, .row.three { grid-template-columns: 1fr; }
|
|
427
440
|
.brief-grid { grid-template-columns: 1fr; }
|
|
428
441
|
}
|
|
429
442
|
|
|
@@ -573,3 +586,31 @@ pre.diff .meta { color: var(--muted); font-weight: 600; }
|
|
|
573
586
|
|
|
574
587
|
.agent-list li.agent.idle { opacity: .62; }
|
|
575
588
|
.agent-list li.agent.idle:hover, .agent-list li.agent.idle.selected { opacity: 1; }
|
|
589
|
+
|
|
590
|
+
/* Agent teams */
|
|
591
|
+
.bl-banner.team { border-color: color-mix(in srgb, var(--accent) 50%, var(--border)); }
|
|
592
|
+
.team-steps { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; font-size: 12px; }
|
|
593
|
+
.team-step { padding: 3px 9px; border-radius: 999px; border: 1px solid var(--border); color: var(--muted); }
|
|
594
|
+
.team-step.done { color: var(--green); border-color: color-mix(in srgb, var(--green) 45%, var(--border)); }
|
|
595
|
+
.team-step.now { color: #fff; background: var(--accent); border-color: var(--accent); }
|
|
596
|
+
.team-step.stop { color: #fff; background: var(--yellow, #c08a1e); border-color: transparent; }
|
|
597
|
+
.team-arrow { color: var(--muted); }
|
|
598
|
+
.team-gate { border: 1px solid var(--accent); border-radius: 6px; padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; }
|
|
599
|
+
.team-gate.blocked { border-color: var(--yellow, #c08a1e); }
|
|
600
|
+
.team-gate textarea { width: 100%; }
|
|
601
|
+
.team-timeline { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; font-size: 13px; }
|
|
602
|
+
.team-body > * { flex-shrink: 0; }
|
|
603
|
+
.team-timeline li { display: grid; grid-template-columns: 160px 80px 1fr; gap: 8px; align-items: baseline; }
|
|
604
|
+
.team-timeline .summary { color: var(--muted); }
|
|
605
|
+
.verdict-word { font-weight: 600; }
|
|
606
|
+
.verdict-word.good { color: var(--green); }
|
|
607
|
+
.verdict-word.bad { color: var(--red); }
|
|
608
|
+
.team-file { border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; max-height: 50vh; overflow: auto; }
|
|
609
|
+
.need-kind.team { background: var(--accent); color: #fff; }
|
|
610
|
+
.flag.role { font-size: 11px; padding: 0 5px; border: 1px solid var(--border); border-radius: 4px; color: var(--muted); }
|
|
611
|
+
@media (max-width: 800px) { .team-timeline li { grid-template-columns: 1fr; gap: 2px; } }
|
|
612
|
+
.team-ask { border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; }
|
|
613
|
+
.team-ask textarea { width: 100%; }
|
|
614
|
+
.team-quest { border: 1px solid var(--border); border-radius: 6px; padding: 6px 10px; }
|
|
615
|
+
.team-quest summary { cursor: pointer; }
|
|
616
|
+
.team-quest .team-timeline { margin-top: 8px; }
|
package/src/cleanup.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Clean up old agents: close sessions nobody has used for a while (they can be resumed), and
|
|
2
|
+
// delete agents that stayed closed long after that, with the worktrees agentmgr created.
|
|
3
|
+
// Strategies: "close-delete" (both, automatically), "close" (close automatically, delete by
|
|
4
|
+
// hand), "manual" (nothing automatic; run it from the Clean up dialog or `agentmgr cleanup`),
|
|
5
|
+
// and "off". Never touched: agents marked Keep, agents still setting up or working, members of a
|
|
6
|
+
// team that is still going, and, for deleting, agents with an open pull request or with work
|
|
7
|
+
// that exists only in their worktree (uncommitted changes or commits on no other branch).
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import * as git from './git.js';
|
|
10
|
+
|
|
11
|
+
export const STRATEGIES = ['close-delete', 'close', 'manual', 'off'];
|
|
12
|
+
export const CLEANUP_DEFAULTS = { strategy: 'close', closeAfterDays: 14, deleteAfterDays: 14 };
|
|
13
|
+
const DAY = 86_400_000;
|
|
14
|
+
const TEAM_DONE = new Set(['done', 'stopped']);
|
|
15
|
+
|
|
16
|
+
const err = (status, message) => Object.assign(new Error(message), { status });
|
|
17
|
+
const days = (ms) => Math.floor(ms / DAY);
|
|
18
|
+
|
|
19
|
+
/** Settings from config.json's "cleanup", with defaults and sane bounds. */
|
|
20
|
+
export function cleanupSettings(config = {}) {
|
|
21
|
+
const c = { ...CLEANUP_DEFAULTS, ...config.cleanup };
|
|
22
|
+
const n = (v, d) => (Number.isFinite(Number(v)) && Number(v) >= 1 ? Math.min(3650, Math.floor(Number(v))) : d);
|
|
23
|
+
return {
|
|
24
|
+
strategy: STRATEGIES.includes(c.strategy) ? c.strategy : CLEANUP_DEFAULTS.strategy,
|
|
25
|
+
closeAfterDays: n(c.closeAfterDays, CLEANUP_DEFAULTS.closeAfterDays),
|
|
26
|
+
deleteAfterDays: n(c.deleteAfterDays, CLEANUP_DEFAULTS.deleteAfterDays),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Work that would be lost with the worktree: uncommitted changes, or commits on no other branch or remote. */
|
|
31
|
+
export async function unsavedWork(agent) {
|
|
32
|
+
const dir = agent.workdir;
|
|
33
|
+
if (!dir || agent.sharedWith || !fs.existsSync(dir)) return null;
|
|
34
|
+
try {
|
|
35
|
+
const dirty = await git.run('git', ['status', '--porcelain'], { cwd: dir });
|
|
36
|
+
if (dirty.trim()) return 'uncommitted changes';
|
|
37
|
+
const branch = agent.branch ?? (await git.run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: dir })).trim();
|
|
38
|
+
const only = await git.run('git', ['rev-list', '--count', 'HEAD', '--not', '--remotes', `--exclude=${branch}`, '--branches'], { cwd: dir });
|
|
39
|
+
const n = Number(only.trim());
|
|
40
|
+
return n > 0 ? `${n} commit${n > 1 ? 's' : ''} not pushed or merged` : null;
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return `couldn't check its worktree (${e.message.split('\n')[0]})`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class Cleanup {
|
|
47
|
+
constructor({ manager, store, config = {}, teams = null, log = (m) => console.log(`[agentmgr] ${m}`) }) {
|
|
48
|
+
this.manager = manager;
|
|
49
|
+
this.store = store;
|
|
50
|
+
this.config = config;
|
|
51
|
+
this.teams = teams;
|
|
52
|
+
this.log = log;
|
|
53
|
+
store.data.cleanup ??= {};
|
|
54
|
+
// A first look a couple of minutes after start, then every few hours.
|
|
55
|
+
this.firstTimer = setTimeout(() => this.sweep().catch((e) => this.log(`clean up: ${e.message}`)), config.cleanupFirstSec ? config.cleanupFirstSec * 1000 : 120_000);
|
|
56
|
+
this.firstTimer.unref?.();
|
|
57
|
+
this.timer = setInterval(() => this.sweep().catch((e) => this.log(`clean up: ${e.message}`)), (config.cleanupEveryMin ?? 360) * 60_000);
|
|
58
|
+
this.timer.unref?.();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
close() {
|
|
62
|
+
clearTimeout(this.firstTimer);
|
|
63
|
+
clearInterval(this.timer);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get settings() {
|
|
67
|
+
return cleanupSettings(this.config);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Change the strategy or the days; kept in config.json by the caller. */
|
|
71
|
+
update(patch = {}) {
|
|
72
|
+
if (patch.strategy !== undefined && !STRATEGIES.includes(patch.strategy)) throw err(400, `strategy must be one of ${STRATEGIES.join(', ')}`);
|
|
73
|
+
for (const k of ['closeAfterDays', 'deleteAfterDays']) {
|
|
74
|
+
if (patch[k] !== undefined && !(Number(patch[k]) >= 1)) throw err(400, `${k} must be 1 or more`);
|
|
75
|
+
}
|
|
76
|
+
const next = cleanupSettings({ cleanup: { ...this.config.cleanup, ...patch } });
|
|
77
|
+
this.config.cleanup = next;
|
|
78
|
+
return next;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Why an agent is left alone whatever its age, or null. */
|
|
82
|
+
protectedBy(agent) {
|
|
83
|
+
if (agent.keep) return 'marked Keep';
|
|
84
|
+
if (agent.status === 'preparing') return 'still setting up';
|
|
85
|
+
const rt = this.manager.runtimes.get(agent.id);
|
|
86
|
+
if (rt?.pty && rt.busy) return 'working';
|
|
87
|
+
const team = agent.team && this.store.data.teams?.[agent.team.id];
|
|
88
|
+
if (team && !TEAM_DONE.has(team.stage)) return 'its team is still going';
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* What a run would do now: agents to close (idle closeAfterDays), agents to delete (closed
|
|
94
|
+
* deleteAfterDays), and agents left alone with the reason. Deleting checks the worktree.
|
|
95
|
+
*/
|
|
96
|
+
async plan({ checkWork = true } = {}) {
|
|
97
|
+
const s = this.settings;
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
const close = [];
|
|
100
|
+
const del = [];
|
|
101
|
+
const kept = [];
|
|
102
|
+
for (const agent of this.store.list()) {
|
|
103
|
+
const rt = this.manager.runtimes.get(agent.id);
|
|
104
|
+
const running = Boolean(rt?.pty);
|
|
105
|
+
const activeAt = Date.parse(this.manager.activeAt(agent, rt) ?? agent.createdAt) || now;
|
|
106
|
+
const idle = now - activeAt;
|
|
107
|
+
const row = { id: agent.id, name: agent.name, idleDays: days(idle) };
|
|
108
|
+
if (running) {
|
|
109
|
+
if (idle < s.closeAfterDays * DAY) continue;
|
|
110
|
+
const why = this.protectedBy(agent);
|
|
111
|
+
if (why) kept.push({ ...row, action: 'close', reason: why });
|
|
112
|
+
else close.push(row);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
// Closed since: when it was closed, or its last activity if that is later (or unknown).
|
|
116
|
+
const closedAt = Math.max(Date.parse(agent.closedAt ?? '') || 0, activeAt);
|
|
117
|
+
const closedFor = now - closedAt;
|
|
118
|
+
if (!agent.closedAt || closedFor < s.deleteAfterDays * DAY) continue;
|
|
119
|
+
const r = { ...row, closedDays: days(closedFor) };
|
|
120
|
+
const why =
|
|
121
|
+
this.protectedBy(agent) ??
|
|
122
|
+
(Object.values(agent.prs ?? {}).some((p) => p.state === 'OPEN') ? 'has an open pull request' : null) ??
|
|
123
|
+
(checkWork ? await unsavedWork(agent) : null);
|
|
124
|
+
if (why) kept.push({ ...r, action: 'delete', reason: why });
|
|
125
|
+
else del.push(r);
|
|
126
|
+
}
|
|
127
|
+
return { close, delete: del, kept };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Close and/or delete what the plan says; `ids` limits it to those agents. */
|
|
131
|
+
async run({ close = true, delete: del = true, ids = null, by = 'person' } = {}) {
|
|
132
|
+
const only = ids ? new Set(ids) : null;
|
|
133
|
+
const plan = await this.plan();
|
|
134
|
+
const done = { at: new Date().toISOString(), by, closed: [], deleted: [], kept: plan.kept, failed: [] };
|
|
135
|
+
if (close) {
|
|
136
|
+
for (const row of plan.close.filter((r) => !only || only.has(r.id))) {
|
|
137
|
+
try {
|
|
138
|
+
await this.manager.stop(row.id);
|
|
139
|
+
this.manager.patch(row.id, { closedBy: 'cleanup' });
|
|
140
|
+
done.closed.push(row);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
done.failed.push({ ...row, reason: e.message });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (del) {
|
|
147
|
+
for (const row of plan.delete.filter((r) => !only || only.has(r.id))) {
|
|
148
|
+
try {
|
|
149
|
+
await this.manager.remove(row.id, { deleteWorkspace: true });
|
|
150
|
+
done.deleted.push(row);
|
|
151
|
+
} catch (e) {
|
|
152
|
+
done.failed.push({ ...row, reason: e.message });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (done.closed.length || done.deleted.length || done.failed.length) {
|
|
157
|
+
this.log(`clean up (${by}): closed ${done.closed.length}, deleted ${done.deleted.length}${done.failed.length ? `, failed ${done.failed.length}` : ''}`);
|
|
158
|
+
}
|
|
159
|
+
this.store.data.cleanup.lastRun = done;
|
|
160
|
+
this.store.save();
|
|
161
|
+
return done;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The automatic pass, as the strategy says. */
|
|
165
|
+
async sweep() {
|
|
166
|
+
// Agents closed before clean up existed have no closing time: count from now, so nothing is
|
|
167
|
+
// deleted the day it is turned on.
|
|
168
|
+
let marked = false;
|
|
169
|
+
for (const agent of this.store.list()) {
|
|
170
|
+
if (!agent.closedAt && !this.manager.runtimes.get(agent.id)?.pty && agent.desired !== 'running') {
|
|
171
|
+
agent.closedAt = new Date().toISOString();
|
|
172
|
+
marked = true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (marked) this.store.save();
|
|
176
|
+
const { strategy } = this.settings;
|
|
177
|
+
if (strategy === 'off' || strategy === 'manual') return null;
|
|
178
|
+
return this.run({ close: true, delete: strategy === 'close-delete', by: 'schedule' });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async status() {
|
|
182
|
+
const s = this.settings;
|
|
183
|
+
return { ...s, ...(await this.plan()), lastRun: this.store.data.cleanup.lastRun ?? null };
|
|
184
|
+
}
|
|
185
|
+
}
|
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,43 @@ 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
|
+
|
|
882
1046
|
program
|
|
883
1047
|
.command('doctor')
|
|
884
1048
|
.description('Check what AI Agent Manager needs on this machine (Node.js, Claude Code, git, gh, the daemon) and how to fix it')
|
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
|
};
|
|
@@ -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. */
|
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] : [])];
|