@staix/agent-hub 0.28.0 → 0.29.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 +10 -0
- package/README.md +2 -2
- package/docs/agent-notes/daemon.md +2 -0
- package/docs/agent-notes/local-worker.md +3 -1
- package/docs/agent-notes/tasks.md +3 -1
- package/docs/agent-notes/tests.md +1 -0
- package/docs/design/task-board.md +1 -1
- package/docs/dogfood.md +6 -3
- package/docs/operations.md +73 -11
- package/docs/quickstart.md +11 -3
- package/docs/sdlc.md +1 -1
- package/docs/security.md +17 -2
- package/docs/verification/2026-11-03-0.29.0.md +59 -0
- package/docs/verification/2026-11-03-control-write-stall-windows.json +241 -0
- package/docs/verification/2026-11-03-control-write-stall-windows.md +20 -0
- package/docs/verification/2026-11-03-live-board-latency.json +556 -0
- package/docs/verification/2026-11-03-live-board-latency.md +64 -0
- package/docs/verified.json +13 -13
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +524 -18
- package/src/cli/help.ts +1 -1
- package/src/cli/output.ts +5 -0
- package/src/cli/recovery-package.ts +9 -0
- package/src/hub/board.ts +3 -2
- package/src/hub/control-client.ts +16 -1
- package/src/hub/control-write-trace.ts +409 -0
- package/src/hub/daemon.ts +171 -47
- package/src/hub/hub-tools.ts +1 -1
- package/src/hub/routing.ts +9 -2
- package/src/hub/runtime-capture-prune.ts +4 -1
- package/src/hub/sdlc-release-authorization.ts +13 -4
- package/src/hub/sdlc-release.ts +23 -10
- package/src/hub/sdlc.ts +15 -2
- package/src/hub/task-worktree.ts +331 -0
- package/src/hub/tasks.ts +162 -17
- package/src/local/deny.ts +49 -0
- package/src/local/tools.ts +4 -2
- package/src/memory/brief.ts +5 -0
- package/src/memory/client.ts +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.29.0
|
|
8
|
+
|
|
9
|
+
- The hub makes the task worktree for a hub-sandboxed owner (Pi, the local worker) and commits and pushes its work at `hub_task_done` (#1115, #1127). The commit goes through a private index and a compare-and-swap ref update, only on top of the recorded base or the hub's own last commit, after the changed paths pass the deny and grant policy; a moved branch or HEAD, a refused path or a changed gitlink refuses the commit with its cause. Decline and cancel remove an empty worktree and keep one with uncommitted work.
|
|
10
|
+
- Every write-class task (`implement`, `bulk_edit`) of a hub-sandboxed owner gets that worktree and hub commit, whether or not it declares `needs_worktree` (#1129, #1132). Where the hub cannot make a worktree (no repository, no first commit, a repository config that names a helper, filter or include), such work is not offered to Pi or the local worker, and an accept or a person's assignment is refused with the cause and the next step.
|
|
11
|
+
- The sandbox refuses writes to `refs/**`, `packed-refs`, `HEAD` and a linked worktree's admin dir under every git dir a sandboxed command can reach, discovered submodule git dirs included (#1123, #1130). A sandboxed peer can no longer commit, branch, tag, stash, switch, merge, rebase or reset, nor `git add` inside a linked worktree; the local `git` tool says so in its result and points at `hub_task_done`. What stays writable under `.git`, and why, is listed in docs/security.md.
|
|
12
|
+
- Control-write diagnostics (#1126: #1128, #1133). The daemon records metadata-only spans for board SQL, queue and status, snapshots, memory reads and external calls, plus an event-loop delay probe, within global caps and a five-minute diagnostic expiry, and writes a bounded record to hub.log when a write or the loop is slow. `ahub status --full` shows the counters; client timing on stderr is opt-in with `AHUB_CONTROL_WRITE_TIMING=1`. No timeout, retry or authority changes; the cause of the #1078 stalls is still open.
|
|
13
|
+
- A re-filed `hub_task_done` that names a new head without `refs.commit`, or whose PR moved, is refused instead of keeping the old review head, and the refusal names both SHAs (#1121, #1124).
|
|
14
|
+
- A captured runtime carries the documentation checker, so `ahub sdlc authorize-release` works from a hub started from a capture (#1120, #1122).
|
|
15
|
+
- Pin `@staix/agent-mem` 0.20.0 (was 0.19.0, #1131). Every amem that shares a memory home must be upgraded together: 0.19.0 cannot drain 0.20.0's reboot-stable hook spool records and can retire them after seven days.
|
|
16
|
+
|
|
7
17
|
## 0.28.0
|
|
8
18
|
|
|
9
19
|
- `ahub init` sets the main project settings by flag (#1100, #1065 T1): `--agents`, `--sdlc`, `--coordination`, `--memory`/`--no-memory`, `--snapshots`/`--no-snapshots`, `--pi`/`--no-pi` and, for a new project, `--autonomy inherit|off`. On an existing project only a named flag changes anything, `--dry-run` shows each change as old -> new, and an invalid value is refused with the allowed values.
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Code, Codex, Kimi Code, Pi and a hub-owned local worker (plus Qwen Code, Jcode a
|
|
|
5
5
|
Antigravity if you want them) share a message bus, a task board and a memory, each
|
|
6
6
|
connected through its own native interface.
|
|
7
7
|
|
|
8
|
-
Status: 0.
|
|
8
|
+
Status: 0.29.0, control protocol 17 (accepts 16). Bun + TypeScript, one loopback daemon per
|
|
9
9
|
project. Site: [agent-hub.staix.net](https://agent-hub.staix.net).
|
|
10
10
|
|
|
11
11
|
## Quick start
|
|
@@ -135,7 +135,7 @@ uses the home must run a version that understands the new layout.
|
|
|
135
135
|
[#258](https://github.com/STAIxBWLB/agent-hub/issues/258)).
|
|
136
136
|
|
|
137
137
|
Live checks that still need a real account or a person are listed in the
|
|
138
|
-
[release verification report](docs/verification/2026-11-
|
|
138
|
+
[release verification report](docs/verification/2026-11-03-0.29.0.md) and the
|
|
139
139
|
[smoke checklist](docs/smoke.md).
|
|
140
140
|
|
|
141
141
|
## Documentation
|
|
@@ -24,6 +24,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
24
24
|
- The plugin bundle is installed apart from the daemon. Breaking a control WS message contract bumps `PROTOCOL` in `control-client.ts`; a new request answered as unknown or an optional field preserves it, as does additive optional public task metadata in `ui_snapshot` that older clients ignore (such as `deps` in #246).
|
|
25
25
|
- `hub_invoke` over the control WS (#445, #991): the daemon binds each admission to its socket and `rid` with an `AbortController` (`invocationRequests`, at most 64 per connection; a duplicate `rid` refuses). `request_cancel` and socket close abort it, and every check after an await confirms the socket is open and still maps that `rid` to the same controller before any effect. Clients go through `ControlClient.request`, whose `controlRequestTimeout` raises a `hub_invoke` wait to `INVOCATION_CONTROL_TIMEOUT_MS`; never call it with a shorter bound.
|
|
26
26
|
- Control write outcomes (#1106, `src/hub/tool-request.ts`): `toolWriteOperation` is the one list of writes whose transport result can be lost (`hub_task_propose`, `hub_review`, `delivery_complete`). After a timeout, a cancel or a closed socket, `ControlClient` answers `outcome: unknown` with the settling read (`toolWriteUnknownReply`), never a plain failure. Replay goes only through `ToolWriteRegistry`, keyed by `requestId` and `toolWriteScope`, with one bucket per actor. `toolWriteCurrent` rechecks authority and privacy before a retained result is returned. The channel strips MCP `request_id` before `taskOp`, and `Tasks` never reads it. An explicit key is refused before sending to a hub without `tool_write_idempotency`. `delivery_complete` keeps the journal's idempotence and never uses the registry. A new retryable write joins `toolWriteOperation`; never add a second key path.
|
|
27
|
+
- Control-write diagnostics (#1126, `src/hub/control-write-trace.ts`): `onControlMessage` wraps the original `onMessage`, keeps its name for the native-branch ratchet, and returns to it untraced before any argument traversal for a client that is not admitted. Privacy guards are factories over primitives (`controlWritePrivacyGuard`), raw-input screens are passed as lazy admission-only callbacks and never retained, and an expired trace is settled and suppressed without touching the operation. Never add a timeout, retry or authority decision to this path; a new boundary is one more closed `ControlWritePhase`, and a test that extracts these closures binds the real guards (tests.md).
|
|
27
28
|
- `peer_stop` is the console's or an admitted conductor's (`admitOperational(sock, "stop")`) and stops, through its adapter, only an owner whose adapter reports `hubOwned === true` and whose `form` is not `tui` (today headless ACP, Pi, local, print-reviewer and Claude implementer); a present `undefined` form refuses. Refuse native-terminal peers and recovery-held mutations; serialize with starts and permission changes, and refuse a recovery `prepare` while a stop runs. An admitted stop holds delivery through `BusOptions.deliveryHeld` before any teardown await (never a console pause that resume can undo, or a global recovery hold that blocks unrelated peers) and withdraws the peer's pending approvals before the adapter stops. A requested stop holds an in-flight delivery as `needs_review` with an ids-only stop reason, suppresses restart/escalation, and never resolves that delivery automatically.
|
|
28
29
|
- Console review guidance (#711): `r` on a task in review opens `reviewChoice` (`src/cli/console-review.ts`, modelled on `modeChoice`) from the row or the open task detail, never a bare prefill; `a` and `c` prefill `review N approved|changes_requested ` with a guide line, a `changes_requested` line with no note is refused in `reduceConsole` before any effect, and the result of a review command is shown as a notice in panels mode (`reviewResult`). The console only prefills and runs `ahub review` as the person: never grant it extra authority, and keep a private task's title out of the choice line (`[pii]`). The CLI cause line (`agentShellCause`, `src/cli/identity.ts`) is printed for the `tools` identity and a person-only hub refusal only, never for the console identity, and never replaces the refusal.
|
|
29
30
|
- Console changed-symbols block (#684 2b): `symbolFields` (`src/cli/console-symbols.ts`) adds `symbols` after `plan` for a task_show-shaped detail only, from the public `codeSymbols`; never from a raw `Task`, never for a private task, never from a stored receipt (one with `repositoryKey` reads as unknown: only the screened public receipt is shown), never as a second copy of the match rule (`planSymbolMatches` in `review-brief.ts`). Unknown is `unknown (no code facts)`, never an empty list.
|
|
@@ -102,6 +103,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
102
103
|
- `native_tui_launch` (#464) is console-only and resolves only a running owned `JcodePeer` under its own kind id (`tuiLaunch`). Refuse it while stopping, during recovery, off roster, during that peer's roster change or requested stop, or when `jcodeMcpReady` fails against the daemon's `SERVER_JS`; a `binding` must name the current `launchId` and `sessionId`. Failures answer fixed errors with details withheld (a `JcodeOptionMismatch` names only the flag in `option`, #559) plus one closed cause `code` (a `JcodeTuiRefusalCode`, or `hub_busy`, `not_running`, `mcp_stale`, `bad_request`), logged as one `hub.log` line (#1093), and the launch tuple (socket, env) goes only into that console reply, never into `status`, `status.json` or a log. A launcher sends it only when the hub's `features` list `native_tui_launch`.
|
|
103
104
|
|
|
104
105
|
- When preserving a recovery source, copy only the exact-pinned `@staix/agent-mem` package through `preserveRuntimeDependencies` in `src/cli/recovery-package.ts`; include its identity and bytes in `packageDigest`. Never copy the whole node_modules tree or link a preserved source to a mutable installation. Verify preserved sources with `ownDependencies: true` so a missing or symlinked memory copy cannot resolve an ancestor installation. Verify staged releases with `dependencyRoot` bound to their retained install directory; never resolve beyond it. Keep no-dependency release digests compatible.
|
|
106
|
+
- Release checker in captures (#1120, 2026-11-02): `scripts/check-docs.mjs` is the one file outside `RUNTIME_ASSETS` that a capture carries (`RELEASE_CHECKER` in `src/cli/recovery-package.ts`, copied only when the source has it and refused when it is not a regular file), and `captureLayout` in `src/hub/runtime-capture-prune.ts` accepts an optional `scripts/` holding exactly that file, so captures made before #1120 still prune. It is deliberately not in `packageDigest`: widening the digest makes every already-staged release refuse as "cached release changed". `readDocEvidenceDetailed` returns `{ unavailable }` for a missing or failing checker and `ReleaseAuthorizations` turns that into its own refusal; only evidence that was read and is not fresh may say "stale documentation". Keep the spawn arguments of `readDocEvidenceDetailed` as they are (seed `release-committed-doc-metadata`).
|
|
105
107
|
|
|
106
108
|
- Render staleness only through the call-scoped privacy preflight and bounded metadata probe (`memory-staleness.ts`); on a tree not modified during the call, private/denied/observed-symlink/unknown paths never produce a marker. Retain the explicit same-user concurrent-swap ceiling in docs/memory.md and the probe-loop ponytail. Keep hints out of stored rows and route native brief decoration after selection so one batch shares its probe budget.
|
|
107
109
|
|
|
@@ -6,7 +6,7 @@ Scope: the hub-native local worker, its tools, path guard, denylist and seatbelt
|
|
|
6
6
|
- The headless Claude host wrapper (#845, `HostShellWrapper` in `src/hub/claude-shell-wrapper.ts`) runs its read-only and check/test/build grants under this same `profile()` plus a deny-only refinement (`src/hub/task-shell-sandbox.ts`, and the hub state dir): it repeats `sandboxedExec`'s own-temp allow suffix and its `readOnlyData` broad-write guard, so change both places together, and keep the refinement deny-only. The wrapper and the read-only shell proof take their fixed environment from `hostShellEnvironment` (`src/local/shell-environment.ts`), which must equal the baseline `sandboxedExec` builds inline (`test/claude-shell-wrapper.test.ts` compares them); never inherit a native agent's environment there.
|
|
7
7
|
- A timeout or abort in `sandboxedExec` marks the result `(killed: <signal>, timeout?)` from its own `killRequested` flag, never only from the exit signal: a shell may report its killed child as a normal exit 137.
|
|
8
8
|
- The sandbox denies home reads by default (toolchain dirs, the project and its real git dir excepted) and all network, loopback included: claude-mem and the Codex app-server listen on loopback without auth. Tests that bind a local port therefore fail when the worker runs them, also with `local.bash_network: true` (the egress proxy keeps loopback closed, and the proxy variables send a test's plain-HTTP `fetch` to the proxy, which refuses it); that is the intended trade-off, and `"direct"` is the switch until a later release removes it.
|
|
9
|
-
- The seatbelt profile refuses the `.git` name itself at any depth (creation, rename, symlink, gitfile), `config`, `config.worktree` and `hooks` directly under it, `commondir` at any depth below it or below an external git dir, and `.agenthub` anywhere in the project, every name folded against the 16 code points HFS+ ignores (#270). Every `modules/` tree the project can reach (the common dir's, the worktree's own, every linked worktree's under `<common>/worktrees/<wt>/modules/` as a pattern so a tree or worktree that does not exist yet is covered, #518, and each discovered submodule's own `modules/`) refuses EVERY write that is not inside a discovered submodule git dir (#520): nothing is exempt by name, so a fake git dir called `refs`, `logs` or anything else cannot be planted for a later unsandboxed `git submodule update --init` to reuse. Inside each discovered git dir `D` the refusals are anchored to `D`: `D/config`, `D/config.worktree` (folded in both halves), `D/hooks` and what is below it, and `D/worktrees/*/config.worktree`, with `D/worktrees`, each `D/worktrees/<name>` and `D/hooks` refused as directory entries too, so none can be moved out, edited and put back; a submodule's own linked worktrees keep a `modules/` of their own at `D/worktrees/<name>/modules`, which gets the same everything-not-discovered refusal by pattern (so one that does not exist yet is covered) and has its `modules` entry refused as a target; `D/
|
|
9
|
+
- The seatbelt profile refuses the `.git` name itself at any depth (creation, rename, symlink, gitfile), `config`, `config.worktree` and `hooks` directly under it, `commondir` at any depth below it or below an external git dir, and `.agenthub` anywhere in the project, every name folded against the 16 code points HFS+ ignores (#270). Every `modules/` tree the project can reach (the common dir's, the worktree's own, every linked worktree's under `<common>/worktrees/<wt>/modules/` as a pattern so a tree or worktree that does not exist yet is covered, #518, and each discovered submodule's own `modules/`) refuses EVERY write that is not inside a discovered submodule git dir (#520): nothing is exempt by name, so a fake git dir called `refs`, `logs` or anything else cannot be planted for a later unsandboxed `git submodule update --init` to reuse. Inside each discovered git dir `D` the refusals are anchored to `D`: `D/config`, `D/config.worktree` (folded in both halves), `D/hooks` and what is below it, and `D/worktrees/*/config.worktree`, with `D/worktrees`, each `D/worktrees/<name>` and `D/hooks` refused as directory entries too, so none can be moved out, edited and put back; a submodule's own linked worktrees keep a `modules/` of their own at `D/worktrees/<name>/modules`, which gets the same everything-not-discovered refusal by pattern (so one that does not exist yet is covered) and has its `modules` entry refused as a target; `D/logs`, `D/objects` and `D/index` stay writable, and since #1123 `D/refs`, `D/packed-refs`, `D/HEAD` and `D/worktrees` are refused like the project's own (`gitRefRules` gets the discovered dirs), so commit, branch, tag and rebase fail in a submodule too, and a submodule really named `refs` or `logs` is protected like any other. `hooks` is anchored by path depth (`hooksUnder`), never by an unanchored any-prefix name, which also matched a project under a directory named `hooks`. The worktrees container and each worktree git dir are refused as directory entries too (otherwise an ancestor can be moved out of the protected path, edited and moved back), and so are the discovered git dirs, the modules root and the components in between (rename, replace, symlink, recreate). Keep every regex literal under 1024 bytes (UTF-8 bytes, not characters), and never put the git path and a folded name in one literal: the root is its own `require-all` filter, so the folded literals are the same size at any path length (`test/sandbox.test.ts` asserts it). Known residuals: a nested repository in bare layout (the hub never runs git there; docs/security.md), folded spellings of the components between `.git` and a submodule's git dir on a volume that folds names (the 1024-byte SBPL literal cannot hold a folded path; external git dirs carry the same residual since #270), and the submodules of a repository nested inside the project (only the project's own common dir is discovered). An external git dir's `config`/`config.worktree`/`hooks` rules, and the pattern rules for every linked worktree git dir under `<common>/worktrees/`, cover only its own children plus the hooks directory's contents: at any depth they would deny refs named `config` or `hooks` in a worktree or submodule project. The fold is a group alternation, never a character class (the seatbelt regex engine is byte-oriented and a quantifier after a multi-byte class member binds to its last byte), and every pattern keeps its `^` anchor (an unanchored starred alternation does not match). Folded literals never contain the project root: an SBPL string literal dies at 1024 bytes. The cost: `git init`, `git clone` and `git worktree add` do not run inside the sandbox, and neither do `git worktree remove`, `git worktree prune` of a stale record, `git clean -ffd` with a nested repository, `git submodule update --init` (its git dir's `config` write is refused), or `rm -rf` of a directory holding a `.git`.
|
|
10
10
|
- The shared object store `<common>/objects` takes only NEW loose objects from a sandboxed command (`objectStoreRules` in `src/local/deny.ts`, #864): every write under `objects/info` (alternates, http-alternates, commit-graph) and `objects/pack` is refused, so are the `objects`, `objects/info` and `objects/pack` entries, the unlink or rename of a fan-out directory, and data, unlink, mode and flags writes to an existing loose object. Keep loose-object creation allowed (sandboxed `git add`, `commit`, `stash` need it) and never reopen packs or `objects/info` for a sandboxed command: git reads packs before loose objects and never rehashes on read, so either one replaces content that every checkout and the #845 host add/commit/push trust. `test/sandbox.test.ts` "(#864)" proves it with the real sandbox-exec. The cost: `git gc`, `repack`, `fetch`, `maintenance` and `commit-graph write` run outside the sandbox. A hardlink to an existing loose object is refused too (into the project and into the command's own temp dir); keep that case in the test. Residual: an object id that does not exist yet can be planted before git writes it; a local checkout then shows the planted bytes, and a push of that commit is rejected by the receiving side, which rehashes what it receives ("missing necessary objects"). The upgrade path is host-side git writes as in #845.
|
|
11
11
|
- Build every git invocation outside the sandbox through the single implementation exported by exact-pinned `@staix/agent-mem/platform` (`memoryGitArgv`/`memoryGitEnv`/`memoryGitSync`), re-exported as `hubGitArgv`/`hubGitEnv`/`hubGitSync` by `src/hub/git.ts`. Keep the git export identity, behavior and direct-spawn tripwire checks in `test/hub-git.test.ts` passing; retain home compatibility and Node-safe process-entry checks in `test/memory-parity.test.ts`. The sandboxed git tool in `src/local/tools.ts` retains its own behavior. Use `{ credentials: true }` only for the memory repository the hub creates itself, never a project's repository. Use the core deny policy re-exported by `src/local/deny.ts` for guards, sandbox rules and capture; do not add another denylist.
|
|
12
12
|
- SBPL strings go through `sbplString` (the plain `"..."` form). In the raw `#"..."` form a backslash escapes nothing, so a `"` in a path ends the literal and the whole profile fails to parse.
|
|
@@ -28,3 +28,5 @@ Scope: the hub-native local worker, its tools, path guard, denylist and seatbelt
|
|
|
28
28
|
- The `bash` tool asks `ctx.permit` with the frozen input, then `ctx.shellExecutionCheck` right before the sandbox spawns (#816): `valid` runs, `ask` repeats the permit once with `{ forceAsk: true }`, `aborted` or a cancelled signal returns "turn cancelled before shell execution". `LocalWorker` binds the check to its turn generation, so a stale turn cannot spawn. A fake `ctx` in a test supplies `shellExecutionCheck` or accepts the `valid` default; never make it return `valid` for an aborted turn.
|
|
29
29
|
- `web_fetch` (#515b, `src/local/web-fetch.ts`) is opt-in like `doc_read` (never in `TOOL_SCHEMAS`; offered only when `local.web_fetch.allow` names a host) and carries the owner's PII rule: it is refused for the WHOLE turn of a PII task, whatever the host, and that refusal is the first thing `webFetch` does, before the URL, the list or the proxy is read, so the answer and the timing do not depend on the request. Never move another check in front of it, and never give it a way to accept a PII turn "for a clean URL": the URL or a query could carry PII to the web, which `pii.paths` and `sanitize()` do not cover. It runs only through its OWN `startEgressProxy` (`webEgress` in `daemon.ts`, list `local.web_fetch.allow`), never the bash proxy: do not merge the two lists or reuse the bash proxy for it. The model sets only `url`, `offset` and `limit`; keep the request a bare GET with no model-chosen header, body or method, keep redirects manual and re-checked, and keep the result and the URL out of `capture.observe` and the research observation on every turn. The pure host checks live in `src/local/web-fetch-config.ts` because the config schema reaches the Claude channel bundle: keep that file free of file-system and process imports.
|
|
30
30
|
- `doc_read` (#515a, `src/local/doc-read.ts`) is an opt-in tool, never part of `TOOL_SCHEMAS`: the worker offers `DOC_READ_SCHEMA` only when the daemon passes `tools.docRead` (`local.doc_read`, machine-local, never editable from settings), and `runTool` refuses it without the port. It goes through `guardPath` (read), then `pii.paths` by path, then the converter (`TextProjections`, the person's `text_projection`), then `signals.pii_patterns` by text, then `sanitize()` and the output cap; a PII task turn passes the first two screens, an ordinary turn does not. Its result is never passed to `capture.observe` or `onTool`: keep both exclusions when you touch the tool loop in `src/adapters/local-worker.ts`.
|
|
31
|
+
- Hub-made task worktree (#1115, 2026-11-02, `src/hub/task-worktree.ts`; rules for `Tasks`: `tasks.md`): the sandbox profile is NOT widened for it. Since #1123 a sandboxed command can no longer write `.git/refs/**` or `.git/worktrees/<name>/**` (next line), but the hub does not rely on the profile and still treats the worktree admin dir and the branch ref as peer-writable: it records the base commit at creation (in `<state>/task-worktrees.json`, outside every sandbox root) and commits only when the worktree is still registered at its path, HEAD is the task branch and the tip is the recorded base or the hub's own last commit. `test/task-worktree-sandbox.test.ts` pins both halves with the real profile, and fails if the profile text changes when the hub makes a worktree. Never say in docs or errors that the sandbox cannot write `.git`: say what it cannot (gitfile, config, hooks, commondir, packs, and since #1123 refs, packed-refs, HEAD and worktree admin files); the main index, logs and new loose objects stay writable.
|
|
32
|
+
- Refs, HEAD and worktree admin writes (#1123, 2026-11-03, `gitRefRules` in `src/local/deny.ts`): the profile refuses `refs/**`, `packed-refs*`, `HEAD*` and `worktrees/**` directly under a `.git` and under each external git dir, so a sandboxed commit, branch, tag, stash, switch and `git add` inside a linked worktree fail by design, and the hub commits instead (#1115). Keep every rule adjacent to its git dir: an unanchored `refs` or `worktrees` filter would match a project under `~/worktrees/` (`test/sandbox-git-refs.test.ts` runs projects below directories named `refs`, `worktrees`, `HEAD` and `packed-refs`). `.git` + `packed-refs` and `.git` + `worktrees` do not fit one 1024-byte literal (803 and 669 bytes folded), so they use a front literal plus a back literal, and the external-dir depth anchor falls back to the unanchored name past about 1000 bytes. A submodule's own git dir gets the same refusals (person's ruling 2026-11-03: deny `.git` writes, submodules included): `gitRefRules(gitDirs, subDirs)` takes the discovered submodule git dirs, so `test/sandbox-git-refs.test.ts` covers a submodule, a nested one and a linked worktree's. A submodule change by a sandboxed peer cannot be committed by the sandbox, and the hub's commit records a submodule only as a commit id: edits inside a submodule's working tree stay uncommitted there, and a changed gitlink refuses the whole hub commit (#1115), so that part needs a person or a vendor agent. A new test that needs a commit under the real profile makes it with the hub's git outside the sandbox, as `test/sandbox.test.ts` now does.
|
|
@@ -68,6 +68,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
68
68
|
- `Tasks.review` captures `reviewAuthority` once and rechecks it in `valid()` after every await, right before each write; a headless reviewer's verdict, and its `hub_task_show` (checked before and after the read), holds only while the same child runs for that task at its bound envelope head and review revision (`Tasks.reviewBindingCurrent`, host-only: `in_review`, same reviewer, non-PII, matching `reviewRequestRevision` and review head). Never cache the answer (#223, #533).
|
|
69
69
|
- Review scope (#852, `src/hub/review-scope.ts`): a reviewer reads `full` (code, affected tests, CI) or `code` (code only). The default is data on the fixed reviewer variants (`AdapterDefinition.reviewer.scope`, `code` for the sandboxed codex and kimi variants, fixed-builtin-only like `instances.modelSelect`); a fresh-reviewer instance may override it with `review_scope` from `config.local.json` only (tier 3, never from a tracked file or the dashboard). `TasksDeps.reviewScope(peer)` is the one read. `sendReview` replaces the Hub-verified, check-result and no-check lines with `CODE_SCOPE_LINE` for a `code` reviewer, which requires every code defect as a structured finding with an id. Reserve findings for defects in the reviewed code, docs or tests; missing check/CI/test-run/PR-or-branch identity evidence goes in `unmet`, never `findings` (Amendment 2). The verdict rule is structural (amended AC3): a `code` reviewer's `changes_requested` is always recorded, never refused, and never classified by reading its unmet text; never add a text classifier (background: README). If its structured `findings` carry no item with disposition `open`, the history entry is `evidenceOnly: true`: it adds nothing to `rejections` (so it cannot escalate), records no failed outcome, contradicts no earlier approval, writes no recurring-finding candidate, is not a rework round in the SDLC metrics, still returns the task to its owner as usual, and sends the configured conductor one ids-only task notice (`evidenceOnlyNotice`; with no conductor, one console notice). A verdict with an open finding counts as today. A person's and a `full` reviewer's verdict are never evidence-only. A `code` reviewer's verdict history entry carries `reviewScope: "code"` and a `full` reviewer's carries nothing, so old rows are unchanged. Merge rule: a task approved by a `code` reviewer is merged by the conductor or the person only after exact-head CI is green and no review thread is unresolved, and the merge note names the CI run.
|
|
70
70
|
- Owner review re-file (#1000, #1047): admit a replacement `hub_task_done` while `in_review` or approved and unmerged only for a different full lowercase SHA through the existing owner, privacy, lifecycle and completion-check gates. Fence the original review revision through every await; same-head, active-check and superseded submissions refuse before effects. Clear stale artifact-head evidence without minting reviewer consent, retain current-head local evidence, open Important findings and rejection counts, and publish a fresh request. A configured check returns the task to `in_progress` until completion. Every verdict pins its initial review request revision across awaits, including off mode with no explicit head. On an approved unmerged task, allow only the reviewer/person to withdraw approval with `changes_requested`; retain Important findings and leave that state for the owner. Persist each verdict head in history and preserve a legacy receipt-only approval head before dropping stale evidence. Refuse merged/cancelled reopens and unknown linked-PR merge state; re-read merge state after other awaits before the reopening write. Require a reviewer after a re-file, including configured-check completion. Project approval binding through `task-approval.ts`; merge queue, decision, dispatch and cleanup must compare it with the latest reported head.
|
|
71
|
+
- Stale heads on re-file (#1121): a re-filed `hub_task_done` after a rejection or re-accept must name the new full SHA in `refs.commit`; summary text is never head proof. A linked PR whose head moved, or whose head cannot be read (bounded head-only read over `SdlcGh`), refuses the re-file and an approval at the old head, naming both SHAs. Owner-submitted full refs outrank host-derived PR receipt heads; revalidate role, task and head after every async read.
|
|
71
72
|
- Keep structured review findings in verdict history, bound to the reviewed head. Validate bounded reports and person-only accepted dispositions before effects; recheck authority, privacy and head identity after awaits. A supplied `head` that differs from the task's resolved head (`findingHead`) is refused pre-effect, and the resolved head is part of the identity rechecked after the screen (#657). Key findings by `id` alone, never by pass (`findingsForHead`): a nit or open reclassification never discharges an Important, only an explicit disposition does. Omitted IDs never settle findings; carry open Important across heads until explicitly dispositioned, while nits stay head-local. Use the shared valid-head resolver for projections. Project text through publicView, and deduplicate recurring public pass/id pairs into backlog proposals through durable `Review finding key:` markers (a pair recurs only across separate work: tasks sharing a PR, a branch in one repository or a supersession count once, `workUnits`, #736; a task counts only where a review that is not withheld recorded the finding's id, whatever its pass, as open or accepted, never where it is only recorded fixed, #328) read only from hub-proposed `plan` cards (first history entry by `hub`), without editing instruction files.
|
|
72
73
|
- Recurring unmet criteria (#514a, `src/hub/learning.ts`): grouping is pure (rows in, groups out); `Tasks.proposeCriterionCandidates` is the only caller and runs beside `proposeRecurringFindings`. It files a backlog card with ids and counts and the `profile_overrides` fragment as text, never applies it, and counts only rows that `candidateTextAllowed` passes (no PII). Its dedupe is the `Criterion candidate key:` line; keep it first in the detail. `ahub learn` (#514b) is the person's verdict on those cards: `parseCandidateCard` reads only that first line (and the hub-authored `plan` card, and the title that must match it), the task count and the line are recomputed from the board (`learnEvidence`, `overridesDiff`), and the card closes through `Tasks.cancel` only after `settingsSet` wrote the one managed key `LEARN_CHECKLIST` (never before, never by a direct board write); every `Tasks.learn*` method refuses any `by` but the person, and a new learn op never joins `CONDUCTOR_TASK_OPS`. Rewording the card's key line or title changes the parser in the same change.
|
|
73
74
|
- Plan conformance (#481 S2, `src/hub/plan-conformance.ts`): compare only literal repository-relative `plan.paths` entries (file or directory prefix, no glob) with complete PR file metadata; `completePlanInventory` requires a full head, `pathsComplete` and `typesComplete` (false on any rename or copy). Root, absolute-unverified or unsafe entries, a missing plan, a truncated inventory and a failed read are `unknown`, never covered; never read patches or the filesystem. An untouched planned path is advice; a changed path outside the plan is a departure, and `planConformanceBlockers` blocks enforce approval on `unknown` and on an unaccepted departure. Plan `proof` and `risks` (32 strings of 512 characters) cover no file. `Board` records `planRevision` only while `trackPlanRevisions` (mode not off), separate from backlog `planningRevision`.
|
|
@@ -105,7 +106,8 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
105
106
|
- Backlog issue planning (#759): allow only `fields.sdlc.repo`/`issue` on unstarted ownerless backlog through `Tasks.planBacklog`, scoped to the effective planner/person; a PII card's identity is person-only and a public one must pass `publicMetadataText`, both pre-effect. Keep arbitrary lifecycle receipts refused on backlog. Reuse lifecycle identity invalidation, keep the planning write atomic and recheck after awaits. Store identity-only undo values; never revive old spec acceptance. Omitted/empty planning fields with a bounded reason record a note-only revision and retain normal pins/frozen/stale guards.
|
|
106
107
|
- Grooming duplicate candidates (#759): suppress only pairs related by directed dependency reachability, either direction, using bounded public ID/dependency context. Include approved intermediate phases when available; a walk that hits its bound marks the digest truncated. Keep overlap findings, unrelated same-issue duplicates and privacy filtering; never infer follow-ups from prose or supersession text.
|
|
107
108
|
|
|
108
|
-
- Declared needs (#1081, 2026-11-02): `needs_network` and `needs_worktree` are task signals set only by an explicit `true` in `Tasks.propose` (never inferred from text). `assign()` in `routing.ts` reads each as a refusal code (`network`, `worktree`) against `PeerCapabilities.network` / `.worktree === false`, and `Tasks.declaredNeedCheck` repeats both at accept, before any write: that check is what stops a sandboxed peer from accepting work, failing at `git worktree add` (#270) and leaving an empty branch ref (#621, #565). `peerCapabilities` in `daemon.ts` is the only producer: unsandboxed peers are `true` for both, hub-sandboxed peers have `worktree
|
|
109
|
+
- Declared needs (#1081, 2026-11-02): `needs_network` and `needs_worktree` are task signals set only by an explicit `true` in `Tasks.propose` (never inferred from text). `assign()` in `routing.ts` reads each as a refusal code (`network`, `worktree`) against `PeerCapabilities.network` / `.worktree === false`, and `Tasks.declaredNeedCheck` repeats both at accept, before any write: that check is what stops a sandboxed peer from accepting work, failing at `git worktree add` (#270) and leaving an empty branch ref (#621, #565). `peerCapabilities` in `daemon.ts` is the only producer: unsandboxed peers are `true` for both, hub-sandboxed peers have `worktree` from `TaskWorktrees.available()` (#1115: true only where the hub can make the worktree, else false with its cause and next step) and `network` from `sandboxNetwork !== false` (the one `local.bash_network` setting). Unknown (no entry) is never a refusal. Keep the four lists together when you add a need: `RoutingRefusalCode`, `TASK_STALL_CODES`, the `requireCapabilities` filter and `declaredNeedCheck`.
|
|
110
|
+
- Hub-made task worktree (#1115, 2026-11-02, `src/hub/task-worktree.ts`): `Tasks.accept` calls `hubWorktreeFor` right before `acceptanceCapacity` and the accept write, only for a hub-sandboxed owner (`TaskWorktrees.applies`), a non-PII task, and write-class work (`writesRepository(task.class)` in `routing.ts`: `implement` and `bulk_edit`, #1129) or a declared `needs_worktree`, and clears it again (`undoWorktree`) when anything throws before the write. `Tasks.decline` and `Tasks.cancel` call `releaseHubWorktree` after their board write. The daemon's `hub_task_done` case calls `tasks.worktreeHandoff` BEFORE `tasks.done` and passes its `{ commit, branch }` refs on, so `Tasks.done` itself is untouched and its head gates see an ordinary full-SHA refile; keep the hook out of `done`. The sandbox profile is not changed by this hook. Since #1123 it refuses ref, HEAD, packed-refs and worktree-admin writes, so a sandboxed peer cannot commit at all and the hub's commit is the only one; still never trust what is under `.git`, and commit only when the branch tip equals the recorded base or the hub's own last commit (the record lives in `<state>/task-worktrees.json`, which no sandbox root contains), because a rule can be loosened later and a same-user process is outside the sandbox. Since #1129 the hook covers every write-class task, so a hub-sandboxed owner of `implement` or `bulk_edit` work always has a hub worktree and a hub commit; keep `writesRepository` the one definition that routing and `Tasks` both read.
|
|
109
111
|
- Paged `hub_task_list` (#1083, 2026-11-02, `src/hub/task-list-page.ts`, the `hub_task_list` case in `daemon.ts`): an agent (`by !== USER`) always gets `{tasks, total, next?}` and the console gets the bare array unless it names `limit`, `cursor`, `ids` or `detail`; keep that split, because `ahub board`/`ahub tasks` and `test/cli-json-guard.test.ts` read the array. Build every row with the existing `listRow` (it starts from `tasks.publicView`, so PII stays a stub and the `board-column-started-projection` seed text is unchanged) and only then reduce it with `compactRow`; never build a compact row from the raw `Task`. The cursor is a keyset (`updated.id`), not an offset. A new caller of `hub_task_list` as a peer reads `.tasks`, or passes `ids` for one task. Do not raise the 200 cap or the default 50 without re-running the 600-task size test.
|
|
110
112
|
- Urgent accept and the WIP refusal (#1080, 2026-11-02): `acceptanceCapacity` takes an `urgent` mode, and only `accept` passes `"displace"`; `acceptReviewed` and the early check in `accept` pass `"check"`, which never writes. `displaceOffers` runs immediately before the accept write, after every gate (spec, plan, study), so a refused accept can never have taken back an offer. It takes back only offers the owner never accepted (`displaceable`: state proposed, owned, not pinned, not urgent, no `taskHasStarted`), newest handed first, and only as many as the limit needs; if they are not enough it takes none and refuses. The refusal comes from `wipSplit` and names accepted, offered and reserved separately, with a next step. Do not widen `displaceable` to accepted or urgent work, and do not call the displace mode from any other path.
|
|
111
113
|
- Planner WIP breakdown (#784 AC3, `Board.plannerWipBreakdown`, `Tasks.wipBreakdown`): a read-only split of `plannerWip` into `active`, `reviewWait` and `reserved` for status, the console Peers detail and the dashboard legend. Read it from ONE grouped query for every peer (the status snapshot builds on a hot path); never loop `wip()` per peer or per task there. Admission, `reservedHold`, `acceptanceCapacity` and the planner pull keep reading `wip()` / `plannerWip` unchanged, and the three parts must add up to it: change the two queries together. The new `taskWip` fields are additive; renderers keep printing the plain count when an older hub sends none.
|
|
@@ -4,6 +4,7 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
|
|
|
4
4
|
|
|
5
5
|
- A fake's gated action that never sees its gate skips the action; never fall through to a late, unsynchronized action. Owned-group tests inject `AcpOwnedGroupPorts`; a test never signals a process group it did not spawn.
|
|
6
6
|
- When extracting production closures into a VM fixture, bind every referenced host port and actual imported policy helper. Keep native identity checks on real unstarted instances; never replace a policy predicate with an always-true stub or weaken existing assertions to accommodate a moved dependency.
|
|
7
|
+
- A daemon fixture that routes write-class work (`implement`, `bulk_edit`) to a hub-sandboxed peer (Pi, `local`) runs in a throwaway repository made with `gitProject()` (`test/fakes/git-project.ts`), never in `ROOT`: the agent-hub checkout carries the machine's git config (a hosted runner's checkout has an `includeIf` credentials entry the hub refuses), so such a test passes on one machine and fails on another, and it would make a hub worktree and `task/*` branch inside the checkout under test (#1129). Before calling a hosted-only failure a flake, reproduce the runner's condition in a scratch clone.
|
|
7
8
|
- A fixture that walks a policy table (a `COMPAT` entry list, a registry) derives its per-entry values from the table, never a fixed list of keys or versions a planned entry is not in: the compat watch test builds every served version and the ahead/unknown row from `COMPAT[id]`, so an added entry (`pi`, #843 AC2) is covered without editing the test.
|
|
8
9
|
- Tests that are not about batching build the bus with `batchMs: 0`; with the default 15 s window a lone status envelope looks like a lost message.
|
|
9
10
|
- A test that writes an executable shim and then times a product call through it (a `git` wrapper on `PATH`, as `test/memory-code-head-684.test.ts` does) runs the shim once by absolute path first, outside the measured region: the first exec of a newly written executable costs about 250 ms on macOS (#519). Never widen the product's bound or weaken the assertion for it.
|
|
@@ -71,7 +71,7 @@ Urgent acceptance can reclaim only enough unaccepted, unstarted, non-pinned and
|
|
|
71
71
|
|
|
72
72
|
## Completion, review and evidence
|
|
73
73
|
|
|
74
|
-
Completion binds the submitted full head and any verified deliverable records. It may run a configured check before requesting review. A review records a verdict against that evidence; it does not itself merge a PR. An unmerged approved task can receive a new owner submission, superseding the earlier review binding rather than treating old approval as authority for changed code ([#1047](https://github.com/STAIxBWLB/agent-hub/issues/1047)).
|
|
74
|
+
Completion binds the submitted full head and any verified deliverable records. It may run a configured check before requesting review. A review records a verdict against that evidence; it does not itself merge a PR. An unmerged approved task can receive a new owner submission, superseding the earlier review binding rather than treating old approval as authority for changed code ([#1047](https://github.com/STAIxBWLB/agent-hub/issues/1047)). A re-file cannot silently inherit an old head after the linked PR moves, and an approval with no supplied head refuses known-stale evidence. Refusals name the recorded and supplied or read heads and direct the owner to re-file `hub_task_done` with the new full SHA in `refs.commit`; summary text is not head evidence. This prevents stale approval from being presented as review of changed code ([#1121](https://github.com/STAIxBWLB/agent-hub/issues/1121)).
|
|
75
75
|
|
|
76
76
|
Review independence and declared scope are different controls. Selection excludes conflicts using current implementation history; a `code` reviewer supplies code findings, while full test/CI proof belongs to the merger's gate. Evidence-only inability to run tests is not a code defect. This keeps valuable sandboxed review without inventing CI evidence or accepting self-review ([#744](https://github.com/STAIxBWLB/agent-hub/issues/744), [#852](https://github.com/STAIxBWLB/agent-hub/issues/852)).
|
|
77
77
|
|
package/docs/dogfood.md
CHANGED
|
@@ -13,7 +13,7 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
|
|
|
13
13
|
| Distinguishable sessions and sender provenance (#1036, `session_identity`) | on after release and upgrade: a connection that names a peer id but is not that peer's current session is a distinct `<peer>~<origin>` session that can send and read, holds none of the peer's roles and replaces nobody; the channel plugin binds to its own project; every envelope and attach event records the connection. Nothing is refused to a connection that worked before. The recovery label `session_identity` applies once an unsettled delivery holds a session's message | `grep '"session":true' .agenthub/state/events.jsonl` lists every message a foreign session sent, with its minted id and no path or text; a recipient's header shows `from "claude~<id>"` for it; test/identity-provenance.test.ts (AC1, AC3, AC4, recovery label) and test/identity-channel-project.test.ts (AC2, real plugin against a new and an old hub) hold it. Not authentication: a same-user process can always act as the user |
|
|
14
14
|
| Cross-project task handoff with state follow-back (#604, `task_handoff`) | on after release and upgrade: `ahub task handoff` and `hub_task_handoff` register work for another registered project that runs agent-hub on this machine as an ownerless proposal on its board; the requesting peer is told each state change as a `kind: task` message. Nothing is queued, and the origin has no authority there | `ahub board --handoffs` lists what this project handed over with the last state each target reported; `ahub board` and `ahub task show` in the target name the origin project and peer; test/handoff-daemon.test.ts runs two real daemons (AC1 to AC5 and the recovery label), test/handoff-core.test.ts and test/handoff-board.test.ts hold the pure core and the tables |
|
|
15
15
|
| Staged project upgrades (#1039) | off: available for the person to upgrade projects independently; this development hub is not upgraded by a fixture | test/published-protocol-compat.test.ts uses the published 0.25.0 client/plugin and reverse daemon; test/staged-upgrade.test.ts checks independent holds and scoped effects; test/staged-runtime-daemon.test.ts checks a real release-root daemon after mutable install removal; test/board-schema-guard.test.ts checks named pre-effect refusal and WAL metadata; `test/project-runtime.test.ts` and `test/project-runtime-lifecycle.test.ts` cover updated resolver retention/refusal and real lifecycle/dashboard routing with synthetic captured daemons; `test/runtime-capture-prune.test.ts` covers references, bounded retention and publication races; `test/upgrade-stopped-project.test.ts` verifies no preservation for stopped projects. These fixtures do not prove published pre-0.26 CLI behavior |
|
|
16
|
-
| Approval head binding and re-review (#1047) |
|
|
16
|
+
| Approval head binding and re-review (#1047, #1121) | source/fixture coverage; the #1121 stale-head protections and actionable refusals await release and upgrade; live re-file verification not run | `test/review-head-binding.test.ts` covers both-head refusals, moved-PR commitless re-files and omitted-head stale approvals; `test/task-review-refile.test.ts` covers owner re-submit, reviewer withdrawal, legacy verdict heads, merged/cancelled refusals and held-screen races; `test/sdlc-queue.test.ts` and `test/sdlc-merge.test.ts` reject stale approval at queue and dispatch. `ahub task show <id> --json` exposes `approval.head`, `latestHead`, `mismatch` and `current`; verdict history retains `reviewedHead` |
|
|
17
17
|
| Default agent set started by `ahub up` (#608, `start.agents`) | off here: this repository's agents are started by their own terminals; it is a machine-local setting and a committed config cannot turn it on. On for a person who runs `ahub setup` and `ahub up` on a project that lists agents. | each `ahub up` prints one line per agent (started, already attached, or the refusal with the manual command); the `peer_start` replies in `hub.log` and the terminal provider's recorded starts show whether a listed agent attached, and `ahub status` shows its state after |
|
|
18
18
|
| `ahub down` (counterpart of `ahub up`) | on: an alias of `ahub kill` for stopping this project's hub | the person stops the hub with `ahub down` and `ahub status` then reports it stopped; test/identity.test.ts keeps it person-only (`console`) and labelled `down`, and test/help.test.ts holds its help entry |
|
|
19
19
|
| Encrypted rooms (#446 X1) | off; source-only crypto/wire, with no broker, room link, invitation CLI or delivery enabled | real WebCrypto vectors and negative wire fixtures; atomic memory fake ports establish ordering only; durable X3 state and the X7 two-machine person receipt remain unverified |
|
|
@@ -87,7 +87,7 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
|
|
|
87
87
|
| Merge-time PR readback, release metrics and security-scan baseline (#481 S7, AC12) | source and fixture coverage with a fake gh and real train transitions; the readback is on wherever auto merge is `on`, release metrics need shipped trains with recorded history, and the security scan and its bands stay off here until `[security_scan]` and `[bands.security_open_alerts]` are set | `cost` on merge records, `ahub sdlc metrics` review/CI-at-merge rows, the releases row of `ahub sdlc release`, `security_scans` rows and `security_open_alerts` cards |
|
|
88
88
|
| Required checks | advise (`check (ubuntu-latest)`, `check (macos-latest)`, `seeded guards`) | first-pass CI rate, same-head reruns (`hub_sdlc_costs`) |
|
|
89
89
|
| Seed anchor diagnostics (#684) | on in the seeded checker; exact-match failure remains mandatory | missing anchors report the base commit, enclosing symbol and current file/line or explicit unknown; `test/seeded-check.test.ts` |
|
|
90
|
-
| Release authorization (#481 S6, #1063) | fixture coverage; delegated live consent awaits release and project upgrade | `test/sdlc-release-authorization.test.ts` and flow fixtures retain exact candidate/docs/origin/restart bindings and record the actual user/conductor actor; agent-read and floor fixtures refuse other peers, private turns and live-test; no fixture authorizes a live release |
|
|
90
|
+
| Release authorization (#481 S6, #1063, #1120) | fixture coverage; delegated live consent awaits release and project upgrade | `test/sdlc-release-authorization.test.ts` and flow fixtures retain exact candidate/docs/origin/restart bindings and record the actual user/conductor actor; agent-read and floor fixtures refuse other peers, private turns and live-test; `test/runtime-capture-checker.test.ts` proves a real capture carries the documentation checker and that a missing or failing one is refused as its own cause, not as stale documentation; no fixture authorizes a live release |
|
|
91
91
|
| SDLC metrics and control bands (#481 S7) | `ahub sdlc metrics` on; bands off until `[bands]` is set in `.agenthub/sdlc.toml`, and they write cards only in advise or enforce (in off the report still shows tiers). Completion checks are off here, so `first_pass_check` and the PR-based figures read unknown | its own report; breach episodes per week by tier, and band cards the planner accepted rather than dropped |
|
|
92
92
|
| Fresh headless reviewers (#223, #519) | configured here (`codex-review`); per-tool approval correction and no-verdict diagnostics/re-offer (#783) take effect after release and upgrade | reviews by fresh sessions, findings they add; a per-review start failure names a closed cause class (#722): `ahub status` reviewer line and hub.log `[cause: ...]`, with the share of failures that are `unknown`; stale admission skips and current-policy `no_verdict` reroutes leave the reviewer available (#781); a real `hub_review` verdict per verified backend/version, beyond inventory-only proof; bounded refusal diagnostics, confirmed re-offers without queue holds, and uncertain fallback holds |
|
|
93
93
|
| Completion checks per class | off: the implementer already runs `scripts/check.sh`; a hub-run copy would double the heavy-slot use | time to verdict when on |
|
|
@@ -139,7 +139,9 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
|
|
|
139
139
|
| Peer terminals open in the target project's Orca worktree (#1089) | on wherever a TUI start opens a terminal through Orca; fixture coverage only (a fake `orca` CLI answers `worktree list` and records every call: a start from another project's terminal, refusals for no match, a parent or child root, an archived entry, two matches, a cut-off list and an Orca failure). Run against a real second project in Orca is unverified and not run here (no live panes) | hub.log lines `the hub opened no watch pane (Orca has no worktree whose root is this project's root ...)` and `ahub start` refusals that name the same cause, and no `ahub <peer>` failing its project-root readback after a hub-made start |
|
|
140
140
|
| Crash-recovery notices that clear (#1082) | on after release and upgrade, for the first crash a hub recovers from after that; fixture coverage only (`test/crash-notices.test.ts`: a real daemon over a planted crashed run, peers attached through the bus, rows resolved through the control path). A real `kill -9` recovery on a project with live peers is not recorded here | the `crash` lines of `ahub status --json` after every peer is attached and the held rows are resolved (expected: the headline only); the headline count against `ahub queue list` for the crash's own rows; any `run ahub ... again` line still showing next to an attached peer |
|
|
141
141
|
| Urgent accept takes back unaccepted offers; WIP refusal splits accepted from offered (#1080) | on after release and upgrade; fixture coverage only (`test/urgent-wip.test.ts`: a real `Tasks` over a real board, an owner at its limit with one accepted task and offers). A live urgent task for a busy owner is not recorded here | task history: `unassigned` by `hub` with a `displaced by urgent task` note; the planner notice `was taken back from`; any `planner WIP capacity` refusal that still reads as one undivided list of open tasks |
|
|
142
|
-
| Declared needs keep work from a peer that cannot do it (#1081: `needs_network`, `needs_worktree`) | on after release and upgrade, for tasks proposed with `needs_network` or `needs_worktree` (or `--needs-network`, `--needs-worktree`); fixture coverage only (`test/routing-network-needs.test.ts` and `test/routing-worktree-needs.test.ts`: routing, `route explain`, explicit assignment, a reservation, accept with a rostered fake Pi and Codex, and a real git repo showing a refused accept creates no branch ref; `test/capabilities-network-worktree.test.ts`: a real daemon's per-peer capabilities). A real Pi or local worker offered flagged work is not recorded here
|
|
142
|
+
| Declared needs keep work from a peer that cannot do it (#1081: `needs_network`, `needs_worktree`) | on after release and upgrade, for tasks proposed with `needs_network` or `needs_worktree` (or `--needs-network`, `--needs-worktree`); fixture coverage only (`test/routing-network-needs.test.ts` and `test/routing-worktree-needs.test.ts`: routing, `route explain`, explicit assignment, a reservation, accept with a rostered fake Pi and Codex, and a real git repo showing a refused accept creates no branch ref; `test/capabilities-network-worktree.test.ts`: a real daemon's per-peer capabilities). A real Pi or local worker offered flagged work is not recorded here. A hub-made worktree for a sandboxed peer (#1115) is covered by fixtures only (`test/task-worktree.test.ts`: real git, a bare origin, the hub's commit and push, a moved branch or HEAD refused, the path policy, decline and cancel cleanup; `test/task-worktree-sandbox.test.ts`: the real `sandbox-exec` profile, unchanged, with the peer editing and the hub's commit after (the peer's own commit failing is asserted in `test/sandbox-git-refs.test.ts`, #1123); `test/task-worktree-tasks.test.ts`: accept, refusal, decline, cancel and the done hand-off through `Tasks`), so it is on after release and upgrade, for flagged work offered to Pi or local in a git repository with a GitHub origin, and its metric is a Pi or local task whose branch was committed and pushed by the hub and reviewed, with no `.git` write attempt from the sandbox (expected: none) and no empty `task/*` refs left by a declined or cancelled one | `ahub route explain` lines `skipped, no network` and `skipped, no worktree` for Pi or local; Pi or local declines of work that needed the network or a worktree (expected: none once the task is flagged); empty branch refs left by a declined Pi or local task (expected: none for flagged tasks); unflagged tasks that were declined for a missing worktree or registry, which show the planner is not flagging them |
|
|
143
|
+
| Sandbox refuses `.git` ref, HEAD and worktree writes (#1123) | on after release and upgrade for the local worker and Pi (hub sandbox); fixture coverage under the real `sandbox-exec` only (`test/sandbox-git-refs.test.ts`: the main checkout, a hub-made worktree, a worktree beside its project, a bare repository's worktree, a submodule, a nested one and a linked worktree's, folded names, projects below directories named like the rules; `test/sandbox.test.ts` and `test/tools.test.ts` rewritten where they asserted a sandboxed commit). It needs macOS, so the Linux gate skips it. A real Pi or local worker task is not recorded here; since #1129 a sandboxed task of a write class gets the hub's worktree and commit whether or not it is flagged | a Pi or local write-class task that ends with uncommitted edits in the main checkout (expected: none, the hub made it a worktree), the local `git` tool's `the sandbox does not let you move a branch` note in turn results, and any `Operation not permitted` on a `.git/refs`, `HEAD` or `worktrees` path from a command that should not need it |
|
|
144
|
+
| Hub worktree and commit for every write-class task of a hub-sandboxed owner (#1129) | on after release and upgrade, for `implement` and `bulk_edit` work offered to Pi or the local worker in a git repository, whether or not the task says `needs_worktree`; fixture coverage only (`test/task-worktree-tasks.test.ts`: an unflagged implement and bulk_edit task get the worktree at accept, the hub commits it at `hub_task_done`, decline and cancel clear it, read-only classes and PII tasks get none, a self-claim makes it; `test/routing-worktree-needs.test.ts`: write-class work is not offered where the hub cannot make a worktree and the trace names the cause, read-only classes are not refused; `test/pi-daemon.test.ts` runs a real Pi accept in a repository). A real Pi or local worker task through a whole accept, edit and done is not recorded here | write-class tasks owned by Pi or local whose `hub_task_done` carries a hub commit and branch (expected: all of them); `ahub route explain` lines `skipped, no worktree` for Pi or local on write-class work outside a git repository (expected there, with the cause), write-class tasks that stay with Claude or Codex because the hub could not make a worktree (the cost of this rule: watch whether it starves Pi), and empty `task/*` refs left by a declined or cancelled one (expected: none) |
|
|
143
145
|
| Paged `hub_task_list` (#1083) | on after release and upgrade, for agents (an agent session's `ahub board` follows the pages); the person's console and `ahub board` from a terminal keep the bare array. Fixture coverage only (`test/task-list-page.test.ts`: a real daemon with 600 seeded tasks; `test/board-agent-paged.test.ts`: the real `ahub board` and `--columns` under an agent identity over a two-page board). A real conductor session reading the live board is not recorded here | the size of a conductor's `hub_task_list` result on the live board (expected: under 64 KB, a `next` cursor while more exist); a page walk that ends with no task repeated; `ahub board` run from an agent shell listing as many tasks as `ahub board` from a terminal; any peer or script that still fails parsing `hub_task_list` as a bare array |
|
|
144
146
|
| `ahub init` guided prompt (#1065 T2) | on wherever a person runs `ahub init` at a terminal on a new project with no flag and no `--yes`; fixture coverage only (the real CLI under a scripted pty in a temporary HOME: `test/init-prompt.test.ts`). A person answering it on a real new project is not yet recorded and not run here | `ahub init` runs by a person that end with `wrote ...` after the plan, against runs that end `nothing was written` (N, Ctrl-C, Ctrl-D, three bad answers); no `Write this?` line in any log of a peer, live-test or piped run; the settings a person changes from the default, per question |
|
|
145
147
|
| `ahub init` settings flags (#1065 T1) | on wherever a person runs `ahub init`, new or existing project; fixture coverage only (the real CLI against scratch projects and a scratch hub home). The guided prompt (T2) and the specs rows (T3) are not built, and a person using the flags on a real project is not yet recorded | each `--dry-run --json` `settings` row is a change a person asked for (key, file, old and new value), `ahub init` refusals name the allowed values, and `git diff` of `.agenthub/config.json` after an init with flags shows only the named keys |
|
|
@@ -151,6 +153,7 @@ agent-hub is developed with agent-hub. This page lists every feature, whether it
|
|
|
151
153
|
| --- | --- | --- |
|
|
152
154
|
| `ahub init --specs/--no-specs/--spec-review` and person-only `ahub specs stamp` (#1064 T3) | on after release: a new or existing project gets the specs block by default; no repository design specs or stamps are recorded with it here yet; fixture coverage only (real CLI against scratch repositories: `test/init-specs.test.ts`, `test/specs-stamp.test.ts`, `test/check-docs-parity.test.ts`). A person stamping a real spec in a terminal is unverified and not run here (no live terminal session) | `ahub init --dry-run --json` `settings` rows for `specs.enabled` and `specs.review`; the specs block present in `AGENTS.md` of projects where `specs.enabled` is true and absent where it is false; `specs stamp` runs that end with `stamped N documents` against refusals for uncommitted covered paths; no `verified-against` change authored by an agent (`git log -p` on spec files) |
|
|
153
155
|
| Per-call write outcomes (#1078) | source/fixture coverage; installed-hub activation and the reported live 35-45 s stall remain unverified | `test/tool-request.test.ts`, `test/control-client.test.ts` and real daemon/channel fixtures count one effect per request ID, preserve pending joins and reject changed scope/payload; the 600-task synthetic p95 receipt is `docs/verification/2026-11-02-control-write-outcomes.md` |
|
|
156
|
+
| Control-write timing and event-loop delay (#1126) | source and isolated fixture coverage; live activation and cause of the reported stall remain unverified | `test/control-write-trace.test.ts` pins metadata/privacy/caps, lazy screens, five-minute diagnostic expiry and completed-span correlation; `test/control-write-telemetry.test.ts` checks admission before scanning, forces a blocked real daemon write and checks phase logs/status counters; historical UTC windows are in `docs/verification/2026-11-03-control-write-stall-windows.md` |
|
|
154
157
|
| Recipient settlement and inbox reads behind held rows (#1079) | source/fixture coverage; installed-hub activation and a live busy-peer settlement remain unverified | `test/recipient-settlement.test.ts` checks busy own-row completed/discard with reason and revision, foreign-row refusal and queued reads behind an unchanged held row; pause/recovery/lifecycle fences remain covered |
|
|
155
158
|
| Design spec inspection and task notices (#1064 T1/T2) | source/fixture coverage only; no repository design specs configured yet; dogfood drafts follow in later phases | `test/specs.test.ts` checks graph/freshness and empty-project silence; `test/specs-tasks.test.ts` checks report/require, head-bound acknowledgements and privacy; committed-head inspection fixtures exclude dirty/main-checkout bypasses; adoption metrics are the counts of `spec not updated` reports, acknowledged reasons and unchanged-spec refusals |
|
|
156
159
|
| Screened room chat and person inbox (#446 X4/X5/X5b) | default off here; requires person enrollment/exposure; console panel 8 and dashboard Agents remain metadata-only; person CLI alone can show one inbox body; real two-machine/account leg remains unverified | signed completed (admitted) versus immutable completed (inbox); once-only inbound count, body-free metadata/list JSON, framed/control-stripped person show, done/discard/expiry purge and fixed stored-byte cap/age refusals, post-ack retries; 1,100-row complete-list ceiling; no broadcast/roles/board/body telemetry, key/storage creation from status or mixed-version success claim; existing restart/binding/disposition regressions |
|
package/docs/operations.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Operations guide
|
|
2
2
|
|
|
3
|
-
This guide describes ahub 0.
|
|
3
|
+
This guide describes ahub 0.29.0 and control protocols 16 through 17. Live verification
|
|
4
4
|
results and remaining prerequisites are recorded separately in [the smoke ledger](smoke.md).
|
|
5
5
|
|
|
6
6
|
## Agent-operated live verification
|
|
@@ -1097,15 +1097,23 @@ routing, a reservation, `ahub task assign` and `hub_task_accept` all refuse it b
|
|
|
1097
1097
|
A hub-sandboxed peer (Pi, the local worker) has none while `local.bash_network` is off: `no network: local.bash_network
|
|
1098
1098
|
is off ...`.
|
|
1099
1099
|
- `needs_worktree` is for work that must run in its own git worktree on a task branch (the repository rule). A
|
|
1100
|
-
hub-sandboxed peer cannot
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
the hub
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1100
|
+
hub-sandboxed peer cannot run `git worktree add`, because it writes under the git common directory and runs outside
|
|
1101
|
+
the sandbox by design (#270), so since #1115 the hub makes the worktree for it at `.worktrees/<peer>-<task>` on the
|
|
1102
|
+
branch `task/<task>-<peer>`, before the accept write. The peer edits files there and does not run git; at
|
|
1103
|
+
`hub_task_done` the hub stages, commits and pushes the branch. The capability is true only where the hub can make
|
|
1104
|
+
the worktree. Otherwise it is false and the refusal says why and what to do: `no worktree: the hub cannot make a task
|
|
1105
|
+
worktree for it: the project is not a git repository; run git init there, ...`. A refused accept creates nothing,
|
|
1106
|
+
so no empty branch ref is left behind (the Pi accepts of #621 and #565 left two). Declining or cancelling removes
|
|
1107
|
+
the worktree and its empty branch; one with uncommitted changes is kept and reported. The sandbox profile is not
|
|
1108
|
+
changed (docs/security.md, "Hub-made task worktree").
|
|
1109
|
+
|
|
1110
|
+
A peer whose capability the hub cannot tell is never refused on this ground. Work that declares no need is routed as
|
|
1111
|
+
before, with one exception (#1129): write-class work (`implement`, `bulk_edit`) is treated as needing a worktree
|
|
1112
|
+
whether or not it is flagged, because a hub-sandboxed owner cannot commit (#1123) and the hub commits in its worktree.
|
|
1113
|
+
Such work is never offered to a hub-sandboxed peer on a project where the hub cannot make a worktree, and
|
|
1114
|
+
`ahub route explain` says `no worktree` and the cause; `test`, `summarize`, `triage`, `review` and `plan` get none,
|
|
1115
|
+
and neither does a PII task. The other flags are a person's or a planner's statement about the work, so an unflagged
|
|
1116
|
+
task can still reach a peer that cannot meet them.
|
|
1109
1117
|
|
|
1110
1118
|
Network is off unless `local.bash_network` says otherwise. With `true` it goes
|
|
1111
1119
|
only through the hub's egress proxy on a loopback port (0.11): commands get
|
|
@@ -1245,6 +1253,60 @@ its delivery ID and generation; it has no request key argument.
|
|
|
1245
1253
|
The latency receipt and its synthetic-only scope are in
|
|
1246
1254
|
[control-write verification](verification/2026-11-02-control-write-outcomes.md).
|
|
1247
1255
|
|
|
1256
|
+
|
|
1257
|
+
## Control write timing and loop delay (#1126)
|
|
1258
|
+
|
|
1259
|
+
After upgrading to a release containing #1126 and restarting through the normal
|
|
1260
|
+
upgrade workflow, `ahub status --full` shows aggregate control-write timing
|
|
1261
|
+
counters. An older hub omits them. They are process-local diagnostics, reset at
|
|
1262
|
+
restart, not durable success receipts or a count of unique business effects.
|
|
1263
|
+
This feature does not change a request timeout or make a retry safe.
|
|
1264
|
+
|
|
1265
|
+
A slow tracked operation (at least 1,000 ms on a monotonic clock) writes a
|
|
1266
|
+
`control_write_slow` JSON record to hub.log. The record contains a closed request
|
|
1267
|
+
kind, an optional public task number, elapsed time, outcome and phase durations.
|
|
1268
|
+
`returned`/`threw` describes the handler, not business success or whether a write applied.
|
|
1269
|
+
Client-side timing is opt-in with `AHUB_CONTROL_WRITE_TIMING=1` in the client or
|
|
1270
|
+
MCP server environment. Its slow records go to stderr, never stdout, and omit task
|
|
1271
|
+
numbers because the client has no authoritative privacy view. No request/rid,
|
|
1272
|
+
actor/session identifier, path, task text, argument, external output or error
|
|
1273
|
+
message enters a timing record. Host privacy is checked again before logging.
|
|
1274
|
+
An unresolved task identity (including a noncanonical string id) suppresses daemon
|
|
1275
|
+
diagnostics while the operation keeps its normal validation and behavior.
|
|
1276
|
+
|
|
1277
|
+
The daemon samples its event loop every 100 ms. A missed tick delayed by at least
|
|
1278
|
+
250 ms can write `control_write_loop_delay`, including bounded observed spans.
|
|
1279
|
+
Recently completed synchronous intervals are retained so a span that blocked the
|
|
1280
|
+
loop can be identified after its callback has returned. An open asynchronous span
|
|
1281
|
+
is only a concurrent wait, not proof that it occupied the CPU; untracked occupancy
|
|
1282
|
+
is reported as unknown. Background snapshot, queue/status and memory boundaries
|
|
1283
|
+
use the fixed `background` kind rather than inventing a tool request.
|
|
1284
|
+
|
|
1285
|
+
Per observer, retained work is capped at 128 operations, 256 active span registrations, 512
|
|
1286
|
+
recent intervals and 8 emitted phase/span items. Phase totals use only the closed
|
|
1287
|
+
phase set; a full active-registration map never discards a finished phase or recent
|
|
1288
|
+
synchronous interval. There is no per-request cutoff that can hide a late block.
|
|
1289
|
+
The slow and loop categories each allow at most one line per second, with a shared
|
|
1290
|
+
60-line/minute ceiling; each JSON record is at most 2,048 UTF-8 bytes. Dropped work
|
|
1291
|
+
and logs are counted. Normal ring rotation and settle-time cleanup are excluded
|
|
1292
|
+
from the full-status dropped total. A stopped probe cannot be started again. Nested durations are not additive. Status counters include
|
|
1293
|
+
background observations and do not identify a blocking cause on their own.
|
|
1294
|
+
|
|
1295
|
+
Unanswered diagnostic requests expire after five monotonic minutes, reclaiming
|
|
1296
|
+
their operation and span slots without cancelling the underlying work or changing
|
|
1297
|
+
its eventual result. Full status lists `expired` separately from capacity/log drops
|
|
1298
|
+
and normal ring rotation. Argument screening starts only after socket admission;
|
|
1299
|
+
model-input diagnostic screens run once only when tracing can admit the observation.
|
|
1300
|
+
|
|
1301
|
+
Board-write, queue/status, memory and external-call phases locate an observed
|
|
1302
|
+
boundary; they do not weaken its existing authority, privacy or execution gates.
|
|
1303
|
+
The probe is unref'd and stopped at daemon shutdown or failed startup. The
|
|
1304
|
+
[historical window correlation](verification/2026-11-03-control-write-stall-windows.md)
|
|
1305
|
+
separates one 60-second all-event gap from review waits with continuing periodic
|
|
1306
|
+
callbacks. The instrumentation is intended to diagnose the next naturally
|
|
1307
|
+
occurring stall; it has not been run against the live daemon by that task.
|
|
1308
|
+
|
|
1309
|
+
|
|
1248
1310
|
## A two-agent task and review
|
|
1249
1311
|
|
|
1250
1312
|
Choose an owner explicitly, then inspect the reviewer selected by the project
|
|
@@ -3390,7 +3452,7 @@ version is older than the selected running daemon; it installs nothing.
|
|
|
3390
3452
|
| Runtime | Control protocol window | Legacy status field |
|
|
3391
3453
|
| --- | --- | --- |
|
|
3392
3454
|
| 0.12.20 through 0.25.0 | 16 only | `protocol: 16` |
|
|
3393
|
-
| 0.26.0 through 0.
|
|
3455
|
+
| 0.26.0 through 0.29.0 | 16 through 17 | `protocol: 16`, `protocolMin: 16`, `protocolMax: 17` |
|
|
3394
3456
|
|
|
3395
3457
|
New clients choose the highest common protocol. The runtime supports N-1, so the
|
|
3396
3458
|
published protocol-16 client and channel plugin remain usable; unrelated feature
|
package/docs/quickstart.md
CHANGED
|
@@ -17,7 +17,7 @@ ahub setup # installs the channel plugin and a pr
|
|
|
17
17
|
Or use the matching GitHub release:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
bun add -g github:STAIxBWLB/agent-hub#v0.
|
|
20
|
+
bun add -g github:STAIxBWLB/agent-hub#v0.29.0
|
|
21
21
|
ahub setup
|
|
22
22
|
```
|
|
23
23
|
|
|
@@ -31,6 +31,14 @@ move a running project to a newer release later, run `ahub upgrade` in a
|
|
|
31
31
|
terminal in that project: it reviews the plan with you, applies it and follows
|
|
32
32
|
it; other projects keep their version.
|
|
33
33
|
|
|
34
|
+
## Upgrading to 0.29.0
|
|
35
|
+
|
|
36
|
+
Upgrade every `amem` that shares your memory home together with the hub: the hub now exact-pins `@staix/agent-mem` 0.20.0, and an `amem` 0.19.0 hook or CLI cannot drain 0.20.0's new hook spool records and can retire them after seven days (#1131). Formats, shards, candidates and indexes are unchanged.
|
|
37
|
+
|
|
38
|
+
A hub-sandboxed peer (Pi, the local worker) can no longer commit, branch, tag, stash, switch, merge, rebase or reset, nor `git add` in a linked worktree: the sandbox refuses writes to refs, HEAD, packed-refs and worktree admin files (#1123). Instead the hub makes a task worktree for its write work (`implement`, `bulk_edit`) at accept and commits and pushes it at `hub_task_done` (#1115, #1129). Where the hub cannot make one (no git repository, no first commit, or a repository config that names a helper, filter or include), that work is not offered to Pi or the local worker, and an assignment to them is refused with the cause; run `git init` and make a first commit, or give the work to Claude, Codex or Kimi.
|
|
39
|
+
|
|
40
|
+
`ahub status --full` shows control-write timing counters, and a slow control write or a blocked event loop leaves a metadata-only record in `hub.log` (#1126). Set `AHUB_CONTROL_WRITE_TIMING=1` for client-side timing on stderr.
|
|
41
|
+
|
|
34
42
|
## Upgrading to 0.28.0
|
|
35
43
|
|
|
36
44
|
`ahub init` now takes flags for the main project settings (#1100, #1107): `--agents <a,b,...|none>`, `--sdlc off|advise|enforce`, `--coordination advisory|turn-free`, `--memory`/`--no-memory`, `--snapshots`/`--no-snapshots`, `--pi`/`--no-pi`, `--specs`/`--no-specs`, `--spec-review off|report|require` and, for a new project only, `--autonomy inherit|off`. On an existing project only a flag you give changes anything, `--dry-run` shows each change as old -> new, and an invalid value is refused with the allowed values. Run at a terminal on a new project with none of these flags and no `--yes`, `ahub init` asks each setting with its default in brackets and shows the plan before it writes anything; `--yes` takes the defaults, and a pipe, a peer or `--dry-run` is never asked (#1110). Design specs are new and on by default (#1097, #1102, #1107). A design spec is a Markdown file with `id` and `type` frontmatter. `ahub specs list|check|covers` reads the specs and checks their links and freshness without a daemon, agents get the read-only `hub_specs`, and `ahub status` adds a specs line when the project has specs. `ahub specs stamp` is your own record, made at a terminal, that you reviewed a spec against HEAD (see [design spec inspection](operations.md#design-spec-inspection)). The next `ahub init` in an existing project adds an `AGENT_HUB_SPECS` block to `AGENTS.md` unless the project set `specs.enabled` to false; `ahub init --no-specs` turns specs off and removes the block.
|
|
@@ -163,7 +171,7 @@ The restarted Claude planner gets the release-train tools (`hub_release_new`, `h
|
|
|
163
171
|
|
|
164
172
|
Jcode must be 0.98.0 or later, below 1.0.0; 0.94.0 is no longer accepted, so `ahub doctor` fails its Jcode row for an older install and the peer does not start (#529). Every owned jcode process runs with `JCODE_NO_TELEMETRY=1`.
|
|
165
173
|
|
|
166
|
-
Since 0.12.37 the hub opens a split memory home through its shards (#534); the current exact `@staix/agent-mem` pin is 0.
|
|
174
|
+
Since 0.12.37 the hub opens a split memory home through its shards (#534); the current exact `@staix/agent-mem` pin is 0.20.0. Before anyone runs `amem migrate --shards` on a memory home, every hub using that home must run 0.12.37 or later: an older hub does not see the split and keeps writing the old layout. `ahub memory profile-check` is split-aware and compares each hub with its own project's shard (#536). See [memory profiles and rollout](memory.md#selected-memory-profile).
|
|
167
175
|
|
|
168
176
|
From 0.12.30 or earlier, existing Jcode project MCP entries need one refresh before conductor or dashboard starts. Once per project, the person runs:
|
|
169
177
|
|
|
@@ -238,7 +246,7 @@ More: `ahub help`. How it is built and why: [`docs/specs/2026-09-19-agent-hub-de
|
|
|
238
246
|
|
|
239
247
|
## Shared memory
|
|
240
248
|
|
|
241
|
-
The hub exact-pins the first-party `@staix/agent-mem` 0.
|
|
249
|
+
The hub exact-pins the first-party `@staix/agent-mem` 0.20.0 core, including deterministic capture; no claude-mem worker is needed. Existing shared-v1 memory remains the default; upgrading does not migrate it. Managed agents use screened `hub_memory_search`, `hub_memory_show` and `hub_memory_graph` tools, or propose a correction with `hub_memory_correct`; only a person accepts candidates through the supported `amem review` path. Since agent-mem 0.7.0, `amem review` also accepts candidates on a selected generation.
|
|
242
250
|
|
|
243
251
|
Before any real generation migration, upgrade every hub sharing that memory home to 0.12.30 or later and check its authenticated binding with `ahub memory profile-check` in each custom registry. Ready bindings prove compatibility, not an atomic live cutover: quiesce consumers and close their handles before migrating, then reopen through the supported session-preserving lifecycle. Invalid or changed profiles disable native memory without falling back to legacy writes; the bus and board continue. See [memory operations](operations.md#hub-memory) for the remaining rollout gates.
|
|
244
252
|
|
package/docs/sdlc.md
CHANGED
|
@@ -403,7 +403,7 @@ Tracked npm cleanliness compares the cached index to HEAD and raw tracked blob b
|
|
|
403
403
|
|
|
404
404
|
## Release preparation and the guarded tag (#787 P3b)
|
|
405
405
|
|
|
406
|
-
Release preparation is an agent task; the release decision remains the person's. Consent may be recorded by the person or by the configured conductor on the explicit version-naming instruction above; preparation alone grants no consent.
|
|
406
|
+
Release preparation is an agent task; the release decision remains the person's. Consent may be recorded by the person or by the configured conductor on the explicit version-naming instruction above; preparation alone grants no consent. A hub captured before #1120 has no `scripts/check-docs.mjs` in its runtime copy, so `ahub sdlc authorize-release` there refuses with "documentation checker ... is missing from the running hub" (not "stale documentation"): record consent from a hub that carries the checker, either `ahub upgrade` to a release that includes #1120 or a hub started from the candidate checkout with `bun src/cli/main.js`, and never restamp documents to get past it.
|
|
407
407
|
|
|
408
408
|
`bun scripts/release-prepare.ts [--version X.Y.Z] [--dry-run]` is the mechanical half of AGENTS.md "Releasing" steps 1 and 2. On a `release/<version>` branch (it refuses `main`, a detached HEAD, a dirty tree and any other branch name) it reads the commits since the last `v*` tag, applies the rule (any `feat` subject bumps the minor, otherwise the patch; a version below the rule or not newer than the last tag is refused, so is a missing or empty `## Unreleased`), and writes `package.json`, the README `Status:` line, the CHANGELOG section (the Unreleased entries move under the version, Unreleased stays empty), the bundle (`bun run build`) and a verification report skeleton `docs/verification/<date>-<version>.md`. The skeleton lists the pull requests the commits name and the native acceptance receipts, which it reads from `docs/verification/` files added since the tag, never from what an agent writes. It leaves `TODO(agent)` markers for what only a review can state: each PR's review and CI, the document review, the checks and the known gaps.
|
|
409
409
|
|
package/docs/security.md
CHANGED
|
@@ -7,6 +7,7 @@ agent-hub connects agents that can each run commands. This page says what the hu
|
|
|
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
9
|
- **Write retry keys grant no authority (#1078).** The `tool_write_idempotency` feature binds top-level control `requestId` keys to authenticated actor/role/launch/generation/execution scope and canonical business arguments. For task proposals and reviews, MCP `request_id` is an explicit retry argument, independent of JSON-RPC correlation; it is removed before dispatch. A timeout or connection loss remains `outcome: unknown`, with a settling read, while a pre-admission refusal says this attempt did not run. Reusing another scope's key, changed arguments, expired or old-instance IDs never starts a new write. Current authority and privacy are rechecked before a cached result is returned, including the PII status of overlap references. Request payloads are hashed rather than retained; result records are bounded and remain in daemon memory. Limits are per authenticated actor (512 entries/15 minutes), not one shared admission budget. Delivery acknowledgement uses the journal's existing idempotence and socket/generation ownership checks without consuming that cache, so another actor's filled window cannot prevent settlement. Legacy clients without keys keep their path; current clients refuse explicit replay to an older daemon that would ignore it. See [safe retry and refusal semantics](operations.md#control-write-outcomes-and-safe-retries-1078).
|
|
10
|
+
- **Control-write diagnostics carry metadata only (#1126).** The daemon's write spans and event-loop probe record a closed set of operation kinds, phases, task numbers and durations, never task text, paths, ids of transports, actors or sessions, credentials, error text or external output, and nothing for a PII task or a private turn. A frame from a client that has not authenticated is never inspected for them. Retention is capped (128 requests, 256 spans, 512 recent intervals, five-minute expiry) and a record in `hub.log` is at most 2,048 bytes; client timing on stderr is off unless `AHUB_CONTROL_WRITE_TIMING=1`.
|
|
10
11
|
- **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, and a headless Claude implementer's proven shell commands through the host wrapper (#845, below); Kimi and Codex use their own native policies. `never-ask` requires explicit runtime confirmation, a person's console `y` for a config default at hub start, or retained autonomy consent bound to that peer's exact never-ask value in the untracked `config.local.json` (#999, below). Without one of these 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.
|
|
11
12
|
- **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` (still accepted, ignored since 0.16.0), `local.read_allow`, `local.bash_network`, `local.network_allow`, `local.doc_read`, `local.web_fetch`, `terminal.open`, `tasks.auto_pull`, `tasks.queue_backpressure`, `context.max_assignment_used`, `schedules` (the `every`, `cron` and `tz` forms alike), `folder_triggers`, `corpus.paths`, `invocation`, `start.agents`, `instances`, `action_policy`, `rooms`) apply only from a config file git confirms nobody committed (`instances`, `action_policy` and `rooms` 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, which agents `ahub up` opens terminals for, a folder the hub reads, work proposed on a timer, a host a model may fetch from, which of your agents' skills other peers may call, 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`).
|
|
12
13
|
- **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.
|
|
@@ -14,7 +15,7 @@ agent-hub connects agents that can each run commands. This page says what the hu
|
|
|
14
15
|
- **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.
|
|
15
16
|
- **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.
|
|
16
17
|
- **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.
|
|
17
|
-
- **The local worker is boxed in.** Paths are resolved through symlinks and must stay inside the project, and name-based refusals (the denylist, `.git`, `.agenthub`) judge both the resolved name and the name typed inside the project, because Bun's realpath can name another hardlink of the same file (#541; facts, deliverables and `ahub source add` too, #552); 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`, `config.worktree` or `hooks` directly under one, nor in any submodule git directory (every modules tree the project can reach is covered: the common dir's, each linked worktree's own under `<common>/worktrees/<wt>/modules/`, matched by pattern so a tree or worktree that does not exist yet is covered too, and each discovered submodule git directory's own `modules/`, nested submodules and a submodule's own linked worktrees included; under a modules tree every write that is not inside a discovered submodule git directory is refused, whatever its name, so a fake git directory cannot be planted for a later unsandboxed `git submodule update --init` to reuse; inside each discovered one its own `config`, `config.worktree` and `hooks` and its linked worktrees' `config.worktree` are refused, while
|
|
18
|
+
- **The local worker is boxed in.** Paths are resolved through symlinks and must stay inside the project, and name-based refusals (the denylist, `.git`, `.agenthub`) judge both the resolved name and the name typed inside the project, because Bun's realpath can name another hardlink of the same file (#541; facts, deliverables and `ahub source add` too, #552); 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`, `config.worktree` or `hooks` directly under one, nor in any submodule git directory (every modules tree the project can reach is covered: the common dir's, each linked worktree's own under `<common>/worktrees/<wt>/modules/`, matched by pattern so a tree or worktree that does not exist yet is covered too, and each discovered submodule git directory's own `modules/`, nested submodules and a submodule's own linked worktrees included; under a modules tree every write that is not inside a discovered submodule git directory is refused, whatever its name, so a fake git directory cannot be planted for a later unsandboxed `git submodule update --init` to reuse; inside each discovered one its own `config`, `config.worktree` and `hooks` and its linked worktrees' `config.worktree` are refused, while the index, logs and objects stay writable and, since #1123, its `refs`, `packed-refs`, `HEAD` and `worktrees` are refused like the project's own, so commit, branch, tag and rebase fail there too; a linked worktree's git directory under `<common>/worktrees/` gets the same `config`, `config.worktree` and `hooks` refusal; every term is anchored at the discovered path, `hooks` by depth, so a directory named `config` or `hooks` above the project neither disables the rule nor trips it; the discovered git directories, the modules roots, the worktrees container, each worktree git directory 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`/`config.worktree`/`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 shared object store takes only new loose objects (#864): `objects/info` (alternates, http-alternates, commit-graph) and `objects/pack` are refused, and so are the `objects`, `objects/info` and `objects/pack` entries, moving a fan-out directory, and changing, removing or replacing an existing loose object, because git reads packs first and never rehashes what it reads, so a planted pack or alternate would change what every checkout and the implementer's host add, commit and push (#845) see; a hardlink to an existing object is refused as well; an object id that does not exist yet can still be planted before git writes it (its exact content must be predicted), which a local checkout then shows, while a push of it is rejected by the receiving side's rehash; the cost is that `git init`, `git clone` and `git worktree add` do not run inside the sandbox (`git add`, `restore`, `checkout -- <file>` and the read commands still do; since #1123 `commit`, `stash`, `branch`, `tag`, `switch`, `merge`, `rebase` and every other command that moves a ref or HEAD do not, see "Refs, HEAD and worktree admin writes" below; `git gc`, `repack`, `fetch` and `maintenance` run outside it, #864), `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. Two opt-in tools, both machine-local and off by default, work outside the command sandbox: `doc_read` (`local.doc_read`) reads a PDF, DOCX or HWPX file through the path guard and the `pii.paths` screen, converts it with the person's `text_projection` and screens the text before the model sees it; `web_fetch` (`local.web_fetch.allow`) sends a bare https GET only to the listed hosts, through its own egress proxy (never the shell's), re-checking every redirect, and is refused for the whole turn of a PII task. Neither result is captured to memory.
|
|
18
19
|
- **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.
|
|
19
20
|
- **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.
|
|
20
21
|
- **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 `tasks.rescreen_max` (default 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.
|
|
@@ -826,7 +827,21 @@ SDLC metrics and control bands (#481 S7). `ahub sdlc metrics` and `hub_sdlc_metr
|
|
|
826
827
|
|
|
827
828
|
Design specs (#1097, #1102, #1107). `hub_specs` is read-only and refused during a private or PII turn. PII tasks and private turns publish no spec paths through task notices. `spec_ack` on `hub_task_done` is refused for private work and while specs review is off. Each id and reason must be public metadata, a withheld reason is refused, and the acknowledgements are recorded at the exact head. `ahub specs stamp` is person-only: refused for an agent identity and under `--live-test`, and refused without a terminal even with `--yes`. After the person answers, it rechecks HEAD, the state of each covered path and every target file's bytes, and any change refuses the whole run.
|
|
828
829
|
|
|
829
|
-
Declared task needs (#1081). A task's `needs_network` or `needs_worktree` is set only by an explicit true, never inferred from its text. Routing, assignment and accept keep such work from a peer whose capability the hub knows is missing (a hub-sandboxed Pi or local worker without `local.bash_network`, or one that cannot run `git worktree add` inside its sandbox, #270), and the accept refusal comes before any write, so no branch ref is left. An unknown capability is never a refusal, so this is advice for routing, not a sandbox boundary; the sandbox itself is unchanged
|
|
830
|
+
Declared task needs (#1081). A task's `needs_network` or `needs_worktree` is set only by an explicit true, never inferred from its text. Routing, assignment and accept keep such work from a peer whose capability the hub knows is missing (a hub-sandboxed Pi or local worker without `local.bash_network`, or one that cannot run `git worktree add` inside its sandbox, #270), and the accept refusal comes before any write, so no branch ref is left. An unknown capability is never a refusal, so this is advice for routing, not a sandbox boundary; the sandbox profile itself is unchanged.
|
|
831
|
+
|
|
832
|
+
Hub-made task worktree (#1115, #1129, `src/hub/task-worktree.ts`). For write-class work (`implement`, `bulk_edit`) and for any work that declares `needs_worktree`, the hub makes the worktree of a hub-sandboxed owner (Pi, the local worker) at the fixed root `.worktrees/<peer>-<task>` on a new branch `task/<task>-<peer>`, before the accept write, through the hardened git helper (`hubGitSync`, no hook, no helper, no prompt, `repositoryIsolation`). The base is `origin/HEAD` when the repository has one, else the main checkout's HEAD, with no fetch, and it is recorded in the hub's state dir (`task-worktrees.json`), which no sandbox root contains. A refused accept (not a git repository, the branch or path is taken, the repository's own configuration names a filter, include, textconv, ssh command or remote override) creates nothing and leaves no ref. That guard reads only what a repository or a peer can plant: the shared `.git/config` (`git config --local`, which a linked worktree reads too) and, where `extensions.worktreeConfig` is on, the worktree's `config.worktree`. The person's global and system git config is theirs and is not read by it, because git-lfs alone puts `filter.lfs.*` there on most machines and refusing it would switch the feature off for them; the hardened git helper's isolation still applies to that config. The sandbox profile is not rebuilt per task and not widened: `test/task-worktree-sandbox.test.ts` compares `profile()` before and after the hub makes a worktree.
|
|
833
|
+
|
|
834
|
+
What the sandbox can and cannot write is stated here so nobody relies on more. A sandboxed command CAN write files in the worktree and it can add NEW loose objects (#864). It CANNOT write a `.git` gitfile, `config`, `config.worktree`, `hooks`, `commondir`, packs or `objects/info`, and since #1123 it cannot write `refs/**`, `packed-refs`, `HEAD` or a worktree's admin dir (`.git/worktrees/<name>/`), so its own `git commit` or `git branch` fails (see "Refs, HEAD and worktree admin writes" below). The peer is also told not to run git. The hub does not rely on either: it never trusts what is under `.git` for this flow, whatever the profile of the day allows (the profile of #1115 alone did not refuse ref writes, and a rule can be loosened later). At `hub_task_done` it commits only when the worktree is still registered at its fixed path, HEAD is still the task branch, and the branch tip still equals the base the hub recorded (or the hub's own previous commit); a peer that moved the branch to another existing commit is refused with the cause and the next step, and nothing is committed or pushed. It then rebuilds the index from the recorded tip (the peer can write the worktree index), stages the working tree, and refuses the whole commit when any changed path is on the denylist or is a `.git`, `.agenthub`, agent-configuration or protected instruction name, or is a submodule, or when more than 512 paths changed. The commit is the hub's, in the peer's name, with `--no-verify` and no signing, and its message is the task's title. The push is one explicit refspec `refs/heads/<branch>:refs/heads/<branch>`, only after every `origin` URL names this project's GitHub repository, with followTags, mirror and submodule recursion off. Fetch and push stay with the hub. A retry after a failed push pushes the commit the hub already made and never makes a second one.
|
|
835
|
+
|
|
836
|
+
On decline or cancel the hub removes the worktree and deletes the branch while it is still the empty base. A worktree with uncommitted changes, or that cannot be checked, is never force-removed: it is kept with its branch and reported as a console notice. A branch the hub committed to is kept, because its commit may be pushed and under review. A peer that makes its own worktree (Claude, Codex, Kimi) is untouched, and a PII task never gets a hub worktree. Residual: a same-user process outside the sandbox can still plant a ref under `.git/refs/heads`; the sandbox no longer lets a sandboxed command do it (#1123), and the hub refuses to commit or push on top of a moved ref either way.
|
|
837
|
+
|
|
838
|
+
Refs, HEAD and worktree admin writes (#1123). A hub-sandboxed peer (Pi, the local worker) cannot move what a branch, `HEAD` or a linked worktree points at. The profile (`gitRefRules` in `src/local/deny.ts`, built into `hubWriteRegexes`) refuses every write to `refs/**` (branches, tags, `refs/stash`, remote refs), `packed-refs` (and its lock and new files), `HEAD` (and `HEAD.lock`) and `worktrees/**` (a linked worktree's admin dir: its `HEAD`, `index`, `logs`, anything new, and a new admin dir). The rules sit directly under the git dir, so a project below a directory that happens to be named `refs`, `worktrees`, `HEAD` or `packed-refs` keeps working. They cover a plain `.git`, and each external git dir (the common dir and a linked worktree's own, whether the worktree is inside the project or beside it, and a bare repository's common dir that is not named `.git`). Every name is folded for the code points HFS+ ignores like the other git names, and each regex literal stays under seatbelt's 1024-byte limit at any project depth (`test/sandbox-git-refs.test.ts`, run with the real sandbox-exec in the main checkout, in a hub-made worktree, in a worktree outside its project and in a bare repository's worktree). The hub's own git is hardened and runs outside the sandbox, so #1115's hub commit and push are unaffected.
|
|
839
|
+
|
|
840
|
+
What a sandboxed command can still write under `.git`, each with its reason, probed with the real profile: the main checkout's `index` and `index.lock` (`git add` and `git restore` stage there), `logs/HEAD`, `ORIG_HEAD`, `FETCH_HEAD`, `MERGE_HEAD`, `COMMIT_EDITMSG`, `info/exclude` and `description` (none can run a program or move a branch), and NEW loose objects under `objects/` (a staged blob, #864; existing objects, `objects/info` and `objects/pack` stay refused). Still refused as before: a gitfile, `config`, `config.worktree`, `hooks`, `commondir` (#270). Inside a linked worktree the sandbox cannot write the worktree's own admin dir, so `git add` there fails on its `index.lock`; the working tree itself is writable and `git status`, `diff`, `log`, `show`, `blame`, `ls-files`, `rev-parse`, `for-each-ref` and `cat-file` all still work.
|
|
841
|
+
|
|
842
|
+
What stops working in the sandbox, stated so nobody is surprised: `git commit`, `branch`, `tag`, `stash`, `switch`, `checkout -b`, `merge`, `rebase`, `cherry-pick`, `revert`, `reset` and `update-ref`, anywhere, and `git add` inside a linked worktree. A sandboxed peer's work is therefore committed by the hub: for every write-class task (`implement`, `bulk_edit`) it owns, and for any task that declares `needs_worktree`, the hub stages, commits and pushes the hub-made worktree at `hub_task_done` (#1115, #1129), whether or not the task was flagged. Read-only classes (`test`, `summarize`, `triage`, `review`, `plan`) and PII tasks get no worktree and no hub commit, because they commit nothing; a hub-sandboxed peer on a project where the hub cannot make a worktree is not offered write-class work at all, and the refusal names the cause (`no worktree: ...`). The local `git` tool says so in its result when a command is refused this way, and its pointer to `hub_task_done` is true for the work that can reach it.
|
|
843
|
+
|
|
844
|
+
Submodules (#1123, the person's ruling of 2026-11-03: deny `.git` writes, submodule git dirs included). Every DISCOVERED submodule git dir gets the same four refusals as the project's own: `.git/modules/<name>`, nested ones (`.git/modules/<a>/modules/<b>`) and a linked worktree's own (`.git/worktrees/<wt>/modules/<name>`, nested too) refuse `refs/**`, `packed-refs*`, `HEAD*` and `worktrees/**`, so a sandboxed `git commit`, `branch`, `tag`, `stash` or `switch` inside a submodule fails with no ref moved (`test/sandbox-git-refs.test.ts` runs commit, branch and tag in a submodule, a nested one and a linked worktree's, with the real sandbox-exec). A submodule git dir that is not discovered is refused whole by #518 and #520, so nothing is left open by not being found. What stays writable there is what a read needs and nothing that moves a ref or runs code: `index` and `index.lock` (`git status` refreshes it), `logs/HEAD`, `ORIG_HEAD`, `MERGE_HEAD`, `COMMIT_EDITMSG` and `info/exclude` (a test writes each in a submodule git dir), and new loose objects as for the project (#864; not separately tested for a submodule). The working tree of a submodule stays writable and its `status`, `diff`, `log` and `show` work. The `.git` writes that stay open (the main checkout's `index`, `logs`, `ORIG_HEAD`, `MERGE_HEAD`) can be used to corrupt local state the person later reads, not to run code or to move a ref. Consequence for work inside a submodule: the sandbox cannot commit there, and the hub's commit (#1115) is a commit of the project's own repository, which records a submodule only as a commit id. So edits made inside a submodule's working tree are not part of the hub's commit and stay uncommitted in that submodule, while a changed submodule pointer (a gitlink) makes the hub refuse the whole commit, naming the path. A task that edits inside a submodule therefore needs a person or a vendor agent (Claude, Codex, Kimi) to commit that part.
|
|
830
845
|
|
|
831
846
|
## Research campaigns and study tools (#930, #932)
|
|
832
847
|
|