@staix/agent-hub 0.12.28 → 0.12.30

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 (68) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +79 -219
  3. package/docs/agent-notes/README.md +12 -0
  4. package/docs/agent-notes/adapters.md +1 -1
  5. package/docs/agent-notes/bus.md +3 -1
  6. package/docs/agent-notes/daemon.md +13 -0
  7. package/docs/agent-notes/local-worker.md +2 -2
  8. package/docs/agent-notes/tasks.md +9 -0
  9. package/docs/agent-notes/tests.md +1 -1
  10. package/docs/memory.md +70 -152
  11. package/docs/operations.md +237 -14
  12. package/docs/plans/2026-10-16-amem-failure-registry.md +101 -0
  13. package/docs/plans/2026-10-16-amem-p0-baseline.md +110 -0
  14. package/docs/plans/2026-10-16-amem-p2-read-tools.md +28 -0
  15. package/docs/plans/amem-cases.json +482 -0
  16. package/docs/quickstart.md +9 -2
  17. package/docs/security.md +39 -2
  18. package/docs/smoke.md +65 -0
  19. package/docs/specs/2026-09-19-agent-hub-design.md +113 -5
  20. package/docs/verification/2026-10-16-0.12.29.md +25 -0
  21. package/docs/verification/2026-10-16-273-live-verification.md +150 -0
  22. package/docs/verification/2026-10-17-0.12.30.md +70 -0
  23. package/docs/verified.json +13 -13
  24. package/package.json +5 -2
  25. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  26. package/plugins/agent-hub/server.js +64 -44
  27. package/src/cli/console-state.ts +92 -20
  28. package/src/cli/console.ts +25 -6
  29. package/src/cli/help.ts +4 -2
  30. package/src/cli/identity-audit.ts +2 -2
  31. package/src/cli/identity.ts +15 -1
  32. package/src/cli/main.js +2 -2
  33. package/src/cli/main.ts +78 -39
  34. package/src/cli/memory.ts +87 -187
  35. package/src/cli/output.ts +6 -0
  36. package/src/cli/recovery-package.ts +59 -10
  37. package/src/cli/upgrade-runtime.ts +60 -18
  38. package/src/cli/upgrade.ts +8 -0
  39. package/src/hub/board.ts +2 -1
  40. package/src/hub/bus.ts +33 -12
  41. package/src/hub/daemon.ts +141 -31
  42. package/src/hub/envelope.ts +30 -0
  43. package/src/hub/events.ts +1 -1
  44. package/src/hub/git.ts +2 -38
  45. package/src/hub/hub-tools.ts +5 -1
  46. package/src/hub/lifecycle.ts +2 -2
  47. package/src/hub/manager.ts +15 -11
  48. package/src/hub/memory-candidates.ts +103 -0
  49. package/src/hub/memory-fail.ts +25 -0
  50. package/src/hub/memory-observations.ts +47 -0
  51. package/src/hub/memory-plan-guard.ts +33 -0
  52. package/src/hub/memory-policy.ts +42 -0
  53. package/src/hub/memory-profile-check.ts +170 -0
  54. package/src/hub/memory-profile.ts +192 -0
  55. package/src/hub/memory-staleness.ts +55 -0
  56. package/src/hub/path-policy.ts +8 -0
  57. package/src/hub/project.ts +4 -37
  58. package/src/hub/read-tools.ts +127 -0
  59. package/src/hub/tasks.ts +201 -41
  60. package/src/hub/ui.ts +10 -7
  61. package/src/local/deny.ts +4 -17
  62. package/src/memory/brief.ts +8 -5
  63. package/src/memory/capture.ts +2 -1
  64. package/src/memory/recall.ts +50 -110
  65. package/src/pi/process-signature.ts +2 -42
  66. package/src/memory/import.ts +0 -90
  67. package/src/memory/store.ts +0 -805
  68. package/src/memory/sync.ts +0 -145
package/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the previous repository, archived on 2026-09-30 when this repository's history was rewritten; the one exception is the open smoke-check issue, formerly #12, which moved here as #1. Numbers in newer entries refer to this repository.
4
4
 
5
+ ## 0.12.30
6
+
7
+ Memory now uses the exact-pinned first-party `@staix/agent-mem` 0.3.0. Do not migrate a memory home until every hub using it runs 0.12.30; run `ahub memory profile-check`, then quiesce and close all consumers before changing profiles. Shared v1 stays the default, with no automatic data migration.
8
+
9
+ - Consume the extracted agent-mem library, retain hub privacy policy, and preserve the exact dependency beside recovery sources so upgrades remain self-contained (#412, PR #423).
10
+ - Bind the selected shared-v1 or generation memory profile, stop memory features on invalid or changed selection without stopping the bus, and add person-only profile-check readiness receipts (#412, PR #430).
11
+ - Drop automatic approval noise from recall, keep complete words and memory headers, and deduplicate restored presence blocks and note references per delivery (#412, PR #419).
12
+ - Let sandboxed peers search, show and graph screened project memory through bounded, privacy-aware hub tools (#412, PR #420).
13
+ - Add advisory fail-first plan briefs, structured fail notes, person-only decay review and file-level staleness hints without changing stored evidence (#412, PR #425).
14
+ - Record screened review failures and agent corrections as machine-local candidates, plus rendered/delivered/cited counters kept outside recall, briefs and graph evidence (#412, PR #426).
15
+ - Add revision-checked task assignment/unassignment and queue completion, retry and discard controls in the console, preserving person-unassigned work across automatic routing (#424, PR #427).
16
+ - Notify a former owner to stop after supersession, while requiring that peer's own acceptance since its latest assignment before treating an unread task as received (#424, PR #429).
17
+ - Wait for restored managed peers to attach before preserved-session verification; retain terminal receipts and bounded timeout diagnostics so resume creates no second terminal (#414, PR #415).
18
+ - Separate portable memory storage from hub privacy policy and platform integration as the seam for the agent-mem package (#412, PR #421).
19
+ - Freeze the read-only memory baseline, 60-case failure registry and outstanding live-leg ownership before the memory changes (#412, PR #417).
20
+ - Rewrite the README around current capabilities, direction, installation and the documentation map, with operational detail in the operations guide (#412, #418, PR #422).
21
+ - Move website source and deployment to the separate agent-hub-site repository while keeping the public site address (#411, PR #413).
22
+
23
+ ## 0.12.29
24
+
25
+ - Let an explicitly authorized agent run live verification through `ahub --live-test`, with operational console authority scoped to the invocation and its ahub continuations, the initiating peer retained in a mandatory delegation audit, and ordinary agent permissions unchanged (#408).
26
+
27
+ - Remove the site caveat card accent edge and refresh the #273 live-verification ledger, including the machine-wide upgrade isolation requirements (#406, #273).
28
+
5
29
  ## 0.12.28
6
30
 
7
31
  - Recover an offline Pi owner with person-selected preserved history via `ahub pi --replace-session --session-file`, retaining prior identity metadata and queued work while refusing live owners and unsettled effects (#403).
package/README.md CHANGED
@@ -1,237 +1,97 @@
1
1
  # agent-hub
2
2
 
3
- Native multi-agent hub for one developer's machine: Claude Code, Codex, Kimi Code, Pi and a
4
- hub-owned local-LLM worker, with optional Qwen Code and Jcode support, collaborate as peers in independent project directories, with
5
- task-aware model routing (Switchyard) in front of a self-hosted gateway (OmniRoute).
6
-
7
- Status: 0.12.28, control protocol 16. Durable delivery records distinguish queued
8
- work from uncertain execution. The [smoke checklist](docs/smoke.md) records
9
- verified paths and remaining prerequisites.
10
-
11
- Read the [operations guide](docs/operations.md) for the daily workflow, queue
12
- reconciliation, and the staged upgrade command for supported source protocols.
13
- Controlled recovery preserves work; uncertain effects are never automatically
14
- replayed and may require operator review.
15
-
16
- [Hub memory](docs/memory.md) keeps optional local short- and long-term notes with
17
- a rebuildable graph index. It runs alongside native claude-mem capture/recall,
18
- which remains a separate service. Machine sync requires a configured remote;
19
- no remote is created by startup.
20
-
21
- ## Start here
22
-
23
- Requires [Bun](https://bun.sh) >=1.3.0 on PATH; Node alone cannot run the CLI.
24
- The npm package is `@staix/agent-hub`, under the `staix` organization.
25
-
26
- Install from npm:
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 and Jcode optional)
5
+ share a message bus and a task board per project and one memory on the machine,
6
+ each attached through its own native interface.
7
+
8
+ Status: 0.12.30, control protocol 16. Bun + TypeScript, one loopback daemon per
9
+ project. Site: [agent-hub.staix.net](https://agent-hub.staix.net).
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 owners of overlapping work are told what
20
+ changed.
21
+ - **Shared memory.** Decisions, contracts, fails and findings go to the hub's own
22
+ store through the exact-pinned first-party `@staix/agent-mem` library (short-
23
+ and long-term tiers, a typed graph, git sync between machines),
24
+ next to optional claude-mem capture. Entries belong to their project unless
25
+ saved as shared, which makes them readable from every project on the machine.
26
+ Agents get memory as a recall preface, as briefs on a task, and through bounded
27
+ `hub_memory_search`, `hub_memory_show` and `hub_memory_graph` tools. Agents
28
+ can propose corrections; candidates stay outside evidence until a person acts.
29
+ - **Local and remote models.** Pi and the local worker run on DGX or local MLX
30
+ through an authenticated relay; Switchyard routes model calls by task in front
31
+ of a self-hosted OmniRoute gateway. Tasks flagged as PII stay on the on-prem
32
+ worker.
33
+ - **Guard rails.** The local worker and Pi run in a sandbox, and by default their
34
+ writes and shell commands wait for the person's approval; a permission mode
35
+ the person picks (`ask-when-needed`, `never-ask`) skips some or all prompts.
36
+ Budgets pause a peer before it runs out of quota, and recovery never replays
37
+ an effect whose outcome is unknown.
38
+ - **Operations.** A terminal console and a local dashboard, one hub per
39
+ repository or worktree with a machine-wide view, and `ahub upgrade` that carries
40
+ running sessions across versions. The console supports revision-checked task
41
+ assignment/unassignment and delivery-queue resolution.
42
+
43
+ ## Where it is going
44
+
45
+ - **Curated memory** ([#412](https://github.com/STAIxBWLB/agent-hub/issues/412)).
46
+ The memory core now lives in [agent-mem](https://github.com/STAIxBWLB/agent-mem)
47
+ (`@staix/agent-mem`, with the `amem` CLI), which agent-hub uses as its one
48
+ exact-pinned first-party dependency. Next, memory becomes curated rather than
49
+ captured: compression, removal, correction
50
+ and failure-driven management, each shipped only after its contract and a
51
+ measurement.
52
+ - **Every peer able to do its share** ([#418](https://github.com/STAIxBWLB/agent-hub/issues/418)).
53
+ Sandboxed peers read a task's spec and run tests in isolated containers through
54
+ hub-owned tools instead of declining the work.
55
+ - **Measured, not assumed.** Add-ons and memory are kept or dropped by controlled
56
+ comparisons ([#248](https://github.com/STAIxBWLB/agent-hub/issues/248),
57
+ [#258](https://github.com/STAIxBWLB/agent-hub/issues/258)).
58
+
59
+ ## Install
60
+
61
+ Requires [Bun](https://bun.sh) 1.4.2 or later on PATH.
27
62
 
28
63
  ```bash
29
64
  bun add -g @staix/agent-hub && ahub setup
30
65
  cd <your project> && ahub init && ahub up
31
66
  ```
32
67
 
33
- Or install the same version from GitHub:
68
+ `ahub init --dry-run --json` shows what init would write first. While hubs run,
69
+ `ahub upgrade` moves them to the latest release.
34
70
 
35
- ```bash
36
- bun add -g github:STAIxBWLB/agent-hub#v0.12.28 && ahub setup
37
- ```
38
-
39
- The installed commands remain `ahub` and `agent-hub`.
71
+ Before migrating a memory home with `amem`, upgrade every hub using that home to
72
+ 0.12.30, check `ahub memory profile-check`, then quiesce and close all memory
73
+ consumers. Shared v1 remains the default; installing this release does not migrate
74
+ data. See [memory profiles and rollout](docs/memory.md#selected-memory-profile).
40
75
 
41
- To upgrade while hubs are running, run `ahub upgrade` in a terminal. It takes
42
- the latest release, shows the plan for every running project, applies it on
43
- one key and follows it: the operation starts the new hub, installs the Claude
44
- plugin and the global CLI, and restores or restarts the agent sessions as the
45
- plan says. The [operations guide](docs/operations.md#upgrade-and-crash-recovery)
46
- has the details, and the one-time start from an older CLI. With no hub running
47
- there is nothing to carry over: install the new version as above and run
48
- `ahub setup`. Afterwards run `ahub init` in a project to refresh its managed
49
- agent instructions.
76
+ ## Documentation
50
77
 
51
- - [Quickstart](docs/quickstart.md): install, first session, the local worker, the commands of a working day
52
- - [Operations guide](docs/operations.md): managed launchers, task/review flow, receipts, shutdown and recovery
78
+ - [Quickstart](docs/quickstart.md): install, the first session, a working day
79
+ - [Operations guide](docs/operations.md): console, task flow, Pi and models, dashboard, projects, upgrade and recovery
80
+ - [Hub memory](docs/memory.md) and [agent-mem](https://github.com/STAIxBWLB/agent-mem)
53
81
  - [Security notes](docs/security.md): what the hub defends and what it does not
54
- - [Design spec](docs/specs/2026-09-19-agent-hub-design.md): how it is built and why, with every amendment made along the way
55
- - [Smoke checklist](docs/smoke.md): the live checks, and what has and has not been verified against real agents
82
+ - [Design spec](docs/specs/2026-09-19-agent-hub-design.md): how it is built and why
83
+ - [Smoke checklist](docs/smoke.md): what has been verified against real agents
56
84
  - [Changelog](CHANGELOG.md), [Contributing](CONTRIBUTING.md)
57
85
 
58
- ## What ahub changes on your machine
59
-
60
- `ahub init --dry-run --json` lists missing project defaults, the managed AGENTS.md
61
- block, legacy CLAUDE.md cleanup and gitignore updates before writing them.
62
- The preview does not register the project.
63
-
64
- `ahub claude|codex|kimi|pi --print-command` (also `--dry-run`) prints the native
65
- argv, Claude session settings and environment variable names without launching.
66
- It uses the normal command builders. Arbitrary user arguments, configured commands
67
- and settings are redacted; runtime-assigned endpoints and session identities are
68
- explicitly unresolved. It does not check executable or account readiness.
69
- See [Security notes](docs/security.md).
86
+ `AGENTS.md` and [`docs/agent-notes/`](docs/agent-notes/) hold the rules coding
87
+ agents follow when they work on this repository.
70
88
 
71
- ## Why not agent-bridge
89
+ ## Origin
72
90
 
73
- [raysonmeng/agent-bridge](https://github.com/raysonmeng/agent-bridge) proves the
74
- Claude <-> Codex pairing. Its core is two-party by construction (single Claude seat,
75
- `source: claude | codex`), and its v3 rooms carry signals only. agent-hub starts from
76
- an N-peer message bus and attaches each agent through its native control surface.
91
+ The first idea came from [agent-bridge](https://github.com/raysonmeng/agent-bridge),
92
+ which lets Claude Code and Codex talk to each other. agent-hub has since grown
93
+ into a different system with its own goals and shares no code with it.
77
94
 
78
- ## Peers and their native surfaces
79
-
80
- | Peer | Inbound | Outbound | Busy handling |
81
- | --- | --- | --- | --- |
82
- | Claude Code | channel plugin push (`notifications/claude/channel`) under `ahub claude`; otherwise tools-only, read with `hub_inbox` | MCP tools | queued by Claude Code, grouped on next turn; tools-only: queued at the hub |
83
- | Codex | app-server `turn/start` (idle) / `turn/steer` (busy) | proxy intercepts `item/agentMessage` | steer or queue |
84
- | Kimi Code | TUI: pull with `hub_inbox`; headless: ACP `session/prompt` | TUI: `hub_send`; headless: ACP `session/update` stream | TUI: hub queue with important-count hints; headless: queue (`turn.agent_busy`) |
85
- | Pi | RPC or managed native TUI extension | hub-approved tools | queued until `agent_settled` |
86
- | Local worker | hub-native agent loop | hub-native | hub-owned |
87
-
88
- ## Pi with DGX and MLX
89
-
90
- Pi is optional. Install Pi separately and set `pi.enabled` to `true` in
91
- `.agenthub/config.json`. Set `pi.auto_start` to `true` to start it with the hub.
92
- Pi starts in its TUI by default; `ahub pi --headless`, or the start mode
93
- setting (`ahub settings set peers.pi.start_mode headless`), asks for the
94
- background peer instead. The project uses its existing OmniRoute connection for DGX;
95
- `pi.dgx_coding` and `pi.dgx_fast` select the gateway's configured model IDs.
96
-
97
- ```bash
98
- ahub models setup # prepare bounded Qwen3.5 4B MLX in an existing Ollama service
99
- ahub models start # verify loopback Ollama/model readiness (no daemon spawn)
100
- ahub models status
101
- ahub pi --headless --backend auto
102
- ahub say @pi "Read the project and summarize the next small implementation task"
103
- ahub pi # its TUI in this terminal; an idle headless session moves into it
104
- ```
105
-
106
- The default routing template assigns implementation, bulk edits and tests to
107
- DGX, and summaries/triage to MLX. Pi and the existing local worker take priority
108
- before cloud peers; planning and final review stay with Claude/Codex. Existing
109
- projects keep their routing file, so add `pi` to the relevant `peers` lists and
110
- set `pi_backend = "dgx"` or `"mlx"` for each class. PII tasks remain local-worker
111
- only. `--backend dgx` or `--backend mlx` explicitly pins a Pi session's backend.
112
-
113
- Pi connects to an authenticated relay with model aliases `dgx/coding`,
114
- `dgx/fast`, and `mlx/fast`; upstream gateway credentials stay in the hub.
115
- Its managed tools use the existing path guards, shell sandbox and terminal
116
- approval flow. Keep `ahub console` open to approve write/edit/shell operations:
117
- a request nobody answers expires, and two in a row end Pi's turn.
118
- Tool-call receipts survive daemon restart; an interrupted operation with an
119
- unknown outcome must be reconciled before repeating it. Cancelling a Pi turn
120
- does not automatically hand its task to a cloud peer.
121
-
122
- The `mlx/fast` alias now defaults to an externally managed Ollama MLX backend.
123
- Install and start Ollama separately, with `OLLAMA_NO_CLOUD=1`,
124
- `OLLAMA_NUM_PARALLEL=1`, `OLLAMA_MAX_LOADED_MODELS=1`, and a finite
125
- `OLLAMA_KEEP_ALIVE` (five minutes is the tested setting). `models setup`
126
- prepares `agenthub-fast-mlx:4b-8k` from `qwen3.5:4b-mlx`, with an 8K total
127
- context and a 2K output cap. The relay budgets input separately at 6,000
128
- estimated tokens and serializes local requests across project hubs.
129
-
130
- `models status` distinguishes model availability from memory residency.
131
- `models stop` refuses to unload an externally managed Ollama model: other
132
- clients may be using it. Ollama owns idle eviction; stopping a hub never
133
- kills Ollama. The OpenAI compatibility endpoint does not enforce a request
134
- `keep_alive` field, so configure the server and verify its expiry readback.
135
- The relay can fall back to DGX before streaming starts as before. See the
136
- [Ollama migration procedure](docs/operations.md#ollama-mlx-migration-issue-51)
137
- for existing Python runtime/configuration cleanup and rollback.
138
-
139
- A project has one managed Pi session owner. Mode changes require the agent to
140
- settle and preserve the session file. Before the first generation, a live source
141
- verified to be empty can instead retain its exact session ID without a file.
142
- Missing history from a nonempty session blocks handover. Protocol 9 adds Pi
143
- recovery metadata. Protocol 10, released in 0.7.0, adds durable delivery
144
- receipts and conservative queue recovery; it is verified live (see the
145
- [smoke checklist](docs/smoke.md)).
146
- Older protocol hubs still require their matching CLI and an attended bootstrap.
147
-
148
- ## Runtime
149
-
150
- Bun + TypeScript. Single daemon per project directory, loopback only.
151
-
152
- ## Local dashboard
153
-
154
- With a running daemon, `ahub ui` opens a local dashboard for messages, peer states
155
- and queues, the task board, budget windows, pending approvals and this machine's
156
- benchmark runs. Use `ahub ui --no-open` to print a single-use link when the
157
- browser cannot be opened.
158
- Open the link within 60 seconds; the browser session lasts one hour.
159
-
160
- The dashboard can send console messages, propose and assign tasks, pause or resume
161
- peers, answer approvals, start a peer in its start mode and show the settings.
162
- An ordinary session changes the safe switches and lowers a mode to `ask`; the
163
- rest needs `ahub ui --settings` or `ahub settings` in a terminal. See
164
- [Settings](docs/operations.md#settings-from-the-dashboard-and-the-terminal) and
165
- [Start modes](docs/operations.md#start-modes-tui-by-default-headless-as-opt-in).
166
- Budget pauses still require the terminal override.
167
- Private envelopes and PII tasks stay redacted. Local-worker and Pi tool details and
168
- allow decisions stay in `ahub tail` / `ahub permit`; the page can deny them.
169
- No dashboard port is opened until requested. See [the session security model](docs/security.md#local-dashboard-sessions-issue-6).
170
-
171
-
172
- ## Multiple projects and worktrees
173
-
174
- Version 0.4.0 adds one independent hub per repository or Git worktree.
175
- Subdirectory commands resolve to that project; symlink aliases resolve to the same
176
- hub. Explicit selection works from any directory:
177
-
178
- ```bash
179
- ahub --project /path/to/project-a init
180
- ahub --project /path/to/project-b init
181
- ahub --project /path/to/project-a up
182
- ahub --project /path/to/project-b up
183
- ahub projects
184
- ahub status --all
185
- ahub --project /path/to/project-a codex
186
- ahub ui --all
187
- ```
95
+ ## License
188
96
 
189
- The unified dashboard lists registered roots and opens their boards, streams,
190
- messages, approvals and budgets. It can start hubs and stop a selected hub after
191
- confirmation. Its Start control opens a peer's TUI only through a terminal provider
192
- (`terminal.open` or Orca); without one, launch Claude/Codex TUIs from a terminal. The manager stays available
193
- when a project stops; `ahub ui --all --stop` stops only the manager. Existing
194
- `ahub ui` still opens the current project's dashboard.
195
-
196
- `ahub projects --json` returns machine-readable live status. Use a listed ID or a
197
- path with `--project`; the selector must precede the command. `ahub projects remove
198
- <id>` forgets a stopped registration without deleting project data. A moved root
199
- gets a new registration. Missing roots remain visible until removed.
200
-
201
- Runtime state stays in each project's `.agenthub/state/`. The machine registry is
202
- `~/.agenthub/registry.db`; existing `ports.json` allocations are imported once.
203
- `AGENTHUB_HOME` selects an isolated machine registry (also used by tests). An
204
- inherited state directory from a different project cannot redirect a new CLI
205
- invocation. Explicit custom state requires a matching project marker or the
206
- initialization pair `AGENTHUB_PROJECT_DIR` / `AGENTHUB_STATE_DIR`.
207
-
208
- Manager-started hubs use attended mode and project configuration, including
209
- credential-file references. They do not inherit the launching project's gateway
210
- URL/key overrides. Native memory remains shared according to claude-mem aliases;
211
- worktrees recall their parent and composite identity. A shared-alias notice does
212
- not mean that task boards or agent sessions are shared. Provider quota readings
213
- may describe the same account and should not be added across projects.
214
-
215
- Before upgrading an older hub, inspect the plan with the matching CLI.
216
- Incompatible or unverified processes are shown explicitly and are never killed by
217
- PID-name matching. See the [operations guide](docs/operations.md) and the
218
- [multi-project specification](docs/specs/2026-09-19-multi-project-design.md).
219
-
220
- ## Agent instructions
221
-
222
- `AGENTS.md` holds what every agent session loads: commands, conventions,
223
- architecture and the rules that hold anywhere in the code. Rules that hold in one
224
- area (benchmarks, adapters, the bus, the local worker, models, the task board,
225
- budget, the daemon and recovery, tests) live in [`docs/agent-notes/`](docs/agent-notes/),
226
- and `AGENTS.md` names the paths that call for each note, so a session loads only
227
- the notes for what it edits (#158). Background behind some of those rules:
228
-
229
- - Benchmark trust entry: the review rounds of #115 kept finding paths that broke
230
- the runner's settlement of the Claude trust entry.
231
- - Reply addressing: leaving `to` empty fanned a reply out to every peer, which is
232
- what made one directed question cost every agent a turn.
233
- - Bun test hang: the stacks of two local hangs show a test timeout starting the next
234
- test inside `Bun.spawnSync`'s own event loop (#115,
235
- `docs/verification/2026-10-03-0.12.6.md`); minimal reproductions did not hang, so
236
- the full trigger is not pinned down. On Bun 1.4.2 the next test still starts there
237
- (#121, checked directly); the spin was not reproduced on demand on either version.
97
+ MIT
@@ -3,3 +3,15 @@
3
3
  Human explanation. Agent instructions: the area notes in this directory.
4
4
 
5
5
  On 2026-10-13, two PR #368 reviews found that a missing delivery id or a missing visible row could select another Queue row. The console navigation rule in daemon.md records the exact-row contract. The missing-row and ID-less secondary-delivery regression tests demonstrate both cases.
6
+
7
+ Background behind some of the rules, moved from the top-level README on 2026-10-16:
8
+
9
+ - Benchmark trust entry: the review rounds of #115 kept finding paths that broke
10
+ the runner's settlement of the Claude trust entry.
11
+ - Reply addressing: leaving `to` empty fanned a reply out to every peer, which is
12
+ what made one directed question cost every agent a turn.
13
+ - Bun test hang: the stacks of two local hangs show a test timeout starting the next
14
+ test inside `Bun.spawnSync`'s own event loop (#115,
15
+ `docs/verification/2026-10-03-0.12.6.md`); minimal reproductions did not hang, so
16
+ the full trigger is not pinned down. On Bun 1.4.2 the next test still starts there
17
+ (#121, checked directly); the spin was not reproduced on demand on either version.
@@ -24,7 +24,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
24
24
  - A peer must set `busy` synchronously inside `deliver()`, otherwise the bus drains the next envelope into a running turn (Kimi answers `turn.agent_busy`). Outside a turn the adapter accepts an occupancy diagnostic (a `context-used` `usage_update` naming its current session) and nothing else: no token total, no chunk, nothing unnamed or for an offline peer (#285).
25
25
  - A watchdog-cancelled turn still reports later. Anything a turn does on completion must check it is still the current turn (`turn` generation in `acp.ts`, `activeTurns` in `codex-appserver.ts`). A rejected prompt other than a recognised quota refusal, or an abnormal prompt end, reports through the adapter's `onTurnFailure` (same shape in `acp.ts` and `pi.ts`), even when ACP streamed partial text; a stale cancelled turn never reports it; a rejected watchdog-cancelled ACP prompt during its journal-less wait is discarded with one log line, not requeued as on the earlier adapter.
26
26
  - Native owner teardown must handle a lost shutdown acknowledgement using verified process identity. Pending launches must be revocable, and active tools/streaming output must count as watchdog activity.
27
- - Pi owner identity is one shared contract, `processSignature` in `src/pi/process-signature.ts`; the startup claim check, the extension's claim, the TUI owner monitor, `recoveryReady` and the verified teardown all go through it. Its `ps` read pins LC_ALL=C and TZ=UTC (the `processTable` contract) because `lstart` follows the reader's timezone and locale, and a hub on UTC with a Pi child on system time otherwise hashes different strings for the same process and refuses the valid owner (#174). Strict pid + start + command identity stays; never accept PID-only or a fuzzy command match.
27
+ - Keep Pi owner identity on the shared `processSignature`/`processLiveness` contract exported by the exact-pinned package's Node-safe `@staix/agent-mem/platform` entry, re-exported unchanged by `src/pi/process-signature.ts` for the startup claim, extension claim, TUI owner monitor, `recoveryReady` and verified teardown. Memory's `sync.lock` uses the same implementation. Preserve the Node-safe process-entry regression in `test/memory-parity.test.ts`, the existing Pi process behavior tests, the LC_ALL=C/TZ=UTC `ps` read and strict pid + start + command identity; never accept PID-only or a fuzzy command match.
28
28
  - Return the shared `toolResultFailed` verdict as `/tool`'s `failed`; map only `failed === true` to Pi's `isError`. Preserve text and the older bridge's absent-flag behavior. The one exception is a person's `!command` whose process exit was observed: its `failed` is `exitCode !== 0` and the reply carries `exitCode`, from `onProcessExit` through `packPiShellResult`, never parsed from the output (#253).
29
29
  - Pi's bridge executes `/tool` only for the verified owner (`verifiedOwner()`: the claimed pid with its `processSignature`; a headless owner is pinned from the spawned process once `get_state` proved its session) with the current `sessionId` and turn `generation`, and admits `/budget` only for that owner and generation, checked again after the awaited admission. Refuse anything else before execution. `tool_abort` aborts that pending call, or fences its id when it arrives before the call (64 ids a turn, then every further call of that turn). A person's `!command` (`purpose: "idle_user_bash"`) is taken only in TUI mode from an idle owner on its one-use reservation, and it alone skips `piPermit`: never widen that to a model's bash call (#253).
30
30
  - 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.
@@ -15,7 +15,9 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
15
15
  - `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.
16
16
  - A pull-only peer (a Claude session without channel pushes or Kimi in its own TUI) is never drained; `Bus.pull` hands it what one push would take. 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.
17
17
  - 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.
18
- - `resolveDelivery` mutates queues in place; a refused or failed resolution puts back only that peer's queue and preface and keeps every pause. Never reload a snapshot on a live bus.
18
+ - 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.
19
+ - Observe memory rendering from final `out`, not `originals`, and suppress telemetry when either contains private envelopes. A pull emits rendered/delivered observations only after its journal transaction commits; observer exceptions must never change delivery settlement.
20
+ - `resolveDelivery` mutates queues in place; a refused or failed resolution puts back only that peer's queue and preface and keeps every pause. Never reload a snapshot on a live bus. Keep the selected delivery id and journal revision fixed in console row actions; retry/discard/completed never change a task role or cancel a native turn.
19
21
  - `restore` takes queued work from the journal, never from the committed snapshot once the journal has state (an empty journal, revision 0, is seeded from the commit): from the commit only an empty queue key is carried over, for an offline peer whose key no delivery ever persisted (#234). Never resurrect a non-empty queue from it.
20
22
  - The journal's owner check (`rejectLiveOwner` in `delivery-journal.ts`) judges the manifest pid with `processLiveness` and its `pidSignature`, never a bare probe (rule: `daemon.md`).
21
23
  - A hub-native peer (Pi, the local worker) does not get to call its own message `important`: `capPriority` caps it unless the delivery it answers held an `important` envelope addressed to it. Check the delivery, not `replyParent`, which ties on hop and takes the later item. Do not bypass it by setting `priority` in the adapter.
@@ -1,5 +1,8 @@
1
1
  # Daemon, control protocol and recovery
2
2
 
3
+ - For explicitly user-authorized live verification, use the per-invocation CLI delegation in `docs/security.md` §Agent CLI identity and conductor authority; keep caller markers and record native evidence separately from fixtures.
4
+ - Keep `export const LIVE_TEST_SUPPORTED = true;` in `src/cli/identity.ts`: delegated recovery staging reads this declaration before any source effect.
5
+
3
6
  - Console attention navigation selects a delivery only by an exact visible row id. A missing id or row prints read-only guidance; never select by peer or fall back to the first Queue row.
4
7
 
5
8
  Scope: the operator console, control WS and its protocol, state files, the dashboard listener and settings, the Claude channel's reconnect, `ahub setup`, git snapshots, upgrade and recovery. Read before editing `src/cli/console.ts`, `src/cli/console-state.ts`, `src/cli/attention.ts`, `src/ui/needs.ts`, `src/hub/daemon.ts`, `src/hub/ui.ts`, `src/hub/settings.ts`, `src/hub/control-client.ts`, `src/adapters/claude-channel.ts`, `src/hub/restart.ts`, `src/hub/recovery-store.ts`, `src/hub/snapshots.ts`, `src/hub/manager.ts`, `src/hub/lifecycle.ts`, `src/hub/crash.ts`, `src/hub/start-mode.ts`, `src/hub/terminal-open.ts`, `src/hub/recovery-error.ts`, `src/hub/db-timeout.ts`, `src/cli/setup.ts`, `src/cli/upgrade*.ts`, `src/cli/terminal-recovery.ts`, `src/cli/recovery-package.ts` or `src/cli/facts-hook.ts`, or before adding a native peer.
@@ -11,6 +14,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
11
14
  - `ahub setup` reads Claude Code's state from `--json` listings and takes one step at a time, re-reading after each; never match paths or names by substring.
12
15
  - A copy of the git index keeps the original's mtime (`snapshot()`): git's racy-entry check compares entries with the index file's time in whole seconds, and a fresh copy makes a same-size edit look clean.
13
16
  - State files that clients read (`status.json`, `control-token`) are written after the port is bound, and `status.json` via temp file + rename.
17
+ - Open native memory through the shared profile-aware helper with an explicit current selector binding; never fall back to shared v1 after invalid/missing generation metadata. Check that binding at hub call-site entry and before candidate/counter writes, including callbacks retained across awaits; do not intercept store methods with proxies/private hooks or claim atomic in-flight cutover. Quiesce and close consumers before migration/selection changes, then reopen through the supported lifecycle. Keep the sidecar at the independent memory root, not inside the selected generation. Report memory availability separately from bus health; never hot-rebind or migrate as a startup repair (`docs/memory.md` §Selected memory profile).
14
18
  - The channel's reconnect loop stops on closes a retry cannot fix (`TERMINAL_CLOSES` in `claude-channel.ts`); a new daemon close code that means "do not come back" belongs there, or two clients fight over it forever.
15
19
  - Close 4000 is the one close that ends on its own, so it is not in `TERMINAL_CLOSES`: the session stands by and reconnects only once `status.json` reports the peer offline and not `claiming`. One exception (#205, `peerHeld` in `claude-channel.ts`): a session with channel pushes also reconnects when the holder is reported `toolsOnly` and not `claiming`, and takes the peer; the daemon refuses the reverse with 4000. Reconnecting blind would evict whoever holds the id now, and the two would trade it forever. `claim()` happens at hello and `attach()` only after the preface, so the peer reads as offline in between: the daemon writes the status file at the claim and the flag covers that window.
16
20
  - Adding a native peer requires testing the command emitted by the actual recovery driver, not only its inspection or launch helpers. Never let a generic non-Codex branch treat a new peer as Claude.
@@ -20,6 +24,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
20
24
  - Judge every recorded owner pid (hub manifest, registry claim, recovery runner, manager owner, launcher record) with `processLiveness` in `src/pi/process-signature.ts`, never a bare `process.kill(pid, 0)`: after a reboot a recorded pid can belong to another process, even one we may not signal, and the bare probe wedged recovery and `ahub up` (#226). A writer records `processSignature(process.pid)` beside its pid; a registry claim stores it as `<instance id> <signature>` because an older daemon's claim rewrites the pid but not that column. Records without a signature keep the bare-pid answer; unknown is never gone.
21
25
  - A restored daemon accepts a changed native session id (Kimi and local aside: they restart as fresh workers) in two ways only: the #64 rule for a Claude session that never persisted a transcript, and the operation-fenced `recovery-waivers.json`, which the coordinator writes wherever it accepts a session other than the planned one: a reconnect-only Claude (#206), an operator-chosen fresh session, the plan's `freshStart` of a Codex thread the hub saw start with no turn, or a zero-turn Claude it accepted (#215). Never waive on any other evidence. Re-preparation after an expired lease keeps every effect receipt, and `restored:<peer>` = `failed` never counts as restored. Judge a restoration launcher by its `terminal-recovery.json` record and process signature (and a `terminal_handle_stale` answer), never by the terminal's exit; an unreadable record is unknown, never "none".
22
26
  - A target that stopped after it started is restarted only through `recordRestart` (`src/cli/upgrade.ts`), recorded before the new one starts and at most once per project per resume: each done `restored:<peer>` becomes `retired:<peer>`, whose terminal is closed (`closedRetired:<peer>`) or found absent before that peer is relaunched, and is never revalidated against the new instance. Launcher evidence is read for every instance in `restarts`, and a session attached without an id is unknown, never gone (#225).
27
+ - After a managed terminal is created, persist its binding before waiting up to `RECONNECT_WAIT_MS` for authenticated attachment to the expected target instance, operation and conversation (#414). Resume waits on the saved binding without creating another terminal. Timeout keeps the receipt blocked and records only terminal/hub metadata; a last native connect attempt that was never recorded is unknown, never inferred from terminal readiness or offline state. Final identity and preservation verification stays strict.
23
28
  - An operation receipt stays `schema: 1`. Only `abandoned()` writes 2, on an operation with a recorded `disposition` whose coordinator predates #215: in the same write as the disposition, or, for a disposition 0.12.20 recorded at 1, by the next command that holds the runner claim. `validateReceipt` accepts 2 only with a disposition (#227). Never write 2 on an operation that can still be resumed.
24
29
  - Three checks read another release's source text for an exact line: `coordinatorCurrent` (`export async function disposeRecovery` in `src/cli/upgrade.ts`), `targetReadsWaivers` (`export function readRecoveryWaivers` in `src/hub/restart.ts`) and `coordinatorRestarts` (`export function recordRestart` in `src/cli/upgrade.ts`). Renaming, moving or un-exporting one makes this release read as an older one: keep those lines until a capability list replaces the checks.
25
30
  - The interactive screens (`src/cli/upgrade-interactive.ts`, #272) change an operation only by running an existing command (`recovery resume|abort|dispose`, `reset`) through `io.run`, and build their menu from `nextChoices`, whose text is `nextActions`: never call `abortRecovery`, `disposeRecovery` or a driver from a screen, and never offer a choice `next` does not have. The one addition is `cancellableWait` (a runner still at `prepare` with no effect): read the receipt again at the key press, stop only a runner `signedRunner` names (a claim without a matching process signature is never signalled), then run `recovery abort`, which decides again. Agents are ended (`endPlannedPeer`) only before an operation holds the lock, and only after the lock, the hub instance, the attached session and the terminal's handle and incarnation were read again: never on the plan as it was drawn. Ctrl+C leaves the screens and the terminal is not read after it; a prompt answers an interrupt with null, never text. What a hub says it waits for (`recovery.waiting`, the daemon's `blockers`) is shown, never decided on.
@@ -36,3 +41,11 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
36
41
  - Use `KNOWN_PEERS` for the native catalog and `rosterAllows` for participation. Change live membership only at the authorized roster settings write or undo, applying exactly that key. Keep external ids and projects without roster settings compatible with the existing admission and lazy status rules. Fence starts and attachment before detaching or stopping a removed runtime; retain its queue, uncertain deliveries and independent pauses. Refuse removal of the conductor holder and require explicit confirmation and `--now` for an active turn.
37
42
 
38
43
  - A Kimi one-launch TUI admission ends on its owning detach, roster removal, matched launcher cleanup or the unclaimed 30 s deadline. An old socket or launcher never clears a newer admission. Pin --mode tui in the hub-opened terminal argv.
44
+
45
+ - When preserving a recovery source, copy only the exact-pinned `@staix/agent-mem` package through `preserveRuntimeDependencies` in `src/cli/recovery-package.ts`; include its identity and bytes in `packageDigest`. Never copy the whole node_modules tree or link a preserved source to a mutable installation. Verify preserved sources with `ownDependencies: true` so a missing or symlinked memory copy cannot resolve an ancestor installation. Verify staged releases with `dependencyRoot` bound to their retained install directory; never resolve beyond it. Keep no-dependency release digests compatible.
46
+
47
+ - Render staleness only through the call-scoped privacy preflight and bounded metadata probe (`memory-staleness.ts`); on a tree not modified during the call, private/denied/observed-symlink/unknown paths never produce a marker. Retain the explicit same-user concurrent-swap ceiling in docs/memory.md and the probe-loop ponytail. Keep hints out of stored rows and route native brief decoration after selection so one batch shares its probe budget.
48
+
49
+ - Observe memory counters only through the machine-local candidate sidecar. Bind delivered counters to a positive journal acknowledgement and the original render context; skip private envelopes and current PII tasks/turns. Leave missing telemetry unknown, and never pass candidates or counters to evidence consumers. Require memory_correct for correction proposals, refuse PII turns, and keep project/identity authority server-side.
50
+
51
+ - Keep full memory home, generation ID, selector revision and malformed-row totals in authenticated person status/CLI receipts, never disk status.json or agent CLI output; low-privilege receipts use a generation prefix and omit home, selector revision and malformed-row totals. Emit typed unbound disabled/unavailable receipts with no profile when no memory handle opened; profile-check may treat an authenticated same-home generation-capable unbound daemon as memory-quiesced, never a stale bound handle. Scope profile-check readiness to its named registry inventory; old, unknown or unreadable receipts refuse readiness, and other custom registries need separate checks.
@@ -5,9 +5,9 @@ Scope: the hub-native local worker, its tools, path guard, denylist and seatbelt
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
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.
7
7
  - 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
- - Every git call the hub makes outside the sandbox goes through `src/hub/git.ts` (`hubGitArgv`/`hubGitEnv`/`hubGitSync`), which keeps the repository's configuration from naming a program the hub runs (#281); a test walks all of `src/` (src/local/ included) for a direct `spawn("git"` and fails on one outside `src/hub/git.ts` and the sandboxed tool's `src/local/tools.ts`. The sandboxed `git` tool keeps its own behavior: it runs inside the sandbox. `hubGitArgv(args, { credentials: true })` keeps the person's credential helper and is only for the repository the hub creates itself (`~/.agenthub/memory`, #258), never a project's. The submodule `config`/`hooks` refusal costs a submodule whose path has `hooks` as a whole component all git-dir writes (docs/security.md).
8
+ - 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
9
  - 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.
10
- - One denylist, `src/local/deny.ts`: the path guard, the seatbelt profile and the memory filter all read it. Seatbelt sees absolute paths, so `local.deny` entries are anchored under the project root (a bare `private/` once denied all of `/private/var`).
10
+ - One denylist, exported by `@staix/agent-mem` and re-exported by `src/local/deny.ts`: the path guard, the seatbelt profile and the memory filter all read it. Seatbelt sees absolute paths, so `local.deny` entries are anchored under the project root (a bare `private/` once denied all of `/private/var`).
11
11
  - `guardPath` walks with `lstat`: `existsSync` follows symlinks, so a dangling link looked like a new file and the write landed at its target.
12
12
  - git arguments never pass through `guardPath`; `gitArgsProblem` refuses absolute paths, `..`, `--no-index` and denylisted `rev:path` forms.
13
13
  - A worker turn builds its messages in a local array and joins the history only as a whole. Never push to `history` mid-turn: one tool call without its result poisons every later request.
@@ -3,6 +3,9 @@
3
3
  Scope: the task flow, assignment, the state machine and the hub's MCP tools. Read before editing `src/hub/tasks.ts`, `src/hub/board.ts`, `src/hub/routing.ts`, `src/hub/task-sweep.ts`, `src/hub/conductor.ts`, `src/hub/supervision.ts`, `src/hub/tool-refusal.ts` or `src/hub/hub-tools.ts`.
4
4
 
5
5
  - Tool callers are models: MCP `inputSchema` is not enforced on the way in. Normalize at the boundary (`cleanRefs`) before anything reaches the board, and never throw after a board write: a publish or notify that follows a `review()` or `done()` board write goes through the one `releaseNotice` guard, which turns a failed publish into a console notice, drops a failed notify and never retries (#231, #243). An assignment's delivery once its board write is saved (#276, #297): the move is fact, the failed publish is not retried in place (the journal's own recovery covers a storage error, and a retry may duplicate a publish that actually left), and a person looks for a queued or needs_review delivery with `ahub queue list --peer <owner>` (then `ahub queue show <id>`, or the accepted entry in `ahub task show <id>` if the journal is down) before re-sending with `ahub task assign <id> <owner>`. The distinction is made once, in `assignOwner`: a failure after the write prints the one console notice from `undeliveredNotice` right there, once, and throws `AssignmentUndeliveredError` carrying the saved task; callers (`review()`'s repeated rejection, `escalate` and its callers, the ready offer, both relays, the idle sweep, rescreen) decide control flow by the type and print nothing more, and `propose`, `assignTo` and `decline` additionally let the error reach the caller in its turn, which is a person or peer that can act. Two exceptions get no notice, by construction: a row superseded meanwhile, meaning a later history entry that moves or settles the task (another assignment, an unassign, the owner's accept, done, approval, close), while entries that do neither (the hub's quiet bookkeeping: answers, reviewer changes, idle sweeps, check results) leave the assignment undelivered and the notice stands, so the original error goes to the caller as before only on supersession; and a claim's own accepted write and "claimed by" line stay outside the delivery rule (a claim is its own hand-over; its overlap and routing announcements to earlier owners are deliveries and share the wrapper, so a failure there does read as one). A failure before the write stays a plain `Error` (#254), a failed write leaves the ready offer unconsumed (#231), and the notices before the write (waits, no-owner, the reserved-owner line) go through `releaseNotice` too, so those pre-write notices cannot fail their caller.
6
+ - Post-await assignment publication is fenced by current role authority: supersession returns the current task without a saved-delivery notice; reviewer-only changes refresh current metadata and do not suppress a valid owner delivery. A replacement notice goes only to a role that actually received the request, not to an unread-only queue.
7
+ - Console task row assignment/unassignment carries the exact role, task id and `flowRevision` (history length), checks it before effects and passes it to `Board.update`; a changed revision requires reload. Person owner removal clears owner/reservation and suppresses automatic offers until a later explicit person assignment/reservation; refuse owner removal under review or after approval, and preserve the required PII person reviewer.
8
+ - Role withdrawal matches the task id, role kind and owner-request header. Owner acceptance evidence must be that peer's accepted entry after the latest ownership event, never another owner's state or an earlier assignment span. Withdraw unread requests without inventing delivery evidence. Preserve the original former-owner stop notice after a superseding escalation, while suppressing stale success/decision tails.
6
9
  - `assign()` never defaults to the task's current owner, or a decline can only come back to the decliner. A reservation (#207) is the same risk: the reserved owner goes first only through `blocked()`, and a decline or an escalation for reason `rejections` or `manual` records the owner it left in the history entry's `from`, which `excluded()` reads, so no later reroute hands the task back through its reservation.
7
10
  - The state machine allows `in_progress -> approved` only for classes without a reviewer; `review()` checks `in_review` itself.
8
11
  - `.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,3 +22,9 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
19
22
  - A planner patch that names `reserved` at all, the same value included, is refused while `hasPersonReservation` holds; planner pull and grooming both ask it (#378: a same-value write laundered a person's reservation).
20
23
  - `done()` gates the SDLC transition once: pass `gated` to `complete()` when nothing was awaited since, and after any awaited gate readback refuse if another done already moved the task, in every mode (#382: advise recorded overlapping dones).
21
24
  - A changed `reviewedHead`, or one supplied with a changed repo or PR, needs the reviewer or a person; a head dropped because the owner relinked the PR does not (#382: the owner could not relink, and could carry a head to a new PR).
25
+
26
+ - Memory read tools (`hub_memory_search`, `hub_memory_show`, `hub_memory_graph`, #412) take their project only from the daemon. Validate UTF-8 input caps before logs or storage, require `memory_read`, refuse PII turns and exclude arguments/results from capture. Screen complete rows and graph metadata before counting or truncating; keep whole-result byte caps, complete id/provenance/kind headers and one neutral unavailable-id refusal.
27
+
28
+ - Compose structured `hub_remember` fail fields through `composeRememberText` before screening or writes; reject over 4000 UTF-8 bytes without truncation. Treat plan-fail matches as screened advisory priority only, never a state-transition guard; skip all plan-memory work for PII tasks.
29
+
30
+ - Keep candidate production in Tasks: screen review/correction payloads and current task/turn privacy before CandidateStore writes; recheck after asynchronous screening. Never make candidate failures roll back a saved review, or let a peer accept a candidate. Keep candidate tool arguments/results out of capture.
@@ -6,7 +6,7 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
6
6
  - In Bun 1.3.14 a test timeout that fires while `Bun.spawnSync` runs can start the next test inside spawnSync's own event loop, where another spawnSync then spins at full CPU for good (trigger not pinned down; see README). Bun 1.4.2 still runs the next test inside the outer spawnSync's event loop. `scripts/check.sh` runs tests with a 20 s timeout and under `scripts/hang-watch.sh`; a test that spawns and reads the process table gets a timeout of its own.
7
7
  - The Switchyard sidecar, sandboxed commands and the MLX server get a fixed environment (`PATH`, `HOME` and their own keys), so test knobs for their fakes travel in a wrapper script or argv, not in `process.env`; native peer children inherit the hub's environment minus the agent markers (`peerChildEnv`).
8
8
  - Bind agent identity markers explicitly in native/CLI fixture children. The full gate starts the test runner without inherited agent markers and without an `ahub claude` session's hub variables (`AGENTHUB_PROJECT_DIR`, `AGENTHUB_STATE_DIR`, `AGENTHUB_INSTANCE_ID`, `AGENTHUB_LAUNCH_ID`); a new variable a native session exports and a fixture child reads joins that `env -u` list in `scripts/check.sh`. Never weaken production marker detection to make them pass.
9
- - `bunfig.toml` preloads `test/hub-home.ts`, which gives every test run (the gate and `bun test <file>` alike) a temp `AGENTHUB_HOME` unless one is set: hub memory (#258) is on with any project config, and no test may write the person's `~/.agenthub`. Do not remove it; a test that needs its own home sets and restores the variable.
9
+ - `bunfig.toml` preloads `test/hub-home.ts`, which first clears inherited `AMEM_HOME` and gives every test run (the gate and `bun test <file>` alike) a temp `AGENTHUB_HOME` unless one is set; scoped tests may set their own `AMEM_HOME` after preload. Hub memory (#258) is on with any project config, and no test may write the person's `~/.agenthub`. Do not remove it; a test that needs its own home sets and restores the variable.
10
10
  - A free-port probe binds `hostname: "127.0.0.1"`, matching the loopback server that takes the port (#232).
11
11
  - `ahub upgrade` without `--to` asks the registry (`npm view`) and hands a target newer than the CLI to `bun x` (#272): a test that reaches either puts a fake `npm` first on `PATH` and a recording stand-in for `process.execPath`, as `test/upgrade-cli.test.ts` does. The interactive screens are tested through a scripted `ScreenIO`; a CLI test that needs them marks `process.stdin` and `process.stdout` as terminals in a wrapper script.
12
12
  - Cross-process fixtures wait for the consuming client to process its handshake (the channel connected diagnostic), not merely the daemon's attached/idle state. Deadline-routing fixtures hold and release responses at the fallback decision; do not place a fixed sleep between deadline fractions and assume scheduler headroom (#287). Receipt-order fixtures use the normal watchdog for ordinary I/O, trigger expiry after observed native acceptance, and await a consumed transport barrier before asserting on late results.