ai-agentmanager 1.0.2 → 1.2.1
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 +23 -0
- package/README.md +95 -3
- package/docs/architecture.md +1 -0
- package/package.json +1 -1
- package/public/app.js +594 -21
- package/public/index.html +156 -0
- package/public/style.css +54 -2
- package/src/assistant.js +269 -0
- package/src/cleanup.js +186 -0
- package/src/cli.js +263 -1
- package/src/client.js +2 -1
- 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 +109 -19
- package/src/notify.js +2 -1
- package/src/server.js +67 -4
- package/src/teams.js +984 -0
- package/src/tracker.js +6 -0
package/public/index.html
CHANGED
|
@@ -22,6 +22,8 @@
|
|
|
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="assistant-btn" title="An always-on session that finds new work and starts agents for it">Assistant <span id="assistant-dot" class="assistant-dot" hidden></span></button>
|
|
26
|
+
<button id="cleanup-btn" title="Close agents idle for a while, and delete old closed ones">Clean up</button>
|
|
25
27
|
<button id="alerts-btn" class="local-only" title="Slack alerts when an agent needs you">Alerts</button>
|
|
26
28
|
<button id="remote-btn" class="local-only" title="Open AI Agent Manager from your phone">Remote access</button>
|
|
27
29
|
<button id="ask-btn" title="A general Claude session with no repo or folder">Ask Claude</button>
|
|
@@ -38,6 +40,13 @@
|
|
|
38
40
|
</div>
|
|
39
41
|
<div id="agents-view">
|
|
40
42
|
<div class="counts" id="counts"></div>
|
|
43
|
+
<div class="recent-bar" id="recent-bar" hidden>
|
|
44
|
+
<label class="recent-switch" title="Hide agents created before then (they're still there; turn this off to see them)">
|
|
45
|
+
<input type="checkbox" id="recent-on"> Created in the last
|
|
46
|
+
</label>
|
|
47
|
+
<input type="number" id="recent-days" min="1" max="3650" value="7" aria-label="Days"> days
|
|
48
|
+
<span class="spacer"></span><span id="recent-hidden"></span>
|
|
49
|
+
</div>
|
|
41
50
|
<ul id="agent-list" class="agent-list" aria-label="Agents"></ul>
|
|
42
51
|
</div>
|
|
43
52
|
<div id="backlog-view" hidden></div>
|
|
@@ -192,6 +201,7 @@
|
|
|
192
201
|
<label class="check"><input type="radio" name="when" value="now" checked /> Start now</label>
|
|
193
202
|
<label class="check"><input type="radio" name="when" value="backlog" /> Queue in backlog</label>
|
|
194
203
|
<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>
|
|
204
|
+
<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
205
|
<label class="check">Priority
|
|
196
206
|
<select name="priority" aria-label="Priority">
|
|
197
207
|
<option value="0">P0 urgent</option>
|
|
@@ -211,6 +221,48 @@
|
|
|
211
221
|
<input name="testCommand" placeholder="npm test" />
|
|
212
222
|
</label>
|
|
213
223
|
</div>
|
|
224
|
+
<div class="team-only" hidden>
|
|
225
|
+
<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>
|
|
226
|
+
<label>
|
|
227
|
+
How the team runs
|
|
228
|
+
<select name="teamMode">
|
|
229
|
+
<option value="lead" selected>One lead session; the roles are its sub-agents</option>
|
|
230
|
+
<option value="agents">Separate agents, one per role (any engine)</option>
|
|
231
|
+
</select>
|
|
232
|
+
</label>
|
|
233
|
+
<div class="row">
|
|
234
|
+
<label>
|
|
235
|
+
Critics
|
|
236
|
+
<select name="critics">
|
|
237
|
+
<option value="0">None</option>
|
|
238
|
+
<option value="1" selected>1</option>
|
|
239
|
+
<option value="2">2</option>
|
|
240
|
+
</select>
|
|
241
|
+
</label>
|
|
242
|
+
<label>
|
|
243
|
+
Test command <span class="hint">optional; a hint for the tester</span>
|
|
244
|
+
<input name="teamTest" placeholder="npm test" />
|
|
245
|
+
</label>
|
|
246
|
+
</div>
|
|
247
|
+
<div class="row three">
|
|
248
|
+
<label>
|
|
249
|
+
Analysis rounds <span class="hint">before it stops for you</span>
|
|
250
|
+
<input name="maxRounds" type="number" min="1" max="10" placeholder="3" />
|
|
251
|
+
</label>
|
|
252
|
+
<label>
|
|
253
|
+
Build rounds <span class="hint">fix rounds per request</span>
|
|
254
|
+
<input name="maxLoops" type="number" min="1" max="10" placeholder="3" />
|
|
255
|
+
</label>
|
|
256
|
+
<label>
|
|
257
|
+
Gap rounds without asking <span class="hint">0: it asks you first</span>
|
|
258
|
+
<input name="gapRounds" type="number" min="0" max="10" placeholder="0" />
|
|
259
|
+
</label>
|
|
260
|
+
</div>
|
|
261
|
+
<label>
|
|
262
|
+
Models by role <span class="hint">optional, e.g. <code>analyst=opus reviewer=opus</code>; others use the model above</span>
|
|
263
|
+
<input name="roleModels" placeholder="analyst=opus reviewer=opus" />
|
|
264
|
+
</label>
|
|
265
|
+
</div>
|
|
214
266
|
<label class="check now-only">
|
|
215
267
|
<input type="checkbox" name="start" checked />
|
|
216
268
|
Launch immediately
|
|
@@ -292,6 +344,17 @@
|
|
|
292
344
|
</div>
|
|
293
345
|
</dialog>
|
|
294
346
|
|
|
347
|
+
<dialog id="team-dialog" class="doc-dialog">
|
|
348
|
+
<div class="doc-head">
|
|
349
|
+
<h2 id="team-title">Team</h2>
|
|
350
|
+
<span class="spacer"></span>
|
|
351
|
+
<button type="button" id="team-stop" class="small ghost" title="Close the team's agents; nothing is deleted">Stop team</button>
|
|
352
|
+
<button type="button" id="team-delete" class="small ghost" title="Forget this team">Forget team</button>
|
|
353
|
+
<button type="button" id="team-close" class="small ghost">Close</button>
|
|
354
|
+
</div>
|
|
355
|
+
<div id="team-body" class="race-body team-body"></div>
|
|
356
|
+
</dialog>
|
|
357
|
+
|
|
295
358
|
<dialog id="race-dialog" class="doc-dialog">
|
|
296
359
|
<div class="doc-head">
|
|
297
360
|
<h2 id="race-title">Race</h2>
|
|
@@ -321,6 +384,7 @@
|
|
|
321
384
|
<label class="check"><input type="checkbox" name="ev_stuck" /> seems stuck (same failure, undoing its edits, no progress)</label>
|
|
322
385
|
<label class="check"><input type="checkbox" name="ev_conflict" /> changes the same files as another agent</label>
|
|
323
386
|
<label class="check"><input type="checkbox" name="ev_question" /> asks a question in the inbox</label>
|
|
387
|
+
<label class="check"><input type="checkbox" name="ev_assistant" /> is started by the assistant for new work</label>
|
|
324
388
|
<label class="check"><input type="checkbox" name="ev_waiting" /> finished its turn and is idle after <input name="waitingDelaySec" type="number" min="0" class="num" />s</label>
|
|
325
389
|
<label class="check"><input type="checkbox" name="skipWhenWatching" /> not while I have its terminal open</label>
|
|
326
390
|
<label class="check"><input type="checkbox" name="includeIntent" /> include the intent text in the message</label>
|
|
@@ -337,6 +401,98 @@
|
|
|
337
401
|
</form>
|
|
338
402
|
</dialog>
|
|
339
403
|
|
|
404
|
+
<dialog id="edit-dialog">
|
|
405
|
+
<form id="edit-form" method="dialog" class="alerts-body">
|
|
406
|
+
<h2>Edit agent</h2>
|
|
407
|
+
<label>Name <input name="name" required maxlength="80" /></label>
|
|
408
|
+
<label>Request <span class="hint">what the agent was asked to do</span>
|
|
409
|
+
<textarea name="intent" rows="4"></textarea>
|
|
410
|
+
</label>
|
|
411
|
+
<label class="check" id="edit-tell-row"><input type="checkbox" name="tell" /> Also tell the agent about the changed request</label>
|
|
412
|
+
<label id="edit-project-row">Project
|
|
413
|
+
<select name="project"></select>
|
|
414
|
+
</label>
|
|
415
|
+
<label id="edit-newproject-row" hidden>New project name <input name="newProject" maxlength="60" /></label>
|
|
416
|
+
<div class="row three">
|
|
417
|
+
<label>Priority
|
|
418
|
+
<select name="priority"><option value="0">P0 urgent</option><option value="1">P1 high</option><option value="2">P2 normal</option><option value="3">P3 low</option></select>
|
|
419
|
+
</label>
|
|
420
|
+
<label>Permission mode <select name="permissionMode"></select></label>
|
|
421
|
+
<label>Model <input name="model" placeholder="as configured" /></label>
|
|
422
|
+
</div>
|
|
423
|
+
<p class="hint" id="edit-hint">Moving changes only where the agent is listed; its code stays in its own repo and branch. Permission mode and model apply from its next launch.</p>
|
|
424
|
+
<p id="edit-error" class="error" hidden></p>
|
|
425
|
+
<div class="dialog-actions">
|
|
426
|
+
<span class="spacer"></span>
|
|
427
|
+
<button type="button" id="edit-close" class="ghost">Cancel</button>
|
|
428
|
+
<button class="primary">Save</button>
|
|
429
|
+
</div>
|
|
430
|
+
</form>
|
|
431
|
+
</dialog>
|
|
432
|
+
|
|
433
|
+
<dialog id="assistant-dialog">
|
|
434
|
+
<form id="assistant-form" method="dialog" class="alerts-body">
|
|
435
|
+
<h2>Assistant</h2>
|
|
436
|
+
<p class="hint">An always-on Claude session that works on your behalf. Every few minutes it checks for new work with its own Jira, GitHub and Slack tools, and starts an agent for each new item, or passes news to the agent already on it. It never merges, approves, comments or sends messages as you.</p>
|
|
437
|
+
<div id="assistant-status" class="assistant-status"></div>
|
|
438
|
+
<fieldset>
|
|
439
|
+
<legend>Watch</legend>
|
|
440
|
+
<label class="check"><input type="checkbox" name="src_jira" /> Jira issues newly assigned to me</label>
|
|
441
|
+
<label class="check"><input type="checkbox" name="src_comments" /> New comments and mentions on my Jira issues (passed to the agent on it)</label>
|
|
442
|
+
<label class="check"><input type="checkbox" name="src_reviews" /> GitHub pull requests waiting for my review</label>
|
|
443
|
+
<label class="check"><input type="checkbox" name="src_ci" /> Failing checks on my open pull requests</label>
|
|
444
|
+
<label class="check"><input type="checkbox" name="src_slack" /> Slack messages to me that ask for work</label>
|
|
445
|
+
</fieldset>
|
|
446
|
+
<div class="row three">
|
|
447
|
+
<label>Check every <span class="hint">minutes</span><input name="everyMin" type="number" min="1" /></label>
|
|
448
|
+
<label>New agents a day <span class="hint">at most</span><input name="maxPerDay" type="number" min="0" /></label>
|
|
449
|
+
<label>Running at once <span class="hint">at most</span><input name="maxRunning" type="number" min="0" /></label>
|
|
450
|
+
</div>
|
|
451
|
+
<label>Notes for the assistant <span class="hint">e.g. "Jira project OPS is the @cc repo; skip issues labeled design"</span>
|
|
452
|
+
<textarea name="notes" rows="3"></textarea>
|
|
453
|
+
</label>
|
|
454
|
+
<div id="assistant-activity" class="assistant-activity"></div>
|
|
455
|
+
<p id="assistant-error" class="error" hidden></p>
|
|
456
|
+
<div class="dialog-actions">
|
|
457
|
+
<button type="button" id="assistant-start" class="primary">Start</button>
|
|
458
|
+
<button type="button" id="assistant-check">Check now</button>
|
|
459
|
+
<button type="button" id="assistant-pause">Pause</button>
|
|
460
|
+
<button type="button" id="assistant-open" class="ghost">Open its session</button>
|
|
461
|
+
<span class="spacer"></span>
|
|
462
|
+
<button type="button" id="assistant-close" class="ghost">Close</button>
|
|
463
|
+
<button id="assistant-save">Save</button>
|
|
464
|
+
</div>
|
|
465
|
+
</form>
|
|
466
|
+
</dialog>
|
|
467
|
+
|
|
468
|
+
<dialog id="cleanup-dialog">
|
|
469
|
+
<form id="cleanup-form" method="dialog" class="alerts-body">
|
|
470
|
+
<h2>Clean up old agents</h2>
|
|
471
|
+
<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>
|
|
472
|
+
<label>Strategy
|
|
473
|
+
<select name="strategy">
|
|
474
|
+
<option value="close-delete">Close, then delete, automatically</option>
|
|
475
|
+
<option value="close">Close automatically; delete when I say</option>
|
|
476
|
+
<option value="manual">Nothing automatic; only when I run it</option>
|
|
477
|
+
<option value="off">Off</option>
|
|
478
|
+
</select>
|
|
479
|
+
</label>
|
|
480
|
+
<div class="row">
|
|
481
|
+
<label>Close after <span class="hint">days with no activity</span><input name="closeAfterDays" type="number" min="1" /></label>
|
|
482
|
+
<label>Delete after <span class="hint">days closed</span><input name="deleteAfterDays" type="number" min="1" /></label>
|
|
483
|
+
</div>
|
|
484
|
+
<div id="cleanup-plan" class="cleanup-plan"></div>
|
|
485
|
+
<p id="cleanup-error" class="error" hidden></p>
|
|
486
|
+
<div class="dialog-actions">
|
|
487
|
+
<button type="button" id="cleanup-run-close" title="Close the sessions that are due now">Close due now</button>
|
|
488
|
+
<button type="button" id="cleanup-run" class="danger" title="Close and delete what is due now">Close and delete due now</button>
|
|
489
|
+
<span class="spacer"></span>
|
|
490
|
+
<button type="button" id="cleanup-close" class="ghost">Close</button>
|
|
491
|
+
<button class="primary">Save</button>
|
|
492
|
+
</div>
|
|
493
|
+
</form>
|
|
494
|
+
</dialog>
|
|
495
|
+
|
|
340
496
|
<dialog id="remote-dialog">
|
|
341
497
|
<div class="remote-body">
|
|
342
498
|
<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 {
|
|
@@ -152,8 +157,10 @@ body.term-max .layout { grid-template-columns: 1fr; }
|
|
|
152
157
|
.shortcut { flex: none; font: 600 11px/16px var(--mono); padding: 0 6px; border-radius: 4px; background: var(--accent); color: var(--accent-text); cursor: pointer; }
|
|
153
158
|
.bl-head .shortcut { cursor: default; }
|
|
154
159
|
.project-head .caret { width: 12px; color: var(--muted); flex: none; }
|
|
155
|
-
.project-head .pname { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
160
|
+
.project-head .pname { font-size: 16px; font-weight: 700; 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,21 @@ 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
|
+
.assistant-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-left: 4px; vertical-align: middle; background: var(--muted); }
|
|
388
|
+
.assistant-dot.on { background: var(--green); }
|
|
389
|
+
.assistant-dot.busy { background: var(--yellow); }
|
|
390
|
+
.assistant-status { font-size: 13px; }
|
|
391
|
+
.assistant-status p { margin: 4px 0; }
|
|
392
|
+
.assistant-started { margin: 4px 0; padding-left: 18px; font-size: 13px; }
|
|
393
|
+
.assistant-activity { font-size: 13px; max-height: 240px; overflow: auto; }
|
|
394
|
+
.assistant-activity h4 { margin: 8px 0 4px; }
|
|
395
|
+
.assistant-activity .md h1, .assistant-activity .md h2, .assistant-activity .md h3 { font-size: 13px; margin: 10px 0 2px; color: var(--muted); }
|
|
396
|
+
.assistant-activity .md p, .assistant-activity .md ul { margin: 2px 0; }
|
|
397
|
+
.tag.assistant { background: var(--panel-2); }
|
|
398
|
+
.cleanup-plan { font-size: 13px; max-height: 260px; overflow: auto; }
|
|
399
|
+
.cleanup-plan h4 { margin: 8px 0 4px; font-size: 13px; }
|
|
400
|
+
.cleanup-plan ul { margin: 0; padding-left: 18px; }
|
|
401
|
+
.cleanup-plan li .hint { margin-left: 6px; }
|
|
380
402
|
#login-form { padding: 20px; display: grid; gap: 12px; }
|
|
381
403
|
#login-form h2 { margin: 0; font-size: 17px; }
|
|
382
404
|
|
|
@@ -394,6 +416,8 @@ dialog::backdrop { background: rgba(0, 0, 0, .45); }
|
|
|
394
416
|
#new-form label.check input { width: auto; }
|
|
395
417
|
.hint { color: var(--muted); font-weight: 400; font-size: 12px; }
|
|
396
418
|
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
419
|
+
.row.three { grid-template-columns: 1fr 1fr 1fr; }
|
|
420
|
+
.team-gaps { max-height: 220px; overflow: auto; margin: 6px 0; font-size: 13px; }
|
|
397
421
|
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
398
422
|
.error { color: var(--red); margin: 0; font-size: 13px; }
|
|
399
423
|
|
|
@@ -423,7 +447,7 @@ dialog::backdrop { background: rgba(0, 0, 0, .45); }
|
|
|
423
447
|
.splitter, .desk-only { display: none !important; }
|
|
424
448
|
.sidebar { max-height: 38vh; border-right: 0; border-bottom: 1px solid var(--border); }
|
|
425
449
|
.top-actions input { width: 130px; }
|
|
426
|
-
.row { grid-template-columns: 1fr; }
|
|
450
|
+
.row, .row.three { grid-template-columns: 1fr; }
|
|
427
451
|
.brief-grid { grid-template-columns: 1fr; }
|
|
428
452
|
}
|
|
429
453
|
|
|
@@ -573,3 +597,31 @@ pre.diff .meta { color: var(--muted); font-weight: 600; }
|
|
|
573
597
|
|
|
574
598
|
.agent-list li.agent.idle { opacity: .62; }
|
|
575
599
|
.agent-list li.agent.idle:hover, .agent-list li.agent.idle.selected { opacity: 1; }
|
|
600
|
+
|
|
601
|
+
/* Agent teams */
|
|
602
|
+
.bl-banner.team { border-color: color-mix(in srgb, var(--accent) 50%, var(--border)); }
|
|
603
|
+
.team-steps { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; font-size: 12px; }
|
|
604
|
+
.team-step { padding: 3px 9px; border-radius: 999px; border: 1px solid var(--border); color: var(--muted); }
|
|
605
|
+
.team-step.done { color: var(--green); border-color: color-mix(in srgb, var(--green) 45%, var(--border)); }
|
|
606
|
+
.team-step.now { color: #fff; background: var(--accent); border-color: var(--accent); }
|
|
607
|
+
.team-step.stop { color: #fff; background: var(--yellow, #c08a1e); border-color: transparent; }
|
|
608
|
+
.team-arrow { color: var(--muted); }
|
|
609
|
+
.team-gate { border: 1px solid var(--accent); border-radius: 6px; padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; }
|
|
610
|
+
.team-gate.blocked { border-color: var(--yellow, #c08a1e); }
|
|
611
|
+
.team-gate textarea { width: 100%; }
|
|
612
|
+
.team-timeline { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; font-size: 13px; }
|
|
613
|
+
.team-body > * { flex-shrink: 0; }
|
|
614
|
+
.team-timeline li { display: grid; grid-template-columns: 160px 80px 1fr; gap: 8px; align-items: baseline; }
|
|
615
|
+
.team-timeline .summary { color: var(--muted); }
|
|
616
|
+
.verdict-word { font-weight: 600; }
|
|
617
|
+
.verdict-word.good { color: var(--green); }
|
|
618
|
+
.verdict-word.bad { color: var(--red); }
|
|
619
|
+
.team-file { border: 1px solid var(--border); border-radius: 6px; padding: 8px 12px; max-height: 50vh; overflow: auto; }
|
|
620
|
+
.need-kind.team { background: var(--accent); color: #fff; }
|
|
621
|
+
.flag.role { font-size: 11px; padding: 0 5px; border: 1px solid var(--border); border-radius: 4px; color: var(--muted); }
|
|
622
|
+
@media (max-width: 800px) { .team-timeline li { grid-template-columns: 1fr; gap: 2px; } }
|
|
623
|
+
.team-ask { border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; display: flex; flex-direction: column; gap: 6px; }
|
|
624
|
+
.team-ask textarea { width: 100%; }
|
|
625
|
+
.team-quest { border: 1px solid var(--border); border-radius: 6px; padding: 6px 10px; }
|
|
626
|
+
.team-quest summary { cursor: pointer; }
|
|
627
|
+
.team-quest .team-timeline { margin-top: 8px; }
|
package/src/assistant.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// The assistant: one always-on session that works on the person's behalf. Every few minutes the
|
|
2
|
+
// daemon asks it to check for new work (newly assigned Jira issues, GitHub review requests,
|
|
3
|
+
// failing checks on the person's PRs, new Jira comments, Slack messages) with its own tools, and
|
|
4
|
+
// it starts agents for that work through agentmgr, or passes news to the agent already on it.
|
|
5
|
+
// The daemon keeps the limits: a daily cap, a cap on its agents running at once, and no second
|
|
6
|
+
// agent for the same issue.
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { paths } from './config.js';
|
|
11
|
+
import { leadingJiraKey } from './tracker.js';
|
|
12
|
+
|
|
13
|
+
export const SOURCES = {
|
|
14
|
+
jira: 'Jira issues newly assigned to the person',
|
|
15
|
+
reviews: "GitHub pull requests where the person's review is requested",
|
|
16
|
+
ci: "the person's own open GitHub pull requests whose checks fail",
|
|
17
|
+
comments: 'new comments and mentions on Jira issues the person owns',
|
|
18
|
+
slack: 'Slack messages to the person (direct messages and mentions) that ask for work',
|
|
19
|
+
};
|
|
20
|
+
export const ASSISTANT_DEFAULTS = {
|
|
21
|
+
everyMin: 15,
|
|
22
|
+
sources: Object.keys(SOURCES),
|
|
23
|
+
maxPerDay: 10,
|
|
24
|
+
maxRunning: 5,
|
|
25
|
+
permissionMode: 'auto',
|
|
26
|
+
model: null,
|
|
27
|
+
notes: '',
|
|
28
|
+
};
|
|
29
|
+
const DAY = 86_400_000;
|
|
30
|
+
const BIN = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'agentmgr.js');
|
|
31
|
+
|
|
32
|
+
const err = (status, message) => Object.assign(new Error(message), { status });
|
|
33
|
+
|
|
34
|
+
export const assistantDir = () => path.join(paths.home, 'assistant');
|
|
35
|
+
|
|
36
|
+
/** Settings from config.json's "assistant", with defaults and sane bounds. */
|
|
37
|
+
export function assistantSettings(config = {}) {
|
|
38
|
+
const c = { ...ASSISTANT_DEFAULTS, ...config.assistant };
|
|
39
|
+
const n = (v, d, lo, hi) => (Number.isFinite(Number(v)) && Number(v) >= lo ? Math.min(hi, Math.floor(Number(v))) : d);
|
|
40
|
+
const sources = Array.isArray(c.sources) ? c.sources.filter((s) => SOURCES[s]) : ASSISTANT_DEFAULTS.sources;
|
|
41
|
+
return {
|
|
42
|
+
everyMin: n(c.everyMin, ASSISTANT_DEFAULTS.everyMin, 1, 1440),
|
|
43
|
+
sources,
|
|
44
|
+
maxPerDay: n(c.maxPerDay, ASSISTANT_DEFAULTS.maxPerDay, 0, 500),
|
|
45
|
+
maxRunning: n(c.maxRunning, ASSISTANT_DEFAULTS.maxRunning, 0, 100),
|
|
46
|
+
permissionMode: typeof c.permissionMode === 'string' && c.permissionMode ? c.permissionMode : null,
|
|
47
|
+
model: typeof c.model === 'string' && c.model ? c.model : null,
|
|
48
|
+
notes: typeof c.notes === 'string' ? c.notes.slice(0, 4000) : '',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The assistant's standing instructions (appended to its system prompt on every launch). */
|
|
53
|
+
export function assistantPrompt(s, dir, bin = `node ${BIN}`) {
|
|
54
|
+
const am = bin;
|
|
55
|
+
const watch = s.sources.map((k) => `- ${SOURCES[k]}`).join('\n');
|
|
56
|
+
const how = [];
|
|
57
|
+
if (s.sources.includes('jira')) {
|
|
58
|
+
how.push(
|
|
59
|
+
`Jira, new assignments: with your Jira / Atlassian tools, find unresolved issues assigned to the person that were created or assigned since your last check (on the first check, the last 2 days). ` +
|
|
60
|
+
`For each one you haven't handled, start an agent: ${am} add @<project> "<the issue link> <a one-line summary of what to do>". ` +
|
|
61
|
+
`Start the request with the issue link: the agent is then named after the issue key, and the daemon refuses a second agent for the same key.`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (s.sources.includes('comments')) {
|
|
65
|
+
how.push(
|
|
66
|
+
`Jira, comments and mentions: find new comments on issues the person owns or is mentioned in, since your last check. ` +
|
|
67
|
+
`If an agent is already working on that issue (its name is the issue key; see ${am} ls --json), pass the comment on: ${am} tell <agent id> "New comment on <key> from <author>: <the comment>". ` +
|
|
68
|
+
`A comment that asks for new work on an issue with no agent: start one as above. Otherwise just record it.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (s.sources.includes('reviews')) {
|
|
72
|
+
how.push(
|
|
73
|
+
`GitHub reviews: gh search prs --review-requested=@me --state=open --json url,title,repository,updatedAt. ` +
|
|
74
|
+
`For each pull request you haven't handled, start a review agent in its project: ${am} add @<project> "Review <the PR link>: read the change, check out the branch if needed, and write the review as comments for the person to post. Don't approve, request changes or post anything yourself."`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (s.sources.includes('ci')) {
|
|
78
|
+
how.push(
|
|
79
|
+
`Failing checks: gh search prs --author=@me --state=open --json url,title,repository, then gh pr checks <url> for each. ` +
|
|
80
|
+
`For a pull request with failing checks: if an agent already works on it (${am} ls --json lists each agent's PRs), tell that agent: ${am} tell <agent id> "Checks fail on <PR link>: <which checks>. Find out why and fix it." ` +
|
|
81
|
+
`Otherwise start one: ${am} add @<project> "Checks fail on <PR link>: find out why and fix it on that branch."`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (s.sources.includes('slack')) {
|
|
85
|
+
how.push(
|
|
86
|
+
`Slack: with your Slack tools, read direct messages to the person and messages that mention them, since your last check. ` +
|
|
87
|
+
`When one clearly asks for work in a known project, start an agent for it and include the message link in the request. Don't reply in Slack; record what you did.`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return [
|
|
91
|
+
`You are the person's assistant inside AI Agent Manager (agentmgr). You run all the time in the background and work on their behalf: you find new work and start agents for it, so the work is under way before they look. You don't do the work yourself.`,
|
|
92
|
+
`Watch:\n${watch}`,
|
|
93
|
+
`Each time you get a message starting with "Check now", do one check of every source above, act, record it, and wait for the next message. Don't loop or sleep on your own.`,
|
|
94
|
+
`How:\n${how.map((h, i) => `${i + 1}. ${h}`).join('\n')}`,
|
|
95
|
+
`Projects: ${am} project ls shows the projects and their shortcuts. Pick the project whose repository matches the work (the Jira project, the GitHub repository). ` +
|
|
96
|
+
`If no project fits, use --repo <git URL> when you know the repository; if you don't know where the work belongs, don't start an agent: list it under "Needs you" in the activity file.`,
|
|
97
|
+
`Limits: the daemon allows at most ${s.maxPerDay} new agents a day and ${s.maxRunning} of yours running at once, and refuses a second agent for the same issue. When ${am} add is refused, don't retry it in this check: record the reason.`,
|
|
98
|
+
`Memory: keep ${path.join(dir, 'handled.json')} as a JSON object from item keys (a Jira key, a PR URL, "<key>#comment-<id>", a Slack message link) to { "at": time, "action": "started <agent id>" | "told <agent id>" | "skipped: why" }. Read it at the start of each check and add what you handled, so you never act on the same item twice. Keep "lastCheck" in it too.`,
|
|
99
|
+
`Activity: after each check, append a short entry to ${path.join(dir, 'activity.md')}: a "## <date and time>" heading, then one line per action ("Started OPS-7 (agent 3fa2b1) in @cc: fix the login timeout", "Told 3fa2b1 about a new comment on OPS-7"), and a "Needs you" list for anything you couldn't decide. Write "Nothing new." when nothing came in. AI Agent Manager shows this file to the person.`,
|
|
100
|
+
`Never merge, approve, close, reassign or comment on anything in Jira, GitHub or Slack, and never send messages as the person. Reading, and starting or telling agents, is all you do.`,
|
|
101
|
+
s.notes ? `The person's notes for you:\n${s.notes}` : '',
|
|
102
|
+
]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join('\n\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class Assistant {
|
|
108
|
+
constructor({ manager, store, config = {}, log = (m) => console.log(`[agentmgr] ${m}`) }) {
|
|
109
|
+
this.manager = manager;
|
|
110
|
+
this.store = store;
|
|
111
|
+
this.config = config;
|
|
112
|
+
this.log = log;
|
|
113
|
+
store.data.assistant ??= {};
|
|
114
|
+
this.timer = setInterval(() => this.tick(), 30_000);
|
|
115
|
+
this.timer.unref?.();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
close() {
|
|
119
|
+
clearInterval(this.timer);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
get settings() {
|
|
123
|
+
return assistantSettings(this.config);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
get state() {
|
|
127
|
+
return this.store.data.assistant;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The assistant agent, if there is one. */
|
|
131
|
+
agent() {
|
|
132
|
+
const id = this.state.agentId;
|
|
133
|
+
const a = id && this.store.get(id);
|
|
134
|
+
return a?.kind === 'assistant' ? a : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Change settings; kept in config.json by the caller. Running sessions get them on the next launch. */
|
|
138
|
+
update(patch = {}) {
|
|
139
|
+
if (patch.sources !== undefined && (!Array.isArray(patch.sources) || patch.sources.some((s) => !SOURCES[s]))) {
|
|
140
|
+
throw err(400, `sources must be a list of: ${Object.keys(SOURCES).join(', ')}`);
|
|
141
|
+
}
|
|
142
|
+
for (const k of ['everyMin', 'maxPerDay', 'maxRunning']) {
|
|
143
|
+
if (patch[k] !== undefined && !(Number(patch[k]) >= (k === 'everyMin' ? 1 : 0))) throw err(400, `${k} must be ${k === 'everyMin' ? 1 : 0} or more`);
|
|
144
|
+
}
|
|
145
|
+
const next = assistantSettings({ assistant: { ...this.config.assistant, ...patch } });
|
|
146
|
+
this.config.assistant = next;
|
|
147
|
+
return next;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Create the assistant if needed and start it; the first check follows once it is ready. */
|
|
151
|
+
start() {
|
|
152
|
+
let a = this.agent();
|
|
153
|
+
if (!a) {
|
|
154
|
+
const s = this.settings;
|
|
155
|
+
fs.mkdirSync(assistantDir(), { recursive: true });
|
|
156
|
+
a = this.manager.create({ kind: 'assistant', name: 'Assistant', permissionMode: s.permissionMode || undefined, model: s.model || undefined, start: true });
|
|
157
|
+
this.state.agentId = a.id;
|
|
158
|
+
this.state.nextAt = new Date(Date.now() + 20_000).toISOString();
|
|
159
|
+
this.store.save();
|
|
160
|
+
this.log('assistant started');
|
|
161
|
+
return this.status();
|
|
162
|
+
}
|
|
163
|
+
this.state.paused = false;
|
|
164
|
+
this.state.nextAt ??= new Date(Date.now() + 20_000).toISOString();
|
|
165
|
+
this.store.save();
|
|
166
|
+
if (!this.manager.runtimes.get(a.id)?.pty) this.manager.start(a.id);
|
|
167
|
+
return this.status();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Stop checking and close the session (it keeps its memory; start resumes it). */
|
|
171
|
+
async pause() {
|
|
172
|
+
const a = this.agent();
|
|
173
|
+
this.state.paused = true;
|
|
174
|
+
this.store.save();
|
|
175
|
+
if (a && this.manager.runtimes.get(a.id)?.pty) await this.manager.stop(a.id);
|
|
176
|
+
return this.status();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Ask for a check now (queued until the session is idle). */
|
|
180
|
+
check({ by = 'person' } = {}) {
|
|
181
|
+
const a = this.agent();
|
|
182
|
+
if (!a) throw err(409, 'The assistant is not set up yet: start it first');
|
|
183
|
+
const last = this.state.lastCheckAt;
|
|
184
|
+
const n = (this.state.checks ?? 0) + 1;
|
|
185
|
+
const text = `Check now (check ${n}, ${new Date().toISOString()}${last ? `; last check ${last}` : '; first check'}${by === 'person' ? ', asked by the person' : ''}).`;
|
|
186
|
+
this.manager.tell(a.id, text, { source: 'assistant' });
|
|
187
|
+
this.state.checks = n;
|
|
188
|
+
this.state.lastCheckAt = new Date().toISOString();
|
|
189
|
+
this.state.nextAt = new Date(Date.now() + this.settings.everyMin * 60_000).toISOString();
|
|
190
|
+
this.store.save();
|
|
191
|
+
return this.status();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The scheduler: a check every everyMin minutes while the assistant is on and free. */
|
|
195
|
+
tick(t = Date.now()) {
|
|
196
|
+
const a = this.agent();
|
|
197
|
+
// Paused, or closed by hand: no checks until it is started again.
|
|
198
|
+
if (!a || this.state.paused || a.desired === 'stopped') return;
|
|
199
|
+
if (Date.parse(this.state.nextAt ?? 0) > t) return;
|
|
200
|
+
const rt = this.manager.runtimes.get(a.id);
|
|
201
|
+
// Busy with the last check, or a message still waiting to be typed in: try again shortly.
|
|
202
|
+
if (rt?.busy || this.manager.outbox.get(a.id)?.length) return;
|
|
203
|
+
if (!rt?.pty && a.status === 'preparing') return;
|
|
204
|
+
try {
|
|
205
|
+
this.check({ by: 'schedule' });
|
|
206
|
+
} catch (e) {
|
|
207
|
+
this.log(`assistant: ${e.message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Agents the assistant started. */
|
|
212
|
+
started() {
|
|
213
|
+
const a = this.agent();
|
|
214
|
+
return a ? this.store.list().filter((x) => x.startedBy === a.id) : [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Called before the assistant's own session creates agents: enforces the daily cap, the cap on
|
|
219
|
+
* its running agents, and one agent per issue. Returns the creation options to add.
|
|
220
|
+
*/
|
|
221
|
+
admit(callerId, intents, opts = {}) {
|
|
222
|
+
const a = this.agent();
|
|
223
|
+
if (!a || callerId !== a.id) return {};
|
|
224
|
+
const s = this.settings;
|
|
225
|
+
const mine = this.started();
|
|
226
|
+
const today = mine.filter((x) => Date.now() - Date.parse(x.createdAt) < DAY).length;
|
|
227
|
+
if (today + intents.length > s.maxPerDay) throw err(429, `The assistant's limit is ${s.maxPerDay} new agents a day (${today} so far); not started`);
|
|
228
|
+
const running = mine.filter((x) => this.manager.runtimes.get(x.id)?.pty || x.status === 'preparing').length;
|
|
229
|
+
if (running + intents.length > s.maxRunning) throw err(429, `The assistant's limit is ${s.maxRunning} of its agents running at once (${running} now); not started`);
|
|
230
|
+
for (const intent of intents) {
|
|
231
|
+
const key = opts.name?.trim() || leadingJiraKey(intent);
|
|
232
|
+
const dup = key && this.store.list().find((x) => x.kind === 'task' && x.name.toLowerCase() === key.toLowerCase());
|
|
233
|
+
if (dup) throw err(409, `An agent for ${key} already exists: ${dup.name} (${dup.id}); tell it instead`);
|
|
234
|
+
}
|
|
235
|
+
return { startedBy: a.id };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
activity({ chars = 20_000 } = {}) {
|
|
239
|
+
try {
|
|
240
|
+
const text = fs.readFileSync(path.join(assistantDir(), 'activity.md'), 'utf8');
|
|
241
|
+
return text.length > chars ? text.slice(text.indexOf('\n## ', text.length - chars) + 1 || text.length - chars) : text;
|
|
242
|
+
} catch {
|
|
243
|
+
return '';
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
status() {
|
|
248
|
+
const a = this.agent();
|
|
249
|
+
const rt = a && this.manager.runtimes.get(a.id);
|
|
250
|
+
const mine = this.started();
|
|
251
|
+
return {
|
|
252
|
+
...this.settings,
|
|
253
|
+
agentId: a?.id ?? null,
|
|
254
|
+
on: Boolean(a && !this.state.paused),
|
|
255
|
+
running: Boolean(rt?.pty),
|
|
256
|
+
busy: Boolean(rt?.busy),
|
|
257
|
+
checks: this.state.checks ?? 0,
|
|
258
|
+
lastCheckAt: this.state.lastCheckAt ?? null,
|
|
259
|
+
nextAt: a && !this.state.paused ? (this.state.nextAt ?? null) : null,
|
|
260
|
+
startedToday: mine.filter((x) => Date.now() - Date.parse(x.createdAt) < DAY).length,
|
|
261
|
+
startedTotal: mine.length,
|
|
262
|
+
started: mine
|
|
263
|
+
.sort((x, y) => y.createdAt.localeCompare(x.createdAt))
|
|
264
|
+
.slice(0, 20)
|
|
265
|
+
.map((x) => ({ id: x.id, name: x.name, intent: x.intent, createdAt: x.createdAt })),
|
|
266
|
+
activity: this.activity(),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|