@staix/agent-hub 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the previous repository, archived on 2026-09-30 when this repository's history was rewritten; the one exception is the open smoke-check issue, formerly #12, which moved here as #1. Numbers in newer entries refer to this repository.
4
4
 
5
+ ## 0.10.0
6
+
7
+ - Review checklists and review outcomes: review requests ask the reviewer to map signatures and call sites to the plan, read the check result and list unmet items (`hub_review {unmet}`); the hub records approved, caught, contradicted and escalated reviews per implementer, reviewer and class, shown by `ahub task show` and `ahub route explain`, and `review.adaptive` (off by default) orders reviewers by that record, after idle before busy and ahead of quota. A record counts tasks, a catch belongs to the owner whose work was caught, and only a failure on the same file or symbol contradicts an approval (#35).
8
+ - Recovery after an unplanned stop: the hub keeps each attached peer's session identity while it runs; a hub started after a crash reports what happened to each peer in `ahub status`, resumes Kimi (ACP `session/load`), a headless Pi and the local worker (on its recorded route) when `recovery.auto_resume_after_crash` is on, and gives each peer a notice of its deliveries left in `needs_review` with its next delivery. A stop someone asked for, even one past the shutdown deadline, is not taken for a crash (#37).
9
+ - The local worker's sandbox starts from deny default: commands may run and read the system, toolchain and project directories (and the selected Xcode or Command Line Tools dir) and write the project and temp, nothing else; `/Applications`, `/nix`, `/Volumes` and `/Users/Shared` now need `local.read_allow`; `local.sandbox: "allow-default"` (machine-local) keeps the profile of 0.9 and earlier for one release; with network on, the public CA bundles stay readable for TLS. Per-peer `capabilities` (`propose`, `assign`, `remember`, `important`) are enforced by the daemon, a malformed entry grants nothing and every refusal is logged, under deny-default the worker's commands can no longer reach the LaunchServices, CoreServices or SecurityServer brokers, and a test pins that a peer can never answer a permission request (#39).
10
+
11
+ ## 0.9.0
12
+
13
+ - Early conflict detection: in a git work tree, a turn that changes a file another owner's open task changed earlier warns both owners (and the console, and `events.jsonl`), once per file and task, marked concurrent when another peer worked meanwhile. `ahub check-path` and the PreToolUse hook template `templates/claude-hooks.json` give Claude the same warning before an edit, without blocking it (#32).
14
+ - Task dependencies: `hub_task_propose` takes `after: [ids]`; a task waits, offered to nobody and not claimable, until those are approved, then goes through assignment (a task the hub stopped before offering is offered once it runs again). `hub_task_list {ready: true}` and `ahub board --ready` show the ready queue, and existing boards gain the column on open (#34).
15
+ - Quota-aware routing: candidates with quota readings are ordered by headroom per hour to their reset, so the window that resets first is drained first; a paused peer whose window resets within `budget.wait_max_min` (30 with a project config) keeps its work unless a task is `urgent`; and a peer whose recent failures in a class outweigh its approvals there is demoted for that class, with a one-day half-life. `ahub route explain` shows both, and `ahub task propose ... --urgent` marks a task urgent (#36).
16
+ - Per-sender limits on what agents send: token buckets per sender, per recipient and for `[IMPORTANT]`, plus dropping the same text to the same recipients, answering the same message, within a window. A refused `hub_send` answers with the reason and the seconds to wait; a turn answer over the important budget goes out as status, and one over a rate limit is dropped, the agent hearing why with its next delivery. `[FYI]` and the console user are never limited (#38).
17
+ - An upgrade or controlled restart from a 0.9.0 or later source waits for completion checks and console task operations still in flight; one that finished after the commit used to leave the operation blocked at verification, unable to resume or abort. A 0.8.x or older source cannot report them: wait for its checks by hand before upgrading (see the operations guide) (#50).
18
+ - Upgrading a running 0.7.x or 0.8.x hub with tasks on its board verifies the board although 0.9.0 adds the `deps` column, like 0.8.1 did for `plan` (#50).
19
+ - Turn snapshots no longer miss an edit that keeps a file's size and lands in the second git last wrote the index: the snapshot's copy of the index now keeps the original's time, which git's racy-entry check compares against in whole seconds. A missed edit could leave a file out of `ahub undo`, let undo restore over another agent's later change, or skip an early conflict warning (#60).
20
+
5
21
  ## 0.8.1
6
22
 
7
23
  - Upgrading a running 0.7.x hub that has a task on its board no longer stops at verification ("queue, manual pause, task board or budget preservation was not verified"): the 0.8.0 target digested its tasks with the new `plan` field, which the 0.7.x source never had. The target now also accepts the digest in the source's shape while every plan is empty; any real change to the board still fails the check. Use the 0.8.1 coordinator, not 0.8.0's, for such an upgrade (#57).
package/README.md CHANGED
@@ -4,7 +4,7 @@ Native multi-agent hub for one developer's machine: Claude Code, Codex, Kimi Cod
4
4
  hub-owned local-LLM worker collaborate as peers in independent project directories, with
5
5
  task-aware model routing (Switchyard) in front of a self-hosted gateway (OmniRoute).
6
6
 
7
- Status: 0.8.1, control protocol 10. Durable delivery records distinguish queued
7
+ Status: 0.10.0, control protocol 10. Durable delivery records distinguish queued
8
8
  work from uncertain execution. The [smoke checklist](docs/smoke.md) records
9
9
  verified paths and remaining prerequisites.
10
10
 
@@ -28,7 +28,7 @@ cd <your project> && ahub init && ahub up && ahub tail
28
28
  Or install the same version from GitHub:
29
29
 
30
30
  ```bash
31
- bun add -g github:STAIxBWLB/agent-hub#v0.8.1 && ahub setup
31
+ bun add -g github:STAIxBWLB/agent-hub#v0.10.0 && ahub setup
32
32
  ```
33
33
 
34
34
  The installed commands remain `ahub` and `agent-hub`.
package/docs/events.md CHANGED
@@ -18,9 +18,10 @@ marked `private: true`, and PII tasks `pii: true`.
18
18
  | `turn_start` | `peer`, `turn` (`<peer>#<hub run>.<n>`, unique across restarts). A turn follows the adapter: pausing a busy peer does not end it |
19
19
  | `turn_end` | `peer`, `turn`, `ms`, `tokens` (when the adapter reported any during the turn), `files` and `snapshotMs` (when snapshots are on: how many files the turn changed, and the time both snapshots took) |
20
20
  | `tokens` | `peer`, `n` (tokens added since the previous report) |
21
- | `task` | `id`, `event` (the board history event, e.g. `proposed`, `assigned`, `done`, `check failed`), `by`, `state`, `owner`, `reviewer`, `class`, `pii` |
21
+ | `task` | `id`, `event` (the board history event, e.g. `proposed`, `assigned`, `done`, `check failed`, `blocked`, `ready`), `by`, `state`, `owner`, `reviewer`, `class`, `pii` |
22
22
  | `overlap` | `task`, `owner`, `others` (`task`, `owner`, `paths`, and `symbols` when plans name the same symbol; a name that matches a PII pattern is left out, so either list can be empty), the structured twin of the console notice |
23
23
  | `quota` | `peer`, `windows` (`id`, `used`, `resetsAt`), `hard`, `measuredAt` (when the reading was taken, if not when it arrived: Claude's numbers come through a file) |
24
+ | `conflict` | `peer`, `task` (its task in progress, when it had one), `other` (the other owner's open task), `owner`, `paths` (names that match a PII pattern left out, so it can be empty), `concurrent` (another peer worked during the turn) |
24
25
 
25
26
  Token usage by adapter:
26
27
 
@@ -1,6 +1,6 @@
1
1
  # Operations guide
2
2
 
3
- This guide describes ahub 0.8.1 and control protocol 10. Live verification
3
+ This guide describes ahub 0.10.0 and control protocol 10. Live verification
4
4
  results and remaining prerequisites are recorded separately in [the smoke ledger](smoke.md).
5
5
 
6
6
  ## Install and start
@@ -27,11 +27,38 @@ what the hub runs, which files it sends as credentials, where task text goes,
27
27
  or how far the local worker's sandbox reaches (`kimi_cmd`, `codex_bin`,
28
28
  `pi.cmd`, `checks`, `mlx.bin`, `mlx.runtimeDir`, `mlx.modelPath`, `omniroute.urls`,
29
29
  `omniroute.access_hosts`, the `omniroute` key files, `memory.worker_url`,
30
- `local.read_allow`, `local.bash_network`) are machine-local: they apply only
31
- from a file git confirms nobody committed. Put them in
30
+ `local.read_allow`, `local.bash_network`, `local.sandbox`) are machine-local:
31
+ they apply only from a file git confirms nobody committed. Put them in
32
32
  `.agenthub/config.local.json` (`ahub init` adds it to `.gitignore`), which is
33
33
  read after `config.json`; outside a git repository they keep their defaults, and
34
34
  an empty value always means the default.
35
+
36
+ The local worker's commands run under a sandbox that starts from deny default
37
+ (0.10): they may run and read the system, toolchain and project directories and
38
+ the selected Xcode or Command Line Tools dir (`xcode-select -p`), write the
39
+ project and temp, and nothing else. With `local.bash_network` on they may also
40
+ read the public CA bundles, which the `*.pem` key deny would otherwise hide.
41
+ `"local": { "sandbox": "allow-default" }` in `config.local.json` brings back the
42
+ profile of 0.9 and earlier for one release, should a toolchain need a path the
43
+ new one lacks; please report it. Newly closed outside home: `/Applications`
44
+ (an app's bundled CLI), `/nix`, `/Volumes` and `/Users/Shared`. A toolchain
45
+ there, or elsewhere in your home (a CI tool cache, a version manager the profile
46
+ does not list), needs its directory in `local.read_allow`, for example
47
+ `"/nix"` or `"~/.pixi"`.
48
+
49
+ `capabilities` in `.agenthub/config.json` narrows what a peer may do with the
50
+ hub's tools: list a peer and it keeps only the capabilities named, from
51
+ `propose` (`hub_task_propose`), `assign` (proposing with another peer as owner),
52
+ `remember` (the `hub_remember` tool; the notes the hub itself keeps of done
53
+ summaries and review verdicts are not gated) and `important` (`[IMPORTANT]`
54
+ messages). For example `"capabilities": { "local": ["propose", "remember"] }`.
55
+ A peer that is not listed keeps all of them, a listed peer whose value is not a
56
+ list gets none, and an unknown capability name grants nothing; `hub.log` says
57
+ so for each, and logs every refusal (`capabilities:`). A refused tool call says
58
+ which capability is missing; an `[IMPORTANT]` turn answer without the
59
+ capability goes out as status, with a note to the sender.
60
+ Approvals are never a capability: only the console (and the dashboard) answers a
61
+ permission request.
35
62
  A committed value is ignored with a line in `hub.log`, a note from `ahub
36
63
  codex` and `ahub models`, and a row in `ahub doctor`.
37
64
 
@@ -87,6 +114,21 @@ external side effect happened unless the task's refs and a live readback show
87
114
  that effect. The hub can reassign after repeated review changes according to
88
115
  the routing configuration.
89
116
 
117
+ A review request carries a checklist: map the changed signatures and call sites
118
+ to the task's plan (without one, to its detail, which the request then
119
+ includes), read the check result, and list what is
120
+ unmet (`hub_review` takes `unmet`, `ahub review ... --unmet <item>`). The hub
121
+ records how each review turned out, per implementer, reviewer and class:
122
+ approved; caught (changes were requested and the owner's redo was approved);
123
+ contradicted (within a week, work on the same file or symbol failed its check
124
+ or review); escalated (after the reviewer asked for changes). A record counts
125
+ tasks, not verdicts. `ahub task show <id>` lists a task's outcomes and `ahub
126
+ route explain` shows each reviewer's record with the implementer. With
127
+ `"review": { "adaptive": true }` in `.agenthub/config.json`, reviewers with at
128
+ least `min_reviews` (5) reviews of that implementer in the class are ordered by
129
+ how their reviews held up, ahead of quota but after idle before busy; off by
130
+ default, which leaves assignment as it was.
131
+
90
132
  An agent can claim work nobody assigned it by proposing a task with itself as
91
133
  owner; without a class, and with no model to name one, the claim is filed as
92
134
  `implement`. A claim or an accept can carry a plan: the files, symbols and
@@ -98,7 +140,20 @@ task is done (after its check passes, when one is configured), the owners of
98
140
  open tasks on the same paths or symbols get a message with the changed files,
99
141
  the plan's signatures and the first line of the summary, each left out when it
100
142
  matches a PII pattern; nobody else does. PII
101
- tasks are left out on both sides. An owner offline longer than `tasks.release_after_min`
143
+ tasks are left out on both sides.
144
+
145
+ A task can wait for others: `hub_task_propose` takes `after: [ids]` (`ahub task
146
+ propose ... --after <id>`). Until every one of them is approved, the task is
147
+ offered to nobody, cannot be claimed, accepted or marked done, and `ahub route
148
+ explain <id>` says what it waits for. When the last one is approved, the task
149
+ goes through assignment like a new one; if the hub stopped before it got that
150
+ far, the task is offered within a minute of a peer that can take it attaching
151
+ to the next run.
152
+ `ahub board --ready` and
153
+ `hub_task_list {ready: true}` list the proposed tasks with nothing left to wait
154
+ for. Dependencies are fixed when a task is proposed and can only name tasks that
155
+ already exist, so they cannot form a cycle. A waiting task cannot name an owner;
156
+ use `ahub task assign` once it is ready. An owner offline longer than `tasks.release_after_min`
102
157
  (default 30, `0` turns it off) in `.agenthub/config.json` loses its open tasks
103
158
  to a peer routing can give them to; with nobody to take them they stay, and a
104
159
  paused peer or a hub in a recovery operation is left alone.
@@ -111,6 +166,27 @@ ahub say @claude "[STATUS] the test run is complete"
111
166
  ahub say @kimi "[FYI] the result is recorded"
112
167
  ```
113
168
 
169
+ What agents send is limited per sender (`limits` in `.agenthub/config.json`; a
170
+ project config gets the values below unless it sets others, `0` turns one off):
171
+
172
+ - `sender_per_min` (12) messages a minute from one agent, `pair_per_min` (6) to
173
+ one recipient (a broadcast counts as one), and `important_per_hour` (6)
174
+ `[IMPORTANT]` messages, each of which can interrupt a running turn. Limits
175
+ count what is sent: a reply goes to the agent it answers, a reply to a
176
+ condensed digest counts against the agents behind it, and an `[IMPORTANT]` the
177
+ hub lowers to status is not important. `[FYI]` costs nobody a turn and is
178
+ never limited.
179
+ - `repeat_window_s` (120): the same text to the same recipients, answering the
180
+ same message, again within the window is dropped. "Yes." to two different
181
+ questions is two messages.
182
+ - A refused `hub_send` answers `not sent: <why>`, with the seconds to wait for a
183
+ rate limit, so the agent learns at once. A turn answer has nobody to refuse
184
+ to: one over the important budget goes out as status, and one over a rate
185
+ limit or repeated is not published; either way the agent gets the reason with
186
+ its next delivery, and has to send a dropped answer again. hub.log records
187
+ each refusal (`limits:`), and a value that is not a number falls back to the
188
+ default above. The console user and the hub itself are never limited.
189
+
114
190
  `@peer` addresses one known peer. With no recipient, `ahub say` broadcasts
115
191
  to attached peers. `[IMPORTANT]` can bypass batching where the peer supports
116
192
  it; `[STATUS]` may batch, and `[FYI]` is recorded without follow-on
@@ -185,6 +261,34 @@ ahub undo <turn> --yes --context # Codex's latest turn: also drop it from Codex
185
261
  older turn says so.
186
262
  - Each `turn_end` event carries `files` and `snapshotMs` (`ahub export`).
187
263
 
264
+ ## Edit conflicts
265
+
266
+ With snapshots on, the hub also compares each turn's files with what other
267
+ owners' open tasks changed before it. A turn's files count for every task its
268
+ peer has in progress, except files that another peer's overlapping turn changed;
269
+ while such a turn's changes are unknown (it still runs), the turn's files count
270
+ for none. When a peer changes a file that another owner's open task changed
271
+ earlier, both get a message naming the file and the other task, the console
272
+ shows a `conflict:` line, and `events.jsonl` records a `conflict` event (`ahub
273
+ report` counts them). Each peer, task and file is reported once per hub run.
274
+ When another peer worked during the same turn, the messages say so: the change
275
+ may be theirs. Edits by Claude or by you during a peer's turn count as that
276
+ peer's, without that note: the hub sees no turn of yours. Nothing is blocked. A
277
+ file only one agent touched, and anything to do with a PII task, warns nobody.
278
+ A file whose name matches a PII pattern is not named, as in overlap notices: the
279
+ messages and the console line only count such files, and the event leaves them out.
280
+
281
+ Claude's edits do not pass through turn snapshots, so Claude can ask before each
282
+ edit instead. `templates/claude-hooks.json` is a PreToolUse hook for Edit, Write,
283
+ MultiEdit and NotebookEdit that runs `ahub check-path --hook`. Merge it into
284
+ `.claude/settings.local.json` (or your user settings) yourself. When another
285
+ owner's open task claims the file (refs or plan paths) or changed it, Claude gets
286
+ the list with the tool result and you see one line. The hook never decides a
287
+ permission, so your permission rules apply as before. Task titles in the list
288
+ are quoted and marked as other agents' text. The hook runs `ahub`, so it has to
289
+ be on the PATH Claude Code's hooks see; otherwise every edit shows a hook error
290
+ (it never blocks). `ahub check-path <file>` prints the same list in a terminal.
291
+
188
292
  ## Approvals and pauses
189
293
 
190
294
  Inspect permission requests in the terminal:
@@ -215,6 +319,25 @@ A budget pause remains authoritative until the budget command explicitly
215
319
  overrides it or the window resets. Check `ahub status` and `ahub board` after
216
320
  a pause or handoff.
217
321
 
322
+ Quota also shapes routing and handoffs:
323
+
324
+ - Among peers a task could go to, those with quota readings are ordered by
325
+ headroom per hour left until the reset of the window that bounds it (the most
326
+ used one, so a week window near its cap is not mistaken for a 5 h window about
327
+ to reset), so the window that resets first is used first. Peers without readings, such as `local` and `pi`, keep
328
+ their place in `routing.toml`. `ahub route explain` shows the reordering.
329
+ - When a paused peer's window resets within `budget.wait_max_min` (30 once the
330
+ project has `.agenthub/config.json` or `config.local.json`, 0 without one; `0`
331
+ always hands over), it keeps its work and only tasks proposed with `urgent`
332
+ (`ahub task propose ... --urgent`) move. If the reset moves past the limit
333
+ while it waits (a week window crosses the gate), its work is handed over after
334
+ all. `ahub budget` shows each decision and why in the pause reason.
335
+ - A peer whose recent failures in a class (failed checks, changes requested,
336
+ escalations by hand) reach 1.5 after decay, and outweigh its recent
337
+ approvals there (a task without a reviewer counts when done), goes behind the
338
+ other candidates in the same state for that class, `local` and `pi` included.
339
+ A failure counts half after a day. `ahub route explain` names demoted peers.
340
+
218
341
  ## Durable delivery and queue resolution
219
342
 
220
343
  Protocol 10 records each recipient delivery in the private project journal.
@@ -302,8 +425,8 @@ source and carries every recovery fix released up to it. Protocol 8 and older
302
425
  project directory, without replacing the global CLI first:
303
426
 
304
427
  ```bash
305
- bunx --package @staix/agent-hub@0.8.1 ahub upgrade --to 0.8.1 --dry-run
306
- bunx --package @staix/agent-hub@0.8.1 ahub upgrade --to 0.8.1 --yes
428
+ bunx --package @staix/agent-hub@0.10.0 ahub upgrade --to 0.10.0 --dry-run
429
+ bunx --package @staix/agent-hub@0.10.0 ahub upgrade --to 0.10.0 --yes
307
430
  ```
308
431
 
309
432
  | Running now | Coordinator to use |
@@ -318,36 +441,64 @@ its verification never matches the board and the operation stays blocked (fixed
318
441
  in 0.8.1). Such a blocked operation can neither resume nor abort, and its lock
319
442
  refuses `up` and `kill` for every project; the [smoke ledger](smoke.md) (0.7.11
320
443
  to 0.8.0) records the manual cleanup. Do not use an older installed CLI as the
321
- coordinator. A 0.6.x CLI cannot target
444
+ coordinator. After an upgrade, do not start an older hub on the same project: it
445
+ does not know the newer task columns, and its board readback breaks a later
446
+ upgrade. A 0.6.x CLI cannot target
322
447
  protocol 10: its plan does not check the target's protocol, so the dry-run shows
323
448
  no blocker, and `--yes` stops at staging ("target protocol requires a newer
324
449
  coordinator") with an operation left to clear by `ahub recovery abort <id>`. An
325
450
  older 0.7.x CLI may lack recovery fixes released after it. The
326
451
  [smoke ledger](smoke.md) records dry-runs from real 0.6.4 and 0.7.5 hubs (issue
327
- #75); an applied upgrade was last proven with the 0.7.0 coordinator.
452
+ #75), an applied upgrade with the 0.7.0 coordinator, and one from a running
453
+ 0.8.1 hub with tasks and a budget pause with the 0.9.0 coordinator.
328
454
 
329
455
  The coordinator verifies and retains the exact target package, preserves its
330
456
  own source, and promotes the global CLI only after restored projects pass
331
457
  readback.
332
458
 
333
- Once the installed CLI is 0.8.1, review the current project or all registered
459
+ Once the installed CLI is 0.10.0, review the current project or all registered
334
460
  projects first:
335
461
 
336
462
  ```bash
337
463
  ahub restart --dry-run
338
- ahub upgrade --to 0.8.1 --dry-run
464
+ ahub upgrade --to 0.10.0 --dry-run
339
465
  ```
340
466
 
341
467
  Apply only after reviewing the plan:
342
468
 
343
469
  ```bash
344
470
  ahub restart --yes
345
- ahub upgrade --to 0.8.1 --yes
471
+ ahub upgrade --to 0.10.0 --yes
346
472
  ahub recovery status <operation-id>
347
473
  ahub recovery resume <operation-id>
348
474
  ahub recovery abort <operation-id>
349
475
  ```
350
476
 
477
+ The coordinator commits only once the source is quiet: no turn running, no
478
+ approval pending, no completion check queued or running. It waits up to 10
479
+ minutes and then aborts, leaving the source running; upgrade between long
480
+ checks. A 0.8.x or older source does not report completion checks or console
481
+ task commands in flight, so the coordinator cannot wait for them. Before
482
+ `--yes`, for each task whose check `hub.log` reported as "queued or running",
483
+ wait until `ahub task show <id>` has `check passed`, `check failed` or `check
484
+ finished late` after `done (checking)` (a pass with a peer reviewer writes no
485
+ line of its own to `hub.log`), and until no task command or dashboard action is
486
+ still running. A check the commit's stop kills writes to the board after the
487
+ commit has recorded it, and the operation stays blocked.
488
+
489
+ 0.9.0 turns on for every project with a config file, whether or not it has the
490
+ block: `limits` (12 messages a minute per sender, 6 per recipient, 6
491
+ `[IMPORTANT]` an hour, 120 s repeats) and `budget.wait_max_min` (30). Set them
492
+ to 0 to opt out.
493
+
494
+ 0.10.0 changes every project's local worker: its commands run under the
495
+ deny-default sandbox described above, and a toolchain outside the listed
496
+ directories needs `local.read_allow`; `"local": { "sandbox": "allow-default" }`
497
+ in `config.local.json` restores the old profile for one release. Off unless set:
498
+ `review.adaptive`, `recovery.auto_resume_after_crash` and `capabilities`. A hub
499
+ before 0.10.0 keeps no session record, so a crash of one is not reported as such
500
+ by the next start.
501
+
351
502
  The 0.7.0 transition stages the verified package and runs a retained
352
503
  coordinator from the source tree. It accepts a verified protocol-9 source and
353
504
  moves to a protocol-10 target. The source journal, queued envelopes, tasks,
@@ -369,6 +520,29 @@ Do not run an upgrade with an incompatible active protocol, an unverified
369
520
  terminal binding, or an unresolved operation lock. Dry-run performs no package,
370
521
  plugin, daemon, or terminal mutation.
371
522
 
523
+ ### After an unplanned stop
524
+
525
+ While it runs, the hub keeps each attached peer's session identity in
526
+ `.agenthub/state/sessions.json` (ids and launch options, no message text); a stop
527
+ removes it as it begins. When a hub starts and finds the file, the previous run
528
+ died (`kill -9`, a crash, a lost machine), and `ahub status` and the console say
529
+ what happened to each peer. Hubs before 0.10.0 kept no such record, so a
530
+ crash of one is not reported this way.
531
+
532
+ - Deliveries that were in flight are in `needs_review` (`ahub queue list`), as
533
+ before. When a peer next attaches, its next delivery starts with a notice that
534
+ lists them by id, sender and task (`[pii]` for a PII task), never their text.
535
+ - Kimi, Pi and the local worker run inside the hub, so they died with it. With
536
+ `"recovery": { "auto_resume_after_crash": true }` in `.agenthub/config.json` the
537
+ hub starts them again: Kimi loads its recorded session (ACP `session/load`), Pi
538
+ resumes its session file, and the local worker starts without its history, on
539
+ its recorded route (or pinned model). Off by default: the report then says
540
+ what to start. A Pi that ran in a terminal (`--mode tui`) is never started by
541
+ the hub; the report gives the command. With `pi.auto_start` and no resume,
542
+ Pi starts on a fresh session as usual.
543
+ - Codex's app-server died with the hub; run `ahub codex` again. Claude Code's
544
+ plugin reconnects by itself while that session is open.
545
+
372
546
  ## Evidence and limits
373
547
 
374
548
  The 0.6.4 release has real measurements in [the smoke checklist](smoke.md):
@@ -17,7 +17,7 @@ ahub setup # installs the Claude Code channel plu
17
17
  Or use the matching GitHub release:
18
18
 
19
19
  ```bash
20
- bun add -g github:STAIxBWLB/agent-hub#v0.8.1
20
+ bun add -g github:STAIxBWLB/agent-hub#v0.10.0
21
21
  ahub setup
22
22
  ```
23
23
 
package/docs/security.md CHANGED
@@ -10,7 +10,10 @@ agent-hub connects agents that can each run commands. This page says what the hu
10
10
  - **A committed config cannot choose launch commands, credential files, data endpoints or a wider sandbox.** The machine-local fields (`kimi_cmd`, `codex_bin`, `pi.cmd`, `checks`, `mlx.bin`, `mlx.runtimeDir`, `mlx.modelPath`, `omniroute.urls`, `omniroute.access_hosts`, the `omniroute` key files, `memory.worker_url`, `local.read_allow`, `local.bash_network`) apply only from a config file git confirms nobody committed: `.agenthub/config.json` or `.agenthub/config.local.json`, matched by file identity so no other spelling the file system accepts slips past, and `.agenthub` itself not a committed symlink or submodule. Without a repository, or when git fails, they keep their defaults; an empty value always means the default. "Untracked" is answered by the repository that contains the project: a checkout copied or extracted into an unrelated repository, or into an ignored directory of one, is trusted like your own files. So a cloned repository cannot choose a launch command, a completion check, a gateway to send a key file to, a memory endpoint, or a wider sandbox. A command in `checks` runs as you, outside the local worker's sandbox, like a git hook. Nothing in `routing.toml` or in task text is ever run. `routing.toml` and the other shared fields still come from the checkout, and they matter: `routing.toml` picks the models the local worker and the hub's inference use at your gateway and can turn the PII constraint off, and roles and budget shape who does what. Review them in a repository you do not trust.
11
11
  - **Telemetry holds no bodies.** `.agenthub/state/events.jsonl` (issue #40) records envelope ids, routing and sizes, task ids and states, overlapping paths and token counts. It never records a message body, a task title or detail, and marks private (PII) envelopes and tasks as such. It stays on the machine; `ahub export` only prints it.
12
12
  - **Snapshots stay in your repository.** Per-turn snapshots (issue #33) are git objects in the project's own object store, written through a temporary index; nothing is referenced, pushed or copied elsewhere, and `git gc` prunes them. They hold what the work tree held, including untracked files that are not ignored, so keep secrets in ignored files. They carry the repository's own permissions, and nothing caps their disk use but `git gc`. A turn of a peer holding an open PII task is not snapshotted; a PII file left in the project is snapshotted by later turns like any other file. `ahub undo` restores only files whose current content is exactly what the turn left.
13
- - **The local worker is boxed in.** Paths are resolved through symlinks and must stay inside the project; a secrets denylist (`.env*`, keys, credential files, the hub's own state) applies to its file tools, its git arguments and its memory capture alike; `.git` and `.agenthub` are not writable. Writes, edits, shell commands and mutating git wait for approval, and the approver sees what will be written or run, with control characters escaped. Everything it executes runs under the macOS sandbox, attended or not: no writes outside the project, its git dir and temp; the home directory is unreadable except toolchains; no `.git/hooks` or `.git/config` writes; no network, loopback included. Without the sandbox there is no `bash` tool.
13
+ - **The edit hook reads, never decides.** `ahub check-path --hook` (issue #32) reads hub.db and returns context for Claude and a line for you; it sets no permission decision, so your permission rules stay in charge. It names other owners' task ids, titles and states, which then reach Claude's model; PII tasks are left out.
14
+ - **The session record holds identities only.** `.agenthub/state/sessions.json` (issue #37, mode 600) keeps each attached peer's recovery metadata: launch options, session and thread ids, Pi's session file path. No message or task text; loss notices name deliveries by id, sender and public task title.
15
+ - **The local worker is boxed in.** Paths are resolved through symlinks and must stay inside the project; a secrets denylist (`.env*`, keys, credential files, the hub's own state) applies to its file tools, its git arguments and its memory capture alike; `.git` and `.agenthub` are not writable. Writes, edits, shell commands and mutating git wait for approval, and the approver sees what will be written or run, with control characters escaped. Everything it executes runs under the macOS sandbox, attended or not. Since 0.10 the profile starts from deny default (issue #39): commands run and read only the system, toolchain and project directories (and the project's git dir, the selected developer dir and temp; with network on, the public CA bundles); no writes outside the project, its git dir and temp; no `.git/hooks` or `.git/config` writes; no network, loopback included, unless `local.bash_network`. `local.sandbox: "allow-default"`, machine-local, brings back the profile of 0.9 and earlier for one release. The user's temp dir is still readable in both: a command can read another tool's leftovers there. Without the sandbox there is no `bash` tool.
16
+ - **Capabilities are enforced, not suggested.** `capabilities` (issue #39) is checked by the daemon where task operations and messages arrive, so a peer cannot get round it by phrasing. A peer can never answer a permission request: the control link takes `permit` from the console role only, and a message that quotes a permit command is just text. Both bind the hub's own tool paths: a vendor agent with its own shell in the project (Codex, Claude, Kimi) can read `.agenthub/state/control-token` and connect as the console, which only the local worker's and Pi's sandbox prevents.
14
17
  - **PII has an enforced path.** A task matching `signals.pii_patterns` goes to `local` or to nobody; its text is absent from other peers' envelopes, the console stream, the log and the board listing; the console user reviews it; `local` answers such a turn to the console only, keeps it out of its history, refuses it when the only gateway is off campus, and may not save notes or spin off tasks during it. Nothing about it is sent to claude-mem, whose observer is a cloud model.
15
18
  - **Secrets stay where they are read.** The gateway key and Cloudflare Access values are read inside the gateway client and go only into request headers: never into logs, errors, envelopes, tool output, memory, or the generated Switchyard config (the key travels by environment variable name). Subscription logins of Claude, Codex and Kimi are never proxied or pooled.
16
19
  - **The hub's own model calls are fenced.** Digest condensation and task triage read agent-written text as data; their output is capped text framed as untrusted, or a value checked against a closed list. It is never used as a route, a peer id, a tool call or an instruction.
package/docs/smoke.md CHANGED
@@ -883,3 +883,57 @@ peers attached.
883
883
  - The release gate for v0.8.0 (Actions run 36807880478) failed once on
884
884
  "removing a running project is refused"; the rerun passed and published. Cause: `ahub kill` returned before
885
885
  the hub released its registry claim. Fixed in 0.8.1 (#58).
886
+
887
+ ## Edit conflict detection (issue #32, 2026-10-01)
888
+
889
+ - AC3, on a clone of this repository (156 tracked files) with 10 open tasks of
890
+ other owners holding 200 touches: the turn-end path (end snapshot, diff,
891
+ touch query, match) takes 57.6 ms median, 59.6 ms max; the detection part
892
+ alone (diff, query, match) 7.1 ms median. The machine was under load from
893
+ other work; the snapshot alone measured 17 ms median earlier the same day.
894
+ - AC4, Claude Code 2.1.286 headless (`claude -p --settings <hook settings>
895
+ --permission-mode acceptEdits`, model Haiku 4.5) in a scratch repository whose
896
+ hub.db had kimi's open task claiming `notes.txt`, asked to edit `notes.txt` and
897
+ quote any agent-hub note:
898
+ - Runs 2 and 3: the hook fired (logged input and output in run 2), Claude
899
+ quoted the warning verbatim, and the edit went through under the session's
900
+ permission mode.
901
+ - Run 1, before the hook was logged: Claude answered NONE. Whether the hook
902
+ did not fire or the model left the reminder out was not determined; that run
903
+ also had no stdin redirect (`< /dev/null`), which runs 2 and 3 had.
904
+
905
+ ## Recovery after an unplanned stop (issue #37, 2026-10-01)
906
+
907
+ - AC1 baseline, released 0.7.11 in a scratch project, the fake ACP agent standing
908
+ in for the Kimi binary (`kimi_cmd`), `kill -9` of the daemon:
909
+ - the ACP child exited with the daemon;
910
+ - `ahub status` failed to reach the stale manifest's port;
911
+ - `ahub up` started a hub with no peers attached; nothing recorded Kimi's
912
+ session, so it could only start a new one.
913
+ - The same run with this branch and `auto_resume_after_crash` on: `ahub up`
914
+ reported the crash and `kimi resumed: ... session s1 ... (ACP session/load)`,
915
+ and Kimi was idle on its recorded session id.
916
+ - Pending, needs real accounts: Kimi 2.x (does it offer `loadSession`, and does
917
+ the resumed session keep its context), Pi with a real session file, and Codex
918
+ and Claude reattachment after `kill -9`.
919
+
920
+ ## 0.8.1 to 0.9.0 attended upgrade (2026-10-01)
921
+
922
+ A scratch project (a git work tree whose `.agenthub/config.json` was written by
923
+ the 0.8.1 `ahub init`) ran a 0.8.1 hub with two proposed tasks, one with a path
924
+ and a detail, and an open budget pause: a peer had attached once and gone
925
+ offline, and `ahub budget set` fed it a 95% reading. No completion check was
926
+ configured or running, and no peer was attached at the upgrade.
927
+
928
+ - `bunx --package @staix/agent-hub@0.9.0 ahub upgrade --to 0.9.0 --dry-run`
929
+ exited 0: one project, source 0.8.1, protocol 10, no blockers.
930
+ - `--yes` scheduled the operation, which completed in about 10 s with the
931
+ project `verified`.
932
+ - After release the hub ran 0.9.0 under a new instance id. Both tasks were on
933
+ the board unchanged, now with `deps: []` (the column the target adds on open),
934
+ the budget pause kept its reset time, and the `outcomes` table was created.
935
+ - The coordinator promoted the global CLI to 0.9.0, and `ahub setup --yes`
936
+ installed the 0.9.0 plugin.
937
+ - For about two minutes after the publish step logged `+ @staix/agent-hub@0.9.0`,
938
+ `npm view` still showed `latest: 0.8.1`; the registry caught up without any
939
+ action.
@@ -921,3 +921,174 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
921
921
  empty plan on accept keeps the plan there is. The completed-change notice
922
922
  leaves out any file, signature or summary line that matches a PII pattern.
923
923
 
924
+ ## Amendment: early conflict detection (issue #32)
925
+
926
+ - Shared tree only. The issue's worktree mode (pairwise `git merge-tree` between
927
+ per-agent worktrees, AC2) moves to #41, which provides those worktrees.
928
+ - Attribution reuses the #33 turn snapshots: the files a turn changed are
929
+ recorded as `touches` (task, peer, path) in hub.db for each task the peer has
930
+ in progress. A later turn by another peer that changes one of those files, while
931
+ the task is open, is a conflict. A file touched by one agent only is not, even
932
+ when its task changed hands. PII tasks are left out on both sides, and a turn of
933
+ a peer with a PII task in progress is not checked.
934
+ - Notices are normal-priority `task` envelopes to both owners, once per (peer,
935
+ task, file) per hub run, plus a console line and a `conflict` event.
936
+ "Concurrent" means another peer had a turn open during this one.
937
+ - The PreToolUse hook template runs `ahub check-path --hook`, which reads hub.db
938
+ read-only and answers with `hookSpecificOutput.additionalContext` and a
939
+ `systemMessage`, never a `permissionDecision`. Claude Code adds that context
940
+ with the tool result, so it warns after the edit is allowed, not before.
941
+ - After review: a turn's touches leave out files an overlapping turn of another
942
+ peer changed (#33's `Turns.overlapping`), and none are recorded while such a
943
+ turn's changes are unknown; detection runs after the `turn_end` event; the
944
+ hook quotes task titles as JSON strings and says they are agents' text; a
945
+ file name that matches a PII pattern is never named, through the same check as
946
+ overlap mentions (#31): notices and the console line count such files, the
947
+ event leaves them out.
948
+
949
+ ## Amendment: task dependencies (issue #34)
950
+
951
+ - `deps` is a JSON column of task ids, set from `after` when a task is proposed
952
+ and never changed. It may name only tasks that exist, so the new task closes no
953
+ cycle (nothing can depend on it yet); the issue's cycle check at propose holds
954
+ by construction, and unknown ids are refused.
955
+ - A task waits while any dependency is not approved. Waiting is an input to
956
+ `assign()` (`waitsFor`), which then returns no owner and says why, so `ahub
957
+ route explain` and assignment agree. Accept and done refuse a waiting task,
958
+ for the console user too.
959
+ - A proposal that names an owner (a claim, or an owner for someone else) and
960
+ would wait is refused: the owner of waiting work is chosen when it is ready,
961
+ by routing or by `ahub task assign`.
962
+ - Approval, by review or by a class without a reviewer, releases the
963
+ dependents that wait for nothing else; each is recorded as `ready` and assigned.
964
+ A proposal re-reads what it waits for after its insert, since triage may have
965
+ awaited an approval. An approval is saved before its dependents are assigned,
966
+ so the daemon's 60 s release timer sweeps for ownerless tasks whose last event
967
+ is `blocked` or `ready` and that wait for nothing: each is offered once per hub
968
+ run, once an attached peer can take it, outside recovery operations.
969
+ - A recovery commit also waits for task operations in flight and for completion
970
+ checks queued or running: both can write the board after its integrity digest
971
+ (a check the commit's stop kills records `check interrupted`).
972
+
973
+ ## Amendment: quota-aware routing, wait or hand off, demotion (issue #36)
974
+
975
+ - `assign()` stays pure: quota (per peer, the headroom of the most used fresh
976
+ window and that window's reset, from `Budget.headroom()`), the clock and the
977
+ demotion weights are inputs. Demoted peers go behind the others (owner role
978
+ only; local and Pi lose their place ahead of the cloud peers too); within each
979
+ group, peers with readings swap places by headroom per hour to reset (at least
980
+ 15 min), and peers without readings keep their configured place. Idle still
981
+ comes before busy, so demotion orders peers in the same state.
982
+ - Wait or hand off is decided at handoff time from the pause record's reset:
983
+ within `budget.wait_max_min` the handoff moves only tasks whose signals include
984
+ `urgent` (set by `hub_task_propose {urgent: true}`), and the pause reason says
985
+ so; beyond it the reason says the work was handed over and why. A wait is
986
+ undone when a later reading moves the reset past the limit: the record is
987
+ unmarked and the next tick hands over the rest, keeping both lists of moved
988
+ tasks. `DEFAULT_BUDGET` has it off, like approvals.notify; any project config turns it
989
+ on at 30 unless it sets another value.
990
+ - Outcomes (`peer`, `class`, ok, time) are recorded in hub.db and kept a week:
991
+ approved counts for the owner, by review or done without a reviewer; changes
992
+ requested, a failed check and an escalation by hand count against it (the
993
+ hub's own escalations are not counted: after repeated changes requested each
994
+ one already was, and after a Pi inference failure the backend failed). Demotion: decayed failures reach 1.5 and outweigh decayed
995
+ successes, half-life one day, fixed constants for now.
996
+
997
+ ## Amendment: per-sender limits (issue #38)
998
+
999
+ - One `Limiter` (`src/hub/limits.ts`) serves both ways an agent sends: the
1000
+ control WS `send` (hub_send from Claude, and from Codex and Kimi through the
1001
+ tools server) refuses with the reason in `error`, which the channel already
1002
+ shows, so the message shape is unchanged; the bus's `admit` option guards
1003
+ adapter messages (turn answers, the local worker's hub_send), drops a refused
1004
+ one and tells the sender in a ride-along line.
1005
+ - Both admit the envelope as it will be sent: a reply without `to` goes to its
1006
+ parent's sender, `digest` is resolved to the senders it replaced, and the
1007
+ priority is capped. The repeat key includes the id of what the message
1008
+ answers. `fyi` is never limited: it reaches no peer.
1009
+ - An `important` hub_send through the control WS (Claude, and Codex and Kimi
1010
+ through the tools server) over its budget is refused, not downgraded, and the
1011
+ reason says to send it without `[IMPORTANT]`. On the bus path (turn answers,
1012
+ and the hub_send of Pi and the local worker) there is no caller to refuse:
1013
+ over its important budget a message is lowered to status and delivered, and
1014
+ the sender is told.
1015
+ - A refusal consumes no token and does not count as a send for repeat
1016
+ suppression. Limits are off in `DEFAULT_CONFIG` and on with any project config
1017
+ (12/min per sender, 6/min per recipient, 6 important an hour, 120 s repeats).
1018
+
1019
+ ## Amendment: review checklists and outcomes (issue #35)
1020
+
1021
+ - A review request carries a checklist: map the changed signatures and call
1022
+ sites to the task's plan, or without a plan to the task detail, which the
1023
+ request then includes; the result of the class's check, or that none ran; and
1024
+ `hub_review`'s `unmet`, one item each, which is appended to the verdict note.
1025
+ - Review outcomes live in a `reviews` table in hub.db: implementer, reviewer,
1026
+ class, kind, task, time. Kinds: `approved`; `caught` (each reviewer who asked
1027
+ for changes on the current owner's work, which was then approved);
1028
+ `contradicted` (an approval, within seven days, of a task on the same file or
1029
+ symbol as one whose check failed or whose review asked for changes, a
1030
+ directory or `.` not counting; once per approval; console approvals and PII
1031
+ tasks are not judged); `escalated` (the reviewer had asked for changes on the
1032
+ work that was escalated, so not an escalation of unreviewed work).
1033
+ - A reviewer's record with an implementer in a class counts tasks: n = the tasks
1034
+ it reviewed (approved, caught or escalated), held = (n - contradicted tasks) /
1035
+ n. `assign()` takes the records as input and always shows them in its trace;
1036
+ with `review.adaptive` it orders reviewer candidates that have at least
1037
+ `min_reviews` by held, others keep their place, and the implementer is never
1038
+ a candidate. The record is applied after quota, so the order is idle before
1039
+ busy, then the record, then quota.
1040
+
1041
+ ## Amendment: recovery after an unplanned stop (issue #37)
1042
+
1043
+ - The continuous record is `sessions.json` (instance id, time, and each attached
1044
+ peer's `recoveryMetadata()`), rewritten when a peer's state changes and
1045
+ removed when a stop of the run that wrote it begins (a stop that then runs past
1046
+ the shutdown deadline is still not a crash). Found at start, with no
1047
+ controlled-restart state in play, it means the previous run crashed; the new
1048
+ run takes the record over at once, so its own clean stop removes it even when
1049
+ no peer attaches. A controlled restart's target removes any record it finds.
1050
+ - Limits: a second crash before the peers attach loses their loss notices (the
1051
+ journal rows stay in `needs_review`, shown by `ahub queue list`), and the first
1052
+ attach rewrites the record with only the attached peers. A Pi in TUI mode is
1053
+ reported with its command, not resumed: the CLI runs its terminal.
1054
+ - Resume goes through the same start path as `ahub kimi` / `ahub pi` / `ahub
1055
+ local`: Kimi with `sessionId` (ACP `session/load`, refused when the agent does
1056
+ not offer `loadSession`), Pi with its session file, the local worker afresh.
1057
+ Codex and Claude are reported, not resumed: the TUI and the Claude session live
1058
+ outside the hub. The issue's Codex `thread/resume` needs the TUI, so the report
1059
+ names the thread instead.
1060
+ - Loss notices use the bus preface, not an envelope: a peer's `needs_review`
1061
+ rows block its later deliveries, so a notice queued behind them would arrive
1062
+ only after they are resolved anyway; the preface leads that next delivery. Only
1063
+ rows still in `needs_review` when the peer attaches are listed.
1064
+ - `recovery.auto_resume_after_crash` is off by default, also with a project
1065
+ config: an automatic start spends quota the user did not ask for.
1066
+
1067
+ ## Amendment: deny-default sandbox and capabilities (issue #39)
1068
+
1069
+ - The deny-default profile allows exec and reads of the system directories
1070
+ (`/usr`, `/bin`, `/sbin`, `/System`, `/Library`, `/opt`, `/private/etc`, the
1071
+ dyld and timezone databases), the toolchain directories in home, `read_allow`,
1072
+ the project, its external git dirs, the selected developer dir (`xcode-select
1073
+ -p`) and temp; writes as before; a short list of mach services (directory
1074
+ lookups, logging, notifications), plus name resolution and TLS trust when
1075
+ network is on, the trust being the public CA bundles allowed by exact path
1076
+ after the denies (the `*.pem` key deny matches them); no brokers that act
1077
+ outside the sandbox (LaunchServices, SecurityServer). The denies at the end (credential
1078
+ stores, the denylist, `.agenthub`, git hooks and config) are shared by both
1079
+ bases.
1080
+ - The issue's allowlist proxy for `local.bash_network` is not built here: with
1081
+ the flag on, network is allowed as in 0.9 and earlier. It is left for a
1082
+ follow-up issue.
1083
+ - `local.sandbox` ("deny-default" | "allow-default") is machine-local, since
1084
+ "allow-default" widens the sandbox.
1085
+ - Capabilities: `propose`, `assign` (a proposal naming another peer as owner),
1086
+ `remember`, `important`. A peer not listed in `capabilities` has all of them,
1087
+ which keeps today's behaviour, and a listed peer whose value is not a list has
1088
+ none; the console user and the hub are never limited. `remember` gates the
1089
+ `hub_remember` tool only, not the notes the hub keeps of done summaries and
1090
+ review verdicts. Every refusal, a malformed entry and an unknown capability
1091
+ name are logged.
1092
+ `important` is checked where messages are admitted, the others in the task
1093
+ operations, so in-process tools (the local worker, Pi) are covered too.
1094
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staix/agent-hub",
3
- "version": "0.8.1",
3
+ "version": "0.10.0",
4
4
  "description": "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-hub",
3
- "version": "0.8.1",
3
+ "version": "0.10.0",
4
4
  "description": "Channel between Claude Code and the agent-hub daemon: peer messages from Codex, Kimi and the local worker arrive as channel events; hub_send replies.",
5
5
  "author": {
6
6
  "name": "Young Joon Lee",