@staix/agent-hub 0.12.42 → 0.12.44
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 +30 -0
- package/README.md +5 -4
- package/docs/adding-an-ai-tool.md +140 -0
- package/docs/agent-notes/README.md +5 -0
- package/docs/agent-notes/adapters.md +11 -4
- package/docs/agent-notes/budget.md +2 -2
- package/docs/agent-notes/bus.md +2 -0
- package/docs/agent-notes/daemon.md +15 -6
- package/docs/agent-notes/tasks.md +7 -2
- package/docs/agent-notes/tests.md +6 -2
- package/docs/operations.md +159 -26
- package/docs/quickstart.md +11 -3
- package/docs/sdlc.md +1 -1
- package/docs/security.md +7 -5
- package/docs/specs/2026-09-19-agent-hub-design.md +209 -13
- package/docs/verification/2026-10-21-0.12.43.md +50 -0
- package/docs/verification/2026-10-21-0.12.44.md +54 -0
- package/docs/verification/2026-10-21-agy-1.4.3-t0.md +65 -0
- package/docs/verification/2026-10-21-grok-config-acp-t0.md +30 -0
- package/docs/verification/2026-10-21-pii-screen.md +14 -0
- package/docs/verified.json +34 -19
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +808 -255
- package/src/adapters/acp-owned-group.ts +124 -0
- package/src/adapters/acp.ts +92 -25
- package/src/adapters/agy/definition.ts +1 -3
- package/src/adapters/agy/entry.ts +4 -1
- package/src/adapters/agy/hooks.ts +7 -2
- package/src/adapters/channel-peer.ts +143 -0
- package/src/adapters/claude-channel.ts +7 -7
- package/src/adapters/codex-appserver.ts +16 -2
- package/src/adapters/configured-acp.ts +124 -0
- package/src/adapters/definitions/builtin-metadata.ts +37 -33
- package/src/adapters/definitions/native-channel-state.ts +41 -0
- package/src/adapters/definitions/native-cli-entry-kimi.ts +3 -1
- package/src/adapters/definitions/native-cli-preview-qwen.ts +1 -1
- package/src/adapters/definitions/native-identity-defaults.ts +14 -0
- package/src/adapters/definitions/qwen.ts +3 -8
- package/src/adapters/definitions/reviewer-variants.ts +3 -7
- package/src/adapters/jcode.ts +4 -0
- package/src/adapters/local-worker.ts +4 -0
- package/src/adapters/pi.ts +50 -16
- package/src/adapters/registry-types.ts +18 -16
- package/src/adapters/registry.ts +68 -6
- package/src/adapters/reviewer-print.ts +4 -0
- package/src/cli/agy-mcp.ts +3 -2
- package/src/cli/completion.ts +173 -0
- package/src/cli/configured-acp-entry.ts +58 -0
- package/src/cli/console-state.ts +116 -24
- package/src/cli/console.ts +9 -6
- package/src/cli/facts-hook.ts +2 -1
- package/src/cli/help.ts +8 -1
- package/src/cli/identity.ts +8 -7
- package/src/cli/launch.ts +5 -4
- package/src/cli/main.js +5 -1
- package/src/cli/main.ts +84 -27
- package/src/cli/memory.ts +13 -10
- package/src/cli/output.ts +1 -0
- package/src/cli/preview.ts +9 -0
- package/src/cli/recovery-guidance.ts +1 -1
- package/src/cli/terminal-recovery.ts +33 -21
- package/src/cli/upgrade-restore-state.ts +3 -2
- package/src/cli/upgrade-runtime.ts +21 -6
- package/src/cli/upgrade.ts +3 -2
- package/src/hub/board.ts +9 -6
- package/src/hub/budget.ts +9 -2
- package/src/hub/bus.ts +6 -2
- package/src/hub/claude-state.ts +2 -37
- package/src/hub/conductor.ts +8 -6
- package/src/hub/config-apply.ts +26 -14
- package/src/hub/config-reload.ts +39 -11
- package/src/hub/config-trust.ts +1 -1
- package/src/hub/context-window.ts +6 -0
- package/src/hub/control-client.ts +1 -1
- package/src/hub/crash.ts +8 -6
- package/src/hub/daemon.ts +471 -338
- package/src/hub/hub-tools.ts +37 -1
- package/src/hub/instance-config.ts +20 -11
- package/src/hub/instance-execution.ts +4 -4
- package/src/hub/instances.ts +45 -21
- package/src/hub/lifecycle-schema.ts +5 -3
- package/src/hub/main-checkout.ts +2 -2
- package/src/hub/manager.ts +10 -2
- package/src/hub/memory-profile.ts +16 -3
- package/src/hub/no-writes.ts +50 -0
- package/src/hub/permission-mode.ts +9 -7
- package/src/hub/roster.ts +7 -4
- package/src/hub/routing.ts +21 -20
- package/src/hub/settings.ts +51 -17
- package/src/hub/snapshots.ts +276 -17
- package/src/hub/start-mode.ts +10 -8
- package/src/hub/task-sweep.ts +8 -2
- package/src/hub/tasks.ts +101 -19
- package/src/memory/capture.ts +2 -1
- package/src/ui/index.html +5 -0
- package/templates/AGENTS.block.md +1 -1
- package/templates/profiles/general.json +1 -1
- package/templates/profiles/research.json +1 -1
- package/templates/profiles/writing.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the previous repository, archived on 2026-09-30 when this repository's history was rewritten; the one exception is the open smoke-check issue, formerly #12, which moved here as #1. Numbers in newer entries refer to this repository.
|
|
4
4
|
|
|
5
|
+
## 0.12.44
|
|
6
|
+
|
|
7
|
+
A standard ACP agent can join a project from config alone (#451 A3), adapter definitions become the single dispatch point (#451 A2b), agy stops reading Claude's state files (#624), and owner turns that write nothing are flagged (#498).
|
|
8
|
+
|
|
9
|
+
- Add project-local config-only ACP adapters: optional headless kinds stay off roster by default, with pinned native identity, strict resume identity, owned-group admission and optional mapped or read-only native modes. Grok live verification remains pending; the generic path does not by itself verify Grok support (#451, #486). Adapter role defaults cannot appoint a conductor; explicit project role policy remains available. Unknown fields in `peers.<id>` now refuse daemon startup as well as strict reload.
|
|
10
|
+
- `ahub permission` and `ahub settings` reach the running hub even when `.agenthub/config.local.json` does not parse, and `ahub settings undo` asks for the peer-id confirmation of a configured kind's roster undo (#638 follow-up).
|
|
11
|
+
- agy no longer shows Claude's context reading, session or transcript usage as its own: Claude state files are read only for Claude peers, and the shared file-name helper refuses any other id (#624). An upgrade no longer refuses a planned fresh start of a `codex-<name>` instance, and native observation uses the sending peer's own roles.
|
|
12
|
+
- Owner turns that write nothing are flagged: two completed `implement` or `bulk_edit` owner turns in a row with no recorded writes or task-tool effects raise a planner notice, and a done report with no writes since acceptance says so in its review request (`task_sweep.no_writes_turns`, default 2, 0 disables). Evidence that cannot be proven, such as private turns, work under `.worktrees` or a recorded branch, PR or head, stays unknown and never counts as empty (#498).
|
|
13
|
+
- In the agy TUI the hub tools can be missing on the first turn: the launcher says so and the `AGENTS.md` block tells agents to use them from the next turn instead of reporting the hub unavailable; run `ahub init` to refresh the block (#607).
|
|
14
|
+
- An invalid `kimi_cmd` releases the Kimi TUI admission instead of holding it (#549).
|
|
15
|
+
- Codex shows a fixed `unidentified-cancellation` status notice in `ahub status`, the console and the dashboard while its offline latch holds; verified reattachment or TUI detach clears it (#451).
|
|
16
|
+
- The recovery lease logs its release retry once per failure streak instead of once a second (#479).
|
|
17
|
+
- Add "Adding an AI Tool" guide (`docs/adding-an-ai-tool.md`) documenting Adapter API 1 definitions, transport selection, and the conformance test kit (#451 A5).
|
|
18
|
+
- Adapter declarations now refuse ordinary agy extra instances at config load; they previously loaded as inert pull-only extras. Fresh-reviewer metadata remains unavailable (#451, #625).
|
|
19
|
+
- Qwen context occupancy is now unknown until a native source is verified; it no longer inherits Kimi's usage parser (#451, #625).
|
|
20
|
+
- When the hub itself starts headless Kimi or Qwen it also opens `ahub watch <peer>` through the terminal provider, a fixed command opened at most once per 30 s; with no provider or a failing one the start still succeeds and the watch command is reported (#286 AC10).
|
|
21
|
+
|
|
22
|
+
## 0.12.43
|
|
23
|
+
|
|
24
|
+
A busy daemon no longer stalls on git HEAD movement under open turns (#576); upgrade restore, task assignment and the agy peer get fixes, `ahub completion` arrives, and the verification gate changes for contributors (#609, #610).
|
|
25
|
+
|
|
26
|
+
- The daemon stays responsive when the checkout's HEAD moves (fast-forward, checkout, worktree merge) while turns are open: snapshot and diff Git calls in one burst share a 250 ms budget and an exhausted budget records unknown evidence, concurrent-edit notices go out once per affected task instead of once per task pair, and HEAD moves are classified from the worktree's HEAD reflog so an incoming checkout, merge or fast-forward is not attributed to a peer while the peer's own commit, amend, rebase replay or cherry-pick still is; ambiguous history stays unknown and paths still dirty at turn end are always kept. On a scratch burst `status` went from 21.3 s to 0.27 s and false conflicts from 144 to 0 (#576).
|
|
27
|
+
- An upgrade or restart no longer blocks on a restored Claude session whose launch record or terminal identity appears a few seconds after its terminal reads idle: the mapping is re-read until the restore deadline, and only a positive contradiction or the deadline blocks (#599).
|
|
28
|
+
- `hub_task_assign` and `ahub task assign` refuse with the routing reason (offline, not attached, excluded, plan review missing ...) instead of returning the unchanged task, and a proposal whose named owner could not take it says why in its result (#601).
|
|
29
|
+
- `ahub completion zsh|bash|fish` prints a completion script; candidates come from the help entries through the hidden `ahub __complete`, so they cannot drift from `ahub help` (#603).
|
|
30
|
+
- agy: native file reads are recorded as peer facts, for agy 1.4.2's `view_file` and 1.4.3's `read_file`, with a line range counted as a partial read (#555). agy 1.4.3 stays unverified (T0 re-run, #267); in its TUI the hub tools appear from the second turn (#607).
|
|
31
|
+
- The console's Peers tab shows each peer's planner WIP against its limit, and `w` edits the limit through the existing settings flow, for declared instances too; the Tasks tab toggles a ranked backlog view (`b`) and adds, pulls, plans, reserves, returns and cancels backlog work from the row, each action bound to the row's id and revision and confirmed with `y` (#532).
|
|
32
|
+
- A manager open that loses the race to another manager stops the process it spawned (#584).
|
|
33
|
+
- Contributors: CI runs per pull request (`check-pr-<number>`, a newer pending run replaces the older) instead of one repository-wide queue; `scripts/check.sh --changed` runs only the tests a change touches, and the gate pins its test environment (`GIT_CONFIG_GLOBAL=/dev/null`, no `NO_COLOR`, `TERM=xterm-256color`, temp root under the OS per-user temp dir); the full gate is CI's (#609, #610). Two timing-dependent tests were fixed (#617, #618). The live PII screen calibration is recorded with the served model id (#198).
|
|
34
|
+
|
|
5
35
|
## 0.12.42
|
|
6
36
|
|
|
7
37
|
`ahub agy` starts again after the Antigravity CLI updated itself (#589); the rest is contributor tooling (#591, #593, #595).
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Codex, Kimi Code, Pi and a hub-owned local worker (Qwen Code, Jcode and Antigrav
|
|
|
5
5
|
optional) share a message bus and a task board per project and one memory on the
|
|
6
6
|
machine, each attached through its own native interface.
|
|
7
7
|
|
|
8
|
-
Status: 0.12.
|
|
8
|
+
Status: 0.12.44, control protocol 16. Bun + TypeScript, one loopback daemon per
|
|
9
9
|
project. Site: [agent-hub.staix.net](https://agent-hub.staix.net).
|
|
10
10
|
|
|
11
11
|
## What it does today
|
|
@@ -42,7 +42,8 @@ project. Site: [agent-hub.staix.net](https://agent-hub.staix.net).
|
|
|
42
42
|
running sessions across versions. Settings reload without a restart where their
|
|
43
43
|
apply class allows; guarded values wait for a person's confirmed
|
|
44
44
|
`ahub settings reload` in a terminal. The console supports revision-checked task
|
|
45
|
-
assignment/unassignment
|
|
45
|
+
assignment/unassignment, delivery-queue resolution, per-peer planner WIP limits
|
|
46
|
+
and backlog management from the Tasks tab. Peers with independent
|
|
46
47
|
tool turns can request one uncertain redelivery; the delivery-only local worker
|
|
47
48
|
and headless ACP peers leave held deliveries for the person to resolve.
|
|
48
49
|
|
|
@@ -87,14 +88,14 @@ keeps headless startup. On 0.12.39 the TUI launcher stopped after the first hub
|
|
|
87
88
|
0.12.40 opens it again (#564). On 0.12.40 `ahub codex` in a recorded Orca terminal
|
|
88
89
|
failed with `invalid remote address`; 0.12.41 starts it again (#586). Since 0.12.42
|
|
89
90
|
`ahub agy` accepts the Antigravity CLI 1.4.x line (1.4.2 verified; other patches warn),
|
|
90
|
-
because agy updates itself and 0.12.41 refused 1.4.3 (#589).
|
|
91
|
+
because agy updates itself and 0.12.41 refused 1.4.3 (#589). Since 0.12.43 agy's native reads are recorded as peer facts on 1.4.2 and 1.4.3 (#555); 1.4.3 stays unverified, and in its TUI the hub tools appear from the second turn (#267, #607). Since 0.12.44 a headless Kimi or Qwen start made by the hub also opens `ahub watch <peer>` through the terminal provider (#286), an owner turn streak with no recorded writes raises a planner notice (`task_sweep.no_writes_turns`, default 2, 0 disables; #498), a declared agy extra instance is refused at config load (#625), and a standard ACP agent can join a project as a config-only adapter declared in untracked `.agenthub/config.local.json`: headless, off roster by default and pinned to its native identity (#451).
|
|
91
92
|
Cancellation needs a daemon advertising `task_cancel`; controlled downgrades
|
|
92
93
|
refuse while cancelled rows exist. Live-test delegated CLI commands need a daemon
|
|
93
94
|
advertising `cli_audit_delegated` and refuse with an upgrade remedy otherwise
|
|
94
95
|
(#410). For local/Pi implementation, opt in to an available
|
|
95
96
|
`checks.runner = "container"`; issue-backed owners also need authenticated
|
|
96
97
|
spec access. PII work and designated reviewers retain their separate exemptions.
|
|
97
|
-
See [release verification](docs/verification/2026-10-
|
|
98
|
+
See [release verification](docs/verification/2026-10-21-0.12.44.md) for the live legs
|
|
98
99
|
still open, including #418 AC8, #438 AC7, the native console settings leg (#439 AC5),
|
|
99
100
|
real upgrades with reattach (#463) and with a managed terminal and cancelled tasks
|
|
100
101
|
(#478), a live headless Claude review (#223), a Claude session over 32 MiB (#517), a
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Adding an AI Tool (Adapter Guide)
|
|
2
|
+
|
|
3
|
+
This guide documents how to add an AI coding agent or tool to **agent-hub** as an adapter. agent-hub treats every agent as a peer in a shared project directory, coordinating through messages, task board assignments, shared memory, and lifecycle gates.
|
|
4
|
+
|
|
5
|
+
Under **Adapter API 1** (issue #451), every tool is integrated as an `AdapterDefinition`. The core daemon, task board, and CLI interact only through this typed contract, without hard-coded vendor name comparisons.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Architecture & Principles
|
|
10
|
+
|
|
11
|
+
1. **One Definition per Kind**: Each tool is identified by a unique `kind` (e.g., `codex`, `kimi`, `agy`, `claude`, `jcode`, `pi`, `local`, `qwen`). Its capabilities, transport, launch argv, permission mappings, and recovery contracts are encapsulated in an `AdapterDefinition`.
|
|
12
|
+
2. **Core Independence**: Core daemon logic (`src/hub/daemon.ts`), task management (`src/hub/tasks.ts`), and routing (`src/hub/routing.ts`) inspect definition fields (such as `transport`, `identity`, `attach`, `budget`), never raw vendor names.
|
|
13
|
+
3. **Strict Conformance**: Every builtin adapter must run the conformance test kit (`test/conformance/`). Unsupported legs must provide explicit, documented reasons rather than silent test skips. (External adapter enrollment lands with A4.)
|
|
14
|
+
4. **Trust & Security Boundaries**:
|
|
15
|
+
- **On-prem vs. Cloud**: `trust.onPrem` controls whether private/PII tasks may be routed to the agent. A cloud agent (`trust.onPrem: false`) never receives PII task text or memory entries.
|
|
16
|
+
- **Path Protection**: Any workspace configuration directory owned by the agent (e.g., `.agents/`, `.codex/`) must be listed in `AGENT_CONFIG_SEGMENTS` (`src/hub/permission-mode.ts`) so agents cannot self-modify their own hooks or plugins.
|
|
17
|
+
- **Process Isolation**: Spawns must run in their own process groups and are stopped as a whole group by the hub.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 2. Choosing a Transport
|
|
22
|
+
|
|
23
|
+
agent-hub supports five transport architectures (`transport` in `AdapterDefinition`):
|
|
24
|
+
|
|
25
|
+
| Transport | Description | Best Suited For | Examples |
|
|
26
|
+
| :--- | :--- | :--- | :--- |
|
|
27
|
+
| **`acp`** | Agent Client Protocol over stdio JSON-RPC | Agents implementing the standard ACP specification | Kimi, Qwen |
|
|
28
|
+
| **`codex-appserver`** | Bidirectional JSON-RPC with proxy intercept and thread tracking | Tools with structured thread resume and proxy-injectable RPC | Codex |
|
|
29
|
+
| **`claude-channel`** | MCP plugin running inside the host agent with WebSocket control link | Agents with native plugin systems that push notifications | Claude Code |
|
|
30
|
+
| **`ws-pull`** | Workspace MCP plugin connecting to daemon WebSocket; pull-only message inbox | Agents with local plugin discovery and interactive TUIs | Google Antigravity (`agy`) |
|
|
31
|
+
| **`hub-native`** | Direct in-process or local sandbox execution | Custom on-prem models, sandboxed local worker, or Pi RPC bridge | `local`, `pi` |
|
|
32
|
+
|
|
33
|
+
### Generic ACP vs. Dedicated Adapter
|
|
34
|
+
|
|
35
|
+
* **Generic ACP (Config-only)**: A3 supports project-local standard ACP (stdio JSON-RPC) definitions without extending the builtin registry. Native support still requires a verified foreground contract and isolated T0 receipt.
|
|
36
|
+
* **Dedicated Adapter**: Current builtin mechanism where each tool exports an `AdapterDefinition` under `src/adapters/` with its transport, hooks, and launch plans.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 3. AdapterDefinition Fields Reference
|
|
41
|
+
|
|
42
|
+
An adapter exports an object conforming to `AdapterDefinition` in `src/adapters/registry-types.ts`. Read that type as the source of truth: most blocks are declared inline there, so there are no separate `Adapter*Definition` types to import, and some members are required. Its top-level fields are:
|
|
43
|
+
|
|
44
|
+
- Required: `api` (always `1`), `kind`, `displayName`, `family`, `transport`, `roles`, `trust` (`onPrem`, `hubSandboxed`), `start`, `identity` (`mcp`), `permission`, `session`, `turnEnd`, `cli`, `display`.
|
|
45
|
+
- Optional: `optional`, `defaultOnRoster`, `enabledSwitchStartOnly`, `instances`, `attach`, `reporting`, `routing`, `budget`, `configuration`, `capabilities`, `hooks`, `reviewerVariant`, `reviewer`.
|
|
46
|
+
- Registry load runs `assertDefinitionCoherence` (`src/adapters/registry.ts`) on every definition and reviewer variant, so an incoherent combination fails at load, not at run time.
|
|
47
|
+
|
|
48
|
+
### Key Sub-Objects
|
|
49
|
+
|
|
50
|
+
#### `start`
|
|
51
|
+
Defines how the agent process is started and commanded:
|
|
52
|
+
* `forms`: `["tui"]`, `["headless"]`, or `["tui", "headless"]`.
|
|
53
|
+
* `defaultForm`: The default startup mode.
|
|
54
|
+
* `launch(ctx)`: Generates the `LaunchPlan` (command, argv, environment variables, launch metadata).
|
|
55
|
+
* `create(ctx)`: Factory constructing the `PeerAdapter` instance for driven daemon peers.
|
|
56
|
+
* `conductorStartable`: Boolean indicating if `ahub up` or conductor can spawn this peer automatically.
|
|
57
|
+
* `ownedFlags`: CLI flags owned by this tool's launcher.
|
|
58
|
+
|
|
59
|
+
#### `identity`
|
|
60
|
+
Defines how the hub MCP server identity reaches the tool:
|
|
61
|
+
* `"acp-session"`: Sent in `session/new` `mcpServers` parameter.
|
|
62
|
+
* `"project-file"`: Written into workspace plugin or config files (e.g. `.agents/plugins/agent-hub/mcp_config.json`).
|
|
63
|
+
* `"channel-plugin"`: Provided by an installed MCP bundle.
|
|
64
|
+
* `"env"`: Passed via environment variables.
|
|
65
|
+
|
|
66
|
+
#### `permission`
|
|
67
|
+
Maps hub permission modes (`ask`, `ask-when-needed`, `never-ask`) to native flags or ACP mode IDs:
|
|
68
|
+
* `grants`: Record mapping `PermissionMode` to native argument/mode string.
|
|
69
|
+
* `unmanaged`: String explanation if native permissions are unmanaged by the hub.
|
|
70
|
+
* `unmanagedForms`: Start forms where permissions remain unmanaged (e.g. `["tui"]`).
|
|
71
|
+
|
|
72
|
+
#### `session`
|
|
73
|
+
Controls session restoration, thread continuity, and crash recovery:
|
|
74
|
+
* `idField`: `"sessionId"` or `"threadId"`.
|
|
75
|
+
* `currentId(peer)`: Extracts the active session or thread ID from a running adapter.
|
|
76
|
+
* `resumeArgv(binding, fresh)`: Builds CLI arguments to resume a persisted session.
|
|
77
|
+
* `transcriptPath(binding)`: Resolves local transcript file path for audit/usage tracking.
|
|
78
|
+
|
|
79
|
+
#### `budget`
|
|
80
|
+
Defines quota and context window consumption readers:
|
|
81
|
+
* `account`: Account identifier for quota grouping.
|
|
82
|
+
* `reader`: `QuotaReader` extracting usage windows from native status outputs.
|
|
83
|
+
* `context`: `ContextReader` measuring token occupancy against window limits.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 4. Config-Only Generic ACP Adapters (#451 A3)
|
|
88
|
+
|
|
89
|
+
Declare `adapters.<kind>` only in a Git-confirmed untracked `.agenthub/config.local.json`. The declaration is wholly machine-local authority and cannot override builtin kinds or select executable modules. Its `transport` is `acp`; it names a command, optional argv, display name and default roles, with optional native identity, permission-mode mappings and read-only mode. Adapter defaults cannot appoint the conductor. A project captures its catalog at startup, so declaration changes require restart.
|
|
90
|
+
|
|
91
|
+
Configured kinds are headless and off roster by default. Before a start can succeed, the native initialize name/version must match the configured pin and the spawned process group must be positively owned. Resume also requires an exact session/load echo. Missing native permission mappings mean unmanaged policy; missing usage/context sources remain exempt/unknown. Titles do not auto-approve tools, and read-only mode requires both native acknowledgement and host mutation fences.
|
|
92
|
+
|
|
93
|
+
Grok declarations require explicit `--no-leader`, since native defaults can inherit ambient `[cli] use_leader`. Grok 1.0.62 remains unsupported: its isolated probe lacked initialize identity and refused session creation. The [T0 receipt](verification/2026-10-21-grok-config-acp-t0.md) records the remaining evidence. The full A3 schema and proof boundary are in the [spec amendment](specs/2026-09-19-agent-hub-design.md#amendment-config-only-acp-adapters-a3-451-486).
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 5. Planned: External Adapter Modules (#451 A4)
|
|
98
|
+
|
|
99
|
+
Support for loading adapter definitions from external modules outside the core repository is planned as board task #135. As decided in issue #451 (P3), external modules will be governed by strict trust rules:
|
|
100
|
+
|
|
101
|
+
* **Machine-Local Only**: Loaded exclusively from local files specified in local configuration; never from repository tracking or network URLs.
|
|
102
|
+
* **Content-Hash Pinning**: Explicit SHA-256 hash pinning to prevent unauthorized tampering.
|
|
103
|
+
* **Person Authority**: Enabling external modules requires explicit human authorization.
|
|
104
|
+
* **Conformance Requirement**: External modules must pass the conformance kit before admission.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 6. The Conformance Kit
|
|
109
|
+
|
|
110
|
+
The conformance kit (`test/conformance/`) validates that an adapter satisfies hub invariants:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Run the complete conformance suite
|
|
114
|
+
bun test test/conformance
|
|
115
|
+
|
|
116
|
+
# Run conformance for a specific kind
|
|
117
|
+
bun test test/conformance/acp.test.ts
|
|
118
|
+
bun test test/conformance/agy.test.ts
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Invariants Verified by the Kit:
|
|
122
|
+
1. **Enrollment & Coherence (`enrollment.test.ts`)**: Every definition in `ADAPTERS` and reviewer variant must be enrolled. A kind without kit coverage needs a reason in `UNSUPPORTED` (`test/conformance/enrollment.ts`), and an ACP kind that skips one of the shared `ACP_LEGS` needs a reason per leg in `UNSUPPORTED_LEGS`.
|
|
123
|
+
2. **Delivery Occupancy**: Sending messages updates driven peer state to `busy` synchronously, delivering digests reliably.
|
|
124
|
+
3. **Recovery Metadata Hygiene**: No prompt text or credentials leaked into `recoveryMetadata`.
|
|
125
|
+
4. **Watchdog Cancellation & Generation Fencing**: Expiry of the turn watchdog cancels prompts cleanly and generation fences prevent stale late answers from publishing.
|
|
126
|
+
5. **Teardown & Cleanup**: When an adapter is stopped (`peer.stop()`), all owned child processes in the process group stop with it (the peer reaches offline).
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 7. Review & Verification Checklist
|
|
131
|
+
|
|
132
|
+
When adding or updating an adapter, verify each item before opening a PR:
|
|
133
|
+
|
|
134
|
+
- [ ] **T0 Receipt Recorded**: Document native CLI version, observed hook payloads, and protocol outputs in `docs/verification/YYYY-MM-DD-<kind>-t0.md`.
|
|
135
|
+
- [ ] **Registered in ADAPTERS**: Added to `src/adapters/registry.ts` with explicit `displayName`, `family`, `transport`, and `trust`.
|
|
136
|
+
- [ ] **Config Path Protection**: Agent configuration directories added to `AGENT_CONFIG_SEGMENTS` in `src/hub/permission-mode.ts`.
|
|
137
|
+
- [ ] **PII Safety Verified**: Cloud models have `trust.onPrem: false`; verify PII markers refuse routing to this peer.
|
|
138
|
+
- [ ] **Process Group Teardown**: Daemon-spawned processes use process groups and exit cleanly on stop.
|
|
139
|
+
- [ ] **Conformance Kit Passes**: `bun test test/conformance` passes with zero regressions and no silent skips.
|
|
140
|
+
- [ ] **Documentation Freshness**: Run `node scripts/check-docs.mjs` and update verified stamps in `docs/verified.json`.
|
|
@@ -15,3 +15,8 @@ Background behind some of the rules, moved from the top-level README on 2026-10-
|
|
|
15
15
|
`docs/verification/2026-10-03-0.12.6.md`); minimal reproductions did not hang, so
|
|
16
16
|
the full trigger is not pinned down. On Bun 1.4.2 the next test still starts there
|
|
17
17
|
(#121, checked directly); the spin was not reproduced on demand on either version.
|
|
18
|
+
|
|
19
|
+
The #451 predicate and identity-lookup moves exposed unbound names in a VM fixture
|
|
20
|
+
that extracts daemon closures. Reconnecting imported helpers and deterministic
|
|
21
|
+
identity ports preserved the fixture's existing assertions; changing the asserted
|
|
22
|
+
behavior would have hidden the missing dependency.
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Native adapters and spawned agents
|
|
2
2
|
|
|
3
|
+
- Require the immutable captured project catalog at instance, roster, execution and permission resolver boundaries; choose a builtin catalog explicitly only for builtin-only callers; accept `adapters` only as wholly machine-local trusted data and require restart for changes. Refuse missing/mismatched pins, non-echoed load sessions, leader/attach argv and unknown owned-group evidence; require an explicit standalone `--no-leader` before argv termination for Grok (kind, executable or pinned native name); signal only the original verified group. Keep unmapped policy unmanaged, absent budgets exempt and read-only instances non-raisable through native acknowledgement plus host/MCP mutation gates. Claim native support only with its isolated T0 receipt.
|
|
4
|
+
- Consume quota/context through the resolved definition's declared readers; absence stays unknown. Use explicit `attach.externalHello` and `instances` policy for admission, never `identity.mcp`; keep runtime owner/root and reviewer isolation proof in the host. Never let configured `roles` include `conductor`. Keep native stderr, prompt-failure, permission-mode and transport error text out of configured-kind logs and turn-failure reasons. A configured CLI start (`src/cli/configured-acp-entry.ts`) accepts only `--mode headless`, `--as`, `--session-id` and one preview flag, and its preview never shows argv or session values.
|
|
5
|
+
- Validate coherence and conformance enrollment for reviewer variants too. Construct driven conformance peers through `start.create`; name dedicated coverage and concrete reasons for unsupported legs. Do not restore removed placeholder launch, lifecycle, init or permission metadata.
|
|
6
|
+
- For conceptual architecture, definition fields, generic ACP config and the new-adapter review checklist, see [Adding an AI Tool](../adding-an-ai-tool.md). For definition interfaces, see [registry-types.ts](../../src/adapters/registry-types.ts).
|
|
7
|
+
|
|
3
8
|
Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` and `activity` events), peer turn state, approvals, and the agent processes the hub spawns and stops. Read before editing `src/adapters/`, `src/pi/`, `src/hub/peers.ts`, `src/hub/permission-mode.ts`, `src/hub/tool-refusal.ts`, `src/hub/child-process.ts`, `src/hub/lifecycle.ts` (shutdown deadline), `src/hub/start-mode.ts`, `src/hub/terminal-open.ts`, `src/memory/capture.ts` (session end), `src/hub/native-hook-launch.ts`, `src/hub/peer-catalog.ts`, the launcher-managed MCP entries (`src/cli/agy-mcp.ts`, `src/cli/kimi-mcp.ts`, `src/cli/jcode-mcp.ts`) or the process-tree stop in `scripts/benchmarks/teardown.ts`, or before adding a native peer.
|
|
4
9
|
|
|
5
10
|
- When an adapter lands, add its project config directories to `AGENT_CONFIG_SEGMENTS` (`src/hub/permission-mode.ts`); scoped automatic writes must ask there, while explicit approval and `never-ask` keep their existing behavior.
|
|
@@ -16,13 +21,13 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
16
21
|
- 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.
|
|
17
22
|
- Report each Pi owner exit once, with code/signal (unknown for an unobserved TUI exit), turn/tool activity and only the last tool name. Auto-restart only a current, started, unexpectedly idle headless owner on its verified persisted session, once per 60 s; preserve shutdown/requested-stop/recovery/startup/active-tool fences and never fall back fresh.
|
|
18
23
|
- Pi's `always` grant (#209) is keyed by `executeTool`'s tool name, never parsed from the approval title, and lives only in that Pi start's closure. Never persist it, share it with the local worker, or let the dashboard give it.
|
|
19
|
-
- Auto-approval covers the hub's own tools by exact `mcp__agent-hub__<name>` title and only ever picks `allow_once`. Identity comes from the permission request title or, when an agent titles the request with the argument JSON (Qwen), from the announced `tool_call` title bound to the call id and resolved against the session's configured MCP servers, never from payload text, a prefix or an unrelated display title. Never widen it to a prefix or replace it with an agent-selected session mode. Set Kimi `yolo`/`auto` only for its verified native identity and an operator mode; config never-ask waits for a console y, and other ACP agents stay unmanaged on fresh/resumed starts without setting/resetting modes or failing over their reported mode; refuse runtime changes until their map exists; await the advertised mode change after session new/load and before a prompt, and fail startup on refusal. Use the shared `KIMI_MODE_IDS` mapping (`src/adapters/definitions/kimi-native.ts`, re-exported by `src/hub/permission-mode.ts`).
|
|
24
|
+
- Auto-approval covers the hub's own tools by exact `mcp__agent-hub__<name>` title and only ever picks `allow_once`. Identity comes from the permission request title or, when an agent titles the request with the argument JSON (Qwen), from the announced `tool_call` title bound to the call id and resolved against the session's configured MCP servers, never from payload text, a prefix or an unrelated display title. Never widen it to a prefix or replace it with an agent-selected session mode. Configured ACP kinds (`nativePolicy` or `requireOwnedGroup` set) never auto-approve from a request or tool-call title, even an exact hub tool name; under a locked read-only mode, reject any request whose ACP `kind` is not read, search or think, and never offer `allow_always`. Set Kimi `yolo`/`auto` only for its verified native identity and an operator mode; config never-ask waits for a console y, and other ACP agents stay unmanaged on fresh/resumed starts without setting/resetting modes or failing over their reported mode; refuse runtime changes until their map exists; await the advertised mode change after session new/load and before a prompt, and fail startup on refusal. Use the shared `KIMI_MODE_IDS` mapping (`src/adapters/definitions/kimi-native.ts`, re-exported by `src/hub/permission-mode.ts`).
|
|
20
25
|
- Permission modes (#242) are a person's and live in the daemon's memory only: `permission` and `permission_default` are console-role control messages, `ahub permission` is `console` in `classifyPeerCommand`, and no hub tool or conductor tool sets one. The dashboard and `ahub settings` set one only through `settingsSet` (`src/hub/daemon.ts`, #269), which calls `changePermission` inside `permissionChange` as the console does: never add another path that writes a runtime choice to `permissionModes`; a reloaded project default (#439) is applied only by `applyPendingPermissionDefault`, and only while that peer is absent or offline. Never write a runtime choice to a restart snapshot.
|
|
21
26
|
- A headless ACP Kimi or Codex whose mode lives in the native agent joins the bus only through `reconcileNativeStart` (`src/hub/daemon.ts`): after `start()` the daemon's current mode is applied, then `bus.add` (it can drain at once), then the state `start()` reached is replayed. Every mode change runs inside `permissionChange`, one per peer at a time.
|
|
22
27
|
- Codex takes a mode as an `approvalPolicy` override on `turn/start`, and the override persists on the thread: pass the hub's and the TUI's `turn/start` alike through `applyPermissionPolicy`, and return to `ask` only by sending the captured native policy once. With none captured, refuse while the TUI is attached; never just stop sending the override. Keep the captured native policy and the restoration debt per thread (`nativeApprovalPolicies`, `overriddenApprovals`) across detach and `thread/start` or `thread/resume`; clear a debt only on a successful response to its restoring `turn/start` while the mode is `ask` (`settlePermissionRestore`), so a rejected turn keeps it. While a thread owes a restoration with a known baseline, in any mode, never replace that baseline: a `thread/resume` response and the TUI's own `turn/start` can both echo the sticky override, and the restoring turn sends the kept baseline whatever the TUI supplied. That protection is one turn: after the restoration succeeded, a policy the TUI sends is taken as the person's choice, captured and passed through, because the proxy cannot tell a repeated echo from one. A detached proxy's `clearPermissionMode` is never refused; if the native policy is unknown, return a CLI-visible note that the thread keeps its last policy. Attached refusal directs the person to close the TUI, then run `ahub permission codex ask`.
|
|
23
28
|
- An unanswered `session/set_mode` leaves the native mode unknown, not unchanged: `AcpPeer` invalidates the turn, goes offline, stops its process group and refuses further changes until restarted. Never report the old or the requested mode after a timeout.
|
|
24
29
|
- A mode grant in `piPermit` or the local worker's `permit` (`src/hub/daemon.ts`) answers as the prompt path does: allowed only while that owner is still the attached one and the call was not aborted. `ask-when-needed` covers `PI_EDIT_TOOLS` on a `grantablePath` only (path rule: `local-worker.md`).
|
|
25
|
-
- Every agent the hub spawns (the Codex app-server, ACP agents such as Kimi, Pi) runs in its own process group and is stopped as one (`stopOwnedProcess(proc, { group: true })`, `trackGroup` after spawn): `codex` is a node launcher, and mid-turn its native app-server does not exit on SIGTERM within the grace period, so a SIGKILL to the launcher alone left the app-server running under init and holding the daemon alive (#113, #115). A new adapter that spawns an agent does the same.
|
|
30
|
+
- Every agent the hub spawns (the Codex app-server, ACP agents such as Kimi, Pi) runs in its own process group and is stopped as one (`stopOwnedProcess(proc, { group: true })`, `trackGroup` after spawn): `codex` is a node launcher, and mid-turn its native app-server does not exit on SIGTERM within the grace period, so a SIGKILL to the launcher alone left the app-server running under init and holding the daemon alive (#113, #115). A new builtin adapter that spawns an agent does the same. A configured ACP kind stops only through `AcpOwnedGroup` (`src/adapters/acp-owned-group.ts`): capture the direct child and its original group after spawn, verify before admission and again before stop, SIGSTOP, re-verify, then SIGKILL only that group; never discover or signal descendants outside it, and with unproven ownership send no signal, close stdin and keep the owner until it is confirmed gone.
|
|
26
31
|
- To stop a process tree, freeze it (SIGSTOP) before reading what it contains, then SIGKILL, and call it done only when the table shows none of it (with no table at all, the stop can only ask the group itself, which cannot see groups below it). A process a read after the freeze shows for the first time is frozen and read again before anything is killed. A snapshot taken while the tree runs misses what it starts next: three review rounds of #113 found such a gap (groups of its own, an unreadable first read, a child started during the grace period).
|
|
27
32
|
- Work that must survive `ahub kill` (claude-mem `session-end`) is awaited in `stop()`; fire-and-forget dies with the process.
|
|
28
33
|
- A peer must set `busy` synchronously inside `deliver()`, otherwise the bus drains the next envelope into a running turn (Kimi answers `turn.agent_busy`). Outside a turn the adapter accepts an occupancy diagnostic (a `context-used` `usage_update` naming its current session) and nothing else: no token total, no chunk, nothing unnamed or for an offline peer (#285).
|
|
@@ -49,7 +54,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
49
54
|
|
|
50
55
|
- Pi TUI hub prompts (#379; an idle snapshot ACK arriving before a personal start inserted a hub prompt into the person's turn): attribute a start, end or settle to a hub turn only by the command's `promptId`, which lasts until the logical `agent_settled`; treat any other start while a hub prompt is pending or running as personal activity that detaches Pi. Settle `failed_safe` only on a `prompt_not_started` refusal bound to the verified owner, session, command id and generation; everything else stays `needs_review`. Refuse roster return without `fencedPrompt`.
|
|
51
56
|
|
|
52
|
-
- Adapter registry (#451): kind-specific policy lives in that peer's `AdapterDefinition` under `src/adapters/definitions/` (runtime definitions `NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN` in `builtin-metadata.ts` plus `qwen.ts`; the daemon constructs every native driver through `start.create` and `DAEMON_PORTS`; `BUILTIN_DEFINITIONS_WITHOUT_QWEN` is the frozen A1 export, read by no runtime consumer). Outside `src/adapters/`, read a semantic field of `definitionOf(id)`: never a raw peer-id comparison, a resolved kind (`kindOf`, `definitionOf(...).kind`) or a constant alias compared with a native name, a vendor-keyed object, a peer-name array, or a closed-list import from `peer-catalog.ts`, `start-mode.ts` or `permission-mode.ts`; the guard resolves aliases and import/re-export bindings. `test/registry-guard.baseline.json` is `[]` and stays empty: a new site moves into a definition, never into the baseline. The only exceptions are the cross-release literals `KNOWN_PEERS` (5), `OPTIONAL_PEERS` (3) and `START_MODE_PEERS` (
|
|
57
|
+
- Adapter registry (#451): kind-specific policy lives in that peer's `AdapterDefinition` under `src/adapters/definitions/` (runtime definitions `NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN` in `builtin-metadata.ts` plus `qwen.ts`; the daemon constructs every native driver through `start.create` and `DAEMON_PORTS`; `BUILTIN_DEFINITIONS_WITHOUT_QWEN` is the frozen A1 export, read by no runtime consumer). Outside `src/adapters/`, read a semantic field of `definitionOf(id)`: never a raw peer-id comparison, a resolved kind (`kindOf`, `definitionOf(...).kind`) or a constant alias compared with a native name, a vendor-keyed object, a peer-name array, or a closed-list import from `peer-catalog.ts`, `start-mode.ts` or `permission-mode.ts`; the guard resolves aliases and import/re-export bindings. `test/registry-guard.baseline.json` is `[]` and stays empty: a new site moves into a definition, never into the baseline. The only exceptions are the cross-release literals `KNOWN_PEERS` (5), `OPTIONAL_PEERS` (3) and `START_MODE_PEERS` (4), pinned separately in `test/registry-guard.test.ts`. `LaunchContext.nativeCommand` carries only validated config argv (or its redacted preview), never model input; a definition's `permission.grants` text is what `permissionGrant` shows, ahead of the built-in wording. Keep native mode-id maps in `definitions/*-native.ts`, free of registry factories (`test/adapter-import-order.test.ts`).
|
|
53
58
|
- Registry load runs `assertDaemonEngine` and `assertNativeCapabilityScope` before publication. A definition that is not a fixed builtin (object identity in `fixedBuiltinDefinitions`, never a kind name) may not declare `start.acpCommand`, `attach.admission`, `budget.resetSoftTokensOnResume`, `session.crashIdentity`, a `pi-rpc` daemon port, `routing`, `reviewerVariant`, `reviewer` or `hooks.factsHook`. Only `definitionOf(id, instances)` picks a kind's `reviewerVariant`, for a declared extra with `start_mode: "headless"`, roles exactly `["reviewer"]`, and either a variant whose `reviewer.declaration` is `"automatic"` (Claude, agy) or `review_mode: "fresh"` (Codex, Kimi); without that opt-in a reviewer-only Codex or Kimi instance keeps its native adapter. Metadata-only reviewer variants never establish native peer support; `isSupportedPeer` shares the registry runtime/validated-instance predicate with the catalog and daemon. A new field that depends on fixed-builtin assumptions or host-global native state joins that list (`test/adapter-capability-scope.test.ts`). `trust.onPrem` admits PII work (rule: `tasks.md`).
|
|
54
59
|
- `validatePiSession` (`src/pi/session.ts`) throws `PiSessionUnresumable` only on positive evidence (ENOENT, a file outside the managed session directory, an unparsable or mismatched header) and `PiSessionBookmarkMismatch` only when a valid header names another session id; every other read failure is rethrown unchanged, because the daemon retires a Pi continuity bookmark only on those two classes (#463). `PiPeer` rethrows a mismatch with the neutral header-id message and never claims a bookmark exists (`test/pi-session-validation.test.ts`).
|
|
55
60
|
|
|
@@ -72,5 +77,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
72
77
|
|
|
73
78
|
- 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` outside the 1.4.x line (1.4.2 is T0-verified; other 1.4.x patches launch with a stderr warning, #589), and a hub without the `native_hook_launch` feature.
|
|
74
79
|
- 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.
|
|
75
|
-
- 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.
|
|
80
|
+
- 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).
|
|
76
81
|
- `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.
|
|
82
|
+
|
|
83
|
+
- Pi terminal ownership reads only the existing monitor's current pid/signature/session/file/token observation, expiring after 1 s; unknown verification withdraws proof. Keep fresh shared process-signature checks at tool/effect admission and teardown, and never revive terminal proof across detach, offline, stop or a changed owner/session (#576).
|
|
@@ -9,7 +9,7 @@ Scope: quota and native context readings, checkpoints, pause, handoff and resume
|
|
|
9
9
|
- The coordinator only lifts its own pauses: `manualPaused` in the daemon keeps a `ahub pause` in place, and `ahub resume` refuses while a budget record is open.
|
|
10
10
|
- The status line tee must never fail or slow the render: no throw, original command run with the same stdin, 5 s cap.
|
|
11
11
|
|
|
12
|
-
- Keep native context occupancy (`src/hub/context-window.ts`, readers in `src/adapters/definitions/native-context.ts`) separate from quota accounting; never use Codex's accumulated `total` as occupancy or estimate Pi counters.
|
|
12
|
+
- Keep native context occupancy (`src/hub/context-window.ts`, readers in `src/adapters/definitions/native-context.ts`) separate from quota accounting; never use Codex's accumulated `total` as occupancy or estimate Pi counters. Host code reads quota and context only through `readNativeQuota` (`budget.ts`) and `readNativeContext` on the resolved definition's declared `budget.reader` / `budget.context`; no declared reader is unknown, never an empty reading (rule: `adapters.md`).
|
|
13
13
|
- Context checkpoint completion requires its request id and the current attached peer/native session. Invalidate its transport generation synchronously at a new peer claim, before asynchronous recall or attachment. Never pause, reassign or replace a session on context pressure. Refuse private-turn, PII-pattern and all associated open PII-task summaries (owner or reviewer, including in_review, with the current routing policy; use `isClosedTaskState` to exclude approved and cancelled work) before local persistence or cloud memory; never broadcast checkpoint text.
|
|
14
14
|
- Invalid/stale context readings expose unknown and do not rearm a crossing. Latch a crossing only after its event is accepted; reconsider paused/recovery-held crossings after release with fresh current-session readings. Tee context fields contain finite numeric values or null only, bound to daemon instance, launcher and native session. Context and session go to the per-peer file `claudeStateFile(base, peer)` (the builtin keeps the unsuffixed name); `claude-usage.json` stays one shared file per account.
|
|
15
15
|
|
|
@@ -17,7 +17,7 @@ Scope: quota and native context readings, checkpoints, pause, handoff and resume
|
|
|
17
17
|
- Kimi's ACP `{used, size}` occupancy is a context reading (`acpContext`, source `acp_usage_update`) bound to the adapter's own session id: never a `tokens` event or a budget window, and an invalid shape reports unknown, not the previous value. A gate crossing sends a checkpoint request only to definitions with `budget.checkpoint` (Claude and Codex); an ACP peer gets the event and the notice (#285).
|
|
18
18
|
- A quota resume notice replaces the peer's unread resume notices: carry their moves into the new one (`refs.budgetResumeMoves`, or the whole text of a notice that has none), publish it, and only then withdraw the ones it replaces, so a failed publish keeps them (#306).
|
|
19
19
|
- Quota handoff context reads `QUOTA_HANDOFF_PLATFORMS` (`src/adapters/registry.ts`, each definition's `hooks.recallPlatform`: claude, codex, kimi, qwen, jcode). It is independent of `hooks.recallInjected`: a handoff platform never joins the injected-platform exclusion in recall (`test/adapter-handoff-platforms.test.ts`).
|
|
20
|
-
- `Budget.report` skips `user`, `hub` and a registered kind with no `budget` block or with `budget.quotaExempt` (only local); an external id keeps quota accounting. `budget.instrumented` (execution budget) never exempts quota.
|
|
20
|
+
- `Budget.report` resolves the reporter and its account through `hooks.definitionOf` (the daemon's startup-bound project catalog; builtin-only when absent) and skips `user`, `hub` and a registered kind, configured kinds included, with no `budget` block or with `budget.quotaExempt` (only local); an external id keeps quota accounting. `budget.instrumented` (execution budget) never exempts quota.
|
|
21
21
|
|
|
22
22
|
- Key quota readings, pause records, overrides, checkpoints and handoffs by the canonical account (`hooks.account`, idempotent); telemetry keeps the reporter id and native context occupancy stays per instance. `instanceBudgetHooks` fans pause, resume, checkpoint and handoff out to every member, and `canHandOff` excludes the paused account's siblings. Startup normalization merges old alias pause rows without dropping a row or marking an unfinished handoff done. A manual pause stays per member. Reviewer variants keep their base kind's `budget.account`.
|
|
23
23
|
- Release-cut quota admission (`releaseQuotaSnapshot` in `src/hub/daemon.ts`, #481 S11): an account has a quota source only when one of its members' definitions has `budget.reader`, or `budget.resetSoftTokensOnResume` with `kimi_tokens_5h > 0`. Without a source a registered account is reported `no quota source` and never defers; with one, an unknown, stale or below-gate reading defers. Manual and budget-record pauses defer for every attached peer.
|
package/docs/agent-notes/bus.md
CHANGED
|
@@ -35,3 +35,5 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
|
|
|
35
35
|
|
|
36
36
|
- `relevant` may return a string: the envelope is dropped with that string as its journal discard reason. Read relevance only through `irrelevance()`.
|
|
37
37
|
- A review-only instance (a declared extra whose roles are exactly `reviewer`) receives only person messages, hub `review` items and hub `task` items addressed to it. Keep `reviewOnlyItem` in both `silence` (at send) and `relevant` (at delivery) (#223).
|
|
38
|
+
|
|
39
|
+
- Concurrent-edit fan-out is linear in affected tasks: retain each notice's own-task stale/owner fence, but never persist the same change-set notice once for every task pair. Preserve per-task-pair research evidence separately (#576).
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Daemon, control protocol and recovery
|
|
2
2
|
|
|
3
|
+
- Keep external-channel behavior in `src/adapters/channel-peer.ts`; the daemon owns socket authentication, admission and generation binding. Use adapter-owned native type predicates in core. Preserve the Codex `unidentified-cancellation` status notice while its attached session is latched; clear it on verified reattachment or TUI detachment.
|
|
4
|
+
|
|
3
5
|
- For explicitly user-authorized live verification, use the per-invocation CLI delegation in `docs/security.md` §Agent CLI identity and conductor authority; keep caller markers and record native evidence separately from fixtures.
|
|
4
6
|
- Keep `export const LIVE_TEST_SUPPORTED = true;` in `src/cli/identity.ts`: delegated recovery staging reads this declaration before any source effect.
|
|
5
7
|
- A delegated non-recovery command runs only after `recordDelegatedCliAudit` committed `delegated` against a hub whose `features` list `cli_audit_delegated`, that instance's console `cli_audit_ack` confirmed retention, and `ControlClient.pinInstance` pinned the invocation to it; a refused feature, failed ack or changed consumer executes nothing (#410). Upgrade, restart and recovery record delegation only in the operation receipt (`delegatedAudits` through `createOperation` or `recordRecoveryDelegation`). The daemon counts an audit retained only after `event()` returned true.
|
|
@@ -11,15 +13,16 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
11
13
|
- The plugin bundle is installed apart from the daemon. Breaking a control WS message contract bumps `PROTOCOL` in `control-client.ts`; a new request answered as unknown or an optional field preserves it, as does additive optional public task metadata in `ui_snapshot` that older clients ignore (such as `deps` in #246).
|
|
12
14
|
- `peer_stop` is console-only and stops only a hub-owned headless ACP, Pi or local runtime through its adapter. Refuse native-terminal peers and recovery-held mutations; serialize with starts and permission changes, and refuse a recovery `prepare` while a stop runs. An admitted stop holds delivery through `BusOptions.deliveryHeld` before any teardown await (never a console pause that resume can undo, or a global recovery hold that blocks unrelated peers) and withdraws the peer's pending approvals before the adapter stops. A requested stop holds an in-flight delivery as `needs_review` with an ids-only stop reason, suppresses restart/escalation, and never resolves that delivery automatically.
|
|
13
15
|
- Settings (#269): the dashboard's `setting`, `setting_undo`, `setting_preview` and `setting_reload` actions and the console-role `settings_get|set|undo|preview|reload` requests go through `settingsSet`, `settingsUndo`, `settingsPreview` (`settingsUndoPreview` for `undo: true`) and `reloadConfig` (`src/hub/daemon.ts`) and the registry in `src/hub/settings.ts`. A new editable setting is a registry entry, never a new write path; a key that chooses what the hub runs or where secrets or task text go joins `NEVER_EDITABLE` instead. Writes go only to `.agenthub/config.local.json` and `.agenthub/routing.local.toml` through `store()`: the loader's own validation first, temp file and rename, never a file git tracks. Authority comes from the dashboard session (`DashboardSession`, `src/hub/ui.ts`) or the console role, never from the request body; `ui_action` from the manager is an ordinary session. Never return a setting's value in an event or notice except through `recordSetting`.
|
|
14
|
-
- Live reload (#439): `reloadConfig` is the only publication path. Settings writes and undo, `ahub settings reload`, the dashboard reload and the debounced watcher of the four `CONFIG_SOURCE_FILES` (`src/hub/config-reload.ts`) all reach it; `prepareConfiguration` validates the complete candidate before anything is published, and recovery or shutdown holds it. A write or undo passes its own key as `scope` and publishes only that path, and `acknowledgeConfiguration` records only the accepted paths, so other edits stay pending. Every changed key, inside the registry or not, is decided by `authorizeConfigLeaf` (`src/hub/config-apply.ts`): a refused key keeps its accepted value and is reported by name while the others apply, `roster.*` applies only through its own scoped roster write, and a change of the settled planner or conductor holders (`roleHolders` over `resolveSettledRoles`) is guarded even when a role list shrinks. Outside a named write, only the console's `settings_reload` (authority `terminal`) adopts guarded file edits, by sending back the `confirmation` token bound to the candidate `fingerprint` and `configRevision`; changed bytes or accepted state refuse it, and a watcher, dashboard or settings session never confirms. An undo that names `key` and `undoDigest` writes only while `pendingUndoIdentity` still matches both. A `restart` key (`configApply`) keeps its running value and is listed in `pendingRestart`; reload notices and events carry key names and reasons, never values. The console Settings panel arms a write only from a preview reply that matches its open edit's token, key and value
|
|
16
|
+
- Live reload (#439): `reloadConfig` is the only publication path. Settings writes and undo, `ahub settings reload`, the dashboard reload and the debounced watcher of the four `CONFIG_SOURCE_FILES` (`src/hub/config-reload.ts`) all reach it; `prepareConfiguration` validates the complete candidate before anything is published, and recovery or shutdown holds it. A write or undo passes its own key as `scope` and publishes only that path, and `acknowledgeConfiguration` records only the accepted paths, so other edits stay pending. Every changed key, inside the registry or not, is decided by `authorizeConfigLeaf` (`src/hub/config-apply.ts`): a refused key keeps its accepted value and is reported by name while the others apply, `roster.*` applies only through its own scoped roster write, and a change of the settled planner or conductor holders (`roleHolders` over `resolveSettledRoles`) is guarded even when a role list shrinks. Outside a named write, only the console's `settings_reload` (authority `terminal`) adopts guarded file edits, by sending back the `confirmation` token bound to the candidate `fingerprint` and `configRevision`; changed bytes or accepted state refuse it, and a watcher, dashboard or settings session never confirms. An undo that names `key` and `undoDigest` writes only while `pendingUndoIdentity` still matches both. Keep config-source snapshots plain and cloneable; classify dynamic ids with validated running/source identity scopes, never suffix discovery or a builtin fallback. Compare restart-bound declarations with the running config on every reload, never a pending desired value accepted by an earlier reload; derived defaults stay bound to that running declaration. A `restart` key (`configApply`) keeps its running value and is listed in `pendingRestart`; reload notices and events carry key names and reasons, never values. The console settings editor (the Settings panel, or the Peers panel's `w` WIP shortcut, which opens only an existing `tasks.wip_limits.<peer>` row for a rostered peer listed in status `taskWip`) stays bound to the panel that opened it (`SettingsEdit.panel`) and arms a write only from a preview reply that matches that panel and its open edit's token, key and value; pasted text never confirms. Console Tasks backlog actions (`p`, `a`, `e`, `R`, `c`) only prefill person CLI commands (`backlog pull-ready|plan|return`, `task cancel`) with the row's immutable id and its planning or flow revision; `validateLockedRow` rechecks the row's state and revision at Enter and again at `y`, and no console path writes the board directly (#532).
|
|
15
17
|
- Retained configuration (#544): a key the reload refused, or a file edit outside a scoped write that still differs from the accepted value, is listed in `retainedConfig` (key and sanitized reason only, never a value) in status, `status.json`, the settings view and the console snapshot until a reload accepts or reverts it (`retainedConfigAfterReload`). A config leaf that selects a peer's native launch belongs in that definition's `cli.launchConfigKeys`: the `start` and `peer_start` replies then carry `retainedLaunchConfig` (the running values), the launcher starts with them and prints `retainedLaunchWarning` before its start request.
|
|
16
18
|
- `pet` is console-local presentation (#483): validate it only through `validatePetConfig`, which both `loadConfig` and the reload schema call, and keep it applied live with no authority (`authorizeConfigLeaf`).
|
|
17
19
|
- Start modes (#269): every start the hub makes itself goes through `hubStart` (`src/hub/daemon.ts`): the dashboard's `start_peer`, the console-role `peer_start`, the conductor's `start` hook and `pi.auto_start`. It takes the mode from `startMode(peer)` and nothing else; never add a caller-chosen mode, flag or command to it, and never fall back to headless when a terminal cannot be opened (refuse with the manual command). The words a terminal runs are assembled there from `process.execPath`, the CLI entry, the project root and `startWords(peer)`. The only continuity exception is a validated Pi bookmark on `pi.auto_start`: append its recorded session file (or its session id when no file was recorded), backend and model; never pass both session selectors or accept caller-written replacements. Recovery and `startPeer` callers that pass a recorded mode keep it. A launcher with no `--mode` takes the daemon's accepted start mode from `status.json` `startModes` (`readControl`) before the file's value. A TUI start of an `owned-acp` peer (Jcode) from the console's `peer_start` opens its terminal even while the ACP owner is online: that terminal is a frontend on the owner. The conductor and the dashboard answer `already` for an attached one (#559). The real terminal providers are wired only in `src/hub/lifecycle.ts` (`DaemonOptions.terminal`): a daemon built without them, as in every test, opens nothing; a test injects a recording provider and never the real one.
|
|
20
|
+
- A headless start that `hubStart` made for a peer with `capabilities.watch` (Kimi, Qwen) also opens `ahub watch <peer>` through `openWatchPane` (#286 AC10): the same provider, fixed words from `process.execPath`, `CLI_ENTRY`, the project root and the peer id, one per peer per `PEER_START_RETRY_MS`. It runs only after the start succeeded and never fails it: no provider or a failed open reports `watch.command` in the result and a notice, with the provider's own text in the log only. Never open a pane for an `already` result or a declared instance.
|
|
18
21
|
- `ahub setup` reads Claude Code's state from `--json` listings and takes one step at a time, re-reading after each; never match paths or names by substring.
|
|
19
22
|
- A copy of the git index keeps the original's mtime (`snapshot()`): git's racy-entry check compares entries with the index file's time in whole seconds, and a fresh copy makes a same-size edit look clean.
|
|
20
23
|
- State files that clients read (`status.json`, `control-token`) are written after the port is bound, and `status.json` via temp file + rename.
|
|
21
24
|
- Open native memory through the shared profile-aware helper with an explicit current selector binding; never fall back to shared v1 after invalid/missing generation metadata. Check that binding at hub call-site entry and before candidate/counter writes, including callbacks retained across awaits; do not intercept store methods with proxies/private hooks or claim atomic in-flight cutover. Quiesce and close consumers before migration/selection changes, then reopen through the supported lifecycle. Keep the sidecar at the independent memory root, not inside the selected generation. Report memory availability separately from bus health; never hot-rebind or migrate as a startup repair (`docs/memory.md` §Selected memory profile).
|
|
22
|
-
- Take the memory project only from `memoryProjectFor` (the alias-resolved `memoryProjectKey`) in the daemon, `ahub memory` and `ahub goal`; never use a raw `memoryProjectKey` as the project of a store, screen or raw command. The daemon resolves it once at start (falling back to the raw key, so the memory home never blocks startup) and passes both `project` and the raw `projectKey`, so the binding latches unavailable once the alias log maps the key elsewhere; a restart binds the new key.
|
|
25
|
+
- Take the memory project only from `memoryProjectFor` (the alias-resolved `memoryProjectKey` in the ambient memory home) in the daemon, `ahub memory` and `ahub goal`, or from `memoryProjectForStore` for a caller-injected store, which resolves the alias in that store's own `profile.home`; bind an injected sharded store with `bindProfileMemory(store, _, memoryProjectKey(cwd))` so a remap latches it unavailable; never use a raw `memoryProjectKey` as the project of a store, screen or raw command. The daemon resolves it once at start (falling back to the raw key, so the memory home never blocks startup) and passes both `project` and the raw `projectKey`, so the binding latches unavailable once the alias log maps the key elsewhere; a restart binds the new key.
|
|
23
26
|
- Detect a curated split by `lstat` of `shards.json` (`splitMarked`), never `existsSync`. An inactive, malformed, dangling or unreadable marker throws `MEMORY_SPLIT_INCOMPLETE` before any shard read, and callers show that message instead of the generic profile refusal. On a split home open only through `openShardStore` with the canonical memory dir and a project key; never a plain `MemoryStore`, and refuse without a key. Profile inspection and `profile-check` on a split home read the shard seals (`shardProfileForProject` for each hub's own project), never the legacy selector (#536).
|
|
24
27
|
- The channel's reconnect loop stops on closes a retry cannot fix (`TERMINAL_CLOSES` in `claude-channel.ts`); a new daemon close code that means "do not come back" belongs there, or two clients fight over it forever.
|
|
25
28
|
- The channel advertises `tools.listChanged`; build its tool list only through `toolListFor` and record `listedTools` from each `ListTools` reply, so a reconnect sends `sendToolListChanged` when the whole definitions differ or the lifecycle stays unreadable (#490).
|
|
@@ -27,7 +30,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
27
30
|
- Adding a native peer requires testing the command emitted by the actual recovery driver, not only its inspection or launch helpers. Never let a generic non-Codex branch treat a new peer as Claude.
|
|
28
31
|
- Cross-version recovery must use the authenticated source protocol for prepare/commit/abort and the target protocol after startup. Prove the transition against a real prior release before claiming compatibility.
|
|
29
32
|
- Preserve `task_cancel` target-feature preflight before source holds, capture or commit. Reuse stored target features only after verifying the same retained root and digest; legacy receipts still capable of commit must obtain feature proof. Do not apply pre-commit compatibility checks to sources already past commit. Every prepare, including the lease renewal before a terminal close and after a resume, sends the operation's stored `targetFeatures` (#478).
|
|
30
|
-
- A recovery target-feature floor (`task_cancel`, `peer_instances`, `release_trains` while a configured train is active or frozen, `reviewer_pool` while a fresh reviewer or `review.pool` is configured) lives in `incompatibleRecoveryTarget`, which every existing check site calls, and its name in `DAEMON_FEATURES`; never check a floor at one site only.
|
|
33
|
+
- A recovery target-feature floor (`task_cancel`, `peer_instances`, `release_trains` while a configured train is active or frozen, `reviewer_pool` while a fresh reviewer or `review.pool` is configured, `configured_acp` while the project declares `adapters`) lives in `incompatibleRecoveryTarget`, which every existing check site calls, and its name in `DAEMON_FEATURES`; never check a floor at one site only.
|
|
31
34
|
- Defer hub notices only for an authenticated recovery `prepare` or `commit` whose target features include `recovery_deferred_hub`, or for a restored daemon. Otherwise `setRecoveryHold(true)` moves the deferred notices into queues before commit records expected integrity, and a bare `fenceRecovery` defers nothing. Abort and lease expiry release the bus hold before clearing the operation; if that release fails, keep the operation and let expiry retry on its 1 s timer (#478).
|
|
32
35
|
- Resume a prepared `commitSent` source that is unavailable or database-busy inside the existing bounded shutdown poll; never re-send commit, re-prepare or close terminals again on that path. Require confirmed `stopped` before target startup, refuse a replacement instance, and retain the prepared receipt on timeout.
|
|
33
36
|
- Keep restoration receipts explicitly tagged as pending, waiting with origin/binding, done with binding, or failed. Project waiting origins as `waiting:pending` and `waiting:failed`; preserve the chosen binding and origin across interruption instead of inferring completion from an object.
|
|
@@ -36,7 +39,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
36
39
|
- Judge every recorded owner pid (hub manifest, registry claim, recovery runner, manager owner, launcher record) with `processLiveness` in `src/pi/process-signature.ts`, never a bare `process.kill(pid, 0)`: after a reboot a recorded pid can belong to another process, even one we may not signal, and the bare probe wedged recovery and `ahub up` (#226). A writer records `processSignature(process.pid)` beside its pid; a registry claim stores it as `<instance id> <signature>` because an older daemon's claim rewrites the pid but not that column. Records without a signature keep the bare-pid answer; unknown is never gone.
|
|
37
40
|
- A restored daemon accepts a changed native session id (Kimi and local aside: they restart as fresh workers) in two ways only: the #64 rule for a Claude session that never persisted a transcript, and the operation-fenced `recovery-waivers.json`, which the coordinator writes wherever it accepts a session other than the planned one: a reconnect-only Claude (#206), an operator-chosen fresh session, the plan's `freshStart` of a Codex thread the hub saw start with no turn, or a zero-turn Claude it accepted (#215). Never waive on any other evidence. Re-preparation after an expired lease keeps every effect receipt, and `restored:<peer>` = `failed` never counts as restored. Judge a restoration launcher by its `terminal-recovery.json` record and process signature (and a `terminal_handle_stale` answer), never by the terminal's exit; an unreadable record is unknown, never "none".
|
|
38
41
|
- A target that stopped after it started is restarted only through `recordRestart` (`src/cli/upgrade.ts`), recorded before the new one starts and at most once per project per resume: each done or waiting `restored:<peer>` binding becomes `retired:<peer>`, whose terminal is closed (`closedRetired:<peer>`) or found absent before that peer is relaunched, and is never revalidated against the new instance. Keep repaired-session provenance across every target restart, disabling automatic fresh/zero-turn replacement. A person may explicitly accept loss of a repaired Codex/Claude conversation only on typed positive missing transcript/rollout evidence and a disposal-time recheck; refuse live-test repaired overrides after read-only receipt classification and before runner/lock or coordinator mutation, fail closed on unclassifiable scope, and repeat in core before lock and after under-claim read, preserving ordinary #408 delegation; record the one-use grant and freshOverride evidence, and consume the grant on exact attachment. Pi stays excluded: positively missing accepted Pi files get stop-and-archive plus the normal project-scoped ahub pi command; bookmark retirement never falls back fresh in the same start. Clear the repair fence only for a done or waiting binding retired from that target; an uncertain creation without a durable binding keeps its fence. Launcher evidence is read for every instance in `restarts`, and a session attached without an id is unknown, never gone (#225).
|
|
39
|
-
- After a managed terminal is created, persist its binding before waiting up to `RECONNECT_WAIT_MS` for authenticated attachment to the expected target instance, operation and conversation (#414). Resume waits on the saved binding without creating another terminal. Count only attached, non-offline session ids; when the chosen terminal has no current launch record, a confirmed-ended row for another handle or an id-less attachment retains the bounded wait after older-launcher checks. A late current record must match the saved terminal; unknown or live replacement evidence still blocks. Timeout keeps the receipt blocked and records only terminal/hub metadata; a last native connect attempt that was never recorded is unknown, never inferred from terminal readiness or offline state. Final identity and preservation verification stays strict.
|
|
42
|
+
- After a managed terminal is created, persist its binding before waiting up to `RECONNECT_WAIT_MS` for authenticated attachment to the expected target instance, operation and conversation (#414). Resume waits on the saved binding without creating another terminal. Count only attached, non-offline session ids; when the chosen terminal has no current launch record, a confirmed-ended row for another handle or an id-less attachment retains the bounded wait after older-launcher checks. A late current record must match the saved terminal; unknown or live replacement evidence still blocks. Timeout keeps the receipt blocked and records only terminal/hub metadata; a last native connect attempt that was never recorded is unknown, never inferred from terminal readiness or offline state. Final identity and preservation verification stays strict. `createTerminal` re-reads the launch record and Orca binding of an idle replacement every `MAP_POLL_MS` until it maps to the original peer and session, a known identity or session contradicts it, or the deadline passes; only then does it report `ambiguous-create`. Missing identity alone is never a mapping (#599).
|
|
40
43
|
- An operation receipt stays `schema: 1`. Only `abandoned()` writes 2, on an operation with a recorded `disposition` whose coordinator predates #215: in the same write as the disposition, or, for a disposition 0.12.20 recorded at 1, by the next command that holds the runner claim. `validateReceipt` accepts 2 only with a disposition (#227). Never write 2 on an operation that can still be resumed.
|
|
41
44
|
- Four checks read another release's source text for an exact line: `coordinatorCurrent` (`export async function disposeRecovery` in `src/cli/upgrade.ts`), `targetReadsWaivers` (`export function readRecoveryWaivers` in `src/hub/restart.ts`), `coordinatorRestarts` (`export function recordRestart` in `src/cli/upgrade.ts`) and `supportsAction` (`export async function reattachRecovery` in `src/cli/upgrade.ts`). Renaming, moving or un-exporting one makes this release read as an older one: keep those lines until a capability list replaces the checks. The reattach CLI dispatch must use the same `supportsAction` check as its printed command; never redirect a fallback command back to a coordinator without that action. Recovery also reads a target's literal `OPTIONAL_PEERS` (`src/hub/peer-catalog.ts`, through `legacyTargetSupport`) and `START_MODE_PEERS` (`src/hub/start-mode.ts`, through `legacyHeadlessArgv`), declared in `src/adapters/definitions/native-recovery.ts`; keep both literal declarations until a published capability replaces them.
|
|
42
45
|
- Publish terminal ownership only after the current native owner is proven through `terminal_owner` (#482); capture and start acknowledgement are pending evidence only. Preserve unrelated and invalid rows, scope current evidence to project/instance/peer, and recheck the same native generation after asynchronous Orca reads. Keep Claude's hook record pre-spawn. Compatibility and retention ceilings are described in Operations, controlled recovery.
|
|
@@ -53,6 +56,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
53
56
|
- Recovery inspection uses its existing 30 s control cap for the first status read and consumes a structured busy reply without a second status probe; ordinary lifecycle probes keep their default. Treat `busy_timeout` as a ceiling for eligible waits, not a guarantee that every write waits: a WAL snapshot conflict may fail immediately. Account for synchronous 3 s hub-database waits and serial timer callbacks when choosing a timeout; several waits may consume the 30 s control margin. Never multiply the busy wait through retries or infer stopped from a shorter probe's timeout.
|
|
54
57
|
- Every `new Database(` under `src/` is followed at once by its own `PRAGMA busy_timeout` from a named `*_BUSY_TIMEOUT_MS` constant (`HUB_DB_BUSY_TIMEOUT_MS` in `src/hub/db-timeout.ts` for `hub.db`), and nothing sets `busy_timeout` again later: `test/db-busy-timeout.test.ts` reads the source and fails on any other opening idiom, so a new one extends that guard first (#303).
|
|
55
58
|
- The manager forwards a dashboard `start_peer` with the terminal provider cap (`MAX_COMMAND_MS`) plus 5 s and reports a missed deadline as `unconfirmed`, never as a failure; it recognises the deadline by the exact text `ControlClient.request` resolves with, so change the two together. The hub's terminal reopen guard and the dashboard's retry hold share `PEER_START_RETRY_MS` (`src/hub/start-mode.ts`): never give either its own number (#298).
|
|
59
|
+
- `openManager` keeps only the manager that answered `/open`: when the answering pid is not the process this open spawned, it stops its own spawn with SIGTERM, so a losing concurrent open never leaves an unowned manager (#584).
|
|
56
60
|
|
|
57
61
|
- Use the registry's `KNOWN_PEERS` (`src/adapters/registry.ts`) for the native catalog (the `src/hub/peer-catalog.ts` literal is a cross-release probe only) and `rosterAllows` for participation. Change live membership only at the authorized roster settings write or undo, applying exactly that key. Keep external ids and projects without roster settings compatible with the existing admission and lazy status rules. Fence starts and attachment before detaching or stopping a removed runtime; retain its queue, uncertain deliveries and independent pauses. Refuse removal of the conductor holder and require explicit confirmation and `--now` for an active turn.
|
|
58
62
|
|
|
@@ -68,11 +72,16 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
68
72
|
|
|
69
73
|
- Keep full memory home, generation ID, selector revision and malformed-row totals in authenticated person status/CLI receipts, never disk status.json or agent CLI output; low-privilege receipts use a generation prefix and omit home, selector revision and malformed-row totals. Emit typed unbound disabled/unavailable receipts with no profile when no memory handle opened; profile-check may treat an authenticated same-home generation-capable unbound daemon as memory-quiesced, never a stale bound handle. Scope profile-check readiness to its named registry inventory; old, unknown or unreadable receipts refuse readiness, and other custom registries need separate checks.
|
|
70
74
|
|
|
71
|
-
- Bind instance declarations once at startup through the daemon-local `definitionOf` closure and the `extraIds` catalogs; never publish them to the global registry or take them from a request. `instances`
|
|
75
|
+
- Bind adapter and instance declarations once at startup through the daemon-local `adapterCatalog` (`createProjectAdapterCatalog(loadedConfig.adapters)`), its `definitionOf` closure and the `configuredIds`/`extraIds` catalogs; never publish them to the global registry or take them from a request. `adapters` and `instances` are restart-class, and `adapters` is `NEVER_EDITABLE` and in `ALL_MACHINE_LOCAL`. Settings rows come only from `projectSettings(declarations, adapterCatalog)`; a `locked` row (a read-only instance's permission) is refused by `checkSettingValue` and `settingRefusal` for every authority, and the console and dashboard never arm an edit or undo for it.
|
|
76
|
+
- Configured ACP starts (`startPeerBody`): before effects refuse native launch overrides (model, route, backend, unattended, provider profile, session file, fresh) and an invalid session selector; answer `already` only for the same requested native session and never pass `qwen_cmd`; after a failed start keep the owner with `pendingOwnedProcess` on the bus, let `peer_stop` admit that offline owner and refuse starts until it is confirmed gone, reporting only a withheld-detail reason. In `taskOp`, refuse a read-only instance's op outside `READ_ONLY_HUB_TOOLS` before effects. Publish `configuredAdapters`, `startablePeers` and `readOnlyPeers` in status as bounded ids only. Keep controlled recovery refused (`session.recoveryRefusal`); crash resume passes only a bounded, control-free recorded `sessionId`, never a recorded model.
|
|
72
77
|
- `validateInstances` admits `review_mode: "fresh"` only on a headless instance whose roles are exactly `["reviewer"]` and whose kind declares a fresh reviewer; a fresh reviewer refuses `worktree` and `permission_mode` (variant selection: `adapters.md`). `review.pool` and `review.rerotate` are validated by `reviewerPoolConfig` at startup and in the config schema: 1 to 16 distinct kinds from `REVIEW_POOL_KINDS`.
|
|
73
78
|
- Every start resolves `resolveInstanceExecutionCwd` and passes that `executionCwd` as adapter cwd, sandbox, grant and Pi session root; set `executionOwners` only for the adapter actually started. Extra-instance native state is per instance, while its MCP binding keeps the canonical hub state dir.
|
|
74
79
|
- Recovery: record an extra Codex launch row only after the start reply supplies `executionCwd`; keep `projectRoot`, `executionCwd` and `terminalRoot` separate and never bind an extra by kind metadata. Abort and archive validate receipts without current declarations; restore, fresh and reattach require them; refuse before effects when `peer_instances` is missing. Extra-instance turns write `turn_workspaces` in the `turns` transaction and leave the legacy snapshot fields unknown; undo requires the same trusted declaration and validated root.
|
|
75
80
|
|
|
76
|
-
- Name every Claude per-peer state file through `claudeStateFile` (`src/hub/claude-state.ts`) with that peer's id, never a hard-coded builtin name; pick a peer's `terminal-recovery.json` row only through `ownTerminalRow`. `sessionPointers` in `src/cli/reset.ts` must match any new per-peer pointer (#450 I3).
|
|
81
|
+
- Name every Claude per-peer state file through `claudeStateFile` (`src/hub/claude-state.ts`) with that peer's id, never a hard-coded builtin name; pick a peer's `terminal-recovery.json` row only through `ownTerminalRow`. `sessionPointers` in `src/cli/reset.ts` must match any new per-peer pointer (#450 I3). Claude state files belong to Claude peers only: select those peers by `transport === "claude-channel"`, never by `identity.mcp === "channel-plugin"` or `turnEnd === "native-hook"` (agy has both), and `claudeStateFile` throws for any other id (#624).
|
|
77
82
|
- A declared Claude channel extra is never hub-started: keep it out of `STARTABLE_PEERS`, instance start modes and `RESERVED_IDS`. A launcher admits `--as <id>` only when the running daemon's status `activeInstances` lists it (`requireActiveInstance`), never from the config on disk.
|
|
78
83
|
- Verify a native Claude Stop and its start baseline only through `readClaudeTranscriptCompletion` (4 MiB backwards chunks, at most 64 MiB plus one boundary byte; incomplete or out-of-bound rows are unavailable), never the size-capped `readClaudeTranscriptUsage`; an `unavailable` refusal leaves the peer busy and notifies once per turn start (#517).
|
|
84
|
+
|
|
85
|
+
- Classify HEAD moves with bounded, append-fenced per-worktree reflog evidence. Exclude only incoming checkout/merge/pull/proven-fast-forward reset paths; preserve commit/amend/cherry-pick and sequencer replay changes; classify rebase/pull --rebase start checkout as incoming, with overlapping replay paths unknown. Never exclude paths still dirty against end HEAD (autostash/checkout -m included). Unreadable or ambiguous movement/capture evidence is unknown, never an exclusion. Compute a concurrent change set once and publish no more than one conditional notice per affected task. Keep per-task-pair research events. Cache only raw checkout-branch probes within one synchronous burst, and reapply current privacy screening on every status rendering (#576).
|
|
86
|
+
|
|
87
|
+
- Share one monotonic 250 ms snapshot Git budget until the next event-loop I/O turn. Repository/HEAD probes, capture and end diffs consume the same remaining deadline; exhaustion or unreadable diff evidence records unknown trees and no attribution/undo, with one diagnostic per burst. Never substitute a stale baseline or apply a late result (#576).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Task board and hub tools
|
|
2
2
|
|
|
3
|
-
Scope: the task flow, assignment, the state machine and the hub's MCP tools. Read before editing `src/hub/tasks.ts`, `src/hub/board.ts`, `src/hub/routing.ts`, `src/hub/task-sweep.ts`, `src/hub/settled-roles.ts`, `src/hub/conductor.ts`, `src/hub/supervision.ts`, `src/hub/tool-refusal.ts`, `src/hub/hub-tools.ts`, `src/hub/review-brief.ts`, `src/hub/reviewer-pool.ts`, `src/hub/reviewer-pool-config.ts` or `src/hub/
|
|
3
|
+
Scope: the task flow, assignment, the state machine and the hub's MCP tools. Read before editing `src/hub/tasks.ts`, `src/hub/board.ts`, `src/hub/routing.ts`, `src/hub/task-sweep.ts`, `src/hub/settled-roles.ts`, `src/hub/conductor.ts`, `src/hub/supervision.ts`, `src/hub/tool-refusal.ts`, `src/hub/hub-tools.ts`, `src/hub/review-brief.ts`, `src/hub/reviewer-pool.ts`, `src/hub/reviewer-pool-config.ts`, `src/hub/release-admission.ts` or `src/hub/no-writes.ts`.
|
|
4
4
|
|
|
5
5
|
- Scope every task-place comparison through `TaskRepositories` (`src/hub/task-repository.ts`, #476): explicit `refs.repo`, else a common outside-absolute-ref repository or parent, else this project. Preserve relative display spelling within that key and gate overlap, contradiction, replay and held evidence by equal keys. Warm/cache repository identity at task mutation and startup; never repeat Git reads for unchanged overlap inputs. Set `refs.repo` for work in another repository; repo-only accept metadata never grants execution or file access.
|
|
6
6
|
- Tool callers are models: MCP `inputSchema` is not enforced on the way in. Normalize at the boundary (`cleanRefs`) before anything reaches the board, and never throw after a board write: a publish or notify that follows a `review()` or `done()` board write goes through the one `releaseNotice` guard, which turns a failed publish into a console notice, drops a failed notify and never retries (#231, #243). An assignment's delivery once its board write is saved (#276, #297): the move is fact, the failed publish is not retried in place (the journal's own recovery covers a storage error, and a retry may duplicate a publish that actually left), and a person looks for a queued or needs_review delivery with `ahub queue list --peer <owner>` (then `ahub queue show <id>`, or the accepted entry in `ahub task show <id>` if the journal is down) before re-sending with `ahub task assign <id> <owner>`. The distinction is made once, in `assignOwner`: a failure after the write prints the one console notice from `undeliveredNotice` right there, once, and throws `AssignmentUndeliveredError` carrying the saved task; callers (`review()`'s repeated rejection, `escalate` and its callers, the ready offer, both relays, the idle sweep, rescreen) decide control flow by the type and print nothing more, and `propose`, `assignTo` and `decline` additionally let the error reach the caller in its turn, which is a person or peer that can act. Two exceptions get no notice, by construction: a row superseded meanwhile, meaning a later history entry that moves or settles the task (another assignment, an unassign, the owner's accept, done, approval, close), while entries that do neither (the hub's quiet bookkeeping: answers, reviewer changes, idle sweeps, check results) leave the assignment undelivered and the notice stands, so the original error goes to the caller as before only on supersession; and a claim's own accepted write and "claimed by" line stay outside the delivery rule (a claim is its own hand-over; its overlap and routing announcements to earlier owners are deliveries and share the wrapper, so a failure there does read as one). A failure before the write stays a plain `Error` (#254), a failed write leaves the ready offer unconsumed (#231), and the notices before the write (waits, no-owner, the reserved-owner line) go through `releaseNotice` too, so those pre-write notices cannot fail their caller.
|
|
@@ -9,6 +9,7 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
9
9
|
- Role withdrawal matches the task id, role kind and owner-request header. Owner acceptance evidence must be that peer's accepted entry after the latest ownership event, never another owner's state or an earlier assignment span. Withdraw unread requests without inventing delivery evidence. Find them with `bus.pendingEnvelopes(peer)`, which includes recovery-deferred notices, never the snapshot's queues (#478). Preserve the original former-owner stop notice after a superseding escalation, while suppressing stale success/decision tails.
|
|
10
10
|
- Capacity declines (#531): `hub_task_decline` takes an optional `capacity: true`, recorded as `capacity: true` on the `declined` history entry. `excluded()` skips such an entry, and `Tasks.decline` passes the decliner as `exclude` to that move's own reroute only, so a later reroute or planner pull may offer the task to it again from its reservation. Never infer a capacity decline from the note text, and never treat a row without the field as one: older rows are refusals. `plannerRefusal` (peer ids and counts only, never task text) names why `pullPlanned` found no owner, and `planBacklog` refuses a reservation for a peer `excluded()` returns. `assign()` stays pure, so `route explain` reads the same `excluded()` and `plannerFull` inputs as assignment.
|
|
11
11
|
- `assign()` never defaults to the task's current owner, or a decline can only come back to the decliner. A reservation (#207) is the same risk: the reserved owner goes first only through `blocked()`, and a decline or an escalation for reason `rejections` or `manual` records the owner it left in the history entry's `from`, which `excluded()` reads, so no later reroute hands the task back through its reservation.
|
|
12
|
+
- A named owner that `assign()` refuses is never a silent no-op (#601): `assignTo` passes `refuseNamed` so `assignOwner` throws with the routing refusal reason before any write (pre-effect for tool callers, a plain `Error` for the console), and a proposal's unmet named owner is reported in the propose result through `namedOwnerNote`, read from the stall the proposal recorded. The task stays where it was; never deliver to an offline peer to make the name stick.
|
|
12
13
|
- The state machine allows `in_progress -> approved` only for classes without a reviewer; `review()` checks `in_review` itself.
|
|
13
14
|
- Treat `cancelled` as closed through `isClosedTaskState`, but keep dependency release restricted to approval. Cancellation goes through `Tasks.cancel`, preserves the row and roles, clears its reservation/cohort, sends revision-bound stop notices and writes no outcome memory; never map it to approval or cancel a native turn or worktree. Planner cancellation stays limited to its own historically unaccepted proposed/backlog work; accepted work remains person-only.
|
|
14
15
|
- Capture `TaskOffer.pullOnly` and the exact offer envelope id at publication. Keep unread-offer attention independent of attachment and restart until acceptance, decline, supersession or `offerRead` evidence; never replace that evidence with a generic inbox timestamp or completion state.
|
|
@@ -38,7 +39,8 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
38
39
|
|
|
39
40
|
- Feed the same current spec/test capability map into every `assign()` path, explicit nomination and reservation. Apply these capability refusals only to non-PII owner admission, preserving reviewer and on-prem PII routing exemptions; refuse a missing required capability before a manual owner assignment write. Bind spec/check authority and result delivery to the current task, owner, peer generation and privacy policy.
|
|
40
41
|
|
|
41
|
-
- Resolve any id that may be an extra instance with `
|
|
42
|
+
- 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`.
|
|
43
|
+
- 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`.
|
|
42
44
|
- Admit a declared extra as owner of `implement`, `bulk_edit` or `test` work only with the current implementer role and `ownerIsolation[id] === true`, the daemon's proof that the live adapter started in `executionRoots[id]` outside the canonical cwd. Never derive it from the declaration's `worktree`, its roles or a reload.
|
|
43
45
|
|
|
44
46
|
- Spec acceptance (#481 S1) is written only by `Tasks.acceptSpec` for the person; never accept it from a tool or a receipt. The daemon uses `Tasks.acceptReviewed` to bind spec and plan readbacks to one task snapshot before `Tasks.accept`; retain its synchronous missing-spec and plan-admission refusals. Apply WIP and plan admission before `done` implicitly accepts proposed work and before cohort writes. Never read or store an issue body: identity and edit times only.
|
|
@@ -55,3 +57,6 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
55
57
|
- 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"]`.
|
|
56
58
|
- `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).
|
|
57
59
|
- 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.
|
|
60
|
+
|
|
61
|
+
- 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.
|
|
62
|
+
- 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.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Scope: tests, fakes and the test gate. Read before adding or changing a test or fake under `test/`, or editing `scripts/check.sh`, `scripts/hang-watch.sh`, `scripts/seeded-check.ts` or `scripts/seeds.json`.
|
|
4
4
|
|
|
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
|
+
- 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.
|
|
5
7
|
- 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.
|
|
6
8
|
- In Bun 1.3.14 a test timeout that fires while `Bun.spawnSync` runs can start the next test inside spawnSync's own event loop, where another spawnSync then spins at full CPU for good (trigger not pinned down; see README). Bun 1.4.2 still runs the next test inside the outer spawnSync's event loop. `scripts/check.sh` runs tests with a 20 s timeout and under `scripts/hang-watch.sh`; a test that spawns and reads the process table gets a timeout of its own.
|
|
7
9
|
- Sandboxed commands and the MLX server get a fixed environment (`PATH`, `HOME` and their own keys), so test knobs for their fakes travel in a wrapper script or argv, not in `process.env`; native peer children inherit the hub's environment minus the agent markers (`peerChildEnv`).
|
|
@@ -12,17 +14,19 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
|
|
|
12
14
|
- `ahub upgrade` without `--to` asks the registry (`npm view`) and hands a target newer than the CLI to `bun x` (#272): a test that reaches either puts a fake `npm` first on `PATH` and a recording stand-in for `process.execPath`, as `test/upgrade-cli.test.ts` does. The interactive screens are tested through a scripted `ScreenIO`; a CLI test that needs them marks `process.stdin` and `process.stdout` as terminals in a wrapper script.
|
|
13
15
|
- Cross-process fixtures wait for the consuming client to process its handshake (the channel connected diagnostic), not merely the daemon's attached/idle state. Deadline-routing fixtures hold and release responses at the fallback decision; do not place a fixed sleep between deadline fractions and assume scheduler headroom (#287). Receipt-order fixtures use the normal watchdog for ordinary I/O, trigger expiry after observed native acceptance, and await a consumed transport barrier before asserting on late results.
|
|
14
16
|
- A bounded polling wait must assert its condition after the last attempt, or document why giving up is intentional. Append-only event histories wait for a correlated closing event, not removal of the opening event. Before changing a slow wait, identify the unreached state and repair its cause (#296).
|
|
17
|
+
- A test of turn or notice behavior that is not about the snapshot budget yields an I/O turn (`await new Promise(r => setImmediate(r))`) between captures: captures in one synchronous burst share the 250 ms budget and may correctly record unknown evidence. A wait on concurrent-edit evidence correlates the expected turn ids and paths (#617).
|
|
15
18
|
- A request that a test expects a stop or shutdown to cut gets its rejection handler where the request is created, never after the awaited teardown: a late `catch` races the reset and the run fails on an unhandled rejection.
|
|
16
19
|
- Run `bun scripts/seeded-check.ts` before reporting the seeded-guard gate complete. 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. 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).
|
|
17
20
|
|
|
21
|
+
- `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.
|
|
18
22
|
- `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.
|
|
19
23
|
- The whole-suite watchdog is `AHUB_CHECK_HANG_S` (default in `scripts/check.sh`, hosted values for both OSes in `.github/workflows/check.yml`; #378 and #593, macOS CI hit it while tests still passed; currently 960 s local, 1080 s Linux, 1260 s macOS): raise it only when the suite was still progressing at the cutoff, never for a hung test, and keep the per-test 20 s timeout. The check job's `timeout-minutes` must leave room above that value for setup: raise them together.
|
|
20
24
|
- A seed's named test must run on Linux, where the seeded job runs: a darwin-only test reports no result there and fails the job (#382). Cover a macOS-only protection with its regression test instead.
|
|
21
25
|
|
|
22
26
|
- Keep CI's Bun version sourced from `.bun-version` through the pinned setup-bun action; update the version file rather than adding per-job version literals.
|
|
23
27
|
- Release verification requires both OS checks and the aggregate `seeded guards` success for an identical tree, a tag matching `package.json` on `origin/main`, and fresh ancestor-bound documentation stamps. Preserve the separate npm OIDC publish job and GitHub Release write job; neither job gets the other's credential authority.
|
|
24
|
-
- `test/conformance/` runs real adapters against fake transports. Record a case that does not apply
|
|
28
|
+
- `test/conformance/` runs real adapters against fake transports; `test/conformance/enrollment.ts` enrolls every `ADAPTERS` definition and each `:reviewer` variant. Record a case that does not apply in its `UNSUPPORTED` or `UNSUPPORTED_LEGS` map with a concrete reason naming the dedicated coverage, never `test.skip` or synthetic success, and never change native production behavior to satisfy a generic case.
|
|
25
29
|
|
|
26
|
-
- Keep the CI admission lock around the whole controller workflow, not just its decision job. Preserve successful identical-tree full proof and required job names; a skipped job, pending controller or unavailable active inventory is never a successful full gate. CI reuse tests use fixed metadata fixtures and never dispatch real duplicate workflows.
|
|
30
|
+
- Keep the CI admission lock around the whole controller workflow, not just its decision job, in its per-PR (`check-pr-<number>`, `queue: single`: a newer pending run replaces the older) or main (`check-main`, `queue: max`) group, never with `cancel-in-progress: true` beside `queue: max` (GitHub refuses it) (#609); two groups admitting one tree at once is an accepted duplicate run, never a reason to fail closed on another tree. Preserve successful identical-tree full proof and required job names; a skipped job, pending controller or unavailable active inventory is never a successful full gate. CI reuse tests use fixed metadata fixtures and never dispatch real duplicate workflows.
|
|
27
31
|
|
|
28
32
|
- Headless-reviewer tests run `test/fakes/fake-claude-print.ts` through `command` or a PATH stand-in, and fresh Codex reviewer tests run `test/fakes/fake-codex-reviewer.ts` through `codex_bin`; a test that reaches the daemon's reviewer start must never run the real `claude` or `codex`. The fake's sandbox markers test host admission only, never native isolation. Fresh Kimi reviewer tests set and restore `KIMI_CODE_HOME` to a fixture native home (the backend links that home's `credentials/`, `oauth/` and `device_id`, else the person's `~/.kimi-code`) and run `test/fakes/fake-kimi-reviewer.ts` through `command` or a PATH stand-in, never the real `kimi`. agy tests put a fake `agy` first on PATH and set `HOME` to a temp dir before any transcript readback (`readAgyTranscript` reads `$HOME/.gemini`).
|