@staix/agent-hub 0.15.0 → 0.16.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 (80) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +12 -8
  3. package/docs/agent-notes/README.md +4 -0
  4. package/docs/agent-notes/adapters.md +6 -3
  5. package/docs/agent-notes/budget.md +4 -2
  6. package/docs/agent-notes/daemon.md +9 -3
  7. package/docs/agent-notes/local-worker.md +2 -2
  8. package/docs/agent-notes/tasks.md +13 -1
  9. package/docs/agent-notes/tests.md +3 -0
  10. package/docs/dogfood.md +13 -4
  11. package/docs/events.md +1 -1
  12. package/docs/memory.md +78 -10
  13. package/docs/operations.md +116 -30
  14. package/docs/quickstart.md +8 -4
  15. package/docs/security.md +80 -18
  16. package/docs/smoke.md +7 -3
  17. package/docs/specs/2026-09-19-agent-hub-design.md +84 -8
  18. package/docs/verification/2026-10-24-always-ask-adapters.md +52 -0
  19. package/docs/verification/2026-10-24-amem-runtime.md +69 -0
  20. package/docs/verification/2026-10-24-codex-0164-reviewer-isolation.md +55 -0
  21. package/docs/verification/2026-10-24-delivery-holds-ac8.md +264 -0
  22. package/docs/verification/2026-10-24-hardcoded-values.md +2108 -0
  23. package/docs/verification/2026-10-25-0.16.0.md +82 -0
  24. package/docs/verified.json +26 -17
  25. package/package.json +2 -2
  26. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  27. package/plugins/agent-hub/server.js +2085 -1702
  28. package/src/adapters/acp.ts +5 -2
  29. package/src/adapters/codex-appserver.ts +69 -1
  30. package/src/adapters/compat.ts +2 -2
  31. package/src/adapters/definitions/builtin-metadata.ts +3 -3
  32. package/src/adapters/local-worker.ts +5 -4
  33. package/src/adapters/registry-types.ts +1 -1
  34. package/src/cli/console-state.ts +20 -2
  35. package/src/cli/console-symbols.ts +3 -2
  36. package/src/cli/console-usage.ts +5 -2
  37. package/src/cli/console.ts +5 -1
  38. package/src/cli/help.ts +7 -5
  39. package/src/cli/identity.ts +3 -1
  40. package/src/cli/main.ts +44 -10
  41. package/src/cli/memory.ts +119 -7
  42. package/src/cli/output.ts +12 -4
  43. package/src/hub/action-class.ts +211 -17
  44. package/src/hub/action-enforce.ts +13 -5
  45. package/src/hub/action-policy.ts +19 -8
  46. package/src/hub/ask.ts +16 -4
  47. package/src/hub/board.ts +86 -31
  48. package/src/hub/budget-notes.ts +65 -0
  49. package/src/hub/budget.ts +42 -10
  50. package/src/hub/bus.ts +1 -1
  51. package/src/hub/code-facts.ts +56 -16
  52. package/src/hub/conductor.ts +5 -2
  53. package/src/hub/config-apply.ts +7 -0
  54. package/src/hub/control-client.ts +1 -1
  55. package/src/hub/daemon.ts +250 -49
  56. package/src/hub/envelope.ts +1 -1
  57. package/src/hub/events.ts +2 -2
  58. package/src/hub/hub-tools.ts +3 -3
  59. package/src/hub/kimi-usage.ts +12 -0
  60. package/src/hub/lifecycle.ts +10 -5
  61. package/src/hub/memory-code-head.ts +44 -0
  62. package/src/hub/memory-code-overlay.ts +119 -0
  63. package/src/hub/memory-plan-guard.ts +10 -1
  64. package/src/hub/memory-stale-anchors.ts +159 -0
  65. package/src/hub/memory-symbols.ts +74 -0
  66. package/src/hub/planning.ts +10 -4
  67. package/src/hub/read-tools.ts +21 -3
  68. package/src/hub/sdlc.ts +34 -0
  69. package/src/hub/settings.ts +17 -2
  70. package/src/hub/settled-roles.ts +15 -3
  71. package/src/hub/task-sweep.ts +25 -3
  72. package/src/hub/tasks.ts +353 -64
  73. package/src/hub/token-window.ts +15 -2
  74. package/src/hub/window-kind.ts +13 -0
  75. package/src/local/tools.ts +5 -5
  76. package/src/memory/brief.ts +22 -12
  77. package/src/memory/capture.ts +26 -40
  78. package/src/memory/client.ts +137 -65
  79. package/src/memory/recall.ts +13 -22
  80. package/src/ui/index.html +100 -18
package/CHANGELOG.md CHANGED
@@ -4,6 +4,31 @@ 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.16.0
8
+
9
+ - The project daemon stops through its normal stop on SIGHUP and SIGQUIT too, not only SIGINT and SIGTERM, so memory sync git (detached since agent-mem 0.7.0) never outlives the hub; SIGQUIT no longer reaches its default core dump (#438).
10
+ - Raise the exact `@staix/agent-mem` pin to 0.7.0: standalone setup, notes and the review queue, decay and promotion candidates, recall validity with person-only history, intent briefs and use telemetry, doctor and the sync cadence. The hub passes its own command names (`ahub memory`, `ahub goal`) into the library's usage and refusal text. A generation home now lists its candidate review queue instead of refusing it. agent-mem 0.7.0 runs memory sync git in its own process group, which the daemon stops on all four exit signals (#438).
11
+ - Add read-time memory code neighbours (#684): opt-in dashboard calls with commit-stamped dashed edges, bounded symbol graph overlays, fails-first related task briefs and a console note count. Code edges are never stored; unknown and partial evidence stay explicit.
12
+
13
+ - Fix #758: the planner "board changed" notice skips the planner whose own write changed the board, tells only a card's planners, replaces an unsettled copy so a planner meets the latest revision once, and is recorded at `fyi` weight (no turn) for state moves, reviews and assignments, at most once per planner per ten minutes. A planning revision or dependency change, and a new card, are still task notices; the hub's own bookkeeping entries tell nobody.
14
+ - Verify the Codex 0.164.0 fresh reviewer (#757): the model-free production `codexReviewerBackend.prepare` passed every isolation leg under temporary HOME and CODEX_HOME (empty and synthetic-ambient configuration, all native canaries) and the six source claims were rechecked at upstream `rust-v0.164.0`, so `COMPAT["codex-review"].verified` lists 0.164.0 and `ahub codex --as codex-review` no longer needs `ahub agents trust` on it. Receipt: `docs/verification/2026-10-24-codex-0164-reviewer-isolation.md`.
15
+ - Memory symbol follow-ups (#684): reuse one exact-tree preparation and unknown name lookups at done, query historical raw signatures alongside normalized nodes, label index-only absence, and keep optional doctor cleanup failures neutral. Transient worktree cleanup remains a person action.
16
+
17
+ - Add the `closing-refs` check (#760): a PR workflow fails a pull request whose `closingIssuesReferences` names an issue that its title and body only mention with `Refs`, a negation or a qualifier ("Does not close #15" is a closing keyword to GitHub), and one whose commit messages carry a closing keyword the PR text does not mean. AGENTS.md gains the rule.
18
+ - Fix #762: the always-ask classifier no longer drops a command at its first shell expansion. `git push origin $B`, `npm publish $D`, `gh issue create --title "$T"` and `rm -rf "$DIR"` are classed from their literal words, the command inside `$(...)` or a backtick is classed too, and where the opaque part can change the class (`git $CMD`, `gh api -X $M`, `gh api graphql -f query=$Q`, an opaque `rm` target) the strictest reachable class is used. A command with no classed literal prefix stays unclassed. `gh repo delete|archive|edit`, `gh secret|variable set|delete` and `gh label create|edit|delete` are `external-write`.
19
+ - Fix #756: the always-ask classifier classes `gh api graphql` by operation. One literal `query` field whose every operation is a query (or shorthand `{...}`) is a read and no longer asked about; a mutation, a subscription, a second query field, a query from a file or stdin and any unparsable document stay `external-write`. `gh run rerun|cancel|delete` and `gh workflow run|enable|disable` are `external-write`. REST `gh api` is unchanged.
20
+ - Backlog planning (#759): dependency-ordered phases sharing an issue are no longer duplicate candidates; overlap findings remain. Effective planners/persons relink only repo/issue identity on unstarted backlog through a planning revision, invalidating old spec acceptance. Omitted/empty tool fields record reason-only revisions; CLI `{}` and tool/help descriptions match.
21
+
22
+ - Add per-task and release planners (#525-2): one or up to eight explicit planner holders, person-only planner reassignment, scoped planner tools and notices, independent plan review with recorded authorship/self-review, and a persisted-authority recovery floor. Legacy null bindings retain their existing authority; singleton settings remain strings.
23
+
24
+ - `ahub deliverables [--task <id>] [--approved] [--check] [--json]` and a dashboard badge (#509b, #317): the person's list of what each task delivered and a reviewer approved (id, hash, size, and a path only where the PII screen allows), `--check` measures the files now and marks one `changed since review`. The dashboard card shows a `Deliverables n` chip and the task sheet a Deliverables group from the board's records, never a file read on the poll; a measurement shows only after a live read. An agent's shell is refused. No new stored data.
25
+ - Canonical task-memory symbol anchors (#684 phase 3): completion notes use current observed symbols with positive code-facts evidence, earlier notes stay connected through their task, and `doctor --stale-anchors` provides a bounded read-only report. Unknown or private evidence never falls back to plan guesses.
26
+
27
+ - Replace claude-mem worker runtime calls with exact-pinned amem 0.6.0 (agent-mem#13 P4): curated recall, briefs and ask use in-process storage; local capture is deterministic and model-free. Privacy and selected-profile fences remain; raw capture is excluded from evidence. Auxiliary model/checkpoint writes become pending candidates. Claude-mem remains an explicit import source only.
28
+
29
+ - Budget window kinds, precedence and notes (#285 phase 4, AC6, AC7, AC8, AC10): `UsageWindow` gains an optional `kind` (`native`, `count`, `manual`). `ahub budget` prints each window's kind, source and age, marks a hub count that a native or manual reading outranks as `info only`, and says why a peer shows no number (Pi and local have no quota and show the hub's own 5 h token count; Kimi without `budget.kimi_tokens_5h`, or with an unreadable wire log, says so). A fresh `ahub budget set` value replaces the provider's and the hub's for its window at the gate and in routing headroom, and a provider refusal clears it. The console peer and Usage details and the dashboard Usage budgets row show kind and source, and `ahub doctor` gains `kimi usage source`. `PROTOCOL` is unchanged.
30
+ - Complete the remaining always-ask adapter paths (#636): received Codex command approval RPCs, local worker and Pi model tools share the classifier and explicit class-budget gate. Codex native no-request calls remain not enforceable in status; local/Pi modes cannot suppress a classed prompt. The local/Pi console shows the complete command even during a private turn; cut or withheld native views are deny-only. Idle headless Claude instances report no current enforcement (#402); weakened local startup policies surface attention.
31
+
7
32
  ## 0.15.0
8
33
 
9
34
  - `ahub backlog list` prints a table, one line per card by rank (RANK, ID, PRI, RESERVED, AFTER, CLASS, AGE, TITLE, with titles cut to the terminal width), and a count by reserved owner; `--full` and piped output keep whole titles, `--json` prints the raw listing (#399).
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.15.0, control protocol 16. Bun + TypeScript, one loopback daemon per
8
+ Status: 0.16.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
@@ -35,26 +35,30 @@ ahub ui # the web dashboard
35
35
  into digests, priorities decide what interrupts a turn, and delivery survives
36
36
  restarts.
37
37
  - **A shared task board.** Work is proposed, accepted, done and reviewed through hub
38
- tools. Roles split planning, implementation and review, `routing.toml` picks
38
+ tools. Roles split planning, implementation and review (up to eight planners,
39
+ each task bound to one), `routing.toml` picks
39
40
  owners by task class, and owners of overlapping work are told what changed. A
40
41
  peer that accepted or finished a task never reviews it, a review request lists
41
42
  the task's acceptance criteria by id, and a done report can name deliverable
42
- files that the hub hashes itself, so an approval binds the bytes reviewed. Extra
43
+ files that the hub hashes itself, so an approval binds the bytes reviewed, and
44
+ `ahub deliverables` lists them. Extra
43
45
  Claude Code instances can implement in their own worktree, in a terminal or
44
46
  headless with one session per task. `ahub sdlc metrics` reads lead times,
45
47
  rework and stalls from board history, and
46
48
  optional control bands turn a breach into a backlog card.
47
49
  - **Shared memory.** Decisions, contracts, fails and findings are kept through the
48
- first-party [`@staix/agent-mem`](https://github.com/STAIxBWLB/agent-mem) library
49
- (short- and long-term tiers, a typed graph, git sync between machines). Agents get
50
+ first-party [`@staix/agent-mem`](https://github.com/STAIxBWLB/agent-mem) library,
51
+ in process with no claude-mem worker (short- and long-term tiers, a typed graph,
52
+ git sync between machines). Agents get
50
53
  it as a recall preface, as briefs on a task, and through bounded search tools.
51
54
  - **Local and on-prem models.** Pi and the local worker can run on DGX or local MLX
52
55
  through an authenticated relay. Tasks marked as PII stay on the on-prem worker.
53
56
  - **Guard rails.** The local worker and Pi run in a sandbox, and their writes and
54
57
  shell commands wait for your approval unless you choose a looser permission mode.
55
58
  Publish, external-write, destroy and install calls ask a person in every mode for
56
- Claude Code and hub-owned ACP peers such as Kimi, wherever the hub sees the call
57
- before it runs; `ahub status` names each peer where it cannot. Budgets pause an
59
+ Claude Code, the local worker, Pi, hub-owned ACP peers such as Kimi and the
60
+ approval requests Codex sends, wherever the hub sees the call before it runs;
61
+ `ahub status` names each peer where it cannot. Budgets pause an
58
62
  agent before it runs out of quota, and recovery never replays an
59
63
  effect whose outcome is unknown.
60
64
  - **Operations.** A terminal console and a local dashboard, one hub per repository or
@@ -90,7 +94,7 @@ uses the home must run a version that understands the new layout.
90
94
  [#258](https://github.com/STAIxBWLB/agent-hub/issues/258)).
91
95
 
92
96
  Live checks that still need a real account or a person are listed in the
93
- [release verification report](docs/verification/2026-10-23-0.15.0.md) and the
97
+ [release verification report](docs/verification/2026-10-25-0.16.0.md) and the
94
98
  [smoke checklist](docs/smoke.md).
95
99
 
96
100
  ## Documentation
@@ -39,3 +39,7 @@ Background moved out of the area notes for 0.13.0:
39
39
  Background moved out of the area notes for 0.15.0:
40
40
 
41
41
  - I-2 as the last reviewer check (#744, tasks.md): placed earlier, it refused an offline former implementer as "implemented this task" instead of "offline", which turned a task that used to approve directly into a blocked person review.
42
+
43
+ Background moved out of the area notes for 0.16.0:
44
+
45
+ - Closing-refs negation list (#760, tests.md): agent-mem PR #36 said "Does not close #15", GitHub read `close #15` as a closing keyword, and the design issue closed with T3 open. The list errs toward flagging: a false positive costs a rewrite of the PR text, a false negative closes a design issue, and a body that really means a qualified close is the author's to reword. A keyword typed into a comment or the merge dialog is out of the check's reach. At 0.16.0 `closing-refs` is not a required status check (branch rules require `check (ubuntu-latest)`, `check (macos-latest)` and `seeded guards`).
@@ -52,7 +52,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
52
52
 
53
53
  - Every agent the hub spawns (the Codex app-server, ACP agents such as Kimi, Pi) runs in its own process group and is stopped as one (`stopOwnedProcess(proc, { group: true })`, `trackGroup` after spawn): `codex` is a node launcher, and mid-turn its native app-server does not exit on SIGTERM within the grace period, so a SIGKILL to the launcher alone left the app-server running under init and holding the daemon alive (#113, #115). A new builtin adapter that spawns an agent does the same. A configured ACP kind stops only through `AcpOwnedGroup` (`src/adapters/acp-owned-group.ts`): capture the direct child and its original group after spawn, verify before admission and again before stop, SIGSTOP, re-verify, then SIGKILL only that group; never discover or signal descendants outside it, and with unproven ownership send no signal, close stdin and keep the owner until it is confirmed gone.
54
54
  - To stop a process tree, freeze it (SIGSTOP) before reading what it contains, then SIGKILL, and call it done only when the table shows none of it (with no table at all, the stop can only ask the group itself, which cannot see groups below it). A process a read after the freeze shows for the first time is frozen and read again before anything is killed.
55
- - Work that must survive `ahub kill` (claude-mem `session-end`) is awaited in `stop()`; fire-and-forget dies with the process.
55
+ - Work that must survive `ahub kill` (amem capture session end) is awaited in `stop()`; fire-and-forget dies with the process.
56
56
  - Native owner teardown must handle a lost shutdown acknowledgement using verified process identity. Pending launches must be revocable, and active tools/streaming output must count as watchdog activity. A reloaded `watchdog_ms` reaches a peer only through `configureWatchdog` (`src/hub/peers.ts`, #439) and applies from its next busy turn, never to the running one.
57
57
  - `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.
58
58
  - 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.
@@ -109,6 +109,9 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
109
109
 
110
110
  ## Always-ask (#636)
111
111
 
112
- - A classed call is decided in `src/hub/action-enforce.ts` and enforced where the hub sees it before it runs: the Claude PreToolUse hook (`actionClasses` on the `facts` reply, `ask` beats `allow` in every mode) and `AcpPeer.answerPermission` (`AcpOptions.actionGate`, consulted before `autoApprove`). It only ever adds an approval: unknown, unparsable or no reported payload is not classed and follows the mode. A throwing gate asks, a classed `PermissionRequest` (`classed: true`) is never answered by `--unattended`, and `allow_always` is withheld for it.
113
- - The class-budget choice is the hub's own option (`ACTION_BUDGET_OPTION`), answered to the agent as its own `allow_once`; only that choice calls `ActionBudgets.approve`, and a refusal answers cancelled. Never let an ordinary allow seed the ledger. Claude never consults the ledger. `ActionBudgets.approve` consumes a slot when another approval landed since its challenge was issued (requests are concurrent; a reset would let N+1 calls through), and the daemon revokes a peer's entry on its `offline` transition and before creating a new ACP owner, so a resumed session id never revives an allowance.
112
+ - Class calls in `src/hub/action-enforce.ts`: the Claude PreToolUse hook returns `actionClasses` (`ask` beats `allow`), `AcpPeer.answerPermission` consults `actionGate` before `autoApprove`, and the daemon's `classedToolPermission` runs before `never-ask`, `ask-when-needed` and `piAlways` in local `permit` and `piPermit`. Keep Pi's gate inside its persisted tool receipt so a replay never spends twice. Classify a local/Pi call with `cwd` and `workdir` set to the host execution root, never the model's fields. Unknown, unparsable or absent payload follows the native mode; a classed request is never answered by `--unattended`. A gate that throws asks deny-only (`command withheld`, `denyOnly`), never allows.
113
+ - Local/Pi approvals supply the host's complete tool title only through the optional `onPremToolTitle` port; match it against the exact bash/git input before showing it with the class line. The person's on-prem console gets that full title even on a PII turn. Keep it out of logs, public status, other peers and dashboard projections. Public/native cut or withheld views set `denyOnly`: drop every grant and budget choice, and reject forged allow/budget answers. Never infer completeness from the presence of a marker string.
114
+ - Codex (`interceptActionApproval` in `codex-appserver.ts`) intercepts only `item/commandExecution/requestApproval` and legacy `execCommandApproval` with a well-formed command; any other request, or no `actionGate` verdict, keeps native handling. Classed command approvals need the current link, thread and observed active turn; without that binding deny, including legacy exec approvals. A pending classed wait is aborted, and answered deny, on stop, watchdog, detach, thread adoption, its turn's completion or interrupt. Keep `approvalIds` tombstones for the socket lifetime and suppress late/duplicate TUI replies. At 1024 ids close both sides rather than evict a fence; reattachment is the upgrade path. Native calls that emit no request remain not enforceable.
115
+ - Action allowances use their own host `actionSession` port; do not widen `contextSession` for Local and thereby change checkpoint, telemetry or supervision behavior. Managed headless Claude enforceability needs a current child generation in busy state, never a truthy method reference.
116
+ - The class-budget choice is the hub's own option (`ACTION_BUDGET_OPTION`), answered to the agent as its own `allow_once`; only that choice calls `ActionBudgets.approve`, and a refusal answers cancelled. Never let an ordinary allow seed the ledger. Claude never consults the ledger. `ActionBudgets.approve` consumes a slot when another approval landed since its challenge was issued (requests are concurrent; a reset would let N+1 calls through), and the daemon revokes a peer's entry on its `offline` transition and before creating a new owned adapter, so a resumed session id never revives an allowance.
114
117
  - Do not claim enforcement a native mode bypasses: Kimi's `yolo` and `auto` send no request for a call they allow, so `ahub status` lists that peer as not enforceable until it is back in `ask`.
@@ -1,6 +1,6 @@
1
1
  # Budget, pause and handoff
2
2
 
3
- Scope: quota and native context readings, checkpoints, pause, handoff and resume, and the status line tee. Read before editing `src/hub/budget.ts`, `src/hub/context-window.ts`, `src/cli/statusline-tee.ts`, `src/adapters/definitions/native-quota.ts`, `src/adapters/definitions/native-context.ts`, `src/hub/bus.ts`, `src/hub/delivery-journal.ts`, `src/hub/restart.ts` (pause persistence), the per-account fanout in `src/hub/instance-budget.ts`, the usage roll-up in `src/hub/usage-rollup.ts` or Kimi's count (`src/adapters/kimi-wire-usage.ts`, `src/hub/kimi-usage.ts`, `src/hub/token-window.ts`).
3
+ Scope: quota and native context readings, checkpoints, pause, handoff and resume, and the status line tee. Read before editing `src/hub/budget.ts`, `src/hub/window-kind.ts`, `src/hub/budget-notes.ts`, `src/hub/context-window.ts`, `src/cli/statusline-tee.ts`, `src/adapters/definitions/native-quota.ts`, `src/adapters/definitions/native-context.ts`, `src/hub/bus.ts`, `src/hub/delivery-journal.ts`, `src/hub/restart.ts` (pause persistence), the per-account fanout in `src/hub/instance-budget.ts`, the usage roll-up in `src/hub/usage-rollup.ts` or Kimi's count (`src/adapters/kimi-wire-usage.ts`, `src/hub/kimi-usage.ts`, `src/hub/token-window.ts`).
4
4
 
5
5
  - Budget: checkpoint first, pause second (a paused peer receives nothing). A handoff that fails is left unmarked so the next tick or hub run retries it; never record it as done.
6
6
  - A handoff needs somebody to hand over to: `canHandOff` is false right after a restart, when no peer is attached yet, and the handoff waits for a later tick instead of stripping tasks of their owner.
@@ -22,10 +22,12 @@ Scope: quota and native context readings, checkpoints, pause, handoff and resume
22
22
  - Key quota readings, pause records, overrides, checkpoints and handoffs by the canonical account (`hooks.account`, idempotent); telemetry keeps the reporter id and native context occupancy stays per instance. `instanceBudgetHooks` fans pause, resume, checkpoint and handoff out to every member, and `canHandOff` excludes the paused account's siblings. Startup normalization merges old alias pause rows without dropping a row or marking an unfinished handoff done. A manual pause stays per member. Reviewer and headless implementer variants keep their base kind's `budget.account`.
23
23
  - Release-cut quota admission (`releaseQuotaSnapshot` in `src/hub/daemon.ts`, #481 S11): an account has a quota source only when one of its members' definitions has `budget.reader`, or `budget.resetSoftTokensOnResume` with `kimi_tokens_5h > 0`. Without a source a registered account is reported `no quota source` and never defers; with one, an unknown, stale or below-gate reading defers. Manual and budget-record pauses defer for every attached peer.
24
24
 
25
+ - Window kinds (#285 AC6, AC8): classify a window only through `windowKind` (`src/hub/window-kind.ts`: `native`, `count`, `manual`); a reporter that knows its kind passes it (`ahub budget set` is `manual`, the Kimi count `count`). Readings are kept per window id and kind, never overwriting each other. The gate and `headroom` act only on `gating()`: a fresh `manual` reading replaces the others for its window, and the hub `count` never gates while a fresh `native` or `manual` reading exists. A hard refusal drops the `manual` reading of each window it reports. `status()` lists the gating reading first and marks the information-only fresh ones `superseded`; never gate or route from `status()` windows.
26
+ - No-quota notes (#285 AC6, AC7): a peer whose definition sets `budget.noQuotaReason` (Pi, local) gets a hub 5 h token count (`hubTokenWindows`, replayed at start through `TokenWindow.seed`) as information only, never a window, a meter or a gate. Build the notes in `ahub budget`, the console and the dashboard only through `budgetNotes` (`src/hub/budget-notes.ts`) from the definition and the counters, and Kimi's source state only from `KimiUsage.status()`: a rule and a count, never a path, a session id or log text.
25
27
  - Keep `readClaudeTranscriptUsage` at its 32 MiB accounting bound. For native turn completion, follow the admission rule in `daemon.md`.
26
28
 
27
29
  - Limit ledger (#683 U1): write `budget_pause`, `budget_resume` and `quota_reset` only through `Budget.writeLedger` (`hooks.ledger`), at the one pause INSERT in `evaluate` (a new pause that replaces an expired open record first writes that record's `budget_resume`, `by: "hub"`) and the one DELETE in `resume` (`by: "person"` only from `override`); never from a call site that reads `budget_pauses`. `quota_reset` is derived in `deriveResets` from the last reading of each window (`lastSeen`, never `readings`, which a resume clears): a forward `resetsAt` move of more than `RESET_JITTER_MS` (15 min), or a `used` drop of `RESET_DROP` (0.5) or more within one window, is a reset, and anything smaller is the same window. It is deduplicated by `peer|window|resetsAt` in `resetsRecorded`, which the daemon seeds from `events.jsonl` at start so a restart adds none. A reading older than the baseline never counts. Entries carry ids, windows and numbers only: never a reason built from agent text, a checkpoint, a summary or a task title (the reason is the coordinator's own text). A throwing hook never costs the pause or the reading. Codex's `usage` event comes from `onTokenBreakdown` beside `onTokens`; keep `tokens` unchanged.
28
30
 
29
31
  - Usage roll-up (#683 U2): `src/hub/usage-rollup.ts` is the only reader of `events.jsonl` for `ahub usage`, the console Usage panel and the dashboard Usage view (rules: `daemon.md`). A pass reads from the stored offset, keeps whole lines only and commits its rows with the new offset in one transaction; its 20 ms slice (`poll(maxMs)`, #719) stops only between whole records and commits the offset of the last one folded; a line longer than the cap is read whole and one past `MAX_LINE_BYTES` is skipped, so the fold always advances. Never store a title, reason or summary there; `limit_events` drops `reason`. issue/pr/release are joined from the board at query time through `TaskLinks`, never stored. A counter with no record behind it is `unknown`, not 0. Claude's total is derived as the sum of its four counters (`claudeReportedTokens`); a source whose input contains the cached part gets none. Every `usage` reply, errors included, answers the rid, and the op is for the console and a configured planner only, never during that planner's private turn. A rebuild from byte 0 must equal the incremental result (`test/usage-rollup.test.ts`). Never present unknown as 0 in a sum: an overall figure with an unknown peer is `partial` and names the peers (`total().unknownPeers`). Never drop rows silently: `limits()` reports `truncated`, `omitted` and the bound. An event-open pause the hub no longer holds is `endedUnknown` (`ahub reset` keeps the log), so `limits(peer, { live })` needs the daemon's held pauses. A poll's fold transaction is IMMEDIATE and rechecks the stored offset, so two roll-ups on one `hub.db` never fold a range twice.
30
32
  - Daily usage (#683 U4): `UsageRollup.daily(days, now)` returns the last `days` UTC calendar days (at most 366) ending today, clipped to the first day with a record. A day inside the window with no record is a row with `empty: true`, never zeros. Per peer and day, take the native total when reported, else the counted tokens (`count`), else `unknown`; a day's total is `partial` when some peers are unknown and `unknown` when all are, never a sum that treats them as 0.
31
- - Kimi's consumption (#285 phase 2): `src/adapters/kimi-wire-usage.ts` is the only reader of Kimi's wire log and `src/hub/kimi-usage.ts` the only owner of the Kimi count; `daemon.ts` supplies ports (events, budget, clock, session id) and binds the session at a turn start (no read) and calls `collectKimiUsage` at a turn end, a `usage_update` and an offline transition. Rows of an earlier read pass are recorded before a later pass can fail; restart seeding counts only rows recorded after the last `budget_resume` of a soft-token peer (`kimiSeedRows`). Tests never reach the person's `~/.kimi-code`: `test/hub-home.ts` sets `KIMI_CODE_HOME` to a temp dir. Never feed `budget.report(kimi, tokens)` from an ACP total or an estimate, and never change `kimiCountedTokens` without the AC5 test: cache reads are recorded, not counted. An unreadable log, an unknown row shape or an untested `protocol_version` is unknown (reading dropped via `Budget.forget`, one notice), never a partial count. A usage row's text never leaves the reader: four integers, a model label and the row time. A resume clears the window (`kimiUsage.reset()`).
33
+ - Kimi's consumption (#285 phase 2): `src/adapters/kimi-wire-usage.ts` is the only reader of Kimi's wire log and `src/hub/kimi-usage.ts` the only owner of the Kimi count; `daemon.ts` supplies ports (events, budget, clock, session id) and binds the session at a turn start (no read) and calls `collectKimiUsage` at a turn end, a `usage_update` and an offline transition. Rows of an earlier read pass are recorded before a later pass can fail; restart seeding counts only rows recorded after the last `budget_resume` of a soft-token peer (`kimiSeedRows`). Tests never reach the person's `~/.kimi-code`: `test/hub-home.ts` sets `KIMI_CODE_HOME` to a temp dir. Never feed `budget.report(kimi, tokens)` from an ACP total or an estimate, and never change `kimiCountedTokens` without the AC5 test: cache reads are recorded, not counted. An unreadable log, an unknown row shape or an untested `protocol_version` is unknown (reading dropped via `Budget.forget(peer, "tokens", "count")`, so only the count's own reading goes; one notice), never a partial count. A usage row's text never leaves the reader: four integers, a model label and the row time. A resume clears the window (`kimiUsage.reset()`).
@@ -8,7 +8,7 @@
8
8
 
9
9
  - 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.
10
10
 
11
- 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`, `src/hub/native-hook-launch.ts`, `src/cli/output.ts`, `src/ui/index.html`, `src/hub/task-code-group.ts` (dashboard Code group) or the instance modules (`src/hub/instances.ts`, `src/hub/instance-config.ts`, `src/hub/instance-execution.ts`), or before adding a native peer.
11
+ 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`), `src/hub/native-hook-launch.ts`, `src/cli/output.ts`, `src/ui/index.html`, `src/hub/task-code-group.ts` (dashboard Code group) or the instance modules (`src/hub/instances.ts`, `src/hub/instance-config.ts`, `src/hub/instance-execution.ts`), or before adding a native peer.
12
12
 
13
13
  - The plugin bundle is installed apart from the daemon. Breaking a control WS message contract bumps `PROTOCOL` in `control-client.ts`; a new request answered as unknown or an optional field preserves it, as does additive optional public task metadata in `ui_snapshot` that older clients ignore (such as `deps` in #246).
14
14
  - `peer_stop` is console-only and stops only a hub-owned headless ACP, Pi, local, print-reviewer or Claude implementer runtime through its adapter. Refuse native-terminal peers and recovery-held mutations; serialize with starts and permission changes, and refuse a recovery `prepare` while a stop runs. An admitted stop holds delivery through `BusOptions.deliveryHeld` before any teardown await (never a console pause that resume can undo, or a global recovery hold that blocks unrelated peers) and withdraws the peer's pending approvals before the adapter stops. A requested stop holds an in-flight delivery as `needs_review` with an ids-only stop reason, suppresses restart/escalation, and never resolves that delivery automatically.
@@ -24,8 +24,9 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
24
24
  - Settings (#269): the dashboard's `setting`, `setting_undo`, `setting_preview` and `setting_reload` actions and the console-role `settings_get|set|undo|preview|reload` requests go through `settingsSet`, `settingsUndo`, `settingsPreview` (`settingsUndoPreview` for `undo: true`) and `reloadConfig` (`src/hub/daemon.ts`) and the registry in `src/hub/settings.ts`. A new editable setting is a registry entry, never a new write path; a key that chooses what the hub runs or where secrets or task text go joins `NEVER_EDITABLE` instead. Writes go only to `.agenthub/config.local.json` and `.agenthub/routing.local.toml` through `store()`: the loader's own validation first, temp file and rename, never a file git tracks. Authority comes from the dashboard session (`DashboardSession`, `src/hub/ui.ts`) or the console role, never from the request body; `ui_action` from the manager is an ordinary session. Never return a setting's value in an event or notice except through `recordSetting`.
25
25
  - Structural config edits (#689): `src/hub/config-tier.ts` is the one tier rule (editor, dashboard refusal and `structuralLeaf` in `authorizeConfigLeaf`), `src/hub/config-edit.ts` plans and writes (loaders first, revision checked twice, bounded backups, atomic), `src/hub/config-edit-host.ts` owns authority, the typed tier 3 confirmation, the `config_edit` event and the scoped publish. The daemon only supplies ports (`createConfigEditor` in `daemon.ts`); never validate, write or classify in `daemon.ts`. A new editable structural key is an entry in `config-tier.ts` and nothing else; a key that chooses what the hub runs or where secrets go is tier 3 and is never offered to the dashboard (the hub refuses it, the page also hides it). The audit event and every notice carry key paths and value hashes, never a value. `structural` reaches `authorizeConfigLeaf` only through `structuralScopes` for the one scoped reload. `sdlc.toml` is read once at start, so it joins `pendingRestart` through `allPendingRestart()`, never through `reloadConfig`. A new ahub settings subcommand that writes is classified `console` by default; keep it that way.
26
26
  - Live reload (#439): `reloadConfig` is the only publication path. Settings writes and undo, `ahub settings reload`, the dashboard reload and the debounced watcher of the four `CONFIG_SOURCE_FILES` (`src/hub/config-reload.ts`) all reach it; `prepareConfiguration` validates the complete candidate before anything is published, and recovery or shutdown holds it. A write or undo passes its own key as `scope` and publishes only that path, and `acknowledgeConfiguration` records only the accepted paths, so other edits stay pending. Every changed key, inside the registry or not, is decided by `authorizeConfigLeaf` (`src/hub/config-apply.ts`): a refused key keeps its accepted value and is reported by name while the others apply, `roster.*` applies only through its own scoped roster write, and a change of the settled planner or conductor holders (`roleHolders` over `resolveSettledRoles`) is guarded even when a role list shrinks. Outside a named write, only the console's `settings_reload` (authority `terminal`) adopts guarded file edits, by sending back the `confirmation` token bound to the candidate `fingerprint` and `configRevision`; changed bytes or accepted state refuse it, and a watcher, dashboard or settings session never confirms. An undo that names `key` and `undoDigest` writes only while `pendingUndoIdentity` still matches both. Keep config-source snapshots plain and cloneable; classify dynamic ids with validated running/source identity scopes, never suffix discovery or a builtin fallback. Compare restart-bound declarations with the running config on every reload, never a pending desired value accepted by an earlier reload; derived defaults stay bound to that running declaration. A `restart` key (`configApply`) keeps its running value and is listed in `pendingRestart`; reload notices and events carry key names and reasons, never values. The console settings editor (the Settings panel, or the Peers panel's `w` WIP shortcut, which opens only an existing `tasks.wip_limits.<peer>` row for a rostered peer listed in status `taskWip`) stays bound to the panel that opened it (`SettingsEdit.panel`) and arms a write only from a preview reply that matches that panel and its open edit's token, key and value; pasted text never confirms. Console Tasks backlog actions (`p`, `a`, `e`, `R`, `c`) only prefill person CLI commands (`backlog pull-ready|plan|return`, `task cancel`) with the row's immutable id and its planning or flow revision; `validateLockedRow` rechecks the row's state and revision at Enter and again at `y`, and no console path writes the board directly (#532).
27
+ - Planner setting (#755): `planner` is one id or a list of one to eight distinct eligible ids, validated only by the `planner` row of `projectSettings` (eligible declared instances join its `values`) and `checkSettingValue`, in `loadConductorPolicy` and `validateRawConfig` alike. Every `resolveSettledRoles` call (`settlePlanner`, `roleHolders`) passes the same `plannerEligible` predicate built from that row, so reload guards compare holders the way the running map settles them.
27
28
  - Retained configuration (#544): a key the reload refused, or a file edit outside a scoped write that still differs from the accepted value, is listed in `retainedConfig` (key and sanitized reason only, never a value) in status, `status.json`, the settings view and the console snapshot until a reload accepts or reverts it (`retainedConfigAfterReload`). A config leaf that selects a peer's native launch belongs in that definition's `cli.launchConfigKeys`: the `start` and `peer_start` replies then carry `retainedLaunchConfig` (the running values), the launcher starts with them and prints `retainedLaunchWarning` before its start request; when a retained value cannot launch (Kimi TUI `kimi_cmd` with no TUI form), the error names the running hub as its source and never echoes the words (#654).
28
- - `action_policy` (#636) is machine-local only: `stripUntrusted` drops it from any file but `config.local.json`, and `authorizeConfigLeaf` refuses an `action_policy` leaf whose `sourceFile` is not an untracked `config.local.json` before any other check. It applies live (`actionBudgets.policyChanged`); Claude's PreToolUse hook and hub-owned ACP peers enforce it (#312); Codex, Pi and the local worker do not yet, so keep `actionEnforcement` (`src/hub/daemon.ts`) honest: a peer is enforceable only where the hub sees the call before it runs, in the mode it runs in now, and says why otherwise.
29
+ - `action_policy` (#636) is machine-local only: `stripUntrusted` drops it from any file but `config.local.json`, and `authorizeConfigLeaf` refuses an `action_policy` leaf whose `sourceFile` is not an untracked `config.local.json` before any other check. It applies live (`actionBudgets.policyChanged`); Claude's PreToolUse hook, hub-owned ACP peers in `ask`, the local worker and Pi (`classedToolPermission`, #750) enforce it, and a headless Claude implementer only while a current child generation is busy. Codex's approval RPCs are gated but Codex stays not enforceable, because its native policy can run a command without a request. Keep `actionEnforcement` (`src/hub/daemon.ts`) honest: a peer is enforceable only where the hub sees the call before it runs, in the mode it runs in now, and every refusal names its reason (rules: `adapters.md`, Always-ask).
29
30
  - `pet` is console-local presentation (#483): validate it only through `validatePetConfig`, which both `loadConfig` and the reload schema call, and keep it applied live with no authority (`authorizeConfigLeaf`).
30
31
  - Start modes (#269): every start the hub makes itself goes through `hubStart` (`src/hub/daemon.ts`): the dashboard's `start_peer`, the console-role `peer_start`, the conductor's `start` hook and `pi.auto_start`. It takes the mode from `startMode(peer)` and nothing else; never add a caller-chosen mode, flag or command to it, and never fall back to headless when a terminal cannot be opened (refuse with the manual command). The words a terminal runs are assembled there from `process.execPath`, the CLI entry, the project root and `startWords(peer)`. The only continuity exception is a validated Pi bookmark on `pi.auto_start`: append its recorded session file (or its session id when no file was recorded), backend and model; never pass both session selectors or accept caller-written replacements. Recovery and `startPeer` callers that pass a recorded mode keep it. A launcher with no `--mode` takes the daemon's accepted start mode from `status.json` `startModes` (`readControl`) before the file's value. A TUI start of an `owned-acp` peer (Jcode) from the console's `peer_start` opens its terminal even while the ACP owner is online: that terminal is a frontend on the owner. The conductor and the dashboard answer `already` for an attached one (#559). The real terminal providers are wired only in `src/hub/lifecycle.ts` (`DaemonOptions.terminal`): a daemon built without them, as in every test, opens nothing; a test injects a recording provider and never the real one.
31
32
  - A headless start that `hubStart` made for a peer with `capabilities.watch` (Kimi, Qwen) also opens `ahub watch <peer>` through `openWatchPane` (#286 AC10): the same provider, fixed words from `process.execPath`, `CLI_ENTRY`, the project root and the peer id, one per peer per `PEER_START_RETRY_MS`. It runs only after the start succeeded and never fails it: no provider or a failed open reports `watch.command` in the result and a notice, with the provider's own text in the log only. Never open a pane for an `already` result or a declared instance.
@@ -41,7 +42,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
41
42
  - 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.
42
43
  - 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.
43
44
  - 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).
44
- - A recovery target-feature floor (`task_cancel`, `peer_instances`, `role_stages` while a declared Claude-kind instance holds `implementer`, `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) lives in `incompatibleRecoveryTarget`, which every existing check site calls, and its name in `DAEMON_FEATURES`; never check a floor at one site only.
45
+ - A recovery target-feature floor (`task_cancel`, `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) lives in `incompatibleRecoveryTarget`, which every existing check site calls, and its name in `DAEMON_FEATURES`; never check a floor at one site only.
45
46
  - 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).
46
47
  - 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.
47
48
  - Keep restoration receipts explicitly tagged as pending, waiting with origin/binding, done with binding, or failed. Project waiting origins as `waiting:pending` and `waiting:failed`; preserve the chosen binding and origin across interruption instead of inferring completion from an object.
@@ -87,6 +88,10 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
87
88
 
88
89
  - Keep full memory home, generation ID, selector revision and malformed-row totals in authenticated person status/CLI receipts, never disk status.json or agent CLI output; low-privilege receipts use a generation prefix and omit home, selector revision and malformed-row totals. Emit typed unbound disabled/unavailable receipts with no profile when no memory handle opened; profile-check may treat an authenticated same-home generation-capable unbound daemon as memory-quiesced, never a stale bound handle. Scope profile-check readiness to its named registry inventory; old, unknown or unreadable receipts refuse readiness, and other custom registries need separate checks.
89
90
 
91
+ - The memory client (#752) is the in-process amem `MemoryClient` built once in `startDaemon` from the alias-resolved `memoryProject`, the bound store and candidates, `screenMemory` and the capture binding; its `check` rechecks that binding and the project before each call. `DaemonOptions.memoryClient` is a host-only seam, never read from configuration or a tool; `memory.worker_url` is ignored legacy configuration, never a destination. Local capture takes `memoryProject`, never the project chain. Shutdown aborts `syncStop`, awaits `syncRun`, then closes `memory`, the candidates and the store, in that order.
92
+ - Symbol anchors (#769, #770): resolve `memorySymbols` only in the `prepareReviewBrief` pass that fills `reviewSymbols`, under the same key and eviction, through `resolveMemorySymbolNodes` (a positive, unambiguous `exists` at the brief's head, at most `MEMORY_SYMBOL_LIMIT` candidates within `MEMORY_SYMBOL_START_BUDGET_MS`); a filled key is reused by the completion pass, a PII task is never resolved, and one that turns PII or changes owner meanwhile resolves unknown. `memorySymbolNodes` answers only for a non-PII task whose `codeSymbols.head` is its `refs.commit`. Raw and normalized symbol forms are query compatibility only (`taskMemoryQueryNodes`, `planGuardNodes`): never rewrite a stored note or id. `ahub doctor --stale-anchors` (`memoryStaleAnchorsCommand`, `src/cli/memory.ts`) is read-only: it never creates a missing profile, machine or shard, never edits, deletes or promotes a note, and reports unavailable, private or ambiguous evidence as unknown, never stale.
93
+ - Read-time code neighbours (#771): derive them only through `memoryCode` and publish only through `currentMemoryCode` (`src/hub/daemon.ts`), which rechecks the memory binding, the overlay head against its `readMemoryCodeHead` proof and every node's canonical id, and screens each attached note again; never store derived `calls` edges or neighbours in amem. Neighbour queries run through `codeFacts.neighbours` with `priority: "optional"`, so review and completion queries win. Only `symbol:` starts query: the dashboard only through `uiMemorySnapshot` for the `memory` view with `codeNeighbours: true` and a node of at most 512 bytes (`ui_snapshot` answers a failure with `memory view unavailable`), and `hub_memory_graph` only after `memory_read`, rechecking the caller's connection generation, private turn and PII context after the await. Console task detail reads `taskMemoryResult(task, true)`, which never starts a query; its `relatedMemory` is a count or `unknown`. A PII task or turn never queries. Keep the cache ceilings (32 entries, 30 s known, 5 s unknown, 5 s between derive starts), and keep direct path/title/symbol notes in `taskMemoryResult` independent of code availability: an optional code failure never drops them.
94
+
90
95
  - Bind adapter and instance declarations once at startup through the daemon-local `adapterCatalog` (`createProjectAdapterCatalog(loadedConfig.adapters, externalDefinitions)`, the definitions `loadExternalAdapterDefinitions` imported at startup; a reload passes the same `externalDefinitions` to `loadConfig` and imports nothing), its `definitionOf` closure and the `configuredIds`/`extraIds` catalogs; never publish them to the global registry or take them from a request. `adapters` and `instances` are restart-class, and `adapters` is `NEVER_EDITABLE` and in `ALL_MACHINE_LOCAL`. Settings rows come only from `projectSettings(declarations, adapterCatalog)`; a `locked` row (a read-only instance's permission) is refused by `checkSettingValue` and `settingRefusal` for every authority, and the console and dashboard never arm an edit or undo for it.
91
96
  - Configured ACP starts (`startPeerBody`): before effects refuse native launch overrides (model, route, backend, unattended, provider profile, session file, fresh) and an invalid session selector; answer `already` only for the same requested native session and never pass `qwen_cmd`; after a failed start keep the owner with `pendingOwnedProcess` on the bus, let `peer_stop` admit that offline owner and refuse starts until it is confirmed gone, reporting only a withheld-detail reason. In `taskOp`, refuse a read-only instance's op outside `READ_ONLY_HUB_TOOLS` before effects. Publish `configuredAdapters`, `startablePeers` and `readOnlyPeers` in status as bounded ids only. Keep controlled recovery refused (`session.recoveryRefusal`); crash resume passes only a bounded, control-free recorded `sessionId`, never a recorded model.
92
97
  - `validateInstances` admits `review_mode: "fresh"` only on a headless instance whose roles are exactly `["reviewer"]` and whose kind declares a fresh reviewer; a fresh reviewer refuses `worktree` and `permission_mode` (variant selection: `adapters.md`). `review.pool` and `review.rerotate` are validated by `reviewerPoolConfig` at startup and in the config schema: 1 to 16 distinct kinds from `REVIEW_POOL_KINDS`.
@@ -104,3 +109,4 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
104
109
  - Share one monotonic 250 ms snapshot Git budget until the next event-loop I/O turn. Repository/HEAD probes, capture and end diffs consume the same remaining deadline; exhaustion or unreadable diff evidence records unknown trees and no attribution/undo, with one diagnostic per burst. Never substitute a stale baseline or apply a late result (#576). `DaemonOptions.snapshotClock` is host-only (tests hold it, rule: `tests.md`): never read it from configuration or a tool, production leaves it unset (`performance.now`), and the share stays 250 ms with `snapshotBurstMs` still clamped to it. Every deadline comparison goes through `budgetNow(budget)`, never `performance.now()` against `budget.deadline`.
105
110
 
106
111
  - Channel tool-result hints (#493, #221): `withPendingHint` (`src/adapters/claude-channel.ts`) appends fresh `delivery_holds` counts, and drops the "call hub_inbox" advice whenever the same reply carries a held-delivery hint, because `bus.pull` refuses under that hold and the advice would send the session into a refused read. A `hub_inbox` result that already says how many messages remain unread adds only the important count, never a second queued count. Do not add a Claude client-version gate on steer without a probe of the older client and an optional hello field that carries the version (background: README).
112
+ - Memory sync (#438): stop an in-flight `syncMemory` only through `syncStop` (`src/hub/daemon.ts`), and await `syncRun` before the memory store closes, as `stopOnce` does. agent-mem runs the sync's git in its own process group, so a process that ends without `daemon.stop()` leaves that git and its transport child holding the memory repository. A signal the hub exits on goes through lifecycle stop (`STOP_SIGNALS` in `src/hub/lifecycle.ts`: SIGINT, SIGTERM, SIGHUP, SIGQUIT), never the default action; SIGQUIT in particular must not reach its default core dump (Bun honours it: agent-mem #435 measured a 5.9 GB core file and a delayed exit). The forced exit after the shutdown deadline stays `process.exit`.
@@ -15,8 +15,8 @@ Scope: the hub-native local worker, its tools, path guard, denylist and seatbelt
15
15
  - After a tool with side effects ran, a failed turn is reported, never redelivered.
16
16
  - `ctx.permit` answers a `ToolApproval` (`true`, `false`, `"expired"`, `"aborted"`, #253): only `true` permits an effect, so never test it for truthiness. A tool returns `approvalError`'s text for the rest: a denial, the expiry (which tells the model not to retry) or the withdrawal. A turn that two unanswered approvals ended (`ApprovalWaitStop`; counting rule: `adapters.md`) completes its missing tool results, settles its delivery `completed` with the reason and is never handed back for replay, also when no tool had a side effect.
17
17
  - `Facts` (`src/hub/facts.ts`, #428) names a large file whose ctime is under `RACY_MS` by its git blob id: in `load()` and `observe()` only through `racyBlob()` (per-file `RACY_HASH_MAX_BYTES`, per-store `RACY_BURST_BYTES` per event-loop turn, memo cleared at `boundary()`), past which it keeps the stat-only stamp and makes no claim; `atHead()` keeps its own `HASHED_BYTES` bound, `tree()` reads large files by stat only, and `largeBlob()` caches only settled stamps.
18
- - Hub tool calls (`hub_send`, `TASK_TOOL_NAMES`, `CONDUCTOR_TOOL_NAMES`) are never passed to `capture.observe`; `Tasks` alone decides what hub tool text reaches memory (#230).
19
- - A tool that asks for approval passes its name to `ctx.permit`, and `write`/`edit` also the `guardPath` result, never the model's path string: also pass the original path separately through every permit wrapper; `ask-when-needed` grants only when both the original project-relative spelling and the real path avoid protected names through `grantablePath` (`src/hub/permission-mode.ts`). `bash` and mutating `git` pass no path. After the awaited approval `write`/`edit` run `guardPath` again and refuse a target that changed (#242).
18
+ - Hub tool calls (`hub_send`, `TASK_TOOL_NAMES`, `CONDUCTOR_TOOL_NAMES`) are never passed to `capture.observe`; `Tasks` alone decides what hub tool text reaches memory (#230). Every live `capture.observe` passes a host `sourceId` (`<session>.<turn>.<step>.<n>`, #752); a new host caller supplies one too. Capture stores `paths` only for `read`/`write`/`edit` and only project-relative ones: command argv is never path metadata.
19
+ - A tool that asks for approval passes its name to `ctx.permit`, and `write`/`edit` also the `guardPath` result, never the model's path string: also pass the original path separately through every permit wrapper, and every tool its parsed arguments as `input` (the always-ask classifier reads it, #750; rule: `adapters.md`); `ask-when-needed` grants only when both the original project-relative spelling and the real path avoid protected names through `grantablePath` (`src/hub/permission-mode.ts`). `bash` and mutating `git` pass no path. After the awaited approval `write`/`edit` run `guardPath` again and refuse a target that changed (#242).
20
20
 
21
21
  - Honor the live `protect_instruction_files` policy in scoped grants for the three instruction names; keep reads, explicit approval and never-ask unchanged. Only the settings registry writes this machine-local policy (#323).
22
22
 
@@ -17,10 +17,13 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
17
17
  - Use the daemon's settled role map (`resolveSettledRoles`, `src/hub/settled-roles.ts`, the same resolver reload compares holders with) for planner authority and notice recipients. An explicit planner designation is exclusive; automatic fallback remains runtime-only. Keep ready-unassigned sweep admission consistent with person-unassignment suppression, planner-pull reservation/WIP limits and ordinary routing guards; persist each ladder step before its notice.
18
18
  - Readiness never puts a reserved owner over its planner WIP cap (#655). `offerReady` asks `reservedHold` (the same `wip()` plus `pendingClaims` against `wipLimit()` that `acceptanceCapacity` refuses at) before the `ready` write is offered; a hold records `ready` and one quiet `ready held` entry (in `QUIET_EVENTS`, never a stall, never an owner) and keeps the reservation. Offer dependents and held work through `readyOrder` (priority, rank, id), and free a slot through `releaseDependents` (approval), `releaseHeld` (cancel, untouched return) or the release timer; never offer a held task past the cap, and never let the idle sweep's auto-reassign override `reservedHold`. `offerReady` records its slot in `readyInFlight` in the same synchronous step as the check and frees it in `finally`, so an await in `assignOwner` never lets two offers share the last slot; `releaseReady` treats `isHeld` work as eligible even after a `planned` entry. Planner-pulled work and an excluded reservation are not held. The hold text carries peer ids and counts only.
19
19
  - Capture the coordination preference once per task at creation (`coordinationModes` from `TasksDeps.coordination`, in `propose` and for each split child, #439). `formCohort` joins only overlaps with the same captured mode and forms a silent cohort only for a turn-free one; a reload never silences an existing task or lifts an existing silent cohort. A daemon restart re-pins retained rows to the startup mode (a compatibility ceiling, not task metadata). `configureSweep` takes effect at the next pass: a running `sweep` keeps the config it started with.
20
+ - The planner "board changed" notice (#758, only with the lifecycle on) is built from what was added to a card's history since the last scan (`Tasks.groomNotices`, `groomScan`), never from the status revision alone: skip the peer whose own entry it was (`entry.by`), tell only the card's planners (`taskPlanner`, #525-2), send a planning revision or dependency change (a `planning` entry, or `deps` that differ from the last scan whatever wrote them: a propose with `after`, a split) and a card's creation (a peer's propose, a split child, a generated card) at task weight, since new work and its dependencies are what a plan review consolidates, and replace an unsettled copy (`sendGroomNotice` withdraws it by the `The board changed` prefix) so a planner meets the latest revision once, and send state moves, reviews and assignments at `fyi` weight (recorded, no turn), at most once per planner per `GROOM_FYI_WINDOW_MS`. The hub's own bookkeeping entries (`by` is `hub` with no `planning`: lifecycle links, idle sweeps, holds, plan observations) tell nobody and spend no window; a deps change with no new entry has no known author, so no planner is skipped for it. Scan every configured planner, not only the attached ones, so a change made while one was offline is kept for it. The first notice a planner gets after a hub start is task weight. Keep the scan out of the status revision and never read history that is not new.
21
+
20
22
  - Completed-change notices (#713, one per recipient per done, accepted work only, superseding by `completedFor`): rule in `bus.md`.
21
23
  - Plan-symbol overlap (#682 1b) compares through `symbolsOverlap` (`src/hub/symbol-normalizer.ts`): arguments stripped, `section `/`source ` keys exact, two qualified names equal, otherwise last segments equal. `places`, `overlapHits`, `contradict`, the completed-notice replay, `plannerGrooming` and `planSymbolMatches` all use it; never compare plan symbols with `includes` or set membership.
22
24
  - Task code symbols (#684 2a) are host-derived: stored only in the `task_code_symbols` table and read through `TASK_SELECT` (`src/hub/board.ts`), so a `SELECT * FROM tasks` reader never exports them. `Board.update` writes `codeSymbols` only with `HOST_CODE_SYMBOLS` and drops it when the head (`refs.commit`, else `sdlc.head`, else `sdlc.localHead`), `refs.repo`, `refs.paths` or `sdlc.repo` changes or the task turns private. Tasks observes them only inside an admitted `done (checking)` or `done` write (`observedCodeSymbols`: non-PII, a full commit, the project repository); a failed or mismatched observation is unknown, never a refusal. Optional code facts (`prepareReviewBrief`) skip PII tasks, never refuse a review, and the actor guard runs again after them.
23
25
  - Public reads of code symbols go through `publicTaskCodeSymbols` (re-screened, no `repositoryKey`). `taskCodeSymbolsDisjoint` only labels a completed-change notice: never suppress a notice or gate on it. `symbolOverlaps` is console detail only: it persists no pair and drops any task marked `pii` or `pii_path`, even with a routing opt-out.
26
+ - Completion-note symbol anchors (#684 phase 3, `completionMemorySymbols`): anchor a done note only on the current head's screened host observations (`publicTaskCodeSymbols`, this project's repository) that `TasksDeps.memorySymbolNodes` positively confirms; never on plan symbols, and nothing for a PII or foreign task. An unknown observation writes no anchor and the note labels its coverage unknown; earlier notes stay immutable, and missing evidence never refuses done. Read-time code advice (graph neighbours, briefs) asks `memoryCodeEligible` (non-PII, this project's repository, no `refs`/`plan` repository mismatch).
24
27
  - `.agenthub/routing.local.toml` (#269) may hold only `stay_switch` and per-class `peers`, `escalate_to`, `route` and `pi_backend` (`checkOverlay` in `src/hub/routing.ts`): never widen it to targets, routes, signals or constraints, which stay in the hand-written `routing.toml`. `parseRouting` is the one merge, and `assign()` reads `routing.sources` only for its trace line.
25
28
  - Conductor authority requires the explicit unique role before capability checks. The only exceptions run before that check in `Conductor.execute` and stay task-scoped: a task's current owner and reviewer read it with `hub_task_show` (#208; a reviewer-backend peer only during its bound review, rule below), and its proposer redirects it with `hub_task_assign` while nobody ever accepted it and no person moved it (#207). Removing them strands reviewers that missed a review request. Assignments preserve the acting peer. A peer releases only its own conductor hold; operator release and manual, quota and recovery holds remain separate paths.
26
29
  - The PII screen (#198) runs in `propose` before anything else reads the task (no board row, envelope, triage or brief before its verdict), and `unknown` is PII. A call that screens free text (`done`, `review`, `remember`) screens only after its own checks pass, and `done` and `review` run them again after the awaited screen, because the task can change meanwhile. A history entry marked `withheld` stays withheld in public views, completed notices and review requests, whatever `nameable()` says; `ahub ask` shows it only on campus, like a note that matches a PII pattern.
@@ -78,6 +81,15 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
78
81
  - `ahub report --dogfood` (#687, `src/hub/dogfood.ts`, `src/hub/dogfood-read.ts`) opens `hub.db` read-only with byte and row caps; an unavailable or incomplete source reports `null`, never 0, and per-PR GitHub reads run only when the task privacy inventory is complete.
79
82
  - Qualified titles (#723): `planIssueIdentity` links a sibling `repo#N` only when `refs.repo` names that repository (`siblingTitleRefs`, `spec-read.ts`), and an `owner/repo#N` only when it is `refs.repo`, else this project's repository; a sibling shorthand beside a bare `#N` is two identities and links neither. A title that names another repository with no `refs.repo` does not link, and `refsRepoHint` tells the proposer to set it (advice in the `hub_task_propose` result, never a refusal; none in mode off, for a PII task or once `refs.repo` or an issue is set). There is no retitle path on the board: a title is fixed at propose.
80
83
 
81
- - Planner walk and reviewer independence (#525-3): `plannerWalk` and `implementers` in `src/hub/routing.ts` are pure; the planner is chosen only inside `assign()` (`planners`, `namedPlanner`, `keptPlanner` in, `Assignment.planner`/`plannerReason` out), and `Tasks` never picks one outside it. `Tasks.weights()` is the one place `planners` is built (rostered holders of `d.roles`, sorted), so every `assign()` path and `explain` agree. Rotation is `task.id mod n` over every holder and is never renumbered by availability; a named planner is exact and never replaced. I-2 (`implemented this task`) lives in `blocked()` for the reviewer role as its LAST check, so the legacy, named and pool paths share it and it only ever removes an otherwise eligible candidate; when I-2 removed every otherwise eligible reviewer, the person reviews with `reviewBlocked`, never an implementer. 525-3 records nothing: do not read `Assignment.planner` into a board write, notice or authority check until 525-2 does it with its own tests.
84
+ - Planner walk and reviewer independence (#525-3): `plannerWalk` and `implementers` in `src/hub/routing.ts` are pure; the planner is chosen only inside `assign()` (`planners`, `namedPlanner`, `keptPlanner` in, `Assignment.planner`/`plannerReason` out), and `Tasks` never picks one outside it. `Tasks.weights()` is the one place `planners` is built (rostered holders of `d.roles`, sorted), so every `assign()` path and `explain` agree. Pass a bound task as `keptPlanner` to that same walk: its explanation names the binding as `kept`; drafts and legacy null bindings still rotate, and explanation never writes a binding or history. Rotation is `task.id mod n` over every holder and is never renumbered by availability; a named planner is exact and never replaced. I-2 (`implemented this task`) lives in `blocked()` for the reviewer role as its LAST check, so the legacy, named and pool paths share it and it only ever removes an otherwise eligible candidate; when I-2 removed every otherwise eligible reviewer, the person reviews with `reviewBlocked`, never an implementer.
82
85
  - Owned implementer child (#747): `propose`, `acceptReviewed`, `done` (through `complete()`) and `remember` take `requestCheck(by)` (`TasksDeps.requestAuthority`) at entry and call it after every await, before the next write; a new await on those paths adds a call, and its refusal stays pre-effect. The child's tool surface is the fixed `IMPLEMENTER_HUB_TOOL_NAMES` (`src/hub/hub-tools.ts`); never widen it from project roles.
83
86
  - Deliverables (#509a, `src/hub/deliverables.ts`, `src/hub/deliverable-files.ts`): the hub hashes each file itself under the owner's verified execution root (`TasksDeps.executionRoot`, the daemon's `ownerWriteRoot`; an unproven root refuses); never accept a caller's hash, size or unknown field. Vet and hash before any board write and refuse pre-effect; a `done (checking)` carries its records to `complete()`, never a later read. An approval rereads the delivered files: a reviewer's verdict is refused before any write unless every file is `unchanged`, and only the person approves moved files, which are rehashed and named in the note. A file the hub cannot read or vouch for is `unknown`, never `unchanged`. Peer-facing records go through `publicDeliverables`: for a PII task or a `pii.paths` file, id, hash and size only.
87
+
88
+ - Task/release planners (#525-2): keep nullable `planner` columns additive, with no backfill. Bind new proposals and null-planner pulls only from `assign()`; install the fixed synchronous `Board.proposalPlanner` host port so proposal, split-child and generated-card bindings enter the original insert transaction before notification. Preserve explicit null callers and existing rows. Preserve existing bindings on owner moves. `Tasks.taskPlanner` keeps a bound rostered settled holder even while offline/paused; a missing, removed or off-roster holder falls back to legacy holders. Every task/release planner effect and its post-await validation uses that fresh scope; project-wide reads remain holder-wide. Naming a planner is person-only, refused for closed or PII work, admits only the holder `assign()` returns for `namedPlanner`, and records only `planner changed`, never an owner delivery event. Split children intentionally run their own pure rotation and may bind a different planner from their parent; do not inherit the parent binding. A removed/off-roster binding is preserved as legacy metadata on pull without recording a false `kept` reason.
89
+ - Plan-review independence (#525 I-4): `recordPlanReview` admits any settled planner holder or the person, independently of the task binding, and rechecks author/eligibility after every await. Derive the author from the latest authoring `planRevision`, then legacy planning `plan`; exclude `plan reviewed` scope-bookkeeping entries from both. Use the initial author only for nonempty intent list fields, never review/CI scope alone; unknown is null. Preserve later actual intent-edit authorship. Refuse agent self-review while another rostered non-offline holder exists; otherwise record `selfReview: true`. Keep person overrides explicit in that marker. Independent requests use the same pure assignment walk excluding the author, ids only through public view, and a history-flow fence rather than timestamps. This exception grants no planning or lifecycle evidence powers on another holder's task.
90
+
91
+ - Planner lifecycle stage identity (#525-2): allow the effective task planner only `stage`/`repo`/`issue` for an explicit stage whose actor is `planner` or `owner/planner`. Preserve existing enforce identity-relink and stage/spec gates, and advise relink behavior. Keep reviewer evidence, checklists, heads and deliverables restricted to their existing owner/reviewer/person authorities.
92
+ - Deliverables list and badge (#509b, #317): `Tasks.deliverableList` is the one reader for `ahub deliverables` (the `deliverables` task op, console only) and `Tasks.deliverableBadge` is the snapshot's per-card field. Both go through `publicDeliverables` and `publicTitle`; neither the snapshot nor a list without `check` may open a file (the page polls every second). A `changed` claim comes only from `deliverableStatus`, cached by record ids and hashes in `deliverableSeen`: never derive it from the board alone. The cache keeps the whole-task `result` (`deliverableResult`: changed, missing, unknown or unchanged), never a boolean: `unchanged` only when every file measured unchanged, so a missing or unreadable file is never shown as unchanged.
93
+
94
+ - Backlog issue planning (#759): allow only `fields.sdlc.repo`/`issue` on unstarted ownerless backlog through `Tasks.planBacklog`, scoped to the effective planner/person; a PII card's identity is person-only and a public one must pass `publicMetadataText`, both pre-effect. Keep arbitrary lifecycle receipts refused on backlog. Reuse lifecycle identity invalidation, keep the planning write atomic and recheck after awaits. Store identity-only undo values; never revive old spec acceptance. Omitted/empty planning fields with a bounded reason record a note-only revision and retain normal pins/frozen/stale guards.
95
+ - Grooming duplicate candidates (#759): suppress only pairs related by directed dependency reachability, either direction, using bounded public ID/dependency context. Include approved intermediate phases when available; a walk that hits its bound marks the digest truncated. Keep overlap findings, unrelated same-issue duplicates and privacy filtering; never infer follow-ups from prose or supersession text.
@@ -23,6 +23,8 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
23
23
  - The whole-suite watchdog is `AHUB_CHECK_HANG_S` (default in `scripts/check.sh`, hosted values for both OSes in `.github/workflows/check.yml`; #378, #593): raise it only when the suite was still progressing at the cutoff, never for a hung test, and keep the per-test 20 s timeout. The check job's `timeout-minutes` must leave room above that value for setup: raise them together.
24
24
  - A seed's named test must run on Linux, where the seeded job runs: a darwin-only test reports no result there and fails the job (#382). Cover a macOS-only protection with its regression test instead.
25
25
 
26
+ - The `closing-refs` workflow (`.github/workflows/closing-refs.yml`, #760) runs `scripts/check-closing-refs.mjs` (PR number from argv or `PR_NUMBER`) on `opened`, `edited`, `reopened` and `synchronize`: it reads `gh pr view --json closingIssuesReferences,body,title,commits` and fails when GitHub would close an issue the title and body only mention with `Refs`, a negation or a qualifier (`Refs #15. Does not close #15.` and `Refs #15. Partially fixes #15.` fail, `Refs #15.` and `Fixes #15` pass), or when a commit message has a closing keyword, negated or not, for an issue the title and body do not mean (a squash merge carries it). An issue linked only in the sidebar passes. A keyword is meant only when nothing earlier in its sentence (ending at `.`, `!`, `?`, `;` or a line break, never a comma) matches `NEGATION`; widen that list, never narrow it to let a qualified close pass (background: README). `mentioned`, `keywordReferences`, `intentionalCloses`, `closingMisuse` and `commitMisuse` are pure and covered by `test/closing-refs.test.ts` with the real `gh` shape (`repository.name`, `repository.owner.login`); its command test puts a fake `gh` first on `PATH`, never the real one. Run it by hand with `GITHUB_REPOSITORY=<owner/repo> node scripts/check-closing-refs.mjs <number>`.
27
+
26
28
  - A test of `runCheck` (`src/hub/checks.ts`, #558) injects a `CheckSpawn` (argv and options in, `{ pid, exited, signalCode, stdout, stderr }` out) and never patches `node:child_process`: the runner reads a child's exit and pipes as values through `Bun.spawn`, because node's event layer can report a quick child's exit inside `spawn()` before a listener exists under load on macOS (oven-sh/bun#45483). Keep the `ended` fixtures for the already-ended case, and do not raise the 5 s limit or the 1 s kill grace to fix a flake.
27
29
 
28
30
  - 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.
@@ -32,3 +34,4 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
32
34
  - Keep the CI admission lock around the whole controller workflow, not just its decision job, in its per-PR (`check-pr-<number>`, `queue: single`: a newer pending run replaces the older) or main (`check-main`, `queue: max`) group, never with `cancel-in-progress: true` beside `queue: max` (GitHub refuses it) (#609); two groups admitting one tree at once is an accepted duplicate run, never a reason to fail closed on another tree. Preserve successful identical-tree full proof and required job names; a skipped job, pending controller or unavailable active inventory is never a successful full gate. In `readFullGateAdmission`, lost successful proof (404, malformed proof, a per-read timeout) only means a fresh gate runs; unknown active coverage and the global read ceilings (`GateReadLimitError`) stay unknown and refuse. CI checks out the PR head (`pull_request.head.sha`), not a merge with main: after a fix lands on main, merge main into the branch before reading CI as proof. CI reuse tests use fixed metadata fixtures and never dispatch real duplicate workflows.
33
35
 
34
36
  - Headless-reviewer tests run `test/fakes/fake-claude-print.ts` and headless Claude implementer tests run `test/fakes/fake-claude-implementer.ts` through `command` or a PATH stand-in (the implementer with `HOME` and `CLAUDE_CONFIG_DIR` in a temp dir), and fresh Codex reviewer tests run `test/fakes/fake-codex-reviewer.ts` through `codex_bin`; a test that reaches the daemon's reviewer or implementer start must never run the real `claude` or `codex`. The fake's sandbox markers test host admission only, never native isolation. Fresh Kimi reviewer tests set and restore `KIMI_CODE_HOME` to a fixture native home (the backend links that home's `credentials/`, `oauth/` and `device_id`, else the person's `~/.kimi-code`) and run `test/fakes/fake-kimi-reviewer.ts` through `command` or a PATH stand-in, never the real `kimi`. agy tests put a fake `agy` first on PATH and set `HOME` to a temp dir before any transcript readback (`readAgyTranscript` reads `$HOME/.gemini`). Code-facts tests build `new CodeFacts({ env })` with a PATH whose first `ripwire` is a wrapper around `test/fakes/fake-ripwire.ts` (`FAKE_RIPWIRE_MODE` selects its failures), or inject Tasks' `prepareReviewBrief`; never rely on an installed ripwire, which CI does not have.
37
+ - Production `MemoryClient` (`src/memory/client.ts`) is the in-process amem store and never opens `memory.worker_url` (#752). A test that needs the legacy worker shape injects `FakeMemoryClient` (`test/fakes/mem-worker.ts`) as `DaemonOptions.memoryClient` or in place of the client; never add an HTTP path to the production client for a test. An amem-backed test sets its own temp `AMEM_HOME` and reads only its own fixture memory directory.
package/docs/dogfood.md CHANGED
@@ -14,11 +14,17 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
14
14
  | Compact board (#724) | on | `ahub board` at 80, 120 and 200 columns: one line per task (test/board-compact.test.ts) |
15
15
  | Backlog list table (#399) | on | `ahub backlog list` at a terminal width: one line per card by rank with the reserved owner and dependencies, and the count by owner (test/backlog-list-output.test.ts); `--json` for scripts |
16
16
  | 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 |
17
- | Planner walk and reviewer independence (#525-3) | on after the release: `ahub route explain <id>` prints the planner walk here, and with `claude` the only planner holder it names `claude` for every task (rotation among one holder); the rotation among several holders and a named planner are not exercised here, and nothing records the planner yet (525-2). Reviewer independence (I-2) is on: a peer that accepted or finished a task never reviews it | the `planner: claude (rotation)` line in `ahub route explain` for a task; I-2 shown as `reviewer user` with the trace line `every eligible reviewer implemented this task: the person reviews` on a task `claude` implemented (such as #399), and as `skipped, implemented this task` on the other candidates; a former implementer that is offline or paused is refused for that reason and leaves the outcome unchanged (the trace still ends `reviewer: none, done will approve directly` where no other reviewer exists) |
17
+ | Planner walk and reviewer independence (#525-3) | on after the release: `ahub route explain <id>` prints the planner walk here, and with `claude` the only planner holder it names `claude` for every task (rotation among one holder); the rotation among several holders and a named planner are not exercised here, task bindings are tracked separately below (525-2). Reviewer independence (I-2) is on: a peer that accepted or finished a task never reviews it | the `planner: claude (rotation)` line in `ahub route explain` for a task; I-2 shown as `reviewer user` with the trace line `every eligible reviewer implemented this task: the person reviews` on a task `claude` implemented (such as #399), and as `skipped, implemented this task` on the other candidates; a former implementer that is offline or paused is refused for that reason and leaves the outcome unchanged (the trace still ends `reviewer: none, done will approve directly` where no other reviewer exists) |
18
+ | Per-task and release planners, I-4 (#525-2) | off until this change is released and the running hub is upgraded; this PR enables no new project planner configuration | task/release planner bindings and selection reasons; wrong-planner pre-effect refusals; independent plan-review requests and `selfReview` receipts; queued notices rerouted after a planner change; `task_planners` recovery refusals |
18
19
  | Acceptance criteria ids in review (#507b) | on after the release: a task with a recorded `criteria` checklist lists its ids in the review request and `hub_review` unmet items that start with an id are recorded as `unmetCriteria`; tasks here rarely record a checklist, so most reviews are unchanged | the `Acceptance criteria` block in a review request and `unmetCriteria` in `ahub task show` history for tasks with a checklist; the share of changes-requested verdicts whose unmet items name an id; unmet items that name an id the checklist does not hold (a typo) |
19
20
  | Deliverable records (#509a) | on after the release: `deliverables` on `hub_task_done` and `hub_sdlc_record`, hashed by the hub in this repository's folder; most tasks here are code with a PR and name none, so only document-shaped work (a report, a spec draft) uses it | the `Deliverables` block in a review request and `deliverables` in `ahub task show`; approvals refused with `deliverable changed since it was delivered`; records with `state: unknown` (a file the hub could not vouch for); claimed-hash refusals as a measure of agents trying to supply a hash |
21
+ | `ahub deliverables` and the dashboard badge (#509b) | on after the release: `ahub deliverables [--check]` on this repository's board; only tasks that named deliverables appear, so it lists document-shaped work only | the person runs `ahub deliverables --check` after a document task is approved and the next time its file is edited: a `changed since review` row names the edit; `Deliverables n` chips on cards; `not measured now` in the sheet means nobody ran `--check` since the daemon started |
20
22
  | Roles per kind and form (#525-1, #525-3, #525-4, #525-5) | on: claude holds the implementer role in the project checkout; isolated terminal and headless Claude implementer support is on, but neither form is declared here | tasks by owner; verified owner isolation and execution-root refusals; headless task-session resumes, inbox task-session refusals, fresh notices and person approval outcomes; missing or mismatched root must never admit a write owner |
23
+ | Read-time memory code neighbours (#684 phase 4) | off until released; dashboard toggle is opt-in | derived call edges with current SHA; attached fail/decision brief coverage; explicit unknown/truncated results; unchanged authoritative memory log bytes |
24
+ | Canonical memory symbol anchors (#684 phase 3) | off until released; optional `doctor --stale-anchors` report only | actual-symbol completion anchors versus plan guesses; symbol-related earlier notes at memory hop two; stale/unknown/truncated report counts; zero note mutations during inspection |
25
+ | Symbol preparation and historical recall (#684 phase 3 follow-ups) | off until release | exists calls per no-reviewer completion; raw-signature fail recall; indexed-definition/unknown labels; unchanged note/candidate bytes and neutral cleanup failures |
21
26
  | Overlap notices | on; plan symbols match by normalized name (arguments stripped, a bare name matches the last segment, two qualified names must be equal; #682 phase 1b) | `ahub report`: overlap warnings, task pairs; false-overlap rate, and symbol-only matches on bare common names (#682) |
27
+ | Backlog phase grooming and issue planning (#759) | off until released and the running hub supports the new planning fields; relinks for cards #138/#287 remain a post-landing planner action | related-phase duplicate count; unrelated same-issue duplicate controls; planning revisions with `fields: ["sdlc"]` or `[]`; old spec acceptance absent after relink/undo |
22
28
  | Planner WIP caps and backlog pull (#655, #718) | on (`tasks.wip_limits.codex = 2`); reserved-pull handoff on after release | board history/offer readback: reserved `backlog pulled` -> `assigned` -> `accepted` for the same peer; stale or unowned offers, capacity refusals and held-ready entries |
23
29
  | Task sweep (stalls, idle owners) | on | stall and sweep events per week |
24
30
  | Turn-free coordination | off: experimental; needs a separate measured trial | coordination events, conflict notices |
@@ -39,6 +45,7 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
39
45
  | Spec acceptance (#481 S1) | advise | build entries without acceptance |
40
46
  | Lifecycle identity linked by the hub (#694, #723) | on in advise; links at propose and accept from the title and `refs.repo`, with a groom backfill as the safety net | open tasks still showing `issue identity missing` right after propose (should be only titles that name no issue or a qualified title without `refs.repo`) |
41
47
  | Planner plan review (#481 S9) | advise | plans reviewed before build |
48
+ | 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 |
42
49
  | Plan conformance (#481 S2) | advise | PRs with files outside the plan |
43
50
  | Test integrity for bug fixes (#481 S4) | advise | bug PRs without a red-first receipt, modified pre-existing tests |
44
51
  | Required checks | advise (`check (ubuntu-latest)`, `check (macos-latest)`, `seeded guards`) | first-pass CI rate, same-head reruns (`hub_sdlc_costs`) |
@@ -51,6 +58,7 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
51
58
 
52
59
  | Feature | State here | Measured by |
53
60
  | --- | --- | --- |
61
+ | amem runtime integration (agent-mem#13 P4) | on after release; no claude-mem worker dependency | no-worker tripwire requests (must be 0), screened capture rows, profile refusals, all-author short-term recall, bounded-view truncation notices and curated-only ask citations |
54
62
  | Shared memory through amem, briefs, recall preface | on | notes saved per week, brief hits, fails that prevented a retry |
55
63
  | `ahub ask` | on | answers with cited ids |
56
64
  | Code structure in the memory graph (#684) | pending | stale anchors, briefs with related notes |
@@ -62,7 +70,7 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
62
70
  | Quota budgets and pauses | on | pauses, resets, time paused (#683) |
63
71
  | Token usage per peer and task | `ahub report` and, from the roll-up in `hub.db`, `ahub usage [--by peer\|task\|issue\|pr\|release\|day\|model]`, `usage limits` and `usage total` (#683 U2); console tab `7` (U3) and dashboard Usage view (U4) shipped | tokens per merged PR |
64
72
  | Context gate | off: `context.gate = 0` | context-pressure events |
65
- | Always-ask action classes (#636) | on for Claude (PreToolUse hook) and hub-owned ACP peers in ask mode, after the release (#312); Codex, Pi and the local worker pending (#313) | `permission` events for `Action approval` requests, the `actionPolicyInfo` rows of `ahub status`, budget refusals; a `never-ask` Kimi is listed as not enforceable, not claimed |
73
+ | Always-ask action classes (#636) | on after release for managed Claude forms, hub-owned ACP in ask mode, local/Pi tools and received Codex command approvals; Codex native bypass stays not enforceable | classed permission requests (a `gh api graphql` query is a read, a mutation, `gh run` or `gh workflow` write and every command with a shell expansion in a classed position (`git push $B`, `rm -rf "$DIR"`) is asked about: #756, #762; `test/action-class.test.ts` holds the table), full local/Pi command visibility and denied effects for incomplete native views; actionPolicyInfo current-child enforceability, remaining class budgets and weakening attention; Codex native no-request and Kimi non-ask ceilings are explicit |
66
74
  | Permission modes | on, person-chosen | approval prompts and their wait times |
67
75
  | Pi and the local worker on DGX | off: DGX unreachable recently; Pi is off the roster | turns served on-prem, relay errors |
68
76
  | PII routing | on | PII tasks and where they ran |
@@ -75,11 +83,12 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
75
83
  | Dogfood scorecard (#687) | on after release; read-only weekly receipt | `ahub report --dogfood --since 7d --json`; bounded evidence and explicit unknowns |
76
84
  | 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 |
77
85
  | Usage view (#683) | CLI (`ahub usage`, U2), console Usage tab `7` (U3) and dashboard Usage view (U4) shipped | its own panels |
78
- | 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) and the `ahub doctor` row `kimi usage source` are deferred to #285's later phases |
86
+ | 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 |
87
+ | 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 |
79
88
  | Settings reload | on | reloads applied, pending restarts |
80
89
  | 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 |
81
90
  | Upgrade and recovery | on, per release | upgrade receipts in `docs/verification/` |
82
- | 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); Kimi 2.3.0 and Qwen 0.26.0 verified by the [model-free receipt](verification/2026-10-22-kimi-qwen-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 |
91
+ | 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), 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 |
83
92
  | Benchmarks (#251, #637) | off: `bench.enabled = false`; routing gate pending | bench receipts |
84
93
 
85
94
  ## Decisions
package/docs/events.md CHANGED
@@ -69,7 +69,7 @@ Token usage by adapter:
69
69
  opaque hash of session, agent and byte offset; no path, session id or text reaches an event. Only the four integers, the model
70
70
  label and the time are copied from a row. The counted share is `inputOther + inputCacheCreation + output` (`kimiCountedTokens`,
71
71
  one function): cache reads are recorded and shown, never counted against `budget.kimi_tokens_5h`. The rolling 5 h sum of the
72
- counted share against that ceiling is the `tokens` window (`hub count, kimi wire log, of kimi_tokens_5h N`). Rows already in the
72
+ counted share against that ceiling is the `tokens` window (`hub count, kimi wire log, of kimi_tokens_5h N`, kind `count`: #285 phase 4 names each window's kind in the budget reply, never in an event, and a person's `ahub budget set` value, kind `manual`, outranks it at the gate). Rows already in the
73
73
  session's log when the hub first sees it (a resumed session) are history and are not counted. An unreadable index, a missing,
74
74
  shrunk or replaced file (a replacement is told by a hash of the consumed head and tail kept with each offset, since Linux can reuse the inode), a truncated last line, an unknown row shape or an untested wire `protocol_version` (only 1.5 is known)
75
75
  makes the count unknown: one notice (`kimi usage unknown (<reason>)`), the `tokens` reading is dropped, and no figure is reported