ai-agentmanager 1.2.1 → 1.3.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 CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.0 (2026-10-31)
4
+
5
+ - **Review changes:** each agent's changes in one screen: changed files, the diff file by file (new files included), line comments sent to the agent as one message, **Commit all**, and **Push & open PR** with `gh`. `agentmgr changes`, `comment`, `commit`, `pr`.
6
+ - **Land tab:** every agent's open pull request by what it needs: ready to merge, needs fixes, behind its base, waiting, drafts, with **Merge**, **Auto-merge**, **Update** (merge the base branch in and push, or ask the agent) and **Ask to fix**. `agentmgr land`.
7
+ - **Failing checks and requested changes go back to the agent:** once per commit, at most 3 times per pull request (`landing` in config.json).
8
+ - **Pull request status:** checks, review decision and conflicts on each agent's pull requests.
9
+ - **Triggers:** labeled GitHub issues start agents (in the repos or owners you list, once per issue), `/agent …` comments on an agent's pull request from the repo's owners, members or collaborators reach the agent, and `POST /api/hooks/agents` with a token starts agents from Jira automation, CI or scripts. No session watches, so no tokens are spent. **Assistant → Triggers…**, `agentmgr triggers`.
10
+ - **Linear:** Linear issue links are recognized like Jira's (the agent is named `ENG-42`, one agent per issue for the assistant), and the assistant can watch Linear issues assigned to you.
11
+ - **Repo memory:** agents record what they learn about a repository (`agentmgr learn`, allowed without a prompt), and every later agent there starts with it. **Repo memory** on an agent to view and tidy it, `agentmgr memory`.
12
+ - **Playbooks:** saved requests with `{{values}}` filled in at run time, run in several projects at once (one agent each), by hand or on a schedule (every hour, day, weekday or week). **Backlog → Playbooks**, **Save as playbook…** in New agents, `agentmgr playbook`.
13
+ - **Results:** **Results** in the top bar shows how agents did over a period: PRs opened and merged, merge rate, median time to a first PR and to merge, cost per merged PR, by engine and model, by source and by project. `agentmgr stats`.
14
+ - **Spend and budgets:** the same dialog shows today's cost, the last two weeks and the costliest agents. A budget per agent (an alert at 80%, the session closed at 100% until you raise it) and a daily limit that stops new agents from starting. `agentmgr spend`, `agentmgr edit --budget`.
15
+ - **Getting started:** with no agent open, a checklist shows what is set up (first agent, Slack alerts, assistant or triggers, phone access, a budget), each with the button that does it, and anything this machine is missing (from `agentmgr doctor`).
16
+ - **Refresh** no longer does nothing when a background refresh is already running; it waits and then checks pull requests.
17
+
3
18
  ## 1.2.1 (2026-10-30)
4
19
 
5
20
  - **Edit an agent:** **Edit** on an agent changes its name, its request (and can tell the agent about the change), priority, permission mode and model (from its next launch). `agentmgr edit`.
package/README.md CHANGED
@@ -27,6 +27,13 @@ Run one background [Claude Code](https://claude.com/claude-code) agent per inten
27
27
  - **Priority.** Mark agents P0–P3. P0 and P1 are highlighted, and lists sort by priority.
28
28
  - **Share for review.** One click writes an **Analysis**, **Design**, **Flow** or **Proposal** document from the agent's work. Copy it as Markdown, download it, or print it to PDF with diagrams.
29
29
  - **Human vs AI.** It splits the open questions about the work into the top 5 that need your judgment and the ones a model can answer from the evidence. It answers those, then a second model double-checks every answer.
30
+ - **Review changes.** Each agent's diff file by file, line comments sent back to the agent as one message, then **Commit all** and **Push & open PR**, without leaving the page.
31
+ - **Landing.** A **Land** tab with every agent's open pull request: ready to merge, needs fixes, behind, waiting. Failing checks and requested changes go back to the agent automatically (a few times at most); **Update**, **Ask to fix** and **Merge** in one click.
32
+ - **Triggers.** A label on a GitHub issue starts an agent for it, `/agent …` comments on an agent's pull request reach the agent, and a webhook starts agents from Jira automation or CI. No model watches, so no tokens are spent waiting.
33
+ - **Repo memory.** What one agent learns about a repo (how to run its tests, a gotcha) is recorded in one line and given to every later agent there.
34
+ - **Playbooks.** Saved requests with `{{values}}`, run in several projects at once (one agent each), by hand or every hour, day, weekday or week.
35
+ - **Results.** Merge rate, time to a first PR and to merge, and cost per merged PR, by engine and model, by source and by project.
36
+ - **Budgets.** What agents cost today and lately, a budget per agent (an alert at 80%, closed at 100%) and a daily limit for all of them.
30
37
  - **Plan review.** Optionally, the agent writes an implementation plan and waits for your approval before changing anything.
31
38
  - **Insights.** Why, What, How and Now for any agent, three memorable points each.
32
39
  - **Explain.** One click gives a plain-language explanation of the intent and the insight behind the problem and solution. It comes from a separate tool-less Claude call, so the agent isn't interrupted.
@@ -76,13 +83,13 @@ Runs on macOS and Linux; on Windows, use WSL2. Requires Node 20+, `git`, and the
76
83
 
77
84
  ```sh
78
85
  npm install -g ai-agentmanager # the latest release
79
- npm install -g ai-agentmanager@1.2.1 # or a specific one
86
+ npm install -g ai-agentmanager@1.3.0 # or a specific one
80
87
  ```
81
88
 
82
89
  Or from the repository, at a release tag:
83
90
 
84
91
  ```sh
85
- git clone --branch v1.2.1 --depth 1 https://github.com/jdmunta/ai-agentmanager-core.git
92
+ git clone --branch v1.3.0 --depth 1 https://github.com/jdmunta/ai-agentmanager-core.git
86
93
  cd ai-agentmanager-core
87
94
  npm install
88
95
  npm link # optional: puts `agentmgr` on your PATH
@@ -102,7 +109,7 @@ npm link
102
109
  | Installed with | Update |
103
110
  | --- | --- |
104
111
  | npm | `npm install -g ai-agentmanager@latest` |
105
- | A clone at a release tag | `git fetch --tags && git checkout v1.2.1 && npm install` (the tag you want) |
112
+ | A clone at a release tag | `git fetch --tags && git checkout v1.3.0 && npm install` (the tag you want) |
106
113
  | A clone of `main` | `git pull && npm install` |
107
114
 
108
115
  **Check your setup.** `agentmgr doctor` checks Node.js, Claude Code and its sign-in, git, `gh`, the data folder, the daemon's port and Chrome, and says how to fix anything missing.
@@ -369,6 +376,36 @@ Tick **Review the implementation plan first** in New agent (or the backlog), or
369
376
 
370
377
  Each card has at most **three points of about 12 words each**, so they're easy to remember and repeat. Insights uses the same context and the same separate, tool-less Claude call as Explain, so the agent isn't interrupted. Use Insights for a quick glance and Explain for the full briefing.
371
378
 
379
+ ## Review changes
380
+
381
+ **Review changes** on an agent opens its changes against where its branch started (the merge base with the branch it came from), new files included:
382
+
383
+ - **Files and diff.** The changed files on the left with lines added and removed, and the diff file by file on the right. Click a file header to fold it; very large files start folded.
384
+ - **Line comments.** Click **+** next to a line (or its line number) to comment on it, and add an overall comment if you like. **Send to agent** types them into the agent as one message, each with its file, line and the code it is about, and asks it to address them. Comments not sent yet are kept in this browser.
385
+ - **Commit all** commits everything in the worktree, new files included, with your git name and email.
386
+ - **Push & open PR** pushes the branch and opens a pull request with `gh`, against the branch the agent started from (title from your text, or from the commits; **Draft** if you like). With a pull request already open, it pushes to it. The pull request is linked to the agent.
387
+
388
+ The same from the API: `GET /api/agents/:id/changes`, `POST …/changes/comments`, `…/changes/commit`, `…/changes/pr`. Plugins can refuse a commit or a pull request (`agent.commit`, `agent.pr`).
389
+
390
+ ## Landing pull requests
391
+
392
+ The **Land** tab lists every agent's open pull request by what it needs to get merged, ready ones first:
393
+
394
+ - **Ready to merge**: checks pass and GitHub says it can merge. **Merge** merges it with `gh` (squash by default) and deletes the branch on GitHub.
395
+ - **Needs fixes**: failing checks, changes requested, or conflicts. **Ask to fix** sends them to the agent.
396
+ - **Behind its base**: **Update** merges the branch it targets into the agent's branch and pushes, when the agent is idle and its work is committed. Otherwise, or when the merge conflicts (it is undone), the agent is asked to do it.
397
+ - **Waiting** for checks or a review, and **Drafts**. **Auto-merge** turns on GitHub's auto-merge, so it merges once it may.
398
+
399
+ Each agent's pull requests show the same tags (checks, review, conflicts). Pull requests are checked every couple of minutes for running agents, and every few minutes for closed agents with a PR still open.
400
+
401
+ **Failures go back to the agent by themselves.** When checks fail on an agent's pull request, the agent is told which ones and how to read their logs; when a reviewer requests changes, it is told to read and address the review. Each commit is reported once, and at most 3 times per pull request, so an agent can't loop on a broken main. Change this at the bottom of the Land tab, with `agentmgr land set`, or in config.json:
402
+
403
+ ```json
404
+ "landing": { "fixChecks": true, "fixReviews": true, "maxFixes": 3, "mergeMethod": "squash", "deleteBranch": true }
405
+ ```
406
+
407
+ From a terminal: `agentmgr land` lists them, `agentmgr land update|fix|merge <agent>` (`--auto` for auto-merge). With `agentmgr changes`, `comment`, `commit` and `pr` you can review and open the pull request too. Plugins can refuse a merge (`agent.merge`).
408
+
372
409
  ## Results
373
410
 
374
411
  The **Results** card is the first thing in the Brief panel, and `agentmgr show` lists it too. It shows:
@@ -469,6 +506,27 @@ Queue asks instead of starting them. Use **+ Add** in the Backlog tab, **Queue i
469
506
 
470
507
  The backlog is saved with the rest of the state.
471
508
 
509
+ ## Playbooks
510
+
511
+ Saved requests you run again: **Backlog → Playbooks**, **Save as playbook…** in New agents, or `agentmgr playbook`. A playbook is a request, the projects to run it in (one agent each), and optionally a model, permission mode, priority and schedule:
512
+
513
+ - **Values at run time.** Write `{{name}}` in the request for something you fill in when it runs: "Upgrade `{{package}}` to the latest version, fix what breaks and open a pull request". **Run now** asks for each value; `agentmgr playbook run Upgrade --var package=react`.
514
+ - **Several projects at once.** "Bump the Node version" in every repo starts one agent per project.
515
+ - **On a schedule.** Every hour, day, weekday or week at a time you pick ("every weekday at 9:00, triage yesterday's flaky tests"). A scheduled playbook needs a default for each `{{value}}`. A run missed while the daemon was off runs once when it is back. Budgets and the daily limit apply.
516
+
517
+ Agents started by a playbook are marked **▶ <playbook>**, and the playbook lists the agents of its last run.
518
+
519
+ ```sh
520
+ agentmgr playbook add "Flaky tests" "Find yesterday's flaky tests in CI and fix or quarantine them" --projects web,api --every weekday --at 09:00
521
+ agentmgr playbook ls
522
+ ```
523
+
524
+ ## Repo memory
525
+
526
+ Agents in the same repository share what they learn. When an agent finds out something non-obvious that would save the next one time (how to build or run the tests, a gotcha, where something lives, a convention that isn't written down), it records one line with `agentmgr learn "…"`, which it may run without asking. Every agent that starts in that repo afterwards gets the list with its instructions. Each line says which agent added it and when, and the same thing isn't added twice.
527
+
528
+ **Repo memory → View and edit** on an agent's Workspace card shows the file to tidy (keep it short and true; it is capped at 16 KB), and `agentmgr memory <agent>` prints it. The files live in `~/.agentmanager/memory/`, one per repository. Turn it off with `"memory": false` in config.json.
529
+
472
530
  ## Decision inbox
473
531
 
474
532
  The **Inbox** tab (and `agentmgr inbox`) lists what needs you, across all agents:
@@ -739,6 +797,7 @@ An always-on Claude session that works on your behalf. **Assistant** in the top
739
797
  | It watches | What it does |
740
798
  |---|---|
741
799
  | Jira issues newly assigned to you | Starts an agent in the matching project, with the issue link first so the agent is named after the issue (`OPS-7`) |
800
+ | Linear issues newly assigned to you (turn it on in the dialog) | The same, with the Linear link first (`ENG-42`) |
742
801
  | New comments and mentions on your Jira issues | Passes the comment to the agent on that issue (`agentmgr tell`), or starts one if the comment asks for new work |
743
802
  | GitHub pull requests waiting for your review | Starts a review agent that drafts the review for you to post |
744
803
  | Failing checks on your open pull requests | Tells the agent on that PR to fix it, or starts one |
@@ -758,6 +817,38 @@ agentmgr assistant pause # stop checking and close the sessio
758
817
 
759
818
  Settings live in `config.json` under `"assistant"`. A running assistant reads new watch lists and notes after **Pause** and **Turn on**.
760
819
 
820
+ ## Triggers
821
+
822
+ Start agents from where the work lives, with no session watching and no tokens spent on it. **Assistant → Triggers…**, or `agentmgr triggers`:
823
+
824
+ - **GitHub label.** Every few minutes, open issues with a label (`agentmgr` by default) in the repos or owners you list each get an agent in that repo, named `repo#12`, asked to open a pull request that fixes the issue. A project already on that repo reuses its clone. Each issue starts one agent, once. Labeling an issue needs triage access to the repo, so that is the go-ahead. **Comment on the issue** (off by default) says an agent is on it.
825
+ - **Pull request comments.** A comment that starts with `/agent` on an agent's open pull request, from the repo's owners, members or collaborators, is typed into that agent: `/agent add a test for Safari`. On by default.
826
+ - **Webhook.** `POST /api/hooks/agents` with `Authorization: Bearer <token>` and `{"intent": "…", "project": "<shortcut>"}` (or `"repo": "<git URL>"`, optional `"name"`, `"priority"`, `"url"`) creates an agent, for Jira automation, CI or scripts. Off until you make a token (**New token**, or `agentmgr triggers set --new-token`); the token is shown once. **Only these repos** limits where it may start agents. The daemon listens on this computer only, so other machines reach it through remote access.
827
+
828
+ Agents started this way are marked **⚡ GitHub** or **⚡ webhook**, Slack alerts can announce them, and triggers start at most 20 agents a day (`maxPerDay`). Agents use your default permission mode; a webhook can't pick another. In config.json:
829
+
830
+ ```json
831
+ "triggers": {
832
+ "github": { "enabled": true, "label": "agentmgr", "repos": ["acme/web"], "owners": [], "everyMin": 5, "comment": false },
833
+ "prCommands": { "enabled": true, "prefix": "/agent" },
834
+ "webhook": { "enabled": false, "repos": [] },
835
+ "maxPerDay": 20
836
+ }
837
+ ```
838
+
839
+ ## Results and spend
840
+
841
+ **Results** in the top bar shows how agents did over the last 7, 30 or 90 days (or all time): how many started and how many opened a pull request, PRs merged and closed, the merge rate, the median time from starting an agent to its first PR and to merge, and the cost per merged PR. The same by engine and model (which one gets merged more, for less), by where agents came from (you, the assistant, triggers, the backlog, teams, races) and by project. `agentmgr stats --days 30`.
842
+
843
+ ### Spend and budgets
844
+
845
+ The same dialog shows what agents cost today, the last two weeks and the costliest agents, from the cost Claude Code records for each session (the API price, even on a subscription plan; Codex and Antigravity record none). Set limits there, with `agentmgr spend --per-agent 5 --per-day 50`, or in config.json (`"budgets": { "perAgentUsd": 5, "perDayUsd": 50 }`):
846
+
847
+ - **Each agent.** At 80% of its budget you get an alert. At its budget its session is closed, and it doesn't start or resume until you raise its budget. **Edit** on an agent (or `agentmgr edit <agent> --budget 20`) gives it its own budget.
848
+ - **All agents a day.** Once today's spend reaches it, no new agents start or resume, from the UI, the CLI, the backlog, triggers or the assistant, until tomorrow or a higher limit. Running agents go on.
849
+
850
+ Alerts for both go to Slack with **Alerts → nears or reaches its budget**.
851
+
761
852
  ## Cleaning up old agents
762
853
 
763
854
  Agents you no longer use keep a session open and a worktree on disk. **Clean up** (in the top bar, or `agentmgr cleanup`) closes agents with no activity for a while and deletes agents that stayed closed long after that. Pick a strategy:
package/docs/plugins.md CHANGED
@@ -53,6 +53,9 @@ Policies are synchronous so every code path can call them. Keep them fast; do sl
53
53
  | `agent.start` | Before an agent's engine is launched or resumed (including after a restart) | `agent`, `fresh` |
54
54
  | `agent.message` | Before a message is typed into an agent (`tell`, inbox replies) | `agent`, `text`, `source` |
55
55
  | `agent.remove` | Before an agent is deleted | `agent`, `deleteWorkspace` |
56
+ | `agent.commit` | Before Commit in the review screen commits an agent's worktree | `agent`, `message` |
57
+ | `agent.pr` | Before the review screen pushes an agent's branch and opens a pull request | `agent`, `title`, `draft` |
58
+ | `agent.merge` | Before Merge (or auto-merge) in the Land tab merges an agent's pull request | `agent`, `url`, `method`, `auto` |
56
59
  | `terminal.open` | Before a live terminal is attached (browser, CLI `attach`, hub) | `agent`, `kind` (`claude` or `shell:N`) |
57
60
  | `hub.action` | Before an action the hub asked for, after this machine's own `hub.allow` check | `action`, `agent` |
58
61
 
@@ -60,7 +63,7 @@ Policies are synchronous so every code path can call them. Keep them fast; do sl
60
63
 
61
64
  ## Daemon events (listeners)
62
65
 
63
- `agent.created`, `agent.starting`, `agent.stopping`, `agent.message`, `agent.removed`, `hub.action`, each with the same `agent` summary and an `at` timestamp.
66
+ `agent.created`, `agent.starting`, `agent.stopping`, `agent.message`, `agent.removed`, `agent.commit` (with `sha`), `agent.pr` (with `url`), `agent.merge`, `hub.action`, each with the same `agent` summary and an `at` timestamp.
64
67
 
65
68
  ## Engines
66
69
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-agentmanager",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Run background coding agents (Claude Code, Codex) per intent, and keep track of them from a web UI, your phone or the CLI",
5
5
  "keywords": [
6
6
  "claude",
package/public/app.js CHANGED
@@ -4,6 +4,12 @@ import { marked } from '/vendor/marked/marked.esm.js';
4
4
  import DOMPurify from '/vendor/dompurify/purify.es.mjs';
5
5
  import { touchScroll } from '/touchscroll.js';
6
6
  import { openStream, post, REMOTE } from '/transport.js';
7
+ import { initReview } from '/review.js';
8
+ import { initLand, prChips } from '/land.js';
9
+ import { initTriggers } from '/triggers.js';
10
+ import { initSpend } from '/spend.js';
11
+ import { initPlaybooks } from '/playbooks.js';
12
+ import { initSetup } from '/setup.js';
7
13
 
8
14
  const $ = (sel) => document.querySelector(sel);
9
15
  const agents = new Map();
@@ -93,6 +99,19 @@ async function act(fn, okMsg) {
93
99
  }
94
100
  }
95
101
 
102
+ const reviewUi = initReview({ api, esc, toast, agents, $ });
103
+ const triggersUi = initTriggers({ api, esc, toast, $ });
104
+ initSpend({ api, esc, toast, $ });
105
+ initSetup({ api, esc, $ });
106
+ const playbooksUi = initPlaybooks({ api, esc, toast, $, select: (id) => (showTab('agents'), select(id)) });
107
+ $('#new-playbook').onclick = () => {
108
+ const f = $('#new-form');
109
+ $('#new-dialog').close();
110
+ playbooksUi.open({ intent: f.intents.value.trim(), projects: f.project.value.trim() ? [f.project.value.trim()] : [], model: f.model.value.trim(), permissionMode: f.permissionMode.value });
111
+ };
112
+ $('#triggers-open').onclick = () => triggersUi.open();
113
+ const landUi = initLand({ api, esc, toast, agents, $, select: (id) => (showTab('agents'), select(id)), openReview: (id) => reviewUi.open(id), isOpen: () => sideTab === 'land' });
114
+
96
115
  function statusOf(a) {
97
116
  if (a.runtime.planning) return a.runtime.attention ? 'attention' : 'planning';
98
117
  if (a.status === 'running') {
@@ -605,7 +624,7 @@ function agentItem(a) {
605
624
  const idle = !isActive(a);
606
625
  return `<li data-id="${a.id}" class="agent pr${a.priority ?? 2} ${a.id === selectedId ? 'selected' : ''} ${idle ? 'idle' : ''}">
607
626
  <div class="line1">${prioChip(a.priority ?? 2)}${a.results?.outcome === 'blocked' ? '<span class="outcome-mark" title="Blocked">⛔</span>' : a.results?.outcome === 'done' ? '<span class="outcome-mark" title="Done">✅</span>' : ''}<span class="name">${esc(a.name)}</span>${flags(a)}<span class="pill ${st}">${pillText(st)}</span></div>
608
- <div class="intent">${a.kind === 'general' ? '<span class="tag">general</span> ' : a.kind === 'assistant' ? '<span class="tag assistant">assistant</span> ' : a.startedBy ? '<span class="tag assistant" title="Started by the assistant">🤖 by assistant</span> ' : ''}${a.engine && a.engine !== 'claude' ? `<span class="tag engine">${esc(ENGINE_LABEL[a.engine] ?? a.engine)}</span> ` : ''}${esc(a.intent || (a.kind === 'assistant' ? 'Finds new work and starts agents for it' : 'General session'))}</div>
627
+ <div class="intent">${a.kind === 'general' ? '<span class="tag">general</span> ' : a.kind === 'assistant' ? '<span class="tag assistant">assistant</span> ' : a.startedBy ? '<span class="tag assistant" title="Started by the assistant">🤖 by assistant</span> ' : a.trigger ? `<span class="tag assistant" title="Started from ${a.trigger.source === 'github' ? 'a labeled GitHub issue' : 'the webhook'}${a.trigger.url ? `: ${esc(a.trigger.url)}` : ''}">⚡ ${a.trigger.source === 'github' ? 'GitHub' : 'webhook'}</span> ` : a.playbook ? `<span class="tag assistant" title="Started by the playbook ${esc(a.playbook.name)}">▶ ${esc(a.playbook.name)}</span> ` : ''}${a.engine && a.engine !== 'claude' ? `<span class="tag engine">${esc(ENGINE_LABEL[a.engine] ?? a.engine)}</span> ` : ''}${esc(a.intent || (a.kind === 'assistant' ? 'Finds new work and starts agents for it' : 'General session'))}</div>
609
628
  ${a.status === 'preparing' && a.progress ? `<div class="progress-line">${esc(progressText(a.progress))}</div>` : ''}
610
629
  <div class="meta">
611
630
  <span class="mono">${a.id}</span>
@@ -759,6 +778,7 @@ function renderBacklog() {
759
778
  <span class="spacer"></span>
760
779
  <button class="link" data-bl="toggle-run" title="${backlogState.autoRun ? 'Stop starting new items (running ones continue)' : 'Start picking up queued items again'}">${backlogState.autoRun ? '⏸ Pause' : '▶ Resume'}</button>
761
780
  <button class="link" data-bl="add">+ Add</button>
781
+ <button class="link" data-bl="playbooks" title="Saved requests you run again, by hand or on a schedule">Playbooks</button>
762
782
  </div>
763
783
  ${!backlogState.autoRun ? '<p class="hint bl-paused">Paused: queued items wait until you resume.</p>' : ''}
764
784
  ${items.length
@@ -807,7 +827,9 @@ function showTab(tab) {
807
827
  $('#agents-view').hidden = tab !== 'agents';
808
828
  $('#backlog-view').hidden = tab !== 'backlog';
809
829
  $('#inbox-view').hidden = tab !== 'inbox';
830
+ $('#land-view').hidden = tab !== 'land';
810
831
  if (tab === 'inbox') loadJudge();
832
+ if (tab === 'land') landUi.load();
811
833
  renderBacklog();
812
834
  renderInbox();
813
835
  }
@@ -830,6 +852,8 @@ $('#backlog-view').addEventListener('click', async (e) => {
830
852
  return blApi('POST', '/backlog/settings', { autoRun: !backlogState.autoRun });
831
853
  case 'add':
832
854
  return openNewDialog('backlog');
855
+ case 'playbooks':
856
+ return playbooksUi.open();
833
857
  case 'limit': {
834
858
  const n = prompt(`How many "${el.dataset.name}" items should agents work on at once?`, el.textContent.split(' ')[0]);
835
859
  if (n === null) return;
@@ -1508,6 +1532,7 @@ function prRow(a, p) {
1508
1532
  <a class="grow" href="${esc(p.url)}" target="_blank" rel="noopener" title="${esc(p.url)}">${label} ${esc(p.title ?? p.repo ?? p.url)}</a>
1509
1533
  <span class="tag" title="How this was found">${esc(p.source)}</span>
1510
1534
  <button class="link" data-unlink="pr" data-key="${esc(p.url)}" title="Remove">✕</button>
1535
+ ${p.state === 'OPEN' && (p.checks || p.review || p.mergeState) ? `<div class="pr-chips">${prChips(p, esc)}</div>` : ''}
1511
1536
  </li>`;
1512
1537
  }
1513
1538
 
@@ -1563,6 +1588,7 @@ function renderDetail() {
1563
1588
  : `<button data-act="start" ${a.status === 'preparing' ? 'disabled' : ''}>Resume in background</button>`}
1564
1589
  <button data-act="fresh" ${running || a.status === 'preparing' ? 'disabled' : ''} title="Start over with a new session ID (old ones are kept)">New session</button>
1565
1590
  <button data-act="shell" ${a.workdir && a.status !== 'preparing' ? '' : 'disabled'} title="A separate terminal in this agent's folder for running commands by hand">Open shell</button>
1591
+ ${noRepo(a) ? '' : `<button data-act="changes" ${a.workdir && a.status !== 'preparing' ? '' : 'disabled'} title="Read its changes, comment on lines for the agent, commit and open a pull request">Review changes${a.git?.dirtyFiles ? ` <span class="count">${a.git.dirtyFiles}</span>` : ''}</button>`}
1566
1592
  <button data-act="fork" ${a.workdir && a.git?.branch ? '' : 'disabled'} title="A new agent with this one's code and conversation, in its own worktree, to try another approach">Fork</button>
1567
1593
  <button data-act="edit" title="Change its name, request, project, priority, permission mode or model">Edit</button>
1568
1594
  <button data-act="refresh" title="Re-scan git, PRs and transcripts">Refresh</button>
@@ -1620,7 +1646,7 @@ function renderDetail() {
1620
1646
  <br>${fmtTokens(u.total)} total incl. cache · ${u.requests} requests</div>`
1621
1647
  : ''}`
1622
1648
  : '—'}</dd>
1623
- <dt>Cost</dt><dd>${totalCost ? `$${totalCost.toFixed(2)} <span class="hint">(reported by Claude Code)</span>` : '—'}</dd>
1649
+ <dt>Cost</dt><dd>${totalCost ? `$${totalCost.toFixed(2)}${a.budgetUsd ? ` of its $${a.budgetUsd.toFixed(2)} budget` : ''} <span class="hint">(reported by Claude Code)</span>` : a.budgetUsd ? `— <span class="hint">budget $${a.budgetUsd.toFixed(2)}</span>` : '—'}${a.budgetHit ? ' <span class="tag fail" title="Its session was closed at its budget; raise it in Edit to continue">budget reached</span>' : ''}</dd>
1624
1650
  <dt>Mode</dt><dd>${esc(a.permissionMode)}${a.model ? ` · ${esc(a.model)}` : ''}</dd>
1625
1651
  </dl>
1626
1652
  </div>
@@ -1635,6 +1661,7 @@ function renderDetail() {
1635
1661
  <dt>Head</dt><dd>${a.git?.head ? `<code>${esc(a.git.head.sha)}</code> ${esc(a.git.head.subject)}` : '—'}</dd>
1636
1662
  ${a.baseRef ? `<dt>Branched from</dt><dd><code>${esc(a.baseRef)}</code></dd>` : ''}
1637
1663
  ${a.worktree ? `<dt>Worktree of</dt><dd><code>${esc(a.sourceDir)}</code></dd>` : ''}
1664
+ ${noRepo(a) ? '' : `<dt>Repo memory</dt><dd><button class="link" data-act="memory" title="What agents learned working in this repo; every agent here gets it">View and edit</button></dd>`}
1638
1665
  </dl>
1639
1666
  </div>
1640
1667
 
@@ -1699,6 +1726,7 @@ function renderDetail() {
1699
1726
  function render() {
1700
1727
  renderList();
1701
1728
  renderBacklog();
1729
+ landUi.render();
1702
1730
  renderInbox();
1703
1731
  renderRace();
1704
1732
  renderTeam();
@@ -2906,6 +2934,10 @@ $('#detail-body').addEventListener('click', async (e) => {
2906
2934
  case 'shell':
2907
2935
  term.open(a.id, 'shell:1');
2908
2936
  break;
2937
+ case 'changes':
2938
+ return reviewUi.open(a.id);
2939
+ case 'memory':
2940
+ return openMemory(a);
2909
2941
  case 'fork':
2910
2942
  forkAgent(a.id);
2911
2943
  break;
@@ -3511,6 +3543,34 @@ let enginesCache = null;
3511
3543
  // The project picked in the dialog: "" while it is listed under its own repo or folder.
3512
3544
  const editProject = (a) => (a.projectMove || a.project ? (a.projectInfo?.shortcut ?? '') : '');
3513
3545
 
3546
+ let memoryAgent = null;
3547
+ async function openMemory(a) {
3548
+ memoryAgent = a.id;
3549
+ const f = $('#memory-form');
3550
+ $('#memory-error').hidden = true;
3551
+ try {
3552
+ const m = await api('GET', `/agents/${a.id}/memory`);
3553
+ $('#memory-repo').textContent = a.projectInfo?.full || a.projectInfo?.name || a.repo || 'this repo';
3554
+ f.text.value = m.text;
3555
+ $('#memory-meta').textContent = `${m.entries} learning${m.entries === 1 ? '' : 's'} · ${(m.bytes / 1000).toFixed(1)} of ${m.max / 1000} KB`;
3556
+ $('#memory-dialog').showModal();
3557
+ } catch (err) {
3558
+ toast(`Error: ${err.message}`, 5000);
3559
+ }
3560
+ }
3561
+ $('#memory-form').addEventListener('submit', async (e) => {
3562
+ e.preventDefault();
3563
+ try {
3564
+ await api('PUT', `/agents/${memoryAgent}/memory`, { text: $('#memory-form').text.value });
3565
+ $('#memory-dialog').close();
3566
+ toast('Saved; agents get it from their next start');
3567
+ } catch (err) {
3568
+ $('#memory-error').hidden = false;
3569
+ $('#memory-error').textContent = err.message;
3570
+ }
3571
+ });
3572
+ $('#memory-close').onclick = () => $('#memory-dialog').close();
3573
+
3514
3574
  async function openEdit(a, focus) {
3515
3575
  editing = a;
3516
3576
  const f = $('#edit-form');
@@ -3519,6 +3579,7 @@ async function openEdit(a, focus) {
3519
3579
  f.tell.checked = false;
3520
3580
  f.priority.value = String(a.priority ?? 2);
3521
3581
  f.model.value = a.model ?? '';
3582
+ f.budgetUsd.value = a.budgetUsd ?? '';
3522
3583
  $('#edit-project-row').hidden = noRepo(a);
3523
3584
  $('#edit-tell-row').hidden = !a.intent;
3524
3585
  // Its own repo or folder, every known project, or a new name.
@@ -3554,6 +3615,7 @@ $('#edit-form').addEventListener('submit', async (e) => {
3554
3615
  if (Number(f.priority.value) !== (a.priority ?? 2)) patch.priority = Number(f.priority.value);
3555
3616
  if ((f.permissionMode.value || null) !== (a.permissionMode ?? null)) patch.permissionMode = f.permissionMode.value || null;
3556
3617
  if ((f.model.value.trim() || null) !== (a.model ?? null)) patch.model = f.model.value.trim() || null;
3618
+ if ((f.budgetUsd.value === '' ? null : Number(f.budgetUsd.value)) !== (a.budgetUsd ?? null)) patch.budgetUsd = f.budgetUsd.value === '' ? null : Number(f.budgetUsd.value);
3557
3619
  if (!noRepo(a)) {
3558
3620
  const target = f.project.value === '__new' ? f.newProject.value.trim() : f.project.value;
3559
3621
  const was = editProject(a);
@@ -3578,7 +3640,7 @@ $('#edit-form').addEventListener('submit', async (e) => {
3578
3640
 
3579
3641
  // ------------------------------------------------------------------ assistant
3580
3642
 
3581
- const ASSISTANT_SOURCES = ['jira', 'comments', 'reviews', 'ci', 'slack'];
3643
+ const ASSISTANT_SOURCES = ['jira', 'comments', 'reviews', 'ci', 'slack', 'linear'];
3582
3644
  let assistantState = null;
3583
3645
 
3584
3646
  function renderAssistantDot() {
package/public/index.html CHANGED
@@ -23,6 +23,7 @@
23
23
  <div class="top-actions">
24
24
  <input id="filter" type="search" placeholder="Filter agents" aria-label="Filter agents" />
25
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="spend-btn" title="Results across agents (merge rate, time to PR, cost per merged PR), what they cost, and spending limits">Results <span id="spend-today" class="spend-today"></span></button>
26
27
  <button id="cleanup-btn" title="Close agents idle for a while, and delete old closed ones">Clean up</button>
27
28
  <button id="alerts-btn" class="local-only" title="Slack alerts when an agent needs you">Alerts</button>
28
29
  <button id="remote-btn" class="local-only" title="Open AI Agent Manager from your phone">Remote access</button>
@@ -37,6 +38,7 @@
37
38
  <button class="side-tab active" data-tab="agents" role="tab">Agents</button>
38
39
  <button class="side-tab" data-tab="inbox" role="tab" title="Everything waiting on you: prompts, questions, stuck agents, overlaps">Inbox <span id="inbox-count" class="badge"></span></button>
39
40
  <button class="side-tab" data-tab="backlog" role="tab">Backlog <span id="backlog-count" class="badge"></span></button>
41
+ <button class="side-tab" data-tab="land" role="tab" title="Agents' open pull requests: what each needs to get merged">Land <span id="land-count" class="badge"></span></button>
40
42
  </div>
41
43
  <div id="agents-view">
42
44
  <div class="counts" id="counts"></div>
@@ -51,6 +53,7 @@
51
53
  </div>
52
54
  <div id="backlog-view" hidden></div>
53
55
  <div id="inbox-view" hidden></div>
56
+ <div id="land-view" hidden></div>
54
57
  <div id="empty" class="empty" hidden>
55
58
  <p>No agents yet.</p>
56
59
  <p>Add one or more intents and a repo or folder to get started.</p>
@@ -269,6 +272,8 @@
269
272
  </label>
270
273
  <p id="new-error" class="error" hidden></p>
271
274
  <div class="dialog-actions">
275
+ <button type="button" id="new-playbook" class="ghost" title="Keep this request to run again, in several projects or on a schedule">Save as playbook…</button>
276
+ <span class="spacer"></span>
272
277
  <button type="button" id="new-cancel" class="ghost">Cancel</button>
273
278
  <button type="submit" class="primary">Create agents</button>
274
279
  </div>
@@ -355,6 +360,15 @@
355
360
  <div id="team-body" class="race-body team-body"></div>
356
361
  </dialog>
357
362
 
363
+ <dialog id="review-dialog" class="doc-dialog review-dialog">
364
+ <div class="doc-head">
365
+ <h2 id="review-title">Review</h2>
366
+ <span class="spacer"></span>
367
+ <button type="button" id="review-close" class="small ghost">Close</button>
368
+ </div>
369
+ <div id="review-body" class="review-body"></div>
370
+ </dialog>
371
+
358
372
  <dialog id="race-dialog" class="doc-dialog">
359
373
  <div class="doc-head">
360
374
  <h2 id="race-title">Race</h2>
@@ -384,7 +398,8 @@
384
398
  <label class="check"><input type="checkbox" name="ev_stuck" /> seems stuck (same failure, undoing its edits, no progress)</label>
385
399
  <label class="check"><input type="checkbox" name="ev_conflict" /> changes the same files as another agent</label>
386
400
  <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>
401
+ <label class="check"><input type="checkbox" name="ev_assistant" /> is started for new work (by the assistant, a GitHub label or a webhook)</label>
402
+ <label class="check"><input type="checkbox" name="ev_budget" /> nears or reaches its budget, or today's spend reaches the daily limit</label>
388
403
  <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>
389
404
  <label class="check"><input type="checkbox" name="skipWhenWatching" /> not while I have its terminal open</label>
390
405
  <label class="check"><input type="checkbox" name="includeIntent" /> include the intent text in the message</label>
@@ -420,6 +435,7 @@
420
435
  <label>Permission mode <select name="permissionMode"></select></label>
421
436
  <label>Model <input name="model" placeholder="as configured" /></label>
422
437
  </div>
438
+ <label>Budget <span class="hint">US dollars; empty for the default limit</span><input name="budgetUsd" type="number" min="0.01" step="0.01" placeholder="default" /></label>
423
439
  <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
440
  <p id="edit-error" class="error" hidden></p>
425
441
  <div class="dialog-actions">
@@ -442,6 +458,7 @@
442
458
  <label class="check"><input type="checkbox" name="src_reviews" /> GitHub pull requests waiting for my review</label>
443
459
  <label class="check"><input type="checkbox" name="src_ci" /> Failing checks on my open pull requests</label>
444
460
  <label class="check"><input type="checkbox" name="src_slack" /> Slack messages to me that ask for work</label>
461
+ <label class="check"><input type="checkbox" name="src_linear" /> Linear issues newly assigned to me</label>
445
462
  </fieldset>
446
463
  <div class="row three">
447
464
  <label>Check every <span class="hint">minutes</span><input name="everyMin" type="number" min="1" /></label>
@@ -458,6 +475,7 @@
458
475
  <button type="button" id="assistant-check">Check now</button>
459
476
  <button type="button" id="assistant-pause">Pause</button>
460
477
  <button type="button" id="assistant-open" class="ghost">Open its session</button>
478
+ <button type="button" id="triggers-open" class="ghost" title="Start agents from GitHub labels, /agent PR comments and a webhook, with no session watching">Triggers…</button>
461
479
  <span class="spacer"></span>
462
480
  <button type="button" id="assistant-close" class="ghost">Close</button>
463
481
  <button id="assistant-save">Save</button>
@@ -465,6 +483,120 @@
465
483
  </form>
466
484
  </dialog>
467
485
 
486
+ <dialog id="triggers-dialog">
487
+ <form id="triggers-form" method="dialog" class="alerts-body">
488
+ <h2>Triggers</h2>
489
+ <p class="hint">Start agents from where the work is, with no model watching (no tokens spent on it). Agents started this way are marked <b>⚡ GitHub</b> or <b>⚡ webhook</b>.</p>
490
+ <fieldset>
491
+ <legend>GitHub label</legend>
492
+ <label class="check"><input type="checkbox" name="gh_enabled" /> Start an agent for each open issue with this label</label>
493
+ <div class="row">
494
+ <label>Label <input name="gh_label" placeholder="agentmgr" /></label>
495
+ <label>Check every <span class="hint">minutes</span><input name="gh_everyMin" type="number" min="1" /></label>
496
+ </div>
497
+ <label>In these repos <span class="hint">owner/name, comma or space separated</span><input name="gh_repos" placeholder="acme/web acme/api" /></label>
498
+ <label>Or any repo of these owners <input name="gh_owners" placeholder="acme" /></label>
499
+ <label class="check"><input type="checkbox" name="gh_comment" /> Comment on the issue that an agent is on it</label>
500
+ </fieldset>
501
+ <fieldset>
502
+ <legend>Pull request comments</legend>
503
+ <label class="check"><input type="checkbox" name="pc_enabled" /> Pass comments that start with <input name="pc_prefix" class="inline" placeholder="/agent" /> on an agent's pull request to that agent</label>
504
+ <p class="hint">Only from the repo's owners, members and collaborators.</p>
505
+ </fieldset>
506
+ <fieldset>
507
+ <legend>Webhook</legend>
508
+ <label class="check"><input type="checkbox" name="wh_enabled" /> Accept <code>POST /api/hooks/agents</code> with the token</label>
509
+ <div id="triggers-token" class="hint"></div>
510
+ <button type="button" id="triggers-newtoken" class="small">New token</button>
511
+ <label>Only these repos <span class="hint">optional</span><input name="wh_repos" /></label>
512
+ </fieldset>
513
+ <label>Agents started by triggers a day <span class="hint">at most</span><input name="maxPerDay" type="number" min="0" /></label>
514
+ <div id="triggers-status" class="hint"></div>
515
+ <p id="triggers-error" class="error" hidden></p>
516
+ <div class="dialog-actions">
517
+ <button type="button" id="triggers-poll">Check now</button>
518
+ <span class="spacer"></span>
519
+ <button type="button" id="triggers-close" class="ghost">Close</button>
520
+ <button class="primary">Save</button>
521
+ </div>
522
+ </form>
523
+ </dialog>
524
+
525
+ <dialog id="memory-dialog">
526
+ <form id="memory-form" method="dialog" class="alerts-body">
527
+ <h2>Repo memory</h2>
528
+ <p class="hint">What agents learned working in <b id="memory-repo"></b>. Every agent in this repo gets it when it starts, and adds to it with <code>agentmgr learn</code>. Tidy it here: keep it short and true.</p>
529
+ <textarea name="text" rows="14" class="memory-text" placeholder="- npm test needs a running Postgres: docker compose up db (you, 2026-10-31)"></textarea>
530
+ <p id="memory-meta" class="hint"></p>
531
+ <p id="memory-error" class="error" hidden></p>
532
+ <div class="dialog-actions">
533
+ <span class="spacer"></span>
534
+ <button type="button" id="memory-close" class="ghost">Cancel</button>
535
+ <button class="primary">Save</button>
536
+ </div>
537
+ </form>
538
+ </dialog>
539
+
540
+ <dialog id="playbooks-dialog" class="doc-dialog">
541
+ <div class="doc-head">
542
+ <h2>Playbooks</h2>
543
+ <span class="spacer"></span>
544
+ <button type="button" id="pb-new" class="small">+ New playbook</button>
545
+ <button type="button" id="pb-close" class="small ghost">Close</button>
546
+ </div>
547
+ <div class="race-body">
548
+ <p class="hint">Saved requests you run again, in one or more projects (one agent each), by hand or on a schedule. Write <code>{{name}}</code> for a value you fill in when it runs.</p>
549
+ <div id="pb-list"></div>
550
+ <form id="pb-form" class="alerts-body pb-form" hidden>
551
+ <input type="hidden" name="id" />
552
+ <label>Name <input name="name" required maxlength="80" placeholder="Upgrade a dependency" /></label>
553
+ <label>Request <textarea name="intent" rows="4" required placeholder="Upgrade {{package}} to the latest version, fix what breaks, run the tests and open a pull request."></textarea></label>
554
+ <label>Run in these projects <span class="hint">shortcuts, comma or space separated; one agent each</span><input name="projects" placeholder="web api" list="project-options" /></label>
555
+ <div id="pb-defaults"></div>
556
+ <div class="row three">
557
+ <label>Model <input name="model" placeholder="as configured" /></label>
558
+ <label>Permission mode <select name="permissionMode"><option value="">As configured</option><option value="acceptEdits">Accept edits</option><option value="plan">Plan</option><option value="auto">Auto</option><option value="bypassPermissions">Bypass permissions</option></select></label>
559
+ <label>Priority <select name="priority"><option value="0">P0</option><option value="1">P1</option><option value="2" selected>P2</option><option value="3">P3</option></select></label>
560
+ </div>
561
+ <div class="row three">
562
+ <label>Schedule <select name="every"><option value="">By hand only</option><option value="hour">Every hour</option><option value="day">Every day</option><option value="weekday">Every weekday</option><option value="week">Every week</option></select></label>
563
+ <label>At <input name="at" type="time" value="09:00" /></label>
564
+ <label>On <select name="day"><option value="mon">Monday</option><option value="tue">Tuesday</option><option value="wed">Wednesday</option><option value="thu">Thursday</option><option value="fri">Friday</option><option value="sat">Saturday</option><option value="sun">Sunday</option></select></label>
565
+ </div>
566
+ <label class="check"><input type="checkbox" name="enabled" checked /> Schedule on</label>
567
+ <p id="pb-error" class="error" hidden></p>
568
+ <div class="dialog-actions">
569
+ <button type="button" id="pb-delete" class="danger ghost">Delete</button>
570
+ <span class="spacer"></span>
571
+ <button type="button" id="pb-cancel" class="ghost">Cancel</button>
572
+ <button class="primary">Save</button>
573
+ </div>
574
+ </form>
575
+ </div>
576
+ </dialog>
577
+
578
+ <dialog id="spend-dialog">
579
+ <form id="spend-form" method="dialog" class="alerts-body">
580
+ <h2>Results <select id="stats-days" class="inline-select"><option value="7">last 7 days</option><option value="30" selected>last 30 days</option><option value="90">last 90 days</option><option value="0">all time</option></select></h2>
581
+ <div id="stats-body"></div>
582
+ <h3 class="spend-h">Spend</h3>
583
+ <div id="spend-summary"></div>
584
+ <div class="row">
585
+ <label>Each agent at most <span class="hint">US dollars; empty for no limit</span><input name="perAgentUsd" type="number" min="0.01" step="0.01" placeholder="no limit" /></label>
586
+ <label>All agents a day at most <span class="hint">US dollars</span><input name="perDayUsd" type="number" min="0.01" step="0.01" placeholder="no limit" /></label>
587
+ </div>
588
+ <p class="hint">At 80% of an agent's budget you get an alert; at its budget its session is closed until you raise it (Edit on the agent). At the daily limit no new agents start or resume until tomorrow; running ones go on.</p>
589
+ <div id="spend-agents"></div>
590
+ <p id="spend-note" class="hint"></p>
591
+ <p id="spend-error" class="error" hidden></p>
592
+ <div class="dialog-actions">
593
+ <span class="spacer"></span>
594
+ <button type="button" id="spend-close" class="ghost">Close</button>
595
+ <button class="primary">Save limits</button>
596
+ </div>
597
+ </form>
598
+ </dialog>
599
+
468
600
  <dialog id="cleanup-dialog">
469
601
  <form id="cleanup-form" method="dialog" class="alerts-body">
470
602
  <h2>Clean up old agents</h2>