ai-agentmanager 1.11.1 → 1.12.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,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.12.0 (2026-11-01)
4
+
5
+ - **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).
6
+ - **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.
7
+ - **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.
8
+ - **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).
9
+
3
10
  ## 1.11.1 (2026-11-01)
4
11
 
5
12
  - **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,161 @@
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
+ approve: ["mcp__jira__transition"] # tools that need a person's yes (asked in chat / Inbox)
32
+ approveTimeout: 30m # no answer by then counts as no
33
+ report: # report to AI Agent Manager (or set AGENTKIT_REPORT_*)
34
+ url: "${AGENTMGR_URL}"
35
+ token: "${AGENTMGR_REPORT_TOKEN}"
36
+
37
+ triggers:
38
+ - every: 10m # interval
39
+ - cron: "0 9 * * 1-5" # schedule (minute hour day month weekday)
40
+ - webhook: /hooks/ticket # POST here starts a run with the body as input
41
+ - watch: ./inbox # a new or changed file starts a run
42
+
43
+ loop:
44
+ plan: true # think and write a plan before acting
45
+ check: true # verify the result against the goal after acting
46
+ maxSteps: 6 # act/check rounds per run before stopping
47
+ budgetUsd: 0.50 # per run (when the engine reports cost)
48
+
49
+ memory: true # keep notes between runs (memory.md)
50
+
51
+ interfaces:
52
+ http: { port: 7700 } # chat page + API
53
+ a2a: true # agent card + tasks over HTTP
54
+ acp: true # `<agent> acp` speaks ACP on stdio
55
+ mcp: true # `<agent> mcp` exposes it as an MCP tool
56
+ ```
57
+
58
+ ## A run
59
+
60
+ 1. **Input** arrives: a trigger fires (with its payload), someone chats, an A2A task or an MCP
61
+ call comes in.
62
+ 2. **Plan** (when `loop.plan`): the engine reads the goal, instructions, memory and input and
63
+ writes a short plan.
64
+ 3. **Act**: the engine carries out the plan with its tools (MCP), in `workdir`.
65
+ 4. **Check** (when `loop.check`): the engine is asked whether the goal is met for this input,
66
+ and what's left. If not and steps remain, it acts again on what's left.
67
+ 5. **Reflect** (when `memory`): one or two lessons for next time are appended to memory.
68
+ 6. **Result**: the final answer goes back to whoever asked; the run (steps, output, cost,
69
+ duration) is logged in `runs/<id>.jsonl`.
70
+
71
+ The engine keeps one session per conversation (a chat, an A2A context), so follow-ups continue
72
+ it. Runs from triggers start fresh sessions.
73
+
74
+ ## Interfaces
75
+
76
+ - **HTTP**: `GET /` chat page; `POST /api/chat {message, conversation?}`; `POST /api/runs {input}`;
77
+ `GET /api/runs`, `GET /api/runs/:id`; `GET /api/health`.
78
+ - **A2A**: agent card at `GET /.well-known/agent.json`; JSON-RPC at `POST /a2a` with
79
+ `message/send` and `tasks/get`.
80
+ - **ACP**: `<agent> acp` runs an ACP agent on stdio (initialize, session/new, session/prompt,
81
+ streamed session/update), so editors (and AI Agent Manager's ACP engine) can drive it.
82
+ - **MCP**: `<agent> mcp` runs an MCP server on stdio with one tool, `ask_<name>`, so other
83
+ agents can delegate to it.
84
+
85
+ ## Running
86
+
87
+ `agentkit run ./ticket-triage` (or, packaged, `./ticket-triage`) starts it in the foreground:
88
+ triggers, the HTTP server, the loop. `agentkit run … --once "<input>"` does one run and prints
89
+ the result.
90
+
91
+ In the background: `agentkit service install ./ticket-triage` (packaged: `./ticket-triage service
92
+ install`) registers it with launchd (macOS) or systemd (Linux, a user service): it starts at login,
93
+ restarts after a crash, and writes to `agent.log` in its folder. It runs through your login shell,
94
+ so it finds the engine CLIs and tokens your terminal has. `service status` and `service uninstall`
95
+ do the rest. On Linux, `loginctl enable-linger $USER` keeps it running while you're logged out.
96
+
97
+ ## One executable
98
+
99
+ `agentkit package ./ticket-triage [--platform linux-x64] [-o dist]` makes one file that runs the
100
+ agent with no Node or agentkit installed: `ticket-triage-macos-arm64`, `-macos-x64`,
101
+ `-linux-x64` or `-linux-arm64` (about 110 MB; Windows isn't supported yet). It is a Node single
102
+ executable application: the runtime bundled, the agent's files (`agent.yaml`, instructions,
103
+ tests) embedded, injected into the official Node binary for that platform (downloaded once from
104
+ nodejs.org, checked against its published checksums, cached in `~/.agentmanager/node-dist`).
105
+ Linux executables build on any Mac or Linux machine; macOS ones are signed, so they build on a Mac.
106
+
107
+ On its first run, the executable unpacks its files to `~/.agentkit/<name>/` (or `AGENTKIT_HOME`),
108
+ where its memory and runs live too. Edits there stay until a newer build of the agent replaces the
109
+ files it shipped. Everything else works as with the folder: `./ticket-triage` (run),
110
+ `--once "<input>"`, `acp`, `mcp`, `check`, `service install`. Its engine's CLI (Claude Code, Codex
111
+ or Muse Code) must be installed and signed in where it runs. ACP engines aren't packaged yet.
112
+
113
+ In AI Agent Manager, a built agent's dialog has **Build executable** (pick the platform) and a
114
+ download link for each one built. Editing the agent drops the old executables.
115
+
116
+ ## Approvals
117
+
118
+ A tool on the `approve` list runs only after a person says yes. The approve list wins over
119
+ `allow`: with `allow: ["mcp__jira__*"]` and `approve: ["mcp__jira__transition"]`, every Jira tool
120
+ runs freely except transitions, which ask.
121
+
122
+ With Claude Code as the engine, the agent passes `--permission-prompt-tool` to point Claude at a
123
+ small MCP server of its own (the approver). Claude asks the approver before any tool it isn't
124
+ allowed outright. The approver hands the question to the running agent over a private local
125
+ address, and the agent asks whoever can answer:
126
+
127
+ - **its chat page**, which shows *Approve …?* with the tool's input and Approve / Deny buttons
128
+ (`GET /api/approvals`, `POST /api/approvals/<id>` with `{"allow": true}`);
129
+ - **AI Agent Manager**, when the agent reports there: the request shows up in the **Inbox** and the
130
+ **Built** tab, and the answer goes back with the agent's next check-in (every 2 seconds while
131
+ something waits).
132
+
133
+ The first answer decides. No answer within `approveTimeout` (default 30m) counts as no, and so does
134
+ having no one to ask (no chat page, not reporting). Tools on neither list are refused without
135
+ asking. Other engines can't ask mid-run, so for them approval tools aren't available at all. Test
136
+ runs in the builder never wait for anyone.
137
+
138
+ ## Reporting to AI Agent Manager
139
+
140
+ An agent that reports sends AI Agent Manager who it is (name, host, engine, triggers, chat
141
+ address), each run (status, source, cost, time, the start of its result) and the approvals it's
142
+ waiting on. It checks in every 30 seconds, and right away after a run or a new approval. The
143
+ **Built** tab lists these agents under *Reporting in*: online or last seen, idle or working, runs,
144
+ failures and total cost, recent runs, and approvals to answer. Runs are counted once; the list is
145
+ kept across restarts.
146
+
147
+ Agents started from the Built tab report automatically. For one running elsewhere (a packaged
148
+ executable on a server, a folder on another Mac), **Connect one from elsewhere** shows what to set
149
+ in its environment (or its service):
150
+
151
+ ```
152
+ AGENTKIT_REPORT_URL=https://192.168.1.20:4718 # AI Agent Manager's remote-access address
153
+ AGENTKIT_REPORT_TOKEN=… # the report token (made on first use)
154
+ AGENTKIT_REPORT_CERT=AB:CD:… # its certificate's SHA-256 fingerprint
155
+ ```
156
+
157
+ Remote access uses a self-signed certificate, so the agent trusts exactly that certificate (by
158
+ its fingerprint) and sends nothing until the fingerprint matches. The connection details are shown
159
+ only on the computer AI Agent Manager runs on. The report token lets a program report as an agent
160
+ and receive the decisions made for it, and nothing else in AI Agent Manager; keep it like a
161
+ 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.12.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,8 @@ $('#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({ api, esc, toast, $, isOpen: () => sideTab === 'built', setCount: (n) => ($('#built-count').textContent = n), reports: () => builtReports });
128
131
  const landUi = initLand({ api, esc, toast, agents, $, scoreBadge, select: (id) => (showTab('agents'), select(id)), openReview: (id) => reviewUi.open(id), isOpen: () => sideTab === 'land' });
129
132
 
130
133
  function statusOf(a) {
@@ -922,6 +925,8 @@ function showTab(tab) {
922
925
  $('#backlog-view').hidden = tab !== 'backlog';
923
926
  $('#inbox-view').hidden = tab !== 'inbox';
924
927
  $('#land-view').hidden = tab !== 'land';
928
+ $('#built-view').hidden = tab !== 'built';
929
+ if (tab === 'built') builtUi.load();
925
930
  if (tab === 'inbox') loadJudge();
926
931
  if (tab === 'land') landUi.load();
927
932
  renderBacklog();
@@ -1001,7 +1006,7 @@ function flags(a) {
1001
1006
  }
1002
1007
 
1003
1008
  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 };
1009
+ const NEED_ORDER = { approval: 0, prompt: 0, waiting: 1, plan: 1, question: 1, team: 1, review: 2, stuck: 3, conflict: 4 };
1005
1010
  /** What an agent needs from the user. `both`: overlaps on both agents (detail), else only the one that should rebase (inbox). */
1006
1011
  function needsFor(a, { both = false } = {}) {
1007
1012
  const items = [];
@@ -1021,23 +1026,34 @@ function teamNeeds() {
1021
1026
  .filter((t) => ['ready', 'blocked'].includes(t.stage) && agents.has(teamOwner(t)))
1022
1027
  .map((t) => ({ a: agents.get(teamOwner(t)), kind: 'team', key: `team:${t.id}`, t }));
1023
1028
  }
1029
+ /** Tools that built agents (agentkit) wait to run until someone approves. */
1030
+ function approvalNeeds() {
1031
+ 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 })));
1032
+ }
1024
1033
  function inboxItems() {
1025
- return [...[...agents.values()].flatMap((a) => needsFor(a)), ...teamNeeds()]
1034
+ return [...approvalNeeds(), ...[...agents.values()].flatMap((a) => needsFor(a)), ...teamNeeds()]
1026
1035
  .sort((x, y) => NEED_ORDER[x.kind] - NEED_ORDER[y.kind] || (x.a.priority ?? 2) - (y.a.priority ?? 2));
1027
1036
  }
1028
1037
 
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' };
1038
+ 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
1039
  /** Why the AI judge thinks so, as a small tag. */
1031
1040
  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
1041
  function needCard(it, { inDetail = false } = {}) {
1033
1042
  const { a } = it;
1034
1043
  const who = inDetail
1035
1044
  ? ''
1045
+ : it.kind === 'approval'
1046
+ ? `<a href="#" class="need-agent" data-ib="built-open">⚙ ${esc(a.name)}</a> <span class="hint">${esc(it.r.host)}</span>`
1036
1047
  : `<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
1048
  const head = `<div class="need-head"><span class="need-kind ${it.kind}">${NEED_LABEL[it.kind]}</span>${who}</div>`;
1038
1049
  const draft = (k, fallback = '') => esc(needDrafts.get(`${it.key}:${k}`) ?? fallback);
1039
1050
  let body = '';
1040
- if (it.kind === 'prompt') {
1051
+ if (it.kind === 'approval') {
1052
+ const p = it.p;
1053
+ body = `<p class="need-title">Wants to run <code>${esc(p.tool)}</code> <span class="hint">asked ${ago(p.at)}</span></p>
1054
+ <pre class="need-input">${esc(JSON.stringify(p.input, null, 2).slice(0, 1500))}</pre>
1055
+ <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>`;
1056
+ } else if (it.kind === 'prompt') {
1041
1057
  const p = a.runtime.prompt;
1042
1058
  body = `<p class="need-title">${esc(p?.question || a.runtime.attention)}</p>
1043
1059
  <div class="need-actions">
@@ -1141,7 +1157,7 @@ function judgeLine() {
1141
1157
 
1142
1158
  function renderInbox() {
1143
1159
  const items = inboxItems();
1144
- const urgent = items.filter((i) => i.kind === 'prompt' || i.kind === 'question' || i.kind === 'waiting').length;
1160
+ const urgent = items.filter((i) => i.kind === 'approval' || i.kind === 'prompt' || i.kind === 'question' || i.kind === 'waiting').length;
1145
1161
  $('#inbox-count').textContent = items.length ? String(items.length) : '';
1146
1162
  $('#inbox-count').className = `badge ${urgent ? 'attention' : ''}`;
1147
1163
  if (sideTab !== 'inbox') return;
@@ -1175,6 +1191,14 @@ async function onNeed(e) {
1175
1191
  const id = card.dataset.agent;
1176
1192
  const key = card.dataset.need;
1177
1193
  switch (el.dataset.ib) {
1194
+ case 'built-open':
1195
+ return showTab('built');
1196
+ case 'approve': {
1197
+ const [, , pid] = key.match(/^approval:(.+):([^:]+)$/);
1198
+ return act(async () => {
1199
+ await api('POST', `/agentkit/agents/${encodeURIComponent(id)}/approvals/${encodeURIComponent(pid)}`, { allow: el.dataset.allow === '1' });
1200
+ }, el.dataset.allow === '1' ? 'Approved; the agent goes on' : 'Declined');
1201
+ }
1178
1202
  case 'key':
1179
1203
  return act(() => api('POST', `/agents/${id}/prompt-key`, { key: el.dataset.key }), 'Sent');
1180
1204
  case 'terminal':
@@ -3604,6 +3628,8 @@ function handleEvent(msg) {
3604
3628
  for (const r of msg.races ?? []) races.set(r.id, r);
3605
3629
  teams.clear();
3606
3630
  for (const t of msg.teams ?? []) teams.set(t.id, t);
3631
+ builtReports = msg.built ?? [];
3632
+ builtUi.render();
3607
3633
  msg.agents.forEach(watchDocs);
3608
3634
  if (selectedId && !agents.has(selectedId)) selectedId = null;
3609
3635
  if (openTermOnLoad && selectedId) {
@@ -3626,6 +3652,9 @@ function handleEvent(msg) {
3626
3652
  teams.set(msg.team.id, msg.team);
3627
3653
  } else if (msg.t === 'team-removed') {
3628
3654
  teams.delete(msg.id);
3655
+ } else if (msg.t === 'built') {
3656
+ builtReports = msg.reports;
3657
+ builtUi.render();
3629
3658
  } else if (msg.t === 'backlog') {
3630
3659
  backlogState = msg.backlog;
3631
3660
  } else if (msg.t === 'projects') {
@@ -0,0 +1,207 @@
1
+ // The Built tab: agents made with the builder (agentkit agents, docs/agentkit.md). Build one from
2
+ // a task; review its analysis, spec and tests; test it; run it here; open its chat; download it.
3
+ import { attachBox } from '/attach.js';
4
+
5
+ const STATUS = { designed: 'designed', 'needs-fixing': 'needs fixing', testing: 'testing…', tested: 'tests pass', failing: 'tests fail', running: 'running' };
6
+
7
+ const ago = (iso) => {
8
+ const s = Math.max(0, (Date.now() - Date.parse(iso)) / 1000);
9
+ return s < 60 ? 'just now' : s < 3600 ? `${Math.round(s / 60)} min ago` : s < 86400 ? `${Math.round(s / 3600)} h ago` : `${Math.round(s / 86400)} d ago`;
10
+ };
11
+
12
+ export function initBuilt({ api, esc, toast, $, isOpen, setCount, reports = () => [] }) {
13
+ const view = $('#built-view');
14
+ const dlg = $('#built-dialog');
15
+ const body = $('#built-body');
16
+ let list = [];
17
+ let open = null; // the built agent shown in the dialog
18
+ let poll = null;
19
+
20
+ async function load() {
21
+ try {
22
+ list = await api('GET', '/builder');
23
+ } catch {
24
+ list = [];
25
+ }
26
+ setCount(list.length || '');
27
+ if (isOpen()) render();
28
+ }
29
+
30
+ function render() {
31
+ view.innerHTML = `<div class="built-head"><button class="small primary" data-bt="new">+ Build an agent</button><span class="hint">An agent for a task or workflow: it plans, acts, checks itself and keeps working in the background.</span></div>
32
+ ${list.length ? `<ul class="built-list">${list.map((b) => `<li data-bt-open="${esc(b.name)}"><div class="line1"><b>${esc(b.title ?? b.name)}</b>${b.running ? ' <span class="dot-run" title="Running">●</span>' : ''}<span class="spacer"></span><span class="tag ${b.status === 'tested' ? 'open' : b.status === 'failing' || b.status === 'needs-fixing' ? 'fail' : ''}">${esc(b.pending ?? STATUS[b.status] ?? b.status)}</span></div>
33
+ <div class="hint">${esc(b.name)} · ${esc(b.engine ?? '')}${b.results?.total ? ` · tests ${b.results.passed}/${b.results.total}` : ''}${b.triggers.length ? ` · ${esc(b.triggers.join(', '))}` : ''}</div></li>`).join('')}</ul>` : '<p class="hint built-empty">No agents built yet.</p>'}
34
+ ${reporting()}`;
35
+ }
36
+
37
+ // ------------------------------------------------------------ agents reporting in (here or elsewhere)
38
+ function reporting() {
39
+ const rs = reports();
40
+ const head = `<div class="built-head reporting-head"><b>Reporting in</b><span class="hint">Agents running here or on other computers: their runs, cost and the tools they wait to have approved.</span><span class="spacer"></span><button class="small ghost" data-bt="connect">Connect one from elsewhere</button></div><div id="built-connect" ${connectHtml ? '' : 'hidden'}>${connectHtml ?? ''}</div>`;
41
+ if (!rs.length) return `${head}<p class="hint built-empty">None yet. Agents you start here report automatically.</p>`;
42
+ return `${head}<ul class="built-list reports">${rs.map((r) => `<li data-report="${esc(r.key)}">
43
+ <div class="line1"><span class="dot-run ${r.online ? '' : 'off'}" title="${r.online ? 'Online' : 'Not heard from'}">●</span> <b>${esc(r.title)}</b> <span class="hint">${esc(r.host)}</span><span class="spacer"></span>
44
+ ${r.chat && r.online ? `<a class="button small ghost" href="${esc(r.chat)}" target="_blank" rel="noopener">Chat ↗</a>` : ''}
45
+ <button class="small ghost" data-rp="forget" title="Remove it from this list (it shows up again when it reports)">Forget</button></div>
46
+ <div class="hint">${esc(r.engine)} · ${r.online ? (r.current ? `working on a run (${esc(r.current.source)})` : 'idle') : `last seen ${ago(r.lastSeen)}`} · ${r.totals.runs} run${r.totals.runs === 1 ? '' : 's'}${r.totals.failed ? `, ${r.totals.failed} failed` : ''}${r.totals.costUsd ? ` · $${r.totals.costUsd.toFixed(2)}` : ''}${r.triggers.length ? ` · ${esc(r.triggers.join(', '))}` : ''}</div>
47
+ ${r.pending.map((p) => `<div class="report-approval" data-ap="${esc(p.id)}"><b>Wants to run <code>${esc(p.tool)}</code></b> <span class="hint">${ago(p.at)}</span><pre>${esc(JSON.stringify(p.input, null, 2).slice(0, 1500))}</pre><button class="small primary" data-rp="approve">Approve</button> <button class="small" data-rp="deny">Deny</button></div>`).join('')}
48
+ ${r.runs.length ? `<details><summary class="hint">Recent runs</summary><ul class="built-runs">${r.runs.map((x) => `<li><span class="tag ${x.status === 'failed' ? 'fail' : x.status === 'done' ? 'open' : ''}">${esc(x.status)}</span> <span class="hint">${esc(x.source)} · ${x.at ? ago(x.at) : ''}${x.costUsd ? ` · $${x.costUsd.toFixed(3)}` : ''}${x.ms ? ` · ${Math.round(x.ms / 1000)}s` : ''}</span><div>${esc(x.result.slice(0, 400))}</div></li>`).join('')}</ul></details>` : ''}
49
+ </li>`).join('')}</ul>`;
50
+ }
51
+
52
+ let connectHtml = null; // the connection details, kept open across live re-renders
53
+ async function connect() {
54
+ const box = $('#built-connect');
55
+ if (connectHtml) return (connectHtml = null), (box.hidden = true);
56
+ try {
57
+ const c = await api('GET', '/agentkit/connect');
58
+ const lines = (url, cert) => [`AGENTKIT_REPORT_URL=${url}`, `AGENTKIT_REPORT_TOKEN=${c.token}`, ...(cert ? [`AGENTKIT_REPORT_CERT=${cert}`] : [])].join('\n');
59
+ box.innerHTML = `<p class="hint">Set these where the agent runs (in its environment, or in its service), then start it. It reports here and asks here for approvals.</p>
60
+ ${c.remote.length ? c.remote.map((u) => `<p><b>From another computer</b> <span class="hint">(over remote access)</span></p><pre class="code-area">${esc(lines(u.replace(/\/$/, ''), c.cert))}</pre>`).join('') : '<p class="hint">Turn on remote access (Settings) to connect agents on other computers.</p>'}
61
+ <p><b>On this computer</b></p><pre class="code-area">${esc(lines(c.local))}</pre>`;
62
+ } catch (err) {
63
+ box.innerHTML = `<p class="hint">${esc(err.message)}. Open AI Agent Manager on this computer to see the connection details.</p>`;
64
+ }
65
+ connectHtml = box.innerHTML;
66
+ box.hidden = false;
67
+ }
68
+
69
+ // ------------------------------------------------------------ build
70
+ const buildDlg = $('#build-dialog');
71
+ const files = attachBox({ slot: $('#build-attach'), form: $('#build-form'), paste: $('#build-form textarea'), esc, toast });
72
+ async function openBuild() {
73
+ const sel = $('#build-form').engine;
74
+ if (sel.options.length <= 1) {
75
+ const engines = await api('GET', '/engines').catch(() => []);
76
+ for (const e of engines.filter((x) => x.installed && x.id !== 'claude')) sel.append(Object.assign(document.createElement('option'), { value: e.id.startsWith('acp-') ? `acp:${e.id.slice(4)}` : e.id, textContent: e.label }));
77
+ }
78
+ $('#build-error').hidden = true;
79
+ buildDlg.showModal();
80
+ $('#build-form textarea').focus();
81
+ }
82
+ $('#build-form').addEventListener('submit', async (e) => {
83
+ e.preventDefault();
84
+ const f = e.target;
85
+ if (files.pending()) return toast('Still uploading the attached files');
86
+ const btn = f.querySelector('button[type=submit]');
87
+ btn.disabled = true;
88
+ btn.textContent = 'Designing… (about a minute)';
89
+ try {
90
+ const b = await api('POST', '/builder', { task: f.task.value, engine: f.engine.value, attachments: files.paths().length ? files.paths() : undefined });
91
+ buildDlg.close();
92
+ f.reset();
93
+ files.clear();
94
+ await load();
95
+ show(b);
96
+ } catch (err) {
97
+ $('#build-error').textContent = err.message;
98
+ $('#build-error').hidden = false;
99
+ } finally {
100
+ btn.disabled = false;
101
+ btn.textContent = 'Design the agent';
102
+ }
103
+ });
104
+ $('#build-cancel').onclick = () => buildDlg.close();
105
+
106
+ // ------------------------------------------------------------ one agent
107
+ function show(b) {
108
+ open = b;
109
+ $('#built-title').textContent = b.title ?? b.name;
110
+ const a = b.analysis ?? {};
111
+ const li = (xs) => (xs?.length ? `<ul>${xs.map((x) => `<li>${esc(x)}</li>`).join('')}</ul>` : '<p class="hint">—</p>');
112
+ const r = b.results;
113
+ body.innerHTML = `
114
+ <div class="built-actions">
115
+ <span class="tag ${b.status === 'tested' ? 'open' : b.status === 'failing' || b.status === 'needs-fixing' ? 'fail' : ''}">${esc(b.pending ?? STATUS[b.status] ?? b.status)}</span>
116
+ ${b.running ? `<span class="hint">running since ${new Date(b.running.since).toLocaleTimeString()}</span>${b.running.url ? ` <a href="${esc(b.running.url)}" target="_blank" rel="noopener">Open its chat ↗</a>` : ''}` : ''}
117
+ <span class="spacer"></span>
118
+ <button class="small" data-bt="test" ${b.pending ? 'disabled' : ''} title="Run each test through the real engine and judge the results">${b.pending === 'testing' ? 'Testing…' : 'Run tests'}</button>
119
+ <button class="small ${b.running ? '' : 'primary'}" data-bt="${b.running ? 'stop' : 'start'}" ${b.specError ? 'disabled' : ''} title="${b.running ? 'Stop it' : 'Run it continuously on this computer: its triggers, chat page and API'}">${b.running ? 'Stop' : 'Start'}</button>
120
+ <a class="button small ghost" href="/api/builder/${esc(b.name)}/download" title="Its folder (agent.yaml, instructions, tests) to run anywhere with agentkit">Download folder</a>
121
+ <button class="small ghost" data-bt="delete">Delete</button>
122
+ </div>
123
+ <div class="built-exe"><b>Single executable</b> <span class="hint">one file that runs this agent anywhere, no Node or agentkit needed (its engine's CLI must be installed and signed in)</span>
124
+ <div class="built-exe-row"><select id="built-platform">${(b.platforms ?? []).map((p) => `<option value="${esc(p)}" ${p === b.host ? 'selected' : ''}>${esc(p)}${p === b.host ? ' (this computer)' : ''}</option>`).join('')}</select>
125
+ <button class="small" data-bt="package" ${b.pending || b.specError ? 'disabled' : ''}>${b.pending?.startsWith('packaging') ? 'Building… (a minute or two)' : 'Build executable'}</button>
126
+ ${Object.entries(b.executables ?? {}).map(([p, x]) => `<a class="button small ghost" href="/api/builder/${esc(b.name)}/executable/${esc(p)}" title="Built ${esc(new Date(x.at).toLocaleString())}">⬇ ${esc(p)} · ${Math.round(x.size / 1e6)} MB</a>`).join('')}</div>
127
+ ${b.packageError ? `<p class="error">${esc(b.packageError)}</p>` : ''}</div>
128
+ ${b.specError ? `<p class="error">${esc(b.specError)}</p>` : ''}
129
+ ${b.missingEnv.length ? `<p class="hint warn">Set these in the environment before running it: ${esc(b.missingEnv.join(', '))}</p>` : ''}
130
+ <details open><summary><b>Analysis</b></summary><p>${esc(a.problem ?? '')}</p><div class="row three"><div><b>Inputs</b>${li(a.inputs)}</div><div><b>Outputs</b>${li(a.outputs)}</div><div><b>Risks</b>${li(a.risks)}</div></div>
131
+ ${b.tools?.length ? `<p><b>Tools</b></p><ul>${b.tools.map((t) => `<li><code>${esc(t.name)}</code> ${esc(t.why)}</li>`).join('')}</ul>` : ''}</details>
132
+ <details ${r ? 'open' : ''}><summary><b>Tests</b> ${r?.total ? `<span class="hint">${r.passed}/${r.total} pass · ${new Date(r.at).toLocaleString()}</span>` : '<span class="hint">not run yet</span>'}</summary>
133
+ ${r?.error ? `<p class="error">${esc(r.error)}</p>` : ''}
134
+ <ol class="built-tests">${(r?.list?.length ? r.list : b.tests ?? []).map((t) => `<li class="${t.pass === true ? 'pass' : t.pass === false ? 'fail' : ''}"><div><b>${t.pass === true ? '✓' : t.pass === false ? '✗' : '·'}</b> ${esc(t.input)}</div><div class="hint">Expect: ${esc(t.expect)}</div>${t.why ? `<div class="hint">${esc(t.why)}</div>` : ''}${t.result ? `<details><summary class="hint">Result</summary><pre>${esc(t.result)}</pre></details>` : ''}</li>`).join('')}</ol></details>
135
+ <details><summary><b>agent.yaml</b> and instructions</summary>
136
+ <textarea id="built-yaml" class="code-area" rows="16" spellcheck="false">${esc(b.yaml)}</textarea>
137
+ <textarea id="built-instr" class="code-area" rows="10" spellcheck="false">${esc(b.instructionsText)}</textarea>
138
+ <div class="dialog-actions"><button class="small primary" data-bt="save">Save</button></div></details>
139
+ ${b.runs.length ? `<details><summary><b>Recent runs</b></summary><ul class="built-runs">${b.runs.map((x) => `<li><span class="tag">${esc(x.status)}</span> <span class="hint">${esc(x.source ?? '')} · ${x.startedAt ? new Date(x.startedAt).toLocaleString() : ''}</span><div>${esc(String(x.result ?? '').slice(0, 300))}</div></li>`).join('')}</ul></details>` : ''}
140
+ <p class="hint">Task: ${esc(b.task ?? '')}</p>`;
141
+ if (!dlg.open) dlg.showModal();
142
+ clearInterval(poll);
143
+ if (b.pending) poll = setInterval(refresh, 3000);
144
+ }
145
+
146
+ async function refresh() {
147
+ if (!open || !dlg.open) return clearInterval(poll);
148
+ try {
149
+ const b = await api('GET', `/builder/${open.name}`);
150
+ if (b.pending !== open.pending || b.updatedAt !== open.updatedAt) show(b);
151
+ if (!b.pending) clearInterval(poll), load();
152
+ } catch {}
153
+ }
154
+
155
+ async function act(what) {
156
+ const name = open?.name;
157
+ try {
158
+ if (what === 'test') show(await api('POST', `/builder/${name}/test`));
159
+ else if (what === 'package') show(await api('POST', `/builder/${name}/package`, { platform: $('#built-platform').value }));
160
+ else if (what === 'start') {
161
+ show(await api('POST', `/builder/${name}/start`));
162
+ setTimeout(refresh, 1500);
163
+ } else if (what === 'stop') show(await api('POST', `/builder/${name}/stop`));
164
+ else if (what === 'save') {
165
+ show(await api('PUT', `/builder/${name}`, { yaml: $('#built-yaml').value, instructions: $('#built-instr').value }));
166
+ toast('Saved');
167
+ } else if (what === 'delete') {
168
+ if (!confirm(`Delete ${name} and its folder?`)) return;
169
+ await api('DELETE', `/builder/${name}`);
170
+ dlg.close();
171
+ }
172
+ load();
173
+ } catch (err) {
174
+ toast(`Error: ${err.message}`, 6000);
175
+ }
176
+ }
177
+
178
+ view.addEventListener('click', async (e) => {
179
+ if (e.target.closest('[data-bt="new"]')) return openBuild();
180
+ if (e.target.closest('[data-bt="connect"]')) return connect();
181
+ const rp = e.target.closest('[data-rp]');
182
+ if (rp) {
183
+ const key = rp.closest('[data-report]').dataset.report;
184
+ try {
185
+ if (rp.dataset.rp === 'forget') await api('DELETE', `/agentkit/agents/${encodeURIComponent(key)}`);
186
+ else {
187
+ rp.disabled = true;
188
+ await api('POST', `/agentkit/agents/${encodeURIComponent(key)}/approvals/${encodeURIComponent(rp.closest('[data-ap]').dataset.ap)}`, { allow: rp.dataset.rp === 'approve' });
189
+ toast(rp.dataset.rp === 'approve' ? 'Approved; the agent goes on' : 'Declined');
190
+ }
191
+ } catch (err) {
192
+ toast(`Error: ${err.message}`, 6000);
193
+ }
194
+ return;
195
+ }
196
+ const li = e.target.closest('[data-bt-open]');
197
+ if (li) show(await api('GET', `/builder/${li.dataset.btOpen}`));
198
+ });
199
+ body.addEventListener('click', (e) => {
200
+ const b = e.target.closest('[data-bt]');
201
+ if (b) act(b.dataset.bt);
202
+ });
203
+ $('#built-close').onclick = () => (clearInterval(poll), dlg.close());
204
+
205
+ load();
206
+ return { load, render };
207
+ }
package/public/index.html CHANGED
@@ -64,6 +64,7 @@
64
64
  <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>
65
65
  <button class="side-tab" data-tab="backlog" role="tab">Backlog <span id="backlog-count" class="badge"></span></button>
66
66
  <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>
67
+ <button class="side-tab" data-tab="built" role="tab" title="Agents you build for a task or workflow: they plan, act, check themselves and run in the background">Built <span id="built-count" class="badge"></span></button>
67
68
  </div>
68
69
  <div id="agents-view">
69
70
  <div class="counts" id="counts"></div>
@@ -79,6 +80,7 @@
79
80
  <div id="backlog-view" hidden></div>
80
81
  <div id="inbox-view" hidden></div>
81
82
  <div id="land-view" hidden></div>
83
+ <div id="built-view" hidden></div>
82
84
  <div id="empty" class="empty" hidden>
83
85
  <p>No agents yet.</p>
84
86
  <p>Add one or more intents and a repo or folder to get started.</p>
@@ -377,6 +379,25 @@
377
379
  </form>
378
380
  </dialog>
379
381
 
382
+ <dialog id="build-dialog">
383
+ <form id="build-form" method="dialog">
384
+ <h2>Build an agent</h2>
385
+ <p class="hint">Describe the task or workflow it takes over: what comes in, what it should do, what it must never do. It's designed for you to review (analysis, spec, test cases) before it runs.</p>
386
+ <label>Task or workflow
387
+ <textarea name="task" rows="8" required placeholder="Every morning, look at new support tickets in Jira, label each by product area, route it to the right team, and draft a first reply for a person to send. Never close or reply to tickets on its own."></textarea>
388
+ </label>
389
+ <div id="build-attach"></div>
390
+ <label>Engine <select name="engine"><option value="claude">Claude Code</option></select></label>
391
+ <p id="build-error" class="error" hidden></p>
392
+ <div class="dialog-actions"><button type="button" id="build-cancel" class="ghost">Cancel</button><button type="submit" class="primary">Design the agent</button></div>
393
+ </form>
394
+ </dialog>
395
+
396
+ <dialog id="built-dialog" class="doc-dialog">
397
+ <div class="doc-head"><h2 id="built-title"></h2><span class="spacer"></span><button type="button" id="built-close" class="small ghost">Close</button></div>
398
+ <div id="built-body" class="built-body"></div>
399
+ </dialog>
400
+
380
401
  <dialog id="doc-dialog" class="doc-dialog">
381
402
  <div class="doc-head">
382
403
  <h2 id="doc-title"></h2>
package/public/style.css CHANGED
@@ -497,6 +497,7 @@ dialog::backdrop { background: rgba(0, 0, 0, .45); }
497
497
  .need-head { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 4px; }
498
498
  .need-kind { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
499
499
  .need.prompt .need-kind { color: var(--red); }
500
+ .need.approval .need-kind { color: var(--red); }
500
501
  .need.question .need-kind { color: var(--accent); }
501
502
  .need.stuck .need-kind { color: var(--yellow); }
502
503
  .need.conflict .need-kind { color: #8e59ff; }
@@ -949,3 +950,26 @@ body.remote .settings-links .local-only { display: none; }
949
950
  .clip-box { margin: 10px 12px; width: calc(100% - 24px); font: 13px var(--mono, ui-monospace, monospace); }
950
951
  .clip-foot { display: flex; align-items: center; gap: 8px; padding: 0 12px 12px; }
951
952
  .clip-foot .hint { flex: 1; }
953
+
954
+ /* Built agents. */
955
+ .built-head { display: flex; flex-direction: column; gap: 6px; padding: 10px 12px; }
956
+ .built-list { list-style: none; margin: 0; padding: 0; }
957
+ .built-list li { padding: 9px 12px; border-bottom: 1px solid var(--border); cursor: pointer; }
958
+ .built-list li:hover { background: var(--panel-2); }
959
+ .built-list .line1 { display: flex; align-items: center; gap: 6px; }
960
+ .built-empty { padding: 0 12px; }
961
+ .dot-run { color: var(--green); font-size: 10px; }
962
+ .dot-run.off { color: var(--muted); }
963
+ .reporting-head { margin-top: 18px; }
964
+ .report-approval { margin: 8px 0; padding: 8px 10px; border: 1px solid var(--yellow); border-radius: 8px; }
965
+ .report-approval pre, .need-input { font-size: 12px; white-space: pre-wrap; max-height: 160px; overflow: auto; margin: 6px 0; }
966
+ #built-connect { padding: 0 12px; } #built-connect pre { white-space: pre-wrap; word-break: break-all; padding: 8px; border: 1px solid var(--border); border-radius: 6px; }
967
+ .built-body { padding: 0 4px 12px; display: flex; flex-direction: column; gap: 10px; }
968
+ .built-body details > summary { cursor: pointer; padding: 4px 0; }
969
+ .built-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
970
+ .built-exe { margin: 10px 0; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; }
971
+ .built-exe-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
972
+ .built-tests li { margin-bottom: 8px; } .built-tests li.pass b { color: var(--green); } .built-tests li.fail b { color: var(--red); }
973
+ .built-tests pre, .built-runs div { white-space: pre-wrap; font-size: 12.5px; }
974
+ .code-area { width: 100%; font: 12.5px/1.45 var(--mono); margin-top: 6px; }
975
+ .built-runs { list-style: none; padding: 0; } .built-runs li { padding: 6px 0; border-bottom: 1px solid var(--border); }