ai-agentmanager 1.12.0 → 1.14.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 +20 -0
- package/README.md +4 -1
- package/docs/agentkit.md +15 -1
- package/package.json +1 -1
- package/public/app.js +66 -7
- package/public/built.js +147 -22
- package/public/index.html +9 -3
- package/public/menu.js +18 -0
- package/public/style.css +12 -0
- package/src/agentkit/engines.js +14 -1
- package/src/builder.js +207 -82
- package/src/cli.js +33 -0
- package/src/manager.js +80 -4
- package/src/rules.js +161 -0
- package/src/server.js +27 -5
- package/src/stuck.js +9 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.14.0 (2026-11-02)
|
|
4
|
+
|
|
5
|
+
- **Fewer questions: approval rules.** When you allow the same command 3 times in a repo (`npm test`, an MCP tool, fetching from a site), the inbox offers **Always allow**. Allowed rules go to every Claude Code agent in that repo at launch and answer its open prompts, including the ones waiting when you allow it. Never offered for compound commands or ones that delete, escalate, publish or reach the network. Settings → Approval rules and `agentmgr rules` list them, with how many prompts each answered, and remove them.
|
|
6
|
+
- **Prompt cards say what they ask about:** "Wants to run `npm test`" (from the session's transcript) instead of only "Do you want to proceed?".
|
|
7
|
+
- **Yes to all:** several agents waiting on the same command get one card that answers them together.
|
|
8
|
+
- **Built agents' tests run in a throwaway copy** of the agent's folder: what a run writes (summaries, changed fixtures, memory) no longer stays in the agent or changes the next test's starting point. The copy keeps the agent's memory, not its engine sessions or run logs. A `workdir` outside the agent's folder is still used in place.
|
|
9
|
+
|
|
10
|
+
## 1.13.0 (2026-11-01)
|
|
11
|
+
|
|
12
|
+
- **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).
|
|
13
|
+
- 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**.
|
|
14
|
+
- 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**.
|
|
15
|
+
- 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.
|
|
16
|
+
- **Fixes from building a real agent:**
|
|
17
|
+
- 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.
|
|
18
|
+
- 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.
|
|
19
|
+
- The test judge sees the files each test run created or changed, not only the agent's reply, and the Tests list shows them.
|
|
20
|
+
- Build sessions read the agentkit reference without asking, and write the agent's files with their file tools.
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
3
23
|
## 1.12.0 (2026-11-01)
|
|
4
24
|
|
|
5
25
|
- **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).
|
package/README.md
CHANGED
|
@@ -541,13 +541,16 @@ The **Inbox** tab (and `agentmgr inbox`) lists what needs you, across all agents
|
|
|
541
541
|
|
|
542
542
|
| Kind | What it is | How you answer |
|
|
543
543
|
| --- | --- | --- |
|
|
544
|
-
| Needs input | A permission prompt or question menu is open in the agent's terminal | Click a choice (1, 2, 3, Esc); no need to open the terminal |
|
|
544
|
+
| Needs input | A permission prompt or question menu is open in the agent's terminal; for Claude Code, the card shows the command or file it asks about | Click a choice (1, 2, 3, Esc); no need to open the terminal. Several agents waiting on the same command: **Yes to all** |
|
|
545
|
+
| Always allow? | You allowed the same command 3 times in a repo | **Always allow** (agents there stop asking) or **Not now** |
|
|
545
546
|
| Waiting on you | The AI judge read an idle screen and saw a question (or an error) no rule recognized | Click a choice or type an answer; **Not waiting** if it misread |
|
|
546
547
|
| Question | The agent asked something without stopping | Click an option or type an answer |
|
|
547
548
|
| AI review | The agent said it was done and the judge checked the work against the intent and brief | **Send back to finish** (types the missing items in) or **Looks good** |
|
|
548
549
|
| Stuck? | The agent seems to be going in circles | Send the suggested nudge (edit it first if you like) or dismiss |
|
|
549
550
|
| Overlap | It changes the same files as another agent | Tell one or both agents; see the suggested merge order |
|
|
550
551
|
|
|
552
|
+
**Fewer questions: approval rules.** Each yes you give a permission prompt counts toward the command, per repo (`npm test`, `pytest`, `git status`; an MCP tool; fetching from a site). After 3, the inbox offers to always allow it there. An allowed rule goes to every agent in that repo when it starts (Claude Code's `--allowedTools`), and answers its prompts that are already open, so you aren't asked again. Rules are never offered for commands that chain, redirect or substitute (`&&`, `|`, `>`, `$(…)`), or that delete, escalate, publish or reach the network (`rm`, `sudo`, `git push`, `npm publish`, `curl`…). Settings → Approval rules (or `agentmgr rules`) lists them with how many prompts each answered; **Remove** to be asked again. Stored in `~/.agentmanager/rules.json`; In config.json, `"rules": { "auto": false }` stops rules answering open prompts and `"rules": { "suggestAfter": 5 }` changes the count. Claude Code agents only for now.
|
|
553
|
+
|
|
551
554
|
**Questions without stopping.** Task agents are told to write questions that don't block all their work to `QUESTIONS.md` next to their brief, with options and the default they'll assume meanwhile, and then keep going:
|
|
552
555
|
|
|
553
556
|
```markdown
|
package/docs/agentkit.md
CHANGED
|
@@ -27,7 +27,8 @@ tools: # MCP servers the engine gets
|
|
|
27
27
|
command: npx
|
|
28
28
|
args: [-y, "@acme/jira-mcp"]
|
|
29
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)
|
|
30
|
+
allow: [Read, Grep, "mcp__jira__*"] # tools it may use without asking (engine names);
|
|
31
|
+
# "Edit(./out/**)" lets it write files under ./out only
|
|
31
32
|
approve: ["mcp__jira__transition"] # tools that need a person's yes (asked in chat / Inbox)
|
|
32
33
|
approveTimeout: 30m # no answer by then counts as no
|
|
33
34
|
report: # report to AI Agent Manager (or set AGENTKIT_REPORT_*)
|
|
@@ -55,6 +56,19 @@ interfaces:
|
|
|
55
56
|
mcp: true # `<agent> mcp` exposes it as an MCP tool
|
|
56
57
|
```
|
|
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 in a throwaway copy of the agent's folder, so what one run writes doesn't change the agent or the next test, and is judged against `expect` and the files it wrote), **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
|
+
|
|
58
72
|
## A run
|
|
59
73
|
|
|
60
74
|
1. **Input** arrives: a trigger fires (with its payload), someone chats, an A2A task or an MCP
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -127,7 +127,15 @@ $('#new-playbook').onclick = () => {
|
|
|
127
127
|
};
|
|
128
128
|
$('#triggers-open').onclick = () => triggersUi.open();
|
|
129
129
|
let builtReports = []; // agents built with agentkit that report here (runs, approvals)
|
|
130
|
-
|
|
130
|
+
let rulesState = { allowed: [], suggestions: [] }; // approval rules learned from your answers (src/rules.js)
|
|
131
|
+
const builtUi = initBuilt({
|
|
132
|
+
api, esc, toast, $,
|
|
133
|
+
isOpen: () => sideTab === 'built',
|
|
134
|
+
setCount: (n) => ($('#built-count').textContent = n),
|
|
135
|
+
reports: () => builtReports,
|
|
136
|
+
openSession: (id) => (showTab('agents'), select(id)),
|
|
137
|
+
onPanel: () => agents.get(selectedId)?.builds && renderDetail(),
|
|
138
|
+
});
|
|
131
139
|
const landUi = initLand({ api, esc, toast, agents, $, scoreBadge, select: (id) => (showTab('agents'), select(id)), openReview: (id) => reviewUi.open(id), isOpen: () => sideTab === 'land' });
|
|
132
140
|
|
|
133
141
|
function statusOf(a) {
|
|
@@ -713,7 +721,7 @@ function agentItem(a) {
|
|
|
713
721
|
const idle = !isActive(a);
|
|
714
722
|
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' : ''}">
|
|
715
723
|
<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>
|
|
716
|
-
<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>
|
|
724
|
+
<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>
|
|
717
725
|
${a.status === 'preparing' && a.progress ? `<div class="progress-line">${esc(progressText(a.progress))}</div>` : ''}
|
|
718
726
|
<div class="meta">
|
|
719
727
|
<span class="mono">${a.id}</span>
|
|
@@ -1006,7 +1014,7 @@ function flags(a) {
|
|
|
1006
1014
|
}
|
|
1007
1015
|
|
|
1008
1016
|
const TRIAGED = { now: 'sent (AI: urgent)', digest: 'held for the digest', skip: 'dropped by the AI judge' };
|
|
1009
|
-
const NEED_ORDER = { approval: 0, prompt: 0, waiting: 1, plan: 1, question: 1, team: 1, review: 2, stuck: 3, conflict: 4 };
|
|
1017
|
+
const NEED_ORDER = { approval: 0, prompt: 0, waiting: 1, plan: 1, question: 1, team: 1, rule: 2, review: 2, stuck: 3, conflict: 4 };
|
|
1010
1018
|
/** What an agent needs from the user. `both`: overlaps on both agents (detail), else only the one that should rebase (inbox). */
|
|
1011
1019
|
function needsFor(a, { both = false } = {}) {
|
|
1012
1020
|
const items = [];
|
|
@@ -1030,20 +1038,49 @@ function teamNeeds() {
|
|
|
1030
1038
|
function approvalNeeds() {
|
|
1031
1039
|
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
1040
|
}
|
|
1041
|
+
/** The repo a rule belongs to, short: its last two path parts. */
|
|
1042
|
+
const repoLabel = (k) => String(k).replace(/^dir:/, '').split('/').filter(Boolean).slice(-2).join('/');
|
|
1043
|
+
/** Commands you keep allowing in a repo, offered as a standing rule. */
|
|
1044
|
+
function ruleNeeds() {
|
|
1045
|
+
return rulesState.suggestions.map((r) => ({ a: { id: 'rules', name: repoLabel(r.repo), priority: 2 }, kind: 'rule', key: `rule:${r.repo}:${r.rule}`, r }));
|
|
1046
|
+
}
|
|
1033
1047
|
function inboxItems() {
|
|
1034
|
-
return [...approvalNeeds(), ...[...agents.values()].flatMap((a) => needsFor(a)), ...teamNeeds()]
|
|
1048
|
+
return [...approvalNeeds(), ...[...agents.values()].flatMap((a) => needsFor(a)), ...teamNeeds(), ...ruleNeeds()]
|
|
1035
1049
|
.sort((x, y) => NEED_ORDER[x.kind] - NEED_ORDER[y.kind] || (x.a.priority ?? 2) - (y.a.priority ?? 2));
|
|
1036
1050
|
}
|
|
1037
1051
|
|
|
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' };
|
|
1052
|
+
const NEED_LABEL = { rule: 'Always allow?', approval: 'Approval', team: 'Team', prompt: 'Needs input', waiting: 'Waiting on you', plan: 'Plan review', question: 'Question', review: 'AI review', stuck: 'Stuck?', conflict: 'Overlap' };
|
|
1039
1053
|
/** Why the AI judge thinks so, as a small tag. */
|
|
1040
1054
|
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>`;
|
|
1055
|
+
/** What a permission prompt asks about: the command, file or tool. */
|
|
1056
|
+
const toolLine = (t) => (t.name === 'Bash' ? `Wants to run <code>${esc(t.text)}</code>` : `Wants to use ${esc(t.name)}${t.text ? ` <code>${esc(t.text)}</code>` : ''}`);
|
|
1057
|
+
/** Agents waiting on the same command, answered together. */
|
|
1058
|
+
function promptGroups(items) {
|
|
1059
|
+
const groups = new Map();
|
|
1060
|
+
for (const it of items) {
|
|
1061
|
+
const p = it.kind === 'prompt' && it.a.runtime.prompt;
|
|
1062
|
+
if (!p?.tool || p.choices?.[0]?.key !== '1' || !/^yes\b/i.test(p.choices[0].label)) continue;
|
|
1063
|
+
const k = `${p.tool.name}\0${p.tool.text}`;
|
|
1064
|
+
(groups.get(k) ?? groups.set(k, []).get(k)).push(it);
|
|
1065
|
+
}
|
|
1066
|
+
return [...groups.values()].filter((g) => g.length > 1);
|
|
1067
|
+
}
|
|
1068
|
+
function groupCard(g) {
|
|
1069
|
+
const t = g[0].a.runtime.prompt.tool;
|
|
1070
|
+
const names = g.map((it) => `<a href="#${it.a.id}" data-goto="${it.a.id}">${esc(it.a.name)}</a>`).join(', ');
|
|
1071
|
+
return `<div class="need prompt group" data-need="group:${esc(g.map((it) => it.a.id).join(','))}" data-agents="${esc(g.map((it) => it.a.id).join(','))}">
|
|
1072
|
+
<div class="need-head"><span class="need-kind prompt">${g.length} agents</span></div>
|
|
1073
|
+
<p class="need-title">${toolLine(t)}</p><p class="hint">${names}</p>
|
|
1074
|
+
<div class="need-actions"><button class="small primary" data-ib="yes-all">Yes to all ${g.length}</button></div></div>`;
|
|
1075
|
+
}
|
|
1041
1076
|
function needCard(it, { inDetail = false } = {}) {
|
|
1042
1077
|
const { a } = it;
|
|
1043
1078
|
const who = inDetail
|
|
1044
1079
|
? ''
|
|
1045
1080
|
: it.kind === 'approval'
|
|
1046
1081
|
? `<a href="#" class="need-agent" data-ib="built-open">⚙ ${esc(a.name)}</a> <span class="hint">${esc(it.r.host)}</span>`
|
|
1082
|
+
: it.kind === 'rule'
|
|
1083
|
+
? `<span class="need-agent" title="${esc(it.r.repo)}">${esc(a.name)}</span>`
|
|
1047
1084
|
: `<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>` : ''}`;
|
|
1048
1085
|
const head = `<div class="need-head"><span class="need-kind ${it.kind}">${NEED_LABEL[it.kind]}</span>${who}</div>`;
|
|
1049
1086
|
const draft = (k, fallback = '') => esc(needDrafts.get(`${it.key}:${k}`) ?? fallback);
|
|
@@ -1053,9 +1090,13 @@ function needCard(it, { inDetail = false } = {}) {
|
|
|
1053
1090
|
body = `<p class="need-title">Wants to run <code>${esc(p.tool)}</code> <span class="hint">asked ${ago(p.at)}</span></p>
|
|
1054
1091
|
<pre class="need-input">${esc(JSON.stringify(p.input, null, 2).slice(0, 1500))}</pre>
|
|
1055
1092
|
<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>`;
|
|
1093
|
+
} else if (it.kind === 'rule') {
|
|
1094
|
+
body = `<p class="need-title">You've allowed <code>${esc(it.r.label)}</code> ${it.r.n} times in this repo. Always allow it here?</p>
|
|
1095
|
+
<p class="hint">Agents in ${esc(a.name)} won't ask about it again. Remove it any time in Settings → Approval rules.</p>
|
|
1096
|
+
<div class="need-actions"><button class="small primary" data-ib="rule-accept">Always allow</button><button class="small" data-ib="rule-dismiss">Not now</button></div>`;
|
|
1056
1097
|
} else if (it.kind === 'prompt') {
|
|
1057
1098
|
const p = a.runtime.prompt;
|
|
1058
|
-
body = `<p class="need-title">${esc(p?.question || a.runtime.attention)}</p
|
|
1099
|
+
body = `${p?.tool ? `<p class="need-title">${toolLine(p.tool)}</p><p class="hint">${esc(p.question || a.runtime.attention)}</p>` : `<p class="need-title">${esc(p?.question || a.runtime.attention)}</p>`}
|
|
1059
1100
|
<div class="need-actions">
|
|
1060
1101
|
${(p?.choices ?? []).map((c) => `<button class="small ${c.key === '1' ? 'primary' : ''}" data-ib="key" data-key="${c.key}">${c.key}. ${esc(c.label)}</button>`).join('')}
|
|
1061
1102
|
<button class="small" data-ib="key" data-key="esc" title="Escape">Esc</button>
|
|
@@ -1164,7 +1205,7 @@ function renderInbox() {
|
|
|
1164
1205
|
const view = $('#inbox-view');
|
|
1165
1206
|
if (view.contains(document.activeElement) && document.activeElement.dataset.draft) return; // don't yank the field being typed in
|
|
1166
1207
|
view.innerHTML = items.length
|
|
1167
|
-
? `<div class="counts"><span>${items.length} waiting on you</span></div><div class="needs inbox">${items.map((it) => needCard(it)).join('')}</div>`
|
|
1208
|
+
? `<div class="counts"><span>${items.length} waiting on you</span></div><div class="needs inbox">${promptGroups(items).map(groupCard).join('')}${items.map((it) => needCard(it)).join('')}</div>`
|
|
1168
1209
|
: `<div class="empty"><p>Nothing needs you right now.</p><p>Prompts waiting for an answer, questions agents ask, agents that seem stuck and agents editing the same files show up here.</p></div>`;
|
|
1169
1210
|
view.insertAdjacentHTML('beforeend', judgeLine());
|
|
1170
1211
|
}
|
|
@@ -1201,6 +1242,19 @@ async function onNeed(e) {
|
|
|
1201
1242
|
}
|
|
1202
1243
|
case 'key':
|
|
1203
1244
|
return act(() => api('POST', `/agents/${id}/prompt-key`, { key: el.dataset.key }), 'Sent');
|
|
1245
|
+
case 'yes-all': {
|
|
1246
|
+
const ids = card.dataset.agents.split(',');
|
|
1247
|
+
return act(() => Promise.all(ids.map((x) => api('POST', `/agents/${x}/prompt-key`, { key: '1' }))), `Allowed for ${ids.length} agents`);
|
|
1248
|
+
}
|
|
1249
|
+
case 'rule-accept':
|
|
1250
|
+
case 'rule-dismiss': {
|
|
1251
|
+
const [, repo, rule] = key.match(/^rule:(.+?):((?:Bash|WebFetch|mcp__).*)$/) ?? [];
|
|
1252
|
+
const accept = el.dataset.ib === 'rule-accept';
|
|
1253
|
+
return act(async () => {
|
|
1254
|
+
rulesState = await api('POST', `/rules/${accept ? 'accept' : 'dismiss'}`, { repo, rule });
|
|
1255
|
+
renderInbox();
|
|
1256
|
+
}, accept ? 'Allowed in this repo; agents there won\'t ask again' : 'Won\'t suggest it again');
|
|
1257
|
+
}
|
|
1204
1258
|
case 'terminal':
|
|
1205
1259
|
showTab('agents');
|
|
1206
1260
|
select(id);
|
|
@@ -1743,6 +1797,7 @@ function renderDetail() {
|
|
|
1743
1797
|
${a.runtime.attention ? `<div class="error-box">${esc(a.runtime.attention)}: the agent is waiting for an answer. Open its terminal to respond.</div>` : ''}
|
|
1744
1798
|
${raceBanner(a)}
|
|
1745
1799
|
${teamBanner(a)}
|
|
1800
|
+
${a.builds ? builtUi.panel(a) : ''}
|
|
1746
1801
|
${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>` : ''}
|
|
1747
1802
|
${(() => {
|
|
1748
1803
|
const items = needsFor(a, { both: true });
|
|
@@ -3629,6 +3684,7 @@ function handleEvent(msg) {
|
|
|
3629
3684
|
teams.clear();
|
|
3630
3685
|
for (const t of msg.teams ?? []) teams.set(t.id, t);
|
|
3631
3686
|
builtReports = msg.built ?? [];
|
|
3687
|
+
if (msg.rules) rulesState = msg.rules;
|
|
3632
3688
|
builtUi.render();
|
|
3633
3689
|
msg.agents.forEach(watchDocs);
|
|
3634
3690
|
if (selectedId && !agents.has(selectedId)) selectedId = null;
|
|
@@ -3641,6 +3697,7 @@ function handleEvent(msg) {
|
|
|
3641
3697
|
agents.set(msg.agent.id, msg.agent);
|
|
3642
3698
|
watchDocs(msg.agent);
|
|
3643
3699
|
agentChanged(msg.agent);
|
|
3700
|
+
if (msg.agent.builds) builtUi.sessionChanged(msg.agent);
|
|
3644
3701
|
} else if (msg.t === 'removed') {
|
|
3645
3702
|
agents.delete(msg.id);
|
|
3646
3703
|
if (selectedId === msg.id) select(null);
|
|
@@ -3652,6 +3709,8 @@ function handleEvent(msg) {
|
|
|
3652
3709
|
teams.set(msg.team.id, msg.team);
|
|
3653
3710
|
} else if (msg.t === 'team-removed') {
|
|
3654
3711
|
teams.delete(msg.id);
|
|
3712
|
+
} else if (msg.t === 'rules') {
|
|
3713
|
+
rulesState = msg.rules;
|
|
3655
3714
|
} else if (msg.t === 'built') {
|
|
3656
3715
|
builtReports = msg.reports;
|
|
3657
3716
|
builtUi.render();
|
package/public/built.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
// The Built tab: agents made with the builder (agentkit agents, docs/agentkit.md).
|
|
2
|
-
//
|
|
1
|
+
// The Built tab: agents made with the builder (agentkit agents, docs/agentkit.md). Each one is
|
|
2
|
+
// developed in an agent session working in its folder: building one starts that session, and the
|
|
3
|
+
// person keeps shaping the agent there. Here (and in a panel on that session) they review its
|
|
4
|
+
// analysis, spec and tests, test it, run it here, open its chat, package and download it.
|
|
3
5
|
import { attachBox } from '/attach.js';
|
|
4
6
|
|
|
5
|
-
const STATUS = { designed: 'designed', 'needs-fixing': 'needs fixing', testing: 'testing…', tested: 'tests pass', failing: 'tests fail', running: 'running' };
|
|
7
|
+
const STATUS = { designing: 'designing…', designed: 'designed', 'needs-fixing': 'needs fixing', testing: 'testing…', tested: 'tests pass', failing: 'tests fail', running: 'running' };
|
|
6
8
|
|
|
7
9
|
const ago = (iso) => {
|
|
8
10
|
const s = Math.max(0, (Date.now() - Date.parse(iso)) / 1000);
|
|
9
11
|
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
12
|
};
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
const sessionText = (s) => (!s ? 'no session' : s.attention ? 'session waiting for you' : s.busy ? 'session working' : s.running ? 'session idle' : 'session closed');
|
|
15
|
+
const tagClass = (b) => (b.status === 'tested' ? 'open' : b.status === 'failing' || b.status === 'needs-fixing' ? 'fail' : '');
|
|
16
|
+
const later = (a, b) => Boolean(a && b && Date.parse(a) > Date.parse(b) + 1000);
|
|
17
|
+
|
|
18
|
+
/** `openSession(id)`: show an agent session (the agents view). `onPanel()`: a session's panel has new data. */
|
|
19
|
+
export function initBuilt({ api, esc, toast, $, isOpen, setCount, reports = () => [], openSession = () => {}, onPanel = () => {} }) {
|
|
13
20
|
const view = $('#built-view');
|
|
14
21
|
const dlg = $('#built-dialog');
|
|
15
22
|
const body = $('#built-body');
|
|
@@ -28,9 +35,9 @@ export function initBuilt({ api, esc, toast, $, isOpen, setCount, reports = () =
|
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
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
|
|
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>'}
|
|
38
|
+
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. Each is developed in its own agent session.</span></div>
|
|
39
|
+
${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>${b.session ? `<a href="#${esc(b.session.id)}" class="button small ghost" data-bt-session="${esc(b.session.id)}" title="Its session: where you shape it">Session${b.session.busy ? ' ●' : ''}</a>` : ''}<span class="tag ${tagClass(b)}">${esc(b.pending ?? STATUS[b.status] ?? b.status)}</span></div>
|
|
40
|
+
<div class="hint">${esc(b.name)} · ${esc(b.engine ?? '')} · ${sessionText(b.session)}${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
41
|
${reporting()}`;
|
|
35
42
|
}
|
|
36
43
|
|
|
@@ -85,20 +92,23 @@ export function initBuilt({ api, esc, toast, $, isOpen, setCount, reports = () =
|
|
|
85
92
|
if (files.pending()) return toast('Still uploading the attached files');
|
|
86
93
|
const btn = f.querySelector('button[type=submit]');
|
|
87
94
|
btn.disabled = true;
|
|
88
|
-
btn.textContent = '
|
|
95
|
+
btn.textContent = 'Starting its session…';
|
|
89
96
|
try {
|
|
90
|
-
const b = await api('POST', '/builder', { task: f.task.value, engine: f.engine.value, attachments: files.paths().length ? files.paths() : undefined });
|
|
97
|
+
const b = await api('POST', '/builder', { task: f.task.value, engine: f.engine.value, name: f.name.value.trim() || undefined, attachments: files.paths().length ? files.paths() : undefined });
|
|
91
98
|
buildDlg.close();
|
|
92
99
|
f.reset();
|
|
93
100
|
files.clear();
|
|
101
|
+
cache.set(b.name, b);
|
|
94
102
|
await load();
|
|
95
|
-
|
|
103
|
+
// Building happens in its session: go there.
|
|
104
|
+
if (b.session) openSession(b.session.id);
|
|
105
|
+
else show(b);
|
|
96
106
|
} catch (err) {
|
|
97
107
|
$('#build-error').textContent = err.message;
|
|
98
108
|
$('#build-error').hidden = false;
|
|
99
109
|
} finally {
|
|
100
110
|
btn.disabled = false;
|
|
101
|
-
btn.textContent = '
|
|
111
|
+
btn.textContent = 'Start building';
|
|
102
112
|
}
|
|
103
113
|
});
|
|
104
114
|
$('#build-cancel').onclick = () => buildDlg.close();
|
|
@@ -110,28 +120,32 @@ export function initBuilt({ api, esc, toast, $, isOpen, setCount, reports = () =
|
|
|
110
120
|
const a = b.analysis ?? {};
|
|
111
121
|
const li = (xs) => (xs?.length ? `<ul>${xs.map((x) => `<li>${esc(x)}</li>`).join('')}</ul>` : '<p class="hint">—</p>');
|
|
112
122
|
const r = b.results;
|
|
123
|
+
const failed = (r?.list ?? []).filter((t) => t.pass === false);
|
|
113
124
|
body.innerHTML = `
|
|
125
|
+
${sessionBlock(b)}
|
|
114
126
|
<div class="built-actions">
|
|
115
|
-
<span class="tag ${b
|
|
127
|
+
<span class="tag ${tagClass(b)}">${esc(b.pending ?? STATUS[b.status] ?? b.status)}</span>
|
|
116
128
|
${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
129
|
<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>
|
|
130
|
+
<button class="small" data-bt="test" ${b.pending || b.specError || b.status === 'designing' ? 'disabled' : ''} title="Run each test through the real engine and judge the results">${b.pending === 'testing' ? 'Testing…' : 'Run tests'}</button>
|
|
131
|
+
<button class="small ${b.running ? '' : 'primary'}" data-bt="${b.running ? 'stop' : 'start'}" ${b.specError || b.status === 'designing' ? 'disabled' : ''} title="${b.running ? 'Stop it' : 'Run it continuously on this computer: its triggers, chat page and API'}">${b.running ? 'Stop' : 'Start'}</button>
|
|
120
132
|
<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
133
|
<button class="small ghost" data-bt="delete">Delete</button>
|
|
122
134
|
</div>
|
|
123
135
|
<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
136
|
<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>
|
|
137
|
+
<button class="small" data-bt="package" ${b.pending || b.specError || b.status === 'designing' ? 'disabled' : ''}>${b.pending?.startsWith('packaging') ? 'Building… (a minute or two)' : 'Build executable'}</button>
|
|
138
|
+
${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())}${later(b.changedAt, x.at) ? ', before its latest change: build it again' : ''}">⬇ ${esc(p)} · ${Math.round(x.size / 1e6)} MB${later(b.changedAt, x.at) ? ' (older)' : ''}</a>`).join('')}</div>
|
|
127
139
|
${b.packageError ? `<p class="error">${esc(b.packageError)}</p>` : ''}</div>
|
|
128
140
|
${b.specError ? `<p class="error">${esc(b.specError)}</p>` : ''}
|
|
129
141
|
${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
|
|
142
|
+
<details open><summary><b>Analysis</b></summary>${b.status === 'designing' && !a.problem ? '<p class="hint">Its session is working it out; this fills in as it writes the files.</p>' : ''}<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
143
|
${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
144
|
<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
145
|
${r?.error ? `<p class="error">${esc(r.error)}</p>` : ''}
|
|
134
|
-
|
|
146
|
+
${r?.at && later(b.changedAt, r.at) ? '<p class="hint warn">The agent changed after these results; run the tests again.</p>' : ''}
|
|
147
|
+
${failed.length && b.session ? `<button class="small" data-bt="fix">Ask its session to fix the ${failed.length} failing test${failed.length > 1 ? 's' : ''}</button>` : ''}
|
|
148
|
+
<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.files?.length ? `<div class="hint">Wrote: ${t.files.map((f) => `<code>${esc(f)}</code>`).join(', ')}</div>` : ''}${t.result ? `<details><summary class="hint">Result</summary><pre>${esc(t.result)}</pre></details>` : ''}</li>`).join('')}</ol></details>
|
|
135
149
|
<details><summary><b>agent.yaml</b> and instructions</summary>
|
|
136
150
|
<textarea id="built-yaml" class="code-area" rows="16" spellcheck="false">${esc(b.yaml)}</textarea>
|
|
137
151
|
<textarea id="built-instr" class="code-area" rows="10" spellcheck="false">${esc(b.instructionsText)}</textarea>
|
|
@@ -140,15 +154,31 @@ export function initBuilt({ api, esc, toast, $, isOpen, setCount, reports = () =
|
|
|
140
154
|
<p class="hint">Task: ${esc(b.task ?? '')}</p>`;
|
|
141
155
|
if (!dlg.open) dlg.showModal();
|
|
142
156
|
clearInterval(poll);
|
|
143
|
-
if (b.pending) poll = setInterval(refresh, 3000);
|
|
157
|
+
if (b.pending || b.status === 'designing') poll = setInterval(refresh, 3000);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Where the agent is developed: its session, and a box to ask it for a change.
|
|
161
|
+
function sessionBlock(b) {
|
|
162
|
+
if (!b.session)
|
|
163
|
+
return `<div class="built-session"><b>No session</b> <span class="hint">Open an agent session in its folder to keep shaping it with you.</span><span class="spacer"></span><button class="small primary" data-bt="session-new">Open a session for it</button></div>`;
|
|
164
|
+
return `<div class="built-session"><div class="line1"><b>Developed in its session</b> <span class="hint">${sessionText(b.session)}</span><span class="spacer"></span><a href="#${esc(b.session.id)}" class="button small primary" data-bt-session="${esc(b.session.id)}">Open session</a></div>
|
|
165
|
+
<div class="built-change"><textarea id="built-change" rows="2" placeholder="Ask its session for a change: “also post a summary to #support every evening”, “never reassign tickets”…"></textarea><button class="small" data-bt="change">Send</button></div></div>`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function failuresText(b) {
|
|
169
|
+
const failed = (b.results?.list ?? []).filter((t) => t.pass === false);
|
|
170
|
+
return `These tests failed. Change the agent so they pass (or fix a test if its expectation is wrong), check it, and tell me what you changed.\n\n${failed.map((t, i) => `${i + 1}. Input: ${t.input}\n Expected: ${t.expect}\n Judge: ${t.why}\n Result: ${String(t.result ?? '').slice(0, 600)}`).join('\n\n')}`;
|
|
144
171
|
}
|
|
145
172
|
|
|
146
173
|
async function refresh() {
|
|
147
174
|
if (!open || !dlg.open) return clearInterval(poll);
|
|
148
175
|
try {
|
|
149
176
|
const b = await api('GET', `/builder/${open.name}`);
|
|
150
|
-
|
|
151
|
-
|
|
177
|
+
cache.set(b.name, b);
|
|
178
|
+
onPanel();
|
|
179
|
+
const typing = document.activeElement?.closest?.('#built-body textarea');
|
|
180
|
+
if (!typing && JSON.stringify(b) !== JSON.stringify(open)) show(b);
|
|
181
|
+
if (!b.pending && b.status !== 'designing') clearInterval(poll), load();
|
|
152
182
|
} catch {}
|
|
153
183
|
}
|
|
154
184
|
|
|
@@ -161,6 +191,16 @@ export function initBuilt({ api, esc, toast, $, isOpen, setCount, reports = () =
|
|
|
161
191
|
show(await api('POST', `/builder/${name}/start`));
|
|
162
192
|
setTimeout(refresh, 1500);
|
|
163
193
|
} else if (what === 'stop') show(await api('POST', `/builder/${name}/stop`));
|
|
194
|
+
else if (what === 'change' || what === 'fix') {
|
|
195
|
+
const text = what === 'fix' ? failuresText(open) : $('#built-change').value.trim();
|
|
196
|
+
if (!text) return toast('Say what to change');
|
|
197
|
+
show(await api('POST', `/builder/${name}/change`, { text }));
|
|
198
|
+
toast('Sent to its session');
|
|
199
|
+
} else if (what === 'session-new') {
|
|
200
|
+
const b = await api('POST', `/builder/${name}/change`, {});
|
|
201
|
+
dlg.close();
|
|
202
|
+
if (b.session) openSession(b.session.id);
|
|
203
|
+
}
|
|
164
204
|
else if (what === 'save') {
|
|
165
205
|
show(await api('PUT', `/builder/${name}`, { yaml: $('#built-yaml').value, instructions: $('#built-instr').value }));
|
|
166
206
|
toast('Saved');
|
|
@@ -193,15 +233,100 @@ export function initBuilt({ api, esc, toast, $, isOpen, setCount, reports = () =
|
|
|
193
233
|
}
|
|
194
234
|
return;
|
|
195
235
|
}
|
|
236
|
+
const ses = e.target.closest('[data-bt-session]');
|
|
237
|
+
if (ses) return e.preventDefault(), openSession(ses.dataset.btSession);
|
|
196
238
|
const li = e.target.closest('[data-bt-open]');
|
|
197
239
|
if (li) show(await api('GET', `/builder/${li.dataset.btOpen}`));
|
|
198
240
|
});
|
|
199
241
|
body.addEventListener('click', (e) => {
|
|
242
|
+
const ses = e.target.closest('[data-bt-session]');
|
|
243
|
+
if (ses) return e.preventDefault(), dlg.close(), openSession(ses.dataset.btSession);
|
|
200
244
|
const b = e.target.closest('[data-bt]');
|
|
201
245
|
if (b) act(b.dataset.bt);
|
|
202
246
|
});
|
|
203
247
|
$('#built-close').onclick = () => (clearInterval(poll), dlg.close());
|
|
204
248
|
|
|
249
|
+
// ------------------------------------------------------------ the panel on its session
|
|
250
|
+
const cache = new Map(); // name -> its latest view (or { missing: true })
|
|
251
|
+
const fetching = new Set();
|
|
252
|
+
async function fetchOne(name) {
|
|
253
|
+
if (fetching.has(name)) return;
|
|
254
|
+
fetching.add(name);
|
|
255
|
+
try {
|
|
256
|
+
cache.set(name, await api('GET', `/builder/${encodeURIComponent(name)}`));
|
|
257
|
+
} catch (err) {
|
|
258
|
+
cache.set(name, /No built agent|Unknown agent/.test(err.message) ? { missing: true, name } : { error: err.message, name });
|
|
259
|
+
} finally {
|
|
260
|
+
fetching.delete(name);
|
|
261
|
+
onPanel();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** The panel shown on a session that builds an agent (agent `a`, with a.builds). */
|
|
266
|
+
function panel(a) {
|
|
267
|
+
const name = a.builds.name;
|
|
268
|
+
const b = cache.get(name);
|
|
269
|
+
if (!b) {
|
|
270
|
+
fetchOne(name);
|
|
271
|
+
return `<div class="built-panel"><b>⚙ Builds the agent ${esc(name)}</b> <span class="hint">loading…</span></div>`;
|
|
272
|
+
}
|
|
273
|
+
if (b.error) return `<div class="built-panel"><b>⚙ Builds the agent ${esc(name)}</b> <span class="hint">· ${esc(b.error)}</span></div>`;
|
|
274
|
+
if (b.missing) return `<div class="built-panel"><b>⚙ Built the agent ${esc(name)}</b> <span class="hint">· deleted from the Built tab</span></div>`;
|
|
275
|
+
const r = b.results;
|
|
276
|
+
const failed = (r?.list ?? []).filter((t) => t.pass === false);
|
|
277
|
+
const stale = r?.at && later(b.changedAt, r.at);
|
|
278
|
+
return `<div class="built-panel" data-bp-name="${esc(name)}">
|
|
279
|
+
<div class="line1"><b>⚙ ${esc(b.title ?? name)}</b> <span class="tag ${tagClass(b)}">${esc(b.pending ?? STATUS[b.status] ?? b.status)}</span>
|
|
280
|
+
<span class="hint">${esc(b.engine ?? '')}${b.triggers.length ? ` · ${esc(b.triggers.join(', '))}` : ''}${r?.total ? ` · tests ${r.passed}/${r.total}${stale ? ' (before the latest change)' : ''}` : b.tests.length ? ` · ${b.tests.length} tests, not run` : ''}${b.running ? ' · running here' : ''}</span>
|
|
281
|
+
<span class="spacer"></span>
|
|
282
|
+
<button class="small" data-bp="test" ${b.pending || b.specError || b.status === 'designing' ? 'disabled' : ''} title="Run each test through the real engine and judge the results">${b.pending === 'testing' ? 'Testing…' : 'Run tests'}</button>
|
|
283
|
+
<button class="small" data-bp="${b.running ? 'stop' : 'start'}" ${b.specError || b.status === 'designing' ? 'disabled' : ''} title="${b.running ? 'Stop it' : 'Run it continuously on this computer: its triggers, chat page and API'}">${b.running ? 'Stop' : 'Start'}</button>
|
|
284
|
+
${b.running?.url ? `<a class="button small ghost" href="${esc(b.running.url)}" target="_blank" rel="noopener">Chat ↗</a>` : ''}
|
|
285
|
+
<button class="small ghost" data-bp="open" title="Analysis, spec, tests, executables, downloads">Details</button></div>
|
|
286
|
+
${b.specError ? `<p class="error">${esc(b.specError)}</p>` : ''}
|
|
287
|
+
${b.missingEnv.length ? `<p class="hint warn">Needs in the environment before it runs: ${esc(b.missingEnv.join(', '))}</p>` : ''}
|
|
288
|
+
${b.description ? `<div class="hint">${esc(b.description)}</div>` : ''}
|
|
289
|
+
${failed.length && !stale ? `<div class="hint">Failing: ${failed.map((t) => esc(t.input.slice(0, 60))).join(' · ')} <button class="small" data-bp="fix">Ask this session to fix them</button></div>` : ''}
|
|
290
|
+
</div>`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
document.addEventListener('click', async (e) => {
|
|
294
|
+
const el = e.target.closest('[data-bp]');
|
|
295
|
+
if (!el) return;
|
|
296
|
+
const name = el.closest('[data-bp-name]').dataset.bpName;
|
|
297
|
+
try {
|
|
298
|
+
if (el.dataset.bp === 'open') return show(await api('GET', `/builder/${name}`));
|
|
299
|
+
if (el.dataset.bp === 'fix') {
|
|
300
|
+
await api('POST', `/builder/${name}/change`, { text: failuresText(cache.get(name)) });
|
|
301
|
+
return toast('Sent to this session');
|
|
302
|
+
}
|
|
303
|
+
el.disabled = true;
|
|
304
|
+
cache.set(name, await api('POST', `/builder/${name}/${el.dataset.bp}`));
|
|
305
|
+
onPanel();
|
|
306
|
+
// Tests and starting finish in the background; follow them.
|
|
307
|
+
const follow = setInterval(async () => {
|
|
308
|
+
await fetchOne(name);
|
|
309
|
+
const b = cache.get(name);
|
|
310
|
+
if (!b || b.missing || (!b.pending && (el.dataset.bp !== 'start' || b.running?.url || !b.running))) clearInterval(follow), load();
|
|
311
|
+
}, 2000);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
toast(`Error: ${err.message}`, 6000);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Its session changed (it edits the files as it goes): fetch the agent again, a moment later.
|
|
318
|
+
const timers = new Map();
|
|
319
|
+
function sessionChanged(a) {
|
|
320
|
+
const name = a.builds?.name;
|
|
321
|
+
if (!name) return;
|
|
322
|
+
clearTimeout(timers.get(name));
|
|
323
|
+
timers.set(name, setTimeout(() => {
|
|
324
|
+
fetchOne(name);
|
|
325
|
+
if (open?.name === name && dlg.open) refresh();
|
|
326
|
+
if (isOpen()) load();
|
|
327
|
+
}, 1000));
|
|
328
|
+
}
|
|
329
|
+
|
|
205
330
|
load();
|
|
206
|
-
return { load, render };
|
|
331
|
+
return { load, render, panel, sessionChanged, openAgent: async (name) => show(await api('GET', `/builder/${name}`)) };
|
|
207
332
|
}
|
package/public/index.html
CHANGED
|
@@ -382,14 +382,15 @@
|
|
|
382
382
|
<dialog id="build-dialog">
|
|
383
383
|
<form id="build-form" method="dialog">
|
|
384
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.
|
|
385
|
+
<p class="hint">Describe the task or workflow it takes over: what comes in, what it should do, what it must never do. An agent session works it out with you in the agent's own folder (analysis, spec, instructions, test cases); you keep shaping it there, then test, run and package it.</p>
|
|
386
386
|
<label>Task or workflow
|
|
387
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
388
|
</label>
|
|
389
389
|
<div id="build-attach"></div>
|
|
390
|
-
<label>Engine <select name="engine"><option value="claude">Claude Code</option></select></label>
|
|
390
|
+
<div class="row"><label>Engine it runs on <select name="engine"><option value="claude">Claude Code</option></select></label>
|
|
391
|
+
<label>Name <input name="name" placeholder="from the task" pattern="[a-z][a-z0-9-]{1,40}" title="lowercase letters, digits and dashes"></label></div>
|
|
391
392
|
<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">
|
|
393
|
+
<div class="dialog-actions"><button type="button" id="build-cancel" class="ghost">Cancel</button><button type="submit" class="primary">Start building</button></div>
|
|
393
394
|
</form>
|
|
394
395
|
</dialog>
|
|
395
396
|
|
|
@@ -744,6 +745,11 @@
|
|
|
744
745
|
<label class="check">Keep at most <input type="number" name="maxRunning" min="1" max="500" placeholder="no limit" class="num" /> agents open at once</label>
|
|
745
746
|
<p class="hint">Above it, the agents idle longest are closed (they resume when you open them), so the machine stays responsive. Never closed: agents working, waiting for your answer, active in the last 5 minutes, marked Keep, or in a team that is still going. <span id="settings-running"></span></p>
|
|
746
747
|
</fieldset>
|
|
748
|
+
<fieldset id="settings-rules">
|
|
749
|
+
<legend>Approval rules</legend>
|
|
750
|
+
<p class="hint">Commands you chose to always allow in a repo. Agents there run them without asking. After you allow the same command 3 times, the inbox offers a rule.</p>
|
|
751
|
+
<ul id="settings-rules-list" class="rules-list"></ul>
|
|
752
|
+
</fieldset>
|
|
747
753
|
<fieldset class="local-only" id="settings-service">
|
|
748
754
|
<legend>Start at login</legend>
|
|
749
755
|
<label class="check"><input type="checkbox" name="startAtLogin" /> Start AI Agent Manager when I log in</label>
|
package/public/menu.js
CHANGED
|
@@ -92,6 +92,7 @@ export function initMenu({ api, esc, toast, $ }) {
|
|
|
92
92
|
form.maxRunning.value = c.maxRunning ?? '';
|
|
93
93
|
$('#settings-running').textContent = `Open now: ${c.running}.`;
|
|
94
94
|
service = null;
|
|
95
|
+
showRules(await api('GET', '/rules').catch(() => null));
|
|
95
96
|
const u = await api('GET', '/update').catch(() => null);
|
|
96
97
|
form.updateCheck.checked = u?.check !== false;
|
|
97
98
|
if (!remote()) {
|
|
@@ -115,6 +116,23 @@ export function initMenu({ api, esc, toast, $ }) {
|
|
|
115
116
|
toast(`Error: ${err.message}`, 5000);
|
|
116
117
|
}
|
|
117
118
|
};
|
|
119
|
+
/** Allowed rules, newest first, each with what it has saved and a Remove button. */
|
|
120
|
+
const showRules = (r) => {
|
|
121
|
+
const list = [...(r?.allowed ?? [])].sort((a, b) => String(b.at).localeCompare(String(a.at)));
|
|
122
|
+
$('#settings-rules-list').innerHTML = list.length
|
|
123
|
+
? list.map((x) => `<li><code>${esc(x.label)}</code> <span class="hint">in ${esc(String(x.repo).replace(/^dir:/, '').split('/').filter(Boolean).slice(-2).join('/'))}${x.uses ? ` · answered ${x.uses} prompt${x.uses === 1 ? '' : 's'}` : ''}</span> <button type="button" class="small ghost" data-rule-remove="${esc(x.rule)}" data-repo="${esc(x.repo)}">Remove</button></li>`).join('')
|
|
124
|
+
: '<li class="hint">None yet.</li>';
|
|
125
|
+
};
|
|
126
|
+
$('#settings-rules-list').addEventListener('click', async (e) => {
|
|
127
|
+
const b = e.target.closest('[data-rule-remove]');
|
|
128
|
+
if (!b) return;
|
|
129
|
+
try {
|
|
130
|
+
showRules(await api('POST', '/rules/remove', { repo: b.dataset.repo, rule: b.dataset.ruleRemove }));
|
|
131
|
+
toast('Rule removed; agents there will ask again');
|
|
132
|
+
} catch (err) {
|
|
133
|
+
toast(`Error: ${err.message}`, 5000);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
118
136
|
$('#settings-cancel').onclick = () => dialog.close();
|
|
119
137
|
dialog.addEventListener('click', (e) => {
|
|
120
138
|
const go = e.target.closest('[data-open]');
|