@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.
- package/CHANGELOG.md +25 -0
- package/README.md +12 -8
- package/docs/agent-notes/README.md +4 -0
- package/docs/agent-notes/adapters.md +6 -3
- package/docs/agent-notes/budget.md +4 -2
- package/docs/agent-notes/daemon.md +9 -3
- package/docs/agent-notes/local-worker.md +2 -2
- package/docs/agent-notes/tasks.md +13 -1
- package/docs/agent-notes/tests.md +3 -0
- package/docs/dogfood.md +13 -4
- package/docs/events.md +1 -1
- package/docs/memory.md +78 -10
- package/docs/operations.md +116 -30
- package/docs/quickstart.md +8 -4
- package/docs/security.md +80 -18
- package/docs/smoke.md +7 -3
- package/docs/specs/2026-09-19-agent-hub-design.md +84 -8
- package/docs/verification/2026-10-24-always-ask-adapters.md +52 -0
- package/docs/verification/2026-10-24-amem-runtime.md +69 -0
- package/docs/verification/2026-10-24-codex-0164-reviewer-isolation.md +55 -0
- package/docs/verification/2026-10-24-delivery-holds-ac8.md +264 -0
- package/docs/verification/2026-10-24-hardcoded-values.md +2108 -0
- package/docs/verification/2026-10-25-0.16.0.md +82 -0
- package/docs/verified.json +26 -17
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +2085 -1702
- package/src/adapters/acp.ts +5 -2
- package/src/adapters/codex-appserver.ts +69 -1
- package/src/adapters/compat.ts +2 -2
- package/src/adapters/definitions/builtin-metadata.ts +3 -3
- package/src/adapters/local-worker.ts +5 -4
- package/src/adapters/registry-types.ts +1 -1
- package/src/cli/console-state.ts +20 -2
- package/src/cli/console-symbols.ts +3 -2
- package/src/cli/console-usage.ts +5 -2
- package/src/cli/console.ts +5 -1
- package/src/cli/help.ts +7 -5
- package/src/cli/identity.ts +3 -1
- package/src/cli/main.ts +44 -10
- package/src/cli/memory.ts +119 -7
- package/src/cli/output.ts +12 -4
- package/src/hub/action-class.ts +211 -17
- package/src/hub/action-enforce.ts +13 -5
- package/src/hub/action-policy.ts +19 -8
- package/src/hub/ask.ts +16 -4
- package/src/hub/board.ts +86 -31
- package/src/hub/budget-notes.ts +65 -0
- package/src/hub/budget.ts +42 -10
- package/src/hub/bus.ts +1 -1
- package/src/hub/code-facts.ts +56 -16
- package/src/hub/conductor.ts +5 -2
- package/src/hub/config-apply.ts +7 -0
- package/src/hub/control-client.ts +1 -1
- package/src/hub/daemon.ts +250 -49
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +2 -2
- package/src/hub/hub-tools.ts +3 -3
- package/src/hub/kimi-usage.ts +12 -0
- package/src/hub/lifecycle.ts +10 -5
- package/src/hub/memory-code-head.ts +44 -0
- package/src/hub/memory-code-overlay.ts +119 -0
- package/src/hub/memory-plan-guard.ts +10 -1
- package/src/hub/memory-stale-anchors.ts +159 -0
- package/src/hub/memory-symbols.ts +74 -0
- package/src/hub/planning.ts +10 -4
- package/src/hub/read-tools.ts +21 -3
- package/src/hub/sdlc.ts +34 -0
- package/src/hub/settings.ts +17 -2
- package/src/hub/settled-roles.ts +15 -3
- package/src/hub/task-sweep.ts +25 -3
- package/src/hub/tasks.ts +353 -64
- package/src/hub/token-window.ts +15 -2
- package/src/hub/window-kind.ts +13 -0
- package/src/local/tools.ts +5 -5
- package/src/memory/brief.ts +22 -12
- package/src/memory/capture.ts +26 -40
- package/src/memory/client.ts +137 -65
- package/src/memory/recall.ts +13 -22
- package/src/ui/index.html +100 -18
package/docs/security.md
CHANGED
|
@@ -6,28 +6,30 @@ agent-hub connects agents that can each run commands. This page says what the hu
|
|
|
6
6
|
|
|
7
7
|
- **Other agents' text is untrusted.** Every message that crosses from one peer to another is framed as untrusted input: a channel tag with `meta.source` for Claude started with `ahub claude`; otherwise a fixed header line (`frame()`, `[agent-hub message from ...]`) with an instruction that says it is untrusted, the plugin's server instructions for a tools-only Claude reading `hub_inbox` and a standing instruction for the others. A message body cannot forge the hub's own headers: such lines are quoted (`sanitize`). Replies inherit a hop count capped at 3, so agents cannot ping-pong forever; neither a digest nor a steer can reset it.
|
|
8
8
|
- **The control link is loopback plus a secret.** The daemon and the Codex proxy bind 127.0.0.1 only. The control WebSocket requires a per-run token (`.agenthub/state/control-token`, mode 600), and both servers refuse any request that carries an `Origin` header: any web page can open a WebSocket to localhost, and browsers always send `Origin`. The bearer header a Codex TUI sends to the proxy carries only the terminal launch id (#586): it binds terminal ownership and is not a credential. External clients cannot claim the console user's id or a hub-managed peer's id.
|
|
9
|
-
- **Permission prompts stay on unless an operator selects a permission mode.** `ask` leaves native decisions unchanged. `ask-when-needed` grants scoped Claude file tools or Pi/local writes and edits; Kimi and Codex use their own native policies. `never-ask` requires explicit runtime confirmation or a person's console `y` for a config default at hub start. Until that config confirmation the peer runs in `ask`. Kimi's and `local`'s permission requests are relayed to the console and cancelled after `approvals.timeout_s` (default 120 s) of silence; the macOS notification for a waiting request carries the peer and the tool name only. The one exception is the hub's own tools (`hub_send` and the task tools, matched by exact name): Kimi's requests for them are approved once without a prompt and logged by name, the same trust Codex gets through `approval_mode` in the hub's config. They invoke hub-owned operations rather than arbitrary file or shell tools, and every call passes the hub's own checks. A call the always-ask policy classes (below) is never approved this way, whatever its title. Identity comes from the exact permission title, or from the earlier tool-call title bound to the same call id and resolved against the configured MCP servers when the permission title contains argument JSON (Qwen). Payload text and unrelated display titles never establish identity. Configured ACP kinds (#451) never get this exception: a request whose title spells a hub tool exactly still goes to the person approval path, and only a mapped native mode changes what the agent itself asks. For ACP agents, a payload longer than the console shows is marked as cut and never offers a session-wide grant. Pi's write, edit, shell and git write requests are relayed the same way and can be answered `Always allow <tool> until Pi restarts`: later calls of that tool by the same Pi start skip the prompt but stay inside the path guard and sandbox. The grant covers every later call of that tool whatever its payload, is kept in memory only, cannot be given from the dashboard, and each call it allows is logged by tool name. A Pi or `local` request whose call was cancelled or whose turn ended is withdrawn at once: a later answer is refused and grants nothing, an `Always allow` included. One shell command runs without a hub approval: the `!command` a person types in the managed Pi TUI. It stays inside the same sandbox and denylist, and the hub takes it only from the verified owner process of a Pi in TUI mode, while Pi is idle, on a reservation that is used once; a model's own bash call never takes that path (issue #253). `--unattended` turns prompts off, says so loudly, and is never the default; a classed call from a hub-owned ACP peer still waits for a person under it.
|
|
9
|
+
- **Permission prompts stay on unless an operator selects a permission mode.** `ask` leaves native decisions unchanged. `ask-when-needed` grants scoped Claude file tools or Pi/local writes and edits; Kimi and Codex use their own native policies. `never-ask` requires explicit runtime confirmation or a person's console `y` for a config default at hub start. Until that config confirmation the peer runs in `ask`. Kimi's and `local`'s permission requests are relayed to the console and cancelled after `approvals.timeout_s` (default 120 s) of silence; the macOS notification for a waiting request carries the peer and the tool name only. The one exception is the hub's own tools (`hub_send` and the task tools, matched by exact name): Kimi's requests for them are approved once without a prompt and logged by name, the same trust Codex gets through `approval_mode` in the hub's config. They invoke hub-owned operations rather than arbitrary file or shell tools, and every call passes the hub's own checks. A call the always-ask policy classes (below) is never approved this way, whatever its title. Identity comes from the exact permission title, or from the earlier tool-call title bound to the same call id and resolved against the configured MCP servers when the permission title contains argument JSON (Qwen). Payload text and unrelated display titles never establish identity. Configured ACP kinds (#451) never get this exception: a request whose title spells a hub tool exactly still goes to the person approval path, and only a mapped native mode changes what the agent itself asks. For ACP agents, a payload longer than the console shows is marked as cut and never offers a session-wide grant. Pi's write, edit, shell and git write requests are relayed the same way and can be answered `Always allow <tool> until Pi restarts`: later calls of that tool by the same Pi start skip the prompt but stay inside the path guard and sandbox. The grant covers every later call of that tool whatever its payload, is kept in memory only, cannot be given from the dashboard, and each call it allows is logged by tool name. A Pi or `local` request whose call was cancelled or whose turn ended is withdrawn at once: a later answer is refused and grants nothing, an `Always allow` included. One shell command runs without a hub approval: the `!command` a person types in the managed Pi TUI. It stays inside the same sandbox and denylist, and the hub takes it only from the verified owner process of a Pi in TUI mode, while Pi is idle, on a reservation that is used once; a model's own bash call never takes that path (issue #253). `--unattended` turns prompts off, says so loudly, and is never the default; a classed call from a hub-owned ACP peer, the local worker or Pi still waits for a person under it.
|
|
10
10
|
- **A committed config cannot choose launch commands, credential files, data endpoints, permission modes or a wider sandbox.** The machine-local fields (`kimi_cmd`, `permission_modes`, `codex_bin`, `pi.cmd`, `checks`, `mlx.bin`, `mlx.runtimeDir`, `mlx.modelPath`, `omniroute.urls`, `omniroute.access_hosts`, the `omniroute` key files, `memory.worker_url`, `local.read_allow`, `local.bash_network`, `local.network_allow`, `terminal.open`, `instances`, `action_policy`) apply only from a config file git confirms nobody committed (`instances` and `action_policy` are read only from an untracked `.agenthub/config.local.json`; in `config.json` they are ignored even when untracked): `.agenthub/config.json` or `.agenthub/config.local.json`, matched by file identity so no other spelling the file system accepts slips past, and `.agenthub` itself not a committed symlink or submodule. Without a repository, or when git fails, they keep their defaults; an empty value always means the default. "Untracked" is answered by the repository that contains the project: a checkout copied or extracted into an unrelated repository, or into an ignored directory of one, is trusted like your own files. So a cloned repository cannot choose a launch command, a completion check, a gateway to send a key file to, a memory endpoint, a permission mode, the command that opens a terminal, or a wider sandbox. A command in `checks` runs as you, outside the local worker's sandbox, like a git hook. So do the `setup` and `verify` commands of a benchmark suite (issue #251): `ahub bench run <suite.json>` is a person's command, refused from an agent session, and it resets the work tree for each attempt, so it runs only with `bench.enabled`, at the root of a clean repository that tracks nothing under `.agenthub/`. Nothing in `routing.toml` or in task text is ever run. `routing.toml` and the other shared fields still come from the checkout, and they matter: `routing.toml` picks the models the local worker and the hub's inference use at your gateway and can turn the PII constraint off, and roles and budget shape who does what. Review them in a repository you do not trust. The hub's own git calls outside the sandbox (snapshots, facts, config trust, project discovery, memory recall, the bench runner) all use the single hardened implementation in the exact-pinned first-party `@staix/agent-mem/platform` package, re-exported by `src/hub/git.ts`, which disables the programs a repository's configuration can name — the file-system monitor, hooks, external diff, the pager, and the credential and askpass helpers, with prompts failing closed — so a config that ever does get written cannot run code with the hub's rights. Of those, the monitor, hooks and external diff are reachable by commands the hub issues (the planted-program test exercises all three; a `diff --no-index` reads the repo's config, so that call also passes `--no-ext-diff` and `--no-textconv`); the pager is not (every call pipes stdout). Credential and askpass helpers are disabled for project repositories; memory sync alone may use credentials for its own memory repository, without prompts. The cost: `core.fsmonitor=false` also turns off a person's enabled built-in monitor for the hub's own calls, so the per-turn snapshot's `add -A` scans the whole tree (the snapshot Git calls of one burst share a 250 ms budget, each call at most 10 s; a boundary that runs out is recorded as unknown, never as a stale baseline) (#576). git has no global switch for filter drivers, and of the hub's commands only the snapshot's `add`/`restore` would run one (its driver command lives in the protected `.git/config`).
|
|
11
11
|
- **Telemetry holds no bodies.** `.agenthub/state/events.jsonl` (issue #40) records envelope ids, routing and sizes, task ids and states, overlapping paths and token counts, and the task id (with a PII flag) that each usage and token record is attributed to. It never records a message body, a task title or detail, and marks private (PII) envelopes and tasks as such. Budget pauses, resumes and quota resets carry the peer, window, times and the hub's own reason text, and a person's release authorization its commit, version and docs digest. The usage roll-up (#683) folds the usage, token and limit records into `hub.db` and joins issue, PR and release from the board when read; `ahub usage` and the console's Usage panel answer only the console and a configured planner, and a planner not during a private turn. Kimi's token records (#285) are counted from its own session wire log under its home: only the four token integers, the model label and the row time are copied, and the record id is an opaque hash, never a path, session id or text. The log stays on the machine; `ahub export` only prints it. Two opt-in stores copy numbers from it into your home directory, shared by every project on the machine (directories mode 700, files 600): research records (issue #247, `research.enabled`) in `~/.agenthub/research/` hold per approved task the ids, states, counts, tokens, times and PII flag the events hold, a person's label from a closed list, and a hash in place of the project id; benchmark runs (issue #251) in `~/.agenthub/bench/` hold suite name and hash, arm label, the attached peers' ids, models and permission modes, task ids, outcomes, exit codes, counts, tokens and times, never suite text or a command's output.
|
|
12
12
|
- **Snapshots stay in your repository.** Per-turn snapshots (issue #33) are git objects in the project's own object store, written through a temporary index; nothing is referenced, pushed or copied elsewhere, and `git gc` prunes them. They hold what the work tree held, including untracked files that are not ignored, so keep secrets in ignored files. They carry the repository's own permissions, and nothing caps their disk use but `git gc`. A turn of a peer holding an open PII task is not snapshotted; a PII file left in the project is snapshotted by later turns like any other file. `ahub undo` restores only files whose current content is exactly what the turn left.
|
|
13
13
|
- **The edit hook reads, never decides.** `ahub check-path --hook` (issue #32) reads hub.db and returns context for Claude and a line for you; it sets no permission decision, so your permission rules stay in charge. It names other owners' task ids, titles and states, which then reach Claude's model; PII tasks are left out.
|
|
14
14
|
- **The session record holds identities only.** `.agenthub/state/sessions.json` (issue #37, mode 600) keeps each attached peer's recovery metadata: launch options, session and thread ids, Pi's session file path. No message or task text; loss notices name deliveries by id, sender and public task title. The Pi continuity bookmark that recovery keeps across stop and archive (`.agenthub/state/recovery-continuity.json`, mode 600, at most 16 KiB) holds identities only too: operation and project ids, the project root, and Pi's session id, session file, backend and model name; a bookmark that is not a private regular file, is larger than that or names another project or operation reads as invalid, which is kept distinct from no bookmark.
|
|
15
|
-
- **Context checkpoints have a separate text record.** Native occupancy readings carry numeric metadata only. An optional context checkpoint persists its summary in `.agenthub/state/context-checkpoint-<peer>.json` with mode 0600 and
|
|
15
|
+
- **Context checkpoints have a separate text record.** Native occupancy readings carry numeric metadata only. An optional context checkpoint persists its summary in `.agenthub/state/context-checkpoint-<peer>.json` with mode 0600 and, when memory is enabled, proposes it as a machine-local memory candidate that only a person can accept. Completion requires the outstanding request id, current peer/native session and transport generation; new connection claims invalidate old requests. Private turns, every open PII task associated with the peer as owner or reviewer (including pending review), and PII-pattern summaries are refused before persistence or memory. Its text is never broadcast, and context pressure does not pause, reassign or replace the session.
|
|
16
16
|
- **The local worker is boxed in.** Paths are resolved through symlinks and must stay inside the project; a secrets denylist (`.env*`, keys, credential files, the hub's own state and the state archives `ahub reset --all` leaves) applies to its file tools, its git arguments and its memory capture alike; `.git` and `.agenthub` are not writable. Writes, edits, shell commands and mutating git wait for approval unless the operator's permission mode grants them (writes and edits inside the grant filter in `ask-when-needed`, all four in `never-ask`; see below), and the approver sees what will be written or run, with control characters escaped. Everything it executes runs under the macOS sandbox, attended or not. Since 0.10 the profile starts from deny default (issue #39): commands run and read only the system, toolchain and project directories (and the project's git dir and the selected developer dir; with network on, the public CA bundles); no writes outside the project, its git dir and a temp dir of its own (`TMPDIR`, made for each command and removed when it ends; issue #63); no `.git` name at any depth, closing the creation, rename, symlink and gitfile routes to a planted repository, no `config` or `hooks` directly under one nor below the discovered modules root of the project's own `.git` (a submodule's git directory, nested submodules included; every term is anchored below that root, so a directory named `refs`, `config` or `hooks` above the project neither disables the rule nor trips it; a `/refs/` or `/logs/` segment below it keeps refs named `config` or `hooks` writable, and leaves a submodule whose own path contains such a segment uncovered; a submodule whose path has `hooks` as a whole component (`tools/hooks` included) fares worse: the rule matches its whole git directory, so every write there is refused; the discovered git directories, the modules root and every component in between are also refused as targets, closing the rename, replace and symlink routes to the same files), no `commondir` at any depth below one, with every such name matched folded against the code points HFS+ ignores (the fold covers the security-relevant names; on a volume that folds names, folded spellings of the components between `.git` and a submodule's git directory are not matched — the 1024-byte SBPL literal cannot hold a folded path — the same residual the external git dir rules carry since #270), and no `.agenthub` anywhere in the project (in an external git dir the `config`/`hooks` rules cover only its own children and the hooks directory's contents, so refs named `config` or `hooks` still work); a nested repository in bare layout stays open — the directory is the git dir, with no `.git` name to anchor a rule on, and the hub never runs git there, whatever the project root is: its calls are all `-C <project root>` or `--no-index` on temp files, and none descends into a nested directory's git layout, so a planted hook fires only if a person or another tool runs git in that directory themselves; the rules are probed on HFS+, which folds the 16 code points the patterns tolerate, and on a case-sensitive APFS image, which folds nothing (a folded spelling is an inert new file there and the real names stay refused); the cost is that `git init`, `git clone` and `git worktree add` do not run inside the sandbox (add, commit, stash, checkout and gc still do), `git submodule update --init` does not either (its git directory's `config` write is refused), and neither do `git worktree remove`, `git worktree prune` of a stale record, `git clean -ffd` in a project with a nested repository, or `rm -rf` of a directory that holds a `.git` (refusing the unlink is what stops the rename-out route); no network, loopback included, unless `local.bash_network`; with it, only through the hub's egress proxy to the hosts in `local.network_allow` (issue #65), which refuses names that resolve to internal addresses (behind NAT64 with the well-known prefix, by the IPv4 address the answer carries; a network-specific prefix is not recognised), so claude-mem and the Codex app-server stay out of reach (`"direct"` opens everything, until 0.13.0). One channel stays open: `trustd`, which TLS clients need, can fetch a certificate's AIA or OCSP URL on a command's behalf, outside the proxy. The allow-default profile of 0.9 and earlier was removed in 0.12.0 (issue #83); a `local.sandbox` setting is ignored with a note. The shared temp dirs (the user's and `/private/tmp`) are closed. Without the sandbox there is no `bash` tool.
|
|
17
17
|
- **Capabilities are enforced, not suggested.** `capabilities` (issue #39) is checked by the daemon where task operations and messages arrive, so a peer cannot get round it by phrasing. A peer can never answer a permission request: the control link takes `permit` from the console role only, and a message that quotes a permit command is just text. Both bind the hub's own tool paths: a vendor agent with its own shell in the project (Codex, Claude, Kimi) can read `.agenthub/state/control-token` and connect as the console, which only the local worker's and Pi's sandbox prevents.
|
|
18
|
-
- **PII has an enforced path.** A task matching `signals.pii_patterns` goes to `local` or to nobody; its text is absent from other peers' envelopes, the console stream, the log and the board listing; the console user reviews it; `local` answers such a turn to the console only, keeps it out of its history, refuses it when the only gateway is off campus, and may not save notes or spin off tasks during it. Nothing about it
|
|
19
|
-
- **A model screen can add to the patterns (issue #198).** With `signals.pii_screen = "local"` in `routing.toml` (default `off`; it applies while `constraints.pii = "local_only"`), a newly proposed task is read by a model before the hub does anything else with it: until the verdict it is on no board, in no envelope, before no triage model, and the hub makes no brief or note call for it. The local worker's
|
|
20
|
-
- **Free text is screened too.** On an ordinary task, a done summary (with its check output: a match withholds the whole note), a review note, an unmet item or a budget handoff that matches a PII pattern (issue #69) is not saved to
|
|
18
|
+
- **PII has an enforced path.** A task matching `signals.pii_patterns` goes to `local` or to nobody; its text is absent from other peers' envelopes, the console stream, the log and the board listing; the console user reviews it; `local` answers such a turn to the console only, keeps it out of its history, refuses it when the only gateway is off campus, and may not save notes or spin off tasks during it. Nothing about it enters hub-managed memory; local storage does not relax this privacy fence.
|
|
19
|
+
- **A model screen can add to the patterns (issue #198).** With `signals.pii_screen = "local"` in `routing.toml` (default `off`; it applies while `constraints.pii = "local_only"`), a newly proposed task is read by a model before the hub does anything else with it: until the verdict it is on no board, in no envelope, before no triage model, and the hub makes no brief or note call for it. The local worker's amem capture leaves its hub tool calls out (issue #230), so a proposal it makes in an ordinary turn cannot reach shared memory ahead of the verdict that way. The screen runs on the model on this machine (MLX or Ollama, loopback only) or, without one, on the gateway only while `onCampus()` holds, and the gateway call is refused once more right before transport if the gateway is behind Access; task text never goes off campus to be screened. When the on-device generation slot is taken (Pi generating, another screen), the screen goes to the campus gateway instead of waiting, and on campus a device that fails or is still loading after 60% of the deadline hands over to the gateway too; off campus it waits for the slot and the device within the deadline. With the legacy MLX provider the screen may start an `mlx_lm.server`, as Pi's relay does, and it stays running until `ahub models stop`. Its answer is `pii` or `clear` plus a category from a closed list (name, student id, phone, address, grade, health, other). Anything else, a timeout (8 s, one call and one generation slot per item), a missing or off-campus model, or text longer than 6000 UTF-8 bytes (about 2000 Hangul syllables) is `unknown`, and an unknown task is a PII task: this is the hub's one model call that fails closed, so with the screener down every new task waits for `local`. An unknown verdict has the costs of any PII task, even when a second look clears it later: opening it lifts every silent turn-free cohort for good and turns facts off while it is open, and off campus a proposal without a class is refused (triage is not asked for a PII task there), with an error that says so. Off campus with the on-device slot taken, a task stays `unknown` until it is screened again: while it is still proposed and no peer ever owned it, the hub's release timer (every 60 s, one task at a time, the least tried first, at most 10 times per task in a hub run; never text too long to read) asks again, and `clear` lifts its `pii` signal and routes it through the class peers (a named owner is not remembered; a reserved one is offered first, as always). A task `local` took keeps its PII path, also once it is declined back to nobody. A pattern match is PII without a call. The task's history records the source of its `pii` signal (`regex`, `screen` with the category, or `unknown` with the reason), an unknown verdict is a console line naming the task id only, and `events.jsonl` gets a `pii_screen` record with the item kind, label, source, category, reason and latency, never the text. A routing explanation of a draft does not call the screen and says so.
|
|
20
|
+
- **Free text is screened too.** On an ordinary task, a done summary (with its check output: a match withholds the whole note), a review note, an unmet item or a budget handoff that matches a PII pattern (issue #69) is not saved to hub-managed memory, and every peer, `local` included, gets a stub naming `ahub task show <id>`; the board keeps the text, `hub.log` notes the withholding by task id only, and `ahub ask` shows such a note only when its model is reached on campus. `hub_remember` refuses a match; what a vendor agent writes to its own memory is not screened. With the model screen on, the same items (and `hub_remember` notes) go through it as well, one call each and only after the call's own checks (owner, state, verdict, a running check, memory enabled) pass: a `pii` or `unknown` verdict withholds the item exactly like a pattern match, and the board marks the history entry (`withheld`) so later views (task lists, completed-change notices, `ahub ask`, a reviewer replaced after a restart) keep withholding it. A budget hand-off is screened once per hand-off, whatever the number of tasks it moves; an escalation's reason is the hub's own text and is not screened. Pattern-only, without the model: the check output tail in a done note (only the summary is screened), a plan given with `hub_task_accept`, a decline reason, a task's refs (paths, branch), and context checkpoint summaries. Ordinary-turn amem capture uses deterministic secret, routing-pattern and private-path screening of complete input/output before bounding; it makes no model classification or network call. Private turns remain excluded before capture. Raw capture is machine-local and never curated evidence. Tools are identified by host session/turn/step/invocation rather than trusting provider ids to be unique. Its hub tool calls (`hub_send`, the task tools, the conductor tools) are not captured (issue #230), so a title, detail, done summary, review note or `hub_remember` text it writes reaches shared memory only through the screened paths above.
|
|
21
21
|
- **Secrets stay where they are read.** The gateway key and Cloudflare Access values are read inside the gateway client and go only into request headers: never into logs, errors, envelopes, tool output, memory. Subscription logins of Claude, Codex and Kimi are never proxied or pooled.
|
|
22
22
|
- **The hub's own model calls are fenced.** Digest condensation, task triage and the PII screen read agent-written text as data; their output is capped text framed as untrusted, or a value checked against a closed list. It is never used as a route, a peer id, a tool call or an instruction.
|
|
23
23
|
|
|
24
24
|
## Native memory authority and profile selection
|
|
25
25
|
|
|
26
|
-
The portable core is the exact-pinned first-party `@staix/agent-mem` 0.
|
|
26
|
+
The portable core is the exact-pinned first-party `@staix/agent-mem` 0.7.0 package; it does not replace the hub's caller identity, PII policy or task-write authority. Deny, hardened git, real paths and process identity are single-sourced in its Node-safe platform entry. Current project privacy is applied by the hub before promotion and again before model-facing reads. Standalone person reads are unscreened, and the standalone `--policy-free` acknowledgement grants no hub or peer authority.
|
|
27
27
|
|
|
28
28
|
`hub_memory_search`, `hub_memory_show` and `hub_memory_graph` take the project from the daemon, require `memory_read`, refuse private turns and validate UTF-8 input before logs or storage work. Complete records and graph metadata are screened before counts and truncation. Search is at most 20 lines/4 KiB (default 8); show accepts at most 8 ids and returns at most 8 KiB; graph is at most 120 nodes/240 edges/6 KiB (default 60 nodes), with no edge lacking an endpoint. Unknown, ambiguous and withheld ids share one neutral refusal. Arguments and results are excluded from hub-managed capture; this does not govern a vendor's independent transcript or memory hooks.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Code neighbours (#684 phase 4). For a symbol start, `hub_memory_graph` and the dashboard Memory view's Code neighbours toggle add one hop of callers and callees, read at the current commit through the optional code-facts boundary and shown with that commit; these edges are never stored. They share the graph limits above, an ambiguous or name-only seed is withheld, attached notes are screened again when published, current HEAD, profile, policy and caller privacy are rechecked across awaits, and a PII task or private turn starts no code query. `ahub doctor --stale-anchors` (#684 phase 3) only reads: it never edits, deletes or repairs a note, and an anchor whose task record is now private, foreign or missing stays unknown.
|
|
31
|
+
|
|
32
|
+
`hub_memory_correct` requires `memory_correct` and proposes a machine-local candidate after the same project/privacy checks, including revalidation after asynchronous screening. It cannot accept or alter a note. Public review failures with unmet items may also propose candidates through Tasks, and `ahub ask --remember` answers and context checkpoints are saved only as candidates, never as curated notes (agent-mem#13 P4). Candidates and usage counters stay at `M/candidates.db`, are not synced/imported, and never enter recall, briefs, ask or graph evidence. A person accepts shared-v1 candidates through the package CLI's TTY/policy checks; the accepted result is an ordinary v1 note, while the sidecar remains local. Generation `amem review` remains unsupported and refuses. Citation counters are observation proxies, not evidence that an agent understood or used a note.
|
|
31
33
|
|
|
32
34
|
The hub binds the selected root/profile at open. Invalid/missing generation metadata refuses native memory without a shared-v1 fallback; the bus and board continue. Explicit checks at hub operation entry and before sidecar writes stop later stale calls, but are not atomic against a selector change during an in-flight core operation. Migration therefore requires quiescing consumers and closing handles before changing selection, then reopening through the supported lifecycle. A public core pre-write hook is the upgrade path for atomic in-flight cutover.
|
|
33
35
|
|
|
@@ -61,8 +63,10 @@ vendor boundaries. The never-ask confirmation names this difference. No mode
|
|
|
61
63
|
turns off the sandbox or grants a session-wide allow-always option.
|
|
62
64
|
The always-ask action policy (#636) classes a call as publish, external-write,
|
|
63
65
|
destroy or install; `action_policy` is read only from an untracked
|
|
64
|
-
`.agenthub/config.local.json`.
|
|
65
|
-
|
|
66
|
+
`.agenthub/config.local.json`. A local policy that disables enforcement, omits a
|
|
67
|
+
class or raises a budget above its default is flagged on every status policy row;
|
|
68
|
+
startup still adopts it without a person's confirmation. Claude's
|
|
69
|
+
managed PreToolUse hook (the headless Claude implementer's included) returns `ask` for a classed call in every permission
|
|
66
70
|
mode, `never-ask` included, and that `ask` replaces any allow the mode would
|
|
67
71
|
add; the hook never denies, and a hub that is down, slow or refusing decides
|
|
68
72
|
nothing, which leaves Claude's own policy. A hub-owned ACP peer's
|
|
@@ -70,7 +74,17 @@ nothing, which leaves Claude's own policy. A hub-owned ACP peer's
|
|
|
70
74
|
hub-tool title, a mode grant and `--unattended` included), offers no
|
|
71
75
|
allow-always, shows `Action approval (<classes>): <command>` built from the
|
|
72
76
|
task's public view and the PII screen (withheld for a private turn or an open PII
|
|
73
|
-
task), and is denied when the approval timeout passes.
|
|
77
|
+
task), and is denied when the approval timeout passes. Since slice 3 (#636) a
|
|
78
|
+
command approval request Codex sends (v2 command execution or legacy exec) is
|
|
79
|
+
classed before it reaches the TUI and answered once, bound to its link, thread
|
|
80
|
+
and live turn; a cancelled, disconnected or stale one is denied. The local
|
|
81
|
+
worker's and Pi's model tool calls are classed at the hub's permit boundary
|
|
82
|
+
before `never-ask`, `ask-when-needed`, Pi's tool-wide grant and `--unattended`,
|
|
83
|
+
and a replayed Pi call spends no allowance twice. Their approval shows the
|
|
84
|
+
complete tool command with its class line on the person's console, private
|
|
85
|
+
turns included; logs, public status, other peers and the dashboard keep the
|
|
86
|
+
screened view. Any view that is cut (`[cut, N chars]`) or withheld offers deny
|
|
87
|
+
only, with no allow-once or class budget. A class budget above one
|
|
74
88
|
adds one person-only choice that allows at most N calls of those classes in the
|
|
75
89
|
same native session; a new session, a changed policy or a stopped, crashed or
|
|
76
90
|
restarted peer starts from zero, and an ordinary allow never spends it. The
|
|
@@ -78,13 +92,56 @@ limits are the point of the visibility rule: the hub enforces only a call it
|
|
|
78
92
|
sees before it runs. A Claude session without the hub hook or not yet proven to
|
|
79
93
|
have run it, an `--unattended` Claude launch (it skips hooks), an ACP agent in a
|
|
80
94
|
native mode that sends no request for a call it allows (Kimi's `yolo` and
|
|
81
|
-
`auto`), a call whose payload the agent did not report,
|
|
82
|
-
|
|
95
|
+
`auto`), a call whose payload the agent did not report, a Codex command its
|
|
96
|
+
native policy runs without sending a request (so status lists Codex as not
|
|
97
|
+
enforceable even though the requests it sends are gated), Jcode, an offline
|
|
98
|
+
local worker, a Pi without a verified live tool bridge and a headless Claude
|
|
99
|
+
implementer with no current child are not enforced, and `ahub status` says
|
|
83
100
|
`always-ask not enforceable: <reason>` for each such peer rather than claiming
|
|
84
101
|
it. The classifier reads command text: unknown syntax keeps native handling, and
|
|
85
102
|
an agent with an unrestricted shell can still reach the same effect another way
|
|
86
103
|
(the hostile-agent boundary above).
|
|
87
104
|
|
|
105
|
+
Expansions (#762). A shell expansion (`$VAR`, `${...}`, `$(...)`, a backtick,
|
|
106
|
+
`$'...'`) is an opaque word, and the literal words around it are still read, so
|
|
107
|
+
`git push origin $B`, `npm publish $D`, `gh issue create --title "$T"` and
|
|
108
|
+
`rm -rf "$DIR"` are classed like their literal forms. The command an expansion
|
|
109
|
+
runs (`echo $(git push)`) is classed too. Where the opaque part can change the
|
|
110
|
+
class the strictest class the command can reach is used, never none: `git $CMD`
|
|
111
|
+
is publish and destroy, `npm $CMD` is publish and install, `gh $GROUP ...` is
|
|
112
|
+
publish and external-write, `gh <group> $ACTION` is external-write (publish for
|
|
113
|
+
`release`), `gh api` with a dynamic method (`-X $M`), a dynamic option word, or a
|
|
114
|
+
trailing operand that is an expansion, is external-write, a GraphQL query with an
|
|
115
|
+
expansion in it is never a read, and `rm`, `cp -f` and `mv -f` with an opaque
|
|
116
|
+
target are destroy (an expansion may hold `..` or an absolute path). A word that
|
|
117
|
+
starts with an expansion may be a flag (`$OPTS` as `-rf`), so where a class
|
|
118
|
+
depends on a flag it counts as that flag: `rm $OPTS "$DIR"`, `git reset $MODE`,
|
|
119
|
+
`git clean $F`, `git branch $F topic`, `cp $F a /etc/x` and `npm install $G pkg`
|
|
120
|
+
are classed (a literal `-n`, or a word after `--`, still decides). The cost of
|
|
121
|
+
failing closed is a prompt for a call that may be harmless: `git branch "$NAME"`
|
|
122
|
+
and `npm install "$PKG"` ask, while `rm "$F"` (one operand), `git log $RANGE` and
|
|
123
|
+
`cp "$A" b` do not. A command
|
|
124
|
+
with no classed literal prefix (`echo $X`, `cat "$F"`, `sudo $CMD`,
|
|
125
|
+
`bash -c "$CMD"`) stays unclassed, and so does an opaque value in a position that
|
|
126
|
+
is data (the endpoint of `gh api`, a header, a `--jq` filter, a read-only
|
|
127
|
+
subcommand's argument). What the policy still cannot see is the content of a
|
|
128
|
+
variable used as the whole command, an alias or function, a script the command
|
|
129
|
+
runs, and text read from a file or stdin.
|
|
130
|
+
|
|
131
|
+
The `gh` classes (#636, #756, #762). `publish`: `gh release create|upload|edit|delete`.
|
|
132
|
+
`external-write`: `gh issue|pr|gist create|comment|edit|close|reopen|merge|review|delete`,
|
|
133
|
+
`gh run rerun|cancel|delete`, `gh workflow run|enable|disable`, and `gh api` with a
|
|
134
|
+
non-GET method or any field or `--input`. `gh api graphql` is the one exception,
|
|
135
|
+
classed by its operation: a call with exactly one literal `query` field whose
|
|
136
|
+
every operation is a `query` (shorthand `{...}` included; fragments allowed) is a
|
|
137
|
+
read and is not asked about, so a reviewer's unresolved-thread check does not
|
|
138
|
+
prompt. A `mutation`, a `subscription`, a second `query` field, a document from a
|
|
139
|
+
file or stdin (`-F query=@file`, `--input`), an unknown option, a non-POST method
|
|
140
|
+
and any document the hub cannot parse stay `external-write`: unknown is never a
|
|
141
|
+
read. Read-only `gh` commands (`view`, `list`, `status`, `watch`, REST GET) are
|
|
142
|
+
unclassed. `gh repo delete|archive|edit`, `gh secret` and `gh variable`
|
|
143
|
+
`set|delete`, and `gh label create|edit|delete` are `external-write` as well.
|
|
144
|
+
|
|
88
145
|
Config defaults apply automatically only up to ask-when-needed. A never-ask
|
|
89
146
|
request from an applied untracked config is held as ask until the person
|
|
90
147
|
confirms its source file in the console at hub start. Git-tracked mode defaults
|
|
@@ -347,7 +404,7 @@ explicitly chmodded 0700 parent directory.
|
|
|
347
404
|
PII-task notes, PII-pattern notes and secret-looking text are refused before
|
|
348
405
|
persistence; promotions are
|
|
349
406
|
screened again. The index can be rebuilt from segments and does not replace the
|
|
350
|
-
|
|
407
|
+
historical claude-mem database used only by the explicit read-only import.
|
|
351
408
|
|
|
352
409
|
Sync starts only after a person configures a remote. Public GitHub remotes are
|
|
353
410
|
refused; unknown visibility proceeds with a warning for the configured remote.
|
|
@@ -382,11 +439,11 @@ fence instance ownership and close; unknown owners are never signalled.
|
|
|
382
439
|
|
|
383
440
|
## Planner, cancellation and delivery authority
|
|
384
441
|
|
|
385
|
-
The `planner` setting is person-controlled. A valid pin determines the planner without adding
|
|
442
|
+
The `planner` setting is person-controlled: one peer id or a list of up to eight (#525-2). A valid pin determines the planner holders without adding them to the roster; otherwise configured rostered planner roles apply, then the first rostered peer in the closed planner catalog. The automatic choice is not written to disk. Each new non-PII task and each planner-created release records one effective planner from the same pure walk `ahub route explain` shows; only that planner or the person changes its planning. A bound planner keeps that authority while busy, paused or offline; one removed from the holders or the roster falls back to the current holders, and older rows with no binding, PII tasks included, keep holder-wide authority. Only the person moves a task's planner (`ahub task assign <id> <peer> --role planner`). Another holder may record a plan review without gaining the task's other planning powers; an agent cannot review its own plan while another rostered holder is not offline, and otherwise the receipt records `selfReview: true` with `planAuthor`. Planner guidance does not grant conductor authority or bypass roster, PII, WIP or owner capability checks.
|
|
386
443
|
|
|
387
|
-
`cancelled` is closed, never approval. The person can cancel non-approved work with a reason and optional supersession reference;
|
|
444
|
+
`cancelled` is closed, never approval. The person can cancel non-approved work with a reason and optional supersession reference; a planner can cancel only its own proposed/backlog work with no historical acceptance, and only as that task's effective planner. Current task revision, privacy and recovery/shutdown holds are checked before writing and again after awaited screening. Cancellation preserves history, releases reservations and overlap membership, writes no outcome memory, and does not stop native turns or delete branches/worktrees. Owner/reviewer stop notices remain relevant; earlier task requests become moot. Dependents remain blocked and receive ids-only advice, including backlog dependents.
|
|
388
445
|
|
|
389
|
-
Current daemons advertise `task_cancel` in hello and status metadata. A current CLI refuses cancellation against a daemon without that feature. Controlled upgrade/restart refuses an unsupported target while cancelled task ids exist; source prepare and commit recheck compatibility, and the rows are never rewritten as approved. A manually started older binary is outside this compatibility guarantee: it can display cancelled rows as open or fail to move them.
|
|
446
|
+
Current daemons advertise `task_cancel` in hello and status metadata. A current CLI refuses cancellation against a daemon without that feature. Controlled upgrade/restart refuses an unsupported target while cancelled task ids exist; source prepare and commit recheck compatibility, and the rows are never rewritten as approved. In the same way it refuses a target without `task_planners` once a planner list or an instance planner is configured or any task or release planner binding or plan-review authorship is stored, even after that configuration is removed. A manually started older binary is outside this compatibility guarantee: it can display cancelled rows as open or fail to move them.
|
|
390
447
|
|
|
391
448
|
Stall causes are persisted and deduplicated by cause and replanning revision. Planner advice joins its next delivery as bounded ids-only context; it does not manufacture an ordinary task turn. Pull-only offer attention clears only after acceptance, decline, supersession or durable positive evidence for that exact offer and recipient. Operator/hub completion of an uncertain delivery is not evidence that the offer was read.
|
|
392
449
|
|
|
@@ -398,13 +455,13 @@ A terminal Claude implementer or verifier instance (#525-5) owns write-class tas
|
|
|
398
455
|
|
|
399
456
|
The headless Claude implementer (#525-4) runs one owned `claude -p` child per turn in its declared worktree, with `--setting-sources ""`, `--strict-mcp-config`, native permission mode `default`, the hub's managed PreToolUse hook and `--permission-prompt-tool mcp__agent-hub__hub_permission_request`; never `--bare`, `acceptEdits`, `dontAsk` or `bypassPermissions`, and `never-ask` is refused. Each task keeps its own persisted session; unscoped chat reuses one only while exactly one task is in progress, and a `hub_inbox` read refuses a batch attributed to another task. The approval path fails closed: `hub_permission_request` is accepted only from the current child's launch and generation, goes to the console's existing approval queue and `approvals.timeout_s`, and offers allow-once and deny only, with no allow-always or session grant. A timeout, a prompt failure, a disconnect, a hub that is down or refusing, or a stale child binding answers deny; a failed hook decides nothing, so the call still waits for a person. The approval title is the tool name and a JSON rendering of the arguments passed through `sanitize()` and the public-text screen (any PII, secret or private-path match withholds the whole value as `[withheld]`, before any cut), never task text, cut at 600 characters with a `[cut, N chars]` marker; the original arguments stay with the child and are not logged. The MCP wire cannot tell a native permission dispatch from a call the model makes itself; either way it needs a fresh allow-once for its own arguments and grants nothing later. A stop holds an uncertain delivery until the owned process group is confirmed gone. The [native permission receipt](verification/2026-10-23-headless-claude-permissions.md) is model-free on Claude Code 2.1.309; it is not sandbox proof, and a real account, model and person interaction remains a live leg.
|
|
400
457
|
|
|
401
|
-
The fresh Codex reviewer (#519) needs `review_mode: "fresh"` and a compat-admitted Codex (receipt-verified 0.146.0 and 0.
|
|
458
|
+
The fresh Codex reviewer (#519) needs `review_mode: "fresh"` and a compat-admitted Codex (receipt-verified 0.146.0, 0.163.0 and 0.164.0; `ahub agents` shows the current policy); an unverified/untrusted version is unavailable. Before each review it requires a model-free isolation receipt: binary and config unchanged, only the two hub tools in the configured MCP inventory, plugins, hooks and Apps disabled (Apps by a separate feature readback), and a named filesystem profile whose fixtures prove public reads, private-path denial (exact, glob and symlink), standard host credential denial and write denial. Credential and tool-home roots are denied even with an empty `local.deny`; that list does not cover every personal file. A dangling or unreadable credential root refuses the reviewer, and the reason names only its path class (`~/.docker`, `$DOCKER_CONFIG`), never the path or its contents. A failed per-review start names one class from a closed list (`version`, `binary`, `mcp_inventory`, `plugins_hooks_apps`, `sandbox_proof`, `credential_root`, `timeout`, `unknown`) with a fixed sentence in the unavailable reason, `hub.log` and the `reviewer` line of `ahub status`; it never carries a path, native CLI text or a token (#722). Its shell is sandboxed read-only, not Claude's Read/Grep/Glob whitelist. Network and web search are configured disabled but not observed: managed requirements can still select cached web search, and no run has shown the review selecting the profile through `default_permissions`. A failed receipt or start never falls back to a continuing native session, and a child that ends without a saved verdict is never approval. The agy reviewer slot is an unavailable placeholder and never runs.
|
|
402
459
|
|
|
403
460
|
The fresh Kimi reviewer (#533) needs `review_mode: "fresh"` and a compat-admitted Kimi Code (receipt-verified 2.2.0 and 2.3.0; `ahub agents` shows the current policy) with its native OAuth login; an unverified/untrusted version, or a failed preparation, is unavailable. The [2.3.0 receipt](verification/2026-10-22-kimi-qwen-compat.md) is model-free and does not prove real login or a model review. It is a native tool-authority boundary, not a sandbox: no Seatbelt wraps it, and same-user native code is not isolated from the filesystem or network. Each review starts a new child in a private `KIMI_CODE_HOME` with managed-provider-only config, a clean temporary working directory and an empty skills directory. Its agent may use only `hub_review` and `hub_task_show`, served by a loopback MCP session bound to the task, head and review revision that never carries the hub's control token; it has no native read, file or shell tool and sees only the screened review brief and host facts. Its `credentials/` and `oauth/` directories and `device_id` link to the person's native ones, so refresh keeps Kimi's own lock and atomic rename; no token is copied and `KIMI_DISABLE_OAUTH_LOCK` is never set. A stop revokes its verdict authority before termination; killing it mid-refresh may require a native re-login. Private tasks are never given to it, and a child that ends without a saved verdict is never approval.
|
|
404
461
|
|
|
405
462
|
The review brief (#520) withholds private changes whole (both sides of a move) and counts them; a rule file that fails the private-data screen is named, not shown. Attributes and rules are read at the merge base, so a change cannot loosen its own review. Optional changed symbols (#684) come from ripwire 0.6.7 on the daemon's host PATH, run as an owned process on a disposable snapshot of the task's commit; each path, name and kind passes the hub's public metadata screen before the brief, the console's task detail or the dashboard shows it, a private task stores and shows none, and a failed or missing read is `unknown`, never a gate. Spec acceptance, plan review, plan conformance, premature-review, cost, test-integrity (#481 S4), issue and PR identity linking (#694) and dogfood scorecard (#687) reads fetch GitHub metadata only (edit times, check states, thread authorship and resolution, changed file names, open PR heads, workflow run conclusions), never issue bodies, comments or patches. Issue identity linking at propose and accept (#723) parses the task's title and refs locally, reads nothing from GitHub and skips PII tasks. The CI workflow keeps `contents: read` and `actions: read`.
|
|
406
463
|
|
|
407
|
-
Deliverable records (#509a). `hub_task_done` and `hub_sdlc_record` name files as `{path, format, version?}`; the hub hashes them itself and refuses a claimed `sha256`, `hash` or `bytes` and any field it does not own. Paths resolve under the owner's execution root with `realPath` and `guardPath` in read mode: outside the project, the secrets denylist, the hub's own state (`.git`, `.agenthub`), a dangling link, a non-regular file and more than 256 MiB each or 512 MiB together are refused, with the path as given and never a resolved one. The file is opened without following a final link and its device, inode, size and time are compared with what was checked, before and after the read, so a swapped or changing file is refused rather than recorded half-read; a bad call writes nothing. A declared worktree instance whose root is not proven refuses, and a later read of it reports `unknown`. No hash of hub state is ever taken, and a file the hub cannot read or vouch for is `unknown`, never `unchanged`. A reviewer's approval re-hashes the files and is refused when one changed, went missing or is unknown; only the person may approve the current bytes, which are then measured and bound. For a PII task, a file under `pii.paths` or a path matching a PII pattern, `publicDeliverables` is the one projection every peer view, review request, status and log uses: id, hash and size only. The board keeps the path, as it keeps a PII task's text.
|
|
464
|
+
Deliverable records (#509a). `hub_task_done` and `hub_sdlc_record` name files as `{path, format, version?}`; the hub hashes them itself and refuses a claimed `sha256`, `hash` or `bytes` and any field it does not own. Paths resolve under the owner's execution root with `realPath` and `guardPath` in read mode: outside the project, the secrets denylist, the hub's own state (`.git`, `.agenthub`), a dangling link, a non-regular file and more than 256 MiB each or 512 MiB together are refused, with the path as given and never a resolved one. The file is opened without following a final link and its device, inode, size and time are compared with what was checked, before and after the read, so a swapped or changing file is refused rather than recorded half-read; a bad call writes nothing. A declared worktree instance whose root is not proven refuses, and a later read of it reports `unknown`. No hash of hub state is ever taken, and a file the hub cannot read or vouch for is `unknown`, never `unchanged`. A reviewer's approval re-hashes the files and is refused when one changed, went missing or is unknown; only the person may approve the current bytes, which are then measured and bound. For a PII task, a file under `pii.paths` or a path matching a PII pattern, `publicDeliverables` is the one projection every peer view, review request, status and log uses: id, hash and size only. The board keeps the path, as it keeps a PII task's text. `ahub deliverables` (#509b) is the person's: an agent shell is refused and `--live-test` cannot delegate it. It lists the board's records through `publicDeliverables` and opens a file only with `--check`, through the same reader and limits. The dashboard's Deliverables chip and task group use the same projection of the same records and never read a file on the poll; a `changed` mark there is only as fresh as the last live read in this daemon.
|
|
408
465
|
|
|
409
466
|
Person release authorization (#481 S6). `ahub sdlc authorize-release <version>` is taken from the console role only and refused under `--live-test`. It binds the daemon checkout's commit, package version, push remote identity and a fresh-docs verdict, lives only in daemon memory (a restart revokes it) and is logged with its commit, version and docs digest. `ahub sdlc release-guard -- <argv>` is a read-only preflight that agents may run, except during a private or PII turn; it never executes the command and refuses ambient target selectors (`GIT_DIR`, `GH_REPO`, npm prefix and registry settings and the like). The pre-push hook that `ahub init --release-guard` installs refuses an unauthorized or unknown `v*` tag push, also with the daemon down, and passes other pushes through. It and the release block in `AGENTS.md` are a cooperative guard, not an operating-system boundary: `git push --no-verify`, a custom `core.hooksPath` or another Git invocation bypasses the hook, and `gh release create` and `npm publish` are checked only when the preflight is run (#312, #313).
|
|
410
467
|
|
|
@@ -425,3 +482,8 @@ Checks serialize through the repository's existing `ahub-heavy.lock`, with one a
|
|
|
425
482
|
Only an unchanged task/owner/native-generation binding receives an agent result. Stale receipts are person-only. Public receipts include the exact source/tree/lockfile, runner/platform, command, exit and bounded output; they omit internal host paths, owner process identity and generation binding. The output tail is screened again before any tool response or hub envelope. For inactive valid receipts, local full output is bounded to 1 MiB and removed when the owner reads its result (person inspection does not consume it), after 24 hours or beyond the newest 20 outputs; at most 200 inactive valid receipts remain, with active runs excluded from retention. Unknown receipts are an explicit retention exception: malformed/unreadable receipt bytes, full output and source staging remain until ownership/inactivity can be established or the receipt is removed. Their ID-only warnings remain visible. Unknown payload maintenance performs no path-based deletion: a same-user process can swap a parent directory after an entry check, and no atomic descriptor-relative cleanup is available at this boundary. Maintenance never removes an unknown container. Removing the whole receipt directory counts as an empty record set, clears its ID warnings and allows ordinary expired unindexed-staging cleanup. Linux VM evidence is separate from macOS Seatbelt and managed-native acceptance.
|
|
426
483
|
|
|
427
484
|
Routing receives capability availability and its reason as pure inputs. Capability admission applies to task owners only: non-PII implement/test/bulk-edit owners require tests, and non-PII issue-backed owners in every class (including review-class owners) require spec access. Designated reviewers are not gated by these owner-only capabilities; roster, privacy, role and other eligibility rules still apply. PII tasks are exempt from these two capability requirements and retain the existing local-only owner and person reviewer rules; spec/check tools still refuse them. Explicit nominations and reservations cannot bypass these checks. Inactive sandboxed peers establish their GitHub probe at first spec use, and every explicit local/Pi start path refreshes it (including a prior authentication failure) with concurrent probes shared; host-provided embeddings can inject bounded probe implementations without implicitly contacting real accounts in fixtures. Neither path grants a model control over the probes.
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
## Structured capture write screening
|
|
488
|
+
|
|
489
|
+
The amem host screens the actual data that may persist, including literal keys/values and its JSON form. It decodes authoritative tool argument/result JSON once before any excerpt cap, so escaping cannot hide whitespace-matching PII, a private data path or a structured secret key/value. It does not recursively interpret arbitrary embedded encodings. Non-persisted host locators (cwd and transport/session identifiers) are kept outside text-policy content; current project/profile authority is checked separately. Ordinary events with a valid private-path policy still capture public data. Private turns remain excluded before this code runs.
|
package/docs/smoke.md
CHANGED
|
@@ -76,7 +76,7 @@ Not run yet (2026-10-12). Source and fakes cover tiers, the graph, retrieval, sy
|
|
|
76
76
|
- [ ] `ahub memory import claude-mem` against the live claude-mem store of this machine: counts and skipped classes, claude-mem unchanged.
|
|
77
77
|
- [ ] `memory.suggest_edges: true` with the hub's model reachable: suggestions appear, are drawn dashed, and stay out of briefs until confirmed.
|
|
78
78
|
- [ ] Dashboard Memory section and Long-term tasks tab in a browser, light and dark, at phone width.
|
|
79
|
-
- [ ] AC11
|
|
79
|
+
- [ ] AC11 collection after P4: declare a new amem-aware condition before runs and collect repeated arms plus research records with `briefs`. The historical `--memory claude-mem` label is not evidence of a worker on the new runtime; do not compare it silently with old worker runs.
|
|
80
80
|
|
|
81
81
|
## Office view (#264, phases 1-4)
|
|
82
82
|
|
|
@@ -629,14 +629,14 @@ Needs a model gateway in `omniroute.urls` (for the owner: the campus gateway ove
|
|
|
629
629
|
1. `ahub doctor`: `omniroute` healthy, `omniroute key` present, every routing model alias served; no Switchyard row.
|
|
630
630
|
2. In a scratch git repo: `ahub up`, `ahub local`, `ahub tail`, then `ahub say @local "fix the typos in <file>, run git diff --stat and report"`. `ahub tail` shows a permission request for the edit; `ahub permit <id> allow`. The file changes, the answer is a short conclusion, `ahub status` shows the served model in the local row's `MODEL` column and `vllm` in the backend table's `PROVIDER` column.
|
|
631
631
|
3. In an isolated scratch project, use `[local] route = "hub/stage"` with served `fast` and `coding` aliases. Confirm the in-process route event and gateway provider. Try a legacy `sy/` selection: it uses `fixed_model` with one warning, starts no sidecar, and leaves the routing file unchanged. The older Switchyard receipts below describe the retired integration.
|
|
632
|
-
4.
|
|
632
|
+
4. amem: after a public tool turn with a unique marker, `amem search "<marker>" --limit 10` in that project shows machine-local capture without a worker or model summarizer. Confirm ordinary curated rows remain separate from raw capture and `ahub ask` never cites a raw capture row.
|
|
633
633
|
|
|
634
634
|
## M4: task board
|
|
635
635
|
|
|
636
636
|
1. `ahub up`, `ahub kimi`, `ahub local`, `ahub tail`. `ahub route explain --class bulk_edit "fix typos"` prints candidates, owner and reviewer.
|
|
637
637
|
2. `ahub task propose bulk_edit "Fix the spelling mistakes in words.ts" --path words.ts`: `local` accepts, edits, calls `hub_task_done`; `ahub board` shows it `approved` (no reviewer attached) or `in_review`.
|
|
638
638
|
3. `ahub task propose test "Check that words.ts contains ..."`: Kimi takes it and reports through the hub's MCP tools (`ahub task show <id>` history: `kimi accepted`, `kimi done`).
|
|
639
|
-
4. PII: propose a task whose title matches `signals.pii_patterns`. Owner `local`, reviewer `user`; `grep <value> .agenthub/state/hub.log` and the `ahub tail` output find nothing; `ahub board` shows `[pii]`; `ahub task show <id>` shows the text; `ahub review <id> approved` closes it;
|
|
639
|
+
4. PII: propose a task whose title matches `signals.pii_patterns`. Owner `local`, reviewer `user`; `grep <value> .agenthub/state/hub.log` and the `ahub tail` output find nothing; `ahub board` shows `[pii]`; `ahub task show <id>` shows the text; `ahub review <id> approved` closes it; amem has no capture, candidate or curated row with the value.
|
|
640
640
|
5. With Claude and Codex attached: a task proposed by Claude, done by Codex, reaches Claude as a review; `tasks.escalate_after` consecutive `changes_requested` (default 3) move it to the next peer in `escalate_to`.
|
|
641
641
|
|
|
642
642
|
## M5: budget relay
|
|
@@ -1976,3 +1976,7 @@ Kimi usage ceiling: 2.2.0 print stream-json exposes no token counts. Keep unobse
|
|
|
1976
1976
|
- In the native TUI call `hub_inbox`, `hub_send`, and a permitted board read/proposal within a non-PII task. Confirm the tool audience and actor remain `agy`, the native model consumes the tool results, and no unsolicited push or PII task text reaches it.
|
|
1977
1977
|
- End the TUI normally. Confirm its group/socket detaches and only its matching managed three-file plugin is cleaned up; unrelated project/global settings remain unchanged.
|
|
1978
1978
|
- This live project/account leg is pending until actually run. An older daemon's missing `native_hook_launch` refusal is not a successful TUI launch or grounds for an automatic restart of other active peers.
|
|
1979
|
+
|
|
1980
|
+
## Held deliveries and peer retries (#434 AC8, 2026-10-24)
|
|
1981
|
+
|
|
1982
|
+
See [delivery holds and peer retry verification](verification/2026-10-24-delivery-holds-ac8.md) for live evidence of hub-settled held deliveries (39 superseded notices in `resolution_history`) and peer retries (2 peer retry rows in `resolution_history`, 27 rows across Claude, Jcode, and Kimi in `delivery_retry_evidence`), alongside daemon status and queue readbacks.
|
|
@@ -926,7 +926,7 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
|
|
|
926
926
|
|
|
927
927
|
This slice provides the pure classifier, machine-local policy, host-only per-session budget ledger and per-peer status. Adapter pre-tool enforcement is a later slice. Until a runtime actually consumes the policy before execution, its status says `always-ask not enforceable: adapter integration pending`; native modes, sandboxes and approval frequency retain their existing behavior. The default-on configuration is not a claim of active enforcement.
|
|
928
928
|
|
|
929
|
-
- `action_policy` may be set only in untracked `.agenthub/config.local.json`, never in `config.json` even when that file is untracked. `ALL_MACHINE_LOCAL` trust drops a tracked/unknown local source. Default: `enabled: true`, all four classes in `always_ask`, budgets `publish: 1`, `destroy: 3`, `external-write: 5`, `install: 2`. Budgets are integers 0..1000; zero caches no follow-up grant. Empty `always_ask` selects no classes. The settings registry exposes enabled and each budget; the closed class set is changed in the local file with a person-confirmed reload. Weakening on live reload requires existing person confirmation; shared-file policy edits are refused before any confirmation offer. Startup
|
|
929
|
+
- `action_policy` may be set only in untracked `.agenthub/config.local.json`, never in `config.json` even when that file is untracked. `ALL_MACHINE_LOCAL` trust drops a tracked/unknown local source. Default: `enabled: true`, all four classes in `always_ask`, budgets `publish: 1`, `destroy: 3`, `external-write: 5`, `install: 2`. Budgets are integers 0..1000; zero caches no follow-up grant. Empty `always_ask` selects no classes. The settings registry exposes enabled and each budget; the closed class set is changed in the local file with a person-confirmed reload. Weakening on live reload requires existing person confirmation; shared-file policy edits are refused before any confirmation offer. Startup adopts the untracked local policy without a retained confirmation and surfaces attention in status when it disables enforcement, omits a class or increases a default budget (ponytail: retain a person-bound startup confirmation). Reload never imports code or changes a sandbox.
|
|
930
930
|
- `classifyAction(tool, args, executionRoot)` returns all matching classes for bounded literal tool/argv or shell forms. Quotes, command/group separators, fd duplication and plain input/output redirections are parsed without evaluation; known wrapper prefixes and literal shell -c forms are unwrapped. Multi-class chains retain every class, so narrowing the ask set cannot hide another class. Publish covers remote Git/package/release publication, external-write covers known non-read HTTP/gh/mail/chat calls, destroy covers destructive Git and recursive deletion/force-overwrite outside the lexical execution root, and install covers known global installs. Unsupported syntax stops parsing while retaining classes from earlier complete segments; the unsupported current segment remains unknown. Hard input/segment limits still return no classification. Unknown forms retain native policy, never an automatic permission. Literal cwd/workdir, home spellings conservatively treated as outside, and conservative literal cd candidates are covered; aliases, functions, expansion, scripts and symlink targets are not attested. The ceiling is bounded syntax/lexical evidence; the upgrade path is typed native tool arguments and canonical path evidence from each adapter.
|
|
931
931
|
- The first selected action asks. One explicit person approval of the class allowance authorizes that invocation and caches at most budget minus one follow-up invocations of each selected class. An ordinary allow_once answer never seeds this cache; adapter prompts must describe the class allowance they request. The next invocation after exhaustion asks again. Requests consume all selected class slots synchronously or none; an unselected/unclassified action returns `native`, which means the existing permission decision still runs. No ledger result overrides a denial, suppresses a prompt required by the native mode, or grants a tool capability. Session identity comes from the host's current runtime port; missing or changed identity cannot use a grant. New sessions, owner revocation and policy publication invalidate grants. One-use approval challenges are bound to the exact session handle and policy, so a late answer cannot grant to a replacement session or changed policy.
|
|
932
932
|
- Status reads never create/bind/reset a session. Status carries only classes, limits, remaining counters and enforcement state, without native session ids or command/task text. Approval copy is built through `actionApprovalText`, which takes the host's `publicView`, private-turn decision and whole-command privacy projection; private commands are withheld. Adapter integrations must retain the existing timeout-denies and generation/privacy rechecks after awaiting a person. Slice 1 creates no approval requests itself and makes no per-adapter A2 or live/native enforcement claim.
|
|
@@ -936,7 +936,10 @@ This slice provides the pure classifier, machine-local policy, host-only per-ses
|
|
|
936
936
|
- **Claude.** The `facts` reply to a PreToolUse hook call carries `actionClasses` when `classifyAction` classes the tool and its arguments under the peer's execution root and the policy selects at least one class. `factsHook` then emits `permissionDecision: "ask"` (with `permissionDecisionReason: "agent-hub always-ask: <classes>"`) in every permission mode, and the `ask` replaces any `allow` the mode would add. The managed hook's contract is therefore: it may add `allow` as before and `ask` for a classed call, never `deny`; a hub that is down, slow or refusing decides nothing (`docs/agent-notes/daemon.md`). A value that is present but unreadable asks as well; an absent or empty one leaves the mode. Claude's dialog cannot carry a class-budget choice, so the ledger is never consulted for Claude: every classed call asks. A hub that cannot answer, or a call outside a verified hub hook, leaves Claude's own policy.
|
|
937
937
|
- **ACP.** `AcpPeer.answerPermission` consults `AcpOptions.actionGate` before `autoApprove`, with the announced and canonical tool names, the ACP kind and the arguments from `rawInput`, the announced `tool_call` or complete streamed JSON. A call with no reported payload is not classed (unknown follows the mode). A verdict (a classed call with no session budget left) is never auto-approved, drops `allow_always`, replaces the title with `actionApprovalText` (the task title only through `publicView`, the command only through the PII screen, withheld for a private turn or an open PII task) and sets `classed` on the `PermissionRequest`, so `--unattended` does not answer it. A throwing gate asks. The request uses the existing approval timeout and a timed-out request denies.
|
|
938
938
|
- **Class budget.** When a selected class has a budget above one, the request carries the hub's own option `agent-hub:action-budget` (kind `allow_once`). Only a person's choice of it calls `ActionBudgets.approve`, which spends the allowance for the current native session and policy; the agent is answered with its own `allow_once` id and never sees the hub's. A refused approval (changed policy, replaced session, no slot left) answers cancelled. An ordinary allow never seeds the ledger. Requests are concurrent, so several challenges can be open while a class is spent: the first approval opens the allowance, a later approval of a challenge issued before it consumes one slot instead of opening another (refused whole when any class has none left), so a budget of N lets through at most N approved calls. The ledger entry is revoked whenever an ACP owner goes offline, is stopped or removed, or a new owner is created, so a resumed session id never brings an allowance back. A call inside the cached allowance follows the permission mode as before.
|
|
939
|
-
- **Visibility.** `status.actionPolicyInfo[peer].enforceable` is true only where the call is seen before it runs: a Claude session with the launcher's PreToolUse hook that is not `--unattended`, and a hub-owned ACP peer in native mode `ask` (Kimi's `yolo` and `auto` send no request for a call they allow). For Claude the launch record alone is not proof (it outlives the process): the attached session must be online and its current generation must have run the hub's hook (`claudePermissionGenerations`).
|
|
939
|
+
- **Visibility.** `status.actionPolicyInfo[peer].enforceable` is true only where the call is seen before it runs: a Claude session with the launcher's PreToolUse hook that is not `--unattended`, and a hub-owned ACP peer in native mode `ask` (Kimi's `yolo` and `auto` send no request for a call they allow). For Claude the launch record alone is not proof (it outlives the process): the attached session must be online and its current generation must have run the hub's hook (`claudePermissionGenerations`). Live local/Pi tool boundaries and a managed headless Claude implementer with a current busy child generation are enforceable. Codex remains conservatively not enforceable because native policy may omit a request; every other peer without a hub pre-tool path says `always-ask not enforceable: <reason>`.
|
|
940
|
+
|
|
941
|
+
- **Remaining adapters (#636 slice 3).** Codex command approval RPCs (v2 command execution, excluding writeStdin, and legacy exec argv) consult the shared gate before native forwarding. A classed request is answered only once, bound to its link/thread/turn; cancellation, disconnect and stale answers deny. Unknown payloads and other approval schemas retain native handling. This proxy cannot intercept a native call that emits no request, so status never claims complete Codex enforcement. Local/Pi tools pass parsed arguments into their existing permit boundary and classify before mode or tool-wide grants. Pi's persisted call receipt encloses the gate, so replay spends no allowance twice. Only verified person-authored idle shell requests retain their existing bypass.
|
|
942
|
+
- **Rendered approvals.** Public/native command views are privacy-screened before rendering. A cut or withheld view offers deny only and cannot authorize even one invocation or a class budget. Local/Pi use their exact full host-generated tool title with a class line on the person's on-prem console, PII turns included; logs, public status, other peers and dashboard projections still withhold it. The same explicit class-budget choice, session/owner revocation and final-answer fence apply to received Codex, local and Pi approvals. Managed Claude forms keep asking through their native prompt path.
|
|
940
943
|
|
|
941
944
|
## Amendment: approval notifications and timeout (issue #5)
|
|
942
945
|
|
|
@@ -4417,7 +4420,7 @@ Stage to role, fixed:
|
|
|
4417
4420
|
- **I-1 (existing).** The reviewer is never the current owner, by id (`routing.ts:494`, `:541`).
|
|
4418
4421
|
- **I-2 (new).** The reviewer is never a peer that implemented this task: any peer with an `accepted` or `done` history entry `by` it on this task, read from `task.history` inside `assign()`. Refusal code `policy`, reason `implemented this task`. When this empties the candidates the reviewer is the person (`reviewBlocked`), never a fallback to an implementer.
|
|
4419
4422
|
- **I-3 (existing, #519).** Other family first; own family only when no other is eligible, recorded as `sameFamilyFallback=true` (`routing.ts:547-550`).
|
|
4420
|
-
- **I-4 (new, enforced in `Tasks.recordPlanReview`, not in `assign()`).** The plan author does not record the plan review of that plan. Plan author: `by` of the latest history entry with `planRevision
|
|
4423
|
+
- **I-4 (new, enforced in `Tasks.recordPlanReview`, not in `assign()`).** The plan author does not record the plan review of that plan. Plan author: `by` of the latest plan-authoring history entry with `planRevision`, then a legacy authoring entry whose `planning.fields` includes `plan`. Exclude `plan reviewed` entries from both paths: review/CI scope bookkeeping never changes the plan-content author. When only the initial proposal remains, use its author only if intent list fields are nonempty; review/CI scope alone leaves the author unknown. A later actual plan edit or explicit intent removal records its own author. If the caller is the author and another planner holder is rostered and not offline, refuse pre-effect ("an independent planner reviews this plan"). If the caller is the author and no other holder is eligible, record the receipt with `selfReview: true` (the single-planner case keeps working).
|
|
4421
4424
|
- **Independent reviewer authority.** Recording a plan review is the one planner act that is not limited to the task's planner: any current settled planner holder other than the plan author may record it, and so may the person. That is what keeps I-4 from deadlocking against R3's per-task authority. The review is the only power this grants: the reviewing holder still cannot plan, split, pull, undo, cancel, reassign or release the task (R3). When the effective planner is the author and an independent holder is eligible, the hub sends that holder a plan-review request (ids only, through `publicView`), chosen by the same rotation as R3 and excluding the author. Unknown author records `planAuthor: null` and does not refuse. The receipt gains `planAuthor`.
|
|
4422
4425
|
- Planner and owner may be the same peer; planner and reviewer may be the same peer (today's default `claude: [planner, reviewer]`). I-4 covers the only conflict between them.
|
|
4423
4426
|
- **Purity.** The planner walk runs inside `assign()` with the same inputs it already takes plus `planners` (sorted settled holder ids) and `namedPlanner`. `Assignment` gains `planner?: PeerId` and `plannerReason?: "rotation" | "named" | "kept" | "none"`. Every caller that routes a new or pulled task reads the planner from this result; `ahub route explain` prints the same walk (`planner candidate X: ...` lines, then `planner: X (reason)`), so explain and assignment cannot drift. `Tasks` never chooses a planner outside `assign()`.
|
|
@@ -4430,9 +4433,10 @@ Stage to role, fixed:
|
|
|
4430
4433
|
- a person-named planner (`namedPlanner`) is exact, refused like a named owner if not a holder (#601 rule);
|
|
4431
4434
|
- otherwise rotation: holders sorted by id, start at `task.id mod holders.length`, walk forward skipping off-roster, offline, paused (`budget_paused = skip_peer`), failing and `routing.allowedRoles`-excluded holders; do not renumber by availability (the #519 seed rule). If all are skipped, take the seed holder with reason `rotation (none available)`.
|
|
4432
4435
|
- PII tasks get `planner: null` and trace `planner: none (pii)`; their planning stays as today (stubs only, person acts).
|
|
4433
|
-
- The choice is written on the `assigned` (or `backlog pulled`) history entry as `planner` and `plannerReason`, through the same metadata path as `reviewerFamily`/`reviewerReason`.
|
|
4434
|
-
- **Effective planner.** `Tasks.taskPlanner(task)`: `task.planner` when it is still a settled planner holder and rostered; otherwise all current holders (legacy). A pause or offline state does not move it; the person moves it.
|
|
4436
|
+
- The choice is written on the `assigned` (or `backlog pulled`) history entry as `planner` and `plannerReason`, through the same metadata path as `reviewerFamily`/`reviewerReason`. A fixed pure host port binds every new proposal, split child and generated backlog card in its original insert transaction, with planner metadata on the initial `proposed`/`backlogged` entry. Notifications never expose an unbound intermediate new row, and split undo keeps its original child snapshot.
|
|
4437
|
+
- **Effective planner.** `Tasks.taskPlanner(task)`: `task.planner` when it is still a settled planner holder and rostered; otherwise all current holders (legacy). A pause or offline state does not move it; the person moves it. Routing explain uses the bound planner as `keptPlanner` and names it as `kept`; a draft or legacy null binding still runs rotation. Explaining either form writes no binding or history.
|
|
4435
4438
|
- **Authority.** `planningAuthority(by)` (`tasks.ts:833`) becomes `planningAuthority(by, task?)`: with a task, `by` must be the person or the task's effective planner; without one (backlog list, release list, new release, grooming read, import) any holder, as today. Every call site that has a task in hand passes it: pull (`:892`), plan (`:1035`), undo (`:1089`), split (`:1148`), assign/configure/cut release (`:1218-1286`, against `releases.planner`), plan review (`:2461`, which follows I-4's independent reviewer rule instead of the task-planner check), planner cancel (`:2247`), and the custom stage actor (`:2511`). Rechecks after awaits keep their places. Grooming reads stay project-wide; grooming, sweep and stall notices (`:397-401`, `:426`, `:2236`, `:938-971`) go to the task's effective planner only.
|
|
4439
|
+
- **Planner lifecycle stage identity.** The effective task planner may record only `stage`, `repo` and `issue` when an explicitly supplied stage has actor `planner` or `owner/planner`. Existing enforce relink and stage/spec gates still apply; advise retains its existing identity relink behavior. This grants no reviewer evidence, checklist, head or deliverable authority.
|
|
4436
4440
|
- **Naming.** `ahub task assign <id> <peer> --role planner` (person only; `TaskAssignmentRole` gains `planner`, `src/hub/tasks.ts:42`; parsers `src/cli/main.ts:1292`, `:2109`; help `src/cli/help.ts:86`). A planner cannot reassign planning to another peer. The write is a `planner changed` entry (reason `named`), which neither moves nor settles the task, so it never counts as supersession of an owner delivery (tasks.md assignment rule). `hub_task_assign` with role `planner` from a peer is refused pre-effect.
|
|
4437
4441
|
- **Release planner.** `newRelease` records `by` as the release's planner (`null` for the person); cut/configure/assign check it like a task's.
|
|
4438
4442
|
- **Migration.** No `planner` key and one planner holder: rotation always yields it. A single pinned planner: same. Old rows and old releases: `null`, legacy authority. Tests that assert a planner tool succeeds for the only planner stay green unchanged.
|
|
@@ -4506,7 +4510,7 @@ Additive optional fields, no `PROTOCOL` bump (daemon.md):
|
|
|
4506
4510
|
- **Planner WIP cap (#655).** Unchanged: `reservedHold`/`acceptanceCapacity` key on the reserved owner and `wipLimit(owner)` (`daemon.ts:1350`, default 1). Each new instance has its own `tasks.wip_limits.<id>`. Planner pull requires the card's effective planner (R3); `plannerRefusal` text stays ids and counts.
|
|
4507
4511
|
- **Budget pauses.** The Claude implementer shares the `claude` account's pauses and readings (I2). A pause holds new deliveries; a running child finishes its turn (not killed by the pause). Rotation skips paused planners at choice time only; a later pause does not move a task's planner. Reviewer pool behaviour is unchanged.
|
|
4508
4512
|
- **Always-ask (#636).** Not a dependency. When #636 lands, its classifier runs in the same PreToolUse hook; an `ask` from the hook in `-p` mode reaches `hub_permission_request`, which carries the class and applies the class budget. Since the headless variant has no `never-ask`, #636 only adds the class label there. #636's adapter table must list both new Claude forms; both have the hook, so neither is "always-ask not enforceable".
|
|
4509
|
-
- **Recovery.**
|
|
4513
|
+
- **Recovery.** Target feature `role_stages` retains its Claude implementer and Codex planner-only instance meaning. Slice 525-2 adds `task_planners` to `DAEMON_FEATURES` and `incompatibleRecoveryTarget` (daemon.md floor rule), required while a `planner` setting is a list or names an instance, or a declared instance holds planner, and while any task or release has a non-null `planner` or plan-review authorship markers remain in its receipt/history. A 0.15.0 daemon already advertises `role_stages` but ignores task/release planner bindings; recovery to it must refuse before any effect. The floor reads bounded SQL `EXISTS` evidence, not only config. Old board parsing tolerates the additive nullable columns without changing legacy null authority (verified with the published 0.15.0 Board parser).
|
|
4510
4514
|
- **Settings.** `planner` stays a guarded settled-holder key (`roleHolders`, `daemon.ts:4431-4435`); a list change goes through the same confirmation.
|
|
4511
4515
|
|
|
4512
4516
|
### Acceptance (from the issue, mapped)
|
|
@@ -4529,7 +4533,7 @@ Additive optional fields, no `PROTOCOL` bump (daemon.md):
|
|
|
4529
4533
|
- Planner walk, `Assignment.planner`/`plannerReason`, I-2, explain lines. No board write of the planner yet (result unused until 525-2).
|
|
4530
4534
|
- ACs: AC-2.
|
|
4531
4535
|
3. **525-2 Per-task planner.** Size L. Depends on 525-3.
|
|
4532
|
-
- Paths: `src/hub/board.ts` (columns, migration), `src/hub/tasks.ts` (`taskPlanner`, `planningAuthority(by, task)`, notices, I-4 in `recordPlanReview`, release planner), `src/hub/settled-roles.ts`, `src/hub/settings.ts`, `src/hub/daemon.ts` (loader, status, `
|
|
4536
|
+
- Paths: `src/hub/board.ts` (columns, migration), `src/hub/tasks.ts` (`taskPlanner`, `planningAuthority(by, task)`, notices, I-4 in `recordPlanReview`, release planner), `src/hub/settled-roles.ts`, `src/hub/settings.ts`, `src/hub/daemon.ts` (loader, status, `task_planners` floor for list/instance planner), `src/cli/main.ts`, `src/cli/help.ts`, verify parser/help parity; leave `docs/verified.json` stamps to the release PR, because this feature PR is squash-merged.
|
|
4533
4537
|
- ACs: AC-5.
|
|
4534
4538
|
4. **525-4 Headless Claude implementer.** Size L. Depends on 525-1 (and 525-2 only for the daemon test's planner).
|
|
4535
4539
|
- Paths: new `src/adapters/claude-implementer.ts`, shared child helper extracted from `src/adapters/reviewer-print.ts`, `src/adapters/definitions/builtin-metadata.ts` (variant), `src/hub/daemon.ts` (start, facts permission for child launch id, tools hello fence, `executionOwners`, `role_stages` floor), `src/hub/claude-state.ts`, `src/cli/reset.ts`, `src/hub/hub-tools.ts` (`hub_permission_request`, published only to this variant's child) and `src/adapters/claude-channel.ts` tools mode (rebuild `plugins/agent-hub/server.js`), `test/fakes/` fake `claude -p`.
|
|
@@ -5040,4 +5044,76 @@ The only artifact identity was GitHub (`repo`, `issue`, `pr`, `head`, `reviewedH
|
|
|
5040
5044
|
- **Approval.** `Tasks.review` re-hashes the delivered files before an approval. Unchanged, the approval carries the same records. Changed, missing or unknown, a reviewer's approval is refused pre-effect (`deliverable changed since it was delivered`); the person may approve the current bytes, which are measured and bound, with a note. The delivered records are rechecked after the awaited hash, so a redelivery during the verdict refuses it.
|
|
5041
5045
|
- **Reads.** `Tasks.deliverableStatus` compares the files as they are now with the approved (else delivered) records: `unchanged`, `changed`, `missing` or `unknown`, and `changedSinceReview` true only for a measured difference. It is added to `task_show` for the console and peers. A file the hub cannot vouch for is `unknown`, never `unchanged`.
|
|
5042
5046
|
- **Privacy.** A record is private for a PII task or a path under `pii.paths` (`matchesPolicy`). `publicDeliverables` is the one projection every peer view, review request and status uses: a private record, or a path matching a PII pattern, shows id, hash and size only. The board keeps the path (as it keeps a PII task's text) so the file can be found again.
|
|
5043
|
-
- **Not here.** `ahub deliverables` and the dashboard
|
|
5047
|
+
- **Not here.** `ahub deliverables` and the dashboard marker (#509 AC3) are the next slice (done in the #509b amendment below). `PROTOCOL` is unchanged: the new tool fields are optional. Known limit: tool input schemas are not enforced on the way in, and a daemon from before this slice reads only `summary` and `refs` from `hub_task_done`, so a newer plugin talking to an older daemon has its `deliverables` silently dropped and the task is recorded without them. The plugin is installed apart from the daemon (docs/agent-notes/daemon.md), so after an upgrade restart the hub before relying on deliverables; a floor like `role_stages` is the upgrade path if that ever matters.
|
|
5048
|
+
|
|
5049
|
+
## Amendment: `ahub deliverables` and the dashboard badge (#509b, #317)
|
|
5050
|
+
|
|
5051
|
+
The second slice of #509 (AC3): a person-facing list of what tasks delivered and what a reviewer approved, and a dashboard marker. Nothing new is stored: both read the records 509a already writes.
|
|
5052
|
+
|
|
5053
|
+
- **Command.** `ahub deliverables [--task <id>] [--approved] [--check] [--json]` is the person's (`identity.ts` classifies it `console`; the daemon's `deliverables` task op sits behind the console gate, so an agent's tools connection is refused; `liveTestRefusal` refuses the delegated form). Each row is a task that has delivered files, newest task first: `#id title [delivered|approved]`, then each file's id, path (only where `publicDeliverables` allows), format, version, size and full sha256. A PII task or a `pii.paths` file shows id, hash and size only, and the title is `publicTitle`. `--task` takes one positive integer (a repeated `--task` is refused rather than keeping the last); `--approved` lists only tasks whose approval bound records, and shows those approved bytes.
|
|
5054
|
+
- **Measured or not.** Without `--check` no file is opened: the list is the board's records, `checked` is false and no row says anything about change. With `--check` every listed file is measured now through `Tasks.deliverableStatus` (the 509a reader, with its limits and its `unknown` for a file the hub cannot vouch for), each file shows `unchanged`, `changed`, `missing` or `unknown`, and the row says `changed since review` only for a measured difference. The row's own reading (`result`) follows the same order: it says `(unchanged)` only when every file did. Consumers of `--json` read `result`: `changedSinceReview` stays false next to `result: "missing"` or `"unknown"`, because it is true only for a measured difference. `Tasks.deliverableList` is the one reader.
|
|
5055
|
+
- **Dashboard.** The snapshot's task rows gain an optional `deliverables: {state, files, checked?}` (`Tasks.deliverableBadge`): the same public projection of the same records, never a file read, because the page polls every second and hashing up to 512 MiB per task on the poll is not acceptable. `checked: {at, result}` appears only when a live read (`task_show`, `ahub deliverables --check`) measured exactly these records; `result` is `changed`, `missing`, `unknown` or `unchanged` for the whole task, and `unchanged` only when every file measured unchanged (a change outranks a missing file, which outranks one the hub could not read, such as every file under an unverified execution root). The page and the CLI say `a file is missing` or `could not be measured` for those, never `unchanged`; `Tasks.deliverableResult` is the one reducer (the cache is keyed by the record ids and hashes, so a redelivery or a new approval clears it). The card shows a `Deliverables n` chip (with `approved` and `changed` when known); the task sheet gains a Deliverables group with each file's path (or `private file`), format, version, size and a twelve-character hash prefix, and says `not measured now: run ahub deliverables --check` when nothing was measured. `PROTOCOL` is unchanged: the field is optional and an older page ignores it.
|
|
5056
|
+
- **Interpretation.** The issue says "a dashboard column". The dashboard's board is kanban columns by task state with no per-task table, so a new column of its own has no place; the chip on the card and the group in the task sheet are the equivalent surface. Say so in review if a table was meant.
|
|
5057
|
+
- **Ceilings.** The dashboard's `changed` is as fresh as the last live read in this daemon's memory (a restart clears it, and a file edited after the last read shows nothing until the next); the command, not the page, is the way to measure now. The list reads the whole board each call (as `ahub board` does); there is no pagination.
|
|
5058
|
+
|
|
5059
|
+
## Amendment: window kinds, source precedence and per-peer budget notes (#285 phase 4)
|
|
5060
|
+
|
|
5061
|
+
- **Kind.** `UsageWindow` gains an optional `kind: "native" | "count" | "manual"` next to `source` (`src/hub/window-kind.ts`). `windowKind` is the one classifier: a named kind wins; an untagged window is `count` when its id is `tokens`, `manual` when its source is `ahub budget set`, else `native`, so a reader that predates the field keeps working. The Kimi count and `ahub budget set` name their kind; the Claude and Codex readers stay untagged and read as native. `PROTOCOL` is unchanged: `kind`, `superseded` and the reply's `peers` and `kimiSource` are optional fields an older client ignores.
|
|
5062
|
+
- **Storage and precedence.** `Budget` keeps readings per window id and kind, so a person's number and the provider's for one window never overwrite each other (and `quota_reset` baselines are per kind). `Budget.gating` decides what the gate and `headroom()` act on: a fresh manual reading replaces the other kinds for its window id; while any native or manual reading is fresh, the count is information only. A stale or reset reading leaves `fresh`, so the count gates again when nothing better is left. `status()` marks such a window `superseded`. A hard provider refusal clears the person's number for its window. `forget(peer, id, kind?)` lets the Kimi count drop only its own reading.
|
|
5063
|
+
- **Notes.** `src/hub/budget-notes.ts` (pure) builds one line per attached peer that shows no number, from the definition and counters, never from a log: `budget.noQuotaReason` (Pi, local) gives `no quota: ...` plus the hub's own 5 h token count for that peer (a `TokenWindow` fed by `addTokens` and seeded from `tokens` events at startup, information only); a peer with a source that has not reported says which; Kimi says by its count state (`KimiUsage.status()`: off, waiting, counting, unreadable with a reason code). The `budget` control reply carries `peers` (the notes) and `kimiSource`; the dashboard snapshot adds `note` and `noteKind` per peer.
|
|
5064
|
+
- **Surfaces.** `ahub budget` prints KIND, SOURCE and MEASURED per window, `info only` for a superseded count, and each note; the console's Peers detail and Usage detail and the dashboard's Usage budgets row show kind and source. The dashboard keeps its own copy of `windowKind` (`windowKindOf`, the page imports nothing); `test/ui.test.ts` runs it against the source. `ahub doctor` adds `kimi usage source` (Hub section) from `kimiSource`; an older hub without it adds no row.
|
|
5065
|
+
- **Not here.** The OAuth probe for Kimi's plan quota (AC9) stays undecided, and the live checks (AC11) stay open.
|
|
5066
|
+
|
|
5067
|
+
## Amendment: amem runtime (agent-mem#13 P4)
|
|
5068
|
+
|
|
5069
|
+
The hub uses exact-pinned @staix/agent-mem 0.7.0 for runtime recall/capture, with no HTTP to the claude-mem worker. The canonical alias-resolved project and selected-profile fence govern every operation. Curated notes remain the evidence for ask and briefs; raw deterministic capture and pending candidates do not become evidence. Explicit task notes use the native store once; auxiliary checkpoint/model answers are candidates. Legacy worker project labels and numeric observation ids are replaced by canonical keys and authentic amem entry ids. Recent recall is curated short-term data, not model-generated tool summaries. Claude-mem is import-only and off by default. Host PII/private-turn/path screening is retained. The supplemental curated view's 4096-row ceiling refuses an oversized view; policy-aware pagination is its upgrade path.
|
|
5070
|
+
|
|
5071
|
+
## Amendment: backlog issue planning and phase grooming (#759)
|
|
5072
|
+
|
|
5073
|
+
- Duplicate candidates exclude two tasks related by directed `deps`/`after` reachability in either direction. No follow-up field or inference from titles, notes or `supersededBy` is introduced. Overlap findings remain unchanged. A bounded public ID/dependency context may include approved intermediate phases; missing/private context is never read as an invented edge.
|
|
5074
|
+
- `hub_backlog_plan` accepts an omitted `fields` or `{}` with the existing bounded reason. It records a note-only `planned` revision with an empty field list; authority, stale revision, pins and frozen-release guards remain.
|
|
5075
|
+
- `fields.sdlc` accepts only `repo`/`issue` on an unstarted, ownerless backlog card, by its effective planner or the person. Null identity members restore a previous absence during undo. Other lifecycle/evidence fields remain unavailable through planning, and ordinary `hub_sdlc_record` still refuses backlog cards.
|
|
5076
|
+
- Issue relinks use the same identity invalidation as `sdlcReceipt`: old spec acceptance and identity-bound evidence cannot survive a different identity. Existing stage is preserved, or the host's initial stage is used when the card had no receipt. The atomic planning write records the new revision and only previous identity members; undo cannot revive an old acceptance receipt. Authority and task/release/privacy state are rechecked after screening.
|
|
5077
|
+
- CLI `ahub backlog plan ... <fields-json> <reason>` uses `{}` for a note and nested `sdlc` for an issue relink. Tool descriptions expose the same contract. Actual cards #138 and #287 are relinked to #525 and #754 only after landing and availability in the running hub, through an authorized planner/person identity.
|
|
5078
|
+
|
|
5079
|
+
## Amendment: the planner board-changed notice (#758)
|
|
5080
|
+
|
|
5081
|
+
The notice "The board changed. Before offering or accepting build work, read `hub_planner_groom` ..." went to every attached planner at every board status revision, including revisions the planner wrote itself, and queued a new copy each time. One planner session received it 12 times in 44 minutes. With the lifecycle on, `flushPlannerNotices` now reads what was added to each card's history since its last scan and decides per planner:
|
|
5082
|
+
|
|
5083
|
+
- **Author skipped.** A history entry whose `by` is the planner raises nothing for that planner.
|
|
5084
|
+
- **Scoped.** Only the card's planners are told (`taskPlanner`, #525-2): a card bound to one planner does not reach another.
|
|
5085
|
+
- **Task weight only when a plan review is needed.** A history entry that carries `planning` (a planning revision or dependency change), a card's creation (a peer's propose, with or without `after`, a split child, a generated card: new work and its dependencies are what a review consolidates) and any change of a card's `deps` since the last scan, whatever wrote it, is a `task` notice. An earlier copy that is still queued for that planner (never attempted or delivered) is withdrawn and replaced, so there is at most one unsettled notice per planner and it names the latest board revision. A settled notice is not replaced.
|
|
5086
|
+
- **The hub's own bookkeeping tells nobody.** An entry by `hub` without `planning` (lifecycle links, idle sweeps, holds, plan observations) neither raises a notice nor spends the `fyi` window.
|
|
5087
|
+
- **`fyi` weight otherwise.** A state move, review or assignment is recorded as an `fyi` task notice (console and log, no turn), at most once per planner per ten minutes (`GROOM_FYI_WINDOW_MS`, bounded by the injected clock `TasksDeps.now`).
|
|
5088
|
+
- **Restart and offline.** The first notice a planner gets after a hub start is task weight, as before. Changes made while a configured planner was offline stay pending for it.
|
|
5089
|
+
|
|
5090
|
+
Ceiling: a scan lists the board once per status revision (at most one per flush); an indexed history read is the upgrade. `PROTOCOL` is unchanged.
|
|
5091
|
+
|
|
5092
|
+
## Amendment: canonical memory symbol anchors (#684 phase 3)
|
|
5093
|
+
|
|
5094
|
+
- Normalize symbol signatures with the phase 1b normalizer and preserve explicit file qualification in the existing `symbol:<qualified>@<path>` format. No amem operation, schema or code index is added.
|
|
5095
|
+
- "Link that task's memory entries" means the new completion note carries screened, current-head, actually changed symbols with positive unambiguous code-facts `exists` evidence. Earlier notes remain immutable and connect through their shared reset-safe task node while the existing short-term completion finding is live. `related(... [symbol] ...)` reaches an earlier note at memory hop two; the package graph's literal two-edge cap reaches the task node. No read-time overlay is part of this phase.
|
|
5096
|
+
- Non-completion notes retain their existing normalized plan-intent associations. Completion notes with unknown observations write no symbols and say unknown, never the plan's guesses. At most 16 candidates are checked; no new check starts after a two-second scheduling allowance, and an in-flight check retains the existing code-facts execution/cleanup bounds. Recheck current privacy, repository/head provenance and the admitted completion observation before writing. PII tasks query and store nothing.
|
|
5097
|
+
- `ahub doctor --stale-anchors [--json]` is read-only, bounded and advisory. Only positive absence at the reported current head means stale. Private, ambiguous, unverified and exhausted evidence stays unknown with explicit coverage/truncation. It never mutates notes or grants authority.
|
|
5098
|
+
|
|
5099
|
+
## Amendment: phase 3 follow-up query and cleanup bounds (#684)
|
|
5100
|
+
|
|
5101
|
+
- Reuse a filled exact-tree symbol-preparation key across prepare-only and completion. Within a pass, memoize unknown and known exists results by query name; preserve privacy/provenance rechecks and optional fail-open results.
|
|
5102
|
+
- Query explicit raw and normalized symbol forms together for historical recall; never copy, supersede or rewrite old notes.
|
|
5103
|
+
- Unsupported `#`, `->` and `:` name forms remain unknown. Final absence rows are labelled "absent from indexed definitions", retaining existing state/reason compatibility; no imported-package existence claim is inferred.
|
|
5104
|
+
- The doctor may use transient detached worktrees. Interrupted cleanup can leave prunable registrations; no automatic prune is added. Repeated owned cleanup failure remains neutral and borrowed handles retain caller ownership.
|
|
5105
|
+
|
|
5106
|
+
## Amendment: memory code-neighbour projection (#684 phase 4)
|
|
5107
|
+
|
|
5108
|
+
- Optional caller/callee edges are computed at read time through code-facts, one hop only, and stamped with the exact current HEAD. Require a unique path-qualified seed; hide ambiguous and name-only edges. Store no code structure or edges in amem.
|
|
5109
|
+
- Dashboard Memory's default is the stored graph. An explicit Code neighbours toggle adds dashed calls and highlights attached notes, fails first; the accessible edge list includes direction and SHA. Prepare asynchronously outside the synchronous snapshot budget, then recheck current policy/profile/HEAD.
|
|
5110
|
+
- Symbol-start `hub_memory_graph` merges the overlay inside the existing node/edge and 6144-byte response caps. Code coverage and truncation are explicit, including unknown evidence. Authored about/rules_out associations remain distinct from derived calls.
|
|
5111
|
+
- Preparation bounds: four seeds, 32 distinct neighbours, 64 call edges, 16 screened notes and a two-second query-start allowance; existing in-flight code-facts timeouts remain. PII tasks/turns query nothing, and names pass publicMetadataText before disclosure.
|
|
5112
|
+
- Native task briefs may prepare asynchronously, merge direct and neighbour-attached fails/decisions first, and preserve their current combined item/line cap, deduplication and fail-open policy. Snapshot the item cap once before selection; reloads apply to the next brief. The console detail reports the bounded selected-note count, not a complete inventory.
|
|
5113
|
+
|
|
5114
|
+
### Round-1 optional-read scheduling bounds
|
|
5115
|
+
|
|
5116
|
+
- Keep screened direct task advice independent of overlay eligibility and validity. Board movement, foreign scope or a private owner turn may drop optional code context but must not drop the existing direct brief. PII tasks still make no optional query.
|
|
5117
|
+
- Default CodeFacts review/completion queries have priority over optional neighbours. Interrupt an active optional CLI, await all existing owned cleanup and then admit priority work; do not run native tree preparations concurrently. Optional unknowns have a bounded five-second TTL and never poison priority cache lookups.
|
|
5118
|
+
- Dashboard code work requires the Memory view to be open and visible. Derive starts share a five-second floor; known topology lives at most thirty seconds, with current note rows refreshed before publication. A single hardened native HEAD probe establishes a bounded HEAD/ref proof; cache checks do not launch Git.
|
|
5119
|
+
- Console task counts use cache-only optional context and return unknown on a miss within the existing task-detail deadline. Snapshot the native brief cap before awaits. Preserve stored nodes when adding derived edges and apply tier/kind/peer filters to neighbour notes; a non-symbol selection resets the toggle.
|