@staix/agent-hub 0.6.4 → 0.7.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 +7 -0
- package/README.md +16 -15
- package/docs/operations.md +217 -0
- package/docs/quickstart.md +121 -0
- package/docs/security.md +57 -0
- package/docs/smoke.md +525 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +675 -0
- package/docs/specs/2026-09-19-multi-project-design.md +95 -0
- package/docs/specs/2026-09-20-durable-delivery-design.md +67 -0
- package/docs/specs/2026-09-20-pi-local-models-design.md +40 -0
- package/docs/specs/2026-09-20-upgrade-recovery-design.md +151 -0
- package/package.json +2 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +31 -10
- package/src/adapters/acp.ts +40 -7
- package/src/adapters/claude-channel.ts +22 -5
- package/src/adapters/codex-appserver.ts +50 -7
- package/src/adapters/local-worker.ts +20 -3
- package/src/adapters/pi.ts +33 -7
- package/src/cli/main.ts +49 -0
- package/src/cli/status-lines.ts +4 -1
- package/src/cli/terminal-recovery.ts +41 -1
- package/src/cli/upgrade-runtime.ts +8 -8
- package/src/hub/bus.ts +290 -38
- package/src/hub/control-client.ts +5 -3
- package/src/hub/daemon.ts +75 -19
- package/src/hub/delivery-journal.ts +830 -0
- package/src/hub/manager.ts +6 -4
- package/src/hub/peers.ts +16 -3
- package/src/hub/restart.ts +2 -0
- package/src/ui/index.html +7 -3
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
# agent-hub design spec
|
|
2
|
+
|
|
3
|
+
Date: 2026-09-19. Status: M1 to M5 merged; M6 implemented on `feat/m6-packaging-inference` (phase spec: issue #2 of the public repository). Version 0.1.0.
|
|
4
|
+
Owner: Young Joon Lee. Repo: STAIxBWLB/agent-hub.
|
|
5
|
+
|
|
6
|
+
Facts below are tagged **verified** (measured on 2026-09-19 on the owner's Mac) or
|
|
7
|
+
**inferred** (from docs or schema, not yet exercised).
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
- Goal: three coding agents on one machine (Claude Code, Codex, Kimi Code) plus a
|
|
12
|
+
hub-owned local-LLM worker collaborate efficiently as peers in one project: real-time
|
|
13
|
+
push messaging with busy-state coordination, role-based task split with cross review,
|
|
14
|
+
subscription-quota relay, and task-aware model selection that can send bulk or
|
|
15
|
+
low-stakes work to self-hosted models.
|
|
16
|
+
- Prior art: raysonmeng/agent-bridge v0.1.31 (Bun/TS, MIT). Verified on 2026-09-19: its
|
|
17
|
+
v1 pair is two-party by construction (single Claude seat, `source: "claude" | "codex"`),
|
|
18
|
+
its v3 rooms carry signals only, and a third agent can only join through the room
|
|
19
|
+
broker. A room MCP adapter for Kimi was built and verified end to end
|
|
20
|
+
(kept as a private memo). Decision: do not fork; reuse its
|
|
21
|
+
protocol ideas (agentMessage-only forwarding, marker tiers, busy guard, turn watchdog).
|
|
22
|
+
- Native control surfaces, all verified on 2026-09-19:
|
|
23
|
+
- Claude Code 2.1.277: channels. MCP server declares
|
|
24
|
+
`capabilities.experimental['claude/channel'] = {}`, pushes
|
|
25
|
+
`notifications/claude/channel` with `{content, meta}`; events queue while Claude is
|
|
26
|
+
busy and are delivered as a group on the next turn. Loaded with
|
|
27
|
+
`--dangerously-load-development-channels plugin:<name>@<marketplace>` (hidden flag,
|
|
28
|
+
accepted by 2.1.277). Reply is an ordinary MCP tool.
|
|
29
|
+
- Codex 0.154.0: `codex app-server --listen ws://127.0.0.1:<port>` spawns and answers
|
|
30
|
+
(verified through agent-bridge's daemon). Schema v2 has `turn/start`,
|
|
31
|
+
`turn/steer` (`threadId, expectedTurnId, input`), `turn/interrupt`,
|
|
32
|
+
`thread/start` with `developerInstructions` and `dynamicTools`,
|
|
33
|
+
`account/rateLimits/read`. TUI attaches with `--enable tui_app_server --remote <ws>`.
|
|
34
|
+
- Kimi Code 0.43.1: `kimi acp` JSON-RPC over stdio. `initialize`, `session/new`
|
|
35
|
+
(returns `configOptions` including a `model` select), `session/prompt` round trip
|
|
36
|
+
(`stopReason: end_turn`), `session/update` stream (`agent_message_chunk`,
|
|
37
|
+
`agent_thought_chunk`, `usage_update`), `session/cancel` (`stopReason: cancelled`),
|
|
38
|
+
`session/load`. A second `session/prompt` during a turn fails with
|
|
39
|
+
`turn.agent_busy`. Hooks exist (Stop, SessionStart, UserPromptSubmit, PreToolUse).
|
|
40
|
+
- Local serving (verified): OmniRoute 3.8.50 on a campus DGX H100 node, reachable on the
|
|
41
|
+
internal network (VPN), `/v1/models` answers 401 without a key, Anthropic and Responses surfaces exist.
|
|
42
|
+
vLLM serves `deepseek-ai/DeepSeek-V4-Flash-0731`; GLM-5.3-Flash and
|
|
43
|
+
Qwen3.8-27B are planned (internal deployment plan).
|
|
44
|
+
OpenCode 1.18.31 already has a `dgx-dsv4f` provider for this gateway.
|
|
45
|
+
- Routing policy engine: NVIDIA-NeMo/Switchyard v0.2.0 (Apache-2.0). Route types
|
|
46
|
+
`llm_classifier`, `stage_router`, `escalation_router`; `auto` needs a source build
|
|
47
|
+
until v0.3.0. Standalone `switchyard-server` proxies `/v1/chat/completions`,
|
|
48
|
+
`/v1/messages`, `/v1/responses` and forwards to any `base_url` upstream. Stability:
|
|
49
|
+
libsy Beta, server Demo ("not for production"), pre-1.0.
|
|
50
|
+
- Shared memory (verified 2026-09-19): claude-mem 13.25.1 keeps one store
|
|
51
|
+
(`~/.claude-mem/claude-mem.db`, worker `127.0.0.1:37701`, loopback, no auth) already
|
|
52
|
+
written by three platforms (`sdk_sessions.platform_source`: claude 1211, kimi 55,
|
|
53
|
+
codex 41). Observations carry `project` (git-root basename), `agent_type`, `agent_id`,
|
|
54
|
+
`metadata`. Kimi is captured by the `dot ai memory` transcript bridge (LaunchAgent
|
|
55
|
+
watching `~/.kimi-code/sessions/**/wire.jsonl`); an ACP-driven `kimi acp` session was
|
|
56
|
+
captured the same way. Worker HTTP API: `POST /api/sessions/init
|
|
57
|
+
{contentSessionId, project, prompt, platformSource}`, `POST /api/sessions/observations
|
|
58
|
+
{contentSessionId, platformSource, tool_name, tool_input, tool_response, cwd, agentId,
|
|
59
|
+
agentType, tool_use_id}`, `POST /api/sessions/summarize`, `POST /api/sessions/session-end`,
|
|
60
|
+
`GET /api/context/inject?projects=<chain>[&platformSource=]`, `POST /api/memory/save
|
|
61
|
+
{text, title, project, metadata}`, `GET /api/search`, `GET /api/timeline`, `/api/corpus/*`.
|
|
62
|
+
Each agent's SessionStart hook injects with its own `platformSource`, so today storage
|
|
63
|
+
is shared but recall is siloed per platform (work project: 6.9 KB all platforms vs
|
|
64
|
+
2.7 KB kimi-only). The Codex claude-mem plugin was reinstalled on 2026-09-19
|
|
65
|
+
(`codex plugin add claude-mem@claude-mem-local`).
|
|
66
|
+
- Decisions inherited from the owner's infrastructure plan:
|
|
67
|
+
no OmniRoute daemon on the Mac; subscription OAuths (Claude Max, ChatGPT, Kimi plan)
|
|
68
|
+
are never pooled through a gateway; local models are for bulk and low-stakes work;
|
|
69
|
+
PII never leaves the campus network (internal routing criteria).
|
|
70
|
+
|
|
71
|
+
## Decisions
|
|
72
|
+
|
|
73
|
+
1. Greenfield hub, not a fork. Bun + TypeScript, one repo, MIT.
|
|
74
|
+
2. Peers are equal. One `PeerAdapter` interface; three adapter kinds (channel,
|
|
75
|
+
app-server, ACP) and one hub-native worker. Four default peers: `claude`, `codex`,
|
|
76
|
+
`kimi`, `local`.
|
|
77
|
+
3. Kimi runs headless under ACP; the user talks to it through the hub console
|
|
78
|
+
(`ahub say @kimi ...`, `ahub tail`). No Kimi TUI in the loop.
|
|
79
|
+
4. Local peer is a hub-native agent loop (model call + tools), not OpenCode. Reason:
|
|
80
|
+
per-call model selection needs the hub to own the call. OpenCode ACP is kept as an
|
|
81
|
+
optional fallback runtime behind the same adapter interface.
|
|
82
|
+
5. Model selection has three layers: L1 hub policy (which peer, which route),
|
|
83
|
+
L2 Switchyard (which model per call for hub-owned traffic), L3 OmniRoute (keys,
|
|
84
|
+
provider fallback, usage). Switchyard runs as a hub-managed sidecar on the Mac for the
|
|
85
|
+
duration of a hub session only; it is not resident. The hub must work with L2 absent
|
|
86
|
+
(fixed model ids straight to OmniRoute).
|
|
87
|
+
6. All hub-originated model traffic (local worker, internal summaries, triage) goes
|
|
88
|
+
Switchyard -> OmniRoute. Subscription CLIs keep their own auth.
|
|
89
|
+
7. Quota relay hands open tasks to the `local` peer first, then waits for the window
|
|
90
|
+
reset for judgment-heavy classes. Task-level escalation on verification failure.
|
|
91
|
+
8. Safety defaults: cross-peer text is framed as untrusted; permission prompts stay on
|
|
92
|
+
unless `--unattended`; ports bind loopback only; Kimi ACP permission requests are
|
|
93
|
+
relayed to the hub console. Loopback is not enough on its own (any web page can open a
|
|
94
|
+
WebSocket to 127.0.0.1), so the control WS requires a per-run token and both hub
|
|
95
|
+
servers refuse requests that carry an `Origin` header (amended in M1).
|
|
96
|
+
9. Single machine, one hub daemon per project directory. Cross-machine broker is out.
|
|
97
|
+
10. Shared memory reuses claude-mem as the single store; the hub never runs its own
|
|
98
|
+
memory database. Native capture stays where it exists (Claude hooks, Codex plugin,
|
|
99
|
+
Kimi transcript bridge); the hub captures only the local worker and adds
|
|
100
|
+
cross-platform recall, task-scoped briefs and explicit shared notes. The bus still
|
|
101
|
+
passes messages, never transcripts.
|
|
102
|
+
|
|
103
|
+
## Design
|
|
104
|
+
|
|
105
|
+
### Components
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
Claude Code TUI ── MCP stdio ── plugins/agent-hub (channel server) ──┐
|
|
109
|
+
Codex TUI ──── --remote ws://127.0.0.1:<proxy> ── app-server proxy ──┤ ahub daemon (Bun)
|
|
110
|
+
kimi acp (child, stdio) ── ACP client adapter ────────────────────────┤ bus · board · budget
|
|
111
|
+
local worker (in-process agent loop) ─────────────────────────────────┤ router L1
|
|
112
|
+
hub CLI / console ── control WS 127.0.0.1:<ctl> ──────────────────────┘
|
|
113
|
+
│ model calls
|
|
114
|
+
switchyard-server sidecar (127.0.0.1, session-scoped) L2
|
|
115
|
+
│ base_url
|
|
116
|
+
OmniRoute: internal URL (VPN) / Access-protected public URL L3
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- `ahub daemon`: owns the message bus, peer registry and state machines, task board,
|
|
120
|
+
budget coordinator, router L1, and the Codex proxy, ACP child and local worker. Survives
|
|
121
|
+
Claude Code restarts. State in `.agenthub/state/` (pid, status.json, sqlite, logs).
|
|
122
|
+
- `plugins/agent-hub`: Claude Code plugin. Its MCP server is the channel; it reconnects to
|
|
123
|
+
the daemon over the control WS with backoff, except after a close no retry can fix: 4401,
|
|
124
|
+
4403, 4409 and 4426. It then stays detached and its tools report why. Close 4000 (another
|
|
125
|
+
session attached as the same peer; the newest hello wins, whatever order the session-start
|
|
126
|
+
recalls finish in) is not one of them (amended, issue #30): that session stands by, and
|
|
127
|
+
reconnects only once `status.json` reports the peer offline and not `claiming`, so it never
|
|
128
|
+
evicts the session holding the id - nor one whose hello is still finishing its preface - and
|
|
129
|
+
the two cannot trade it back and forth. The daemon writes the status file at the claim, before
|
|
130
|
+
the preface, so the arriving session is visible for that window. Its tools say it is standing by
|
|
131
|
+
until it has the peer again. It exits when its host closes stdin. Exposes tools `hub_send`,
|
|
132
|
+
`hub_inbox` (fallback drain), `hub_task_*`, `hub_review`, `hub_remember`, `hub_checkpoint`.
|
|
133
|
+
- Codex adapter: spawns `codex app-server --listen ws://127.0.0.1:<port>`, runs a
|
|
134
|
+
transparent proxy the TUI attaches to (`--enable tui_app_server --remote`). The proxy never
|
|
135
|
+
runs its own handshake: it learns the thread id from the TUI's `thread/start` /
|
|
136
|
+
`thread/resume` responses and sends hub requests with negative JSON-RPC ids whose
|
|
137
|
+
responses are swallowed. It shares one message per turn, the last `agentMessage` whose
|
|
138
|
+
`phase` is not `commentary` (0.154.0 items carry `text` and `phase`, verified), emitted on
|
|
139
|
+
`turn/completed`; injects via `turn/start` when idle and `turn/steer`
|
|
140
|
+
for `important` messages while busy, registers hub tools through `dynamicTools` on
|
|
141
|
+
`thread/start` and answers `item/tool/call`. Per-turn inactivity watchdog.
|
|
142
|
+
- Kimi adapter (ACP client): spawns `kimi acp`, `initialize`, `session/new` (or
|
|
143
|
+
`session/load` for resume), injects via `session/prompt`, collects
|
|
144
|
+
`agent_message_chunk` into one outbound message per turn, tracks busy from the
|
|
145
|
+
in-flight prompt, queues on `turn.agent_busy`. Relays `session/request_permission`
|
|
146
|
+
to the console (`ahub tail` shows the request, `ahub permit <id> <option>` answers, 120 s of
|
|
147
|
+
silence cancels; `ahub up --unattended` auto-selects the agent's `allow_once` option). The
|
|
148
|
+
prompt names what will run, not only the tool (amended, issue #31): the request's `rawInput`
|
|
149
|
+
when it has one, otherwise what the `tool_call` update announced for the same `toolCallId`.
|
|
150
|
+
A payload that cannot be resolved either way is titled as unresolved and its `allow_always`
|
|
151
|
+
option is withheld, so a blind click cannot grant every later call of the session. The
|
|
152
|
+
watchdog sends `session/cancel` before forcing idle. `--model` maps to `kimi --model <alias> acp`
|
|
153
|
+
(verified flag). Reusable for `opencode acp`.
|
|
154
|
+
- Local worker (amended in M3): a hub-native tool-calling loop over non-streaming
|
|
155
|
+
`POST /chat/completions` with plain `fetch`, no SDK (the repo keeps zero runtime
|
|
156
|
+
dependencies). Tools `read`, `write`, `edit`, `bash`, `git`, `hub_send` (`hub_task_*` with
|
|
157
|
+
M4). Paths are realpath-scoped to cwd with a secrets denylist; `.git` and `.agenthub` are
|
|
158
|
+
not writable. `write`, `edit`, `bash` and mutating `git` go through the console permission
|
|
159
|
+
relay unless the hub runs `--unattended`. Everything that executes runs under seatbelt
|
|
160
|
+
(`sandbox-exec`), attended or not: writes only under cwd, its real git dir (submodule,
|
|
161
|
+
worktree) and temp; the home directory is unreadable except the project, toolchain dirs and
|
|
162
|
+
`local.read_allow` (whatever a command reads can reach cloud-hosted peers through the
|
|
163
|
+
answer); no reads of credential stores or denylisted files, no `.git/hooks` or `.git/config` writes, no network, loopback included, unless
|
|
164
|
+
`local.bash_network`, scrubbed environment; without seatbelt there is no `bash`. 30 steps per
|
|
165
|
+
turn, `reasoning_content` is never stored or shared. Model comes from `routing.toml`:
|
|
166
|
+
a Switchyard route id, or `fixed_model` straight to OmniRoute.
|
|
167
|
+
- Console: `ahub tail` (live stream), `ahub say [@peer] <text>`, `ahub permit`, `ahub doctor`, `ahub board`,
|
|
168
|
+
`ahub route explain <task>`, `ahub budget`, `ahub status`, `ahub logs`, `ahub kill`.
|
|
169
|
+
- Launchers: `ahub up`, `ahub claude [--safe|--unattended] [--via dgx]`,
|
|
170
|
+
`ahub codex [--new] [--profile dgx]`, `ahub kimi [--model <alias>]`, `ahub local`.
|
|
171
|
+
Launchers inject only the flags the hub owns and refuse user-supplied duplicates.
|
|
172
|
+
M1 accepts `--safe` and `--new` as explicit spellings of the default; `--via` and
|
|
173
|
+
`--profile dgx` arrive with M3. Peers are registered lazily, on first attach, so a peer
|
|
174
|
+
that is never launched accumulates no queue.
|
|
175
|
+
|
|
176
|
+
### Message model
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
interface Envelope {
|
|
180
|
+
id: string; trace: string; hop: number;
|
|
181
|
+
from: PeerId; to?: PeerId[]; // absent = broadcast
|
|
182
|
+
kind: "chat" | "task" | "review" | "status" | "budget" | "presence";
|
|
183
|
+
priority: "important" | "status" | "fyi";
|
|
184
|
+
body: string; // agent conclusions only, never tool noise
|
|
185
|
+
refs?: { repo?: string; branch?: string; commit?: string; paths?: string[]; task?: string };
|
|
186
|
+
ts: number;
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- Never delivered back to `from`; `hop` capped at 3. A message produced by a turn the hub
|
|
191
|
+
injected inherits that envelope's `trace` with `hop + 1` (Claude passes `reply_to` on
|
|
192
|
+
`hub_send`); a turn the user started begins a fresh trace at hop 0. Envelopes over the
|
|
193
|
+
cap are dropped for peers but still shown on the console.
|
|
194
|
+
- A reply is addressed, not broadcast (amended after the 0.6.1 measurement run, issue #29).
|
|
195
|
+
An adapter answering a delivery addresses `to` at the senders it answers (`replyAudience`);
|
|
196
|
+
a reply that names no target inherits the sender of the envelope it answers. `user` and `hub`
|
|
197
|
+
are not peers, so an answer to the console or to a hub workflow envelope reaches the console
|
|
198
|
+
and the log and costs no peer a turn - stakeholders already hear about a task through the
|
|
199
|
+
hub's own `task` and `review` envelopes. A peer that wants every peer to hear it leaves `to`
|
|
200
|
+
empty on purpose. Before this, one directed question cost every attached peer a turn, and a
|
|
201
|
+
Pi task report woke the local worker into unsolicited work. A delivery that was condensed is
|
|
202
|
+
answered by what it stands for, not by what the peer was handed: the bus maps the reserved
|
|
203
|
+
`digest` sender back to the senders of the originals it kept next to `out`.
|
|
204
|
+
- One queue rule per peer (amended in M2): the whole queue goes out as one delivery, a single
|
|
205
|
+
envelope or a digest of up to 10, when the peer is idle and the queue holds an `important`
|
|
206
|
+
envelope, or `batch_max` (3) envelopes, or its oldest one has waited `batch_ms` (15 s).
|
|
207
|
+
`important` to a busy Codex is steered instead. `fyi` is never delivered to a peer: console
|
|
208
|
+
and log now, the task board from M4. A digest is one prompt (Codex, Kimi) or one channel
|
|
209
|
+
notification (`meta.source = hub-digest`, senders in `meta.sources`); its reply answers the
|
|
210
|
+
highest-hop item, so neither a digest nor a steer can reset the hop cap. Important items lead
|
|
211
|
+
the digest; an envelope that failed before is retried alone. The control WS hello carries a
|
|
212
|
+
wire version (2 since digests): a plugin bundle older than the daemon is refused with close
|
|
213
|
+
code 4426 rather than dropping digests silently.
|
|
214
|
+
- A hub-native peer (Pi, the local worker) may not rate its own urgency: `important` it claims
|
|
215
|
+
for itself is capped to `status` unless the delivery it answers held an `important` envelope
|
|
216
|
+
addressed to it. The whole delivery is checked, not `replyParent` alone - an important request
|
|
217
|
+
can share a hop with a later status item. A message that answers nothing is always `status`.
|
|
218
|
+
Peers driven by their own session keep the priority they claim.
|
|
219
|
+
- Markers `[IMPORTANT]`, `[STATUS]`, `[FYI]` at the start of agent text, `hub_send` or `ahub say`
|
|
220
|
+
set the priority and are stripped. Default `status` for agents, `important` for the console
|
|
221
|
+
user (a human typing `ahub say` should not wait out the batch window).
|
|
222
|
+
- Every inbound cross-peer body is wrapped as untrusted (Claude: `<channel>` tag with
|
|
223
|
+
`meta.source=<peer>`; Codex and Kimi: a fixed prefix line plus a standing instruction
|
|
224
|
+
injected once per session).
|
|
225
|
+
- The fixed prefix includes envelope `kind`; Claude uses `meta.kind` for a single
|
|
226
|
+
item and includes each item's kind in digest headers. Only `hub` / `presence`
|
|
227
|
+
items are reference-only recall. Hub `task`, `review`, and `budget` items are
|
|
228
|
+
workflow events handled through the board and the recipient's assigned role,
|
|
229
|
+
within existing user authorization. Sender and kind grant no higher authority.
|
|
230
|
+
Reply-parent selection skips only hub presence items, retaining workflow hops.
|
|
231
|
+
|
|
232
|
+
### Peer state and delivery
|
|
233
|
+
|
|
234
|
+
| State | Enter | Delivery rule |
|
|
235
|
+
| --- | --- | --- |
|
|
236
|
+
| idle | turn completed / prompt returned | inject now |
|
|
237
|
+
| busy | turn started / prompt in flight | Claude: push (Claude Code queues); Codex: steer if important else queue; Kimi/local: queue |
|
|
238
|
+
| paused | budget gate (M5) or user `ahub pause` / `ahub resume`; a bus-level flag over the adapter state | queue, never steer; reassign open tasks (M5) |
|
|
239
|
+
| offline | adapter disconnected | queue |
|
|
240
|
+
|
|
241
|
+
Every queue is bounded (`queue_cap`, default 200): on overflow the oldest non-`important`
|
|
242
|
+
envelope is dropped and reported on the console.
|
|
243
|
+
|
|
244
|
+
Inactivity watchdog per busy turn (default 300 s) forces `idle` after cancelling the silent
|
|
245
|
+
turn (`session/cancel` for ACP, `turn/interrupt` for Codex); a late result of the cancelled
|
|
246
|
+
turn is discarded. Queues are drained in order on `idle`. Delivery is at-least-once with
|
|
247
|
+
idempotency by `id`: a failed delivery returns to the queue head and is retried after 1 s, and
|
|
248
|
+
after 3 failures it is reported as undeliverable on the console so one envelope cannot block a
|
|
249
|
+
peer. Peer ids claimed over the control WS must not be `user` or a hub-managed adapter id.
|
|
250
|
+
|
|
251
|
+
### Task board and roles
|
|
252
|
+
|
|
253
|
+
- sqlite table `tasks` (`bun:sqlite`, `.agenthub/state/hub.db`, survives `ahub kill`): `id, title,
|
|
254
|
+
detail, class, owner, reviewer, state, refs, signals, rejections, history`.
|
|
255
|
+
- `class`: `plan | implement | bulk_edit | test | review | summarize | triage`.
|
|
256
|
+
- `state` (amended in M4): `proposed -> in_progress -> in_review -> approved | changes_requested`,
|
|
257
|
+
`changes_requested -> in_progress`. `accept` and `done` are transitions recorded in `history`,
|
|
258
|
+
not states: they carried no behaviour. A verdict is only accepted on a task in review.
|
|
259
|
+
- Roles in `.agenthub/config.json`; default `claude: [planner, reviewer]`,
|
|
260
|
+
`codex: [implementer]`, `kimi: [implementer, verifier]`, `local: [implementer, verifier]`.
|
|
261
|
+
- Flow: `hub_task_propose` creates a card and the hub assigns owner and reviewer (L1);
|
|
262
|
+
the owner accepts or declines (decline moves to the next candidate); `hub_task_done` sends a
|
|
263
|
+
`review` envelope with summary and refs to the reviewer, or approves directly when there
|
|
264
|
+
is none; `hub_review` returns the verdict. `changes_requested` reopens as `in_progress`; two
|
|
265
|
+
in a row escalate the task to the next attached peer in `escalate_to`, with its review
|
|
266
|
+
notes (`ahub task escalate` does it by hand). Only the owner, the reviewer or the console
|
|
267
|
+
user may act on a task.
|
|
268
|
+
- Tools on every peer (amended in M4): one implementation. The bundled MCP server of the
|
|
269
|
+
Claude plugin has a tools-only mode (`AGENTHUB_MODE=tools`, control WS role `tools`: acts for
|
|
270
|
+
a peer, never a delivery target). Kimi gets it through ACP `session/new` `mcpServers`
|
|
271
|
+
(verified live), Codex through `-c mcp_servers.agent-hub.*` on the app-server the hub
|
|
272
|
+
spawns (verified live with a real Codex turn: tools called without an approval prompt under
|
|
273
|
+
`approval_mode = "approve"`), `local`
|
|
274
|
+
natively. This replaces Codex `dynamicTools`: no rewriting of proxied TUI traffic.
|
|
275
|
+
- Role contract: Claude in the plugin `instructions`, Codex, Kimi and `local` with the
|
|
276
|
+
standing instruction of their first delivery, all of them in the `AGENT_HUB` marker
|
|
277
|
+
blocks; `roles` in `.agenthub/config.json` is the source. Codex `developerInstructions`
|
|
278
|
+
injection is dropped for the same reason as `dynamicTools`.
|
|
279
|
+
- PII (amended in M4): a task matching `signals.pii_patterns` is owned by `local` or by
|
|
280
|
+
nobody; its envelopes are `private` (console tail and `hub.log` print a stub), lists show
|
|
281
|
+
`[pii]` to everyone but `local`, `ahub task show` is the one place the console reads it; the
|
|
282
|
+
reviewer is the console user; `local` answers such a turn to the console only, keeps it out
|
|
283
|
+
of its history, refuses it when the only gateway is off campus (Cloudflare Access), and
|
|
284
|
+
nothing reaches claude-mem (no brief, no note, no capture), because claude-mem's observer is
|
|
285
|
+
a cloud model (verified: `/api/health` reports `ai.provider: claude`).
|
|
286
|
+
|
|
287
|
+
### Routing (three layers)
|
|
288
|
+
|
|
289
|
+
L1, hub policy, `.agenthub/routing.toml`, hot-reloaded:
|
|
290
|
+
|
|
291
|
+
```toml
|
|
292
|
+
[signals]
|
|
293
|
+
pii_patterns = ["\\b\\d{6}-\\d{7}\\b"] # PII => local_only; add your own
|
|
294
|
+
long_context_tokens = 120000
|
|
295
|
+
|
|
296
|
+
[classes.implement]
|
|
297
|
+
peers = ["codex", "kimi", "local"] # preference order among idle, unpaused peers
|
|
298
|
+
route = "sy/coding" # Switchyard route id for the local worker
|
|
299
|
+
escalate_to = ["kimi", "claude"] # on verification failure
|
|
300
|
+
|
|
301
|
+
[classes.bulk_edit]
|
|
302
|
+
peers = ["local", "kimi"]
|
|
303
|
+
route = "sy/fast"
|
|
304
|
+
|
|
305
|
+
[classes.review]
|
|
306
|
+
peers = ["claude", "codex"]
|
|
307
|
+
route = "sy/review"
|
|
308
|
+
local_allowed = false # never route judgment classes to local
|
|
309
|
+
|
|
310
|
+
[classes.summarize]
|
|
311
|
+
peers = ["local"]
|
|
312
|
+
route = "sy/fast"
|
|
313
|
+
|
|
314
|
+
[constraints]
|
|
315
|
+
pii = "local_only" # hard: on-prem models only
|
|
316
|
+
budget_paused = "skip_peer"
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
L2, Switchyard sidecar (amended in M3, verified against `switchyard-server` 0.2.0 built with
|
|
320
|
+
`cargo install --locked switchyard-server`; there is no prebuilt macOS binary).
|
|
321
|
+
`routing.toml` carries `[targets.*]` and `[routes."sy/..."]` in Switchyard's own table
|
|
322
|
+
shapes; the hub copies them into `.agenthub/state/switchyard.toml` (0600, removed on stop)
|
|
323
|
+
and adds `schema_version`, one `[llm_clients.gateway]` (`format = "openai_chat"`, the live
|
|
324
|
+
OmniRoute URL, `api_key_env = "OMNIROUTE_API_KEY"`, Access `extra_headers` only for an
|
|
325
|
+
Access host), `llm_client` on targets and the public `id` on routes. The key reaches the
|
|
326
|
+
sidecar through its environment only. Verified facts: the binary rejects `timeout_ms` on
|
|
327
|
+
`llm_clients`; there is no `escalation_router` type, escalation is `llm_classifier` with
|
|
328
|
+
`mode = "escalation"` and a mandatory `[routes.<name>.escalation]` table; the default bind is
|
|
329
|
+
`0.0.0.0`, so the hub always passes `--host 127.0.0.1`; a route is selected by sending its id
|
|
330
|
+
as `model`; the chosen target returns in `x-model-router-selected-model`; tool calls pass
|
|
331
|
+
through. The config goes through `--dry-run` first. The sidecar starts on the first
|
|
332
|
+
hub-owned model call and stops with the hub. A missing binary, a rejected config, failed
|
|
333
|
+
health, an exit or a failed call turns L2 off for the hub run with one log line and the
|
|
334
|
+
worker calls `fixed_model` on OmniRoute directly. Until a second model (GLM-5.3-Flash) is served the
|
|
335
|
+
shipped routes are `passthrough` to DeepSeek-V4-Flash; the `stage_router` and escalation
|
|
336
|
+
blocks ship commented out and validate against the real binary.
|
|
337
|
+
|
|
338
|
+
L3, OmniRoute (amended in M3): candidates from `omniroute.urls` (for the owner: the internal
|
|
339
|
+
URL over VPN, then a public URL behind Cloudflare Access; the tool ships with none)
|
|
340
|
+
are probed at the same time with `GET <base>/models`; the most preferred one that answers 2xx
|
|
341
|
+
within 4 s wins (a Cloudflare Access 403 is not healthy);
|
|
342
|
+
OmniRoute 3.8.50 has `/healthz` and `/api/health` but no `/health` (verified). Key from
|
|
343
|
+
`OMNIROUTE_API_KEY` or `omniroute.api_key_file`; the two Cloudflare Access headers, read from
|
|
344
|
+
files, go only to hosts in `omniroute.access_hosts`. `omniroute tokens create` issues CLI
|
|
345
|
+
access tokens, not inference credentials. Inference keys come from `omniroute api api-keys`
|
|
346
|
+
(admin context): the owner issued `agent-hub-local` on 2026-09-19 and `omniroute.api_key_file`
|
|
347
|
+
points at it, so the dashboard separates the hub's usage.
|
|
348
|
+
|
|
349
|
+
Assignment is a pure function of `routing.toml`, the task's signals and the peers' bus states
|
|
350
|
+
(idle before busy, paused, offline and detached skipped, `local_allowed`, `long_context =
|
|
351
|
+
"skip_local"`, `pii = "local_only"`); `ahub route explain <id | --class <c> <title>>` runs the
|
|
352
|
+
same function and prints its trace: signals, every candidate with the reason it was kept
|
|
353
|
+
or skipped, owner, reviewer and route. `local` uses the class's `route` / `fixed_model` for a
|
|
354
|
+
task turn and `[local]` otherwise. Verification failure (tests fail, review `changes_requested` twice) escalates
|
|
355
|
+
the task to the next peer in `escalate_to`; Switchyard handles per-call escalation
|
|
356
|
+
inside a peer.
|
|
357
|
+
|
|
358
|
+
### Budget relay
|
|
359
|
+
|
|
360
|
+
- Sources (amended in M5). Codex: `account/rateLimits/read` sent through the TUI's
|
|
361
|
+
connection with a hub id, `account/rateLimits/updated` forwarded from the proxy, and a
|
|
362
|
+
turn refused with `usageLimitExceeded` as a hard limit (verified live: the owner's account
|
|
363
|
+
answered `primary {usedPercent: 100, windowDurationMins: 10080, resetsAt}`). Claude: no
|
|
364
|
+
OAuth probe. Claude Code passes `rate_limits.five_hour` / `seven_day` (`used_percentage`,
|
|
365
|
+
`resets_at`) to the status line command (verified in the input this Mac's HUD script
|
|
366
|
+
reads); `ahub claude` puts a tee in front of the user's status line command through
|
|
367
|
+
`--settings` for that session, records the limits in `.agenthub/state/claude-usage.json`
|
|
368
|
+
and runs the original command unchanged. `~/.claude/settings.json` is never edited; a
|
|
369
|
+
user-supplied `--settings` wins and turns the tee off. The `--settings` injection is
|
|
370
|
+
verified live (2026-09-19: `claude-usage.json` appears within seconds, the wrapped HUD
|
|
371
|
+
renders unchanged, `ahub budget` shows both windows). Kimi: `usage_update` tokens over a
|
|
372
|
+
rolling 5 h against `budget.kimi_tokens_5h` (off by default). Verified live on kimi
|
|
373
|
+
2.0.1: one update per turn, payload `{"sessionUpdate":"usage_update","used":<tokens>,
|
|
374
|
+
"size":<context window>}`; `used` is the session's context occupancy against `size`
|
|
375
|
+
(1M), not billed quota; the parser matches it through the `used` fallback and it grows
|
|
376
|
+
monotonically within a session (a compaction reads as a new session). `ahub budget set`
|
|
377
|
+
feeds a reading by hand. `local` has no quota and is never paused.
|
|
378
|
+
- Gate at `budget.gate` (default 0.9) on any fresh window; readings older than
|
|
379
|
+
`budget.stale_min` are ignored. Checkpoint first, pause second: the peer gets one
|
|
380
|
+
important envelope asking it to write `.agenthub/checkpoint.md` and call `hub_checkpoint`,
|
|
381
|
+
the hub waits up to `budget.checkpoint_timeout_s`, then pauses it; a hard-limited peer is
|
|
382
|
+
paused at once. Its open tasks are reassigned, `local` first and then the class list,
|
|
383
|
+
through the M4 constraints, with the checkpoint summary (or the peer's platform block
|
|
384
|
+
from claude-mem) and the task brief in the envelope; tasks it was reviewing get another
|
|
385
|
+
reviewer; PII tasks never get handoff text. One open record per peer in `hub.db`:
|
|
386
|
+
repeated readings do nothing, a restart keeps the pause and finishes an interrupted
|
|
387
|
+
handoff. Resume at `resetsAt` plus a minute, or on a fresh reading under `gate - 0.1`,
|
|
388
|
+
with one important envelope from the hub listing what moved; this replaces the per-peer
|
|
389
|
+
resume calls and the `hub_ack_resume` tool; the notice is queued before the peer is
|
|
390
|
+
released, so it leads the first delivery. Moved tasks stay with their new owners. A manual
|
|
391
|
+
`ahub pause` is never lifted by the coordinator; `ahub resume` does not override a budget
|
|
392
|
+
pause, `ahub budget resume <peer>` does, and the coordinator then leaves that peer alone
|
|
393
|
+
until the window that paused it has reset. A handoff waits until another peer is attached
|
|
394
|
+
(right after a restart nobody is), readings keep their own timestamp, and a window whose
|
|
395
|
+
reset time has passed no longer counts. A peer whose window reset while the hub was down is
|
|
396
|
+
not paused again, and still gets the resume envelope once it attaches. With no status line
|
|
397
|
+
of the user's own to wrap, the tee prints a short usage line instead of a blank one.
|
|
398
|
+
|
|
399
|
+
### Shared memory (claude-mem)
|
|
400
|
+
|
|
401
|
+
- Capture. Claude: native plugin hooks. Codex: claude-mem Codex plugin. Kimi: `dot ai
|
|
402
|
+
memory` transcript bridge (covers ACP sessions, verified). Local worker: the hub acts
|
|
403
|
+
as the hook client: `sessions/init` on worker start (`platformSource: "agent-hub"`,
|
|
404
|
+
`agentId: "local"`, `project` = git-root basename of the hub cwd), one
|
|
405
|
+
`sessions/observations` per tool call (mirrors `CLAUDE_MEM_SKIP_TOOLS`; a call that names
|
|
406
|
+
a denylisted path is not posted at all), `sessions/summarize` at the end of a turn that used
|
|
407
|
+
tools, `session-end` on stop (awaited, the hub exits right after). Verified live: a
|
|
408
|
+
`summarize` that carries `agentId` is answered `skipped: subagent_context`, so only
|
|
409
|
+
observations carry `agentId = local`. Fail-open: a down worker never blocks a turn; events
|
|
410
|
+
are dropped with one log line.
|
|
411
|
+
- Session-start cross recall (amended in M2). Before a peer can receive anything the hub
|
|
412
|
+
fetches `context/inject?projects=<chain>` (chain = git superprojects down to this repo,
|
|
413
|
+
comma-separated, primary last; verified) and trims it to `memory.inject_tokens` (default
|
|
414
|
+
2000, counted as 3 characters per token, cut at a line boundary). Kimi and the local worker
|
|
415
|
+
get all platforms in one call. Claude and Codex already get their own platform from their
|
|
416
|
+
own hooks, so they get one filtered call per other platform, legend stripped, the token
|
|
417
|
+
budget split evenly between the platforms that returned something. The block is
|
|
418
|
+
not a message of its own, which would cost a turn just to be acknowledged: it rides as the
|
|
419
|
+
first item (`from: hub`) of the peer's first delivery. Once per peer per hub run, so a
|
|
420
|
+
restarted peer session does not get it again. A `# claude-mem status` page (unknown project,
|
|
421
|
+
empty filter; verified) or a down worker means no block.
|
|
422
|
+
- Task brief on handoff. When a task is assigned, escalated or reassigned by the budget
|
|
423
|
+
relay, the hub runs `search(query = title + refs.paths, project, limit 10)` and
|
|
424
|
+
`timeline(anchor = top hit)`, and attaches a brief of at most `memory.brief_items`
|
|
425
|
+
(default 8) lines `#id time type title` plus one facts line to the task envelope.
|
|
426
|
+
The receiver calls `get_observations([ids])` only for the ones it needs (layered
|
|
427
|
+
workflow). Per peer, the hub keeps a `seen_ids` set for the session so a brief never
|
|
428
|
+
repeats an observation already delivered to that peer.
|
|
429
|
+
- Explicit shared notes. Tool `hub_remember(text, title?, tags?)` on every adapter and
|
|
430
|
+
in the console (`ahub remember`) posts `memory/save` with
|
|
431
|
+
`metadata: {peer, task, kind: decision | finding | contract}` (payload verified live). The
|
|
432
|
+
hub auto-saves the transitions that carry content, `done`, the review verdict and
|
|
433
|
+
escalation (amended in M4: `proposed` and `accepted` would add two empty memories per
|
|
434
|
+
task), and checkpoint summaries (M5), so handoff history is recallable next session.
|
|
435
|
+
- Budget relay (amended in M5). The hub owns a claude-mem session id only for `local`, so
|
|
436
|
+
`sessions/summarize` cannot be called for Claude, Codex or Kimi. The handoff context is
|
|
437
|
+
the peer's own checkpoint summary, else its platform block from `context/inject`, and it
|
|
438
|
+
travels with the task brief in the task envelope; `.agenthub/checkpoint.md` remains as the
|
|
439
|
+
file the peer writes.
|
|
440
|
+
- Efficiency rules. Index first (`search`), then `timeline`, then details; injection
|
|
441
|
+
and briefs are token-capped; project chain filtering only; tool_input containing
|
|
442
|
+
denylisted paths is never posted; no transcript or full observation bodies cross the
|
|
443
|
+
bus.
|
|
444
|
+
- `ahub ask` (added after 0.1.0). Retrieval first, model second, read-only. Evidence is gathered from the task
|
|
445
|
+
board, claude-mem (index search plus the timeline around the top hit) and this run's hub log, capped at 6000
|
|
446
|
+
characters; the hub's model (`sy/fast`) answers from that list only and has to cite its ids, and an answer that
|
|
447
|
+
cites nothing from the list is dropped. No evidence means no model call; no model means the evidence is the result.
|
|
448
|
+
Every row is capped and rows that match the question lead, so one long row cannot empty the list. Every cited id
|
|
449
|
+
has to be in the list: one invented id drops the answer. PII task text is evidence only when an on-campus model is
|
|
450
|
+
positively confirmed (a gateway answered, it is not behind Access, and no sidecar points off campus); otherwise
|
|
451
|
+
the row stays as a `[pii]` stub so counts are right. A question that itself carries PII skips the memory worker.
|
|
452
|
+
Such results are marked and never saved. `--remember` saves a real answer as a model-written note attributed to
|
|
453
|
+
the hub, under a title later asks exclude from their evidence; "nothing found" is never saved. The call is
|
|
454
|
+
interactive: its own 45 s clock, outside the backoff that protects deliveries. Console only. claude-mem's corpus endpoints were checked
|
|
455
|
+
(verified 2026-09-19: `GET /api/corpus` lists none, and a corpus has to be built and primed with a model session
|
|
456
|
+
of its own), so search plus timeline stays the retrieval path and the hub still owns no store.
|
|
457
|
+
- Config: `memory.enabled` (default true when the worker answers `/api/health`),
|
|
458
|
+
`memory.worker_url` (default from `~/.claude-mem/settings.json`
|
|
459
|
+
`CLAUDE_MEM_WORKER_PORT`), `memory.inject_tokens`, `memory.brief_items`,
|
|
460
|
+
`memory.platform_source` (default `agent-hub`).
|
|
461
|
+
- Prerequisites: claude-mem worker running; Codex claude-mem plugin installed; `dot ai
|
|
462
|
+
memory status` green for kimi. `ahub doctor` reports all three.
|
|
463
|
+
|
|
464
|
+
### Safety
|
|
465
|
+
|
|
466
|
+
- Untrusted framing on all cross-peer text; standing instruction once per session.
|
|
467
|
+
- `ahub claude` and `ahub codex` keep normal permission prompts. `--unattended` opts into
|
|
468
|
+
`--dangerously-skip-permissions` (Claude) and `--dangerously-bypass-approvals-and-sandbox`
|
|
469
|
+
(Codex 0.154.0 documents this flag, not `--yolo`) and prints a warning.
|
|
470
|
+
- Kimi and local tools: cwd-scoped, secrets denylist (a secrets directory, `.env*`, keys).
|
|
471
|
+
- Loopback binds only; ports per project from a registry (base 4600, stride 10).
|
|
472
|
+
- No subscription OAuth through any gateway.
|
|
473
|
+
|
|
474
|
+
### Configuration and state
|
|
475
|
+
|
|
476
|
+
- `.agenthub/config.json` (roles, ports, filter tiers, watchdog), `.agenthub/routing.toml`.
|
|
477
|
+
- `.agenthub/state/` gitignored: `hub.pid`, `status.json`, `control-token` (0600, per run),
|
|
478
|
+
`hub.db` (tasks, messages, budget; from M4), `hub.log`, `switchyard.toml`, `checkpoint.md`.
|
|
479
|
+
- Env: `AGENTHUB_STATE_DIR`, `AGENTHUB_OMNIROUTE_URL`, `OMNIROUTE_API_KEY`,
|
|
480
|
+
`AGENTHUB_SWITCHYARD_BIN`, `AGENTHUB_UNATTENDED`.
|
|
481
|
+
|
|
482
|
+
### Testing
|
|
483
|
+
|
|
484
|
+
- Unit: bus routing, priority batching, state machines, L1 policy evaluation, envelope
|
|
485
|
+
framing, marker parsing.
|
|
486
|
+
- Fakes: fake ACP server, fake app-server (ws), fake MCP client, fake OpenAI-compatible
|
|
487
|
+
model server. Integration tests spawn the daemon against fakes.
|
|
488
|
+
- Live smoke (manual, documented): trio chat, steer during a Codex turn, Kimi
|
|
489
|
+
busy-queue drain, local worker completes a bulk edit through Switchyard -> OmniRoute
|
|
490
|
+
with `x-omniroute-provider` visible, budget pause and resume with a mocked probe.
|
|
491
|
+
- One command gates everything: `scripts/check.sh` (typecheck, unit, integration).
|
|
492
|
+
|
|
493
|
+
## Changes
|
|
494
|
+
|
|
495
|
+
New repo layout:
|
|
496
|
+
|
|
497
|
+
```
|
|
498
|
+
src/hub/ daemon, bus, peer-registry, state-machines, board, budget, router
|
|
499
|
+
src/adapters/ claude-channel.ts, codex-appserver.ts, acp.ts, local-worker.ts
|
|
500
|
+
src/switchyard/ config generator, sidecar lifecycle, health
|
|
501
|
+
src/memory/ claude-mem worker client, capture for the local worker, recall (inject, brief), remember
|
|
502
|
+
src/cli/ up, claude, codex, kimi, local, say, tail, board, route, budget, status, logs, kill, init
|
|
503
|
+
plugins/agent-hub/ .claude-plugin/plugin.json, .mcp.json, server bundle, hooks (SessionStart health, Stop announce)
|
|
504
|
+
.claude-plugin/marketplace.json
|
|
505
|
+
templates/ CLAUDE.md and AGENTS.md marker blocks, routing.toml default, config.json default
|
|
506
|
+
scripts/ check.sh, build.mjs, smoke-*.ts
|
|
507
|
+
docs/specs/ this file
|
|
508
|
+
CLAUDE.md, REVIEW.md
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## Acceptance criteria
|
|
512
|
+
|
|
513
|
+
1. `ahub up && ahub claude && ahub codex --new && ahub kimi` in one project: a message from
|
|
514
|
+
any peer reaches the other three within 2 s when idle, with untrusted framing.
|
|
515
|
+
2. A message sent while Codex is mid-turn arrives as `turn/steer` if `important`, else
|
|
516
|
+
after `turn/completed`; while Kimi is mid-prompt it is queued and drained on
|
|
517
|
+
`end_turn`, never lost.
|
|
518
|
+
3. `[STATUS]` messages from one peer are batched into one digest per recipient.
|
|
519
|
+
4. A task proposed by Claude, accepted by Codex, marked done, is auto-routed to the
|
|
520
|
+
reviewer; `changes_requested` twice escalates per `routing.toml`.
|
|
521
|
+
5. `ahub local` completes a `bulk_edit` task; OmniRoute log shows the request with the
|
|
522
|
+
per-peer token and `x-omniroute-provider: vllm`; `ahub route explain` shows the
|
|
523
|
+
chosen route.
|
|
524
|
+
6. With Switchyard absent, the local worker still works via `fixed_model`.
|
|
525
|
+
7. A PII-flagged task never leaves the `local` peer (test with a fake pattern).
|
|
526
|
+
8. Mocked Codex quota at 0.95 pauses Codex, checkpoints, reassigns its open
|
|
527
|
+
`implement` task to `local`, and resumes Codex after the mocked reset.
|
|
528
|
+
9. `scripts/check.sh` exits 0; live smoke checklist recorded in `docs/smoke.md`.
|
|
529
|
+
10. Default launches keep permission prompts; `--unattended` prints the warning.
|
|
530
|
+
11. A local-worker task produces observations in claude-mem with
|
|
531
|
+
`platform_source = agent-hub` and `agent_id = local`; `search` from any peer finds
|
|
532
|
+
them without a `platformSource` filter.
|
|
533
|
+
12. Starting `ahub kimi` after a Claude session in the same project injects a context
|
|
534
|
+
block that includes at least one Claude-platform observation, capped at
|
|
535
|
+
`memory.inject_tokens`; starting a second Kimi session does not re-inject it.
|
|
536
|
+
13. Assigning a task whose title matches an earlier observation attaches a brief with
|
|
537
|
+
that observation id; reassigning the same task to the same peer does not repeat
|
|
538
|
+
ids already delivered.
|
|
539
|
+
14. `hub_remember` from Codex creates a `memory/save` note whose metadata carries
|
|
540
|
+
`peer = codex` and the task id; the budget-relay resume prompt for the receiving
|
|
541
|
+
peer contains the paused peer's session summary.
|
|
542
|
+
15. With the claude-mem worker stopped, every acceptance criterion 1 to 10 still passes
|
|
543
|
+
and `ahub doctor` reports memory as unavailable.
|
|
544
|
+
|
|
545
|
+
## Tasks
|
|
546
|
+
|
|
547
|
+
M1 messaging core and three adapters
|
|
548
|
+
- [x] Repo scaffold: Bun, TS strict, `scripts/check.sh`, `CLAUDE.md`, `REVIEW.md`, labels
|
|
549
|
+
- [x] Bus, envelope, peer registry, state machines, control WS, state dir, port registry
|
|
550
|
+
- [x] Claude channel plugin: capability, push, `hub_send`, `hub_inbox`, reconnect
|
|
551
|
+
- [x] Codex adapter: spawn, proxy, agentMessage intercept, `turn/start`, watchdog
|
|
552
|
+
- [x] ACP adapter: spawn `kimi acp`, session lifecycle, prompt, chunk aggregation
|
|
553
|
+
- [x] CLI `up/claude/codex/kimi/say/tail/status/logs/kill`, `ahub init` marker blocks
|
|
554
|
+
- [x] Fakes plus unit and integration tests
|
|
555
|
+
- [x] Live trio chat smoke (`docs/smoke.md`): Kimi, Codex and Claude interactive legs passed on 2026-09-19; the later four-peer broadcast also passed
|
|
556
|
+
- [x] claude-mem worker client (`src/memory/`), `ahub doctor` memory check, fake worker for tests
|
|
557
|
+
|
|
558
|
+
M2 coordination
|
|
559
|
+
- [x] Priority tiers and status batching, marker parsing
|
|
560
|
+
- [x] Codex `turn/steer` for important while busy (plain busy queue and drain for every peer shipped in M1: without it a second Kimi prompt fails with `turn.agent_busy`)
|
|
561
|
+
- [x] Paused and offline queues, idempotent delivery, drop rules
|
|
562
|
+
- [x] Session-start cross-platform recall (token cap, once per peer per hub run)
|
|
563
|
+
- [x] Live `turn/steer` against real Codex (2026-09-19, after the weekly window reset)
|
|
564
|
+
|
|
565
|
+
M3 local worker and routing L2/L3
|
|
566
|
+
- [x] Local worker agent loop with cwd-scoped tools, secrets denylist, approvals and seatbelt sandbox
|
|
567
|
+
- [x] OmniRoute client with Cloudflare Access headers and the owner-issued inference key; the off-campus live path remains blocked in `docs/smoke.md` (#12)
|
|
568
|
+
- [x] Switchyard sidecar: config generation, lifecycle, health, fallback to fixed model
|
|
569
|
+
- [x] `ahub local`, smoke through OmniRoute with provider header check, and through the real sidecar
|
|
570
|
+
- [x] Local worker capture into claude-mem (`sessions/init`, `observations`, `summarize`, `session-end`, skip list)
|
|
571
|
+
|
|
572
|
+
M4 task board, roles, routing L1
|
|
573
|
+
- [x] sqlite board, `hub_task_*` and `hub_review` tools on all adapters
|
|
574
|
+
- [x] Role contract injection per native surface
|
|
575
|
+
- [x] `routing.toml` loader, signals (PII, context length, quota), `ahub route explain`
|
|
576
|
+
- [x] Review handoff and task-level escalation
|
|
577
|
+
- [x] Task brief on handoff (search + timeline, `seen_ids`), `hub_remember` tool and console command, auto-saved board transitions
|
|
578
|
+
- [x] Live: Codex calling hub tools in a real turn (2026-09-19)
|
|
579
|
+
- [x] Live: Claude plugin task tools and digests in a real interactive session (`docs/smoke.md`, 2026-09-19)
|
|
580
|
+
|
|
581
|
+
M5 budget relay
|
|
582
|
+
- [x] Quota sources (Codex native, Claude status line, Kimi tokens, manual), gate, pause, checkpoint
|
|
583
|
+
- [x] Reassignment to local, one resume envelope, idempotency and restart recovery
|
|
584
|
+
- [x] Handoff context (checkpoint summary or memory block) plus task briefs in the task envelope
|
|
585
|
+
- [x] Live: the Claude status line tee in an interactive session (`docs/smoke.md`, 2026-09-19)
|
|
586
|
+
- [ ] Live: a real pause driven by Codex's own numbers with a TUI attached; the natural near-limit prerequisite remains blocked by issue #12
|
|
587
|
+
|
|
588
|
+
M6 internal inference, packaging
|
|
589
|
+
- [x] Status digests and triage through `sy/fast` (amended: optional and fail-open with a backoff. Only plain status
|
|
590
|
+
chatter over a threshold is condensed, into one item from `digest` that keeps every sender, envelope id and the
|
|
591
|
+
highest hop; important, task, review, budget, preface and private items are never touched. A task proposed
|
|
592
|
+
without a class is labelled from the closed class list, a PII task only when the gateway is on campus. The
|
|
593
|
+
model's output is capped text framed as untrusted or a validated enum, never a route, peer id or instruction.)
|
|
594
|
+
- [x] Packaging (amended: no compiled binary, because the daemon re-spawns itself and five places resolve assets
|
|
595
|
+
relative to the source tree; the package ships the tree and installs from GitHub with Bun, verified:
|
|
596
|
+
`bun add -g github:STAIxBWLB/agent-hub`. `package.json` is the one version, stamped into the plugin manifest
|
|
597
|
+
and the MCP server and checked by the gate. `ahub setup` installs or updates the Claude plugin from the
|
|
598
|
+
package after asking. CI runs the gate on Ubuntu and macOS; a `v*` tag matching `package.json` cuts a release.
|
|
599
|
+
v0.4.0 was published to npm with provenance; clean registry installation and provenance
|
|
600
|
+
readback remain smoke follow-ups, and brew remains out of scope.)
|
|
601
|
+
- [x] docs: `docs/quickstart.md`, `docs/security.md`, `docs/smoke.md`, `CONTRIBUTING.md`, `CHANGELOG.md`
|
|
602
|
+
|
|
603
|
+
## npm distribution amendment (issue #4)
|
|
604
|
+
|
|
605
|
+
- Publish `@staix/agent-hub` under the `staix` npm organization; installed commands
|
|
606
|
+
remain `ahub` and `agent-hub`. GitHub installation remains available.
|
|
607
|
+
- Ship the source tree and relative runtime assets. The gate checks the actual
|
|
608
|
+
`npm pack --dry-run` file list, including hidden marketplace and plugin manifests.
|
|
609
|
+
- Use a plain-JavaScript `src/cli/main.js` bin entrypoint that checks for Bun before
|
|
610
|
+
dynamically importing `main.ts`. This refines the issue's proposed guard location:
|
|
611
|
+
a guard in `main.ts` cannot precede its static Bun-only imports, and older Node
|
|
612
|
+
versions cannot load the TypeScript file at all. No preinstall hook or Node build.
|
|
613
|
+
- The release workflow validates the tag/version, runs the gate, then publishes
|
|
614
|
+
publicly with provenance and `NPM_TOKEN`. The token belongs to an authorized
|
|
615
|
+
organization member. Organization conversion and credential provisioning are
|
|
616
|
+
owner operations; this implementation does not perform them.
|
|
617
|
+
- The v0.4.0 release workflow completed registry publication with provenance.
|
|
618
|
+
Registry metadata/provenance readback and real Claude setup from a clean registry
|
|
619
|
+
installation remain to be verified.
|
|
620
|
+
|
|
621
|
+
## Out of scope
|
|
622
|
+
|
|
623
|
+
- Cross-machine broker or rooms, Windows. The web UI was outside the original six milestones; issue #6 adds the local dashboard described below.
|
|
624
|
+
- OmniRoute daemon on the Mac; pooling subscription OAuth through any gateway.
|
|
625
|
+
- Gemini CLI and OpenCode adapters beyond the ACP fallback note.
|
|
626
|
+
- Switchyard `auto` route until v0.3.0 ships.
|
|
627
|
+
- Running Claude Code itself on a local model (depends on the Anthropic-to-chat
|
|
628
|
+
translation check in the OmniRoute plan v1, Phase 0).
|
|
629
|
+
- A hub-owned memory store, cmem.ai cloud sync, automatic corpus building (a project
|
|
630
|
+
corpus for `ahub ask` is an optional M6 item), and any change to the vault LEARN loop.
|
|
631
|
+
|
|
632
|
+
## Risks
|
|
633
|
+
|
|
634
|
+
| Risk | Mitigation |
|
|
635
|
+
| --- | --- |
|
|
636
|
+
| Claude channels are research preview; flag or protocol may change | Adapter isolated in one file; plugin version pinned to a Claude Code range; `hub_inbox` fallback |
|
|
637
|
+
| `switchyard-server` is Demo grade, pre-1.0 | Session-scoped sidecar, pinned version, L1 `fixed_model` fallback |
|
|
638
|
+
| Anthropic-to-chat translation drops `tool_use` | Claude never runs on local models in this design |
|
|
639
|
+
| Local worker executes untrusted room text | Untrusted framing, cwd scope, denylist, no `--unattended` by default |
|
|
640
|
+
| Steering a Codex turn changes its plan mid-flight | Only `important` steers; default tier is `status` |
|
|
641
|
+
| Kimi ACP has no quota API | Token-based soft limit only; documented |
|
|
642
|
+
| claude-mem worker API is internal and unversioned for third-party clients | Client isolated in `src/memory/`, pinned to a plugin version range, fail-open everywhere, contract test against a fake worker plus one live smoke |
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
## Post-milestone amendment: local dashboard (issue #6)
|
|
646
|
+
|
|
647
|
+
Owner instruction to implement issue #6 approves its local, read-mostly dashboard.
|
|
648
|
+
The session/origin model is defined in `docs/security.md`, written before the code.
|
|
649
|
+
`ahub ui` lazily opens an ephemeral loopback listener and exchanges a single-use
|
|
650
|
+
fragment ticket for an HttpOnly, SameSite=Strict browser session. The control link
|
|
651
|
+
retains its blanket Origin refusal. The browser receives no control token.
|
|
652
|
+
|
|
653
|
+
One static HTML file, without a build step or runtime dependency, polls public
|
|
654
|
+
snapshots once per second. The latest 200 redacted bus events are retained only
|
|
655
|
+
once the listener starts. The API exposes only permission answers, peer
|
|
656
|
+
pause/resume, console messages, task proposal and assignment. All task changes
|
|
657
|
+
use `Tasks`; budget pauses keep the terminal's override requirement.
|
|
658
|
+
|
|
659
|
+
Local-worker permission titles may contain PII file contents or commands, so the
|
|
660
|
+
page shows a terminal-only stub and offers denial only. Allowing those requests
|
|
661
|
+
requires reading `ahub tail` and answering with `ahub permit`. Other agent
|
|
662
|
+
approvals offer their original options. Task refs/history and checkpoint summaries
|
|
663
|
+
are omitted from browser snapshots. Private envelope bodies retain the tail stub.
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
## Amendment: concurrent projects and unified dashboard (issue #19)
|
|
667
|
+
|
|
668
|
+
The approved [multi-project specification](2026-09-19-multi-project-design.md)
|
|
669
|
+
extends the single-machine model to simultaneous independent repository/worktree
|
|
670
|
+
hubs. It replaces the unlocked JSON port registry with a transactional SQLite
|
|
671
|
+
project registry and adds explicit project selection, authenticated instance
|
|
672
|
+
ownership and a separate optional dashboard manager. Runtime data and routing
|
|
673
|
+
remain per project. Native memory aliases remain compatible and shared.
|
|
674
|
+
Control protocol 7 adds project/instance identity and console-only dashboard
|
|
675
|
+
snapshot/action forwarding. Existing project-local UI security rules still apply.
|