@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.
- package/CHANGELOG.md +38 -0
- package/README.md +7 -6
- package/docs/agent-notes/adapters.md +10 -2
- package/docs/agent-notes/bus.md +6 -2
- package/docs/agent-notes/daemon.md +10 -3
- package/docs/agent-notes/local-worker.md +3 -1
- package/docs/agent-notes/models.md +3 -0
- package/docs/agent-notes/tasks.md +6 -3
- package/docs/agent-notes/tests.md +3 -2
- package/docs/bench.md +143 -0
- package/docs/events.md +6 -2
- package/docs/operations.md +655 -34
- package/docs/quickstart.md +12 -10
- package/docs/research.md +92 -0
- package/docs/security.md +59 -17
- package/docs/smoke.md +137 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +586 -16
- package/docs/specs/2026-09-20-pi-local-models-design.md +2 -0
- package/docs/specs/2026-09-20-upgrade-recovery-design.md +345 -0
- package/docs/specs/2026-10-04-switchyard-source-port-design.md +10 -0
- package/docs/verified.json +29 -18
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +65 -26
- package/src/adapters/acp.ts +67 -6
- package/src/adapters/claude-channel.ts +61 -20
- package/src/adapters/codex-appserver.ts +70 -2
- package/src/adapters/local-worker.ts +8 -3
- package/src/adapters/pi.ts +58 -4
- package/src/cli/bench.ts +287 -0
- package/src/cli/console-state.ts +531 -110
- package/src/cli/console.ts +59 -26
- package/src/cli/facts-hook.ts +50 -8
- package/src/cli/help.ts +130 -0
- package/src/cli/identity.ts +11 -5
- package/src/cli/launch.ts +98 -17
- package/src/cli/main.ts +325 -88
- package/src/cli/preview.ts +14 -3
- package/src/cli/reset.ts +194 -0
- package/src/cli/status-lines.ts +10 -5
- package/src/cli/terminal-recovery.ts +196 -40
- package/src/cli/upgrade-runtime.ts +327 -60
- package/src/cli/upgrade.ts +442 -45
- package/src/hub/ask.ts +1 -1
- package/src/hub/bench.ts +211 -0
- package/src/hub/board.ts +15 -7
- package/src/hub/bus.ts +79 -8
- package/src/hub/child-process.ts +1 -0
- package/src/hub/conductor.ts +54 -21
- package/src/hub/config-trust.ts +1 -0
- package/src/hub/control-client.ts +9 -4
- package/src/hub/daemon.ts +401 -48
- package/src/hub/delivery-journal.ts +6 -6
- package/src/hub/events.ts +9 -2
- package/src/hub/execution-budget.ts +15 -6
- package/src/hub/hub-tools.ts +4 -4
- package/src/hub/inference.ts +136 -1
- package/src/hub/lifecycle.ts +13 -12
- package/src/hub/manager.ts +16 -14
- package/src/hub/peers.ts +2 -0
- package/src/hub/permission-mode.ts +44 -0
- package/src/hub/recovery-store.ts +68 -8
- package/src/hub/registry.ts +36 -22
- package/src/hub/report.ts +43 -1
- package/src/hub/research.ts +255 -0
- package/src/hub/restart.ts +25 -0
- package/src/hub/routing.ts +48 -16
- package/src/hub/snapshots.ts +3 -2
- package/src/hub/tasks.ts +295 -115
- package/src/hub/tool-refusal.ts +7 -0
- package/src/hub/ui.ts +4 -1
- package/src/local/deny.ts +2 -1
- package/src/local/tools.ts +14 -10
- package/src/models/mlx.ts +11 -8
- package/src/models/ollama.ts +2 -2
- package/src/models/relay.ts +190 -42
- package/src/models/route/relay-selector.ts +43 -31
- package/src/models/route/runtime.ts +17 -7
- package/src/models/route/signals.ts +12 -1
- package/src/models/route/stage.ts +57 -3
- package/src/omniroute/client.ts +1 -1
- package/src/pi/process-signature.ts +23 -0
- package/src/pi/tool-receipts.ts +7 -1
- package/src/ui/index.html +84 -6
- package/src/ui/task-progress.ts +33 -0
- package/templates/AGENTS.block.md +1 -1
- 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
|
|
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.
|
|
101
|
-
|
|
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
|
|
283
|
-
reviewer is the console user; `local` answers such a turn
|
|
284
|
-
of its history, refuses it when the only gateway
|
|
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
|
|
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: `
|
|
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
|
|
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
|
|
990
|
-
|
|
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.
|
|
1369
|
-
|
|
1370
|
-
|
|
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.
|
|
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).
|