ai-agentmanager 1.11.1 → 1.13.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,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.13.0 (2026-11-01)
4
+
5
+ - **Agents are built in an agent session.** **+ Build an agent** now starts an agent session that works in the new agent's folder (`~/.agentmanager/built/<name>/`) and goes straight to it. The session writes the analysis (`analysis.json`), `agent.yaml`, `instructions.md` and `tests.yaml`, and checks that the agent loads. You keep shaping the agent in that conversation, like any other session (terminal, resume, inbox).
6
+ - The session's page has a panel for its agent: status, tests, **Run tests**, **Start**/**Stop**, its chat, **Details**. Failing tests go back to the session with **Ask this session to fix them**.
7
+ - In the Built tab, each agent links to its session. The details dialog has a box that sends a change request to the session. Agents built before this get **Open a session for it**.
8
+ - Test results and executables built before the agent's latest change are marked as out of date. Deleting a built agent closes its session; the conversation is kept.
9
+ - **Fixes from building a real agent:**
10
+ - A permission prompt is now flagged when newer Claude Code blinks a ⏺ beside the pending tool call. The blinking used to keep the session looking busy, so the prompt never reached the Inbox. This affects every session, not just build sessions.
11
+ - agentkit agents can write files under a folder they're allowed: Claude Code ignores `Write(<path>)` allow rules, so each one now also gets its `Edit(<path>)` rule.
12
+ - The test judge sees the files each test run created or changed, not only the agent's reply, and the Tests list shows them.
13
+ - Build sessions read the agentkit reference without asking, and write the agent's files with their file tools.
14
+ - The build dialog takes an optional name; otherwise the name comes from the task's first words. `POST /api/builder/:name/change` sends a change request to the session from scripts.
15
+
16
+ ## 1.12.0 (2026-11-01)
17
+
18
+ - **agentkit (phase 1): agents you build and run.** An agent is a folder with `agent.yaml` (goal, instructions, engine, MCP tools, allow/approve lists, triggers, loop, memory, interfaces); `agentkit run <agent>` runs it continuously. Each input is a run of plan → act → check (acting again on what's left, up to `maxSteps`) → reflect into memory, through an engine's CLI (Claude Code, Codex, Muse Code or an ACP agent), self-contained (only its own MCP servers; none of your personal Claude settings). Triggers: `every`, `cron`, `webhook`, `watch`. Interfaces: a chat page and HTTP API, A2A (agent card and `message/send` / `tasks/get`), ACP on stdio (`agentkit acp`), and an MCP tool (`agentkit mcp`, one tool `ask_<name>`). `agentkit init`, `check`, `run --once`. See [docs/agentkit.md](docs/agentkit.md).
19
+ - **Agent builder:** a **Built** tab with **+ Build an agent**: describe a task or workflow (attachments welcome) and pick an engine; one call designs the agent: an analysis (problem, inputs, outputs, risks), its `agent.yaml` and instructions, suggested MCP tools and 3–5 test cases. Review and edit the spec (checked on save), **Run tests** (each through the real runtime, judged against its expectation), **Start** it on this computer (its chat page and API), **Download** its folder. Built agents live in `~/.agentmanager/built/<name>/`; `/api/builder` for scripts.
20
+ - **Single executables and background service for agents:** `agentkit package <agent> [--platform]` makes one file (macOS arm64/x64, Linux x64/arm64) that runs the agent with no Node or agentkit installed: a Node single executable with the agent's files embedded, built on the official Node binary (downloaded from nodejs.org, checksum-verified, cached). Linux ones build on a Mac. The executable unpacks to `~/.agentkit/<name>/` and takes the same commands (`--once`, `acp`, `mcp`, `check`). `service install|status|uninstall` keeps an agent running in the background (launchd or a systemd user service: started at login, restarted after a crash). In the Built tab: **Build executable** for a platform, with download links.
21
+ - **Approvals and monitoring for agents:** tools on an agent's `approve` list now ask a person instead of being unavailable (Claude Code engine, through `--permission-prompt-tool` and the agent's own approver), on its chat page and in AI Agent Manager's **Inbox**; the first answer decides, silence past `approveTimeout` is a no, and the approve list wins over `allow`. Agents report to AI Agent Manager (runs, cost, results, open approvals; every 30s, at once after a run): the **Built** tab lists them under *Reporting in*. Agents started here report automatically; others connect with `AGENTKIT_REPORT_URL`, `AGENTKIT_REPORT_TOKEN` and, over remote access, `AGENTKIT_REPORT_CERT` (certificate pinning).
22
+
3
23
  ## 1.11.1 (2026-11-01)
4
24
 
5
25
  - **Copy and paste in terminals on phones:** **Copy** and **Paste** in the terminal's key bar (and in the Terminal's bar). A phone can't select the terminal's text or paste into it directly, so Copy opens its recent output as plain text you can select (or **Copy all**), and Paste reads the clipboard (the phone asks first) or offers a box to paste into, then inserts it as one paste without pressing Enter.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ // agentkit: run an agent described by agent.yaml (docs/agentkit.md).
3
+ // agentkit run <agent> [--once "<input>"] [--port N] triggers, chat page, API and A2A; or one run
4
+ // agentkit acp <agent> ACP on stdio
5
+ // agentkit mcp <agent> MCP on stdio (one tool: ask_<name>)
6
+ // agentkit check <agent> validate agent.yaml
7
+ // agentkit init <folder> a starter agent to edit
8
+ import { main } from '../src/agentkit/cli.js';
9
+
10
+ main(process.argv.slice(2));
@@ -0,0 +1,175 @@
1
+ # agentkit: agents you build, download and run
2
+
3
+ Status: the runtime, the builder (AI Agent Manager's **Built** tab), single executables, the background service, approvals and reporting to AI Agent Manager are in.
4
+
5
+ An **agentkit agent** is a folder (or one executable made from it) that does one job continuously: triage new
6
+ support tickets, keep a dependency report current, answer questions about a codebase, run a
7
+ release checklist. It is described by `agent.yaml`, runs its work through an engine's CLI (Claude
8
+ Code, Codex, Muse Code or any ACP agent), uses tools through MCP servers, and can be reached by
9
+ people (a chat page, an HTTP API), by other agents (A2A, MCP) and by editors (ACP).
10
+
11
+ ## agent.yaml
12
+
13
+ ```yaml
14
+ name: ticket-triage # lowercase, used for files and URLs
15
+ title: Support ticket triage
16
+ description: Reads new tickets, labels and routes them, drafts a first reply.
17
+ goal: |
18
+ Every new ticket is labelled, routed to the right team and has a drafted first reply
19
+ within 10 minutes, without closing or replying on its own.
20
+ instructions: instructions.md # or inline text: how to do the job, step by step
21
+ engine: claude # claude | codex | muse | acp:<name>
22
+ model: sonnet # optional, passed to the engine
23
+ workdir: ./work # where it works (a repo, a folder); default ./work
24
+
25
+ tools: # MCP servers the engine gets
26
+ jira:
27
+ command: npx
28
+ args: [-y, "@acme/jira-mcp"]
29
+ env: { JIRA_TOKEN: "${JIRA_TOKEN}" } # "${VAR}" (quoted) comes from the environment, never stored
30
+ allow: [Read, Grep, "mcp__jira__*"] # tools it may use without asking (engine names);
31
+ # "Edit(./out/**)" lets it write files under ./out only
32
+ approve: ["mcp__jira__transition"] # tools that need a person's yes (asked in chat / Inbox)
33
+ approveTimeout: 30m # no answer by then counts as no
34
+ report: # report to AI Agent Manager (or set AGENTKIT_REPORT_*)
35
+ url: "${AGENTMGR_URL}"
36
+ token: "${AGENTMGR_REPORT_TOKEN}"
37
+
38
+ triggers:
39
+ - every: 10m # interval
40
+ - cron: "0 9 * * 1-5" # schedule (minute hour day month weekday)
41
+ - webhook: /hooks/ticket # POST here starts a run with the body as input
42
+ - watch: ./inbox # a new or changed file starts a run
43
+
44
+ loop:
45
+ plan: true # think and write a plan before acting
46
+ check: true # verify the result against the goal after acting
47
+ maxSteps: 6 # act/check rounds per run before stopping
48
+ budgetUsd: 0.50 # per run (when the engine reports cost)
49
+
50
+ memory: true # keep notes between runs (memory.md)
51
+
52
+ interfaces:
53
+ http: { port: 7700 } # chat page + API
54
+ a2a: true # agent card + tasks over HTTP
55
+ acp: true # `<agent> acp` speaks ACP on stdio
56
+ mcp: true # `<agent> mcp` exposes it as an MCP tool
57
+ ```
58
+
59
+ ## Building one in AI Agent Manager
60
+
61
+ **Built → + Build an agent** takes a task or workflow (attachments welcome), the engine it runs on and an optional name. It starts an **agent session** whose folder is the new agent (`~/.agentmanager/built/<name>/`), and opens that session. The session writes the agent and checks that it loads (`agentkit check`):
62
+
63
+ - `analysis.json`: the problem, inputs, outputs, risks and the MCP tools it chose
64
+ - `agent.yaml`
65
+ - `instructions.md`
66
+ - `tests.yaml`: 3 to 8 `{input, expect}` cases
67
+
68
+ It asks you what only you know: accounts, labels, teams. You keep developing the agent in that conversation.
69
+
70
+ The session's page shows a panel for its agent: status and test results, with **Run tests** (each test runs through the real runtime and is judged against `expect`), **Start**/**Stop** (runs it continuously on this computer, with its chat page and API) and **Details**. Failing tests can go straight back to the session. The Built tab lists every built agent with a link to its session. Its details dialog sends change requests to the session and has the spec, executables and downloads. Results and executables from before the latest change are marked out of date.
71
+
72
+ ## A run
73
+
74
+ 1. **Input** arrives: a trigger fires (with its payload), someone chats, an A2A task or an MCP
75
+ call comes in.
76
+ 2. **Plan** (when `loop.plan`): the engine reads the goal, instructions, memory and input and
77
+ writes a short plan.
78
+ 3. **Act**: the engine carries out the plan with its tools (MCP), in `workdir`.
79
+ 4. **Check** (when `loop.check`): the engine is asked whether the goal is met for this input,
80
+ and what's left. If not and steps remain, it acts again on what's left.
81
+ 5. **Reflect** (when `memory`): one or two lessons for next time are appended to memory.
82
+ 6. **Result**: the final answer goes back to whoever asked; the run (steps, output, cost,
83
+ duration) is logged in `runs/<id>.jsonl`.
84
+
85
+ The engine keeps one session per conversation (a chat, an A2A context), so follow-ups continue
86
+ it. Runs from triggers start fresh sessions.
87
+
88
+ ## Interfaces
89
+
90
+ - **HTTP**: `GET /` chat page; `POST /api/chat {message, conversation?}`; `POST /api/runs {input}`;
91
+ `GET /api/runs`, `GET /api/runs/:id`; `GET /api/health`.
92
+ - **A2A**: agent card at `GET /.well-known/agent.json`; JSON-RPC at `POST /a2a` with
93
+ `message/send` and `tasks/get`.
94
+ - **ACP**: `<agent> acp` runs an ACP agent on stdio (initialize, session/new, session/prompt,
95
+ streamed session/update), so editors (and AI Agent Manager's ACP engine) can drive it.
96
+ - **MCP**: `<agent> mcp` runs an MCP server on stdio with one tool, `ask_<name>`, so other
97
+ agents can delegate to it.
98
+
99
+ ## Running
100
+
101
+ `agentkit run ./ticket-triage` (or, packaged, `./ticket-triage`) starts it in the foreground:
102
+ triggers, the HTTP server, the loop. `agentkit run … --once "<input>"` does one run and prints
103
+ the result.
104
+
105
+ In the background: `agentkit service install ./ticket-triage` (packaged: `./ticket-triage service
106
+ install`) registers it with launchd (macOS) or systemd (Linux, a user service): it starts at login,
107
+ restarts after a crash, and writes to `agent.log` in its folder. It runs through your login shell,
108
+ so it finds the engine CLIs and tokens your terminal has. `service status` and `service uninstall`
109
+ do the rest. On Linux, `loginctl enable-linger $USER` keeps it running while you're logged out.
110
+
111
+ ## One executable
112
+
113
+ `agentkit package ./ticket-triage [--platform linux-x64] [-o dist]` makes one file that runs the
114
+ agent with no Node or agentkit installed: `ticket-triage-macos-arm64`, `-macos-x64`,
115
+ `-linux-x64` or `-linux-arm64` (about 110 MB; Windows isn't supported yet). It is a Node single
116
+ executable application: the runtime bundled, the agent's files (`agent.yaml`, instructions,
117
+ tests) embedded, injected into the official Node binary for that platform (downloaded once from
118
+ nodejs.org, checked against its published checksums, cached in `~/.agentmanager/node-dist`).
119
+ Linux executables build on any Mac or Linux machine; macOS ones are signed, so they build on a Mac.
120
+
121
+ On its first run, the executable unpacks its files to `~/.agentkit/<name>/` (or `AGENTKIT_HOME`),
122
+ where its memory and runs live too. Edits there stay until a newer build of the agent replaces the
123
+ files it shipped. Everything else works as with the folder: `./ticket-triage` (run),
124
+ `--once "<input>"`, `acp`, `mcp`, `check`, `service install`. Its engine's CLI (Claude Code, Codex
125
+ or Muse Code) must be installed and signed in where it runs. ACP engines aren't packaged yet.
126
+
127
+ In AI Agent Manager, a built agent's dialog has **Build executable** (pick the platform) and a
128
+ download link for each one built. Editing the agent drops the old executables.
129
+
130
+ ## Approvals
131
+
132
+ A tool on the `approve` list runs only after a person says yes. The approve list wins over
133
+ `allow`: with `allow: ["mcp__jira__*"]` and `approve: ["mcp__jira__transition"]`, every Jira tool
134
+ runs freely except transitions, which ask.
135
+
136
+ With Claude Code as the engine, the agent passes `--permission-prompt-tool` to point Claude at a
137
+ small MCP server of its own (the approver). Claude asks the approver before any tool it isn't
138
+ allowed outright. The approver hands the question to the running agent over a private local
139
+ address, and the agent asks whoever can answer:
140
+
141
+ - **its chat page**, which shows *Approve …?* with the tool's input and Approve / Deny buttons
142
+ (`GET /api/approvals`, `POST /api/approvals/<id>` with `{"allow": true}`);
143
+ - **AI Agent Manager**, when the agent reports there: the request shows up in the **Inbox** and the
144
+ **Built** tab, and the answer goes back with the agent's next check-in (every 2 seconds while
145
+ something waits).
146
+
147
+ The first answer decides. No answer within `approveTimeout` (default 30m) counts as no, and so does
148
+ having no one to ask (no chat page, not reporting). Tools on neither list are refused without
149
+ asking. Other engines can't ask mid-run, so for them approval tools aren't available at all. Test
150
+ runs in the builder never wait for anyone.
151
+
152
+ ## Reporting to AI Agent Manager
153
+
154
+ An agent that reports sends AI Agent Manager who it is (name, host, engine, triggers, chat
155
+ address), each run (status, source, cost, time, the start of its result) and the approvals it's
156
+ waiting on. It checks in every 30 seconds, and right away after a run or a new approval. The
157
+ **Built** tab lists these agents under *Reporting in*: online or last seen, idle or working, runs,
158
+ failures and total cost, recent runs, and approvals to answer. Runs are counted once; the list is
159
+ kept across restarts.
160
+
161
+ Agents started from the Built tab report automatically. For one running elsewhere (a packaged
162
+ executable on a server, a folder on another Mac), **Connect one from elsewhere** shows what to set
163
+ in its environment (or its service):
164
+
165
+ ```
166
+ AGENTKIT_REPORT_URL=https://192.168.1.20:4718 # AI Agent Manager's remote-access address
167
+ AGENTKIT_REPORT_TOKEN=… # the report token (made on first use)
168
+ AGENTKIT_REPORT_CERT=AB:CD:… # its certificate's SHA-256 fingerprint
169
+ ```
170
+
171
+ Remote access uses a self-signed certificate, so the agent trusts exactly that certificate (by
172
+ its fingerprint) and sends nothing until the fingerprint matches. The connection details are shown
173
+ only on the computer AI Agent Manager runs on. The report token lets a program report as an agent
174
+ and receive the decisions made for it, and nothing else in AI Agent Manager; keep it like a
175
+ password (give each computer the same one, or rotate it by deleting `~/.agentmanager/agentkit-reports.json`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-agentmanager",
3
- "version": "1.11.1",
3
+ "version": "1.13.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",
@@ -23,7 +23,8 @@
23
23
  "license": "Apache-2.0",
24
24
  "type": "module",
25
25
  "bin": {
26
- "agentmgr": "bin/agentmgr.js"
26
+ "agentmgr": "bin/agentmgr.js",
27
+ "agentkit": "bin/agentkit.js"
27
28
  },
28
29
  "files": [
29
30
  "bin",
@@ -51,11 +52,14 @@
51
52
  "@xterm/xterm": "^6.0.0",
52
53
  "commander": "^15.0.0",
53
54
  "dompurify": "^3.4.17",
55
+ "esbuild": "^0.25.12",
54
56
  "express": "^5.2.1",
55
57
  "marked": "^18.0.14",
56
58
  "mermaid": "^12.2.0",
57
59
  "node-pty": "^1.1.0",
60
+ "postject": "^1.0.0-alpha.6",
58
61
  "qrcode": "^1.5.4",
59
- "ws": "^8.22.0"
62
+ "ws": "^8.22.0",
63
+ "yaml": "^2.9.1"
60
64
  }
61
65
  }
package/public/app.js CHANGED
@@ -7,6 +7,7 @@ import { openStream, post, REMOTE } from '/transport.js';
7
7
  import { attachBox } from '/attach.js';
8
8
  import { initSide } from '/side.js';
9
9
  import { openCopy, pasteInto } from '/termclip.js';
10
+ import { initBuilt } from '/built.js';
10
11
  import { addTools, download, htmlFile, initExport } from '/export.js';
11
12
  import { badgeHtml as scoreBadge, scorecardHtml, scorecardSpeech } from '/scorecard.js';
12
13
  import { bindListen, listenHtml, play, speakable, now as speaking, stop as stopSpeaking } from '/speak.js';
@@ -125,6 +126,15 @@ $('#new-playbook').onclick = () => {
125
126
  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 });
126
127
  };
127
128
  $('#triggers-open').onclick = () => triggersUi.open();
129
+ let builtReports = []; // agents built with agentkit that report here (runs, approvals)
130
+ const builtUi = initBuilt({
131
+ api, esc, toast, $,
132
+ isOpen: () => sideTab === 'built',
133
+ setCount: (n) => ($('#built-count').textContent = n),
134
+ reports: () => builtReports,
135
+ openSession: (id) => (showTab('agents'), select(id)),
136
+ onPanel: () => agents.get(selectedId)?.builds && renderDetail(),
137
+ });
128
138
  const landUi = initLand({ api, esc, toast, agents, $, scoreBadge, select: (id) => (showTab('agents'), select(id)), openReview: (id) => reviewUi.open(id), isOpen: () => sideTab === 'land' });
129
139
 
130
140
  function statusOf(a) {
@@ -710,7 +720,7 @@ function agentItem(a) {
710
720
  const idle = !isActive(a);
711
721
  return `<li data-id="${a.id}" ${a.kind === 'general' || a.kind === 'assistant' ? '' : 'draggable="true" '}class="agent pr${a.priority ?? 2} ${a.id === selectedId ? 'selected' : ''} ${idle ? 'idle' : ''}">
712
722
  <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>
713
- <div class="intent" title="${esc(describeTitle(a) ?? '')}">${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> ` : ''}${a.isolation === 'container' ? '<span class="tag engine" title="Runs in its own Docker container: it sees only its own folder">📦 container</span> ' : ''}${esc(describe(a) || (a.kind === 'assistant' ? 'Finds new work and starts agents for it' : 'General session'))}</div>
723
+ <div class="intent" title="${esc(describeTitle(a) ?? '')}">${a.builds ? `<span class="tag assistant" title="Develops the agent ${esc(a.builds.name)} (Built tab)">⚙ builds agent</span> ` : ''}${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> ` : ''}${a.isolation === 'container' ? '<span class="tag engine" title="Runs in its own Docker container: it sees only its own folder">📦 container</span> ' : ''}${esc(describe(a) || (a.kind === 'assistant' ? 'Finds new work and starts agents for it' : 'General session'))}</div>
714
724
  ${a.status === 'preparing' && a.progress ? `<div class="progress-line">${esc(progressText(a.progress))}</div>` : ''}
715
725
  <div class="meta">
716
726
  <span class="mono">${a.id}</span>
@@ -922,6 +932,8 @@ function showTab(tab) {
922
932
  $('#backlog-view').hidden = tab !== 'backlog';
923
933
  $('#inbox-view').hidden = tab !== 'inbox';
924
934
  $('#land-view').hidden = tab !== 'land';
935
+ $('#built-view').hidden = tab !== 'built';
936
+ if (tab === 'built') builtUi.load();
925
937
  if (tab === 'inbox') loadJudge();
926
938
  if (tab === 'land') landUi.load();
927
939
  renderBacklog();
@@ -1001,7 +1013,7 @@ function flags(a) {
1001
1013
  }
1002
1014
 
1003
1015
  const TRIAGED = { now: 'sent (AI: urgent)', digest: 'held for the digest', skip: 'dropped by the AI judge' };
1004
- const NEED_ORDER = { prompt: 0, waiting: 1, plan: 1, question: 1, team: 1, review: 2, stuck: 3, conflict: 4 };
1016
+ const NEED_ORDER = { approval: 0, prompt: 0, waiting: 1, plan: 1, question: 1, team: 1, review: 2, stuck: 3, conflict: 4 };
1005
1017
  /** What an agent needs from the user. `both`: overlaps on both agents (detail), else only the one that should rebase (inbox). */
1006
1018
  function needsFor(a, { both = false } = {}) {
1007
1019
  const items = [];
@@ -1021,23 +1033,34 @@ function teamNeeds() {
1021
1033
  .filter((t) => ['ready', 'blocked'].includes(t.stage) && agents.has(teamOwner(t)))
1022
1034
  .map((t) => ({ a: agents.get(teamOwner(t)), kind: 'team', key: `team:${t.id}`, t }));
1023
1035
  }
1036
+ /** Tools that built agents (agentkit) wait to run until someone approves. */
1037
+ function approvalNeeds() {
1038
+ return builtReports.flatMap((r) => r.pending.map((p) => ({ a: { id: r.key, name: r.title, priority: 1 }, kind: 'approval', key: `approval:${r.key}:${p.id}`, r, p })));
1039
+ }
1024
1040
  function inboxItems() {
1025
- return [...[...agents.values()].flatMap((a) => needsFor(a)), ...teamNeeds()]
1041
+ return [...approvalNeeds(), ...[...agents.values()].flatMap((a) => needsFor(a)), ...teamNeeds()]
1026
1042
  .sort((x, y) => NEED_ORDER[x.kind] - NEED_ORDER[y.kind] || (x.a.priority ?? 2) - (y.a.priority ?? 2));
1027
1043
  }
1028
1044
 
1029
- const NEED_LABEL = { team: 'Team', prompt: 'Needs input', waiting: 'Waiting on you', plan: 'Plan review', question: 'Question', review: 'AI review', stuck: 'Stuck?', conflict: 'Overlap' };
1045
+ const NEED_LABEL = { approval: 'Approval', team: 'Team', prompt: 'Needs input', waiting: 'Waiting on you', plan: 'Plan review', question: 'Question', review: 'AI review', stuck: 'Stuck?', conflict: 'Overlap' };
1030
1046
  /** Why the AI judge thinks so, as a small tag. */
1031
1047
  const aiTag = (confidence, what = 'AI') => `<span class="tag ai" title="Judged by a model from the agent's activity; your reactions tune later calls">🤖 ${what}${confidence != null ? ` ${Math.round(confidence * 100)}%` : ''}</span>`;
1032
1048
  function needCard(it, { inDetail = false } = {}) {
1033
1049
  const { a } = it;
1034
1050
  const who = inDetail
1035
1051
  ? ''
1052
+ : it.kind === 'approval'
1053
+ ? `<a href="#" class="need-agent" data-ib="built-open">⚙ ${esc(a.name)}</a> <span class="hint">${esc(it.r.host)}</span>`
1036
1054
  : `<a href="#${a.id}" class="need-agent" data-goto="${a.id}">${prioChip(a.priority ?? 2)}${esc(a.name)}</a>${a.projectInfo?.name ? ` <span class="hint">${esc(a.projectInfo.name)}</span>` : ''}`;
1037
1055
  const head = `<div class="need-head"><span class="need-kind ${it.kind}">${NEED_LABEL[it.kind]}</span>${who}</div>`;
1038
1056
  const draft = (k, fallback = '') => esc(needDrafts.get(`${it.key}:${k}`) ?? fallback);
1039
1057
  let body = '';
1040
- if (it.kind === 'prompt') {
1058
+ if (it.kind === 'approval') {
1059
+ const p = it.p;
1060
+ body = `<p class="need-title">Wants to run <code>${esc(p.tool)}</code> <span class="hint">asked ${ago(p.at)}</span></p>
1061
+ <pre class="need-input">${esc(JSON.stringify(p.input, null, 2).slice(0, 1500))}</pre>
1062
+ <div class="need-actions"><button class="small primary" data-ib="approve" data-allow="1">Approve</button><button class="small" data-ib="approve">Deny</button>${it.r.chat ? `<a class="button small ghost" href="${esc(it.r.chat)}" target="_blank" rel="noopener">Its chat ↗</a>` : ''}</div>`;
1063
+ } else if (it.kind === 'prompt') {
1041
1064
  const p = a.runtime.prompt;
1042
1065
  body = `<p class="need-title">${esc(p?.question || a.runtime.attention)}</p>
1043
1066
  <div class="need-actions">
@@ -1141,7 +1164,7 @@ function judgeLine() {
1141
1164
 
1142
1165
  function renderInbox() {
1143
1166
  const items = inboxItems();
1144
- const urgent = items.filter((i) => i.kind === 'prompt' || i.kind === 'question' || i.kind === 'waiting').length;
1167
+ const urgent = items.filter((i) => i.kind === 'approval' || i.kind === 'prompt' || i.kind === 'question' || i.kind === 'waiting').length;
1145
1168
  $('#inbox-count').textContent = items.length ? String(items.length) : '';
1146
1169
  $('#inbox-count').className = `badge ${urgent ? 'attention' : ''}`;
1147
1170
  if (sideTab !== 'inbox') return;
@@ -1175,6 +1198,14 @@ async function onNeed(e) {
1175
1198
  const id = card.dataset.agent;
1176
1199
  const key = card.dataset.need;
1177
1200
  switch (el.dataset.ib) {
1201
+ case 'built-open':
1202
+ return showTab('built');
1203
+ case 'approve': {
1204
+ const [, , pid] = key.match(/^approval:(.+):([^:]+)$/);
1205
+ return act(async () => {
1206
+ await api('POST', `/agentkit/agents/${encodeURIComponent(id)}/approvals/${encodeURIComponent(pid)}`, { allow: el.dataset.allow === '1' });
1207
+ }, el.dataset.allow === '1' ? 'Approved; the agent goes on' : 'Declined');
1208
+ }
1178
1209
  case 'key':
1179
1210
  return act(() => api('POST', `/agents/${id}/prompt-key`, { key: el.dataset.key }), 'Sent');
1180
1211
  case 'terminal':
@@ -1719,6 +1750,7 @@ function renderDetail() {
1719
1750
  ${a.runtime.attention ? `<div class="error-box">${esc(a.runtime.attention)}: the agent is waiting for an answer. Open its terminal to respond.</div>` : ''}
1720
1751
  ${raceBanner(a)}
1721
1752
  ${teamBanner(a)}
1753
+ ${a.builds ? builtUi.panel(a) : ''}
1722
1754
  ${a.forkOf ? `<div class="bl-banner">⑂ Forked from <a href="#${a.forkOf.agentId}" data-goto="${a.forkOf.agentId}">${esc(a.forkOf.name)}</a>${a.forkOf.checkpoint ? ` at checkpoint ${a.forkOf.checkpoint}` : ''} ${ago(a.forkOf.at)}</div>` : ''}
1723
1755
  ${(() => {
1724
1756
  const items = needsFor(a, { both: true });
@@ -3604,6 +3636,8 @@ function handleEvent(msg) {
3604
3636
  for (const r of msg.races ?? []) races.set(r.id, r);
3605
3637
  teams.clear();
3606
3638
  for (const t of msg.teams ?? []) teams.set(t.id, t);
3639
+ builtReports = msg.built ?? [];
3640
+ builtUi.render();
3607
3641
  msg.agents.forEach(watchDocs);
3608
3642
  if (selectedId && !agents.has(selectedId)) selectedId = null;
3609
3643
  if (openTermOnLoad && selectedId) {
@@ -3615,6 +3649,7 @@ function handleEvent(msg) {
3615
3649
  agents.set(msg.agent.id, msg.agent);
3616
3650
  watchDocs(msg.agent);
3617
3651
  agentChanged(msg.agent);
3652
+ if (msg.agent.builds) builtUi.sessionChanged(msg.agent);
3618
3653
  } else if (msg.t === 'removed') {
3619
3654
  agents.delete(msg.id);
3620
3655
  if (selectedId === msg.id) select(null);
@@ -3626,6 +3661,9 @@ function handleEvent(msg) {
3626
3661
  teams.set(msg.team.id, msg.team);
3627
3662
  } else if (msg.t === 'team-removed') {
3628
3663
  teams.delete(msg.id);
3664
+ } else if (msg.t === 'built') {
3665
+ builtReports = msg.reports;
3666
+ builtUi.render();
3629
3667
  } else if (msg.t === 'backlog') {
3630
3668
  backlogState = msg.backlog;
3631
3669
  } else if (msg.t === 'projects') {