@staix/agent-hub 0.24.0 → 0.25.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 (70) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +27 -5
  3. package/docs/agent-notes/adapters.md +1 -0
  4. package/docs/agent-notes/bus.md +1 -0
  5. package/docs/agent-notes/daemon.md +15 -2
  6. package/docs/agent-notes/tasks.md +9 -2
  7. package/docs/agent-notes/tests.md +2 -1
  8. package/docs/dogfood.md +12 -2
  9. package/docs/memory.md +1 -1
  10. package/docs/operations.md +137 -4
  11. package/docs/profiles.md +2 -0
  12. package/docs/quickstart.md +10 -3
  13. package/docs/security.md +155 -3
  14. package/docs/specs/2026-09-19-agent-hub-design.md +25 -1
  15. package/docs/specs/2026-09-19-multi-project-design.md +12 -1
  16. package/docs/specs/2026-11-01-research-campaigns-design.md +351 -0
  17. package/docs/verification/2026-11-01-0.25.0.md +91 -0
  18. package/docs/verified.json +31 -15
  19. package/package.json +4 -3
  20. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  21. package/plugins/agent-hub/server.js +5118 -4708
  22. package/src/adapters/claude-channel.ts +29 -6
  23. package/src/adapters/definitions/native-cli-doctor.ts +30 -0
  24. package/src/adapters/jcode.ts +3 -0
  25. package/src/adapters/registry.ts +1 -1
  26. package/src/cli/acon-register.ts +29 -0
  27. package/src/cli/handoff-args.ts +27 -0
  28. package/src/cli/handoff-output.ts +25 -0
  29. package/src/cli/help.ts +6 -3
  30. package/src/cli/identity.ts +7 -1
  31. package/src/cli/main.ts +76 -7
  32. package/src/cli/output.ts +8 -5
  33. package/src/cli/schedule-output.ts +8 -4
  34. package/src/cli/study-output.ts +71 -0
  35. package/src/cli/study.ts +58 -0
  36. package/src/cli/task-output.ts +1 -0
  37. package/src/hub/board.ts +871 -2
  38. package/src/hub/channel-project.ts +61 -0
  39. package/src/hub/control-client.ts +10 -1
  40. package/src/hub/daemon.ts +247 -11
  41. package/src/hub/envelope.ts +2 -0
  42. package/src/hub/events.ts +4 -1
  43. package/src/hub/execution-budget.ts +12 -0
  44. package/src/hub/handoff.ts +126 -0
  45. package/src/hub/hub-tools.ts +12 -2
  46. package/src/hub/manager.ts +64 -57
  47. package/src/hub/no-writes.ts +1 -1
  48. package/src/hub/path-policy.ts +12 -8
  49. package/src/hub/project.ts +14 -5
  50. package/src/hub/provenance.ts +30 -0
  51. package/src/hub/research-campaign.ts +265 -0
  52. package/src/hub/research-experiments.ts +445 -0
  53. package/src/hub/restart.ts +26 -0
  54. package/src/hub/schedules.ts +210 -9
  55. package/src/hub/session-identity.ts +49 -0
  56. package/src/hub/tasks.ts +894 -22
  57. package/src/hub/ui.ts +15 -6
  58. package/src/memory/capture.ts +2 -2
  59. package/src/ui/index.html +5 -3
  60. package/src/ui/office-space.ts +118 -0
  61. package/src/ui/plugin.ts +293 -0
  62. package/templates/AGENTS.block.md +1 -0
  63. package/templates/profiles/general.json +1 -1
  64. package/templates/profiles/research.json +4 -4
  65. package/templates/profiles/writing.json +1 -1
  66. package/templates/roles/research/analyst.md +1 -0
  67. package/templates/roles/research/fact-checker.md +1 -0
  68. package/templates/roles/research/research-lead.md +1 -0
  69. package/templates/roles/research/researcher.md +1 -0
  70. package/templates/roles/research/statistician.md +1 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.25.0
8
+
9
+ - Pin `@staix/agent-mem` 0.18.0 (was 0.17.0), which adds forward-tolerant amem hook-spool handling, an amem console Recap panel and screened tool-input capture. The hub continues to use the library API and does not call the hook spool.
10
+ - Pin `@staix/agent-mem` 0.17.0 (was 0.15.0). agent-mem 0.16.0 requires every amem writer on one memory home, hub pins included, to run 0.16.0 or later: a 0.15.0 drain deletes hook-spool records that 0.16.0 staged without reading them. Upgrade the hub together with a global `amem` of 0.16.0 or later. 0.17.0 adds the acon console plugin (`@staix/agent-mem/ui-plugin`) and `CandidateStore.reopen`. The hub does not pass `forgetNearby`, so the changed `amem forget --json` output does not reach it. Recall, capture, briefs and memory homes are otherwise unchanged.
11
+ - acon console plugin (#935, D1 and D2): `@staix/agent-hub/ui-plugin` is an agent-console api 1 plugin. Its panels are the dashboard's views (`DASHBOARD_VIEWS`). The provider uses the dashboard manager's own project listing and forwarding (`projectConsole`, lifted out of `startManager` unchanged), so every action passes the manager's whitelist and every view passes `dashboardView`; there is no protocol change. Its inbox lists pending approvals (Allow once / Deny, answered with the hub's own `permit`) and in-review tasks whose reviewer is you. Outward or destructive actions (permit, approve, deny, send, stop, setting changes, config apply, peer start, split) answer `needsConfirm` first. The panel is the `ahub ui` page itself: `renderDashboard()` returns the injected page, `ahub ui` still serves it inlined and hashed (byte-identical, same CSP), and the plugin builds its panel module from the same blocks with `post()` routed to the console host. `ahub setup` writes `~/.agenthub/console/plugins/ahub.json` with the module's sha256.
12
+ - acon Space (#1012 Part 2): the plugin declares `capabilities.space` and answers `snapshot({ panel: "space" })` with a SpaceScene built from the dashboard's office model (`src/ui/office-space.ts`). Peers are actors (state from condition, approvals and review, with marks), the office rooms are queue, work, review and done zones, tasks are cards, overlap/conflict links and message arcs are links. Ids are project-prefixed, scenes from all running projects are merged within acon's caps (32 actors, 200 cards, 64 links), and only labels reach the scene: no bodies, details, approval titles or reasons.
13
+ - Research campaigns (#930, #932; #1024, #1027, #1028, #1029, #1031, #1041): `ahub study draft|revise|approve|show|close` and the `hub_study_draft`, `hub_study_revise`, `hub_study_show` and `hub_study_handoff` tools keep a bounded multi-unit study contract on an approved parent task. Planners store drafts only; only the person's `ahub study approve` creates the unit tasks, as Backlog cards. Before a unit task is offered, assigned, pulled or handed over, the hub rechecks the latest approved revision, the deadline, the stop policy and an optional execution budget `hardCap`, and holds it otherwise (held owner offers survive a restart). Unit owners record evidence with `hub_study_handoff`, and `ahub study show` reports coverage per unit and source. Experiment protocols, comparisons and attempts are stored with explicit provenance, and `ahub study approve-protocol <hash>` approves one exact protocol and evaluator; no tool records experiments yet. Private campaigns show ids, counts and states only, study tool text stays out of memory capture, and a restart, upgrade or recovery onto a release without the `research_*` features is refused while their rows exist. See [operations](docs/operations.md#research-campaigns-ahub-study-930).
14
+ - Calendar schedules (#1017, #1022): a schedule in `config.local.json` takes a five-field `cron` expression with an optional IANA `tz` instead of `every`. While one does, a restart, upgrade or recovery onto a release without `calendar_schedules` is refused.
15
+ - An owner may call `hub_task_done` again during review with a different full commit SHA (#1000, #1021): the reviewer gets a fresh request for the new head, the CI watch and stale merge evidence are replaced, and open findings and the rejection count stay. The same head, a malformed SHA or another owner refuses without effect.
16
+ - `ahub doctor` reads its `memory capture: codex|kimi` rows from `amem setup <tool> --json` (a preview that writes nothing) instead of `dot ai memory status`; a missing or refusing `amem` reads as unknown, never ready (#1008, #1025).
17
+ - Cross-project task handoff (#1030, #1043): `ahub task handoff --project <root|id> [<class>] <title...>` (the person) and `hub_task_handoff` (a configured planner or the conductor) put work onto another running agent-hub project's board on this machine as an ownerless proposal, where that project's own roles plan, implement and review it. Accepted, in review, approved, declined and cancelled come back to the requesting peer as one task message with an event name, a PR number and a SHA, never the target's text, and a restarted origin asks running targets for its open handoffs. `hub_task_handoffs` and `ahub board --handoffs` list them. Unregistered, same-project, stopped or older targets, PII turns and tasks, oversized or malformed requests and callers who are not the person, a planner or the conductor are refused by name, with no row on either board.
18
+ - Distinguishable sessions and sender provenance, not authentication (#1036, #1037): a connection that names a peer but is not that peer's current session is bound to a daemon-minted id such as `claude~a1b2c3`. It may `send`, read `status` and read tasks, holds none of the peer's roles, receives no deliveries and replaces nobody; everything else is refused naming the session. Message headers name the sender as it is. A channel plugin uses an inherited hub only when its recorded root matches the working directory's. No connection that works today is refused.
19
+
7
20
  ## 0.24.0
8
21
 
9
22
  - Autonomy preset (#999 T1 and T2, tasks 581 and 583, #1009, #1010): `ahub autonomy on [--all-projects] [--yes] | off [--all-projects] | status [--json]`, person only. Agent shells, delegated live tests, hub tools and the conductor are refused. `on` previews the settings and their apply timing, then takes one confirmation. It applies `templates/presets/autonomous.json` to this project's untracked `config.local.json`: task flow, supported permission and start defaults, and `action_policy.floor`. It also writes the machine opt-in `~/.agenthub/defaults.json` (owner-only 0600; an unsafe file is ignored with a notice). `--all-projects` applies to every registered project after one combined preview: live hubs through their console, stopped ones through the same validated writer. `off` removes the fixed preset keys, including hand-set values of those keys, and the retained consent; unrelated keys stay. Nothing changes until the person opts in, and a machine without the file keeps the shipped ask defaults.
package/README.md CHANGED
@@ -5,7 +5,7 @@ Code, Codex, Kimi Code, Pi and a hub-owned local worker (plus Qwen Code, Jcode a
5
5
  Antigravity if you want them) share a message bus, a task board and a memory, each
6
6
  connected through its own native interface.
7
7
 
8
- Status: 0.24.0, control protocol 16. Bun + TypeScript, one loopback daemon per
8
+ Status: 0.25.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
11
  ## Quick start
@@ -26,6 +26,15 @@ ahub say "Each of you: reply with your name."
26
26
  ahub ui # the web dashboard
27
27
  ```
28
28
 
29
+ `ahub setup` also registers agent-hub as a plugin of the family console
30
+ ([agent-console](https://github.com/STAIxBWLB/agent-console), `acon ui`): the same dashboard
31
+ appears as a panel for every registered project, and pending approvals and reviews assigned
32
+ to you reach the console inbox. The plugin is `@staix/agent-hub/ui-plugin`; it uses the
33
+ dashboard manager's own listing and forwarding, so it adds no protocol and no action.
34
+ The plugin also feeds the console's Space view (Flat, Room and 3D): agents become actors
35
+ at their desks, the task board's columns become zones, and task links and recent messages
36
+ become links. The scene carries labels and states only, never message bodies or details.
37
+
29
38
  `ahub init --dry-run --json` shows what init would write. The
30
39
  [Quickstart](docs/quickstart.md) walks through a first session and a working day.
31
40
 
@@ -45,7 +54,15 @@ ahub ui # the web dashboard
45
54
  Claude Code instances can implement in their own worktree, in a terminal or
46
55
  headless with one session per task. `ahub sdlc metrics` reads lead times,
47
56
  rework and stalls from board history, and
48
- optional control bands turn a breach into a backlog card.
57
+ optional control bands turn a breach into a backlog card. Work that belongs to
58
+ another project running agent-hub on this machine goes onto that project's board
59
+ with `ahub task handoff` (agents: `hub_task_handoff`), and its state changes come
60
+ back as task messages.
61
+ - **Research campaigns.** `ahub study` keeps a multi-unit study contract as a draft
62
+ (a planner drafts it with `hub_study_draft`); only the person approves it, which
63
+ creates one task per unit, and `ahub study show` reads coverage per unit and
64
+ source. A unit is covered by its owner's `hub_study_handoff` with evidence, not by
65
+ a done task.
49
66
  - **Shared memory.** Decisions, contracts, fails and findings are kept through the
50
67
  first-party [`@staix/agent-mem`](https://github.com/STAIxBWLB/agent-mem) library,
51
68
  in process with no claude-mem worker (short- and long-term tiers, a typed graph,
@@ -62,11 +79,16 @@ ahub ui # the web dashboard
62
79
  (`action_policy.floor`, which `ahub autonomy on` sets) lets a push of a non-default
63
80
  branch to `origin` and `gh pr create|comment|review|merge` in the project's own
64
81
  repository run without asking; force, default-branch and tag pushes and releases
65
- still ask. Budgets pause an
82
+ still ask. Every message records its sender's origin, and a session that is not
83
+ the project's own peer appears as `<peer>~<origin>` with none of that peer's
84
+ roles: sessions are distinguishable, which is provenance, not authentication.
85
+ Budgets pause an
66
86
  agent before it runs out of quota, and recovery never replays an
67
87
  effect whose outcome is unknown.
68
88
  - **Operations.** A terminal console and a local dashboard, one hub per repository or
69
- worktree with a machine-wide view, live settings reload, a one-confirmation autonomy
89
+ worktree with a machine-wide view, live settings reload, machine-local schedules
90
+ that propose tasks by interval or by a `cron` expression in a time zone
91
+ (`ahub schedule list`), a one-confirmation autonomy
70
92
  preset (`ahub autonomy on|off|status`), structural config
71
93
  (instances, review, lifecycle) edited with a validated diff and a backup from the
72
94
  console or a dashboard settings session, token usage and quota
@@ -99,7 +121,7 @@ uses the home must run a version that understands the new layout.
99
121
  [#258](https://github.com/STAIxBWLB/agent-hub/issues/258)).
100
122
 
101
123
  Live checks that still need a real account or a person are listed in the
102
- [release verification report](docs/verification/2026-10-31-0.24.0.md) and the
124
+ [release verification report](docs/verification/2026-11-01-0.25.0.md) and the
103
125
  [smoke checklist](docs/smoke.md).
104
126
 
105
127
  ## Documentation
@@ -65,6 +65,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
65
65
  - `stop(reason?)` (`src/hub/peers.ts`): a reason marks a person's requested stop (`peer_stop`, rule: `daemon.md`). Fence before the first await (the adapter's stopping flag, or `offline`) so `deliver()` and buffered native callbacks do nothing more, hold the in-flight delivery as `needs_review` with that reason, and end `offline`. For ACP without a journal, return ordinary in-flight envelopes once through `onFailed` before a requested stop invalidates the turn; never return the watchdog-cancelled original prompt. A requested stop never completes, escalates or reports the turn it cut (`AcpPeer` bumps `turn`; `PiPeer.fail` skips the failure message and `onTurnFailure` while `requestedStopReason` is set). `PiPeer.stop()` waits for aborted tools only up to `stopGraceMs`: a tool past it never blocks the teardown, and the stop throws `PiToolSettlementFailure` once the owner is down.
66
66
  - A terminal the hub opens for a person (`src/hub/terminal-open.ts`, #269) gets `personEnv`, which strips every agent marker; never `peerChildEnv`, which sets one.
67
67
  - Spawn a native peer with `peerChildEnv(peer, source)` so its own `AGENTHUB_PEER_ID` replaces inherited agent markers. Sandbox tool environments carry their executing peer too. Preserve the native shell environment policy; Codex's `CODEX_THREAD_ID` supplies the fallback after filtering. Before changing marker detection or launcher environments, read `docs/verification/2026-10-09-agent-shell-t0.md` for pinned source and live-probe limits.
68
+ - Launch ids and peer ids (agent-hub#1036): `peerChildEnv` gives a spawned peer `AGENTHUB_PEER_ID`; a Claude session's launcher also puts `AGENTHUB_LAUNCH_ID` into the environment every shell and MCP helper it spawns inherits, and a tools hello that names a Claude channel peer is a distinct session `<peer>~<origin>` only when it PRESENTS a launch id that is not the current launch record's (`sessionHello`). A hello with NO id stays the peer: the native facts hook, a helper and a person's shell connect that way and must keep working (#636, #525-5), so `ControlClient` never injects an inherited id. Nothing is refused: a new adapter that gives its tools socket a peer id keeps its marker admission, and adds its own launch id and `sessionHello` branch only when it wants the same distinction. The daemon labels an envelope an adapter publishes itself with the launch read by shape from `managedLaunchId` (channel) or `ownedLaunchId` (Jcode) (rule: `daemon.md` "Sender provenance"): keep those names, and never admit or authorize anything by them. This makes sessions distinguishable, it does not authenticate them: a same-user process can always act as the user.
68
69
  - Spawn with `peerChildEnv(<instance id>)`, not the kind name. An ACP `kind` comes only from the host declaration, never from the id suffix or agent text.
69
70
 
70
71
  ## Pi
@@ -14,6 +14,7 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
14
14
  - Invocations (#445 C2, #986): an `invocation` or `invocation_result` (`isInvocationKind`) goes alone in `take` and `takeBudget`, is never condensed, and is never steered into: both steer sites and `steerQueued` ask `invocationTurnOpen` first. In `onMessage`, a peer's answer goes to `onAnswer` before `parseMarker` and before any publish, when it names the request (`inReplyTo` in the handed delivery) or names nothing while the invocation turn is open. A taken answer, or a throwing hook, publishes, stores and emits nothing.
15
15
  - An invocation is never replayed through ordinary retry (#991). `retryDelivery` refuses a row that holds one. An uncertain or thrown delivery holds the peer (`recoveryHeldPeers`) and calls `onInvocationUncertain` instead of `failed()`. `failed()` allows at most one automatic retry, and only when `canRetryInvocation` returns true; unknown or a throw refuses.
16
16
  - Add an envelope kind only to `KINDS` (`envelope.ts`); the journal, restart reader, deferred-hub check and held hints read that one list. Test room provenance with `remoteEnvelope(env)`, never `from.includes("@")`: a relayed room invocation arrives `from: hub` with `refs.invocationRemote` (#992).
17
+ - `Envelope.session` (agent-hub#1036) marks a sender that is a distinct session, `from` being its minted `<peer>~<origin>` id (rules: `daemon.md`). It grants nothing; keep it on queued and journaled originals, because the `session_identity` recovery guard reads it from unsettled deliveries.
17
18
  - A condensed digest is sent by `digest`, not `hub`: `replyParent()` skips only the hub's `presence` item, and a reply to a digest must keep the highest hop of what it replaced. On a `failed_safe` delivery or a `retry` resolution the bus puts back the originals, never the digest.
18
19
  - 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.
19
20
  - 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` out or empty fans the message out to every peer. The control `send` takes a nonempty string `to` as that one peer and refuses any other non-array shape or bad item before publishing (#963); never turn a malformed `to` into a broadcast.
@@ -18,7 +18,7 @@
18
18
 
19
19
  - 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.
20
20
 
21
- 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/adapters/definitions/native-recovery*.ts`, `src/adapters/definitions/native-policy-aux.ts`, `src/adapters/definitions/native-config-*.ts`, `src/adapters/definitions/native-cli-*.ts`, `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/hub/config-reload.ts`, `src/hub/config-apply.ts`, `src/hub/config-tier.ts`, `src/hub/config-edit.ts`, `src/hub/config-edit-host.ts`, `src/hub/claude-launch-proof.ts`, `src/hub/recovery-continuity.ts`, `src/cli/recovery-presentation-options.ts`, `src/cli/setup.ts`, `src/cli/upgrade*.ts`, `src/cli/terminal-recovery.ts`, `src/cli/recovery-package.ts`, `src/cli/facts-hook.ts`, `src/hub/usage.ts`, `src/hub/memory-profile.ts`, the memory code modules (`src/hub/memory-symbols.ts`, `src/hub/memory-code-overlay.ts`, `src/hub/memory-code-head.ts`, `src/hub/memory-stale-anchors.ts`, `src/cli/memory.ts`), the task brief host (`src/memory/brief.ts`, `src/hub/memory-plan-guard.ts`), `src/hub/native-hook-launch.ts`, `src/cli/output.ts`, `src/ui/index.html`, `src/hub/task-code-group.ts` (dashboard Code group), the agent drift sweep (`src/hub/agent-drift.ts`, `src/cli/agents.ts`) or the instance modules (`src/hub/instances.ts`, `src/hub/instance-config.ts`, `src/hub/instance-execution.ts`), or before adding a native peer.
21
+ 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/adapters/definitions/native-recovery*.ts`, `src/adapters/definitions/native-policy-aux.ts`, `src/adapters/definitions/native-config-*.ts`, `src/adapters/definitions/native-cli-*.ts`, `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/hub/config-reload.ts`, `src/hub/config-apply.ts`, `src/hub/config-tier.ts`, `src/hub/config-edit.ts`, `src/hub/config-edit-host.ts`, `src/hub/claude-launch-proof.ts`, the session and provenance modules (`src/hub/session-identity.ts`, `src/hub/provenance.ts`, `src/hub/channel-project.ts`), `src/hub/recovery-continuity.ts`, `src/cli/recovery-presentation-options.ts`, `src/cli/setup.ts`, `src/cli/upgrade*.ts`, `src/cli/terminal-recovery.ts`, `src/cli/recovery-package.ts`, `src/cli/facts-hook.ts`, `src/hub/usage.ts`, `src/hub/memory-profile.ts`, the memory code modules (`src/hub/memory-symbols.ts`, `src/hub/memory-code-overlay.ts`, `src/hub/memory-code-head.ts`, `src/hub/memory-stale-anchors.ts`, `src/cli/memory.ts`), the task brief host (`src/memory/brief.ts`, `src/hub/memory-plan-guard.ts`), `src/hub/native-hook-launch.ts`, `src/cli/output.ts`, `src/ui/index.html`, the acon plugin (`src/ui/plugin.ts`, `src/ui/office-space.ts`, `src/cli/acon-register.ts`), `ahub study` (`src/cli/study.ts`, `src/cli/study-output.ts`), `src/hub/task-code-group.ts` (dashboard Code group), the agent drift sweep (`src/hub/agent-drift.ts`, `src/cli/agents.ts`) or the instance modules (`src/hub/instances.ts`, `src/hub/instance-config.ts`, `src/hub/instance-execution.ts`), or before adding a native peer.
22
22
 
23
23
  - 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).
24
24
  - `hub_invoke` over the control WS (#445, #991): the daemon binds each admission to its socket and `rid` with an `AbortController` (`invocationRequests`, at most 64 per connection; a duplicate `rid` refuses). `request_cancel` and socket close abort it, and every check after an await confirms the socket is open and still maps that `rid` to the same controller before any effect. Clients go through `ControlClient.request`, whose `controlRequestTimeout` raises a `hub_invoke` wait to `INVOCATION_CONTROL_TIMEOUT_MS`; never call it with a shorter bound.
@@ -28,6 +28,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
28
28
  - 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`. Beyond `list`, the `queue` op is the console's or an admitted conductor's (`admitOperational`, `show`/`resolve`/`discard` 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`).
29
29
  - 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.
30
30
  - Dashboard snapshot `view` (#683 U4): the daemon's `ui_snapshot` and the manager's forward pass a request's `view` only through `dashboardView` (`DASHBOARD_VIEWS` in `src/hub/ui.ts`); keep that list equal to the page's `DASHBOARD_VIEWS` in `src/ui/index.html`. Any other value is dropped, never forwarded.
31
+ - acon plugin (#935): `src/ui/plugin.ts` never bypasses `uiAction`'s whitelist or `DASHBOARD_VIEWS`. It reaches project hubs only through `projectConsole` (`src/hub/manager.ts`), the same listing and forwarding `ahub ui --all` uses; widen neither for the plugin. Its inbox verbs map to the hub's own `permit` with an option the request offers. Its panel is built from `renderDashboard()` by exact rewrites that throw when a target line moves: when you edit a rewritten line in `src/ui/index.html` (post, the cookie helper, `$`, the theme `apply`, the view key listener, the title update or the landmark tags), update `panelAssets()` in the same PR. `lifecycle.ts` is loaded lazily there, because it reaches the daemon and CLI files acon's import scan reads. `ahub setup` registers it only through `registerAconPlugin` (`src/cli/acon-register.ts`): `console/plugins/ahub.json` under the hub home names the module and its sha256 (directories 0700, written to a temp file and renamed); the package export `./ui-plugin` is pinned by `scripts/check-package.mjs`. The Space panel (#1012, `src/ui/office-space.ts`) maps only the #264-allowlisted `officeModel` scene, pinned by the #264 marker fixture in `test/acon-plugin.test.ts`; never feed it an envelope body, task detail, path or symbol. A Space snapshot answers within acon's 5 s: the listing gets half of `SPACE_BUDGET_MS` and falls back to the last listing (a late one still replaces it), projects are read concurrently and a late one is left out of that poll, the cached office model is keyed by the daemon's `instanceId` (a restarted daemon restarts its ui sequence at zero), and `pollMs` stays under acon's 3000 ms link gap.
31
32
  - Dashboard Usage view (#683 U4): `uiSnapshot` builds `usage` only when the request's view is `agents`, never on a hidden view's poll. It folds with `catchUp(100)` and sets `behind: true` when that is not done, so a partial figure is never shown as the whole. A failed read answers `{ unavailable: true }`, never `undefined`, which the page reads as a hidden view and keeps the old figures. The snapshot carries task ids, a `pii` flag and numbers only, never a title; the page shows a private task as `#id [pii]`. Daily figures come only from `UsageRollup.daily` (rule: `budget.md`).
32
33
  - Dashboard Code group (#684 2c): `codeGroup` (`src/hub/task-code-group.ts`) is the only dashboard classifier of changed symbols against `plan.symbols`, called in `uiSnapshot` with the public `codeSymbols`, `tasks.publicMetadataText` and `tasks.isPii`; the page (`codeGroupSection`) draws it and never matches names. A private task gets no `codeGroup`. A stored receipt (one with `repositoryKey`), a missing or an unknown one is `state: "unknown"`. A malformed or screened-out plan name is counted in `planWithheld` and marks the group incomplete, never dropped. Lists stop at `CODE_GROUP_MAX` (40) with the rest in `more`, and `counts` are totals. The memory view reads a `node` of up to 512 characters only when it starts with `symbol:` (200 otherwise).
33
34
  - Agent CLI drift (#843 AC3): the daemon's `sweepAgentDrift` (once after start, then `agentDriftEveryMs`, default one day, on its own timer apart from the task sweep) is the only caller of `noticeDrift` (`src/hub/agent-drift.ts`). It reads versions only through `agentVersionsAsync` (`src/cli/agents.ts`), which shares `probeTarget` and `probeRow` with the `ahub agents` probe: never add a second probe path, a network read or an install. The first probe is owed until one has run (`agentDriftOwed`): a daemon that starts inside a controlled recovery skips it and the 1 s tick pays it once `recoveryActive()` ends, so a restart or upgrade never waits a day for the first notice. `startDaemon` probes only when `agentDrift` is set (`lifecycle.ts` sets `true`; tests inject a function), so a test daemon never spawns a real CLI. Drift is an installed executable with a readable version whose state is not `verified`; a missing or unreadable one is silent. The notice carries id, version, state and the receipt path only, is marked per (id, version, planner) in `agent_drift_notices` before it is published to that planner alone (and forgotten if it cannot be queued, so only that planner is retried), and goes to `plannerPeers()` that are rostered; with none, nothing is recorded. `agentVersionsAsync` reads stdout as a stream with the 64 KiB cap enforced as it arrives (kill and cancel on overflow) and cancels the reader at its deadline and a short grace after the probe exits, so a descendant holding the pipe cannot hang it. Never put CLI output in it.
@@ -55,7 +56,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
55
56
  - 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.
56
57
  - Cross-version recovery must use the authenticated source protocol for prepare/commit/abort and the target protocol after startup. Prove the transition against a real prior release before claiming compatibility.
57
58
  - Preserve `task_cancel` target-feature preflight before source holds, capture or commit. Reuse stored target features only after verifying the same retained root and digest; legacy receipts still capable of commit must obtain feature proof. Do not apply pre-commit compatibility checks to sources already past commit. Every prepare, including the lease renewal before a terminal close and after a resume, sends the operation's stored `targetFeatures` (#478).
58
- - A recovery target-feature floor (`task_cancel`, `invocation` while the invocation ledger has an open (`queued` or `delivered`) row, `peer_instances`, `role_stages` while a declared Claude-kind instance holds `implementer`, `task_planners` while a list/instance planner is configured or `Board.hasPlannerState()` finds a task/release binding or plan-review authorship marker, `release_trains` while a configured train is active or frozen, `reviewer_pool` while a fresh reviewer or `review.pool` is configured, `configured_acp` while the project declares `adapters`, `external_adapter_modules` while external module definitions are loaded, `external_adapter_instances` with `external_adapter_modules` and `peer_instances` while an external module-kind instance is declared, `conductor_recovery` while `Board.hasConductorManualState()` finds open conductor manual-assignment intent, `project_autonomy` while the daemon holds retained autonomy consent or a raw `config.json` or `config.local.json` has a top-level `_autonomy` key (an unreadable file refuses as unknown), `action_policy_floor` while the raw `config.json` or `config.local.json` names `action_policy.floor`, true or false, even where the key is ignored or normalized away: an older target parses the raw key and rejects it before any trust rule, so check raw file presence, never the effective config (#582, seen twice)) lives in `incompatibleRecoveryTarget`, which every existing check site calls, and its name in `DAEMON_FEATURES`; never check a floor at one site only.
59
+ - A recovery target-feature floor (`task_cancel`, `invocation` while the invocation ledger has an open (`queued` or `delivered`) row, `peer_instances`, `role_stages` while a declared Claude-kind instance holds `implementer`, `task_planners` while a list/instance planner is configured or `Board.hasPlannerState()` finds a task/release binding or plan-review authorship marker, `release_trains` while a configured train is active or frozen, `reviewer_pool` while a fresh reviewer or `review.pool` is configured, `configured_acp` while the project declares `adapters`, `external_adapter_modules` while external module definitions are loaded, `external_adapter_instances` with `external_adapter_modules` and `peer_instances` while an external module-kind instance is declared, `conductor_recovery` while `Board.hasConductorManualState()` finds open conductor manual-assignment intent, `project_autonomy` while the daemon holds retained autonomy consent or a raw `config.json` or `config.local.json` has a top-level `_autonomy` key (an unreadable file refuses as unknown), `calendar_schedules` while a raw `schedules` entry has a `cron` or `tz` key (#1017: an older target refuses the key at load), `research_records` while `Board.hasResearchState()` or a raw `config.json` or `config.local.json` `research` object names `runners` (unreadable refuses as unknown), `research_dispatch`, `research_dropped_offers`, `research_experiments` and `research_comparisons` while `Board.hasResearchDispatchState()`, `hasResearchDroppedOffers()`, `hasResearchExperimentState()` and `hasResearchComparisonState()` find rows (each research digest joins `integrity()` only when its rows exist), `action_policy_floor` while the raw `config.json` or `config.local.json` names `action_policy.floor`, true or false, even where the key is ignored or normalized away: an older target parses the raw key and rejects it before any trust rule, so check raw file presence, never the effective config (#582, seen twice)) lives in `incompatibleRecoveryTarget`, which every existing check site calls, and its name in `DAEMON_FEATURES`; never check a floor at one site only.
59
60
  - Project autonomy (`autonomyCommand` in `src/hub/daemon.ts`, `writeAutonomyPreset` in `src/hub/settings.ts`): the `autonomy` control message is console-only; its operations come only from `planAutonomy` and `autonomyRemovalKeys`, never from the request; `off` removes the fixed key list and clears consent whatever `_autonomy` says. `_autonomy` is inert metadata, never approval provenance. Retained consent is created only by `createAutonomyConsent(...).confirm` inside the daemon (console `on`, or startup under a trusted `readAutonomyDefaults` opt-in with an exact plan match), never by a CLI or offline writer; a config `never-ask` takes effect only for a peer in `autonomyConfirmed`.
60
61
  - Defer hub notices only for an authenticated recovery `prepare` or `commit` whose target features include `recovery_deferred_hub`, or for a restored daemon. Otherwise `setRecoveryHold(true)` moves the deferred notices into queues before commit records expected integrity, and a bare `fenceRecovery` defers nothing. Abort and lease expiry release the bus hold before clearing the operation; if that release fails, keep the operation and let expiry retry on its 1 s timer (#478).
61
62
  - Resume a prepared `commitSent` source that is unavailable or database-busy inside the existing bounded shutdown poll; never re-send commit, re-prepare or close terminals again on that path. Require confirmed `stopped` before target startup, refuse a replacement instance, and retain the prepared receipt on timeout.
@@ -79,6 +80,9 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
79
80
  - `ahub claude` (`src/cli/launch.ts`, #270) publishes `claude-launch.json` (`claude-launch.<id>.json` for an `--as` instance) for every launch against a running hub, atomically and without a lock: hooks, permission status, session observation and recovery read its `launchId`, so never make it wait for native-proof state. The native child's pid and signature go only into the private `claude-native-<binding hash>.json`, keyed by the exact launch record, written once under `claude-launch.lock` and removed at that child's own exit; never put them in `claude-launch.json`, where a late callback of an earlier launch could replace a newer record. Crash cleanup of a previous launch's settings file (`cleanupStaleClaudeSettings`) needs the launcher gone by `processLiveness` and that native proof, and for a `direct-child` proof the native pid absent from an available `processTable`: a direct child may exec in place, so its signature certifies nothing. A script or interpreter gets no proof and keeps its settings file.
80
81
  - Read a `claude-launch*.json` record in the daemon only through `readClaudeLaunchProof` (`src/hub/claude-launch-proof.ts`: no-follow, 16 KiB cap, this instance only), never by parsing the file. An `--as` launch records the `executionCwd` its entry got from `resolveExecutionCwd`; the entry never chooses or creates a worktree.
81
82
  - Terminal Claude implementer roots (#525-5): bind `executionRoots`/`executionOwners` only at a hello where the resolved worktree is not the project root, the private launch record names this hello's `launchId` and that root, and the channel's reported `cwd` resolves to it; bind it to that socket and session generation. `currentExecutionOwner` rechecks the record and the worktree in each event-loop turn (reuse only within synchronous code), and `revokeTerminalExecution` drops the proof on a mismatch, detach or new claim. Status (`ownerIsolation`, `ownerIsolationNotes`, `tasks.stalled({ ownerIsolation })`) reads only `ownerIsolationProjection`, the retained binding plus owner, generation and socket checks: never add a terminal-root Git or filesystem validation to the status path, and never authorize admission, facts grants or recovery from that projection. Missing or unknown evidence keeps the channel attached but never grants isolation; the channel omits `cwd` when it cannot resolve its own.
83
+ - Distinguishable sessions (agent-hub#1036, `src/hub/session-identity.ts`): a tools hello that asks to be a session (`session`, sent by a channel plugin that has no project of its own and inherited another project's state dir) or that names a Claude channel peer while PRESENTING a launch id that is not that peer's current one (`sessionHello`; an ABSENT id stays the peer, so the native facts hook and a person's shell keep working; a missing launch record, another daemon's record, codex, kimi, qwen, owned ACP peers and the implementer child are untouched) is bound to an id the daemon mints, `<peer>~<origin>`. The id has no `@`, so it never reaches the remote-room paths (`isRemotePeerId`, `membershipAllowed`, `remoteEnvelope`, `Bus.emit`'s body withholding); `PEER_ID` is not widened, and the ids live in the daemon-owned `sessionIds` set. A session is never a bus peer: no delivery, no push, no roster. `sessionGate` runs before the dispatch switch and allows only `send`, `status` and the read task ops in `SESSION_READ_OPS`; every other message is refused with `this session is <id>, not this project's <peer>`, so a new control message is refused to a session until it is listed. Its envelopes carry `session: true`, the header shows the minted id as is, and its `attach` and envelope events carry `session: true` and the id, never a path or text. A label that is not already a plain lowercase token is dropped (never cleaned up into one) and the hub mints a short id. A plugin never asks for a session from a hub whose `status.json` lacks `session_identity`: that hub would bind the real peer id. `session_identity` is its own recovery label: a target without it is refused while an unsettled delivery holds an envelope with `session: true`. Nothing is refused to a connection that worked before. Limit: this makes sessions distinguishable and traceable, it does not authenticate them.
84
+ - Channel plugin project (agent-hub#1036, `src/hub/channel-project.ts`): the plugin picks its hub with `channelProject`, which shares `inheritedStateDir` (`src/hub/project.ts`) with the CLI's `projectContext`; keep that ownership rule in the one function. An inherited `AGENTHUB_STATE_DIR` is used as the peer only when its recorded root is this cwd's project root (or, unrecorded, `AGENTHUB_PROJECT_DIR` names it), or for a declared extra instance whose cwd is its managed worktree. A process with a project of its own (a linked worktree with its config) keeps that project's hub; one with no project of its own joins the inherited hub as a session with an empty label, so no directory name reaches a header; with nothing to join the plugin connects nowhere and says so. A tools-mode helper (`AGENTHUB_MODE=tools` with both variables set: the implementer child, an external-module instance) trusts the pair, because the daemon checks its launch id at the hello.
85
+ - Sender provenance (`src/hub/provenance.ts`): `originOf` is pure and keeps only role, a peer-shaped id, and a launch or instance id that matches its id pattern; never add a path, cwd, root, body or token, and drop a malformed value rather than copy it. Each admitted peer or tools hello writes one `attach` event after admission (a refused hello writes none). An envelope event's `origin` is the sending connection's, recorded at `send` in the bounded `envelopeOrigins` map and taken once when the event is logged, else `adapterOrigin` for an envelope an adapter published itself; the hub's own writers (`hub`, `user`, `digest`) and room ids get none. `adapterOrigin` reads `managedLaunchId`/`ownedLaunchId` by shape, never through `isWsPeer`: it only labels the log, and an `isWsPeer` call there counts as a native branch in `test/daemon-native-branches.json`.
82
86
  - A headless Claude implementer's tools socket (#525-4) is admitted only with the launch id of that peer's current child (`ownedTools`) and a verified worktree root, and bound as `toolsOwner`: every request and every reply rechecks `currentImplementerTools` and fails closed. Its `task` ops are limited to `IMPLEMENTER_HUB_TOOL_NAMES` and run inside `childToolAuthority`, so `Tasks.requestCheck` refuses before an effect once the child changed. Its `inbox` refuses, before `bus.pull`, a batch holding a private item or one attributed to another task. Its person `permission_request` path is `classed: true`, offers allow-once and deny only, and denies on expiry, abort or a changed child. Automatic Bash permission uses only the fixed host-wrapper contract in `adapters.md`, including the explicit client protocol capability; a raw native allow is never an automatic grant. Its facts get `permission` only for the current child's launch id, session id and this instance.
83
87
 
84
88
  - Keep SQLite recovery contention in the existing blocked receipt as retryable `database_busy`, retaining bounded throw-site frames and the ordinary `ok: false` rid reply. Use a neutral control error; the receipt's next actions name resume or the partway disposition. Clear a superseded failure on abort, disposal, fresh-session choice or a refused resume of a disposition. After a commit request, treat busy inspection as unconfirmed and keep polling only inside the existing shutdown bound.
@@ -130,3 +134,12 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
130
134
  - Anything the daemon starts on a timer and registers only with `startupCleanup` is not stopped by a normal `stop()`: `startupCleanup` runs on a failed startup only. Clear the timer and close the store in `stopOnce`, and wait for a pass in flight before the board closes (`schedulesPass`, #512a). A console op that runs the same writer on demand (`schedule_run`, #335) joins that wait through `manualSchedulePasses`; add any new on-demand entry to it, or `stopOnce` closes the board and the store under it.
131
135
  - Corpus (#510a, `src/hub/corpus.ts`, `corpus-config.ts`): `corpus.paths` is machine-local and `corpus` is never editable from settings. Keep `corpus-config.ts` free of file-system imports, because the config schema (`config-apply.ts`) reaches the Claude channel bundle. A refusal of a root is a reason string, never a thrown error and never a partial read: evidence the check cannot read refuses, and only a completed read certifies a folder. The vault check is `.obsidian` at the real root, in every ancestor and anywhere below (any entry of that name), always on the `realPath`. A folder the pii, deny or local.deny rules protect is never listed, so it cannot be shown vault-free: the root that holds it is refused, never certified with that subtree skipped (a protected file is only skipped).
132
136
  - Domain packs (#513a, `src/hub/packs.ts`): `ahub init --profile` resolves a shipped profile name first, then a pack through `loadPacks` (shipped: `templates/packs/` listed in `src/hub/shipped-packs.ts`; a project pack in `.agenthub/roles/<name>.pack.json` replaces a shipped one of the same name); keep `parsePack` closed (exact fields, bounded, plain ASCII, ids through `criterionId` from `src/hub/workflow.ts`) and never give a pack a field that carries a command, a path or an authority. The preview names a pack by ids only, never rubric or check text, and the apply goes through `packOverrides`, which `resolveProfile` validates. Reads use `lstat` and `O_NOFOLLOW` and refuse a hardlinked file, as the role catalog does. A pack whose roles are not in `loadRoleCatalog` is refused whole.
137
+
138
+ - Campaign dispatch (#597): keep `research_dispatch` additive to `research_records`; any stored campaign requires it, because old campaign stop policies also need current dispatch enforcement. Preserve the conditional dispatch metadata/handoff integrity digest and every other recovery feature guard. A study-dropped owner offer (#603) is a `research_dropped_offers` row written through `Board.recordResearchDroppedOffer` from `Tasks.relevant`, never an in-memory map: a restart between the drop and the approval must still re-offer. It joins `researchDispatchIntegrity` only when rows exist (so a project without one keeps its digest), requires a `research_units` row for its task and a peer-shaped owner, and `studyReoffer` clears it only after the offer is published or the record is stale.
139
+
140
+ - Experiment storage (#598): preserve `research_experiments` as an additive conditional recovery floor, the experiment-table integrity digest, and all previous research/calendar/autonomy guards. Read check evidence from this project's host reader only; no record input creates a measured field.
141
+ - `ahub study` (#601, `src/cli/study.ts`, `src/cli/study-output.ts`): `study.ts` is pure argv parsing (ids `rc-` and twelve hex digits, revisions as positive integers, `--reason` takes the rest of the words) and runs before any connection; `main.ts` reads the contract file against `invokedFrom` with a 64 KiB bound and sends it to the hub, which validates it. `show` is `allowed` in `classifyPeerCommand` through `peerReadForms`; draft, revise, approve, approve-protocol and close are `console`, and the audit label names at most the subcommand (approve-protocol is labelled `study`; a campaign id, a protocol hash and a contract path are the person's text). The default output is for a person and `--json` prints the hub's text byte for byte (`test/output-cli.test.ts`); a new `study` form needs its `--json` branch and a renderer, never a `TEXT_DEBT` entry.
142
+
143
+ - Experiment comparisons (#599): preserve the separate conditional `research_comparisons` floor and integrity for plans, exact protocol/evaluator approvals and causal protocol-context rows. Never backfill legacy order from timestamps or carry approval to a changed hash. Keep the approval op under the console-only `study_` dispatch guard.
144
+ - Doctor memory capture (#1008, #1025): the `memory capture: <tool>` rows come only from `amem setup <tool> --json`, a preview that writes nothing (`readMemorySetup` in `src/cli/main.ts`: no shell, 10 s, 256 KiB), judged by the pure `nativeMemoryCaptureRow` (`src/adapters/definitions/native-cli-doctor.ts`). A plan not in the documented shape, a non-zero exit, a timeout or unparsable output is unknown, never ready; never read dotfiles text or a client's config, and do not scrub the caller's identity before the call.
145
+ - Handoff transport (#604): `handoffOut` connects to the target as a `console` role through `ControlClient` (same token file and machine user as every control message; the hello checks the target's project root and instance), and the target answers every handoff message, an unknown one included. `handoffNotifyOrigin` hangs off `board.onChange` inside its own try, so a task move never waits for, or fails because of, another project's daemon. A target is judged by its own `status.json` features (`checkTarget`), never by a guess from its version. The inbound `task_handoff` is refused while recovery is active, counts in `taskOpsInFlight` so a recovery commit waits for it, and hands `handoffIn` a fence (recovery or `stopping`) that it asks again right before the board write, after the PII screen's await. Keep `task_handoff` in `DAEMON_FEATURES` and its recovery guard beside `research_experiments`; no `PROTOCOL` bump, because a new message type is answered as unknown by an older hub and an optional field is ignored.
@@ -1,6 +1,6 @@
1
1
  # Task board and hub tools
2
2
 
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/settled-roles.ts`, `src/hub/conductor.ts`, `src/hub/supervision.ts`, `src/hub/tool-refusal.ts`, `src/hub/hub-tools.ts`, `src/hub/review-brief.ts`, `src/hub/reviewer-pool.ts`, `src/hub/reviewer-pool-config.ts`, `src/hub/release-admission.ts`, `src/hub/main-checkout.ts`, `src/hub/checks.ts`, `src/hub/task-repository.ts`, `src/hub/review-findings.ts`, `src/hub/review-scope.ts`, `src/hub/plan-conformance.ts`, `src/hub/sdlc-metrics.ts`, `src/hub/sdlc-merge.ts`, `src/hub/sdlc-mirror.ts`, `src/hub/no-writes.ts`, `src/hub/workflow.ts`, `src/hub/deliverables.ts`, `src/hub/deliverable-files.ts` or `src/hub/ci-watch.ts`.
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/settled-roles.ts`, `src/hub/conductor.ts`, `src/hub/supervision.ts`, `src/hub/tool-refusal.ts`, `src/hub/hub-tools.ts`, `src/hub/review-brief.ts`, `src/hub/reviewer-pool.ts`, `src/hub/reviewer-pool-config.ts`, `src/hub/release-admission.ts`, `src/hub/main-checkout.ts`, `src/hub/checks.ts`, `src/hub/task-repository.ts`, `src/hub/review-findings.ts`, `src/hub/review-scope.ts`, `src/hub/plan-conformance.ts`, `src/hub/sdlc-metrics.ts`, `src/hub/sdlc-merge.ts`, `src/hub/sdlc-mirror.ts`, `src/hub/no-writes.ts`, `src/hub/workflow.ts`, `src/hub/deliverables.ts`, `src/hub/deliverable-files.ts`, `src/hub/research-campaign.ts`, `src/hub/research-experiments.ts`, `src/hub/ci-watch.ts` or the cross-project handoff (`src/hub/handoff.ts`, `src/cli/handoff-args.ts`, `src/cli/handoff-output.ts`).
4
4
 
5
5
  - Scope every task-place comparison through `TaskRepositories` (`src/hub/task-repository.ts`, #476): explicit `refs.repo`, else a common outside-absolute-ref repository or parent, else this project. Preserve relative display spelling within that key and gate overlap, contradiction, replay and held evidence by equal keys. Warm/cache repository identity at task mutation and startup; never repeat Git reads for unchanged overlap inputs. Set `refs.repo` for work in another repository; repo-only accept metadata never grants execution or file access.
6
6
  - 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.
@@ -66,6 +66,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
66
66
 
67
67
  - `Tasks.review` captures `reviewAuthority` once and rechecks it in `valid()` after every await, right before each write; a headless reviewer's verdict, and its `hub_task_show` (checked before and after the read), holds only while the same child runs for that task at its bound envelope head and review revision (`Tasks.reviewBindingCurrent`, host-only: `in_review`, same reviewer, non-PII, matching `reviewRequestRevision` and review head). Never cache the answer (#223, #533).
68
68
  - Review scope (#852, `src/hub/review-scope.ts`): a reviewer reads `full` (code, affected tests, CI) or `code` (code only). The default is data on the fixed reviewer variants (`AdapterDefinition.reviewer.scope`, `code` for the sandboxed codex and kimi variants, fixed-builtin-only like `instances.modelSelect`); a fresh-reviewer instance may override it with `review_scope` from `config.local.json` only (tier 3, never from a tracked file or the dashboard). `TasksDeps.reviewScope(peer)` is the one read. `sendReview` replaces the Hub-verified, check-result and no-check lines with `CODE_SCOPE_LINE` for a `code` reviewer, which requires every code defect as a structured finding with an id. Reserve findings for defects in the reviewed code, docs or tests; missing check/CI/test-run/PR-or-branch identity evidence goes in `unmet`, never `findings` (Amendment 2). The verdict rule is structural (amended AC3): a `code` reviewer's `changes_requested` is always recorded, never refused, and never classified by reading its unmet text; never add a text classifier (background: README). If its structured `findings` carry no item with disposition `open`, the history entry is `evidenceOnly: true`: it adds nothing to `rejections` (so it cannot escalate), records no failed outcome, contradicts no earlier approval, writes no recurring-finding candidate, is not a rework round in the SDLC metrics, still returns the task to its owner as usual, and sends the configured conductor one ids-only task notice (`evidenceOnlyNotice`; with no conductor, one console notice). A verdict with an open finding counts as today. A person's and a `full` reviewer's verdict are never evidence-only. A `code` reviewer's verdict history entry carries `reviewScope: "code"` and a `full` reviewer's carries nothing, so old rows are unchanged. Merge rule: a task approved by a `code` reviewer is merged by the conductor or the person only after exact-head CI is green and no review thread is unresolved, and the merge note names the CI run.
69
+ - Owner review re-file (#1000): admit a replacement `hub_task_done` while `in_review` only for a different full lowercase SHA through the existing owner, privacy, lifecycle and completion-check gates. Fence the original review revision through every await; same-head, active-check and superseded submissions refuse before effects. Clear stale artifact-head evidence without minting reviewer consent, retain current-head local evidence, open Important findings and rejection counts, and publish a fresh request. A configured check returns the task to `in_progress` until completion. Every verdict pins its initial review request revision across awaits, including off mode with no explicit head.
69
70
  - Keep structured review findings in verdict history, bound to the reviewed head. Validate bounded reports and person-only accepted dispositions before effects; recheck authority, privacy and head identity after awaits. A supplied `head` that differs from the task's resolved head (`findingHead`) is refused pre-effect, and the resolved head is part of the identity rechecked after the screen (#657). Key findings by `id` alone, never by pass (`findingsForHead`): a nit or open reclassification never discharges an Important, only an explicit disposition does. Omitted IDs never settle findings; carry open Important across heads until explicitly dispositioned, while nits stay head-local. Use the shared valid-head resolver for projections. Project text through publicView, and deduplicate recurring public pass/id pairs into backlog proposals through durable `Review finding key:` markers (a pair recurs only across separate work: tasks sharing a PR, a branch in one repository or a supersession count once, `workUnits`, #736; a task counts only where a review that is not withheld recorded the finding's id, whatever its pass, as open or accepted, never where it is only recorded fixed, #328) read only from hub-proposed `plan` cards (first history entry by `hub`), without editing instruction files.
70
71
  - Recurring unmet criteria (#514a, `src/hub/learning.ts`): grouping is pure (rows in, groups out); `Tasks.proposeCriterionCandidates` is the only caller and runs beside `proposeRecurringFindings`. It files a backlog card with ids and counts and the `profile_overrides` fragment as text, never applies it, and counts only rows that `candidateTextAllowed` passes (no PII). Its dedupe is the `Criterion candidate key:` line; keep it first in the detail. `ahub learn` (#514b) is the person's verdict on those cards: `parseCandidateCard` reads only that first line (and the hub-authored `plan` card, and the title that must match it), the task count and the line are recomputed from the board (`learnEvidence`, `overridesDiff`), and the card closes through `Tasks.cancel` only after `settingsSet` wrote the one managed key `LEARN_CHECKLIST` (never before, never by a direct board write); every `Tasks.learn*` method refuses any `by` but the person, and a new learn op never joins `CONDUCTOR_TASK_OPS`. Rewording the card's key line or title changes the parser in the same change.
71
72
  - Plan conformance (#481 S2, `src/hub/plan-conformance.ts`): compare only literal repository-relative `plan.paths` entries (file or directory prefix, no glob) with complete PR file metadata; `completePlanInventory` requires a full head, `pathsComplete` and `typesComplete` (false on any rename or copy). Root, absolute-unverified or unsafe entries, a missing plan, a truncated inventory and a failed read are `unknown`, never covered; never read patches or the filesystem. An untouched planned path is advice; a changed path outside the plan is a departure, and `planConformanceBlockers` blocks enforce approval on `unknown` and on an unaccepted departure. Plan `proof` and `risks` (32 strings of 512 characters) cover no file. `Board` records `planRevision` only while `trackPlanRevisions` (mode not off), separate from backlog `planningRevision`.
@@ -106,10 +107,16 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
106
107
  - Review request CI evidence (#824, `Tasks.hubVerifiedBlock`, `ReviewReadiness.checks`): `sendReview` renders one `Hub-verified at <head>` block from the `admission` it already carries and never makes a `gh` call of its own (the AC5 test pins the admission path to its three reads: a fourth call fails it). Its words come only from the hub's readiness read and recorded `check passed` events, never the builder's summary or a `hub_sdlc_record` claim; a PII task and a request with no admission get no block. A producer of `ReviewReadiness` (`readReviewReadiness`, `reviewReadinessFromEvidence`) must fill `checks` or the block names no checks. The checklist's `{check}` line drops "run the affected tests yourself" only for a ready admission with no `check passed` event; keep the #223 R2 withheld summary.
107
108
  - Verdict mirror (#786 T3, `src/hub/sdlc-mirror.ts`, `Tasks.mirrorVerdict`): one `COMMENT` review per (task, head, verdict), called after the approved and changes_requested writes in `Tasks.review`, never awaited and never throwing (a failure is a number-only notice). The only review write is `gh api repos/<repo>/pulls/<pr>/reviews -f event=COMMENT -f commit_id=<head> -f body=<text>`, pinned by `assertMirrorArgv`: never `APPROVE`, `REQUEST_CHANGES`, another endpoint or a method flag, and keep the `test/sdlc-mirror.test.ts` argv test. Idempotency lives on GitHub (list the PR's reviews for the headline at that head), not on the task, so it adds no board or receipt field and survives a restart; an unknown read posts nothing. The note goes out only when public (`!isPii`, not withheld, not a private turn, `publicMetadataText`); a PII task mirrors the headline alone. `mirrorVerdict` reads the PR head and state again AFTER the review listing and judges on that last read with nothing awaited before the post (a head checked before an await is not trusted after it; the same invariant as the guarded merge's dispatch): a new read in that module means a new re-read after it and a fake-gh test that moves GitHub during it. An approval's mirror settles before `autoMerge` starts (`autoMerge(task, after)`), so keep that order. It follows `auto_merge` through `mergeModeFor`.
108
109
  - Guarded merge (#786 T1, `src/hub/sdlc-merge.ts`, `Tasks.guardedMerge`): the hub's only merge method is `gh pr merge <pr> --repo <repo> --squash --match-head-commit <head>`, built by `mergeArgv` and checked by `assertMergeArgv` right before the call; never add `--admin`, `--auto` (until the repository allows it), another merge method or a ref update used to merge, and keep `test/sdlc-merge.test.ts` AC5 over the recorded argv. `decideMerge` names every failing gate and treats unknown evidence as a failure, and judges the non-head gates (checks including the `[repos]` table's and the eval gate's, review requests, threads, draft, change request) on the first read AND a last read taken after the queue (`evidenceGates`), because `--match-head-commit` pins only the head, and compares every board-side input again in one synchronous dispatch segment (`dispatchGates` on `MergeInput.fresh`, then the `run` call, with no await between them: state, repo/PR/reviewed head, privacy, dependency edges and states, open Important findings, release stage, merge mode, required checks, authority and the sweep, recovery and storage holds). The invariant: a gate checked before an await is never trusted after it, so a new input to the decision is a new field in `FreshBoard`, a new line in `dispatchGates` and a new entry in the `flips` table of `test/sdlc-merge.test.ts`; a new gate is a new refusal string plus a test line. The approval trigger (`autoMerge` after `releaseDependents` in `review()`) is fire-and-forget through `Promise.resolve().then(...).catch(...)`: it never awaits, never throws and never changes a verdict. Notices and results carry the task number and gates only (no `publicTitle`), and a PII task is refused before any read. Success is GitHub reporting `MERGED` on a readback, not the exit code. The control op (`sdlc` / `merge`) answers every request and admits the console user or the one configured conductor; post-run result recording and cleanup go through `Tasks`. After verified MERGED plus merge commit, the only permitted ref write is the same-repository/nondefault/unprotected PR head CAS deletion in #786 AC5; never delete a local worktree or branch.
109
- - Schedules (#512a, `src/hub/schedules.ts`, `Tasks.proposeFromSchedules`): the hub proposes a scheduled task only through `Tasks.propose` as `HUB`, never `Board.propose`, so the PII screen, class validation, triage and routing apply. "Its previous task is open" is read from the board (`openScheduled`: first history entry by the hub, detail starting `Schedule key: <id>`, state not closed), never from `schedule_runs`. A run restarts the period at `now`; a hold or an open task counts a skip and proposes nothing, so a long hold ends in one task. Status rows carry ids, times and a state, never the title or detail.
110
+ - Schedules (#512a, `src/hub/schedules.ts`, `Tasks.proposeFromSchedules`): the hub proposes a scheduled task only through `Tasks.propose` as `HUB`, never `Board.propose`, so the PII screen, class validation, triage and routing apply. "Its previous task is open" is read from the board (`openScheduled`: first history entry by the hub, detail starting `Schedule key: <id>`, state not closed), never from `schedule_runs`. A run restarts the period at `now`; a hold or an open task counts a skip and proposes nothing, so a long hold ends in one task. Status rows carry ids, times and a state, never the title or detail. A schedule is `every` or `cron` (+ `tz`), never both (#1017): every consumer of its period goes through `scheduleNext(entry, anchor)` and `scheduleZone`, never `parseEvery(entry.every)` (`every` is absent on a cron entry), and a view carries `cron` and `zone` instead of `every`. The cron core is pure and takes its time as an argument; keep the clock and the machine zone out of it, and keep the daylight-saving rule (a skipped time runs at the next valid minute, a repeated one once) in `instantOf` alone. No third-party cron or time-zone library.
111
+ - Research campaign dispatch (#930, #597; `docs/specs/2026-11-01-research-campaigns-design.md` "T2 dispatch and coverage implementation boundary"): a task bound to a campaign unit reaches an owner only through the paths that call `studyDispatchReason` / `studyDispatchCheck` (offer, assign, planner pull, owner change, acceptance, and a queued owner offer re-checked at delivery). Each reads the stored campaign, its deadline and stop limits, and for a hard cap the named budget, synchronously, and `assignOwner` asks again immediately before the owner write with nothing awaited between that read and the write; a new owner write asks the same read there rather than checking earlier. An explicit request refuses before any effect; the hub's own offer holds with `holdReady`. Budget enforcement is the named budget by id (`ExecutionBudget.covers`), never `applies` (any budget), and it only reads: a campaign never resets, copies or writes a counter, and a `kind: "run"` budget stays peer-wide. A read that fails holds, never dispatches, and a hold never touches a native turn already running. `studyShow` returns ids, enums, counts and times, and strips source ids from a private campaign. A later slice that exposes a new dispatch or wire requirement adds its own `DAEMON_FEATURES` label and recovery guard.
112
+ - Campaign storage (#930 T1, #596; spec "T1 implementation boundary"): every study write runs inside `studyWrite` (counted by `studiesPending`, which `recoveryReady` and the shutdown drain read; `stopStudies` fences new ones) and asks `requestCheck(by, () => this.studyHold())` again after every await and right before the board write. `studyApprove` screens and builds units outside any transaction, re-reads the latest hash and references, then writes once through `Board.activateCampaign`; never move a screen, source lookup or file read into that transaction. A replay creates nothing (it may only tighten privacy), a stable unit id keeps its task only for an identical definition (changed work takes a new id), and a private campaign never retains a public task. `research-campaign.ts` validates only inert plain data (plain prototype, no proxy, accessor or symbol key; a caller's `toJSON` never runs) before canonical hashing. A receipt carries ids, revision, status and counts; no raw contract or close reason enters parent history or memory.
113
+ - Experiments (#932, #598, #599; spec "Experiment T1 implementation boundary" and "T2 comparison implementation boundary"): `Tasks.experimentRecord` and `experimentShow` have no hub tool or CLI yet; `study_approve_protocol` (`experimentApproveProtocol`, person only) is the one wired op, and a new tool or form needs its own admission and recovery review. A writer is the current in-progress owner of a unit task in the protocol's current approved campaign revision (`Board.researchDispatchBinding`), rechecked after every screen, receipt read and hash. A host measurement is its own `research_attempt_observations` row, never a rewrite of a declared event, and no path upgrades a provenance level. Output-path privacy uses `matchesPolicy` with `failClosedOnDiscoveryLimit` (`experimentOutputPrivate`); keep that opt-in to experiments, never the default for other callers. Approval binds the exact protocol hash and evaluator hash, never carries to a changed hash, and a comparison never ranks.
114
+ - Study readback (#930 T3, #601): `ahub study draft|revise|approve|close` and the hub tools are thin callers of `Tasks.study*` and add no board write. The daemon's person ops are `study_draft|revise|approve|approve_protocol|close` (`taskOpBody`), refused on any non-console connection by an explicit `study_` check in the `task` control case before `Tasks` is asked; agents use `hub_study_draft` and `hub_study_revise` (configured planner through `planningAuthority`, never approve), `hub_study_show` (read-only, also in `READ_ONLY_HUB_TOOLS`) and `hub_study_handoff` (the bound in-progress owner). Pass the tool's arguments to `Tasks` whole: `studyFields` refuses an unknown field, and re-picking fields in the daemon would silently drop it (mutation-checked in `test/study-daemon.test.ts`). `STUDY_TOOLS` keeps the four tools' arguments and results out of memory capture. An implementer child gets `hub_study_show` and `hub_study_handoff` only. `Tasks.studyList` returns one page `{ campaigns, next? }` of receipts (ids, revision, status, counts), never question, scope or sources, at most 50 per call: the bound is the `LIMIT` in `Board.researchCampaignPage` (observed on its own SQL in `test/research-campaign-projection.test.ts`), `after` is the cursor, and a caller cannot raise the page size. `studyShow` blanks source ids for a private campaign and carries `budgetNamed`, so a named budget keeps every resource row with `-` for an absent limit: do not add text to either. `liveTestRefusal` refuses every `study` form except `show`, so an agent shell cannot delegate a study write to the person through `--live-test`; the contract file is read through one opened regular-file descriptor, at most 64 KiB plus one byte.
110
115
  - Folder triggers (#512c, `src/hub/folder-trigger.ts`, `Tasks.runFolderTriggers`): the folder pass shares `schedulesRunning` with the schedules pass and `ahub schedule run`, so it is serialized with them and counted by `schedulesPending`; keep it so, and never start it from a path that does not take the guard. A file is recorded in `folder_seen` only AFTER its task exists, and the pass stops at the first failure so order is kept; do not record first (a failure would lose the file) or continue past a failure. A task made for a file names the source id and never the file's name or path, because both can be private. A trigger's baseline (the files there when it starts, possibly over several polls) proposes nothing, and a refused folder never begins the trigger; do not make either propose. The scan must keep no file's bytes (it hashes in chunks under a file and byte budget and never cuts the listing); `registerScanned` re-reads the one file being registered and refuses it unless it still hashes to what the scan saw. Ask `isNew` for each file inside the loop, not only when building the list, and `observe` (never skip) every file whose content is already known, in the list and in the loop, so it is found by `known` next time and cannot starve a new file of the hash budget. `folder_triggers` stays in `MACHINE_LOCAL` and `NEVER_EDITABLE`.
111
116
  - A scheduled run is fenced twice (#512a): `proposeFromSchedules` checks the hold before it starts and passes `propose` a `fence` that `requestCheck` asks again right before the board write, because the PII screen and triage are awaited in between and `requestAuthority` only fences child generations. Any other host writer that awaits a screen before `Board.propose` needs the same fresh admission at the write. A recovery commit also waits for a pass in flight (`Tasks.schedulesPending`, read by `recoveryReady()` in the daemon): every host writer that can write across an await gets a pending count there, next to `checksPending` and `autoPullsPending`. A person's schedule command that changes what a run in flight should do (`pause`, `resume`, #335) must reach that run's fence too: `ScheduleStore.epoch` is bumped by `setPaused` and `runSchedule`'s fence compares it, so the acknowledged command wins over an admitted run; `ScheduleStore.ran` never moves the anchor back. Any manual entry into the same writer (`Tasks.runScheduleNow`) takes the same `schedulesRunning` guard and is registered with the shutdown drain (`manualSchedulePasses` in `startDaemon`), never called around it.
112
117
 
113
118
  - Person-or-conductor writers (#933): `importGitHub`, `approveIntake`, `runScheduleNow`, `setSchedulePaused` and external intake in `propose` admit only the person or a peer for which `TasksDeps.operational` answers true, and record that peer as the actor, never `USER`; `runScheduleNow` asks the same check again in the hold its board-write fence reads, so authority that ended during the PII screen writes nothing. `setRole` admits the person or the task's owner, and only the person while the task is `in_review`. An agent's `ahub task cancel` reaches `Tasks.cancel` unchanged; never add a second own-task rule for it. `task_label_check` (the daemon's answer before an agent's CLI writes a research label) admits only the task's owner of the current board row, so the CLI refuses an agent's `--created` (an earlier task with that id) before asking; an agent's label record carries `by`, and every reader of labels goes through `effectiveLabels` (`src/hub/research.ts`), where a person's label always wins.
114
119
 
115
120
  - Claim verdicts and fact_check provenance (#901, `Tasks.recordClaimVerdict`, `parseClaimVerdict`, `claimListId`): only the console user records a verdict (`ahub task claim`; an agent tool never does), the claim is named by its 1-based slot and its text is never typed, stored or printed, and every field is parsed closed. A verdict entry carries `list`, the `claimListId` of the plan's ordered claims, and counts only while that id equals the plan's current one, so a replaced or reordered list leaves the slot unknown. `claim verdict` is in `QUIET_EVENTS`. The hub stamps `checkKey` and `missingKeys` on its own check entries from the lookup that chose the command, never from output beyond the example check's exact last line. In `dogfood-profile.ts`, `factCheck.agreement` counts per label only the slots whose model verdict was entered and equals the person's: a slot without the model's verdict is unknown, one that differs counts for no label, and a person verdict alone is never an agreement. Every one of these rows is null, never zero, when its source is missing.
121
+
122
+ - Cross-project handoff (#604, agent-hub#1030, `src/hub/handoff.ts`, `Tasks.handoff*`, `Board.handoff*`): the pure core (`parseHandoff`, `resolveTarget`, `checkTarget`, `handoffEventFor`, `followBackLine`) takes no clock, file or socket; the daemon's `handoffOut` is the one origin path (parse closed, `handoffOutAuthority`: the person, a configured planner or the conductor, refused in a private turn and for a `[pii]` own task; then registry, the target's own status descriptor, the target's reply) and a refusal at any step leaves no row in either board. The target's `Tasks.handoffIn` parses again (a control message is data from another process), sanitizes, refuses a PII pattern or an uncleared screen, asks the daemon's recovery fence before and after the screen's await, and proposes through `board.propose(HUB, ...)`: the origin peer is a record in `task_handoff_origin` and never an actor, a roster peer or a `PeerId` that `Tasks` admits. The spec `issue` is kept as a line of the task's detail. Once the task and its origin row are written nothing fails the handoff (a link, notice or assignment failure becomes the reply's `note`), so the origin always learns the task id. `handoffStatesFor` answers only for tasks whose recorded origin is the asking project, with the fields a live event carries. The inbound `task_handoff`, `task_handoff_event` and `task_handoff_states` control messages are console-role only (the daemon answers an agent connection with a reason, and an unknown one with the usual "does not know"). Follow-back is read from the history entry's event and the task's state (`handoffEventFor`: observed names `accepted`, `done` (in_review, or approved for a class with no reviewer), `approved`, `declined`, `cancelled`), carries only an event name, a PR number and a full SHA, and never throws after a board write; the origin's outbound row is idempotent and a start-up catch-up (`handoffCatchUp`) asks each running target for the open ones. The tables are additive and `task_handoff` is its own recovery label while either holds a row (`Board.hasHandoffState`). `HANDOFF_TOOLS` keep the arguments and results out of memory capture. Never add text from the target to the follow-back line.
@@ -10,6 +10,7 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
10
10
  - A test timeout that fires while `Bun.spawnSync` runs can start the next test inside spawnSync's own event loop (Bun 1.3.14 and 1.4.2; background: README). `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, never above 20 s: an explicit larger timeout overrides the gate's `--timeout 20000`, so shorten the scenario instead.
11
11
  - 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`).
12
12
  - Bind agent identity markers explicitly in native/CLI fixture children. The full gate starts the test runner without inherited agent markers (including `AGENTHUB_DELIVERY_ONLY`) 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.
13
+ - A fixture that spawns the real channel plugin (`src/adapters/claude-channel.ts` or `plugins/agent-hub/server.js`) to act as a peer runs it with `cwd` at the hub's project root, made with `realPath` (the macOS temp dir is a symlink). The plugin chooses its hub through `channelProject` (rule: `daemon.md`), so from another cwd it joins the wrong hub, joins as a distinct session or connects nowhere. Only tools mode with both `AGENTHUB_STATE_DIR` and `AGENTHUB_PROJECT_DIR` set skips that check.
13
14
  - `bunfig.toml` preloads `test/hub-home.ts`, which first clears inherited `AMEM_HOME`, gives every test run (the gate and `bun test <file>` alike) a temp `AGENTHUB_HOME` unless one is set, and always replaces `KIMI_CODE_HOME` with a temp dir, an inherited value included (#285); scoped tests may set their own `AMEM_HOME` or `KIMI_CODE_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. The preload keeps an inherited `AGENTHUB_HOME`, which an agent shell exports as the person's hub home and `scripts/check.sh` does not unset: run `bun test` and the gate with `env -u AGENTHUB_HOME`.
14
15
  - `test/registry-guard.test.ts` statically scans `src/` against the committed `test/registry-guard.baseline.json` (#451); a test never writes, regenerates or repairs that baseline, and a failing guard is fixed in the source by moving the policy into a definition (rule: `docs/agent-notes/adapters.md`); the baseline stays `[]`. Scanner behavior is pinned on inline sources in `test/registry-guard-fixtures.test.ts`, never on edits to real files.
15
16
  - A fixture that needs a port takes it from a free-port probe, never a fixed number, which collides with whatever holds it on a shared runner (#548). The probe binds `127.0.0.1`, matching the loopback server that takes the port (#232).
@@ -37,7 +38,7 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
37
38
 
38
39
  - A daemon test that writes a config file and then calls `settings_reload` passes `configWatch: false` (`DaemonOptions`, #546), unless the watcher is what it tests. The daemon's file watcher (100 ms poll, 100 ms debounce) publishes the same bytes, so whichever of the two runs second reports `applied: []`, and a watcher pass between a preview and its confirm replaces the confirmation token. Never lengthen a sleep or loosen `applied` for it; the watcher has its own tests in `test/settings-reload-daemon.test.ts`.
39
40
  - A daemon test of a tool that reaches a public host passes the host-only transport seam (`DaemonOptions.webFetchTransport`, #553) and asserts what the daemon handed the tool, never a real round trip or a longer wait. The tool, the proxy URL and the allowlist stay the daemon's own; only the request is replaced. The proxy's own behaviour has its tests in `test/proxy.test.ts` and the web-proxy block of `test/web-fetch.test.ts`.
40
- - The preload replaces neither `HOME` nor `CLAUDE_CONFIG_DIR`. A test that spawns `ahub setup`, `ahub autonomy` or `ahub init` under machine defaults, or reaches `discoverClaudeHomes` or the profile hook, sets `HOME`, `CLAUDE_CONFIG_DIR`, `AGENTHUB_HOME` and `AMEM_HOME` to temp directories, as `test/autonomy-machine-cli.test.ts` does; unit tests pass `{ home, env }` instead of `process.env`.
41
+ - The preload replaces neither `HOME` nor `CLAUDE_CONFIG_DIR`. A test that spawns `ahub setup`, `ahub autonomy` or `ahub init` under machine defaults, or reaches `discoverClaudeHomes` or the profile hook, sets `HOME`, `CLAUDE_CONFIG_DIR`, `AGENTHUB_HOME` and `AMEM_HOME` to temp directories, as `test/autonomy-machine-cli.test.ts` does; unit tests pass `{ home, env }` instead of `process.env`. A test that runs `ahub doctor` through the CLI also sets `CODEX_HOME` to a temp directory and puts a fake `amem` first on PATH, because doctor runs `amem setup <tool> --json` (#1025, `test/doctor-memory-capture.test.ts`).
41
42
  - Raising the `@staix/agent-mem` exact pin moves its version literal in `test/memory.test.ts`, `test/memory-profile.test.ts`, `test/memory-profile-daemon.test.ts` and `test/doctor-model-aliases.test.ts` in the same PR; that is the only test edit a pin raise makes.
42
43
  - Keep CI's Bun version sourced from `.bun-version` through the pinned setup-bun action; update the version file rather than adding per-job version literals.
43
44
  - Release verification requires both OS checks and the aggregate `seeded guards` success for an identical tree, a tag matching `package.json` on `origin/main`, and fresh ancestor-bound documentation stamps. Preserve the separate npm OIDC publish job and GitHub Release write job; neither job gets the other's credential authority.
package/docs/dogfood.md CHANGED
@@ -10,6 +10,8 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
10
10
 
11
11
  | Feature | State here | Measured by |
12
12
  | --- | --- | --- |
13
+ | Distinguishable sessions and sender provenance (#1036, `session_identity`) | on after release and upgrade: a connection that names a peer id but is not that peer's current session is a distinct `<peer>~<origin>` session that can send and read, holds none of the peer's roles and replaces nobody; the channel plugin binds to its own project; every envelope and attach event records the connection. Nothing is refused to a connection that worked before. The recovery label `session_identity` applies once an unsettled delivery holds a session's message | `grep '"session":true' .agenthub/state/events.jsonl` lists every message a foreign session sent, with its minted id and no path or text; a recipient's header shows `from "claude~<id>"` for it; test/identity-provenance.test.ts (AC1, AC3, AC4, recovery label) and test/identity-channel-project.test.ts (AC2, real plugin against a new and an old hub) hold it. Not authentication: a same-user process can always act as the user |
14
+ | Cross-project task handoff with state follow-back (#604, `task_handoff`) | on after release and upgrade: `ahub task handoff` and `hub_task_handoff` register work for another registered project that runs agent-hub on this machine as an ownerless proposal on its board; the requesting peer is told each state change as a `kind: task` message. Nothing is queued, and the origin has no authority there | `ahub board --handoffs` lists what this project handed over with the last state each target reported; `ahub board` and `ahub task show` in the target name the origin project and peer; test/handoff-daemon.test.ts runs two real daemons (AC1 to AC5 and the recovery label), test/handoff-core.test.ts and test/handoff-board.test.ts hold the pure core and the tables |
13
15
  | `ahub down` (counterpart of `ahub up`) | on: an alias of `ahub kill` for stopping this project's hub | the person stops the hub with `ahub down` and `ahub status` then reports it stopped; test/identity.test.ts keeps it person-only (`console`) and labelled `down`, and test/help.test.ts holds its help entry |
14
16
  | Encrypted rooms (#446 X1) | off; source-only crypto/wire, with no broker, room link, invitation CLI or delivery enabled | real WebCrypto vectors and negative wire fixtures; atomic memory fake ports establish ordering only; durable X3 state and the X7 two-machine person receipt remain unverified |
15
17
  | Blind room broker (#446 X2) | off; standalone source/fixture work, no public TLS service or hub room link configured here | fake-client auth/fan-out/revocation/restart, ciphertext-only child-process canary, owner admission/offline room reset, queue caps/TTL/FIFO/migration, safe directory modes, and person-only CLI tests; live proxy/account/X7 receipts remain unverified |
@@ -24,7 +26,7 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
24
26
  | Headless Claude failure stages (task 551, #845) | source and fake-native fixtures; installed/native/account proof unverified until release and restart | fixed stage and host metadata in failed outcomes/logs; private stub, raw-canary absence, stale/foreign launch and bounded-probe cases in `test/claude-failure-stage-daemon.test.ts` |
25
27
  | Optional model for a headless Claude implementer (#844) | on after the release and once the person sets `model` on `instances.claude-impl` in `config.local.json` (a hub daemon restart applies it); unset it changes nothing | `ahub status` shows `model source` on the instance row; the native argv carries `--model <value>` on every turn and none when unset (test/claude-implementer.test.ts, test/claude-implementer-daemon.test.ts); a tracked-file or non-implementer declaration is refused or ignored with a reason (test/instances.test.ts, test/jcode-instances.test.ts) |
26
28
  | Remote isolated check transport (#793) | off here; fake checks, a bounded local Docker transport receipt and a local run of the lockfile-keyed dependency volume; person opt-in, SSH and the live receipt follow separately | remote tests capability unavailable on failed SSH/Docker/image/transfer probe; one networked install per new lockfile key, none on later runs, the check mounting the volume read-only with no network; exact Git receipt, non-root hardened argv, zero Mac dependency copies and confirmed Docker cleanup before lease release; Darwin tar ignores PATH shims, probe warnings do not pollute JSON, bounded probes do not become unknown task-run orphans |
27
- | Task board, routing, roles | on (claude planner, reviewer and implementer; codex, jcode, agy implementers) | board history: lead time from propose to approve, review rounds, escalations |
29
+ | Task board, routing, roles | on (claude planner, reviewer and implementer; codex, jcode, agy implementers) | board history: lead time from propose to approve, review rounds, escalations; owner new-head re-files and stale-verdict refusals (#1000 fixtures; active after upgrade) |
28
30
  | Readable `sdlc` output by default (#814 T1, #899) | on after the release: `ahub sdlc status\|explain\|queue\|release\|metrics\|costs\|authorize-release\|merge` print tables and labelled fields here; since #899 `ahub sdlc merge` (guarded merge, dry-run included) prints its report for a person by default and the JSON document only with `--json`, and `ahub doctor --orphans --kill` likewise prints the stopped pids by default and the hub's reply only with `--json`; scripts pass `--json` | the person reading `ahub sdlc status`, `sdlc metrics` and a `sdlc merge --dry-run` report without `--json`; `test/sdlc-output.test.ts` holds every `sdlc merge` outcome's readable default with `--json` byte for byte the pre-#899 output and the exit code unchanged; `test/conductor-cli.test.ts` holds the same for `doctor --orphans --kill`; `test/cli-json-guard.test.ts` lists the CLI paths that still print JSON by default (since #899 only the named data formats: its not-yet-converted list is empty) |
29
31
  | Readable restart, upgrade, recovery and model output by default (#814 T3) | on after the release: `ahub restart --dry-run`, `upgrade --dry-run`, `recovery status`, `models start`, `queue resolve` and `budget execution configure\|disable` print text here; scripts pass `--json` | the person reading a dry-run plan and a recovery status without `--json`; `test/cli-json-guard.test.ts` lists the CLI paths that still print JSON by default (target: only named data formats) |
30
32
  | Conductor-bound recovery continuation and task unassign (task 535, #900) | source/fixture coverage only; whether this repository's running installed daemon advertises the matching `conductor_recovery` feature is unverified | fake source/target authorization, same-actor and policy-mismatch checks, task-unassign actor/suppression fixtures; no native or real-account receipt; same-user key and record replacement remains outside the host boundary |
@@ -70,7 +72,7 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
70
72
  | Planner plan review (#481 S9) | advise | plans reviewed before build |
71
73
  | Planner board-changed notice (#758) | on here, with the lifecycle in `advise`: `claude` is the planner, so a card another peer proposes or re-plans reaches it once at task weight, and its own writes, state moves, reviews and assignments cost it no turn | per planner and week: `task`-kind hub envelopes whose body starts `The board changed` (target: one per planning change, none after the planner's own write), the same envelopes at `fyi` priority (recorded, no turn: at most one per 10 minutes), and the count still queued at once (target: at most one). Read from `ahub log` and the event log; the 12 copies in 44 minutes seen on 2026-10-24 are the baseline |
72
74
  | Planner capacity and dependency notices (#785 P1a) | source/fixture coverage; active after release and hub upgrade | write-triggered `capacity_released` and `backlog_ready` lines in `Planner work ready:` task envelopes; at most one queued envelope per effective planner, peer/WIP/card ids only; no automatic pull |
73
- | Release preparation and guarded tag (#787 P3b) | scratch-repo fixtures with a local bare remote; the real history was dry-run in a disposable clone; first real use is the next release | `scripts/release-prepare.ts` run on a `release/<version>` branch and a release PR that carries the version, CHANGELOG, report and stamps; `scripts/release-tag.ts` refusing without the person's consent and pushing one tag after it |
75
+ | Release preparation and guarded tag (#787 P3b) | scratch-repo fixtures with a local bare remote; the real history was dry-run in a disposable clone; first real use is the next release | `scripts/release-prepare.ts` run on a `release/<version>` branch and a release PR that carries the version, CHANGELOG, report and stamps; `scripts/release-tag.ts` refusing without the person's consent and pushing one tag after it; native version-named receipts retained while dated release reports are excluded (#1004 fixture) |
74
76
  | Opt-in auto pull (#785 P1c) | source/fixture coverage and a real daemon test; off by default, active only where a person sets `tasks.auto_pull` in `config.local.json` | `backlog pulled` history entries with `by: hub` and an `auto_pull:` note; at most one per capacity release; no pull with the setting off, for unreserved, PII, pinned, frozen or dependency-blocked cards |
75
77
  | Opt-in assignment queue backpressure (#800) | off by default; this change enables no live repository policy | `ahub route explain` and status queue-count/age/unknown reasons beside WIP; bounded summary reads, held-offer release, post-await offline/not-attached origin retention with plan-policy handoff, and role-scoped override regressions; no delivery disposition or routing-template change |
76
78
  | Per-head CI watcher and `ci_completed` (#785 P1b) | source/fixture coverage with a fake gh; active after release and hub upgrade | one `ci_completed:` task notice per head to owner and reviewer, a premature review re-offered once green; watchers started per `hub_task_done` head and gone at the result, deadline, newer head, verdict or shutdown; no gh read outside a watcher |
@@ -95,6 +97,7 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
95
97
  | Source registry, `ahub source add\|list` (#508 AC1, AC2, AC5 CLI) | on after release; local (no daemon needed for the person); `add` is the person's or the configured conductor's (#933, admitted and audited by the hub); `hub_source_read` adds bounded, screened snapshots and private stubs; plan validation and the dashboard list are later slices | `ahub source list` rows (id, size, private, origins); `sources/` under the hub state dir; a refusal in the CLI audit for an agent shell |
96
98
  | Intake drafts, `ahub intake` (#511 AC1 CLI, AC2 to AC4) | on after release; person-only except `approve`, which the configured conductor may run too (#933); a draft is held until `ahub intake approve`, so nothing changes for work that does not use it; the dashboard text box is a later slice | drafts opened, approved and discarded in `ahub backlog list` and the board history (`draft approved`, `intake unsplit` with its reason); the share of drafts the planner or hub model split without a reason on file |
97
99
  | Machine-local schedules (#512 AC1, AC3) | on after release for a person who adds `schedules` to `config.local.json` (`every` `<n>m\|h\|d`, 15 minute minimum); nothing runs without it, and a committed config cannot set it | `ahub status` shows one `schedule <id>` line (next run, last run, task, skipped runs); the backlog cards the hub proposed; no second card while the first is open |
100
+ | Calendar-anchored schedules, `cron` and `tz` (#1017) | on after release for a person who writes `cron` (five fields) with an optional `tz` in `schedules`; `every` is unchanged and a project without `cron` sees no change | `ahub schedule list` and `ahub status` show the expression and the next run in the schedule's zone; one task per fire, not one per missed fire after downtime; a skipped or repeated local time on a change day runs once; a too-frequent expression refused at load with the entry named |
98
101
  | Schedule commands and dashboard next run, `ahub schedule list\|run\|pause\|resume` (#512 AC4, AC3 UI) | on after release for a person who configured `schedules`; `run`, `pause` and `resume` are the person's or the configured conductor's (#933), nothing changes for a project with none; the dashboard table is read-only | `ahub schedule list` rows and the dashboard Schedules table (state, next run, last run, task); `paused` and manual runs in the board history of the task a run makes; a pause that is never replayed as a run on resume |
99
102
  | Folder triggers, `folder_triggers` (#512 AC2) | on after release for a person who adds `folder_triggers` to `config.local.json`; the first poll records only, nothing runs without it, and a committed config cannot set it | tasks whose first detail line is `Folder trigger: <id>`; `folder_seen` rows and `last_error` in hub.db; sources registered by a trigger in `ahub source list`; the share of new files that produced exactly one task |
100
103
  | Jcode peers told their hub tools are deferred MCP tools (#912) | on after release for every Jcode peer; nothing to configure | the first prompt of a Jcode session starts with the guidance; a fresh Jcode v0.99.x peer asked to reply with `hub_send` finds and calls it in its first turn (AC1, a live leg the conductor runs with the person after merge) |
@@ -139,6 +142,11 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
139
142
  | Cross-hub read invocations (#446 X6) | off until both persons expose peers and independently pin the exact capability/digest/read class; discovery is manual; two-daemon broker/crypto fixtures are the verification target, live X7 receipts remain unverified | source/target invocation ids, request admission versus capability result and result transport outcomes; needs_review counts and no duplicate native dispatch |
140
143
  | Dogfood scorecard (#687) | on after release; read-only weekly receipt | `ahub report --dogfood --since 7d --json`; bounded evidence and explicit unknowns |
141
144
  | Claim verdicts and fact_check provenance (#901) | source/fixture coverage; the live research pilot (a person entering verdicts with `ahub task claim`) remains unverified | `ahub report --dogfood` research rows: `claims.reachedPersonVerdict`, per-slot `personVerdict`/`modelLabel`/`agreement`, `factCheck.selection`/`passed`/`failed`/`missingKeysPerRun`; null until a verdict or a keyed check is recorded |
145
+ | Research campaigns (#930 T1) | source and CPU-only fixtures; no real campaign here; the public study CLI and tools are the T3 row below | draft versus child counts, stable unit/task ids across approved revisions and restart, intake reuse, stale role/source/revision refusals, private views, rollback and research_records recovery floors; no live/model receipt claimed |
146
+ | Study CLI, tools and readback (#930 T3, #601) | Fake peers and a real daemon over its control socket, plus the real `ahub` process against that daemon; no native peer or live model has run a campaign here, so fake versus native evidence stays separate (the `research-live-example` claim in docs/profiles.md is still open) | `test/study-daemon.test.ts` (person's draft, approve and close; no agent reaches an approve or close op; planner drafts and cannot approve; read-only show; a private source shows no source id, question, scope or path; handoff only from the bound owner with every missing field named; the three-source, two-peer example moving from none to incomplete to complete), `test/output-cli.test.ts` (parse before connecting, `--json` byte for byte), `test/study-guidance.test.ts` (guidance names only published tools) |
147
+ | Research dispatch and coverage (#930 T2) | Source/isolated fixtures only; no live research campaign | `test/research-dispatch.test.ts` and projection/recovery fixtures: no expired/revised dispatch, named-budget coverage, handoffs and shared ledger |
148
+ | Experiment records (#932 T1) | Source and isolated fixtures only; no live research attempt | Schema, storage, Tasks and recovery fixtures: declared/host provenance stays distinct, all attempts counted, replay and privacy fences |
149
+ | Experiment comparisons and protocol approval (#932 T2) | Fixture only; no scientific reproduction claimed on this repository | `test/research-comparison.test.ts`, `test/research-comparison-tasks.test.ts`, `test/research-protocol-cli.test.ts` and recovery tests verify incompatibility, fresh exact-hash approval, causal post-hoc context and restart identity; failed/cancelled attempts stay counted. |
142
150
  | Dirty-submodule notices (#798) | source/fixture coverage; the live writing/general pilot (a workspace with 26 submodules) remains unverified | `ahub turns` shows `<path> (changed, contents unobserved)` for a turn that only edited a submodule; the D1 notice at `hub_task_propose` and `hub_task_accept`; `ahub undo` names what it cannot restore; a project without `.gitmodules` makes the same git calls per turn (counting-shim test) |
143
151
  | Profile scorecard (#799) | source/fixture coverage; live writing/general/research pilot remains unverified | ahub report --dogfood profile rows; pure and reader dev byte goldens, private-text exclusion, research declarations/unknown verdicts and per-source unknown tests; anonymous weekly receipt templates |
144
152
  | Domain packs (#318/#322, #513) | off here: this repository has no pack in `.agenthub/roles`, so `ahub init --profile <pack>` is not used on it; live adoption in another project is unverified | the `ahub init --profile <pack>` preview and apply, the pack validator and the shipped-pack catalogue, covered by test/packs.test.ts (40 mutants killed); adoption would show as a `.pack.json` under `.agenthub/roles` and a `profile` in `.agenthub/config.json` of the adopting project, read from `ahub init --profile` runs. The writing-pack, analysis and ops packs load with shipped roles; test/domain-packs.test.ts executes all three standalone checks on synthetic passing/refusing fixtures. Their notes remain advisory and no check is auto-selected; live domain-workflow use remains unverified |
@@ -152,10 +160,12 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
152
160
  | Console and dashboard | on; #719 pending-history reads and bounded native launch retries take effect after release | control-request latency, connection timeouts and launch retry notices; daemon CPU/RSS samples, measured separately from lifetime CPU |
153
161
  | Usage view (#683) | CLI (`ahub usage`, U2), console Usage tab `7` (U3) and dashboard Usage view (U4) shipped | its own panels |
154
162
  | Kimi usage from its wire log (#285 phase 2) | on after the release when `budget.kimi_tokens_5h` is set; this repository sets it | `tokens` and `usage` (`kimi_wire`) events per Kimi turn, the `tokens` window in `ahub budget`, and the count of `kimi usage unknown` notices; the live check against Kimi's `/usage` (AC11) stays opens |
163
+ | Doctor memory-capture rows from agent-mem setup state (#1008) | on after release; `ahub doctor` runs here and reads `amem setup <tool> --json` (a preview that writes nothing) for `codex` and `kimi`; no dotfiles text and no `dot ai memory status` call | `memory capture: codex` and `memory capture: kimi` rows read ok (set up), warn with `amem setup <tool> --apply` (pending), warn with `client missing` (the native CLI is not installed) or unknown (no `amem`, a refusal or unparseable output); the Codex row notes the native `/hooks` review; a fixture run proves `dot` is never called |
155
164
  | Budget window kinds and notes (#285 phase 4) | on after the release; `ahub budget`, the console peer detail, the dashboard Usage budgets row and `ahub doctor` (`kimi usage source`) run here | each window shows its kind and source, a peer without a number says why (Pi, local and Kimi included); a manual `ahub budget set` value outranks the Kimi count at the gate; the doctor row reads ok while the wire log counts |
156
165
  | Settings reload | on | reloads applied, pending restarts |
157
166
  | Config editing from the console and dashboard (#689) | on after release; `ahub settings edit` and console key `c` run here, the dashboard Structure group needs `ahub ui --settings`; executable keys stay console only | `config_edit` events (edits applied by tier and source), refusals shown with the loader reason, restarts left pending |
158
167
  | Upgrade and recovery | on, per release | upgrade receipts in `docs/verification/` |
168
+ | acon console plugin (#935) | off here until a person runs `ahub setup` after the release and `acon ui` on this machine; the plugin registers per agenthub home, not per repository | `acon plugins` shows `ahub verified`; the console's agent-hub panel shows this repository's live board, and an approval answered from the console inbox appears in hub.log as `answered option allow_once by dashboard`. Space (#1012): adopted with the plugin, no separate switch; acon's Space view (Flat, Room, 3D) shows this repository's peers as actors (state and marks matching the dashboard's office), the board columns as queue/work/review/done zones with the same counts, and overlap links plus message arcs (broadcasts to every other desk) as links; clicking an actor or card opens this project's agents or board panel |
159
169
  | Agent CLI version policy and trust (#691) | on after the release: Codex 0.163.0 verified by the [2026-10-22 receipt](verification/2026-10-22-codex-reviewer-isolation.md) and 0.164.0 by the [2026-10-24 receipt](verification/2026-10-24-codex-0164-reviewer-isolation.md); Kimi 2.3.0 and Qwen 0.26.0 verified by the [model-free receipt](verification/2026-10-22-kimi-qwen-compat.md), Kimi 2.3.1 by the [2026-10-25 model-free receipt](verification/2026-10-25-kimi-231-reviewer-compat.md) and 2.3.2 by the [2026-10-26 one](verification/2026-10-26-kimi-232-reviewer-compat.md), with account/provider live legs unverified; agy 1.4.4 in-line | model-free isolation receipts, `ahub agents` verdicts, versions trusted per machine, lead time from an `agent-compat` issue to a receipt |
160
170
  | Qwen's own TUI over dual output (#286 Part 3) | off here: this repository's hub runs no Qwen peer (roster.qwen is off), and the limits (a hub deny ends the turn without an answer; a stalled turn is stopped only with Esc in the Qwen terminal) are for the person at that terminal to accept. Verified on Qwen 0.26.1 under an isolated HOME by the [2026-10-25 receipt](verification/2026-10-25-qwen-dual-output-t0.md); the live account leg is the person's | `test/qwen-dual.test.ts` against a fake dual-output Qwen; in real use `ahub status` stalled/detached notices per peer and the `needs_review` count of Qwen deliveries (an Esc or an exited terminal ends a turn that way; a deny completes) |
161
171
  | Benchmarks (#251, #637) | off: `bench.enabled = false`. The task-routing gate (#637 part 1) is on in CI for every PR: `routing-receipt` fails a change to `templates/routing.toml` without a matching receipt; the model-routing gate (#637 part 2) is the second step of the same workflow: `scripts/check-model-routing-receipt.ts` fails a change to the model-routing keys without a matching `-modelrouting-` receipt, and wants a by-hand `--live` half when an alias or judge moves | bench receipts; `docs/verification/*-routing-*.md` and `*-modelrouting-*.md` receipts; a `routing-eval` check run on every PR with `eval_pass_rate=` and `eval_suite=` in its title (name it as `eval_check` to make it the eval gate); `bun scripts/benchmarks/routing-gate.ts` and `model-routing-gate.ts` print the default's numbers (100% on both splits); the live half (served model ids) has not been run against a gateway yet |
package/docs/memory.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Hub memory integration
2
2
 
3
- Agent-hub consumes the exact-pinned first-party [`@staix/agent-mem`](https://github.com/STAIxBWLB/agent-mem) library (current pin `0.15.0`, initial pin `0.1.1`). The package owns portable storage, tiers, graph, goals, sync, import and raw commands. Agent-hub owns caller authority, current project privacy policy, task lifecycle integration and presentation. Claude-mem is an optional, explicit import source only; runtime recall and capture make no worker HTTP requests.
3
+ Agent-hub consumes the exact-pinned first-party [`@staix/agent-mem`](https://github.com/STAIxBWLB/agent-mem) library (current pin `0.18.0`, initial pin `0.1.1`). The package owns portable storage, tiers, graph, goals, sync, import and raw commands. Agent-hub owns caller authority, current project privacy policy, task lifecycle integration and presentation. Claude-mem is an optional, explicit import source only; runtime recall and capture make no worker HTTP requests.
4
4
 
5
5
  ## Portable core and data directory
6
6