@staix/agent-hub 0.28.0 → 0.30.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 +22 -0
- package/README.md +5 -3
- package/docs/agent-notes/README.md +12 -0
- package/docs/agent-notes/adapters.md +2 -1
- package/docs/agent-notes/bus.md +1 -0
- package/docs/agent-notes/daemon.md +4 -1
- package/docs/agent-notes/local-worker.md +3 -1
- package/docs/agent-notes/tasks.md +7 -3
- package/docs/agent-notes/tests.md +3 -0
- package/docs/design/adapters.md +56 -0
- package/docs/design/agent-hub-architecture.md +12 -0
- package/docs/design/bus.md +8 -0
- package/docs/design/task-board.md +19 -1
- package/docs/dogfood.md +13 -7
- package/docs/operations.md +177 -20
- package/docs/quickstart.md +18 -3
- package/docs/sdlc.md +2 -2
- package/docs/security.md +45 -4
- package/docs/smoke.md +13 -2
- package/docs/specs/2026-10-12-office-view-design.md +86 -3
- package/docs/verification/2026-11-03-0.29.0.md +59 -0
- package/docs/verification/2026-11-03-0.30.0.md +54 -0
- package/docs/verification/2026-11-03-agy-1.4.12-launcher-nonce.md +42 -0
- package/docs/verification/2026-11-03-agy-1.4.12.md +90 -0
- package/docs/verification/2026-11-03-agy-tui-close.md +49 -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-grok-1.0.62-t0.md +150 -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/verification/2026-11-03-pi-150-compat.md +40 -0
- package/docs/verification/2026-11-03-pi-title-api.md +26 -0
- package/docs/verification/2026-11-03-qwen-0265-acp.md +55 -0
- package/docs/verification/2026-11-03-qwen-0265-dual-output.md +40 -0
- package/docs/verification/dogfood/2026-11-03.md +3095 -0
- package/docs/verification/pi-150-model-free.ts +57 -0
- package/docs/verification/pi-150-native.mjs +21 -0
- package/docs/verification/pi-150-title-api.mjs +31 -0
- package/docs/verified.json +14 -14
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +715 -45
- package/src/adapters/compat.ts +10 -10
- package/src/adapters/definitions/builtin-metadata.ts +2 -2
- package/src/adapters/local-worker.ts +22 -6
- package/src/adapters/pi.ts +92 -10
- package/src/adapters/registry-types.ts +4 -0
- package/src/adapters/registry.ts +2 -0
- package/src/cli/autonomy.ts +66 -1
- package/src/cli/console-state.ts +6 -2
- package/src/cli/help.ts +3 -1
- package/src/cli/identity.ts +8 -1
- package/src/cli/main.ts +39 -0
- package/src/cli/output.ts +5 -0
- package/src/cli/recovery-package.ts +9 -0
- package/src/cli/worktree-tidy.ts +158 -0
- package/src/cli/worktrees-output.ts +43 -0
- package/src/hub/board.ts +31 -5
- package/src/hub/bus.ts +4 -4
- package/src/hub/conductor.ts +4 -3
- package/src/hub/config-apply.ts +2 -1
- package/src/hub/control-client.ts +16 -1
- package/src/hub/control-write-trace.ts +409 -0
- package/src/hub/daemon.ts +408 -66
- package/src/hub/dogfood-read.ts +62 -9
- package/src/hub/hub-tools.ts +12 -6
- package/src/hub/manager.ts +9 -1
- package/src/hub/peer-doing.ts +33 -3
- package/src/hub/peers.ts +7 -0
- 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/settings.ts +3 -0
- package/src/hub/task-worktree.ts +331 -0
- package/src/hub/tasks.ts +192 -25
- package/src/hub/ui.ts +2 -0
- package/src/hub/worktree-prs.ts +100 -0
- package/src/hub/worktree-shelf.ts +246 -0
- package/src/local/deny.ts +49 -0
- package/src/local/tools.ts +4 -2
- package/src/memory/brief.ts +5 -0
- package/src/memory/capture.ts +1 -1
- package/src/memory/client.ts +5 -2
- package/src/pi/extension.ts +43 -3
- package/src/ui/index.html +133 -10
- package/src/ui/office-model.ts +64 -8
- package/src/ui/office-space.ts +1 -0
- package/src/ui/plugin.ts +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ 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.30.0
|
|
8
|
+
|
|
9
|
+
- Worktree shelf (#1057: #1156, #1158, #1159, #1160). The hub derives at read time which worktrees are live and which are settled (its PR merged or closed with no newer work, or quiet for `worktrees.quiet_days`, default 7), and the person can Park or Pin a row. `ahub worktrees [--json]` lists both; `ahub worktrees tidy` previews what is safe to remove, with every refused row's reasons, and `ahub worktrees tidy --confirm <fingerprint>` removes only those, in the person's own CLI process, with `git worktree remove` and `git branch -d` and never a force flag. Dirty, locked, unpushed, unmerged, bound-to-open-task, main and instance worktrees are refused. The dashboard's Agents panel gets a Worktrees section with Park, Pin and an explicit "Refresh PR states" (at most 8 PRs); no read makes a GitHub call, and an inbox alert fires above `worktrees.alert_over` (default 10). The shelf adds no removal to the daemon: it still removes only an empty hub-made task worktree on decline or cancel (0.29.0).
|
|
10
|
+
- Pi and the local worker name their own session with `hub_set_title` (#1157, #1055). The first name is final; the person replaces it with `ahub title <peer> <text...>` or the console's `t` editor. An unnamed session shows a screened label from its first incoming message to the console and the conductor only, `unnamed` to everyone else. Claude, Codex, Kimi and ACP tool schemas are unchanged.
|
|
11
|
+
- Office view phase 5 (#264: #1142, #1152): a Backlog wall before To do (rank order, at most 8 cards, reserved, pinned and waiting marks), up to three release docks with approved/scope and cut date, and the unplanned count on each planner's desk. Everything comes from the existing snapshot; no daemon change.
|
|
12
|
+
- Plan sources are validated (#1144, #1151): a plan's `sources` must name registered `src:<16 hex>` ids (`ahub source list`), or the proposal or acceptance is refused before any effect. The dashboard Plan sheet lists source metadata and stages natural-language requests through the same held-draft intake as the CLI.
|
|
13
|
+
- `ahub autonomy status` is readable (#1137, #1138): a peer table first, identical floor verdicts and unknowns once with their peer lists, and class enforcement counts. `--json` is unchanged.
|
|
14
|
+
- `hub_backlog_return_untouched` takes `fields.reserved` (a peer id or null), applied in the same write as the return, so a parked card is not offered straight back under auto-pull (#1154, #1155).
|
|
15
|
+
- The weekly dogfood scorecard reads PR flow on a board over 500 tasks (#1147, #1153): a separate, text-free PII inventory decides privacy, and task rows are read for the scorecard window only. The 2026-11-03 scorecard and the #788 progression measures are recorded (#1149).
|
|
16
|
+
- Compatibility receipts (#888): Pi 1.5.0 (#1148), qwen ACP 0.26.5 (#1150; qwen-dual stays at 0.26.1) and agy 1.4.12 (#1141, #1146) join `verified`. Run agy with `AGY_CLI_DISABLE_AUTO_UPDATE=1`: without it agy replaced its own binary during a launch (#1140). The `ahub agy` launcher close leg (#1140) and an isolated Grok 1.0.62 T0 (#1139, #635) are recorded.
|
|
17
|
+
- Pin `@staix/agent-mem` 0.20.1 (was 0.20.0, #1136). A setup receipt written by 0.20.1 can be v2, which 0.20.0 refuses to apply or undo; undo with the release that wrote it.
|
|
18
|
+
|
|
19
|
+
## 0.29.0
|
|
20
|
+
|
|
21
|
+
- 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.
|
|
22
|
+
- 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.
|
|
23
|
+
- 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.
|
|
24
|
+
- 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.
|
|
25
|
+
- 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).
|
|
26
|
+
- A captured runtime carries the documentation checker, so `ahub sdlc authorize-release` works from a hub started from a capture (#1120, #1122).
|
|
27
|
+
- 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.
|
|
28
|
+
|
|
7
29
|
## 0.28.0
|
|
8
30
|
|
|
9
31
|
- `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.30.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
|
|
@@ -100,7 +100,9 @@ defaults. The
|
|
|
100
100
|
worktree with a machine-wide view, live settings reload, machine-local schedules
|
|
101
101
|
that propose tasks by interval or by a `cron` expression in a time zone
|
|
102
102
|
(`ahub schedule list`), a one-confirmation autonomy
|
|
103
|
-
preset (`ahub autonomy on|off|status`),
|
|
103
|
+
preset (`ahub autonomy on|off|status`), a worktree shelf that lists live and
|
|
104
|
+
settled worktrees and removes the safe ones only on your confirmation
|
|
105
|
+
(`ahub worktrees`), structural config
|
|
104
106
|
(instances, review, lifecycle) edited with a validated diff and a backup from the
|
|
105
107
|
console or a dashboard settings session, token usage and quota
|
|
106
108
|
history (`ahub usage` and the dashboard's Usage view), and `ahub upgrade`, which
|
|
@@ -135,7 +137,7 @@ uses the home must run a version that understands the new layout.
|
|
|
135
137
|
[#258](https://github.com/STAIxBWLB/agent-hub/issues/258)).
|
|
136
138
|
|
|
137
139
|
Live checks that still need a real account or a person are listed in the
|
|
138
|
-
[release verification report](docs/verification/2026-11-
|
|
140
|
+
[release verification report](docs/verification/2026-11-03-0.30.0.md) and the
|
|
139
141
|
[smoke checklist](docs/smoke.md).
|
|
140
142
|
|
|
141
143
|
## Documentation
|
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Human explanation. Agent instructions: the area notes in this directory.
|
|
4
4
|
|
|
5
|
+
In the #1055 review, the new title-binding lookup preceded invocation,
|
|
6
|
+
pending-delivery and supervision bookkeeping in `Bus.onDelivered`. The bus's
|
|
7
|
+
outer observer catch protected settlement but could not finish bookkeeping
|
|
8
|
+
after a journal lookup threw. This is the background for isolating that
|
|
9
|
+
fallible observer work in the bus area note.
|
|
10
|
+
|
|
11
|
+
Two #1055 refusal tests exposed Bun 1.4.2 replacing a received `error` string
|
|
12
|
+
with the `StringContaining` object supplied to `toMatchObject`. A minimal
|
|
13
|
+
same-version reproduction showed `typeof error` changing from `string` to
|
|
14
|
+
`object`. Preserving the original value keeps later error-text and canary
|
|
15
|
+
checks meaningful; changing production messages would not address this cause.
|
|
16
|
+
|
|
5
17
|
On 2026-10-13, two PR #368 reviews found that a missing delivery id or a missing visible row could select another Queue row. The console navigation rule in daemon.md records the exact-row contract. The missing-row and ID-less secondary-delivery regression tests demonstrate both cases.
|
|
6
18
|
|
|
7
19
|
Background behind some of the rules, moved from the top-level README on 2026-10-16:
|
|
@@ -51,6 +51,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
51
51
|
- A durable Codex steer stays pending while its native turn runs. Its RPC may answer after `turn/completed`: bound unanswered replies from that turn's end, requeue a definitive late refusal, and hold a late acceptance as uncertain without attaching it to a later turn's audience (#296).
|
|
52
52
|
- Codex reports each thread it adopts, started or resumed (`onThread`, the `native_thread` event), before any turn on it starts: the upgrade planner (Codex's `session.zeroTurn` in `src/adapters/definitions/native-recovery.ts`, read through `nativeZeroTurn` in `src/cli/upgrade-runtime.ts`, #215) gives each codex `turn_start` to the thread adopted last before it and plans a thread it saw start with no turn (and no rollout) as a fresh start.
|
|
53
53
|
- Peer DOING (#878): an adapter reports a native session title or its plan's in-progress item only through `onDoing` (set by `Bus.add`), with `reset` when it binds a new session or thread; never put that text in a log line, event, message, status notice or memory capture. Codex reads only `thread/name/updated` and `turn/plan/updated` of the bound thread, ACP only a `plan` update inside the current turn; never guess a value for a peer without such a source.
|
|
54
|
+
- Session titles (#1055, spec `docs/design/adapters.md` "Session title slots"): only a fixed builtin may declare `title` or `titlePersistence` (`assertNativeCapabilityScope`), and only a `title: "hub"` definition (Pi, local) is offered `hub_set_title` (`sessionTitleTools`); keep it off Claude, Codex, Kimi and ACP tool lists and native auto-approval names. Write a title only through `setSessionTitle` (unnamed-only for an agent, `current()` rechecked right before the effect) and report it only through `onDoing("title", ...)`; never pass a title call to `observeTool` or capture, and never store a derived unnamed label.
|
|
54
55
|
- Codex 0.154.0 `agentMessage` items carry `text` and `phase` (not `content[]`); only the last non-`commentary` message of a turn is shared.
|
|
55
56
|
- Hide `hub_delivery_retry` from delivery-only ACP/local tool lists and omit its retry hint. Treat `AGENTHUB_DELIVERY_ONLY` only as an exposure hint; the daemon must refuse by the actual attached adapter, and native children must not inherit the marker. A Claude push that cannot reach its model reports `failed_safe`; if the daemon refuses that optional receipt state, try `needs_review` and retain uncertainty when neither receipt is confirmed. Keep `hub_delivery_resolve` on every list, delivery-only included: settling one's own `needs_review` row is not a retry (#1112).
|
|
56
57
|
- ACP `onActivity` is a closed, transient person-only projection. Never publish it through the bus, log/event writers, UI, tools or memory. Emit only for an active turn; latch private envelopes and associated PII work, including unreadable privacy state, and discard pending chunks on turn teardown. Native terminal references do not authorize filesystem/terminal RPC reads.
|
|
@@ -106,7 +107,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
106
107
|
- Jcode MCP set (#572): `jcode acp` and `jcode serve` start every server named in `<JCODE_HOME>/mcp.json` and the project's `.jcode/mcp.json`, `.mcp.json` and `.claude/mcp.json`, and no flag or variable selects them (`--mcp-tools`, `JCODE_MCP_TOOLS`, `JCODE_DISABLED_TOOLS` and `JCODE_DISABLE_CLAUDE_MCP` do not). Do not paper over it: `ahub doctor` lists the extra names through `jcodeExtraMcpServers` (`src/adapters/definitions/native-cli-jcode-mcp.ts`), names only, those four paths only, no home search, no link followed. Below the project root or home, `.jcode` and `.claude` must be real directories and the file a regular file (per-component `lstat`, an `O_NOFOLLOW` open and an `fstat` identity recheck); a system alias at or above the configured root stays allowed. Revisit if upstream adds a server selector.
|
|
107
108
|
- Extra Jcode instances (#685, #368): one `JcodePeer` per declared id (at most 8), each with its own `jcode_home`, socket and state. The person authenticates inside Jcode; the hub passes `-p`, `--model` and `--provider-profile` exactly as declared, handles no model credential or URL, and only checks that the home is a directory the person owns (`jcodeHomeRefusal`: no read or write under it). Never put `AGENTHUB_PEER_ID` in the shared `.jcode/mcp.json` entry (`jcodeMcpEntry`): every owned launch supplies it through `peerChildEnv(<id>)` or the TUI launch env, together with its `AGENTHUB_LAUNCH_ID`. The MCP server with no `AGENTHUB_PEER_ID` in tools mode refuses every tool and never defaults to a peer id. The daemon admits a `tools` hello for a peer whose definition is `owned-acp` only when it carries the launch id of that peer's current owned launch (`JcodePeer.ownsLaunch`, from `jcodeLaunches`); never trust the env alone. `jcode --as <id>` never opens a TUI. Declare the semantic `instances.nativeHome` on a definition, never compare the kind name (`registry-guard`); it is a fixed-builtin capability that `assertNativeCapabilityScope` refuses elsewhere. `checkJcodeInstanceHomes` (`src/hub/instances.ts`, run once at daemon startup) refuses a home equal to or nested in the base Jcode's or another instance's; compare homes only through its `canonicalHome` (`realPath`, case-folded on a volume that folds case, inode), never `resolve()` text. The preview of `--as <id>` passes the instance to `previewJcode` so it shows the daemon's real flags.
|
|
108
109
|
- Jcode defers MCP tools (#912, `JCODE_DEFERRED_TOOLS_INSTRUCTION` in `src/hub/envelope.ts`): a Jcode peer lists only its built-in tools until the model searches for an MCP tool, so without a hint it reports the hub tools missing and answers in its final message. The guidance is written in exactly one place, that constant, and `JcodePeer` sends it through `jcodePreamble` (`src/adapters/jcode.ts`) ahead of the role contract, once with a session's first prompt, for the base peer and every extra instance. Never copy it into `STANDING_INSTRUCTION`, `templates/AGENTS.block.md` or a profile template (the Jcode bullet there is the same text in every template, and `test/jcode-deferred-tools.test.ts` pins both), and never change Jcode's own deferral or cache settings from the hub. The tools it names must exist, and the server name must be `JCODE_MCP_NAME`.
|
|
109
|
-
- Antigravity P1 (`src/adapters/agy/`): keep it optional, cloud, off roster by default, implementer/verifier only and pull-only. Until per-mode native prompt behavior is verified, reject hub permission/unattended mappings, headless starts and resume selectors; do not infer a quota source. The launcher refuses extra instances, any `agy --version` that compat `agy` refuses (1.4.2 and 1.4.
|
|
110
|
+
- Antigravity P1 (`src/adapters/agy/`): keep it optional, cloud, off roster by default, implementer/verifier only and pull-only. Until per-mode native prompt behavior is verified, reject hub permission/unattended mappings, headless starts and resume selectors; do not infer a quota source. The launcher refuses extra instances, any `agy --version` that compat `agy` refuses (1.4.2, 1.4.7 and 1.4.12 are T0-verified; other 1.4.x patches launch with a stderr warning, #589; another version needs the person's trust, #691), and a hub without the `native_hook_launch` feature.
|
|
110
111
|
- Only the person's `ahub agy` launcher manages `.agents/plugins/agent-hub/{plugin.json,mcp_config.json,hooks.json}`. Refuse linked/foreign entries before any write; cleanup only the matching launch. Never write project-level `.agents/hooks.json` or personal Gemini settings. The plugin MCP server is named `agent-hub_agent-hub` by the pinned native client.
|
|
111
112
|
- Managed native facts use the definition's typed `NativeFactsPort` and the daemon's ephemeral `native_hook_launch` authority. Bind every observation to the daemon, launch, native session, project and current socket generation. Stop requires `fullyIdle`, normal termination and a fresh validated completion row; a missing readback keeps the peer busy. Native fact acknowledgement accepts only SDK ephemeral-message rows in the exact session transcript path. `decodeAgyHook` classifies as `Read` only the observed native read (`view_file` `AbsolutePath`, or 1.4.3 `read_file` `Path`); a `StartLine`/`EndLine` range becomes `offset`/`limit`, so a partial read never counts as a whole-file view. Unobserved edits get no scoped grant or effect attribution. The daemon records Pre/PostToolUse through `Facts.preTool`/`postTool` and offers nothing on them (#555).
|
|
112
113
|
- `terminalOwner(launchId)` (`src/hub/peers.ts`, #482) answers only for the adapter's current native TUI owner bound to that launch (Codex: the claimed link with that `launchId`, both sockets open; the launcher sends the id as the upgrade's bearer token through `--remote-auth-token-env`, because `codex --remote` refuses any path or query (#586); Pi: a started, verified TUI owner on its requested session); a start acknowledgement is never ownership. A Codex link that is not the claimed one or has a closed socket never adopts a thread.
|
package/docs/agent-notes/bus.md
CHANGED
|
@@ -24,6 +24,7 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
|
|
|
24
24
|
- The `onImportantQueued` nudge for a pull-only peer (#398) is advisory: it never dequeues, steers, journals or settles, and a throw from it never stops an enqueue.
|
|
25
25
|
- Merge recall and restored prefaces through `mergePrefaces`/`uniqueMemoryLines`; preserve distinct context and new notes, deduplicate memory identities, and keep colliding short prefixes distinct. Use `recallDisplayText`/`recallMemoryLines` before display caps: suppress only matching automatic approval outcomes, retain nonempty rejection details, preserve id headers and complete-word excerpts, and keep token trimming at line boundaries. Keep these changes display-only.
|
|
26
26
|
- Observe memory rendering from final `out`, not `originals`, and suppress telemetry when either contains private envelopes. A pull emits rendered/delivered observations only after its journal transaction commits; observer exceptions must never change delivery settlement.
|
|
27
|
+
- Read `Bus.onDelivered`'s optional persisted delivery ID only at its positive-ACK observation site; isolate fallible observers so they cannot skip invocation, pending-delivery or supervision bookkeeping.
|
|
27
28
|
- Classify automatic held-row completion only through `supersededDeliveryReason`: every original must be a hub presence or a provably moot hub task/review notice. An owner offer (`refs.taskOffer`) is moot only while that recipient owns the task in `in_progress`, `in_review` or `changes_requested` and its own `accepted` entry follows the latest ownership event within the 4096-entry bound; a person's acceptance, an unmarked reminder or missing proof keeps the hold (#434). Unknown/live/mixed rows stay held; keep `deliveryTaskContext` display-only. Tag only current-owner notices as owner audience; leave former-owner withdrawals untagged until the task closes. For cancelled tasks, settle earlier workflow notices but retain the stop notice whose `refs.taskCancelled` matches the cancellation revision. Preserve the requested-stop exclusion in `daemon.md`. Record the hub actor through `resolveDelivery`, restoring presence originals through `restorePreface` only for hub completion (person completion stays consumed); run on hold entry, restore, recovery release and board changes, never replay uncertain work. List every hold once in status, console notices and route explanations (#434).
|
|
28
29
|
- `resolveDelivery` mutates queues in place; a refused or failed resolution puts back only that peer's queue and preface and keeps every pause. Never reload a snapshot on a live bus. Keep the selected delivery id and journal revision fixed in console row actions; retry/discard/completed never change a task role or cancel a native turn.
|
|
29
30
|
- Recovery-deferred hub notices (`deferredHub`, #478) are accepted work kept outside the queues and the carried-queue integrity projection: never put them in `queues` or the integrity view while held. Every read of pending work includes them (`pendingEnvelopes`, `withdraw`, `revokeSupervision`, `queuedBacklogIds`, `get`, reset). Past `MAX_DEFERRED_HUB` refuse the publish; never drop a kept notice.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- For conductor recovery (#900), keep source-issued private continuity separate from public actor/audit metadata; recheck the same initiator and every affected project at effect boundaries. Compare candidate config and routing with the running accepted policy. Verify and use the same captured startup inputs before any adapter import; never publish a later raw-file reread. Keep captured TUI restoration and unknown source/target evidence person-only. Derive target feature floors from verified target bytes, never a request or receipt claim (`docs/operations.md`, "Conductor-bound restart and recovery continuation").
|
|
13
13
|
- Conductor operational refusals (#812, #839): throw `OperationalAuthorityError` (`src/hub/operational-authority.ts`) for an authority refusal, never a plain `Error`. Only it sets `authorityRefused: true` and its scope on the reply, which the control `reply` wrapper audits once as a refused `agent_cli` event; any other failure of a labelled request keeps its own error with `authorityRefused: false` and is never audited as refused, and the CLI's `fail` audits only when `authorityRefused` is not `false`. An unreachable or dead hub is "authorization unavailable", never a missing conductor role. `publicOperationalView` is cached on the board status revision, config revision, routing and the private-path policy: a new input to that projection joins the cache key.
|
|
14
14
|
|
|
15
|
-
- Peer DOING (#878): `observeDoing` is the only writer and `doingRows` the only reader (`src/hub/daemon.ts`, helpers in `src/hub/peer-doing.ts`). Decide privacy at observation (`contextPii` or a refusing or throwing `tasks.publicMetadataText` latches a stub) and again at display (an open PII task owned or reviewed, or a private turn); reuse those screens, never add one. `status()` adds it only with `includeDoing` (the `status` reply and the dashboard snapshot), so `writeStatus` never writes it to `status.json`; the conductor's `hub_status` gets the same rows, each non-stub value framed by `publicConductorStatus` as untrusted (`sanitize`d text, `
|
|
15
|
+
- Peer DOING (#878): `observeDoing` is the only writer and `doingRows` the only reader (`src/hub/daemon.ts`, helpers in `src/hub/peer-doing.ts`). Decide privacy at observation (`contextPii` or a refusing or throwing `tasks.publicMetadataText` latches a stub) and again at display (an open PII task owned or reviewed, or a private turn); reuse those screens, never add one. `status()` adds it only with `includeDoing` (the `status` reply and the dashboard snapshot), so `writeStatus` never writes it to `status.json`; the conductor's `hub_status` gets the same rows, each non-stub value framed by `publicConductorStatus` as untrusted (`sanitize`d text, `untrusted: true`, and `from` with `doingNote`, or `note: INCOMING_LABEL_NOTE` for a `label` row); never give an agent DOING text without that frame. A `label` row (`unnamedSessionLabel`: an unnamed `title: "hub"` session's first acknowledged incoming delivery, #1055) reaches only the console, the dashboard and the conductor (`includeIncomingLabels`); every other status reader gets `unnamed`. The console `title` op and `hub_set_title` both go through `setSessionTitle` (writer rule: `adapters.md`), and `session-title-inputs.json` keeps only session, delivery and envelope ids (`sessionTitleBindings`, `titlePersistence: "native"` peers only), never a name, body or label. Claude's title comes from the transcript path the facts hook reported and `claudeTranscript` validated, read through `readAiTitle` (1 MiB per read, lines over 64 KiB skipped); its current item only from PreToolUse input, never the transcript; every cached TaskUpdate subject, current or not, is screened when it is learned (`contextPii` or `tasks.publicMetadataText` refusing or throwing, via `ClaudeTodoSource`), keeps that verdict and passes it to `observeDoing` as `learnedPrivate`. A doing failure never changes a facts reply.
|
|
16
16
|
- Crash-recovery lines (#1111): `recoverAfterCrash` adds a line only through its `report(line, notice)`. Tag a line that asks something of a peer's owner with `peer`; it clears when that peer attaches. Tag the headline with `inFlight`; its count is read live from `crashLostIds`. A line about what recovery found or did gets neither and stays. Status shows only `crashView()` (`openCrashNotices`), never the raw list. `crashReported`, which the Jcode TUI hint reads, never clears.
|
|
17
17
|
|
|
18
18
|
- Conductor console ops (#933): a tools caller's `CONDUCTOR_TASK_OPS` request (`github_import`, `intake_approve`, `schedule_run|pause|resume`, `task_unassign`) and `goal_link` are admitted by `admitOperational` in `onMessage` before Tasks sees them, and the task op runs inside `operationalContext`, which `TasksDeps.operational` reads; never let `taskOpBody` admit a non-person caller to a console op any other way. `conductor_operational` `admit` audits a `CONDUCTOR_LOCAL_OPERATIONS` form once; `authorize` only rechecks the role. `task_catalog_role` from a tools caller passes the peer to `Tasks.setRole` (never `USER`), and is refused for a child, external, read-only or PII-context caller.
|
|
@@ -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.
|
|
@@ -33,6 +34,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
33
34
|
- acon plugin (#935): `src/ui/plugin.ts` never bypasses `uiAction`'s whitelist or `DASHBOARD_VIEWS`. It reaches project hubs only through `projectConsole` (`src/hub/manager.ts`), the same listing and forwarding `ahub ui --all` uses; widen neither for the plugin. Its inbox verbs map to the hub's own `permit` with an option the request offers. Its panel is built from `renderDashboard()` by exact rewrites that throw when a target line moves: when you edit a rewritten line in `src/ui/index.html` (post, the cookie helper, `$`, the theme `apply`, the view key listener, the title update or the landmark tags), update `panelAssets()` in the same PR. `lifecycle.ts` is loaded lazily there, because it reaches the daemon and CLI files acon's import scan reads. `ahub setup` registers it only through `registerAconPlugin` (`src/cli/acon-register.ts`): `console/plugins/ahub.json` under the hub home names the module and its sha256 (directories 0700, written to a temp file and renamed); the package export `./ui-plugin` is pinned by `scripts/check-package.mjs`. The Space panel (#1012, `src/ui/office-space.ts`) maps only the #264-allowlisted `officeModel` scene, pinned by the #264 marker fixture in `test/acon-plugin.test.ts`; never feed it an envelope body, task detail, path or symbol. A Space snapshot answers within acon's 5 s: the listing gets half of `SPACE_BUDGET_MS` and falls back to the last listing (a late one still replaces it), projects are read concurrently and a late one is left out of that poll, the cached office model is keyed by the daemon's `instanceId` (a restarted daemon restarts its ui sequence at zero), and `pollMs` stays under acon's 3000 ms link gap.
|
|
34
35
|
- Dashboard Usage view (#683 U4): `uiSnapshot` builds `usage` only when the request's view is `agents`, never on a hidden view's poll. It folds with `catchUp(100)` and sets `behind: true` when that is not done, so a partial figure is never shown as the whole. A failed read answers `{ unavailable: true }`, never `undefined`, which the page reads as a hidden view and keeps the old figures. The snapshot carries task ids, a `pii` flag and numbers only, never a title; the page shows a private task as `#id [pii]`. Daily figures come only from `UsageRollup.daily` (rule: `budget.md`).
|
|
35
36
|
- Dashboard Code group (#684 2c): `codeGroup` (`src/hub/task-code-group.ts`) is the only dashboard classifier of changed symbols against `plan.symbols`, called in `uiSnapshot` with the public `codeSymbols`, `tasks.publicMetadataText` and `tasks.isPii`; the page (`codeGroupSection`) draws it and never matches names. A private task gets no `codeGroup`. A stored receipt (one with `repositoryKey`), a missing or an unknown one is `state: "unknown"`. A malformed or screened-out plan name is counted in `planWithheld` and marks the group incomplete, never dropped. Lists stop at `CODE_GROUP_MAX` (40) with the rest in `more`, and `counts` are totals. The memory view reads a `node` of up to 512 characters only when it starts with `symbol:` (200 otherwise).
|
|
37
|
+
- Worktree shelf (#1057, `worktreeShelf` and `worktreesView` in `src/hub/daemon.ts`; spec `docs/design/task-board.md`): never add a worktree or branch removal to the daemon (its only one stays the empty hub-made task worktree in `src/hub/task-worktree.ts`); shelf removal exists only in the person's `ahub worktrees tidy --confirm` (`src/cli/worktree-tidy.ts`), which `daemon.ts`, `worktree-shelf.ts`, `board.ts`, `ui.ts` and `plugin.ts` never import. GitHub is read only by `worktrees_refresh_prs` (`refreshPrStates`); a snapshot, `ahub worktrees` and the inbox alert never reach it, and `worktreesView` answers from its 30 s cache without awaiting git. The `worktrees` read is open to a tools caller outside a private turn; override set/clear and the refresh are console-only and refused for a live-test dashboard session. After the awaited git reads, ask the recovery/stopping fence again, then read and write the board with nothing awaited between. The board keeps only the person's Park/Pin (`worktree_override`), never a derived settled state, and accepts one only for a path the shelf just listed, never the main checkout or an instance worktree.
|
|
36
38
|
- Agent CLI drift (#843 AC3): the daemon's `sweepAgentDrift` (once after start, then `agentDriftEveryMs`, default one day, on its own timer apart from the task sweep) is the only caller of `noticeDrift` (`src/hub/agent-drift.ts`). It reads versions only through `agentVersionsAsync` (`src/cli/agents.ts`), which shares `probeTarget` and `probeRow` with the `ahub agents` probe: never add a second probe path, a network read or an install. The first probe is owed until one has run (`agentDriftOwed`): a daemon that starts inside a controlled recovery skips it and the 1 s tick pays it once `recoveryActive()` ends, so a restart or upgrade never waits a day for the first notice. `startDaemon` probes only when `agentDrift` is set (`lifecycle.ts` sets `true`; tests inject a function), so a test daemon never spawns a real CLI. Drift is an installed executable with a readable version whose state is not `verified`; a missing or unreadable one is silent. The notice carries id, version, state and the receipt path only, is marked per (id, version, planner) in `agent_drift_notices` before it is published to that planner alone (and forgotten if it cannot be queued, so only that planner is retried), and goes to `plannerPeers()` that are rostered; with none, nothing is recorded. `agentVersionsAsync` reads stdout as a stream with the 64 KiB cap enforced as it arrives (kill and cancel on overflow) and cancels the reader at its deadline and a short grace after the probe exits, so a descendant holding the pipe cannot hang it. Never put CLI output in it.
|
|
37
39
|
- SDLC band timer (#481 S7): the daemon's band interval (`bandsEveryMs`, default 5 min) is the only caller of `tasks.proposeFromBands`. It skips while stopping or in recovery, when `tasks.bandsState()` is not ready or `bandsHeld` holds, and when `board.statusRevision()` with the band signature is unchanged since the last pass that ran; it records that key after the write. A failure is one sanitized `hub.log` line. Writer rules: `tasks.md`.
|
|
38
40
|
- `ahub board` (#724): `renderBoard` prints one line per task only with a terminal width and without `--full`; `--full`, `ahub task show` and output without a width keep the full table and SDLC advice. `STAGE_CODES` (`src/cli/output.ts`) keys on blocker-text prefixes from `src/hub/sdlc.ts`: rewording a blocker there updates its code in the same PR. TITLE is cut, never wrapped; STAGE is never dropped (it falls back to the meter and the code); a PII row keeps its `(ahub task show N)` hint while 8 title columns remain.
|
|
@@ -102,6 +104,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
102
104
|
- `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
105
|
|
|
104
106
|
- 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.
|
|
107
|
+
- 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
108
|
|
|
106
109
|
- 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
110
|
|
|
@@ -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.
|
|
@@ -55,6 +55,8 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
55
55
|
|
|
56
56
|
- Resolve any id that may be an extra instance or a configured kind through the startup-bound project catalog: `this.d.adapterCatalog` with `this.d.instances?.()` in Tasks, `opts.adapterCatalog` in `assign()` and `relayCandidates`, and the `adapterCatalog` hook in `Conductor`/`conductorStart`; a bare `definitionOf(id)` answers builtins only. Only a kind's routing preference expands to its instances; manual ids, reservations and nominations stay exact. Reviewer independence compares families: take the own family only when no other is eligible, record it through `assignmentNote` (`sameFamilyFallback=true`), explicit nominations included; persist the `instance`/`family` refusal codes as `policy`.
|
|
57
57
|
- Tool publication (`projectToolList`, `projectConductorTools`, `configuredToolContext` in `src/hub/hub-tools.ts`) only narrows the list or adds start targets, using authenticated status metadata (`configuredAdapters`, `startablePeers`, `readOnlyPeers`); in a configured project, malformed or incomplete metadata publishes the read-only set and adds no start target. Publication is never authority: `taskOp` refuses read-only mutations independently, and MCP task and conductor refusals keep their text and set `isError: true`.
|
|
58
|
+
- `hub_set_title` (`SESSION_TITLE_TOOL`, #1055) is in `TASK_TOOL_NAMES` but never in `TASK_TOOLS`; its publication rule is in `adapters.md`, and its `taskOpBody` case accepts only `title`.
|
|
59
|
+
- Plan sources (#1144): `propose`, `acceptReviewed` and `accept` call `validatePlanSources` on the plan they will keep (the new one, else the stored one) before any board write or notice; an id the source registry (`TasksDeps.intake.sourceKnown`) does not know is a `PreEffectToolRefusal` naming `ahub source list`. A new path that admits a plan calls it too.
|
|
58
60
|
- SDLC metrics and bands (#481 S7): `sdlc-metrics.ts` is pure (history in, numbers out, no clock, no I/O); every metric a task cannot derive is `null`/`unknown`, never 0, and a private task is only counted as withheld. Stale-head re-reviews need both heads on a round (`reviewCost.head` on the done, `reviewFindings.head` on the verdict): never derive them from event order. `Tasks.sdlcMetrics` is the one read (planner or person, refused during a private turn) and `Tasks.proposeFromBands` the one writer. It is held like every host writer (`sweepHeld`, `bus.isRecoveryHeld`, `bus.storageError`), keys a breach by its episode (`BandResult.episode`, derived from history so a restart finds the same key), writes nothing while the metric has an open band card, and records a log notice in `band_notices` so it is announced once. Card text is fixed words, numbers and task ids, screened through `signalsFor` and `candidateTextAllowed` like the recurring-finding card; never put task titles, notes or PR text in it, and never open a GitHub issue. Drift honours `min_delta` like every tier. `[bands]` is validated by `bandsConfig` with field-level paths; do not add a tier or a metric without a test that fires it. Merge-time figures (#442) come from `MergeRecord.cost`, written once by `guardedMerge` after it observes the merge (`mergeCostFrom`, `cleanMergeCost` in `cost-governance.ts`): re-validate a stored record with `cleanMergeCost` before use, never let that read touch the merge result, and keep a missing or partial read `null`, never 0. `hotfix_rate` and `security_open_alerts` are external series (`EXTERNAL_METRICS`, `Tasks.externalSeries`) whose points are releases and scans, so a card names its `subject`; a legacy release with no train history is left out of the series, never a 0. The security scan (`security-baseline.ts`, `Tasks.runSecurityScan`) reads GitHub's own open-alert counts, runs no scanner, schedules nothing without `[security_scan]`, is held like every host writer, re-judges its schedule after the awaited read and records counts only; a source it cannot read is an unknown total, never zero.
|
|
59
61
|
- Admit a declared extra (a person-started `ahub claude --as` instance and a headless Claude implementer included, #742, #747) as owner of any class whose owner role is implementer (`ownerRoleOfClass`) only with the current implementer role and `ownerIsolation[id] === true`, the daemon's proof that the live adapter generation started in `executionRoots[id]` outside the canonical cwd. Never derive it from the declaration's `worktree`, its roles or a reload. Only the status path passes a display `ownerIsolation` projection (`stalled(projection)`); every admission and effect caller uses fresh `TasksDeps.ownerIsolation`.
|
|
60
62
|
|
|
@@ -68,6 +70,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
68
70
|
- `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
71
|
- 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
72
|
- 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.
|
|
73
|
+
- 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
74
|
- 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
75
|
- 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
76
|
- 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`.
|
|
@@ -81,13 +84,13 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
81
84
|
- `Tasks.retryReviewer` is host-only (the daemon's reviewer `onUnavailable`), never a tool or CLI path. It acts only while the failed peer is still the reviewer of an `in_review`, non-PII task, with the same review head, the request's `reviewRequestRevision` and a current child generation; `valid()` rechecks all of it after every await and right before the write, which also passes the flow revision. A manually named reviewer's `no_verdict` and a no-verdict with no declared pool go only to the person; other manually named failures keep explicit person control. Confirm fallback only after accepted reviewer publication or a person offer and a fresh binding/generation check; failed publication or notification never confirms settlement. Each failed peer is tried once per head and round (`reviewerFailure` history). `reviewRequestRevision` counts only role events (`done`, `reviewer changed`, `reviewer unavailable`, ownership), so bookkeeping never fences a request; keep `refs.reviewRevision` and `refs.commit` on the review envelope.
|
|
82
85
|
- Release trains (#481 S11): change trains only through `Tasks.newRelease`, `configureRelease`, `assignRelease` and `cutRelease` (planner or person), and refuse every `hub_release_*` in a PII turn. `assignRelease` writes only `release` and refuses a frozen, shipped or dropped release on either side; private or pinned scope is person-only. One active or frozen train at a time. A cut freezes only through `Board.freezeRelease` after a fresh `releaseWindow` (host lease from `readReleaseHostAdmission`, quota, `current()`, at most 5 s old); unknown evidence defers, and the window is closed in `finally`. A cut never publishes, tags or upgrades, and upgrade and recovery never consult `releaseWindow`. Keep two of `RELEASE_TRAIN_HISTORY_LIMIT` slots for cut and ship; a configuration no-op appends nothing. An S9 plan receipt survives only contiguous planning revisions that changed exactly `["release"]`.
|
|
83
86
|
- Release authorization (#481 S6, `src/hub/sdlc-release-authorization.ts`): the console user or the one configured conductor through `sdlc authorize-release` creates one under the same admission as `sdlc merge` (tools role, matching configured conductor, no `toolsOwner`, private turn or caller PII context). Keep the actual asker in receipt `by` and the audit, and the cooperative explicit-person-instruction rule in AGENTS.md Releasing. Keep consent bound to the current HEAD commit, its committed package version, the origin identity and a fresh, error-free docs verdict, all reread after the docs check. It lives in daemon memory only: a restart revokes it and the event is audit, never restored. `release-guard` and `release-guard-hook` are refused in a private turn; a tag push needs a matching receipt, and npm publish also a clean tracked worktree and the same docs digest. Never accept authorization from a tool, a task receipt or a file.
|
|
84
|
-
- `Board.update`'s untouched-return exception is the only way owned or accepted work returns to backlog: keep every condition (exact event, owner and reviewer null, state, `expectedFlowRevision`, non-empty note, no work evidence) and reach it only through `Tasks.returnUntouchedToBacklog` (#481 S9).
|
|
87
|
+
- `Board.update`'s untouched-return exception is the only way owned or accepted work returns to backlog: keep every condition (exact event, owner and reviewer null, state, `expectedFlowRevision`, non-empty note, no work evidence) and reach it only through `Tasks.returnUntouchedToBacklog` (#481 S9). Optional return `fields` accepts only `reserved`, follows the existing reservation authority and target-admission rules before and after screening, and saves the reservation with Board-derived planning history and revision in the same state write before capacity notices or auto-pull; omitted fields retain the plan (#1154).
|
|
85
88
|
- The main-checkout note (`mainCheckoutWarning`, `src/hub/main-checkout.ts`, #572) is advisory text in the `hub_task_done` result, the review request and status, never a refusal. A branch name or other metadata the hub reads from outside the board reaches task text only after `Tasks.publicMetadataText` (PII patterns, secret shapes, private paths) and a plain ref-name check; otherwise use the generic sentence that carries none of it.
|
|
86
89
|
|
|
87
90
|
- No-write advisories (#498): count only host-captured, completed owner turns bound to the current acceptance of an `implement` or `bulk_edit` task. Require a known empty snapshot diff, unchanged HEAD/shared branch refs and no task-tool effect; changes, effects, pending/unknown effects or unknown captures break the streak. Treat empty canonical-root captures as unknown when an owned task records branch/commit, worktree paths or SDLC PR/head evidence. Require exclusive Git worktree scope at both boundaries and no `.worktrees` directory before trusting negative canonical evidence; uncommitted and detached worktrees otherwise remain unknown. Keep builtin-owner D1 inert while `.worktrees` exists; narrow this deliberate false-negative ceiling only after binding each owner to its real execution worktree. Keep `task_sweep.no_writes_turns` independent of the time sweep (default 2, 0 disables), and route structured `no-writes` stalls through the existing public-title planner notice and grooming paths.
|
|
88
91
|
- At done, add `no writes recorded since acceptance` to review only when the daemon proves no owner touches since the first acceptance after the current ownership boundary and an empty acceptance-to-now diff, with no branch, review head or PR evidence. Preserve that baseline and earlier touches across plan-revising re-acceptance. Require a canonical-root D2 result to pass the same elsewhere-hint and exclusive-current-worktree checks before treating it as negative evidence. Bind extra-instance evidence to its current verified execution owner/root; missing, private, disabled, expired-budget or restart-lost baselines stay unknown. Capture through the guarded `TasksDeps.ownerAccepted` callback for explicit accept/self-claim only; an implicit done acceptance has no pre-work baseline and remains unknown. Do not capture private work for this advisory, block done, or accept evidence flags from models.
|
|
89
92
|
|
|
90
|
-
- `ahub report --dogfood` (#687, `src/hub/dogfood.ts`, `src/hub/dogfood-read.ts`) opens `hub.db` read-only with byte and row caps; an unavailable or incomplete source reports `null`, never 0, and per-PR GitHub reads run only when the
|
|
93
|
+
- `ahub report --dogfood` (#687, `src/hub/dogfood.ts`, `src/hub/dogfood-read.ts`) opens `hub.db` read-only with byte and row caps; an unavailable or incomplete source reports `null`, never 0, and per-PR GitHub reads run only when the PII inventory is complete. That inventory (`dogfoodPiiInventory`, #1147: ids and PR refs of PII tasks, no text, 2000-task cap) is read from the whole board, while `dogfoodTasks` takes a `{since, until}` window and the PR numbers of the merged PRs, so a board over 500 tasks still reads complete and every sibling task of an in-window PR is read whatever its update time (a partial sibling set would give wrong lead times, so overflow makes the source incomplete); `dogfoodPrs` without an inventory argument still uses the task list as the inventory.
|
|
91
94
|
- Qualified titles (#723): `planIssueIdentity` links a sibling `repo#N` only when `refs.repo` names that repository (`siblingTitleRefs`, `spec-read.ts`), and an `owner/repo#N` only when it is `refs.repo`, else this project's repository; a sibling shorthand beside a bare `#N` is two identities and links neither. A title that names another repository with no `refs.repo` does not link, and `refsRepoHint` tells the proposer to set it (advice in the `hub_task_propose` result, never a refusal; none in mode off, for a PII task or once `refs.repo` or an issue is set). There is no retitle path on the board: a title is fixed at propose.
|
|
92
95
|
|
|
93
96
|
- Planner walk and reviewer independence (#525-3): `plannerWalk` and `implementers` in `src/hub/routing.ts` are pure; the planner is chosen only inside `assign()` (`planners`, `namedPlanner`, `keptPlanner` in, `Assignment.planner`/`plannerReason` out), and `Tasks` never picks one outside it. `Tasks.weights()` is the one place `planners` is built (rostered holders of `d.roles`, sorted), so every `assign()` path and `explain` agree. Pass a bound task as `keptPlanner` to that same walk: its explanation names the binding as `kept`; drafts and legacy null bindings still rotate, and explanation never writes a binding or history. Rotation is `task.id mod n` over every holder and is never renumbered by availability; a named planner is exact and never replaced. I-2 (`implemented this task`) lives in `blocked()` for the reviewer role as its last capability/independence check before queue admission, so the legacy, named and pool paths share it and it only ever removes an otherwise eligible candidate; when I-2 removed every otherwise eligible reviewer, the person reviews with `reviewBlocked`, never an implementer.
|
|
@@ -105,7 +108,8 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
105
108
|
- 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
109
|
- 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
110
|
|
|
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
|
|
111
|
+
- 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`.
|
|
112
|
+
- 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
113
|
- 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
114
|
- 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
115
|
- 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,8 @@ 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
|
+
- On Bun 1.4.2, preserve original fields before `toMatchObject` with asymmetric matchers, or assert primitive fields and strings separately; never use a matcher-mutated value for a later content/canary assertion. Recheck this restriction when upgrading Bun.
|
|
8
|
+
- 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
9
|
- 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
10
|
- 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
11
|
- 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.
|
|
@@ -28,6 +30,7 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
|
|
|
28
30
|
- Run `bun scripts/seeded-check.ts` before reporting the seeded-guard gate complete. After changing seeded guard logic, run its affected seeds even when search-anchor counts are unchanged. Put asynchronous calls inside `resolves`/`rejects` expectations so a mutated refusal fails a named assertion. Keep the `scripts/seeds.json` cases sequential; require the same named test green before its seeded assertion fails. Seed rot, survivors, setup/compiler failures, timeout/watchdog termination and process leaks fail the gate. A change that moves or rewrites a line a `scripts/seeds.json` case searches for updates that case's search and replacement in the same PR (seed rot fails the gate), keeping its mutant effect and named test. `scripts/check.sh`, `--changed` included, never runs the seeds, so rot shows only in CI's `seeded guards` job (the `profile-cli-dry-flags` anchor needed a search-text update in both #1100 and #1110, 2026-11-02). Before pushing a change to a file `scripts/seeds.json` names, confirm every anchor still matches once: `bun -e 'const f=require("fs");for(const x of JSON.parse(f.readFileSync("scripts/seeds.json","utf8")).seeds){const n=f.readFileSync(x.file,"utf8").split(x.search).length-1;if(n!==1)console.log("seed rot:",x.id,n)}'` prints nothing when every anchor is intact. Never run the full seed runner from a unit test or mutate the source checkout; inspect preserved failed fixtures before removing them. CI runs it as four sequential Linux shards (`--shard i/4`) beside the ordinary check, separately from `scripts/check.sh`; the required `seeded guards` job passes only when every shard passed, or when the decision job reused a verified identical-tree full gate (`admission == 'reuse'`). Keep that job name: branch protection and `scripts/ci-gates.mjs` read it (#457). On seed rot the checker names the anchor's symbol at a base commit and where it is now (`seedRotContext`): `--base <commit>` sets that base (CI passes the PR base from a `fetch-depth: 0` checkout; keep both), and without it the base is `merge-base origin/main HEAD`. The context is evidence only: it never relaxes the one exact match, and an unavailable base says so instead of guessing.
|
|
29
31
|
- `scripts/lib/ts-ast.ts` is the one TypeScript AST helper for scripts and source-scanning tests (`parseSource`, `callSites`, `symbolSpans`, `enclosingSymbol`, `findSymbol`; inline HTML scripts keep their offsets). It gives lexical locations only, never resolved call edges, and stays dev tooling: never import it from `src/`.
|
|
30
32
|
- The native branch ratchet (`test/daemon-native-branches.json`) is reviewed data, never regenerated by a test (rule: `adapters.md`). `test/daemon-native-proof-gates.test.ts` runs daemon code cut out of `src/hub/daemon.ts` by its text, from ` function frontendProxyState(` to ` const manualPaused =` and from ` async function changePermission(` to ` async function confirmPermissionDefault(`, and the ` const isSupportedPeer =` and ` const readOnlyPeer =` bindings: renaming, reordering or reindenting them updates the test in the same PR. Test Claude launch permission proof through `createClaudeLaunchProof` in `src/hub/claude-launch-proof.ts`; never restore the removed daemon wrapper just to slice it in a fixture.
|
|
33
|
+
- The #1057 worktree tests cut source by its text: `test/worktree-shelf.test.ts` reads `src/hub/daemon.ts` from `case "worktrees":` to `// #604 (agent-hub#1030)`; `test/worktree-panel.test.ts` reads it from `async function worktreeShelf(` and `function worktreesView(` to `async function uiMemorySnapshot(`, `src/ui/index.html` from `function renderWorktrees(shelf)` to `function renderBench(bench)` (plus the helper lines it names) and `src/hub/sdlc.ts` from `export async function readSdlcPr(` to `export async function readSdlcPrHead(`. Moving, renaming or reordering those lines updates the test in the same PR. The removal-module lists pinned there and in `test/worktree-tidy.test.ts` (`src/hub/code-facts.ts`, `src/hub/task-worktree.ts`, `src/cli/worktree-tidy.ts`) are never extended to let a new removal pass (rule: `daemon.md`).
|
|
31
34
|
|
|
32
35
|
- `scripts/check.sh` pins the test environment (#610): `GIT_CONFIG_GLOBAL=/dev/null`, `NO_COLOR` unset, `TERM=xterm-256color`, temp root under the OS per-user temp dir, agent markers cleared. A test that needs a person's git config or color settings sets them itself. `--changed` selects tests with `scripts/changed-tests.mjs`: changed tests (nested suites included) plus every test that names a changed file directly; docs need none; any other changed file no test names, and any gate, package or shared-fixture change, runs everything.
|
|
33
36
|
- `scripts/check.sh` holds the repo's heavy-job slot (`<git common dir>/ahub-heavy.lock`, #398) for the whole run, so a test never runs the real gate; `test/check-lock.test.ts` runs a copy in a scratch repo.
|
package/docs/design/adapters.md
CHANGED
|
@@ -71,6 +71,62 @@ Native CLI compatibility is centralized in `compat.ts`. Each gated operation ask
|
|
|
71
71
|
|
|
72
72
|
External adapter modules are an explicit, person-owned, machine-local opt-in. The daemon verifies and pins the captured bundle at startup, then admits only the bounded headless ACP instance contract through host-managed launch and current-owner checks. Module code runs in the daemon trust domain; pinning is integrity evidence, not a sandbox. Modules cannot supply native reviewer engines, proof ports, permission callbacks, provider selectors, or arbitrary recovery authority. This ceiling limits what imported code can claim and prevents live reload from replacing the executable catalog ([#754](https://github.com/STAIxBWLB/agent-hub/issues/754)).
|
|
73
73
|
|
|
74
|
+
## Session title slots
|
|
75
|
+
|
|
76
|
+
Only fixed Pi and local definitions opt into `title: "hub"` (#1055). Their
|
|
77
|
+
standalone `hub_set_title` schema is excluded from native-peer tool lists, and
|
|
78
|
+
the daemon independently checks the resolved definition and current owner.
|
|
79
|
+
An agent fills only an unnamed slot; a person's console rename replaces it.
|
|
80
|
+
Pi's native session name is canonical and its bridge rechecks the native name,
|
|
81
|
+
owner, session and generation before writing. Local keeps its name in the
|
|
82
|
+
worker's existing in-memory session. Both expose a separate `titleSession`
|
|
83
|
+
identity for display continuity; Local's native-session/provenance authority
|
|
84
|
+
does not change. Titles pass the current metadata/privacy screen at write and
|
|
85
|
+
display. No helper inference or task/Git/instance naming path is involved.
|
|
86
|
+
|
|
87
|
+
Refusals keep the `already_named`, `private` and `invalid` reason codes and
|
|
88
|
+
include a public `error` explaining the cause and next step. Errors identify
|
|
89
|
+
length, offline/owner, session/generation and confirmation failures without
|
|
90
|
+
copying the supplied title or private details. If a write might have landed,
|
|
91
|
+
the result says it may have been applied and directs the caller to inspect
|
|
92
|
+
`ahub status` before retrying.
|
|
93
|
+
|
|
94
|
+
An unnamed label is derived from the first positively acknowledged original,
|
|
95
|
+
never stored as a name. The #1055 restart-recomputation design is amended with
|
|
96
|
+
`session-title-inputs.json` under the daemon state directory: the journal does
|
|
97
|
+
not itself bind an original delivery to the recipient's native session.
|
|
98
|
+
Only fixed definitions declaring `titlePersistence: "native"` retain restart
|
|
99
|
+
bindings. This private, atomically replaced file maps each retained peer ID
|
|
100
|
+
to exactly three opaque IDs, `session`, `delivery` and `envelope`. It contains no source body,
|
|
101
|
+
session title or derived label. Only the first positive delivery acknowledgement
|
|
102
|
+
for a peer's current title-session can create or replace its entry;
|
|
103
|
+
`hub_set_title` and person rename never write it.
|
|
104
|
+
|
|
105
|
+
The file is limited to 128 peer entries and 64 KiB of serialized UTF-8,
|
|
106
|
+
including JSON formatting and the final newline. Each stored ID is nonempty,
|
|
107
|
+
at most 512 UTF-16 code units, and excludes control and format characters.
|
|
108
|
+
An unreadable, malformed, oversized, symlinked or multiply linked input is
|
|
109
|
+
ignored. Capacity or write failure leaves delivery successful and makes that
|
|
110
|
+
new restart binding unavailable.
|
|
111
|
+
|
|
112
|
+
Native-session bindings survive daemon stop/restart until replaced by the
|
|
113
|
+
first acknowledged delivery of a later session for that peer, or until the
|
|
114
|
+
state directory is removed. Entries for removed instances are dropped when
|
|
115
|
+
their removal applies. Local's session exists only in daemon memory and gets
|
|
116
|
+
a fresh ID each start, so its binding is not retained on disk; startup removes
|
|
117
|
+
legacy Local entries. A stale native-session entry grants no display value: restore
|
|
118
|
+
requires the same title-session ID and the original still retained in that
|
|
119
|
+
project's journal. A different session or pruned original yields no inherited
|
|
120
|
+
label. There is no separate retention of message text and no timer or automatic
|
|
121
|
+
replay. The derived label remains absent from disk status, events, logs,
|
|
122
|
+
capture and rooms.
|
|
123
|
+
|
|
124
|
+
Incoming-message labels are visible only to the console and the configured
|
|
125
|
+
conductor. Other status readers receive `unnamed`. A label has its own source
|
|
126
|
+
tag and explanation: its text comes from an incoming message and is never
|
|
127
|
+
attributed as speech written by the session's peer. Public-metadata screening
|
|
128
|
+
and private stubs still apply to authorized label viewers.
|
|
129
|
+
|
|
74
130
|
## Evidence boundary
|
|
75
131
|
|
|
76
132
|
Compatibility probes and fake transports verify source contracts, not an account login, live model turn, native permission prompt, or operating-system isolation. Those receipts remain separate because protocol inventory alone cannot prove runtime behavior ([#625](https://github.com/STAIxBWLB/agent-hub/issues/625), [#754](https://github.com/STAIxBWLB/agent-hub/issues/754)).
|
|
@@ -60,6 +60,18 @@ The hub's portable memory storage, sync, indexing, and project helpers come from
|
|
|
60
60
|
|
|
61
61
|
## Privacy and recovery
|
|
62
62
|
|
|
63
|
+
Session titles for the two hub-owned title slots (#1055) are conditional writes
|
|
64
|
+
to the current adapter session, under the same open-PII-task/private-turn and
|
|
65
|
+
public-metadata screens as DOING. Person rename remains console-only. Live
|
|
66
|
+
unnamed labels are computed from acknowledged source messages and shown only
|
|
67
|
+
to the console and configured conductor; other readers see `unnamed`.
|
|
68
|
+
Their source note identifies incoming text, not speech by the recipient.
|
|
69
|
+
Only opaque
|
|
70
|
+
session/delivery/envelope bindings persist in `session-title-inputs.json`, never
|
|
71
|
+
copied labels. Its fields, bounds and lifetime are defined by the
|
|
72
|
+
[session-title storage contract](adapters.md#session-title-slots). Native title,
|
|
73
|
+
task title, Git branch and instance identity remain independent.
|
|
74
|
+
|
|
63
75
|
Privacy decisions are made at hub boundaries, not delegated to model routing. A private task or turn is represented by a private envelope and a restricted public view; task text is screened before it enters memory, shared notices, or model calls. The PII screen can run on-device; its gateway path requires positive campus admission, so an unknown network result is not treated as safe. This makes each downstream observer consume the same host decision rather than infer privacy from a label or vendor ([#198](https://github.com/STAIxBWLB/agent-hub/issues/198), [#752](https://github.com/STAIxBWLB/agent-hub/issues/752)).
|
|
64
76
|
|
|
65
77
|
Recovery is a coordinated daemon operation over separately persisted board, delivery, and native-session evidence. A restart or upgrade can continue only with compatible feature evidence from the verified target and host-held proofs that still match the project and actor. Adapters report native evidence; they do not certify their own recovery authority. This avoids treating a stale status file or an unverified receipt as proof that a native process is safe to resume ([#37](https://github.com/STAIxBWLB/agent-hub/issues/37), [#625](https://github.com/STAIxBWLB/agent-hub/issues/625), [#900](https://github.com/STAIxBWLB/agent-hub/issues/900)).
|
package/docs/design/bus.md
CHANGED
|
@@ -33,6 +33,14 @@ The bus applies limits to a sender, sender-recipient pair, important messages, a
|
|
|
33
33
|
|
|
34
34
|
The in-memory bus schedules messages; `DeliveryJournal` durably records an individual transport attempt and the original envelopes behind any rendered output. A peer receipt such as `accepted` or `completed` describes transport, not the corresponding task's acceptance, completion, or approval. That separation lets a timeout or restart remain explicitly uncertain without inventing a task outcome ([#37](https://github.com/STAIxBWLB/agent-hub/issues/37)).
|
|
35
35
|
|
|
36
|
+
For #1055, `Bus.onDelivered(peer, originals, deliveryId?)` exposes the existing
|
|
37
|
+
journal delivery ID to the observer at the first positive transport ACK. The
|
|
38
|
+
optional argument accompanies the original envelopes, including when the
|
|
39
|
+
rendered delivery was condensed. Queueing supplies no positive-ACK evidence,
|
|
40
|
+
and an observer exception cannot alter delivery settlement. This supports the
|
|
41
|
+
[ID-only session-title restart binding](adapters.md#session-title-slots) without
|
|
42
|
+
changing dispatch, retry, task acceptance or task-completion authority.
|
|
43
|
+
|
|
36
44
|
Push peers receive bounded batches. Condensation is optional, excludes important, remote, and invocation messages, and keeps `originals` separate from `out` so replies, safe failures, and recovery still refer to what the peer was actually given. On an eligible safe retry, a failed batch falls back to individual envelopes, and a previously failed envelope goes alone so a poison item cannot block its neighbors. These bounds preserve queue progress when summarization or one recipient fails ([#713](https://github.com/STAIxBWLB/agent-hub/issues/713), [#553](https://github.com/STAIxBWLB/agent-hub/issues/553)).
|
|
37
45
|
|
|
38
46
|
Pull-only peers catch up oldest-first under a byte budget, but the first envelope always goes even when it exceeds that budget so one oversized item cannot starve the queue; push peers use the digest batch limit. Pull records returned envelopes as completed at handout because the tool result is the readback, not a later native receipt. A busy Claude channel can also pull its own messages through `hub_inbox`; that avoids making a long-running turn an unbounded queue lock ([#493](https://github.com/STAIxBWLB/agent-hub/issues/493), [#713](https://github.com/STAIxBWLB/agent-hub/issues/713)).
|
|
@@ -47,6 +47,8 @@ paths:
|
|
|
47
47
|
- src/cli/handoff-args.ts
|
|
48
48
|
- src/cli/handoff-output.ts
|
|
49
49
|
- src/hub/task-list-page.ts
|
|
50
|
+
- src/hub/worktree-shelf.ts
|
|
51
|
+
- src/hub/worktree-prs.ts
|
|
50
52
|
---
|
|
51
53
|
|
|
52
54
|
# Task Board
|
|
@@ -69,9 +71,17 @@ Automatic offer and backlog paths respect `plannedStart`; a future card becomes
|
|
|
69
71
|
|
|
70
72
|
Urgent acceptance can reclaim only enough unaccepted, unstarted, non-pinned and non-urgent offers to fit the limit. It does so after other admission checks and immediately before the accept write; if insufficient, it reclaims none. Accepted work is never displaced merely to satisfy a new urgent request, because an offer and an active commitment have different costs ([#1080](https://github.com/STAIxBWLB/agent-hub/issues/1080)).
|
|
71
73
|
|
|
74
|
+
Untouched returns may atomically update `reserved` while moving eligible proposed or accepted work back to Backlog. `Tasks` retains the existing authority, execution-evidence, pin, release and reservation gates, including post-screen rechecks. An explicit reservation patch advances planning revision and records its prior value in the same state/history write, before capacity publication can trigger auto-pull. Clearing it therefore parks the card; omitting fields preserves its planning. Assignment and auto-pull selection remain the existing rules, with no time-based suppression ([#1154](https://github.com/STAIxBWLB/agent-hub/issues/1154)).
|
|
75
|
+
|
|
76
|
+
## Source-backed plans and request intake
|
|
77
|
+
|
|
78
|
+
For a profile that supports `plan.sources`, `Tasks` validates every retained source ID against the source registry at proposal and acceptance, before board writes, admission effects or peer notices. Unknown IDs are refused with the ID and `ahub source list` guidance. Overlap notices compare the registry IDs; private task plans remain withheld by the shared public view.
|
|
79
|
+
|
|
80
|
+
Natural-language intake from the CLI or dashboard enters the same `Tasks.intake` path and creates held draft Backlog cards, not offers. Dashboard task replies use the public view, and a ticket-bound live-test session cannot create these person-attributed drafts. Approval and discard retain their existing authority. Read-only source snapshots carry only ID, kind, byte size, hash prefix and registration time, with no content or origin paths ([#1144](https://github.com/STAIxBWLB/agent-hub/issues/1144)).
|
|
81
|
+
|
|
72
82
|
## Completion, review and evidence
|
|
73
83
|
|
|
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)).
|
|
84
|
+
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
85
|
|
|
76
86
|
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
87
|
|
|
@@ -84,3 +94,11 @@ Agent list reads return bounded, newest-updated compact pages built from public
|
|
|
84
94
|
Lifecycle policy can be off, advisory or enforced; this is separate from always-applicable role and privacy invariants. Plans, cost/check scope, review findings and external evidence retain their own typed provenance. Research campaigns and experiments likewise distinguish declared, host-observed and runner-attested facts instead of promoting prose to a signed result ([#481](https://github.com/STAIxBWLB/agent-hub/issues/481), [#932](https://github.com/STAIxBWLB/agent-hub/issues/932), [#1018](https://github.com/STAIxBWLB/agent-hub/issues/1018)). Detailed research contracts remain in the [campaign design](../specs/2026-11-01-research-campaigns-design.md).
|
|
85
95
|
|
|
86
96
|
When the specs policy is enabled, covering design specs participate in proposal/accept notices and completion/review reports. Report mode informs; require mode needs an updated spec or a recorded acknowledgement for each applicable unchanged spec. Commitless and verified no-change work remains quiet, and private evidence is withheld rather than exposed to satisfy a report. These modes preserve owner choice while making design drift visible ([#1064](https://github.com/STAIxBWLB/agent-hub/issues/1064)).
|
|
97
|
+
|
|
98
|
+
## Worktree shelf (#1057)
|
|
99
|
+
|
|
100
|
+
The board stores facts about worktrees and never a verdict. `worktree_override` holds only a person's Park (`settled`) or Pin (`active`) per absolute path and its time; whether a worktree is settled is derived on every read by the pure `partition` from the git facts, the bound task (the open or latest task whose `refs.branch` is the worktree branch, never a PII task), the PR state and those overrides. An override counts only while no newer activity exists, and a later write removes an inert one.
|
|
101
|
+
|
|
102
|
+
The PR state has two sources and no others. The board's own evidence says `merged` only through the guarded merge's record (`sdlc.merge`); a cancelled task or a task with no record says nothing about its PR and reads `unknown`. The person's explicit refresh (`worktrees_refresh_prs`, `src/hub/worktree-prs.ts`) reads the PR through `readSdlcPr`, bounded and one at a time, and keeps `open`, `merged` or `closed` with GitHub's own change time in the daemon's memory. `worktree-shelf.ts` holds no GitHub access, so a panel read, `ahub worktrees` and the inbox alert make no GitHub call.
|
|
103
|
+
|
|
104
|
+
A worktree whose activity could not be read is live and never settled. The shelf reads git asynchronously under a total budget and answers `complete: false` when it is cut; the daemon's event loop is never held. The daemon deletes no worktree and no local branch: its git port allows three read argv shapes, and removal exists only in the person's own `ahub worktrees tidy --confirm`, which is console class and issues `git worktree remove` and `git branch -d`, never `--force` or `-D`. Operations: `docs/operations.md`, "The worktree shelf (#1057)".
|