@staix/agent-hub 0.12.19 → 0.12.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +7 -6
  3. package/docs/agent-notes/adapters.md +10 -2
  4. package/docs/agent-notes/bus.md +6 -2
  5. package/docs/agent-notes/daemon.md +10 -3
  6. package/docs/agent-notes/local-worker.md +3 -1
  7. package/docs/agent-notes/models.md +3 -0
  8. package/docs/agent-notes/tasks.md +6 -3
  9. package/docs/agent-notes/tests.md +3 -2
  10. package/docs/bench.md +143 -0
  11. package/docs/events.md +6 -2
  12. package/docs/operations.md +655 -34
  13. package/docs/quickstart.md +12 -10
  14. package/docs/research.md +92 -0
  15. package/docs/security.md +59 -17
  16. package/docs/smoke.md +137 -0
  17. package/docs/specs/2026-09-19-agent-hub-design.md +586 -16
  18. package/docs/specs/2026-09-20-pi-local-models-design.md +2 -0
  19. package/docs/specs/2026-09-20-upgrade-recovery-design.md +345 -0
  20. package/docs/specs/2026-10-04-switchyard-source-port-design.md +10 -0
  21. package/docs/verified.json +29 -18
  22. package/package.json +1 -1
  23. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  24. package/plugins/agent-hub/server.js +65 -26
  25. package/src/adapters/acp.ts +67 -6
  26. package/src/adapters/claude-channel.ts +61 -20
  27. package/src/adapters/codex-appserver.ts +70 -2
  28. package/src/adapters/local-worker.ts +8 -3
  29. package/src/adapters/pi.ts +58 -4
  30. package/src/cli/bench.ts +287 -0
  31. package/src/cli/console-state.ts +531 -110
  32. package/src/cli/console.ts +59 -26
  33. package/src/cli/facts-hook.ts +50 -8
  34. package/src/cli/help.ts +130 -0
  35. package/src/cli/identity.ts +11 -5
  36. package/src/cli/launch.ts +98 -17
  37. package/src/cli/main.ts +325 -88
  38. package/src/cli/preview.ts +14 -3
  39. package/src/cli/reset.ts +194 -0
  40. package/src/cli/status-lines.ts +10 -5
  41. package/src/cli/terminal-recovery.ts +196 -40
  42. package/src/cli/upgrade-runtime.ts +327 -60
  43. package/src/cli/upgrade.ts +442 -45
  44. package/src/hub/ask.ts +1 -1
  45. package/src/hub/bench.ts +211 -0
  46. package/src/hub/board.ts +15 -7
  47. package/src/hub/bus.ts +79 -8
  48. package/src/hub/child-process.ts +1 -0
  49. package/src/hub/conductor.ts +54 -21
  50. package/src/hub/config-trust.ts +1 -0
  51. package/src/hub/control-client.ts +9 -4
  52. package/src/hub/daemon.ts +401 -48
  53. package/src/hub/delivery-journal.ts +6 -6
  54. package/src/hub/events.ts +9 -2
  55. package/src/hub/execution-budget.ts +15 -6
  56. package/src/hub/hub-tools.ts +4 -4
  57. package/src/hub/inference.ts +136 -1
  58. package/src/hub/lifecycle.ts +13 -12
  59. package/src/hub/manager.ts +16 -14
  60. package/src/hub/peers.ts +2 -0
  61. package/src/hub/permission-mode.ts +44 -0
  62. package/src/hub/recovery-store.ts +68 -8
  63. package/src/hub/registry.ts +36 -22
  64. package/src/hub/report.ts +43 -1
  65. package/src/hub/research.ts +255 -0
  66. package/src/hub/restart.ts +25 -0
  67. package/src/hub/routing.ts +48 -16
  68. package/src/hub/snapshots.ts +3 -2
  69. package/src/hub/tasks.ts +295 -115
  70. package/src/hub/tool-refusal.ts +7 -0
  71. package/src/hub/ui.ts +4 -1
  72. package/src/local/deny.ts +2 -1
  73. package/src/local/tools.ts +14 -10
  74. package/src/models/mlx.ts +11 -8
  75. package/src/models/ollama.ts +2 -2
  76. package/src/models/relay.ts +190 -42
  77. package/src/models/route/relay-selector.ts +43 -31
  78. package/src/models/route/runtime.ts +17 -7
  79. package/src/models/route/signals.ts +12 -1
  80. package/src/models/route/stage.ts +57 -3
  81. package/src/omniroute/client.ts +1 -1
  82. package/src/pi/process-signature.ts +23 -0
  83. package/src/pi/tool-receipts.ts +7 -1
  84. package/src/ui/index.html +84 -6
  85. package/src/ui/task-progress.ts +33 -0
  86. package/templates/AGENTS.block.md +1 -1
  87. package/templates/routing.toml +8 -0
@@ -89,7 +89,8 @@ Facts below are tagged **verified** (measured on 2026-09-19 on the owner's Mac)
89
89
  7. Quota relay hands open tasks to the `local` peer first, then waits for the window
90
90
  reset for judgment-heavy classes. Task-level escalation on verification failure.
91
91
  8. Safety defaults: cross-peer text is framed as untrusted; permission prompts stay on
92
- unless `--unattended`; ports bind loopback only; Kimi ACP permission requests are
92
+ unless `--unattended` or an operator-selected permission mode (issues #240,
93
+ #242); ports bind loopback only; Kimi ACP permission requests are
93
94
  relayed to the hub console. Loopback is not enough on its own (any web page can open a
94
95
  WebSocket to 127.0.0.1), so the control WS requires a per-run token and both hub
95
96
  servers refuse requests that carry an `Origin` header (amended in M1).
@@ -97,8 +98,9 @@ Facts below are tagged **verified** (measured on 2026-09-19 on the owner's Mac)
97
98
  10. Shared memory reuses claude-mem as the single store; the hub never runs its own
98
99
  memory database. Native capture stays where it exists (Claude hooks, Codex plugin,
99
100
  Kimi transcript bridge); the hub captures only the local worker and adds
100
- cross-platform recall, task-scoped briefs and explicit shared notes. The bus still
101
- passes messages, never transcripts.
101
+ cross-platform recall, task-scoped briefs and explicit shared notes. Hub tool calls
102
+ are not captured; Tasks alone decides what screened tool text reaches memory.
103
+ The bus still passes messages, never transcripts.
102
104
 
103
105
  ## Design
104
106
 
@@ -279,9 +281,10 @@ peer. Peer ids claimed over the control WS must not be `user` or a hub-managed a
279
281
  injection is dropped for the same reason as `dynamicTools`.
280
282
  - PII (amended in M4): a task matching `signals.pii_patterns` is owned by `local` or by
281
283
  nobody; its envelopes are `private` (console tail and `hub.log` print a stub), lists show
282
- `[pii]` to everyone but `local`, `ahub task show` is the one place the console reads it; the
283
- reviewer is the console user; `local` answers such a turn to the console only, keeps it out
284
- of its history, refuses it when the only gateway is off campus (Cloudflare Access), and
284
+ `[pii]` to everyone but `local` inside a PII turn; `ahub task show` is the one place
285
+ the console reads it. The reviewer is the console user; `local` answers such a turn
286
+ to the console only, keeps it out of its history, refuses it when the only gateway
287
+ is off campus (Cloudflare Access), and
285
288
  nothing reaches claude-mem (no brief, no note, no capture), because claude-mem's observer is
286
289
  a cloud model (verified: `/api/health` reports `ai.provider: claude`).
287
290
 
@@ -486,7 +489,8 @@ inside a peer.
486
489
  ### Safety
487
490
 
488
491
  - Untrusted framing on all cross-peer text; standing instruction once per session.
489
- - `ahub claude` and `ahub codex` keep normal permission prompts. `--unattended` opts into
492
+ - `ahub claude` and `ahub codex` keep native permission defaults unless the person
493
+ selects a permission mode (issues #240, #242). `--unattended` opts into
490
494
  `--dangerously-skip-permissions` (Claude) and `--dangerously-bypass-approvals-and-sandbox`
491
495
  (Codex 0.154.0 documents this flag, not `--yolo`) and prints a warning.
492
496
  - Kimi and local tools: cwd-scoped, secrets denylist (a secrets directory, `.env*`, keys).
@@ -496,7 +500,7 @@ inside a peer.
496
500
  ### Configuration and state
497
501
 
498
502
  - `.agenthub/config.json` (roles, ports, filter tiers, watchdog), `.agenthub/routing.toml`.
499
- - `.agenthub/state/` gitignored: `hub.pid`, `status.json`, `control-token` (0600, per run),
503
+ - `.agenthub/state/` gitignored: `status.json` (with the daemon's pid and process signature, #226; daemons up to 0.12.20 also wrote `hub.pid`), `control-token` (0600, per run),
500
504
  `hub.db` (tasks, messages, budget; from M4), `hub.log`, `switchyard.toml`, `checkpoint.md`.
501
505
  - Env: `AGENTHUB_STATE_DIR`, `AGENTHUB_OMNIROUTE_URL`, `OMNIROUTE_API_KEY`,
502
506
  `AGENTHUB_SWITCHYARD_BIN`, `AGENTHUB_UNATTENDED`.
@@ -763,7 +767,8 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
763
767
  `hub_send` or a task tool, are answered with their `allow_once` option by the
764
768
  hub, without a console prompt, and logged by tool name only. This matches the
765
769
  `approval_mode = "approve"` Codex already gets for the same tools. Every other
766
- request keeps the console path; `allow_always` is never chosen automatically.
770
+ request keeps the console path unless the person opts into the permission modes
771
+ below (issues #240, #242); `allow_always` is never chosen automatically.
767
772
  - For the other tools, the console payload falls back to the streamed argument
768
773
  text once it is a complete JSON object. A partial stream stays unresolved and
769
774
  keeps `allow_always` withheld.
@@ -776,6 +781,68 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
776
781
  configured server of the same name, so only the hub's `agent-hub` server can
777
782
  produce the approved names.
778
783
 
784
+ ## Amendment: operator permission modes (issues #240, #242)
785
+
786
+ - Modes: ask leaves native decisions unchanged; ask-when-needed uses native
787
+ Kimi/Codex policies or scoped Claude/Pi/local grants; never-ask suppresses
788
+ prompts within each peer's existing execution boundaries.
789
+ - Runtime CLI permission changes are refused from ordinary agent shells.
790
+ Never-ask needs --yes; console mode selection needs a separate y. These
791
+ policy/role checks do not contain a hostile same-user shell: Kimi can clear
792
+ markers, use --yes or read the control token and impersonate the console.
793
+ The security and operations guides name that boundary explicitly.
794
+ - permission_modes defaults merge per peer across config files. Automatic
795
+ effects stop at ask-when-needed. Never-ask from an applied untracked config remains ask until
796
+ a person's source-labelled console y at hub start; no boot --yes applies it.
797
+ Git-tracked mode defaults are ignored and never offered for confirmation.
798
+ Every non-ask start logs mode and source (config file or runtime choice). Successful
799
+ runtime changes clear pending config confirmations; startup choices are reconciled
800
+ under the peer fence before a new adapter is exposed to bus delivery. Runtime changes and
801
+ confirmations remain daemon-local and expire on stop.
802
+ - Newly added native peers replay their non-offline state to drain queued work,
803
+ publish state telemetry and persist sessions. Offline/absent ask clears only
804
+ the hub choice. Unattended metadata is saved after successful startup, and
805
+ concurrent joined starts with different flags are refused. Refused default
806
+ confirmation remains handled until state changes; Esc defers without declining.
807
+ - Kimi mapping requires the actual Kimi Code CLI identity: yolo for
808
+ ask-when-needed, auto for never-ask, default for ask. Another ACP agent starts fresh or resumed as unmanaged: its reported mode is
809
+ untouched, and runtime changes are refused until its table exists. Set-mode replies precede prompts;
810
+ missing ids/refused startup changes fail. A timeout is unknown/offline and
811
+ the owned process group stops, so a late reply cannot restore a live peer.
812
+ Loaded non-default Kimi modes are reset for project ask.
813
+ - Codex overlays on-request/never on every outgoing turn/start without changing
814
+ sandbox fields. Ask restores a known native policy once after an accepted
815
+ turn; rejected restoration remains pending. Unknown native baseline refuses
816
+ restoration. Actual native unattended flags, not broker-wide flags, govern
817
+ the refusal to change a bypassed session.
818
+ - Claude ask-when-needed allows only Read/Edit/Write/MultiEdit/NotebookEdit/LS
819
+ and bounded Glob/Grep targets resolved with realPath beneath project root,
820
+ outside .agenthub/.git/.claude/.codex/.qwen/.kimi/.pi/.mcp.json. Missing/nonresolving or escaping targets,
821
+ protected case aliases, symlinks and uncertain wildcard/filter targets yield
822
+ no decision. Never-ask answers allow; ask and transport errors decide nothing.
823
+ Merged/generated settings are 0600 state files passed by path, never inline
824
+ in argv. Preview writes no settings file. The owning launcher removes its settings file
825
+ after native exit; earlier active or unknown launchers retain theirs. Verified
826
+ dead wrapper and native identities permit crash fallback cleanup. Without
827
+ separate native identity proof, prior files remain for manual inspection.
828
+ - Permission-only Claude PreToolUse never drives busy state. Only the former
829
+ observation purposes retain native turn bookkeeping. Hook identity/purpose
830
+ are launch/session bound; unverified hook status reads unverified.
831
+ - Pi/local read never asks. Ask-when-needed grants write/edit inside the project, outside the shared native
832
+ configuration segments (.claude/.codex/.qwen/.kimi/.pi/.mcp.json, compared folded and by the path within the
833
+ project) and never through a hard link; bash and mutating
834
+ git retain console approval. Never-ask grants once, retaining sandbox and
835
+ path guards; no mode gives allow-always. ACP tools have no hub sandbox and
836
+ the never-ask confirmation names that difference.
837
+ - Console-role permission and permission_default requests are additive;
838
+ ordinary tools/conductor/messages cannot change or confirm modes. Status
839
+ includes non-ask/unverified/unknown mode state and pending defaults; facts
840
+ replies include permission and authenticated projectRoot. Unknown requests
841
+ are answered, so PROTOCOL remains unchanged. permission_mode events retain
842
+ schema 1 and include only peer/from/to, never tool arguments.
843
+ - Live benign Kimi/Claude results and unverified Codex/Pi limits are recorded
844
+ in docs/smoke.md. No extra account prompts accompany these review corrections.
845
+
779
846
  ## Amendment: approval notifications and timeout (issue #5)
780
847
 
781
848
  - A waiting permission request raises one macOS notification (`osascript`) with
@@ -986,15 +1053,19 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
986
1053
  route explain` and assignment agree. Accept and done refuse a waiting task,
987
1054
  for the console user too.
988
1055
  - A proposal that names an owner (a claim, or an owner for someone else) and
989
- would wait is refused: the owner of waiting work is chosen when it is ready,
990
- by routing or by `ahub task assign`.
1056
+ would wait stores that owner as the task's reserved owner (issue #207 below
1057
+ replaced the refusal this bullet first specified).
991
1058
  - Approval, by review or by a class without a reviewer, releases the
992
1059
  dependents that wait for nothing else; each is recorded as `ready` and assigned.
993
1060
  A proposal re-reads what it waits for after its insert, since triage may have
994
1061
  awaited an approval. An approval is saved before its dependents are assigned,
995
1062
  so the daemon's 60 s release timer sweeps for ownerless tasks whose last event
996
1063
  is `blocked` or `ready` and that wait for nothing: each is offered once per hub
997
- run, once an attached peer can take it, outside recovery operations.
1064
+ run, once an attached peer can take it, outside recovery operations. An offer
1065
+ is used up only by an assignment, after an approval and on the timer alike;
1066
+ failed dependent reads, ready writes or assignments do not escape a saved
1067
+ approval. Their notices are best effort because notification can read the
1068
+ same unavailable board. An ownerless task stays eligible once a peer can take it.
998
1069
  - A recovery commit also waits for task operations in flight and for completion
999
1070
  checks queued or running: both can write the board after its integrity digest
1000
1071
  (a check the commit's stop kills records `check interrupted`).
@@ -1360,14 +1431,59 @@ approval. Deny is direct. Pending requests include expiry and are withdrawn by
1360
1431
  Panels expose Peers, Approvals, Tasks, Queue and Events with bounded polling and
1361
1432
  Unicode cell widths, fall back below 80x24, and restore terminal state on exit.
1362
1433
 
1434
+ Whole-board progress (#246) uses one pure `taskProgress` function shared by the
1435
+ console and hashed dashboard script. It reads only public id/state/deps fields;
1436
+ PII stubs count like other tasks. Waiting partitions proposed tasks whose public
1437
+ dependencies are not approved, including unknown dependencies. Approved/all gives
1438
+ the done fraction (zero on an empty board). Four stages are proposed, in progress,
1439
+ in review and approved; changes requested returns to stage two and is marked.
1440
+ The console Tasks panel has a 20-cell ASCII summary bar and fixed four-cell row
1441
+ meters, preserving width and color-off geometry. Cells and percentage round down;
1442
+ count labels use importance order changes requested, review, waiting, in progress,
1443
+ proposed, then unknown when nonzero, and drop only from the end. The stream count is in the fixed footer,
1444
+ from existing all-state status counts; it makes no board fetch and retains a
1445
+ row-one scroll region for terminal scrollback. Loading/failed reads never imply
1446
+ confirmed empty progress. Dashboard filters affect rows only, not totals. Its
1447
+ full-width stacked SVG and stages have text equivalents; the existing signature
1448
+ guard prevents unchanged snapshots from mutating the progress live region.
1449
+ System/Light/Dark choice is applied as `data-theme` before first paint, persists
1450
+ in a one-year host-scoped `SameSite=Strict` cookie through guarded access and
1451
+ follows OS CSS preference on System. Cookie port independence shares the choice
1452
+ between hub starts and projects. The shared model is injected before the page's
1453
+ own inline script/style CSP hashes, with no new control message or CLI JSON change.
1454
+ UI public rows expose optional public-view deps for waiting. This is additive
1455
+ metadata enrichment of the existing ui_snapshot payload, not a changed control
1456
+ message envelope/operation, so it does not bump PROTOCOL. Unknown task states
1457
+ count in an explicit unknown bucket with stage zero; no NaN or prototype-key
1458
+ updates are permitted. Approval fraction is derived from counts/total at display
1459
+ time, avoiding a redundant floating-ratio field.
1460
+
1363
1461
  Console semantic colors (issue #201) use spans with a fixed terminal-native
1364
1462
  palette: cyan information, bold cyan active/selected labels, green availability
1365
1463
  and success, yellow attention/waiting, red failure/intervention, and restrained
1366
1464
  bright-black metadata. Offline peers, ordinary titles, action details and body
1367
1465
  lines use the default foreground. Existing labels, selection markers and
1368
- confirmation prompts remain sufficient without color. Stream headers use event
1369
- structure for their tone; body text is never parsed for meaning or passed
1370
- through as terminal styling. Denial requests and observed expiry/cancellation
1466
+ confirmation prompts remain sufficient without color. Console stream headers
1467
+ color meaningful tokens only (#239; dated issue Decisions): Claude bright blue
1468
+ (94), Codex bright cyan (96), all other peers default; `task` magenta (35, never
1469
+ bold), numbers bold default foreground (1), task references magenta underlined
1470
+ (4;35), and other issue references underlined default foreground (4). No token
1471
+ SGR equals a state or peer SGR. Green/yellow/red remain states/approvals/failures,
1472
+ cyan remains navigation. Case-insensitive `task #N`, `Task #N`, `[task #N]` and
1473
+ `[review #N]` identify task references; unbracketed prose `review #N` remains an
1474
+ issue reference. Ordinary words keep default foreground;
1475
+ there is no hashing or user theme.
1476
+ Only hub-written peer slots, state words and structural markers receive their
1477
+ semantic tones; title words and markers cannot choose them. Numeric tokens have
1478
+ whole-token boundaries, so UUIDs and ISO identifiers stay plain. Legacy notices
1479
+ lack typed reference ranges: the task/review prefix heuristic is bounded by a
1480
+ `ponytail:` comment; other `#N` tokens use the issue tone until structured notice
1481
+ spans classify assign/which references. Header text crosses `terminalText` before tokenization. Wrapped headers normalize
1482
+ tabs to spaces first; plain headers retain their original tabs in both color modes. Wrapped spans retain the source classification. After a projection
1483
+ miss on the first physical header, only structural tones remain. A missed
1484
+ continuation and all later continuations are fully plain. Projection advances
1485
+ a span index through the source once, keeping long permission titles bounded. Bodies and every
1486
+ command-output line stay plain. Denial requests and observed expiry/cancellation
1371
1487
  are red; an answered remote closure lacks option-kind metadata and is not
1372
1488
  guessed to be a denial.
1373
1489
 
@@ -1377,13 +1493,66 @@ checks even for redirected stream output, without enabling panels or raw mode;
1377
1493
  never disables styling. Invalid values fail with usage text before connection.
1378
1494
  Plain `renderConsole` remains available, while `renderConsoleLines` exposes
1379
1495
  semantic spans. Geometry is computed from sanitized plain Unicode text before
1380
- painting. `paint` sanitizes every span, inserts only fixed palette SGR sequences
1496
+ painting. Wrapping breaks at whitespace and splits only a word longer than a whole
1497
+ line; a wrapped line's continuations start four columns deeper than
1498
+ its source line; a source indent and a continuation indent are both capped at half the width, so wrapped peer text in the stream
1499
+ never starts at a column where hub-written lines start (#212). `paint` sanitizes every span, inserts only fixed palette SGR sequences
1381
1500
  and resets each styled span. Interactive exits, signals, disconnect and errors
1382
1501
  retain the console restoration sequence. No new dependencies, protocol changes,
1383
1502
  polling or redraw triggers are added. Tail, logs and machine-readable output
1384
1503
  remain unchanged. Native light/dark readability and idle-CPU observations are
1385
1504
  separate smoke evidence, not inferred from fake-terminal tests.
1386
1505
 
1506
+ Console layout (issue #213) separates the panel header (the project root's
1507
+ directory name, since the registry id is a hash, then the tabs), the body and
1508
+ the footer with ASCII `-` rules, and puts one rule above the stream footer; the
1509
+ console emits no box-drawing or other ambiguous-width glyph. The active tab is
1510
+ bracketed; the Approvals and Queue tabs count pending requests and
1511
+ `needs_review` deliveries. Peers, Approvals, Tasks and Queue are tables: a
1512
+ header row, columns two spaces apart sized with `Bun.stringWidth` from every
1513
+ row (at most a third of the width), the last column taking the rest. Peers' LINK
1514
+ column reads `tools-only` when the optional `toolsOnly` status field (#205) is
1515
+ present. Zero counters read `-`, durations `45s`, `12m`, `3h05m` or `2d03h`, and
1516
+ resets and pauses are relative. Detail views and the Approvals panel's request
1517
+ block put labels at column 0 and values one column past the longest label; a
1518
+ list puts `- ` before each item, and every other line of a value (its own
1519
+ newlines, wrapped continuations) starts two columns deeper, so untrusted text
1520
+ passes neither for a label nor for an item. Only a field's own string shows as
1521
+ written: every agent-written string below it (nested pairs and lists, list
1522
+ items) and every option name or id in the Approvals block, the prompts and the
1523
+ stream's permission line is JSON-quoted unless it is a plain word starting with
1524
+ a letter, so `;`,
1525
+ `,`, `)`, quotes and newlines in it cannot pass for hub structure. Values show
1526
+ no JSON objects, epoch milliseconds or ISO times. Allow options keep one line
1527
+ each; `d deny` always stays in view, options that do not fit are counted on a
1528
+ `(more)` line, table rows give way, and a cut title is marked `(more)` in the
1529
+ label column. The selected request is tracked by id: only the first request is
1530
+ selected on arrival, `[`, `]` and j/k move the selection, `a`, `d` and `v` act on
1531
+ it alone, and its detail is rendered from the live request. When it closes, the
1532
+ selection and its detail clear with a notice; `a`, `d` and `v` act on nothing
1533
+ until `[`, `]` or j/k select again, so a request that arrives as the person
1534
+ reacts is never acted on. Everything pending on a request belongs to the
1535
+ selected one, checked after every key and every prune: an option choice or
1536
+ confirmation for another request is dropped, an open detail follows the
1537
+ selection from its top, and the option prompt names the request. `a`, `d` and
1538
+ `v` act only while the request list or its detail is shown, and a task or
1539
+ delivery detail that arrives after the person moved to another panel or row is
1540
+ dropped; with a task, delivery or peer detail open, the row keys act on the item
1541
+ the detail shows, by its id. A pushed request's agent-supplied fields are kept as
1542
+ strings, so a malformed one cannot throw on a draw. The key table is modal: only
1543
+ `?`, Escape and `q` act under it, and a paste is dropped. Offline peers keep the default foreground, also
1544
+ when selected, and a tab counts as one space when a line is cut. The footer
1545
+ hint lists only the keys valid in the mode, panel and state, and `?` shows a
1546
+ key table grouped by panel (in stream mode it is printed into the stream, as
1547
+ `v`'s copy of a request is, without entering the Events buffer). Errors and refusals take the failure tone, informing notices (a closed or
1548
+ missing selection) the attention tone. A notice clears at the next key, or once 10 s have passed when the
1549
+ existing 1 s tick redraws. In the stream, `v` prints the selected request with
1550
+ the push's framing, and `:` command output lines start at column 4. `fit`
1551
+ measures a cut line as `paint` prints it, so a cut that ends a span in
1552
+ `[agent-hub` cannot widen the line with a `> ` marker. A context reading in the
1553
+ console stream and Events panel shows its measurement time as local time, as
1554
+ event headers do; `ahub tail` and `ahub status` keep the ISO time.
1555
+
1387
1556
  Agent shell CLI calls connect as the detected peer in tools mode. Hub launches
1388
1557
  set `AGENTHUB_PEER_ID`; the pinned installed Codex shell injects
1389
1558
  `CODEX_THREAD_ID` after environment filtering, and Claude uses `CLAUDECODE`.
@@ -1534,3 +1703,404 @@ only. The path guard, denylist and sandbox still apply to each call; the
1534
1703
  dashboard still offers Pi only deny; unattended mode still picks `allow_once`;
1535
1704
  the local worker keeps allow and deny. The console and `ahub permit` already
1536
1705
  handle more than one allow option, so the control protocol is unchanged.
1706
+
1707
+ ## Amendment: Claude sessions without channel evidence (issue #205)
1708
+
1709
+ Claude Code without `--dangerously-load-development-channels` drops channel
1710
+ notifications without an error, so a plain `claude` session used to mark every
1711
+ push `accepted` and show none of them.
1712
+
1713
+ - Evidence: `ahub claude` passes the flag and, in the same launch, sets
1714
+ `AGENTHUB_CHANNEL=1` in Claude's environment (`nativeLaunchEnv`);
1715
+ `peerChildEnv` removes the marker from every other native child. The plugin's
1716
+ MCP server inherits Claude Code's environment (a local check on 2026-10-09,
1717
+ not a live smoke: a plain session's plugin server inherited a shell-only
1718
+ `AGENTHUB_*` variable, and the plugin's `.mcp.json` sets no env), as it
1719
+ already does for
1720
+ `AGENTHUB_STATE_DIR` and `AGENTHUB_PEER_ID`. Only the value `1` counts. With
1721
+ the marker the server keeps
1722
+ the push and settlement contract of "Live channel settlement" unchanged.
1723
+ Without it (a plain `claude`, an IDE session, or the flag passed by hand) the
1724
+ session attaches tools-only.
1725
+ - Decision: the launch marker is sufficient evidence, and no positive readback
1726
+ is required before the first `accepted`.
1727
+ - The marker and the flag come from the same launch, so the marker shows that
1728
+ this Claude process got the flag. A readback would add per-push proof of
1729
+ display, which stays out of scope: `accepted` means the bridge handed the
1730
+ notification over, and `hub_delivery_done` or a correlated reply settles
1731
+ handled work.
1732
+ - A readback (a native turn citing the pushed id) would hold every first
1733
+ delivery until the model chose to answer, and a session that never answers
1734
+ would look the same as one that never rendered.
1735
+ - The existing launch identity (`AGENTHUB_INSTANCE_ID`, `AGENTHUB_LAUNCH_ID`)
1736
+ is not used: `ahub claude` sets it only when a daemon is already running at
1737
+ launch, so an `ahub claude` started before `ahub up` would lose pushes it
1738
+ can show.
1739
+ - Limits: the marker is inherited by the session's own shell children, so a
1740
+ plain `claude` started inside an `ahub claude` session reads as
1741
+ channel-capable (it also inherits the peer id and takes the peer over); so
1742
+ does `claude -p` run by a hook or script inside such a session, with its
1743
+ state dir and peer id. The marker is per process while the flag names MCP
1744
+ servers: `ahub claude --mcp-config <candidate>` without
1745
+ `--strict-mcp-config` loads channels only for `server:agent-hub`, yet the
1746
+ installed plugin server in the same process also sees
1747
+ `AGENTHUB_CHANNEL=1`. A session started with the flag by hand reads as
1748
+ tools-only and is pointed to `ahub claude`.
1749
+ - Tools-only attach: the server declares tools only, sends `channel: false` in
1750
+ its `peer` hello and lists `hub_inbox` but not `hub_delivery_done`. It never
1751
+ reports `accepted`: a `deliver` it is handed anyway goes back as a
1752
+ `needs_review` receipt. The daemon attaches its `WsPeer` as `pullOnly`; a
1753
+ push drain needs the peer deliverable (`Bus.deliverable`: not held, idle, not
1754
+ pull-only), checked at each loop and again after condensation, so a plain
1755
+ session that takes the peer while a push is condensed gets nothing. Its
1756
+ messages stay queued and count as `queued`. Board routing still treats the
1757
+ peer as attached.
1758
+ - `hub_inbox` sends `inbox`. The daemon answers "not attached yet; retry"
1759
+ between welcome and attach, and refuses it for a session with pushes.
1760
+ `Bus.pull` is held by every hold a drain respects (`Bus.held`: recovery, an
1761
+ uncertain delivery, a console, budget or conductor pause), though unlike a
1762
+ drain it does not wait for the peer to be idle; a held pull journals nothing
1763
+ and the refusal names the hold and the next action. Otherwise it takes what one
1764
+ push delivery would (the preface, then `take`'s batch of at most ten,
1765
+ important first; the reply says how many still wait) and checkpoints it like
1766
+ a push, so queued journal rows of the same envelopes (an operator retry) are
1767
+ grouped into it, then marks it `completed`, reason `read through hub_inbox`,
1768
+ in the same transaction: the tool result that returns them is the readback,
1769
+ so nothing waits in `accepted`. What it hands over is registered (and moved
1770
+ to the newest end of the cache) for `reply_to` like a push, and a read clears
1771
+ the peer's failure streak as a completed push does. If that journal write fails, only this peer's queue
1772
+ and preface are put back (every pause stays) and the bus stops with
1773
+ `delivery journal unavailable`. A refused or failed `resolveDelivery` likewise
1774
+ puts back only that peer's queue and preface and keeps every pause. Queue
1775
+ contents change in place, preserving arrays held by a suspended drain; only
1776
+ the constructor and recovery restore reload a bus snapshot. A pull is not a native turn and is not
1777
+ counted as supervision. The row is completed at hand-out, before anything
1778
+ shows the model saw it: a reply lost on the way, a tool call cancelled with
1779
+ Esc or a plugin that dies loses the batch (a `ponytail:` ceiling in
1780
+ `Bus.pull`), and `ahub queue resolve --action retry` refuses a terminal row.
1781
+ The completed row keeps the envelopes, so the operator reads them with `ahub
1782
+ queue show <id>` and sends what matters again.
1783
+ - Who holds the peer: an `ahub claude` session takes it from a plain session
1784
+ as any newer hello does, a standing-by one included (its standby check does
1785
+ not wait for a holder that status reports `toolsOnly`); attach clears
1786
+ `pullOnly`, the waiting queue is pushed to it, and the plain session stands by
1787
+ (close 4000). A plain session
1788
+ never takes it from a session with pushes, attached or still arriving: its
1789
+ hello is closed with 4000 and it stands by the same way, so an IDE or plain
1790
+ `claude` with the user-scope plugin cannot strand the channel session's
1791
+ unsettled deliveries. It attaches once that session leaves.
1792
+ - Status: `status.json` peers carry `toolsOnly`, a line naming the state and the
1793
+ next action (`ahub claude`); `ahub status`, the console footer and the
1794
+ dashboard show it.
1795
+ - Control protocol 16 adds the hello `channel` field and `inbox`. Recovery
1796
+ sources add protocol 15 (0.12.17 through 0.12.19); the transition from a real
1797
+ 0.12.19 hub is not yet proven.
1798
+ ## Amendment: owner and reviewer read their task (issue #208)
1799
+
1800
+ `hub_task_show` stays a conductor tool, and a task's current owner and current
1801
+ reviewer may also call it for that task. They get exactly the conductor's
1802
+ view: `publicConductorTask` over `Tasks.publicView(task, true)`, so the whole
1803
+ history with its done summary and check line, notes that match a PII pattern
1804
+ withheld, and a PII task as a stub with no history. Any other peer is refused
1805
+ with the conductor-role error, whatever the task. That refusal is about the
1806
+ tool, not a secret: the view holds nothing private beyond the PII stub, and the
1807
+ control op `task_show` (`ahub task show` from an agent shell) already returns
1808
+ the same public view to any peer. These reads write no conduct audit event, as
1809
+ task list polling does not. `hub_status` stays conductor-only.
1810
+
1811
+ ## Amendment: reserved owners and proposer redirects (issue #207)
1812
+
1813
+ - `reserved` is a nullable text column on `tasks`, added to existing boards
1814
+ like `plan` and `deps` (old rows reserve nobody). `hub_task_propose` with an
1815
+ `owner` and an `after` that still waits stores the owner there instead of
1816
+ refusing; the capability check for naming another peer is unchanged. With
1817
+ nothing left to wait for, a named owner is assigned as before.
1818
+ - The reservation is an input of `assign()`, read from the task, so `ahub
1819
+ route explain` and assignment agree. When no candidate list is given, the
1820
+ reserved owner is the only candidate if `blocked()` passes it; otherwise the
1821
+ trace says why it is not honored and routing proceeds through the class
1822
+ peers. PII, capability limits (`local_allowed`, Pi's context limit),
1823
+ exclusions, failing peers and offline or paused peers all pass it over, so a
1824
+ PII task still goes to `local` or nobody.
1825
+ - Lasting exclusions are refusals only: a decline records the owner it was
1826
+ declined for in its history entry's `from` (also when the console declines
1827
+ for that owner), and an escalation with reason `rejections` (a reviewer's
1828
+ repeated changes_requested) or `manual` (by hand) records the owner it took
1829
+ the task from the same way. `excluded()` reads both (older decline rows name
1830
+ only the decliner), so those peers stay out of every later reroute and of
1831
+ `route explain`: the reservation never hands the task back to them. The
1832
+ hub's own escalations after a failed delivery or inference
1833
+ (`delivery_failed`, `inference_failed`), and an owner released as gone, moved
1834
+ by the idle sweep or relayed for its budget, exclude that owner only from
1835
+ that move's own reroute: none of these is a refusal, so a later reroute may
1836
+ give it the task again from its reservation. A person's explicit assign
1837
+ (`ahub task assign`, the dashboard) is not blocked by lasting exclusions and
1838
+ clears an agent's reservation, so a later reroute does not hand the task to
1839
+ the agent's choice; the conductor's and a proposer's assigns respect the
1840
+ exclusions and keep the reservation.
1841
+ - A named candidate list (console or conductor assign, escalation, budget
1842
+ relay, the idle sweep's auto-reassignment of its suggestion) replaces the
1843
+ reservation; the idle sweep's step-3 suggestion itself is an `assign()`
1844
+ without candidates, so it consults the reservation like routing does. A task
1845
+ that waits shows its reservation in the trace before the readiness check,
1846
+ which then stops; the trace says whether routing would honor it only once
1847
+ the task is ready. `route explain <id>` also names it in the header. The
1848
+ reservation stays after it is honored or passed over.
1849
+ - Passing over a reservation is a console and hub.log notice naming the
1850
+ reserved peer and the reason, from assignment and from the idle sweep's
1851
+ suggestion alike, and the assignment's history note keeps it, appended to a
1852
+ note the caller gave (a gone owner's release). Only news is said: nothing
1853
+ when the reserved peer is the owner the task moves away from (its decline,
1854
+ escalation, release or idle sweep), and nothing once it refused the task for
1855
+ good (the decline or escalation said that). The reserved peer is not
1856
+ messaged.
1857
+ - `Tasks.assignTo` on a task that waits records a `reserved` event with reason
1858
+ `manual` and changes the reserved owner instead of the silent no-op it was;
1859
+ this applies to the console, the conductor and a proposer alike. The release
1860
+ timer treats a `reserved` last event like `blocked`.
1861
+ - `hub_task_assign` stays a conductor tool, and the task's proposer (the `by`
1862
+ of its first history entry) may call it while the task is `proposed` and its
1863
+ history has no `accepted` entry: a decline or a release puts a task that was
1864
+ worked on back in `proposed`, and that task is no longer the proposer's to
1865
+ redirect. Handing it to another peer needs `assign` when the proposer has a
1866
+ capabilities entry; redirecting to itself does not. The conductor needs
1867
+ `assign` for any assign when it has a capabilities entry. The proposer path
1868
+ is refused while the last owner or reservation change of the task
1869
+ (`assigned`, `reassigned`, `escalated`, `unassigned`, `reserved`) that the
1870
+ hub did not make itself was made by the console user: a person's assign or
1871
+ reservation stands until the conductor or the console moves it, also after
1872
+ the hub carried it out or passed it over. The id is taken as `Tasks.need()`
1873
+ takes it, a whole number or a digit string. Any other non-conductor peer, and
1874
+ the proposer once the task was accepted or a person moved it, get the
1875
+ conductor-role error. A proposer's redirect goes through `Tasks`, keeps the
1876
+ proposer as the actor with reason `manual` (on the `reassigned` or `reserved`
1877
+ entry), and writes no conduct audit event; during a PII turn it is refused
1878
+ like the conductor's.
1879
+ ## Amendment: ahub reset (issue #214)
1880
+
1881
+ `ahub reset [--all] [--yes]` returns a project's hub to a clean state. It is
1882
+ human-only (the CLI identity gate refuses it from an agent shell) and a dry run
1883
+ unless `--yes`: the dry run reads `hub.db` read-only (with `immutable=1` when
1884
+ `hub.db-wal` or `-shm` is absent, so it creates neither) and prints delivery ids,
1885
+ peer names and counts per category, never task or message text. With `--yes`
1886
+ it stops a running hub through `stopProject`, the path `ahub kill` takes, then
1887
+ acts on the stopped state directory.
1888
+
1889
+ - Refusals, each before anything changes: the machine's recovery lock is held
1890
+ (`recoveryLock()` read directly: a reset is never part of an operation, so
1891
+ `AGENTHUB_RECOVERY_OPERATION` does not let it through as it lets the
1892
+ operation's own processes through `assertLifecycleAvailable`); no
1893
+ registration matches this root and state directory, running or not (`ahub
1894
+ up` registers a stopped project; when this root is registered with another
1895
+ state directory the error names `ahub --project <id> reset` instead, since
1896
+ `ahub up` would register the default directory and orphan that state); the
1897
+ hub's ownership is not verified (any inspection state other than stopped, running
1898
+ or stopping); a running hub reports a recovery operation whose phase is not
1899
+ `released`; `--all` with a state directory other than the literal
1900
+ `<root>/.agenthub/state` (an override or symlink elsewhere would cross file
1901
+ systems or pull outside state into the tree); `--all` when `.agenthub` or
1902
+ the state directory is not a real directory of this user, when
1903
+ `.agenthub/archive` exists as anything else (a symlink could carry hub.db,
1904
+ PII included, out of the project), or when its `.gitignore` is not a regular
1905
+ file (a symlink would truncate the file it names). These are checked before
1906
+ the stop and again right before the rename (`archiveProblem`); a hard-linked
1907
+ `.gitignore` is replaced by a temp file and rename, never written through.
1908
+ Checked, then renamed by path: a symlink swapped in between still wins
1909
+ (marked `ponytail:`; `renameat` on a held descriptor would close it).
1910
+ - Between stop and act: after `stopProject` the CLI takes the project's
1911
+ registry claim (`Registry.claim` with its own pid and process signature), the claim a daemon must
1912
+ hold to run, and checks that no manifest names a daemon whose pid is alive
1913
+ or uncertain (`processLiveness`, the rule `inspectProject` uses, #226); either failing
1914
+ means a hub started in between and nothing is reset. A reset's claim carries
1915
+ the instance prefix `reset-`, so losing the claim to a concurrent reset says
1916
+ so. A crashed daemon's manifest (`status.json`, `control-token`, `hub.pid`
1917
+ with a dead pid, after SIGKILL or the forced shutdown exit; an empty or
1918
+ garbled `hub.pid` beside a dead `status.json` included) does not count: the runtime reset
1919
+ removes it and `--all` archives it. With the claim held, the CLI checks the
1920
+ machine's recovery lock again, so an upgrade or recovery that took it in
1921
+ between does not run beside the reset. The claim is released when the reset
1922
+ ends; a reset that dies leaves a dead or reused pid with its signature, which the next claim takes over.
1923
+ - Failures: the plan is read only for the dry run, so `--all --yes` archives a
1924
+ state directory it cannot read as it is (its dry run says what it could not
1925
+ read). A failure after the stop says how far it got. A runtime reset that
1926
+ cannot read the state (a corrupt `hub.db`, malformed JSON or valid JSON of
1927
+ the wrong shape, which `planReset` checks as the journal would, a journal the
1928
+ constructor refuses: root mismatch, corrupt metadata, too many rows) fails
1929
+ the same way on a rerun, so its error points to `--all`; any other runtime
1930
+ failure is finished by a rerun, every step being idempotent. `--all` prints
1931
+ the archive path as soon as the rename happened; a failure before it says
1932
+ the hub is stopped and nothing was moved. JSON and SQLite errors are printed
1933
+ by class and code only (`failureText`): a parser's message can quote the
1934
+ data it choked on, envelope bodies and task text included.
1935
+ - Runtime scope (default): the CLI opens the journal as a new instance, which
1936
+ turns the stopped run's dispatching and accepted rows into `needs_review`,
1937
+ and settles every `queued` and `needs_review` entry of `Bus.queueList`,
1938
+ including queue entries without a row, with `resolveDelivery(..., "discard",
1939
+ "reset")`: one `resolution_history` entry each. It clears manual holds
1940
+ through the bus, deletes the `budget_pauses` and `conductor_holds` rows and
1941
+ removes `sessions.json`, `claude-session.json` and `claude-context.json`,
1942
+ and a crashed run's manifest.
1943
+ The board, turns and touches, execution budgets, logs, `events.jsonl`,
1944
+ `cli-audit/`, recovery records, `pi-sessions/` and configuration stay.
1945
+ - Full scope (`--all`): the state directory is renamed, unchanged, to
1946
+ `<root>/.agenthub/archive/state-<YYYYMMDDTHHMMSSZ>/` (0700); an existing
1947
+ target refuses the move. The archive directory gets a `.gitignore` of `*`
1948
+ (checked on every `--all` and rewritten when it differs) because `hub.db`
1949
+ holds task text, PII included; turn snapshots exclude `**/.agenthub/archive/**`
1950
+ as they exclude the state directory, also when the user's index tracks it or
1951
+ the `.gitignore` is gone; and `.agenthub/archive` is a
1952
+ `DENY_SEGMENTS` entry (`src/local/deny.ts`), so the local worker's and Pi's
1953
+ path guard, seatbelt profile and memory capture refuse it like
1954
+ `.agenthub/state`. A new 0700 state directory (created with `recursive`, as
1955
+ the status line tee may have re-created it) holds only a copy of
1956
+ `project.json`; the project id is the hash of its root,
1957
+ so the registration and id are unchanged. Archives are never pruned.
1958
+ - Output says that claude-mem is not touched and that attached Claude Code
1959
+ sessions must be relaunched with `ahub claude`. The control protocol is
1960
+ unchanged: reset never talks to a running hub except to inspect and stop it.
1961
+
1962
+ ## Amendment: model-based PII screen (issue #198)
1963
+
1964
+ - `routing.toml` `[signals] pii_screen = "off" | "local"`, default `off`; any other
1965
+ value fails the load like a bad pattern. With `off`, or with `constraints.pii =
1966
+ "off"`, nothing changes and no screen call is made.
1967
+ - Order. With `local`, `Tasks.propose` screens the task's title, detail and plan
1968
+ text right after the pattern check and before triage. "Held as private" is met
1969
+ by holding the task back entirely: until the verdict it is not on the board, so
1970
+ no envelope, notice, triage call, brief or note of the hub's can read it. The
1971
+ local worker never captures hub tool calls (#230), so a proposal's arguments
1972
+ cannot bypass the verdict through capture. Writing it as
1973
+ a PII task first was rejected: a PII task opening lifts every silent turn-free
1974
+ cohort for good and switches facts off (#107, #108), so every proposal would do
1975
+ that. The proposal's tool call waits for the verdict, as it already waits for
1976
+ triage: for an ordinary task at worst the screen (8 s), triage (8 s) and a brief
1977
+ (2 x 2 s), 20 s; for a task the screen marked PII, triage first runs its own
1978
+ on-campus probe (two 4 s rounds when the gateway stalls), so about 24 s. Both
1979
+ stay under the plugin's 30 s control timeout. `hub_task_done` and `hub_review`
1980
+ put the screen (8 s) ahead of their existing waits, such as the briefs
1981
+ `releaseDependents` fetches for the tasks an approval releases.
1982
+ - The screener (`screenPii`, `src/hub/inference.ts`): the on-device model when
1983
+ `mlx.enabled` and it can be had (`ensureMlx`, loopback only; a legacy MLX runtime
1984
+ is started the way Pi's relay starts it and keeps running until `ahub models
1985
+ stop`), else the gateway's `local.fixed_model` only while the
1986
+ daemon's `onCampus()` holds, with `onCampusOnly` so the client refuses an Access
1987
+ host once more before transport; never the Switchyard sidecar. One call per item,
1988
+ an 8 s deadline over the whole call, and a generation slot of its own: it asks
1989
+ for one without waiting (`acquire(abort.signal, 0)`: one look, then
1990
+ `MlxBusyError`); with all taken (Pi generating, another screen) it uses the
1991
+ campus gateway when `onCampus()` holds and otherwise waits for the slot under the
1992
+ deadline. On campus the device gets 60% of the deadline: an error reply or a
1993
+ device still loading then hands over to the gateway within the rest; off campus
1994
+ it keeps the whole deadline. `max_tokens` 32, temperature 0 and `reasoning_effort: "none"` on both
1995
+ paths, so a reasoning model does not spend the budget thinking. The
1996
+ prompt frames the text as data and carries Korean and English examples and hard
1997
+ negatives. The answer must be exactly `clear` or `pii <category>` with a category
1998
+ from `name, student_id, phone, address, grade, health, other`.
1999
+ - Fail closed: an answer outside that form, a timeout, a failed or missing model,
2000
+ an off-campus gateway, or text over 6000 UTF-8 bytes (every token is at least a
2001
+ byte, so text and prompt fit the 8k on-device context; Ollama could otherwise
2002
+ judge cut input) is `unknown`. `pii` and `unknown` add the `pii` signal,
2003
+ so the task takes the whole existing PII path (local or nobody, user review,
2004
+ private envelopes, redacted views, no claude-mem). A pattern match is PII without
2005
+ a call. The source is recorded as a `screened` history entry (`pii: regex`,
2006
+ `pii: screen, <category>`, `pii: unknown, <reason>`, `clear`) and a `pii_screen`
2007
+ event; an unknown verdict is a console and hub.log line with the task id and the
2008
+ closed reason only. `route explain` on a draft does not call the screen and adds
2009
+ a trace line saying a proposal would. The cost of `unknown` is the cost of any PII
2010
+ task, even if a re-screen clears it later: it lifts every silent turn-free cohort
2011
+ for good and turns facts off while open, and off campus triage is not asked, so a
2012
+ classless proposal that is not a claim is refused with an error naming that.
2013
+ - Re-screen. Off campus with the device slot taken, the verdict is `unknown`. A
2014
+ task whose last `screened` entry is `pii: unknown` (not `too long`, which a
2015
+ second look cannot change), that is still `proposed` and that no peer ever owned
2016
+ (no history entry carries an owner) has had nothing private sent anywhere, so
2017
+ `Tasks.rescreen()`, on the daemon's 60 s release timer (one task per tick, the
2018
+ least tried first, at most 10 tries per task in a hub run, in memory), screens it
2019
+ again. `clear` lifts the `pii` signal through `Board.update` (a `screened` entry
2020
+ `clear, screened again`), notifies by task, and routes it through the class
2021
+ peers (a named owner or a claim is not remembered; a reserved owner, #207, is
2022
+ offered first as `assign()` always does); `pii` settles it; another `unknown`
2023
+ changes only the event record. A task an owner took (`local`) keeps its PII path,
2024
+ also after it was declined back to nobody: its review notes, summary and decline
2025
+ reason were never screened.
2026
+ - Free text (#69): the done summary, the review note with its unmet items, a
2027
+ budget hand-off (once per hand-off, however many tasks it moves) and a
2028
+ `hub_remember` note go through the same screen on an ordinary task when they
2029
+ match no pattern; a match is recorded as a `regex` verdict without a call. The
2030
+ call's cheap checks (owner, state, verdict, a running check, memory enabled) run
2031
+ first, then the screen, then the checks again on a fresh read. An escalation's
2032
+ reason is the hub's own text and stays pattern-only. `pii` or `unknown` withholds the
2033
+ item exactly as a pattern match does (stubs, no memory note, no summary line in
2034
+ completed-change or integration notices; `hub_remember` is refused). The board
2035
+ keeps the text and marks the history entry `withheld: true`, so public views,
2036
+ later reviewers, held cohort notices and `ahub ask` keep withholding it after a
2037
+ restart without another call. The check output tail of a done note, a plan given
2038
+ at accept, a decline reason, refs (paths, branch) and context checkpoint
2039
+ summaries stay pattern-only.
2040
+ - Calibration (AC4): `test/fixtures/pii-screen.json` holds synthetic Korean and
2041
+ English positives across the categories and hard negatives (roles,
2042
+ placeholders, field and function names, ports, codes); none is copied from the
2043
+ prompt, and its account, passport and resident numbers have impossible lengths
2044
+ or dates. `bun scripts/pii-screen-eval.ts [--project <dir>] [--out <file>]` screens
2045
+ each item once, in order, through `screenPii` with the project's configuration
2046
+ and reports recall, recall as handled (pii or unknown), precision, the unknown
2047
+ rate, held negatives, category agreement and latency. The bound, fixed here
2048
+ before any measurement: the model itself labels at least 90% of the positives
2049
+ `pii` (an unknown verdict counts as a miss). The report goes to
2050
+ `docs/verification/`; the script exits 1 under the bound.
2051
+
2052
+ ## Amendment: research records (issue #247)
2053
+
2054
+ - Opt-in per project (`research.enabled`, off by default). When a task is approved, the daemon builds its record from
2055
+ the project's `events.jsonl` with the same function `ahub research backfill` uses (`taskRecords` in
2056
+ `src/hub/research.ts`), so live and backfilled records agree, and appends it to the machine-wide store
2057
+ `~/.agenthub/research/<sha256(project id)[0:16]>.jsonl`. Records hold ids, states, counts, tokens and times only,
2058
+ because the events do; schema and fields: [research](../research.md).
2059
+ - The board has no terminal state but `approved`, and it is final, so each approved task gets one record; a person's
2060
+ later verdict (`ahub task label`, including `abandoned`) is a separate label record bound to the task's proposal
2061
+ time. A task is keyed by project, id and proposal time, because `ahub reset --all` restarts the ids and the store
2062
+ keeps the older records. Measures (success, first pass, rework, check failures over checks run, tokens and wall time
2063
+ per task) are derived when reported, never stored.
2064
+ - The record counts everything attributed to the task, the turn that approved it included: that turn ends after the
2065
+ approval and its usage arrives later, so the writer waits until no open turn is attributed to the task (polled every
2066
+ 10 s, at most 30 minutes; a stopping hub writes once its peers have stopped). The first record written for a
2067
+ task is final; a label binds to the current board's task, else to the store's only record with that id, or with
2068
+ `--created` to a named one from before a reset. Task events carry the board's move `reason` (a closed
2069
+ list) so reassignments are counted by reason.
2070
+ - Recording never blocks or fails the task flow: the writer runs after the board event, and a failure is one
2071
+ hub.log line per run. ponytail: it reads the whole events file for each record; an incremental per-task index
2072
+ if that file grows large.
2073
+ - Agents may read the measures and export; backfill and labels are a person's (identity gate).
2074
+
2075
+ ## Amendment: proven Pi tool refusals and idle exit recovery (issues #254, #255)
2076
+
2077
+ - A trusted guard uses `PreEffectToolRefusal` only when that operation has performed no task or board effect; PII screening and its telemetry record may precede the refusal. Pi receives the same `error: <message>` refusal as the control client, and its session/call receipt is `done`. Initial task ownership/dependency/state checks and conductor entry peer/mode/assignment/escalation validation opt into the type; shared helpers default to ordinary errors, and conductor checks after an awaited release validation stay ordinary. Generic errors, board readback failures after a write, and receipt settlement write failures remain uncertain/pending and are never repeated. A legacy pending row has no pre-effect proof in its stored fingerprint/state/result, so startup preserves it; neither present board state nor a matching error message establishes historical refusal.
2078
+ - Each Pi owner reports one exit with cause, exit code or signal, whether startup completed, whether a turn/tool was active, whether teardown was expected, and the last validated tool name only. TUI shutdown/owner-loss and verified stop teardown lack observed OS status and record unknown. A successfully stopped claimed TUI owner reports once; a never-owned launch reports no invented exit. Internal teardown notices direct inspection and `ahub pi`, without claiming a person requested them. Console notices name the next action or automatic-start state; headless code 0 is still an exit and settles active deliveries as uncertain.
2079
+ - With `pi.auto_start`, an unexpected idle headless exit attempts its verified persisted session once in a 60 s window. The daemon owns the attempt/window across adapter replacements, blocks throughout an in-flight attempt, and rearms the 60 s window when that attempt settles. It checks current-owner identity, completed startup, turn/tool inactivity, start serialization, shutdown and recovery before and after awaited receipt drain/session capture. Missing or invalid persisted state leaves Pi offline with `ahub pi` guidance, with no fresh-session fallback. A second exit inside the window stays offline. Requested stops, stale owners, startup failures, active turns/tools and native terminal exits never trigger this automatic path; crash recovery's existing #66 behavior remains separate.
2080
+ ## Amendment: benchmarks (issue #251)
2081
+
2082
+ - `ahub bench` runs a person's suite file (tasks with a starting commit, an optional setup and a verify command)
2083
+ against the peers attached to a hub, in a project opted in with `bench.enabled`, because every attempt resets the work
2084
+ tree (`git checkout --force --detach <commit>` with HEAD checked against the pin, `git clean -ffdx -e /.agenthub`, then a
2085
+ clean-tree check). Before
2086
+ anything changes the runner requires the project to be the root of its repository, nothing tracked under `.agenthub/`
2087
+ (in any letter case) in the tree or in any suite ref, every ref to be a commit, which it pins for the run, and a
2088
+ clean tree. The runner is a console client: it proposes each
2089
+ task as the console does and polls the board, so the task flow is unchanged and no new control request exists.
2090
+ - `setup` and `verify` run in their own process group, bounded; the group is stopped at the bound, on an interrupt and
2091
+ when the command exits. An approved attempt's measures come from #247's `taskRecords` for the task proposed last with
2092
+ that id, read once the turns started since its proposal have ended (waiting only while their peers are busy or
2093
+ paused, at most 10 minutes, outside the attempt's duration); a timeout, an error, an interrupted wait, a wait that
2094
+ runs out with the turn still open, or a hub status that cannot be read while it is open leaves none, never partial
2095
+ ones. The fingerprint lists only the peers attached at the start.
2096
+ - Results go to `~/.agenthub/bench/<run>.jsonl` (ids, outcomes, exit codes, counts, times; never suite text or command
2097
+ output). A run's state comes from its end record or its runner's liveness (`processLiveness`). Comparisons are
2098
+ derived when asked: an arm is every run of it that is over (a stopped run included), runs of different suites or
2099
+ suite versions are not mixed unless asked, and each difference has a seeded 95% bootstrap interval, inconclusive
2100
+ below five attempts counted for that measure per arm (errors are not counted for pass rate or wall time).
2101
+ - A task that times out stops the run, because an agent may still work in the tree the next attempt would reset.
2102
+ ponytail: a board state to withdraw an open task would let the run continue. The fingerprint holds what the hub's
2103
+ status reports (a requested model for Pi, permission modes once #242 lands); ponytail: read each peer's launch
2104
+ settings for the rest.
2105
+ - The dashboard snapshot carries run summaries and per-arm measures only. `bench run` is a person's (identity gate,
2106
+ wherever `--json` stands); reading results is not. Schema and commands: [bench](../bench.md).