@staix/agent-hub 0.12.45 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +83 -116
  3. package/docs/agent-notes/README.md +6 -0
  4. package/docs/agent-notes/adapters.md +10 -7
  5. package/docs/agent-notes/budget.md +6 -1
  6. package/docs/agent-notes/bus.md +6 -4
  7. package/docs/agent-notes/daemon.md +6 -1
  8. package/docs/agent-notes/local-worker.md +2 -1
  9. package/docs/agent-notes/tasks.md +10 -0
  10. package/docs/agent-notes/tests.md +4 -2
  11. package/docs/dogfood.md +92 -0
  12. package/docs/events.md +14 -1
  13. package/docs/operations.md +250 -19
  14. package/docs/profiles.md +2 -2
  15. package/docs/quickstart.md +8 -2
  16. package/docs/sdlc.md +36 -6
  17. package/docs/security.md +20 -11
  18. package/docs/smoke.md +9 -4
  19. package/docs/specs/2026-09-19-agent-hub-design.md +759 -8
  20. package/docs/verification/2026-10-19-reviewer-isolation.md +2 -0
  21. package/docs/verification/2026-10-22-code-facts.md +78 -0
  22. package/docs/verification/2026-10-22-codex-reviewer-isolation.md +47 -0
  23. package/docs/verification/2026-10-22-kimi-qwen-compat.md +79 -0
  24. package/docs/verification/2026-10-23-0.13.0.md +89 -0
  25. package/docs/verification/dogfood/2026-10-22.md +3109 -0
  26. package/docs/verified.json +14 -14
  27. package/package.json +1 -1
  28. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  29. package/plugins/agent-hub/server.js +613 -329
  30. package/src/adapters/agy/entry.ts +5 -4
  31. package/src/adapters/claude-channel.ts +20 -9
  32. package/src/adapters/codex-appserver.ts +18 -2
  33. package/src/adapters/codex-review-isolation.ts +12 -5
  34. package/src/adapters/compat.ts +140 -0
  35. package/src/adapters/definitions/builtin-metadata.ts +2 -1
  36. package/src/adapters/definitions/native-cli-doctor.ts +12 -5
  37. package/src/adapters/definitions/native-cli-entry-jcode.ts +9 -6
  38. package/src/adapters/definitions/native-cli-jcode-mcp.ts +9 -2
  39. package/src/adapters/definitions/native-cli-preview-common.ts +2 -0
  40. package/src/adapters/definitions/native-cli-preview-jcode.ts +8 -2
  41. package/src/adapters/definitions/native-cli-preview-qwen.ts +2 -1
  42. package/src/adapters/definitions/qwen-native.ts +7 -3
  43. package/src/adapters/definitions/qwen.ts +2 -1
  44. package/src/adapters/jcode.ts +72 -31
  45. package/src/adapters/kimi-review.ts +6 -3
  46. package/src/adapters/registry-types.ts +3 -1
  47. package/src/adapters/registry.ts +1 -0
  48. package/src/cli/agents.ts +40 -0
  49. package/src/cli/console-review.ts +54 -0
  50. package/src/cli/console-state.ts +124 -29
  51. package/src/cli/console-symbols.ts +74 -0
  52. package/src/cli/console-usage.ts +89 -0
  53. package/src/cli/console.ts +26 -1
  54. package/src/cli/help.ts +16 -6
  55. package/src/cli/identity.ts +29 -6
  56. package/src/cli/init.ts +109 -11
  57. package/src/cli/main.ts +122 -11
  58. package/src/cli/output.ts +44 -1
  59. package/src/cli/preview.ts +1 -1
  60. package/src/cli/sdlc-release-guard.ts +82 -0
  61. package/src/hub/action-class.ts +362 -0
  62. package/src/hub/action-policy.ts +106 -0
  63. package/src/hub/board.ts +43 -8
  64. package/src/hub/budget.ts +71 -2
  65. package/src/hub/bus.ts +43 -3
  66. package/src/hub/checks.ts +80 -31
  67. package/src/hub/code-facts.ts +223 -0
  68. package/src/hub/config-apply.ts +10 -1
  69. package/src/hub/config-trust.ts +7 -4
  70. package/src/hub/daemon.ts +237 -22
  71. package/src/hub/dogfood-read.ts +199 -0
  72. package/src/hub/dogfood.ts +163 -0
  73. package/src/hub/envelope.ts +1 -1
  74. package/src/hub/events.ts +11 -2
  75. package/src/hub/hub-tools.ts +24 -2
  76. package/src/hub/inbox-polling.ts +25 -0
  77. package/src/hub/instances.ts +73 -1
  78. package/src/hub/profiles.ts +7 -2
  79. package/src/hub/release-worktree.ts +56 -0
  80. package/src/hub/review-brief.ts +36 -0
  81. package/src/hub/sdlc-queue.ts +31 -9
  82. package/src/hub/sdlc-release-authorization.ts +173 -0
  83. package/src/hub/sdlc.ts +49 -11
  84. package/src/hub/settings.ts +5 -2
  85. package/src/hub/snapshots.ts +93 -8
  86. package/src/hub/symbol-normalizer.ts +26 -0
  87. package/src/hub/task-code-symbols.ts +90 -0
  88. package/src/hub/task-sweep.ts +3 -2
  89. package/src/hub/tasks.ts +292 -54
  90. package/src/hub/test-integrity.ts +162 -0
  91. package/src/hub/usage-rollup.ts +361 -0
  92. package/src/local/sandbox.ts +5 -3
  93. package/src/ui/index.html +1 -0
  94. package/templates/AGENTS.block.md +1 -1
  95. package/templates/profiles/dev.json +6 -1
  96. package/templates/profiles/general.json +6 -1
  97. package/templates/profiles/research.json +6 -1
  98. package/templates/profiles/writing.json +6 -1
  99. package/templates/release-guard.block.md +6 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
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
+ ## Unreleased
6
+
7
+ ## 0.13.0
8
+
9
+ - Add the #636 action-policy foundation: a pure classifier, machine-local always-ask configuration, session-bound approval budgets and explicit per-peer enforcement status. Adapter pre-tool gates remain pending; status reports that limit instead of claiming protection.
10
+ - Person-only release approval binds the candidate commit, version and fresh docs; an opt-in init-written Git tag pre-push guard refuses missing or mismatched approval and preserves existing hooks (#481 S6).
11
+ - Bug-labelled tasks gain source-labelled red-first receipts and PR test-integrity readbacks. Enforce blocks review and approval when proof is missing/unknown or a pre-existing test is modified/deleted; advise shows the findings. Status and merge queues retain private metadata screening (#481 S4).
12
+ - Version policy for native agent CLIs (#691): one verified/in-line/trusted/refused decision in `src/adapters/compat.ts` for the Codex and Kimi reviewers, Jcode, agy and Qwen; `ahub agents [--json]` shows it and the person-only `ahub agents trust <id> <version> [--revoke]` accepts an untested version on this machine; refusals name the version and the trust command; a weekly workflow reports upstream drift. Receipts add Codex reviewer 0.163.0 (#697), Kimi reviewer 2.3.0 and Qwen Code 0.26.0 (#714).
13
+ - Extra Jcode instances with their own home, socket and state; hub tools are bound to an owned launch (#685).
14
+ - Usage: ledger events for Codex usage breakdown, budget pause/resume and derived quota resets (#683 U1); an incremental roll-up in `hub.db`, the `usage` control op and `ahub usage [limits|total]` (U2); the console Usage panel (tab `7`) with windows, pauses, last resets and tokens per peer and per selected task (U3).
15
+ - Code structure (#682, #684): optional bounded code facts from ripwire 0.6.7 in the review brief (observed symbols, unknown change kinds, deleted definitions not covered); head-bound task symbol observations kept apart from the task row and dropped on a head, repository or privacy change; the console task detail shows changed symbols (matching, unplanned, untouched) and screened overlaps, and a completed-change notice says `same file, different symbols` only on complete disjoint evidence; plan symbols match by normalized name across overlap notices, grooming and the review brief (#682 phase 1b).
16
+ - SDLC: the hub links lifecycle issue and PR identity from what it can verify (#694); test integrity for bug fixes with red-first receipts and modified pre-existing test detection (#481 S4); `ahub report --dogfood`, a bounded weekly scorecard of real use (#687), with CI flow measured per recorded PR head (#704); a profile-selected review checklist and hub tool descriptions (#507).
17
+ - Console review guidance (#711): `r` on a task in review asks `a approve` or `c request changes`, shows a guide line, refuses an empty changes note and shows the result; an agent shell refused a person-only action is told why; the user-reviewed done notice names the console path.
18
+ - Pull-only peers catch up (#713): `hub_inbox` drains up to 64 KiB oldest first and says how many are waiting; completed-change notices are one per recipient per done, a newer one supersedes the queued one, and none go to unaccepted work; a queued delivery can be discarded while its peer is busy; refusals name their cause; the person's `ahub queue discard --peer <id> --reason <text> [--yes]` and the console Queue `D` clear a backlog. Empty pull-only polling is detected (#681).
19
+ - Fixes: a check's exit and pipes are read through Bun.spawn so an early exit is not lost (#558); snapshot attribution survives own-commit rebases and long reflogs (#612); readiness respects the reserved owner's WIP cap (#655); host timeout kill markers are preserved in the sandbox (#700); the Codex reviewer disables legacy notify (#697).
20
+ - Retired as announced (#481): `sdlc.workflow` (gsd-lite) no longer loads, and the `phase` receipt name is refused; record a `criteria` checklist with `hub_sdlc_record` instead. `local.bash_network: "direct"` still works and still warns; its removal moves to a later release.
21
+ - Releases: a release with any feature bumps the minor version; a fix-only release bumps the patch.
22
+
5
23
  ## 0.12.45
6
24
 
7
25
  The dashboard is redesigned (#469), a machine-local, content-pinned adapter module can add an ACP agent (#451 A4), the peer lists and the CLI surface come from the adapter definitions (#625), and reviews gain structured findings with an Important gate (#481 S3) and plan conformance with a person-only departure acceptance (#481 S2).
package/README.md CHANGED
@@ -1,139 +1,106 @@
1
1
  # agent-hub
2
2
 
3
- A hub on one developer's machine where coding agents work as a team: Claude Code,
4
- Codex, Kimi Code, Pi and a hub-owned local worker (Qwen Code, Jcode and Antigravity
5
- optional) share a message bus and a task board per project and one memory on the
6
- machine, each attached through its own native interface.
3
+ A hub on your own machine where coding agents work together as one team. Claude
4
+ Code, Codex, Kimi Code, Pi and a hub-owned local worker (plus Qwen Code, Jcode and
5
+ Antigravity if you want them) share a message bus, a task board and a memory, each
6
+ connected through its own native interface.
7
7
 
8
- Status: 0.12.45, control protocol 16. Bun + TypeScript, one loopback daemon per
8
+ Status: 0.13.0, control protocol 16. Bun + TypeScript, one loopback daemon per
9
9
  project. Site: [agent-hub.staix.net](https://agent-hub.staix.net).
10
10
 
11
- ## What it does today
12
-
13
- - **Peers, not a pipeline.** Every agent can message the others, and the hub
14
- delivers to each through its native surface (Claude channel plugin, Codex
15
- app-server, ACP, Pi RPC or TUI). Messages are batched into digests, priorities
16
- decide what interrupts a turn, and delivery is durable across restarts.
17
- - **A shared task board.** Tasks are proposed, accepted, done and reviewed through
18
- hub tools. `routing.toml` assigns owners by task class, roles split planning,
19
- implementation and review, and a designated planner can be changed live.
20
- Cause-deduplicated stalls and unread pull-only offers need attention; cancellation
21
- closes work without approving it or releasing its dependents. Owners of
22
- overlapping work in the same repository are told what changed.
23
- - **Shared memory.** Decisions, contracts, fails and findings go to the hub's own
24
- store through the exact-pinned first-party `@staix/agent-mem` library (short-
25
- and long-term tiers, a typed graph, git sync between machines),
26
- next to optional claude-mem capture. Entries belong to their project unless
27
- saved as shared, which makes them readable from every project on the machine.
28
- Agents get memory as a recall preface, as briefs on a task, and through bounded
29
- `hub_memory_search`, `hub_memory_show` and `hub_memory_graph` tools. Agents
30
- can propose corrections; candidates stay outside evidence until a person acts.
31
- - **Local and remote models.** Pi and the local worker run on DGX or local MLX
32
- through an authenticated relay; in-process hub routes select models by task
33
- through a self-hosted OmniRoute gateway. Tasks flagged as PII stay on the on-prem
34
- worker.
35
- - **Guard rails.** The local worker and Pi run in a sandbox, and by default their
36
- writes and shell commands wait for the person's approval; a permission mode
37
- the person picks (`ask-when-needed`, `never-ask`) skips some or all prompts.
38
- Budgets pause a peer before it runs out of quota, and recovery never replays
39
- an effect whose outcome is unknown.
40
- - **Operations.** A terminal console and a local dashboard, one hub per
41
- repository or worktree with a machine-wide view, and `ahub upgrade` that carries
42
- running sessions across versions. Settings reload without a restart where their
43
- apply class allows; guarded values wait for a person's confirmed
44
- `ahub settings reload` in a terminal. The console supports revision-checked task
45
- assignment/unassignment, delivery-queue resolution, per-peer planner WIP limits
46
- and backlog management from the Tasks tab. Peers with independent
47
- tool turns can request one uncertain redelivery; the delivery-only local worker
48
- and headless ACP peers leave held deliveries for the person to resolve.
11
+ ## Quick start
49
12
 
50
- ## Where it is going
51
-
52
- - **Curated memory** ([#412](https://github.com/STAIxBWLB/agent-hub/issues/412)).
53
- The memory core now lives in [agent-mem](https://github.com/STAIxBWLB/agent-mem)
54
- (`@staix/agent-mem`, with the `amem` CLI), which agent-hub uses as its one
55
- exact-pinned first-party dependency. Next, memory becomes curated rather than
56
- captured: compression, removal, correction
57
- and failure-driven management, each shipped only after its contract and a
58
- measurement.
59
- - **Every peer able to do its share** ([#418](https://github.com/STAIxBWLB/agent-hub/issues/418)).
60
- Bounded task-spec reads and opt-in isolated checks are implemented. Installed
61
- managed-Pi verification (AC8) remains open; source and CI results do not certify
62
- that live runtime.
63
- - **Measured, not assumed.** Add-ons and memory are kept or dropped by controlled
64
- comparisons ([#248](https://github.com/STAIxBWLB/agent-hub/issues/248),
65
- [#258](https://github.com/STAIxBWLB/agent-hub/issues/258)).
13
+ You need [Bun](https://bun.sh) 1.4.2 or later on PATH.
66
14
 
67
- ## Install
15
+ ```bash
16
+ bun add -g @staix/agent-hub && ahub setup
17
+ cd <your project> && ahub init && ahub up # daemon plus the operator console
18
+ ```
68
19
 
69
- Requires [Bun](https://bun.sh) 1.4.2 or later on PATH.
20
+ Then start the agents you use, one terminal each, and talk to them:
70
21
 
71
22
  ```bash
72
- bun add -g @staix/agent-hub && ahub setup
73
- cd <your project> && ahub init && ahub up
23
+ ahub claude # Claude Code with the hub channel
24
+ ahub codex # Codex TUI, attached through the hub
25
+ ahub say "Each of you: reply with your name."
26
+ ahub ui # the web dashboard
74
27
  ```
75
28
 
76
- `ahub init --dry-run --json` shows what init would write first. While hubs run,
77
- `ahub upgrade` moves them to the latest release.
78
-
79
- After upgrading from 0.12.30 or earlier, run `ahub jcode --mode headless` once in each
80
- project that uses Jcode to refresh its owned MCP entry before conductor or dashboard
81
- starts of Jcode. The entry's format is unchanged in 0.12.37; a daemon start that still
82
- reports the entry missing or stale is fixed by the same command, which since 0.12.36
83
- writes the running hub's own plugin bundle (#502). Since 0.12.37 the hub accepts jcode
84
- 0.98.0 or later, below 1.0.0, and no longer 0.94.0 (#529).
85
- Since 0.12.39 `ahub jcode` opens Jcode's native TUI on the hub-owned ACP session by
86
- default (#464); `--mode headless` or `ahub settings set peers.jcode.start_mode headless`
87
- keeps headless startup. On 0.12.39 the TUI launcher stopped after the first hub turn;
88
- 0.12.40 opens it again (#564). On 0.12.40 `ahub codex` in a recorded Orca terminal
89
- failed with `invalid remote address`; 0.12.41 starts it again (#586). Since 0.12.42
90
- `ahub agy` accepts the Antigravity CLI 1.4.x line (1.4.2 verified; other patches warn),
91
- because agy updates itself and 0.12.41 refused 1.4.3 (#589). Since 0.12.43 agy's native reads are recorded as peer facts on 1.4.2 and 1.4.3 (#555); 1.4.3 stays unverified, and in its TUI the hub tools appear from the second turn (#267, #607). Since 0.12.44 a headless Kimi or Qwen start made by the hub also opens `ahub watch <peer>` through the terminal provider (#286), an owner turn streak with no recorded writes raises a planner notice (`task_sweep.no_writes_turns`, default 2, 0 disables; #498), a declared agy extra instance is refused at config load (#625), and a standard ACP agent can join a project as a config-only adapter declared in untracked `.agenthub/config.local.json`: headless, off roster by default and pinned to its native identity (#451). Since 0.12.45 the dashboard has a sidebar or tabs, view keys `1`-`7`, a task side sheet, a Plan sheet for the board forms and loading, empty and disconnected states (#469); a person can add a bundled ACP adapter module, pinned by `sha256` in untracked `.agenthub/config.local.json` and imported only by the daemon at startup, and module CI gets the `@staix/agent-hub/conformance` kit (#451); reviews take structured findings, and in SDLC enforce an open Important finding or a changed file outside the plan refuses approval until fixed, amended or accepted by the person with `ahub sdlc accept-departure` (#481); and `ahub roster remove` deletes the roster key of an instance no longer declared (#225).
92
- Cancellation needs a daemon advertising `task_cancel`; controlled downgrades
93
- refuse while cancelled rows exist. Live-test delegated CLI commands need a daemon
94
- advertising `cli_audit_delegated` and refuse with an upgrade remedy otherwise
95
- (#410). For local/Pi implementation, opt in to an available
96
- `checks.runner = "container"`; issue-backed owners also need authenticated
97
- spec access. PII work and designated reviewers retain their separate exemptions.
98
- See [release verification](docs/verification/2026-10-22-0.12.45.md) for the live legs
99
- still open, including #418 AC8, #438 AC7, the native console settings leg (#439 AC5),
100
- real upgrades with reattach (#463) and with a managed terminal and cancelled tasks
101
- (#478), a live headless Claude review (#223), a Claude session over 32 MiB (#517), a
102
- live fresh Codex review (#519), a live split of a real memory home (#536), the live
103
- agy TUI/account leg (#485, hub task #251), Kimi's interactive-TUI coexistence and
104
- usage checks (#533), Jcode's TUI with a personal account, a real provider and hub-tool
105
- calls (#464), a live Jcode tool call held past the inactivity watchdog (#542), a live
106
- steer of a busy Jcode turn (#572) and a real upgrade that exercises the terminal owner
107
- record (#482).
108
-
109
- Before migrating a memory home with `amem`, upgrade every hub using that home to
110
- 0.12.30 or later, check `ahub memory profile-check`, then quiesce and close all memory
111
- consumers. Shared v1 remains the default; installing this release does not migrate
112
- data. The curated shard migration (`amem migrate --shards`, agent-mem 0.4.0) needs
113
- every hub using that home to run 0.12.37 or later, which opens memory through shards
114
- (#534). An older hub (0.12.36 or earlier, agent-mem 0.3.1) does not detect the
115
- split: it keeps reading and writing the old layout, and its writes reach the shards
116
- only through `amem migrate --shards --resume`. Upgrade or stop every such hub before
117
- splitting. `ahub memory profile-check` is split-aware (#536). See [memory profiles and rollout](docs/memory.md#selected-memory-profile).
29
+ `ahub init --dry-run --json` shows what init would write. The
30
+ [Quickstart](docs/quickstart.md) walks through a first session and a working day.
31
+
32
+ ## What you get
33
+
34
+ - **Peers, not a pipeline.** Any agent can message the others. Messages are batched
35
+ into digests, priorities decide what interrupts a turn, and delivery survives
36
+ restarts.
37
+ - **A shared task board.** Work is proposed, accepted, done and reviewed through hub
38
+ tools. Roles split planning, implementation and review, `routing.toml` picks
39
+ owners by task class, and owners of overlapping work are told what changed.
40
+ - **Shared memory.** Decisions, contracts, fails and findings are kept through the
41
+ first-party [`@staix/agent-mem`](https://github.com/STAIxBWLB/agent-mem) library
42
+ (short- and long-term tiers, a typed graph, git sync between machines). Agents get
43
+ it as a recall preface, as briefs on a task, and through bounded search tools.
44
+ - **Local and on-prem models.** Pi and the local worker can run on DGX or local MLX
45
+ through an authenticated relay. Tasks marked as PII stay on the on-prem worker.
46
+ - **Guard rails.** The local worker and Pi run in a sandbox, and their writes and
47
+ shell commands wait for your approval unless you choose a looser permission mode.
48
+ Budgets pause an agent before it runs out of quota, and recovery never replays an
49
+ effect whose outcome is unknown.
50
+ - **Operations.** A terminal console and a local dashboard, one hub per repository or
51
+ worktree with a machine-wide view, live settings reload, token usage and quota
52
+ history (`ahub usage`), and `ahub upgrade`, which carries running sessions across
53
+ versions.
54
+
55
+ ## Upgrading
56
+
57
+ Run `ahub upgrade` while your hubs are running. Read the notes for your version in
58
+ the [Quickstart](docs/quickstart.md) ("Upgrading to ...") and the
59
+ [Changelog](CHANGELOG.md) first: some releases need a one-time step, such as
60
+ refreshing an agent's MCP entry.
61
+
62
+ Agent CLIs update themselves. `ahub agents` shows which installed versions the hub
63
+ accepts, and `ahub agents trust <id> <version>` accepts an untested one on this
64
+ machine.
65
+
66
+ Before you migrate a memory home with `amem`, read
67
+ [memory profiles and rollout](docs/memory.md#selected-memory-profile). Every hub that
68
+ uses the home must run a version that understands the new layout.
69
+
70
+ ## Where it is going
71
+
72
+ - **Curated memory** ([#412](https://github.com/STAIxBWLB/agent-hub/issues/412)):
73
+ compression, removal, correction and failure-driven management, each shipped only
74
+ after its contract and a measurement.
75
+ - **Every agent able to do its share** ([#418](https://github.com/STAIxBWLB/agent-hub/issues/418)).
76
+ - **Measured, not assumed.** Add-ons and memory are kept or dropped by controlled
77
+ comparisons ([#248](https://github.com/STAIxBWLB/agent-hub/issues/248),
78
+ [#258](https://github.com/STAIxBWLB/agent-hub/issues/258)).
79
+
80
+ Live checks that still need a real account or a person are listed in the
81
+ [release verification report](docs/verification/2026-10-23-0.13.0.md) and the
82
+ [smoke checklist](docs/smoke.md).
118
83
 
119
84
  ## Documentation
120
85
 
121
- - [Quickstart](docs/quickstart.md): install, the first session, a working day
122
- - [Operations guide](docs/operations.md): console, task flow, Pi and models, dashboard, projects, upgrade and recovery
123
- - [Hub memory](docs/memory.md) and [agent-mem](https://github.com/STAIxBWLB/agent-mem)
124
- - [Security notes](docs/security.md): what the hub defends and what it does not
125
- - [Design spec](docs/specs/2026-09-19-agent-hub-design.md): how it is built and why
126
- - [Smoke checklist](docs/smoke.md): what has been verified against real agents
127
- - [Changelog](CHANGELOG.md), [Contributing](CONTRIBUTING.md)
86
+ | Read | For |
87
+ | --- | --- |
88
+ | [Quickstart](docs/quickstart.md) | install, the first session, a working day, upgrade notes |
89
+ | [Operations guide](docs/operations.md) | console, task flow, Pi and models, dashboard, projects, upgrade and recovery |
90
+ | [Hub memory](docs/memory.md) | how memory is stored, shared and recalled |
91
+ | [Security notes](docs/security.md) | what the hub defends and what it does not |
92
+ | [Design spec](docs/specs/2026-09-19-agent-hub-design.md) | how it is built and why |
93
+ | [Smoke checklist](docs/smoke.md) | what has been verified against real agents |
94
+ | [Changelog](CHANGELOG.md), [Contributing](CONTRIBUTING.md) | releases and how to contribute |
128
95
 
129
- `AGENTS.md` and [`docs/agent-notes/`](docs/agent-notes/) hold the rules coding
130
- agents follow when they work on this repository.
96
+ Coding agents that work on this repository follow [`AGENTS.md`](AGENTS.md) and
97
+ [`docs/agent-notes/`](docs/agent-notes/).
131
98
 
132
99
  ## Origin
133
100
 
134
101
  The first idea came from [agent-bridge](https://github.com/raysonmeng/agent-bridge),
135
- which lets Claude Code and Codex talk to each other. agent-hub has since grown
136
- into a different system with its own goals and shares no code with it.
102
+ which lets Claude Code and Codex talk to each other. agent-hub has since grown into a
103
+ different system with its own goals and shares no code with it.
137
104
 
138
105
  ## License
139
106
 
@@ -29,3 +29,9 @@ Background moved out of the area notes for 0.12.45:
29
29
  - Process-tree stops (adapters.md): three review rounds of #113 found snapshot gaps while the tree ran (groups of its own, an unreadable first read, a child started during the grace period).
30
30
  - Local worker (local-worker.md): a bare `private/` deny entry once denied all of `/private/var`; `existsSync` made a dangling link look like a new file, so a write landed at its target.
31
31
  - Suite watchdog (tests.md): macOS CI hit `AHUB_CHECK_HANG_S` while tests still passed (#378, #593).
32
+
33
+ Background moved out of the area notes for 0.13.0:
34
+
35
+ - Jcode MCP entry (#685, adapters.md): jcode gives an MCP entry's own env precedence over the process env (probed on 0.99.0), so a peer id in the shared `.jcode/mcp.json` entry would make every extra instance act as `jcode`.
36
+ - Bulk queue discard (#713, bus.md): a queued row with no journal record of its own carries the journal's global revision, which every discard moves, so a revision read once at the start goes stale after the first discard.
37
+ - Usage roll-up (#683 U2, budget.md): a pass cap smaller than one log line once hung the fold.
@@ -1,6 +1,6 @@
1
1
  # Native adapters and spawned agents
2
2
 
3
- Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` and `activity` events), peer turn state, approvals, and the agent processes the hub spawns and stops. Read before editing `src/adapters/`, `src/pi/`, `src/hub/peers.ts`, `src/hub/permission-mode.ts`, `src/hub/tool-refusal.ts`, `src/hub/child-process.ts`, `src/hub/lifecycle.ts` (shutdown deadline), `src/hub/start-mode.ts`, `src/hub/terminal-open.ts`, `src/memory/capture.ts` (session end), `src/hub/native-hook-launch.ts`, `src/hub/peer-catalog.ts`, the launcher-managed MCP entries (`src/cli/agy-mcp.ts`, `src/cli/kimi-mcp.ts`, `src/cli/jcode-mcp.ts`) or the process-tree stop in `scripts/benchmarks/teardown.ts`, or before adding a native peer.
3
+ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` and `activity` events), peer turn state, approvals, and the agent processes the hub spawns and stops. Read before editing `src/adapters/`, `src/pi/`, `src/hub/peers.ts`, `src/hub/permission-mode.ts`, `src/hub/tool-refusal.ts`, `src/hub/child-process.ts`, `src/hub/lifecycle.ts` (shutdown deadline), `src/hub/start-mode.ts`, `src/hub/terminal-open.ts`, `src/memory/capture.ts` (session end), `src/hub/native-hook-launch.ts`, `src/hub/peer-catalog.ts`, the launcher-managed MCP entries (`src/cli/agy-mcp.ts`, `src/cli/kimi-mcp.ts`, `src/cli/jcode-mcp.ts`), `src/cli/configured-acp-entry.ts`, the version policy (`src/adapters/compat.ts`, `src/cli/agents.ts`) or the process-tree stop in `scripts/benchmarks/teardown.ts`, or before adding a native peer.
4
4
 
5
5
  - For conceptual architecture, definition fields, generic ACP config and the new-adapter review checklist, see [Adding an AI Tool](../adding-an-ai-tool.md). For definition interfaces, see [registry-types.ts](../../src/adapters/registry-types.ts).
6
6
 
@@ -31,7 +31,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
31
31
  - Pi and local approvals (#253): `permit` answers a `ToolApproval` (contract: `local-worker.md`) and `requestPermission` (`src/hub/daemon.ts`) tells the adapter's `ApprovalObserver` what settled it. An expiry counts toward the stop only as `source: "expired"` with `eligibleExpiry` not false (no person answered a request of that peer while it was pending); `person` resets the count, `automatic` (a mode grant, Pi's `always`) never does, and a new turn starts it over. The second one ends the turn, never the peer: local throws `ApprovalWaitStop`, Pi sends `abort_budget` with `cause: "approval"`, and both settle the delivery `completed` with the reason; a Pi whose extension did not announce `approvalTurnAbort` goes offline with the delivery held. `requestPermission` deletes a request from `permissions` before it runs an observer, resolves or reports, and an aborted signal closes it as `cancelled`: a late answer must find nothing to grant, and a failed event, notice or console push never strands the caller.
32
32
  - Pi's `always` grant (#209) is keyed by `executeTool`'s tool name, never parsed from the approval title, and lives only in that Pi start's closure. Never persist it, share it with the local worker, or let the dashboard give it.
33
33
  - Refuse ACP permissions while offline or waiting for watchdog cancellation before consulting automatic or human approvers. Abort pending permission signals on watchdog, offline transition and stop; the daemon withdraws their registered console approvals before any late person answer. Restart creates new requests without reviving withdrawn ones (#311).
34
- - Apply QWEN_MODE_IDS only to a production qwen025 adapter with verified qwen-code 0.25.0 native identity and advertised ids; legacy benchmark ACP stays unmanaged. Await the native acknowledgement before bus admission, preserve existing approval fences, and never map auto or plan. No Qwen quota classification exists until a pinned native refusal is observed.
34
+ - Apply QWEN_MODE_IDS only to a production qwen025 adapter with a qwen-code native identity admitted by compat and advertised ids; legacy benchmark ACP stays unmanaged. Await the native acknowledgement before bus admission, preserve existing approval fences, and never map auto or plan. No Qwen quota classification exists until a pinned native refusal is observed.
35
35
 
36
36
  ## Turns, watchdogs and hub tools
37
37
 
@@ -74,12 +74,15 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
74
74
  ## TUI launchers, native facts and Antigravity
75
75
 
76
76
  - Kimi TUI is a pull-only WsPeer under the one kimi id, with no hub busy/watchdog/permission/quota machinery. Only the person's launcher merges or removes its project MCP entry; a daemon headless start refuses a stale entry and never edits the file. Scrub mode/channel markers from the native child; managed AGENTS instructions and tool descriptions carry the pull rules, not MCP initialize instructions.
77
+ - Empty-inbox polling advice (#681, `InboxPolling` in `src/hub/inbox-polling.ts`) is advisory: never refuse or delay an inbox read for it. The daemon computes it only for a pull-only peer after the pull completed, so a throw drops the advice, never the pulled envelopes; it counts only empty reads with no open, non-backlog task as owner or reviewer, and an assignment resets it. The plugin shows `[polling detected]` only for a well-formed `pollingAdvice` (count >= 3, minutes 0 to 10).
77
78
  - TUI launchers (Kimi, Jcode, agy) write the plugin bundle the hub reports in `status` into their MCP entry, and the CLI's own copy only when the hub reports none (#502). Only the Jcode start path checks the entry against the daemon's own `SERVER_JS` (`jcodeMcpReady`); Kimi TUI admission does not compare it.
78
- - Jcode (#529): accept the native version only through `jcodeVersionSupported` (from `JCODE_MIN_VERSION` up to, not including, `JCODE_BREAKING_VERSION`), in both the version probe and ACP `expectedAgent`; never pin the minor, and lower the breaking version when a release breaks the contract. Every owned jcode process (probe, serve, ACP and TUI) gets `JCODE_OWNED_ENV`, and the private socket name ends in `.sock`.
79
+ - Native CLI versions (#691): every version gate asks `assessVersion` in `src/adapters/compat.ts`; no other file holds a version constant. Add a version to `verified` only with a receipt in `docs/verification/`; a `line` is for contract gates only (an isolation gate never assumes a newer version compatible); the person's `ahub agents trust` admits one exact version on this machine and never skips a runtime proof; `ahub agents` with anything but `--json` stays `console` in `classifyPeerCommand` and refused in `liveTestRefusal` (`src/cli/identity.ts`). A version refusal starts with `VERSION_REFUSAL` and names only versions and the trust command; keep it that way so the daemon may show it.
80
+ - Jcode (#529): accept the native version only through `jcodeVersionSupported` (compat `jcode` line, 0.98.0 up to 1.0.0), in the daemon preflight (`jcodeVersionAsync`: off the event loop, owning its process group, output capped at 64 KiB), the doctor probe (`jcodeVersion`) and ACP `expectedAgent`; never pin the minor, and lower `line.below` when a release breaks the contract. Every owned jcode process (probe, serve, ACP and TUI) gets `JCODE_OWNED_ENV`, and the private socket name ends in `.sock`.
79
81
  - Jcode TUI (#464): use only the current owned launch's top-level `--socket`/`--resume` tuple after native saved-session proof. Prove the save only through `savedSession` (no-follow, single-link regular file, size-bounded, unchanged across the read, its `id` and `messages` matching) and refuse a requested model, provider profile or session that differs from the running launch with `JcodeOptionMismatch`, which names the flag, never its value (#559); both the launcher and `--print-command` report it through `refuseNamedOption` (#574). Hand the tuple only to a person's launcher through `native_tui_launch` (admission: `daemon.md`); previews (`--dry-run`, or `--print-command` from anyone but a person) start nothing and never resolve it. Never synthesize a save or prompt to induce one. Ctrl-C during the save wait and frontend close retain ACP; preserve session/load identity refusal and headless owned recovery metadata, and never record the TUI as a managed terminal (`recordTerminalLaunch`): Jcode has no terminal recovery adapter (#564).
80
82
  - Jcode steer (#572): reach a busy owned jcode only through `nativeSteer` on its private server socket, never by cancelling its turn: `ping` must list `soft_interrupt_ids`, `subscribe` uses `target_session_id` (the ACP session id) and `continue_on_disconnect`, `state` must report `is_processing` (a soft interrupt sent while idle starts a turn by itself), then `soft_interrupt {content, urgent: false, client_id}`. `canSend` rechecks the captured turn, busy state, owned server process, socket identity and session immediately before the write. Every refusal before the send is `failed_safe` with nothing sent; a sent message that is not acknowledged, or whose turn cannot be confirmed, is `needs_review`, never `failed_safe` or `completed`. Read `currentTurn()` before sending and adopt only through `AcpPeer.adoptIntoTurn(turn, ...)`, which refuses a different or ended turn; adopted senders join the turn answer's audience (`replyParent` and `replyAudience` over the turn envelopes, so the hop cap holds), and each steer receipt is `accepted` at once and settles with the turn (`completed` on a normal end, `needs_review` on a cancel, failure, stop or watchdog). Headless Kimi and Qwen have no steer: status reports their waiting important messages only through `importantWaitingStatus` (`src/hub/daemon.ts`) past `budget.stale_min`. A turn that ends and restarts between jcode's `state` reply and the write still receives the message, held `needs_review` and adopted into nobody: keep that ceiling until upstream offers an atomic per-turn steer, and never add a cancel-to-deliver path.
81
83
  - Jcode MCP set (#572): `jcode acp` and `jcode serve` start every server named in `<JCODE_HOME>/mcp.json` and the project's `.jcode/mcp.json`, `.mcp.json` and `.claude/mcp.json`, and no flag or variable selects them (`--mcp-tools`, `JCODE_MCP_TOOLS`, `JCODE_DISABLED_TOOLS` and `JCODE_DISABLE_CLAUDE_MCP` do not). Do not paper over it: `ahub doctor` lists the extra names through `jcodeExtraMcpServers` (`src/adapters/definitions/native-cli-jcode-mcp.ts`), names only, those four paths only, no home search, no link followed. Below the project root or home, `.jcode` and `.claude` must be real directories and the file a regular file (per-component `lstat`, an `O_NOFOLLOW` open and an `fstat` identity recheck); a system alias at or above the configured root stays allowed. Revisit if upstream adds a server selector.
82
- - Antigravity P1 (`src/adapters/agy/`): keep it optional, cloud, off roster by default, implementer/verifier only and pull-only. Until per-mode native prompt behavior is verified, reject hub permission/unattended mappings, headless starts and resume selectors; do not infer a quota source. The launcher refuses extra instances, any `agy --version` outside the 1.4.x line (1.4.2 is T0-verified; other 1.4.x patches launch with a stderr warning, #589), and a hub without the `native_hook_launch` feature.
84
+ - Extra Jcode instances (#685, #368): one `JcodePeer` per declared id (at most 8), each with its own `jcode_home`, socket and state. The person authenticates inside Jcode; the hub passes `-p`, `--model` and `--provider-profile` exactly as declared, handles no model credential or URL, and only checks that the home is a directory the person owns (`jcodeHomeRefusal`: no read or write under it). Never put `AGENTHUB_PEER_ID` in the shared `.jcode/mcp.json` entry (`jcodeMcpEntry`): every owned launch supplies it through `peerChildEnv(<id>)` or the TUI launch env, together with its `AGENTHUB_LAUNCH_ID`. The MCP server with no `AGENTHUB_PEER_ID` in tools mode refuses every tool and never defaults to a peer id. The daemon admits a `tools` hello for a peer whose definition is `owned-acp` only when it carries the launch id of that peer's current owned launch (`JcodePeer.ownsLaunch`, from `jcodeLaunches`); never trust the env alone. `jcode --as <id>` never opens a TUI. Declare the semantic `instances.nativeHome` on a definition, never compare the kind name (`registry-guard`); it is a fixed-builtin capability that `assertNativeCapabilityScope` refuses elsewhere. `checkJcodeInstanceHomes` (`src/hub/instances.ts`, run once at daemon startup) refuses a home equal to or nested in the base Jcode's or another instance's; compare homes only through its `canonicalHome` (`realPath`, case-folded on a volume that folds case, inode), never `resolve()` text. The preview of `--as <id>` passes the instance to `previewJcode` so it shows the daemon's real flags.
85
+ - Antigravity P1 (`src/adapters/agy/`): keep it optional, cloud, off roster by default, implementer/verifier only and pull-only. Until per-mode native prompt behavior is verified, reject hub permission/unattended mappings, headless starts and resume selectors; do not infer a quota source. The launcher refuses extra instances, any `agy --version` that compat `agy` refuses (1.4.2 is T0-verified; other 1.4.x patches launch with a stderr warning, #589; another version needs the person's trust, #691), and a hub without the `native_hook_launch` feature.
83
86
  - Only the person's `ahub agy` launcher manages `.agents/plugins/agent-hub/{plugin.json,mcp_config.json,hooks.json}`. Refuse linked/foreign entries before any write; cleanup only the matching launch. Never write project-level `.agents/hooks.json` or personal Gemini settings. The plugin MCP server is named `agent-hub_agent-hub` by the pinned native client.
84
87
  - Managed native facts use the definition's typed `NativeFactsPort` and the daemon's ephemeral `native_hook_launch` authority. Bind every observation to the daemon, launch, native session, project and current socket generation. Stop requires `fullyIdle`, normal termination and a fresh validated completion row; a missing readback keeps the peer busy. Native fact acknowledgement accepts only SDK ephemeral-message rows in the exact session transcript path. `decodeAgyHook` classifies as `Read` only the observed native read (`view_file` `AbsolutePath`, or 1.4.3 `read_file` `Path`); a `StartLine`/`EndLine` range becomes `offset`/`limit`, so a partial read never counts as a whole-file view. Unobserved edits get no scoped grant or effect attribution. The daemon records Pre/PostToolUse through `Facts.preTool`/`postTool` and offers nothing on them (#555).
85
88
  - `terminalOwner(launchId)` (`src/hub/peers.ts`, #482) answers only for the adapter's current native TUI owner bound to that launch (Codex: the claimed link with that `launchId`, both sockets open; the launcher sends the id as the upgrade's bearer token through `--remote-auth-token-env`, because `codex --remote` refuses any path or query (#586); Pi: a started, verified TUI owner on its requested session); a start acknowledgement is never ownership. A Codex link that is not the claimed one or has a closed socket never adopts a thread.
@@ -88,10 +91,10 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
88
91
 
89
92
  - Keep the default reviewer kind order Claude, Codex, Kimi (selection rules: `tasks.md`).
90
93
  - Keep the retained agy reviewer slot unavailable with exact reason `agy is an implementer peer (#485)`, including explicit reviewer-pool requests. Do not add agy reviewer launch/admission or personal configuration writes to #234; implementer work belongs to #485.
91
- - For fresh Kimi reviewers and task-bound MCP (#533), retain the common per-child task/head/review-revision/generation verdict fences, privacy screening and cleanup-before-settlement. Expose only a host in-process two-tool MCP capability for `hub_review` and `hub_task_show`; never give the native child the full hub control token. Supply source evidence only through the screened review brief/read result and host facts; exclude native Read/file/shell tools. Admit covered Kimi 2.2.0 only when current runtime preparation passes; otherwise keep it unavailable with the refusal reason. Keep actual interactive-TUI/usage ceilings separate from completed live/fake receipts; do not reuse historical Seatbelt/dummy-provider probes as revised-contract passes.
92
- - Start fresh Kimi 2.2.0 without Seatbelt, with private `KIMI_CODE_HOME`, managed-provider-only configuration, clean temporary cwd, explicit empty skills directory and exactly `mcp__agent-hub__hub_review` plus `mcp__agent-hub__hub_task_show` in its agent tools. Use directory symlinks for the person's native `credentials/` and `oauth/`, and link `device_id` to the native identity; do not copy token files, select static-API-key mode or set `KIMI_DISABLE_OAUTH_LOCK`. Let native OAuth share refresh/atomic rename/`proper-lockfile` coordination and the existing Kimi account budget. Revoke verdict authority before shutdown; allow bounded TERM grace while the OAuth lock is held, then use verified owned-group teardown. Report a forced-kill/re-login requirement without attempting host auth repair.
94
+ - For fresh Kimi reviewers and task-bound MCP (#533), retain the common per-child task/head/review-revision/generation verdict fences, privacy screening and cleanup-before-settlement. Expose only a host in-process two-tool MCP capability for `hub_review` and `hub_task_show`; never give the native child the full hub control token. Supply source evidence only through the screened review brief/read result and host facts; exclude native Read/file/shell tools. Admit only compat-covered Kimi versions when current runtime preparation passes; otherwise keep it unavailable with the refusal reason. Keep actual interactive-TUI/usage ceilings separate from completed live/fake receipts; do not reuse historical Seatbelt/dummy-provider probes as revised-contract passes.
95
+ - Start fresh Kimi reviewers without Seatbelt, with private `KIMI_CODE_HOME`, managed-provider-only configuration, clean temporary cwd, explicit empty skills directory and exactly `mcp__agent-hub__hub_review` plus `mcp__agent-hub__hub_task_show` in its agent tools. Use directory symlinks for the person's native `credentials/` and `oauth/`, and link `device_id` to the native identity; do not copy token files, select static-API-key mode or set `KIMI_DISABLE_OAUTH_LOCK`. Let native OAuth share refresh/atomic rename/`proper-lockfile` coordination and the existing Kimi account budget. Revoke verdict authority before shutdown; allow bounded TERM grace while the OAuth lock is held, then use verified owned-group teardown. Report a forced-kill/re-login requirement without attempting host auth repair.
93
96
  - Bind every running print review to its immutable envelope head and review revision. Recheck `Tasks.reviewBindingCurrent` for task-MCP operations, legacy print reads and the verdict authority after awaits; an old child never gains a replacement request. A committed verdict acknowledgement does not authorize subsequent reads or verdicts.
94
97
  - `ReviewerPrintPeer` (`src/adapters/reviewer-print.ts`, #223, #519; `ClaudePrintPeer` is its Claude wrapper) runs one fresh child per review and clears `reviewing` at the leader's exit and first in `kill()`. A stop or watchdog settles `needs_review` at once, before the kill; `completed`, `idle` and `offline` come only after the whole process group is confirmed gone, and a failed cleanup keeps the child handle and stays `busy`. The daemon's `reviewAuthority` compares `reviewToken`: never widen it.
95
98
  - A reviewer backend is a fixed builtin `ReviewerBackend` in a definition's `reviewer`, never config. Its `prepare` must return a complete isolation receipt (matching backend id, binary, version, profile, evidence and `readOnly` true, plus `strictMcp` and `ephemeral` true, or for a `nativePolicy` backend `enforcement: "native-policy"`, `freshSession` and `hubToolsOnly` true) before every child. A `taskMcp` backend refuses without the host's `taskMcp` port and gets only that task's MCP config, never the hub-wide one. `admit(env)` and `launch.current()` are rechecked right before spawn, and a `watchCurrent` launch is polled while the review runs: a change aborts it as `review policy changed`; a missing or failed proof refuses, never a weaker launch or a continuing native session. The daemon puts the reviewer on the bus only after `start()` proved isolation; a failed start or a `start_failed` report marks it in `reviewerUnavailable`, which routing reads as `failing`. An unavailable reviewer variant has no reviewer backend or launch code; `startPeerBody` refuses it before preflight, independently of any native TUI launcher.
96
99
  - Report reviewer failures (`onUnavailable`: `start_failed`, `quota`, `no_verdict`) only after the child's group is confirmed gone and only while its generation is current: a person's stop owns its hold and never reroutes. `quota` comes only from the backend's structured `failure(event)`, never from vendor text. An NDJSON frame over 256 KiB or more than 4 MiB of output aborts the review as `needs_review`.
97
- - Codex reviewer isolation (`src/adapters/codex-review-isolation.ts`): accept only the pinned `codex-cli` version; prove model-free that Apps, plugins and hooks are disabled (`features list`), ambient MCP servers are replaced by disabled stubs, and the hub server alone is enabled with exactly `hub_review` and `hub_task_show`; run the native sandbox fixture probe before every launch; hash the binary once and fence binary and config files by metadata. Credential denies come only from the bounded `HOST_CREDENTIAL_PATHS` and `HOST_CREDENTIAL_ENV` lists, denied at the lexical and canonical path; refuse glob syntax or a root inside the checkout. A root that is dangling or unreadable refuses too; the reason names only its path class (`~/.docker`, `$DOCKER_CONFIG`), never a path or contents. Never pass the control token in a reviewer child's argv, env or MCP config: keep the hub state dir in its `privatePaths`.
100
+ - Codex reviewer isolation (`src/adapters/codex-review-isolation.ts`): accept only a `codex-cli` version that compat `codex-review` admits (verified or the person's trust, #691), and still run every proof below after trust; pass `-c notify=[]` (legacy notify off) on every launch; prove model-free that Apps, plugins and hooks are disabled (`features list`), ambient MCP servers are replaced by disabled stubs, and the hub server alone is enabled with exactly `hub_review` and `hub_task_show`; run the native sandbox fixture probe before every launch; hash the binary once and fence binary and config files by metadata. Credential denies come only from the bounded `HOST_CREDENTIAL_PATHS` and `HOST_CREDENTIAL_ENV` lists, denied at the lexical and canonical path; refuse glob syntax or a root inside the checkout. A root that is dangling or unreadable refuses too; the reason names only its path class (`~/.docker`, `$DOCKER_CONFIG`), never a path or contents. Never pass the control token in a reviewer child's argv, env or MCP config: keep the hub state dir in its `privatePaths`.
@@ -1,6 +1,6 @@
1
1
  # Budget, pause and handoff
2
2
 
3
- Scope: quota and native context readings, checkpoints, pause, handoff and resume, and the status line tee. Read before editing `src/hub/budget.ts`, `src/hub/context-window.ts`, `src/cli/statusline-tee.ts`, `src/adapters/definitions/native-quota.ts`, `src/adapters/definitions/native-context.ts`, `src/hub/bus.ts`, `src/hub/delivery-journal.ts`, `src/hub/restart.ts` (pause persistence) or the per-account fanout in `src/hub/instance-budget.ts`.
3
+ Scope: quota and native context readings, checkpoints, pause, handoff and resume, and the status line tee. Read before editing `src/hub/budget.ts`, `src/hub/context-window.ts`, `src/cli/statusline-tee.ts`, `src/adapters/definitions/native-quota.ts`, `src/adapters/definitions/native-context.ts`, `src/hub/bus.ts`, `src/hub/delivery-journal.ts`, `src/hub/restart.ts` (pause persistence), the per-account fanout in `src/hub/instance-budget.ts` or the usage roll-up in `src/hub/usage-rollup.ts`.
4
4
 
5
5
  - Budget: checkpoint first, pause second (a paused peer receives nothing). A handoff that fails is left unmarked so the next tick or hub run retries it; never record it as done.
6
6
  - A handoff needs somebody to hand over to: `canHandOff` is false right after a restart, when no peer is attached yet, and the handoff waits for a later tick instead of stripping tasks of their owner.
@@ -23,3 +23,8 @@ Scope: quota and native context readings, checkpoints, pause, handoff and resume
23
23
  - Release-cut quota admission (`releaseQuotaSnapshot` in `src/hub/daemon.ts`, #481 S11): an account has a quota source only when one of its members' definitions has `budget.reader`, or `budget.resetSoftTokensOnResume` with `kimi_tokens_5h > 0`. Without a source a registered account is reported `no quota source` and never defers; with one, an unknown, stale or below-gate reading defers. Manual and budget-record pauses defer for every attached peer.
24
24
 
25
25
  - Keep `readClaudeTranscriptUsage` at its 32 MiB accounting bound. For native turn completion, follow the admission rule in `daemon.md`.
26
+
27
+ - Limit ledger (#683 U1): write `budget_pause`, `budget_resume` and `quota_reset` only through `Budget.writeLedger` (`hooks.ledger`), at the one pause INSERT in `evaluate` (a new pause that replaces an expired open record first writes that record's `budget_resume`, `by: "hub"`) and the one DELETE in `resume` (`by: "person"` only from `override`); never from a call site that reads `budget_pauses`. `quota_reset` is derived in `deriveResets` from the last reading of each window (`lastSeen`, never `readings`, which a resume clears): a forward `resetsAt` move of more than `RESET_JITTER_MS` (15 min), or a `used` drop of `RESET_DROP` (0.5) or more within one window, is a reset, and anything smaller is the same window. It is deduplicated by `peer|window|resetsAt` in `resetsRecorded`, which the daemon seeds from `events.jsonl` at start so a restart adds none. A reading older than the baseline never counts. Entries carry ids, windows and numbers only: never a reason built from agent text, a checkpoint, a summary or a task title (the reason is the coordinator's own text). A throwing hook never costs the pause or the reading. Codex's `usage` event comes from `onTokenBreakdown` beside `onTokens`; keep `tokens` unchanged.
28
+
29
+ - Usage roll-up (#683 U2): `src/hub/usage-rollup.ts` is the only reader of `events.jsonl` for `ahub usage`. A pass reads from the stored offset, keeps whole lines only and commits its rows with the new offset in one transaction; a line longer than the cap is read whole and one past `MAX_LINE_BYTES` is skipped, so the fold always advances. Never store a title, reason or summary there; `limit_events` drops `reason`. issue/pr/release are joined from the board at query time through `TaskLinks`, never stored. A counter with no record behind it is `unknown`, not 0. Claude's total is derived as the sum of its four counters (`claudeReportedTokens`); a source whose input contains the cached part gets none. Every `usage` reply, errors included, answers the rid, and the op is for the console and a configured planner only, never during that planner's private turn. A rebuild from byte 0 must equal the incremental result (`test/usage-rollup.test.ts`). Never present unknown as 0 in a sum: an overall figure with an unknown peer is `partial` and names the peers (`total().unknownPeers`). Never drop rows silently: `limits()` reports `truncated`, `omitted` and the bound. An event-open pause the hub no longer holds is `endedUnknown` (`ahub reset` keeps the log), so `limits(peer, { live })` needs the daemon's held pauses. A poll's fold transaction is IMMEDIATE and rechecks the stored offset, so two roll-ups on one `hub.db` never fold a range twice.
30
+
@@ -1,9 +1,8 @@
1
1
  # Bus, digests and replies
2
2
 
3
- - Supervision replaces only pending, never attempted or accepted, envelopes with the same stable key. Preserve the first batch deadline and existing digest/queue ceilings; role/feed revocation withdraws pending notices only. Count supervision from actual transport admission followed by a successful native turn completion, never from enqueue or failure-to-idle.
4
-
5
- Scope: delivery, digests and condensation, reply addressing, priority and limits. Read before editing `src/hub/bus.ts`, `src/hub/envelope.ts`, `src/hub/limits.ts`, `src/hub/delivery-journal.ts`, `src/hub/inference.ts` (digest condensation), or how an adapter addresses a reply or sets its priority.
3
+ Scope: delivery, digests and condensation, reply addressing, priority and limits. Read before editing `src/hub/bus.ts`, `src/hub/envelope.ts`, `src/hub/limits.ts`, `src/hub/delivery-journal.ts`, `src/hub/delivery-superseded.ts`, `src/hub/deferred-hub.ts`, `src/cli/reset.ts`, `src/hub/inference.ts` (digest condensation), or how an adapter addresses a reply or sets its priority.
6
4
 
5
+ - Supervision replaces only pending, never attempted or accepted, envelopes with the same stable key. Preserve the first batch deadline and existing digest/queue ceilings; role/feed revocation withdraws pending notices only. Count supervision from actual transport admission followed by a successful native turn completion, never from enqueue or failure-to-idle.
7
6
  - Lifecycle `deliveryHeld` fences both drain/pull and important steering before owner teardown awaits; retain newly queued work for an explicit restart without altering manual, budget or conductor pauses.
8
7
  - Apply reloaded `batch_max`, `batch_ms` and `queue_cap` only through `Bus.configureScheduling` (#439): it validates the values and changes later batching and admission only. Never re-arm an existing batch deadline, trim or reorder queued work, or touch prefaces, pauses, recovery holds or in-flight delivery from it.
9
8
  - A failed digest is retried one envelope at a time, so a poison envelope cannot take its neighbours down with it.
@@ -16,7 +15,7 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
16
15
  - Limits admit what is sent: the envelope `newEnvelope` builds (a reply inherits its parent's sender, `digest` resolves to the originals, `capPriority` applies), never the raw `to` and priority. Admit first and build later, and implicit replies all count as broadcasts.
17
16
  - A reply is addressed, never broadcast. An adapter that answers a delivery passes `to: replyAudience(envs)`; anything else with an `inReplyTo` inherits that envelope's sender. Leaving `to` empty fans the message out to every peer.
18
17
  - `digest` is not a peer: addressing a reply at the envelopes the peer was handed sends it nowhere once a delivery was condensed. The bus resolves `digest` back through `lastDelivery.originals`; anything else that reads a reply's `to` has to do the same.
19
- - A pull-only peer (a Claude session without channel pushes, Kimi in its own TUI, or agy) is never drained; `Bus.pull` hands it what one push would take. An attached channel session may also pull its own queue through `hub_inbox` while busy (#493); the same holds and journal completion apply. Drain and pull share `held()` (recovery, an uncertain delivery, any pause, the lifecycle `deliveryHeld` fence) and `remember()` (so `reply_to` resolves), and a drain rechecks `deliverable()` after condensing because such a session can take the peer meanwhile. A pull is journaled `completed` in the same transaction as the bus snapshot, never `accepted`; a failed one puts back only that peer's queue and preface and never reloads the snapshot.
18
+ - A pull-only peer (a Claude session without channel pushes, Kimi in its own TUI, or agy) is never drained by a push; `Bus.pull` hands it its queue oldest first up to `PULL_BUDGET_BYTES` (64 KiB of `frame` text, #713; `takeBudget`), the first item always going so one huge envelope cannot stall it, and a poison one still alone. A push peer, and an attached channel session that is not pull-only, keep `take`'s `DIGEST_MAX`: never raise that for a pull. The daemon's `inbox` reply carries `waiting` and the plugin says `N more waiting: call hub_inbox again before you start work`. An attached channel session may also pull its own queue through `hub_inbox` while busy (#493); the same holds and journal completion apply. Drain and pull share `held()` (recovery, an uncertain delivery, any pause, the lifecycle `deliveryHeld` fence) and `remember()` (so `reply_to` resolves), and a drain rechecks `deliverable()` after condensing because such a session can take the peer meanwhile. A pull is journaled `completed` in the same transaction as the bus snapshot, never `accepted`; a failed one puts back only that peer's queue and preface and never reloads the snapshot.
20
19
  - Record exact task-offer reads only through `DeliveryJournal.completeDelivered` on an active-fenced adapter completion, correlated reply or actual pull, atomically with completion. Capture original offers before terminal pruning and record their ids, never condensed ids; accepted receipts, generic transitions, operator/hub dispositions and restored completed rows are not read evidence.
21
20
  - The `onImportantQueued` nudge for a pull-only peer (#398) is advisory: it never dequeues, steers, journals or settles, and a throw from it never stops an enqueue.
22
21
  - Merge recall and restored prefaces through `mergePrefaces`/`uniqueMemoryLines`; preserve distinct context and new notes, deduplicate memory identities, and keep colliding short prefixes distinct. Use `recallDisplayText`/`recallMemoryLines` before display caps: suppress only matching automatic approval outcomes, retain nonempty rejection details, preserve id headers and complete-word excerpts, and keep token trimming at line boundaries. Keep these changes display-only.
@@ -37,3 +36,6 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
37
36
  - A review-only instance (a declared extra whose roles are exactly `reviewer`) receives only person messages, hub `review` items and hub `task` items addressed to it. Keep `reviewOnlyItem` in both `silence` (at send) and `relevant` (at delivery) (#223).
38
37
 
39
38
  - Concurrent-edit fan-out is linear in affected tasks: retain each notice's own-task stale/owner fence, but never persist the same change-set notice once for every task pair. Preserve per-task-pair research evidence separately (#576).
39
+
40
+ - Completed-change notices (#713): `Tasks.tellCompleted` publishes one notice per recipient per done through `whileOpenFor`, naming all of that recipient's overlapping tasks, and only for tasks the recipient accepted (`ownerAcceptance` plus an in-work state): never one per overlapping task, never for an offered or proposed task, never again for a repeat done without superseding. The envelope carries `refs.completedFor` (the done task); `Bus.enqueue` replaces an earlier queued, never-attempted notice for the same `completedFor` and recipient through `discardQueued` (a `superseded completed-change notice for task #N` journal outcome), and never touches a user message or another hub notice. `relevant` keeps a grouped notice while any of its tasks is still open for the recipient. Do not widen `completedFor` to anything but the done task id.
41
+ - `resolveDelivery` (#713): a `queued` delivery was never handed to the peer, so a busy peer does not block discarding it; `dispatching` and `accepted` stay refused as `active delivery`, and every other resolution keeps the busy fence. The daemon names the refusal from a closed list (`resolutionRefusal`), never from free error text. `queue discard --peer` goes through `resolveDelivery` row by row, reading each row's current revision just before its discard, never the revision listed at the start.
@@ -12,9 +12,14 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
12
12
 
13
13
  - The plugin bundle is installed apart from the daemon. Breaking a control WS message contract bumps `PROTOCOL` in `control-client.ts`; a new request answered as unknown or an optional field preserves it, as does additive optional public task metadata in `ui_snapshot` that older clients ignore (such as `deps` in #246).
14
14
  - `peer_stop` is console-only and stops only a hub-owned headless ACP, Pi or local runtime through its adapter. Refuse native-terminal peers and recovery-held mutations; serialize with starts and permission changes, and refuse a recovery `prepare` while a stop runs. An admitted stop holds delivery through `BusOptions.deliveryHeld` before any teardown await (never a console pause that resume can undo, or a global recovery hold that blocks unrelated peers) and withdraws the peer's pending approvals before the adapter stops. A requested stop holds an in-flight delivery as `needs_review` with an ids-only stop reason, suppresses restart/escalation, and never resolves that delivery automatically.
15
+ - Console review guidance (#711): `r` on a task in review opens `reviewChoice` (`src/cli/console-review.ts`, modelled on `modeChoice`) from the row or the open task detail, never a bare prefill; `a` and `c` prefill `review N approved|changes_requested ` with a guide line, a `changes_requested` line with no note is refused in `reduceConsole` before any effect, and the result of a review command is shown as a notice in panels mode (`reviewResult`). The console only prefills and runs `ahub review` as the person: never grant it extra authority, and keep a private task's title out of the choice line (`[pii]`). The CLI cause line (`agentShellCause`, `src/cli/identity.ts`) is printed for the `tools` identity and a person-only hub refusal only, never for the console identity, and never replaces the refusal.
16
+ - Console changed-symbols block (#684 2b): `symbolFields` (`src/cli/console-symbols.ts`) adds `symbols` after `plan` for a task_show-shaped detail only, from the public `codeSymbols`; never from a raw `Task`, never for a private task, never from a stored receipt (one with `repositoryKey` reads as unknown: only the screened public receipt is shown), never as a second copy of the match rule (`planSymbolMatches` in `review-brief.ts`). Unknown is `unknown (no code facts)`, never an empty list.
17
+ - Console Queue `D` (#713) only prefills `queue discard --peer <selected row's peer> --reason `; Enter refuses a missing peer or reason and arms a `y` confirm that names the count, then runs the person's `ahub queue discard ... --yes`. The `queue` op stays console-only; `discard` without `confirmed` only counts, and a confirmed one needs a reason, is refused during recovery or shutdown and discards row by row through `resolveDelivery` (rule: `bus.md`).
18
+ - Console Usage panel (#683 U3): tab `7`, text from `src/cli/console-usage.ts` (pure strings, no terminal or state import), rendered in `console-state.ts`, fetched in `console.ts` `refresh()` only while panel 7 is open. It reads the `usage` op and nothing else: no event-log read, no board field beyond the task id selected in Tasks. A refusal shows `usage unavailable`, never empty usage; an unknown counter is `unknown`, never 0; a private task is `#id [pii]` from the op's `taskPii`, never a title. The `usage` op folds with `catchUp(250)` and answers `behind: true` when it is not done: never answer from one pass of a big log. Keep Left/Right from 7 wrapping like Settings so the `1`..`5` cycle (#309) stays pinned, and keep PAUSE and 7D out of the width drop list.
15
19
  - Settings (#269): the dashboard's `setting`, `setting_undo`, `setting_preview` and `setting_reload` actions and the console-role `settings_get|set|undo|preview|reload` requests go through `settingsSet`, `settingsUndo`, `settingsPreview` (`settingsUndoPreview` for `undo: true`) and `reloadConfig` (`src/hub/daemon.ts`) and the registry in `src/hub/settings.ts`. A new editable setting is a registry entry, never a new write path; a key that chooses what the hub runs or where secrets or task text go joins `NEVER_EDITABLE` instead. Writes go only to `.agenthub/config.local.json` and `.agenthub/routing.local.toml` through `store()`: the loader's own validation first, temp file and rename, never a file git tracks. Authority comes from the dashboard session (`DashboardSession`, `src/hub/ui.ts`) or the console role, never from the request body; `ui_action` from the manager is an ordinary session. Never return a setting's value in an event or notice except through `recordSetting`.
16
20
  - Live reload (#439): `reloadConfig` is the only publication path. Settings writes and undo, `ahub settings reload`, the dashboard reload and the debounced watcher of the four `CONFIG_SOURCE_FILES` (`src/hub/config-reload.ts`) all reach it; `prepareConfiguration` validates the complete candidate before anything is published, and recovery or shutdown holds it. A write or undo passes its own key as `scope` and publishes only that path, and `acknowledgeConfiguration` records only the accepted paths, so other edits stay pending. Every changed key, inside the registry or not, is decided by `authorizeConfigLeaf` (`src/hub/config-apply.ts`): a refused key keeps its accepted value and is reported by name while the others apply, `roster.*` applies only through its own scoped roster write, and a change of the settled planner or conductor holders (`roleHolders` over `resolveSettledRoles`) is guarded even when a role list shrinks. Outside a named write, only the console's `settings_reload` (authority `terminal`) adopts guarded file edits, by sending back the `confirmation` token bound to the candidate `fingerprint` and `configRevision`; changed bytes or accepted state refuse it, and a watcher, dashboard or settings session never confirms. An undo that names `key` and `undoDigest` writes only while `pendingUndoIdentity` still matches both. Keep config-source snapshots plain and cloneable; classify dynamic ids with validated running/source identity scopes, never suffix discovery or a builtin fallback. Compare restart-bound declarations with the running config on every reload, never a pending desired value accepted by an earlier reload; derived defaults stay bound to that running declaration. A `restart` key (`configApply`) keeps its running value and is listed in `pendingRestart`; reload notices and events carry key names and reasons, never values. The console settings editor (the Settings panel, or the Peers panel's `w` WIP shortcut, which opens only an existing `tasks.wip_limits.<peer>` row for a rostered peer listed in status `taskWip`) stays bound to the panel that opened it (`SettingsEdit.panel`) and arms a write only from a preview reply that matches that panel and its open edit's token, key and value; pasted text never confirms. Console Tasks backlog actions (`p`, `a`, `e`, `R`, `c`) only prefill person CLI commands (`backlog pull-ready|plan|return`, `task cancel`) with the row's immutable id and its planning or flow revision; `validateLockedRow` rechecks the row's state and revision at Enter and again at `y`, and no console path writes the board directly (#532).
17
21
  - Retained configuration (#544): a key the reload refused, or a file edit outside a scoped write that still differs from the accepted value, is listed in `retainedConfig` (key and sanitized reason only, never a value) in status, `status.json`, the settings view and the console snapshot until a reload accepts or reverts it (`retainedConfigAfterReload`). A config leaf that selects a peer's native launch belongs in that definition's `cli.launchConfigKeys`: the `start` and `peer_start` replies then carry `retainedLaunchConfig` (the running values), the launcher starts with them and prints `retainedLaunchWarning` before its start request; when a retained value cannot launch (Kimi TUI `kimi_cmd` with no TUI form), the error names the running hub as its source and never echoes the words (#654).
22
+ - `action_policy` (#636) is machine-local only: `stripUntrusted` drops it from any file but `config.local.json`, and `authorizeConfigLeaf` refuses an `action_policy` leaf whose `sourceFile` is not an untracked `config.local.json` before any other check. It applies live (`actionBudgets.policyChanged`); adapters do not enforce it yet, so keep its settings hint saying so until they do.
18
23
  - `pet` is console-local presentation (#483): validate it only through `validatePetConfig`, which both `loadConfig` and the reload schema call, and keep it applied live with no authority (`authorizeConfigLeaf`).
19
24
  - Start modes (#269): every start the hub makes itself goes through `hubStart` (`src/hub/daemon.ts`): the dashboard's `start_peer`, the console-role `peer_start`, the conductor's `start` hook and `pi.auto_start`. It takes the mode from `startMode(peer)` and nothing else; never add a caller-chosen mode, flag or command to it, and never fall back to headless when a terminal cannot be opened (refuse with the manual command). The words a terminal runs are assembled there from `process.execPath`, the CLI entry, the project root and `startWords(peer)`. The only continuity exception is a validated Pi bookmark on `pi.auto_start`: append its recorded session file (or its session id when no file was recorded), backend and model; never pass both session selectors or accept caller-written replacements. Recovery and `startPeer` callers that pass a recorded mode keep it. A launcher with no `--mode` takes the daemon's accepted start mode from `status.json` `startModes` (`readControl`) before the file's value. A TUI start of an `owned-acp` peer (Jcode) from the console's `peer_start` opens its terminal even while the ACP owner is online: that terminal is a frontend on the owner. The conductor and the dashboard answer `already` for an attached one (#559). The real terminal providers are wired only in `src/hub/lifecycle.ts` (`DaemonOptions.terminal`): a daemon built without them, as in every test, opens nothing; a test injects a recording provider and never the real one.
20
25
  - A headless start that `hubStart` made for a peer with `capabilities.watch` (Kimi, Qwen) also opens `ahub watch <peer>` through `openWatchPane` (#286 AC10): the same provider, fixed words from `process.execPath`, `CLI_ENTRY`, the project root and the peer id, one per peer per `PEER_START_RETRY_MS`. It runs only after the start succeeded and never fails it: no provider or a failed open reports `watch.command` in the result and a notice, with the provider's own text in the log only. Never open a pane for an `already` result or a declared instance.
@@ -82,7 +87,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
82
87
  - A declared Claude channel extra is never hub-started: keep it out of `STARTABLE_PEERS`, instance start modes and `RESERVED_IDS`. A launcher admits `--as <id>` only when the running daemon's status `activeInstances` lists it (`requireActiveInstance`), never from the config on disk.
83
88
  - Verify a native Claude Stop and its start baseline only through `readClaudeTranscriptCompletion` (4 MiB backwards chunks, at most 64 MiB plus one boundary byte; incomplete or out-of-bound rows are unavailable), never the size-capped `readClaudeTranscriptUsage`; an `unavailable` refusal leaves the peer busy and notifies once per turn start (#517).
84
89
 
85
- - Classify HEAD moves with bounded, append-fenced per-worktree reflog evidence. Exclude only incoming checkout/merge/pull/proven-fast-forward reset paths; preserve commit/amend/cherry-pick and sequencer replay changes; classify rebase/pull --rebase start checkout as incoming, with overlapping replay paths unknown. Never exclude paths still dirty against end HEAD (autostash/checkout -m included). Unreadable or ambiguous movement/capture evidence is unknown, never an exclusion. Compute a concurrent change set once and publish no more than one conditional notice per affected task. Keep per-task-pair research events. Cache only raw checkout-branch probes within one synchronous burst, and reapply current privacy screening on every status rendering (#576).
90
+ - Classify HEAD moves with bounded, append-fenced per-worktree reflog evidence. Exclude only incoming checkout/merge/pull/proven-fast-forward reset paths; preserve commit/amend/cherry-pick and sequencer replay changes. Take a rebase/pull --rebase start checkout's incoming paths from the single `merge-base --all` of old and upstream to upstream (no base or several is unknown), so replayed own commits stay attributed; a proven fast-forward reset stays incoming even when its target holds the peer's commit from another worktree (#612). Batch `diff-tree --stdin` only for single-parent pick and commit rows; diff squash, fixup, reword and edit rows one by one. Never exclude paths still dirty against end HEAD (autostash/checkout -m included). Unreadable or ambiguous movement/capture evidence is unknown, never an exclusion. Compute a concurrent change set once and publish no more than one conditional notice per affected task. Keep per-task-pair research events. Cache only raw checkout-branch probes within one synchronous burst, and reapply current privacy screening on every status rendering (#576).
86
91
 
87
92
  - Share one monotonic 250 ms snapshot Git budget until the next event-loop I/O turn. Repository/HEAD probes, capture and end diffs consume the same remaining deadline; exhaustion or unreadable diff evidence records unknown trees and no attribution/undo, with one diagnostic per burst. Never substitute a stale baseline or apply a late result (#576). `DaemonOptions.snapshotClock` is host-only (tests hold it, rule: `tests.md`): never read it from configuration or a tool, production leaves it unset (`performance.now`), and the share stays 250 ms with `snapshotBurstMs` still clamped to it. Every deadline comparison goes through `budgetNow(budget)`, never `performance.now()` against `budget.deadline`.
88
93
 
@@ -3,7 +3,8 @@
3
3
  Scope: the hub-native local worker, its tools, path guard, denylist and seatbelt sandbox. Read before editing `src/adapters/local-worker.ts`, `src/local/`, `src/memory/capture.ts` (reads the denylist), `src/hub/facts.ts` (denylist and large-file stamps), `src/hub/git.ts`, the grant filter in `src/hub/permission-mode.ts`, or the sandbox root in `src/hub/instance-execution.ts`.
4
4
 
5
5
  - Nothing the local worker executes may run outside `sandboxedExec`; a new tool that spawns a process goes through it, and a tool that touches a path goes through `guardPath`.
6
- - The sandbox denies home reads by default (toolchain dirs, the project and its real git dir excepted) and all network, loopback included: claude-mem and the Codex app-server listen on loopback without auth. Tests that bind a local port therefore fail when the worker runs them, also with `local.bash_network: true` (the egress proxy keeps loopback closed, and the proxy variables send a test's plain-HTTP `fetch` to the proxy, which refuses it); that is the intended trade-off, and `"direct"` is the switch until 0.13.0 removes it.
6
+ - A timeout or abort in `sandboxedExec` marks the result `(killed: <signal>, timeout?)` from its own `killRequested` flag, never only from the exit signal: a shell may report its killed child as a normal exit 137.
7
+ - The sandbox denies home reads by default (toolchain dirs, the project and its real git dir excepted) and all network, loopback included: claude-mem and the Codex app-server listen on loopback without auth. Tests that bind a local port therefore fail when the worker runs them, also with `local.bash_network: true` (the egress proxy keeps loopback closed, and the proxy variables send a test's plain-HTTP `fetch` to the proxy, which refuses it); that is the intended trade-off, and `"direct"` is the switch until a later release removes it.
7
8
  - The seatbelt profile refuses the `.git` name itself at any depth (creation, rename, symlink, gitfile), `config` and `hooks` directly under it, `commondir` at any depth below it or below an external git dir, and `.agenthub` anywhere in the project, every name folded against the 16 code points HFS+ ignores (#270). A submodule's git dir under the common dir's `modules/` tree gets the same `config`/`hooks` refusal at the depth submodules nest, every term anchored below the discovered modules root so ancestor directories named `refs`/`config`/`hooks` neither disable nor trip the rule, and a `/refs/` or `/logs/` segment below it excludes the path so refs named `config` or `hooks` stay writable (#281); the discovered git dirs, the modules root and the components in between are also refused as directory-entry targets (rename, replace, symlink, recreate). Known residuals: a submodule whose own path holds a whole `refs` or `logs` segment (a substring, `catalogs` included, keeps the refusal; the exclusion matches whole components only), a nested repository in bare layout (the hub never runs git there; docs/security.md), folded spellings of the components between `.git` and a submodule's git dir on a volume that folds names (the 1024-byte SBPL literal cannot hold a folded path; external git dirs carry the same residual since #270), and the submodules of a repository nested inside the project (only the project's own common dir is discovered). An external git dir's `config`/`hooks` rules cover only its own children plus the hooks directory's contents: at any depth they would deny refs named `config` or `hooks` in a worktree or submodule project. The fold is a group alternation, never a character class (the seatbelt regex engine is byte-oriented and a quantifier after a multi-byte class member binds to its last byte), and every pattern keeps its `^` anchor (an unanchored starred alternation does not match). Folded literals never contain the project root: an SBPL string literal dies at 1024 bytes. The cost: `git init`, `git clone` and `git worktree add` do not run inside the sandbox, and neither do `git worktree remove`, `git worktree prune` of a stale record, `git clean -ffd` with a nested repository, `git submodule update --init` (its git dir's `config` write is refused), or `rm -rf` of a directory holding a `.git`.
8
9
  - Build every git invocation outside the sandbox through the single implementation exported by exact-pinned `@staix/agent-mem/platform` (`memoryGitArgv`/`memoryGitEnv`/`memoryGitSync`), re-exported as `hubGitArgv`/`hubGitEnv`/`hubGitSync` by `src/hub/git.ts`. Keep the git export identity, behavior and direct-spawn tripwire checks in `test/hub-git.test.ts` passing; retain home compatibility and Node-safe process-entry checks in `test/memory-parity.test.ts`. The sandboxed git tool in `src/local/tools.ts` retains its own behavior. Use `{ credentials: true }` only for the memory repository the hub creates itself, never a project's repository. Use the core deny policy re-exported by `src/local/deny.ts` for guards, sandbox rules and capture; do not add another denylist.
9
10
  - SBPL strings go through `sbplString` (the plain `"..."` form). In the raw `#"..."` form a backslash escapes nothing, so a `"` in a path ends the literal and the whole profile fails to parse.
@@ -14,7 +14,12 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
14
14
  - Treat `cancelled` as closed through `isClosedTaskState`, but keep dependency release restricted to approval. Cancellation goes through `Tasks.cancel`, preserves the row and roles, clears its reservation/cohort, sends revision-bound stop notices and writes no outcome memory; never map it to approval or cancel a native turn or worktree. Planner cancellation stays limited to its own historically unaccepted proposed/backlog work; accepted work remains person-only.
15
15
  - Capture `TaskOffer.pullOnly` and the exact offer envelope id at publication. Keep unread-offer attention independent of attachment and restart until acceptance, decline, supersession or `offerRead` evidence; never replace that evidence with a generic inbox timestamp or completion state.
16
16
  - Use the daemon's settled role map (`resolveSettledRoles`, `src/hub/settled-roles.ts`, the same resolver reload compares holders with) for planner authority and notice recipients. An explicit planner designation is exclusive; automatic fallback remains runtime-only. Keep ready-unassigned sweep admission consistent with person-unassignment suppression, planner-pull reservation/WIP limits and ordinary routing guards; persist each ladder step before its notice.
17
+ - Readiness never puts a reserved owner over its planner WIP cap (#655). `offerReady` asks `reservedHold` (the same `wip()` plus `pendingClaims` against `wipLimit()` that `acceptanceCapacity` refuses at) before the `ready` write is offered; a hold records `ready` and one quiet `ready held` entry (in `QUIET_EVENTS`, never a stall, never an owner) and keeps the reservation. Offer dependents and held work through `readyOrder` (priority, rank, id), and free a slot through `releaseDependents` (approval), `releaseHeld` (cancel, untouched return) or the release timer; never offer a held task past the cap, and never let the idle sweep's auto-reassign override `reservedHold`. `offerReady` records its slot in `readyInFlight` in the same synchronous step as the check and frees it in `finally`, so an await in `assignOwner` never lets two offers share the last slot; `releaseReady` treats `isHeld` work as eligible even after a `planned` entry. Planner-pulled work and an excluded reservation are not held. The hold text carries peer ids and counts only.
17
18
  - Capture the coordination preference once per task at creation (`coordinationModes` from `TasksDeps.coordination`, in `propose` and for each split child, #439). `formCohort` joins only overlaps with the same captured mode and forms a silent cohort only for a turn-free one; a reload never silences an existing task or lifts an existing silent cohort. A daemon restart re-pins retained rows to the startup mode (a compatibility ceiling, not task metadata). `configureSweep` takes effect at the next pass: a running `sweep` keeps the config it started with.
19
+ - Completed-change notices (#713, one per recipient per done, accepted work only, superseding by `completedFor`): rule in `bus.md`.
20
+ - Plan-symbol overlap (#682 1b) compares through `symbolsOverlap` (`src/hub/symbol-normalizer.ts`): arguments stripped, `section `/`source ` keys exact, two qualified names equal, otherwise last segments equal. `places`, `overlapHits`, `contradict`, the completed-notice replay, `plannerGrooming` and `planSymbolMatches` all use it; never compare plan symbols with `includes` or set membership.
21
+ - Task code symbols (#684 2a) are host-derived: stored only in the `task_code_symbols` table and read through `TASK_SELECT` (`src/hub/board.ts`), so a `SELECT * FROM tasks` reader never exports them. `Board.update` writes `codeSymbols` only with `HOST_CODE_SYMBOLS` and drops it when the head (`refs.commit`, else `sdlc.head`, else `sdlc.localHead`), `refs.repo`, `refs.paths` or `sdlc.repo` changes or the task turns private. Tasks observes them only inside an admitted `done (checking)` or `done` write (`observedCodeSymbols`: non-PII, a full commit, the project repository); a failed or mismatched observation is unknown, never a refusal. Optional code facts (`prepareReviewBrief`) skip PII tasks, never refuse a review, and the actor guard runs again after them.
22
+ - Public reads of code symbols go through `publicTaskCodeSymbols` (re-screened, no `repositoryKey`). `taskCodeSymbolsDisjoint` only labels a completed-change notice: never suppress a notice or gate on it. `symbolOverlaps` is console detail only: it persists no pair and drops any task marked `pii` or `pii_path`, even with a routing opt-out.
18
23
  - `.agenthub/routing.local.toml` (#269) may hold only `stay_switch` and per-class `peers`, `escalate_to`, `route` and `pi_backend` (`checkOverlay` in `src/hub/routing.ts`): never widen it to targets, routes, signals or constraints, which stay in the hand-written `routing.toml`. `parseRouting` is the one merge, and `assign()` reads `routing.sources` only for its trace line.
19
24
  - Conductor authority requires the explicit unique role before capability checks. The only exceptions run before that check in `Conductor.execute` and stay task-scoped: a task's current owner and reviewer read it with `hub_task_show` (#208; a reviewer-backend peer only during its bound review, rule below), and its proposer redirects it with `hub_task_assign` while nobody ever accepted it and no person moved it (#207). Removing them strands reviewers that missed a review request. Assignments preserve the acting peer. A peer releases only its own conductor hold; operator release and manual, quota and recovery holds remain separate paths.
20
25
  - The PII screen (#198) runs in `propose` before anything else reads the task (no board row, envelope, triage or brief before its verdict), and `unknown` is PII. A call that screens free text (`done`, `review`, `remember`) screens only after its own checks pass, and `done` and `review` run them again after the awaited screen, because the task can change meanwhile. A history entry marked `withheld` stays withheld in public views, completed notices and review requests, whatever `nameable()` says; `ahub ask` shows it only on campus, like a note that matches a PII pattern.
@@ -43,7 +48,9 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
43
48
  - Tool publication (`projectToolList`, `projectConductorTools`, `configuredToolContext` in `src/hub/hub-tools.ts`) only narrows the list or adds start targets, using authenticated status metadata (`configuredAdapters`, `startablePeers`, `readOnlyPeers`); in a configured project, malformed or incomplete metadata publishes the read-only set and adds no start target. Publication is never authority: `taskOp` refuses read-only mutations independently, and MCP task and conductor refusals keep their text and set `isError: true`.
44
49
  - Admit a declared extra as owner of `implement`, `bulk_edit` or `test` work only with the current implementer role and `ownerIsolation[id] === true`, the daemon's proof that the live adapter started in `executionRoots[id]` outside the canonical cwd. Never derive it from the declaration's `worktree`, its roles or a reload.
45
50
 
51
+ - Lifecycle identity (#694): the hub links `sdlc.repo`/`issue` only through `Tasks.linkIssue` (the one issue `planIssueIdentity` resolves) at plan review, done and the groom backfill, and stages a done's PR through `Tasks.identityLink`/`withLink` (the link carries the repository and the commit or branch it was read for, and is dropped when the done's effective repository or head changed during the read; the issue is re-resolved at apply time; `reviewRequestAdmission` compares the board copy through the same `stage` projection) (`openPrFor`: the one open PR whose head is the done's full commit, or with no commit its branch; a full 100-row page or a malformed row is unknown). Persist a done's link only in the admitted done update or `done (checking)`, never before a gate that can still throw `PreEffectToolRefusal` (#254); reapply it after every re-read in `complete()`. Write a first link with `linkedReceipt`, never `sdlcReceipt` (its relink rule would drop red-first and local-gate records); never relink an existing or foreign identity, never parse agent text for a PR, skip PII tasks before any read, and run the groom backfill after its authority checks with its own bound.
46
52
  - Spec acceptance (#481 S1) is written only by `Tasks.acceptSpec` for the person; never accept it from a tool or a receipt. The daemon uses `Tasks.acceptReviewed` to bind spec and plan readbacks to one task snapshot before `Tasks.accept`; retain its synchronous missing-spec and plan-admission refusals. Apply WIP and plan admission before `done` implicitly accepts proposed work and before cohort writes. Never read or store an issue body: identity and edit times only.
53
+ - Test integrity (#481 S4, `src/hub/test-integrity.ts`): in enforce only the person relinks an established `sdlc.repo` or `issue`. A supplied `redFirst` that fails `publicMetadataText` on a public task is refused pre-effect, and `publicView` drops one that fails it later. Integrity is read only for a non-PII task with current PR evidence; otherwise it is unknown with a blocker, never a pass. Enforce adds `test integrity:` blockers to review admission and the merge queue. All gh reads of one lifecycle gate or status share one 20 s deadline; never give each read its own.
47
54
 
48
55
  - Preserve planner-reviewed cost keys when an accept replaces only file-list intent; explicit enforce scope drift needs a new planner receipt. New enforce plan-review receipts require typed review/CI scopes; keep legacy missing scope unspecified until its planning revision invalidates it. Run premature-review admission before done/cohort writes, recheck state and actor authority after every await, and keep admission separate from the review brief. In advise, discard readback evidence after quiet-only history appends and continue with unknown readiness, without another external read; re-derive a changed reviewer from the current task before sending review, refuse other drift and keep enforce strict. Recheck final completion fences after delayed admission, and tell the owner through the private-aware task notice to call done again when delayed completion is refused only if the task remains in_progress with the owner that started the check. Scope is cost metadata, never a permission or reviewer bypass.
49
56
  - Cost reports recheck planner/private-turn authority and current linked privacy before each underlying external read and final projection. Do not emit a stale/relinked observation as a complete zero-cost row. S7 owns band configuration and automatic proposal writes.
@@ -58,8 +65,11 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
58
65
  - Reviewer pool (#519): `selectReviewerPool` is pure and called only inside `assign()`, which builds the candidates (declared reviewer-only `routing.reviewOnly` instances) and their blocked reasons; Tasks passes only `reviewPool` (`pool`, `rerotate`, `failed`, `required`). With no fresh reviewer declared and no configured `review.pool`, routing stays the legacy path. The seed is the linked PR number, else the task id; compute its modulo over configured kinds with declared candidates, including blocked/offline/paused/quota/failing candidates, and do not renumber by availability; append undeclared-kind trace entries (`no declared reviewer`) after the declared walk without assigning them modulo shares or changing a selected reason; the implementer's family is skipped while another can run, and a delta review keeps the last `changes_requested` family unless `rerotate`. `required` (an explicit `review.pool`) exhaustion goes to the person with `reviewBlocked`; only the optional default pool may fall back to an eligible planner, other family first. Record the choice through `reviewMetadata` (`reviewerFamily`, `reviewerReason`, including `same-family fallback`), never in note text alone. PII tasks, `review`-class tasks and a reviewer a person named (`reviewer changed`, reason `manual`) stay outside the pool.
59
66
  - `Tasks.retryReviewer` is host-only (the daemon's reviewer `onUnavailable`), never a tool or CLI path. It acts only while the failed peer is still the reviewer of an `in_review`, non-PII, not manually named task, with the same review head, the request's `reviewRequestRevision` and a current child generation; `valid()` rechecks all of it after every await and right before the write, which also passes the flow revision. Each failed peer is tried once per head and round (`reviewerFailure` history). `reviewRequestRevision` counts only role events (`done`, `reviewer changed`, `reviewer unavailable`, ownership), so bookkeeping never fences a request; keep `refs.reviewRevision` and `refs.commit` on the review envelope.
60
67
  - Release trains (#481 S11): change trains only through `Tasks.newRelease`, `configureRelease`, `assignRelease` and `cutRelease` (planner or person), and refuse every `hub_release_*` in a PII turn. `assignRelease` writes only `release` and refuses a frozen, shipped or dropped release on either side; private or pinned scope is person-only. One active or frozen train at a time. A cut freezes only through `Board.freezeRelease` after a fresh `releaseWindow` (host lease from `readReleaseHostAdmission`, quota, `current()`, at most 5 s old); unknown evidence defers, and the window is closed in `finally`. A cut never publishes, tags or upgrades, and upgrade and recovery never consult `releaseWindow`. Keep two of `RELEASE_TRAIN_HISTORY_LIMIT` slots for cut and ship; a configuration no-op appends nothing. An S9 plan receipt survives only contiguous planning revisions that changed exactly `["release"]`.
68
+ - Release authorization (#481 S6, `src/hub/sdlc-release-authorization.ts`): only the person's console `sdlc authorize-release` creates one, bound to the current HEAD commit, its committed package version, the origin identity and a fresh, error-free docs verdict, all reread after the docs check. It lives in daemon memory only: a restart revokes it and the event is audit, never restored. `release-guard` and `release-guard-hook` are refused in a private turn; a tag push needs a matching receipt, and npm publish also a clean tracked worktree and the same docs digest. Never accept authorization from a tool, a task receipt or a file.
61
69
  - `Board.update`'s untouched-return exception is the only way owned or accepted work returns to backlog: keep every condition (exact event, owner and reviewer null, state, `expectedFlowRevision`, non-empty note, no work evidence) and reach it only through `Tasks.returnUntouchedToBacklog` (#481 S9).
62
70
  - The main-checkout note (`mainCheckoutWarning`, `src/hub/main-checkout.ts`, #572) is advisory text in the `hub_task_done` result, the review request and status, never a refusal. A branch name or other metadata the hub reads from outside the board reaches task text only after `Tasks.publicMetadataText` (PII patterns, secret shapes, private paths) and a plain ref-name check; otherwise use the generic sentence that carries none of it.
63
71
 
64
72
  - No-write advisories (#498): count only host-captured, completed owner turns bound to the current acceptance of an `implement` or `bulk_edit` task. Require a known empty snapshot diff, unchanged HEAD/shared branch refs and no task-tool effect; changes, effects, pending/unknown effects or unknown captures break the streak. Treat empty canonical-root captures as unknown when an owned task records branch/commit, worktree paths or SDLC PR/head evidence. Require exclusive Git worktree scope at both boundaries and no `.worktrees` directory before trusting negative canonical evidence; uncommitted and detached worktrees otherwise remain unknown. Keep builtin-owner D1 inert while `.worktrees` exists; narrow this deliberate false-negative ceiling only after binding each owner to its real execution worktree. Keep `task_sweep.no_writes_turns` independent of the time sweep (default 2, 0 disables), and route structured `no-writes` stalls through the existing public-title planner notice and grooming paths.
65
73
  - At done, add `no writes recorded since acceptance` to review only when the daemon proves no owner touches since the first acceptance after the current ownership boundary and an empty acceptance-to-now diff, with no branch, review head or PR evidence. Preserve that baseline and earlier touches across plan-revising re-acceptance. Require a canonical-root D2 result to pass the same elsewhere-hint and exclusive-current-worktree checks before treating it as negative evidence. Bind extra-instance evidence to its current verified execution owner/root; missing, private, disabled, expired-budget or restart-lost baselines stay unknown. Capture through the guarded `TasksDeps.ownerAccepted` callback for explicit accept/self-claim only; an implicit done acceptance has no pre-work baseline and remains unknown. Do not capture private work for this advisory, block done, or accept evidence flags from models.
74
+
75
+ - `ahub report --dogfood` (#687, `src/hub/dogfood.ts`, `src/hub/dogfood-read.ts`) opens `hub.db` read-only with byte and row caps; an unavailable or incomplete source reports `null`, never 0, and per-PR GitHub reads run only when the task privacy inventory is complete.