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
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><rect width="512" height="512" rx="112" fill="#151821"/><path d="M256 76l180 180-180 180L76 256z" fill="#6d8bff"/><path d="M256 166l90 90-90 90-90-90z" fill="#151821"/></svg>
|
package/public/index.html
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
7
7
|
<title>AI Agent Manager</title>
|
|
8
8
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 16 16%22%3E%3Cpath d=%22M8 1l7 7-7 7-7-7z%22 fill=%22%233b5bdb%22/%3E%3C/svg%3E" />
|
|
9
|
+
<link rel="manifest" href="/manifest.webmanifest" crossorigin="use-credentials" />
|
|
10
|
+
<meta name="theme-color" content="#151821" />
|
|
11
|
+
<link rel="apple-touch-icon" href="/icons/icon-192.png" />
|
|
9
12
|
<link rel="stylesheet" href="/vendor/xterm/css/xterm.css" />
|
|
10
13
|
<link rel="stylesheet" href="/style.css" />
|
|
11
14
|
</head>
|
|
@@ -19,6 +22,7 @@
|
|
|
19
22
|
</div>
|
|
20
23
|
<div class="top-actions">
|
|
21
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>
|
|
22
26
|
<button id="alerts-btn" class="local-only" title="Slack alerts when an agent needs you">Alerts</button>
|
|
23
27
|
<button id="remote-btn" class="local-only" title="Open AI Agent Manager from your phone">Remote access</button>
|
|
24
28
|
<button id="ask-btn" title="A general Claude session with no repo or folder">Ask Claude</button>
|
|
@@ -35,6 +39,13 @@
|
|
|
35
39
|
</div>
|
|
36
40
|
<div id="agents-view">
|
|
37
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>
|
|
38
49
|
<ul id="agent-list" class="agent-list" aria-label="Agents"></ul>
|
|
39
50
|
</div>
|
|
40
51
|
<div id="backlog-view" hidden></div>
|
|
@@ -44,10 +55,12 @@
|
|
|
44
55
|
<p>Add one or more intents and a repo or folder to get started.</p>
|
|
45
56
|
</div>
|
|
46
57
|
</aside>
|
|
58
|
+
<div class="splitter split-side" id="split-side" role="separator" aria-orientation="vertical" title="Drag to resize the list; double-click to reset"></div>
|
|
47
59
|
|
|
48
60
|
<section class="detail" id="detail">
|
|
49
61
|
<div class="placeholder" id="placeholder">Select an agent to see its session, folder, repo, PRs and bugs.</div>
|
|
50
62
|
<div id="detail-body" hidden></div>
|
|
63
|
+
<div class="splitter split-term" id="split-term" role="separator" aria-orientation="horizontal" title="Drag to resize the terminal; double-click to reset"></div>
|
|
51
64
|
<div class="term-wrap" id="term-wrap" hidden>
|
|
52
65
|
<div class="term-bar">
|
|
53
66
|
<div class="term-tabs" role="tablist">
|
|
@@ -62,6 +75,8 @@
|
|
|
62
75
|
<button id="term-attach" class="ghost small" title="Attach files or images (you can also drop or paste them into the terminal)">📎 Attach</button>
|
|
63
76
|
<input type="file" id="term-file" multiple hidden>
|
|
64
77
|
<button id="term-fit" class="ghost small" title="Refit the terminal to the panel">Fit</button>
|
|
78
|
+
<button id="term-max" class="ghost small desk-only" title="Expand to fill the window (Ctrl+Shift+E)">⤢ Expand</button>
|
|
79
|
+
<button id="term-full" class="ghost small desk-only" title="Full screen (Esc to leave)">⛶ Full screen</button>
|
|
65
80
|
<button id="term-close" class="ghost small" title="Hide the terminal (the agent keeps running)">Hide terminal</button>
|
|
66
81
|
</div>
|
|
67
82
|
<div id="signin-bar" class="signin-bar" hidden></div>
|
|
@@ -185,6 +200,7 @@
|
|
|
185
200
|
<label class="check"><input type="radio" name="when" value="now" checked /> Start now</label>
|
|
186
201
|
<label class="check"><input type="radio" name="when" value="backlog" /> Queue in backlog</label>
|
|
187
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>
|
|
188
204
|
<label class="check">Priority
|
|
189
205
|
<select name="priority" aria-label="Priority">
|
|
190
206
|
<option value="0">P0 urgent</option>
|
|
@@ -204,6 +220,48 @@
|
|
|
204
220
|
<input name="testCommand" placeholder="npm test" />
|
|
205
221
|
</label>
|
|
206
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>
|
|
207
265
|
<label class="check now-only">
|
|
208
266
|
<input type="checkbox" name="start" checked />
|
|
209
267
|
Launch immediately
|
|
@@ -285,6 +343,17 @@
|
|
|
285
343
|
</div>
|
|
286
344
|
</dialog>
|
|
287
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
|
+
|
|
288
357
|
<dialog id="race-dialog" class="doc-dialog">
|
|
289
358
|
<div class="doc-head">
|
|
290
359
|
<h2 id="race-title">Race</h2>
|
|
@@ -330,6 +399,34 @@
|
|
|
330
399
|
</form>
|
|
331
400
|
</dialog>
|
|
332
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
|
+
|
|
333
430
|
<dialog id="remote-dialog">
|
|
334
431
|
<div class="remote-body">
|
|
335
432
|
<h2>Remote access</h2>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "AI Agent Manager",
|
|
3
|
+
"short_name": "Agent Manager",
|
|
4
|
+
"description": "Run many coding agents in parallel without babysitting terminals.",
|
|
5
|
+
"id": "/",
|
|
6
|
+
"start_url": "/",
|
|
7
|
+
"scope": "/",
|
|
8
|
+
"display": "standalone",
|
|
9
|
+
"background_color": "#0d0f14",
|
|
10
|
+
"theme_color": "#151821",
|
|
11
|
+
"icons": [
|
|
12
|
+
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
|
13
|
+
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
|
|
14
|
+
{ "src": "/icons/icon.svg", "sizes": "any", "type": "image/svg+xml" }
|
|
15
|
+
]
|
|
16
|
+
}
|
package/public/style.css
CHANGED
|
@@ -88,7 +88,27 @@ input, select, textarea {
|
|
|
88
88
|
textarea { resize: vertical; font-family: var(--mono); font-size: 13px; }
|
|
89
89
|
|
|
90
90
|
/* ---------- layout */
|
|
91
|
-
.layout { flex: 1; display: grid; grid-template-columns: 340px 1fr; min-height: 0; }
|
|
91
|
+
.layout { flex: 1; display: grid; grid-template-columns: var(--side-w, 340px) 0 1fr; min-height: 0; }
|
|
92
|
+
|
|
93
|
+
/* Drag handles: the list's width, and the brief above the terminal. */
|
|
94
|
+
.splitter { position: relative; z-index: 3; }
|
|
95
|
+
.splitter::after { content: ''; position: absolute; }
|
|
96
|
+
.split-side::after { top: 0; bottom: 0; left: -4px; width: 8px; cursor: col-resize; }
|
|
97
|
+
.split-term { display: none; flex: none; height: 0; }
|
|
98
|
+
.detail.with-term > #detail-body:not([hidden]) + .split-term { display: block; }
|
|
99
|
+
.split-term::after { left: 0; right: 0; top: -4px; height: 8px; cursor: row-resize; }
|
|
100
|
+
.splitter:hover::after, .splitter.dragging::after { background: var(--accent); opacity: .5; }
|
|
101
|
+
body.resizing { user-select: none; }
|
|
102
|
+
body.resizing iframe, body.resizing .terminal { pointer-events: none; }
|
|
103
|
+
body.resizing.cols { cursor: col-resize; }
|
|
104
|
+
body.resizing.rows { cursor: row-resize; }
|
|
105
|
+
|
|
106
|
+
/* Expanded: the terminal, shell or browser fills the window. */
|
|
107
|
+
body.term-max .topbar, body.term-max .sidebar, body.term-max .split-side,
|
|
108
|
+
body.term-max #detail-body, body.term-max .split-term, body.term-max #placeholder { display: none !important; }
|
|
109
|
+
body.term-max .layout { grid-template-columns: 1fr; }
|
|
110
|
+
.term-wrap:fullscreen { width: 100vw; height: 100vh; }
|
|
111
|
+
.term-bar button.on { background: #262c38; border-color: #58a6ff; }
|
|
92
112
|
.sidebar { border-right: 1px solid var(--border); overflow-y: auto; background: var(--panel); }
|
|
93
113
|
.side-tabs { display: flex; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--panel); z-index: 2; }
|
|
94
114
|
.side-tab { flex: 1; border: 0; border-radius: 0; background: transparent; padding: 9px; font-weight: 600; color: var(--muted); border-bottom: 2px solid transparent; }
|
|
@@ -122,6 +142,11 @@ textarea { resize: vertical; font-family: var(--mono); font-size: 13px; }
|
|
|
122
142
|
.row.when { align-items: center; }
|
|
123
143
|
.row.when select { width: auto; margin-left: 6px; padding: 2px 6px; }
|
|
124
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; }
|
|
125
150
|
.agent-list { list-style: none; margin: 0; padding: 0; }
|
|
126
151
|
.agent-list li.project { border-bottom: 1px solid var(--border); }
|
|
127
152
|
.project-head {
|
|
@@ -134,6 +159,8 @@ textarea { resize: vertical; font-family: var(--mono); font-size: 13px; }
|
|
|
134
159
|
.project-head .caret { width: 12px; color: var(--muted); flex: none; }
|
|
135
160
|
.project-head .pname { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
136
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; }
|
|
137
164
|
.project-head .pill { padding: 0 6px; }
|
|
138
165
|
.project-agents { list-style: none; margin: 0; padding: 0; }
|
|
139
166
|
.agent-list li.agent {
|
|
@@ -172,7 +199,8 @@ textarea { resize: vertical; font-family: var(--mono); font-size: 13px; }
|
|
|
172
199
|
.detail { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
|
|
173
200
|
.placeholder { margin: auto; color: var(--muted); padding: 24px; text-align: center; }
|
|
174
201
|
#detail-body { padding: 16px 20px; overflow-y: auto; flex: 0 1 auto; max-height: 100%; }
|
|
175
|
-
.detail.with-term #detail-body { max-height: 42
|
|
202
|
+
.detail.with-term #detail-body { max-height: var(--brief-h, 42%); border-bottom: 1px solid var(--border); }
|
|
203
|
+
.detail.with-term.brief-sized #detail-body { height: var(--brief-h); flex: none; }
|
|
176
204
|
.d-head { display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap; }
|
|
177
205
|
.d-title { flex: 1 1 420px; min-width: 0; }
|
|
178
206
|
.d-title h2 { margin: 0 0 4px; font-size: 18px; display: flex; gap: 6px 10px; align-items: center; flex-wrap: wrap; }
|
|
@@ -356,6 +384,10 @@ body.remote .local-only { display: none; }
|
|
|
356
384
|
.alerts-body .check { display: flex; gap: 8px; align-items: center; font-size: 13px; }
|
|
357
385
|
.alerts-body .check input[type=checkbox] { width: auto; }
|
|
358
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; }
|
|
359
391
|
#login-form { padding: 20px; display: grid; gap: 12px; }
|
|
360
392
|
#login-form h2 { margin: 0; font-size: 17px; }
|
|
361
393
|
|
|
@@ -373,6 +405,8 @@ dialog::backdrop { background: rgba(0, 0, 0, .45); }
|
|
|
373
405
|
#new-form label.check input { width: auto; }
|
|
374
406
|
.hint { color: var(--muted); font-weight: 400; font-size: 12px; }
|
|
375
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; }
|
|
376
410
|
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
377
411
|
.error { color: var(--red); margin: 0; font-size: 13px; }
|
|
378
412
|
|
|
@@ -399,9 +433,10 @@ dialog::backdrop { background: rgba(0, 0, 0, .45); }
|
|
|
399
433
|
.term-bar { flex-wrap: wrap; row-gap: 6px; }
|
|
400
434
|
.term-bar > .term-tabs { flex: 1 0 100%; }
|
|
401
435
|
.layout { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
|
|
436
|
+
.splitter, .desk-only { display: none !important; }
|
|
402
437
|
.sidebar { max-height: 38vh; border-right: 0; border-bottom: 1px solid var(--border); }
|
|
403
438
|
.top-actions input { width: 130px; }
|
|
404
|
-
.row { grid-template-columns: 1fr; }
|
|
439
|
+
.row, .row.three { grid-template-columns: 1fr; }
|
|
405
440
|
.brief-grid { grid-template-columns: 1fr; }
|
|
406
441
|
}
|
|
407
442
|
|
|
@@ -548,3 +583,34 @@ pre.diff .meta { color: var(--muted); font-weight: 600; }
|
|
|
548
583
|
#import-dialog .import-foot label { display: flex; flex-direction: row; align-items: center; gap: 6px; }
|
|
549
584
|
#import-dialog .import-foot input { width: auto; }
|
|
550
585
|
.judge-line { margin: 14px 12px; line-height: 1.5; }
|
|
586
|
+
|
|
587
|
+
.agent-list li.agent.idle { opacity: .62; }
|
|
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
|
+
}
|