@staix/agent-hub 0.12.16 → 0.12.17
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 +8 -0
- package/README.md +4 -4
- package/docs/agent-notes/adapters.md +1 -0
- package/docs/agent-notes/bus.md +2 -0
- package/docs/agent-notes/tasks.md +3 -1
- package/docs/agent-notes/tests.md +1 -0
- package/docs/operations.md +106 -9
- package/docs/quickstart.md +3 -3
- package/docs/security.md +24 -0
- package/docs/smoke.md +93 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +120 -1
- package/docs/verification/2026-10-09-agent-shell-t0.md +123 -0
- package/docs/verified.json +26 -17
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +17 -5
- package/src/adapters/acp.ts +2 -2
- package/src/adapters/claude-channel.ts +3 -2
- package/src/adapters/codex-appserver.ts +3 -2
- package/src/adapters/local-worker.ts +5 -5
- package/src/cli/console-state.ts +213 -0
- package/src/cli/console.ts +201 -0
- package/src/cli/facts-hook.ts +8 -1
- package/src/cli/identity-audit.ts +66 -0
- package/src/cli/identity.ts +54 -0
- package/src/cli/launch.ts +15 -2
- package/src/cli/main.ts +62 -29
- package/src/cli/tail-render.ts +17 -0
- package/src/cli/upgrade-runtime.ts +1 -1
- package/src/hub/board.ts +6 -2
- package/src/hub/bus.ts +82 -10
- package/src/hub/child-process.ts +11 -0
- package/src/hub/conductor.ts +196 -0
- package/src/hub/control-client.ts +3 -3
- package/src/hub/daemon.ts +367 -46
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +6 -1
- package/src/hub/hub-tools.ts +13 -0
- package/src/hub/report.ts +53 -4
- package/src/hub/supervision.ts +152 -0
- package/src/hub/tasks.ts +30 -15
- package/src/hub/usage.ts +37 -2
- package/src/pi/launch.ts +2 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Agent shell identity T0, issues #193 and #194
|
|
2
|
+
|
|
3
|
+
## Evidence collected
|
|
4
|
+
|
|
5
|
+
- Installed CLI reports `codex-cli 0.146.0` (`codex --version`, 2026-10-09). `codex app-server --help` supports configuration overrides and WebSocket listeners; its example names `shell_environment_policy.inherit=all`.
|
|
6
|
+
- The [version-pinned native environment implementation](https://github.com/openai/codex/blob/rust-v0.146.0/codex-rs/protocol/src/shell_environment.rs) applies inheritance, default exclusions, custom exclusions, explicit values, and `include_only` in that order. It then injects `CODEX_THREAD_ID`. `AGENTHUB_PEER_ID` is an ordinary inherited variable and can be removed by filtering. The native thread marker is injected after filtering, which supplies a fallback without widening the user's environment policy.
|
|
7
|
+
- The [version-pinned core wrapper](https://github.com/openai/codex/blob/rust-v0.146.0/codex-rs/core/src/exec_env.rs) explicitly documents thread-marker injection even with `include_only`.
|
|
8
|
+
- The [current configuration reference](https://learn.chatgpt.com/docs/config-file/config-reference) describes inheritance and filtering, and now describes a `filters` map alongside legacy `exclude` and `include_only`. Current documentation is not evidence that every new option exists in installed 0.146.0.
|
|
9
|
+
- The hub's Codex adapter passes a child environment even without optional launch values. It now sets its own marker and removes stale vendor markers inherited from the caller. Recovery authority remains scrubbed by `childEnv`.
|
|
10
|
+
|
|
11
|
+
## Native Codex observations (2026-10-09)
|
|
12
|
+
|
|
13
|
+
- Real installed `codex exec --json --model gpt-5.5 --sandbox workspace-write`
|
|
14
|
+
completed a single fixed Python shell probe in an isolated git fixture.
|
|
15
|
+
With explicit `inherit=all`, the hub marker, native thread marker and harmless
|
|
16
|
+
canary were present; a Claude marker was absent. The disposable loopback
|
|
17
|
+
responder was reachable by the harness, while the native shell request failed
|
|
18
|
+
with `URLError`, errno 1. No sandbox or network exception was requested.
|
|
19
|
+
- A second real run used `inherit=none`, an explicit PATH and
|
|
20
|
+
`include_only=["PATH"]`. The hub marker, native marker and canary still appeared
|
|
21
|
+
in the observed shell. This does not establish that filtering removed them;
|
|
22
|
+
the difference from the pinned environment construction is unresolved in this
|
|
23
|
+
installed host environment. The fallback is supported by source and marker
|
|
24
|
+
presence, without widening the caller's configured policy.
|
|
25
|
+
- A real native MCP call under `workspace-write` completed `agent-hub.hub_status`
|
|
26
|
+
against the candidate bundle and an isolated conductor daemon. Native JSONL
|
|
27
|
+
recorded the MCP call completing, and the daemon independently recorded a
|
|
28
|
+
codex `conduct/status` event. Only this exact hub tool was approved through the
|
|
29
|
+
existing per-tool `approval_mode=approve` configuration; its daemon role check
|
|
30
|
+
still applied. This proves MCP access separately from the blocked shell path.
|
|
31
|
+
- The first run with the user's configured `gpt-6.1-sol` failed before tools:
|
|
32
|
+
that slug was rejected by the installed ChatGPT-account endpoint. The probe's
|
|
33
|
+
explicit model selection did not change the user's configuration.
|
|
34
|
+
|
|
35
|
+
## Native Claude observations (2026-10-09)
|
|
36
|
+
|
|
37
|
+
- The final observer proof used source `20ad5e6`. Its read-only feed-off
|
|
38
|
+
continuation verified one completed native turn, 263,413 cached-inclusive
|
|
39
|
+
tokens, a matching opaque daemon Stop receipt, native idle and no pending
|
|
40
|
+
deliveries. The fresh feed-own run verified two actual owner completions and
|
|
41
|
+
independent file reviews, nine native finals matched by nine daemon Stops,
|
|
42
|
+
27 unique usage records and 1,888,242 tokens. Eight completed turns contained
|
|
43
|
+
supervision, totalling 1,292,922 tokens. The receipt matched the final native
|
|
44
|
+
message, session, launcher and instance; earlier incomplete captures below
|
|
45
|
+
were not retroactively marked complete.
|
|
46
|
+
|
|
47
|
+
- In the original captures, Claude Code 2.1.295, Opus 5.5, ran the candidate MCP tools in real PTYs
|
|
48
|
+
after its five-hour quota reset. Both feed-off and feed-own fixtures started
|
|
49
|
+
local and headless Pi, proposed two tasks, reassigned Beta to Pi, held and
|
|
50
|
+
released both peers, and independently read the exact ALPHA/BETA files before
|
|
51
|
+
approving their tasks. The console independently audited four allow-once
|
|
52
|
+
answers in each fixture, covering the two writes and bounded file checks.
|
|
53
|
+
- The real feed-off TUI displayed inbound review requests; the feed-own TUI
|
|
54
|
+
displayed actual supervision and review pushes. MCP success and channel
|
|
55
|
+
delivery were observed separately. A transient startup warning about the
|
|
56
|
+
server name did not prevent the later observed channel deliveries.
|
|
57
|
+
- In the feed-own native TUI, the operator submitted the fixed Python probe
|
|
58
|
+
through `!`. Its actual command output was `AGENTHUB_PEER_ID=true`,
|
|
59
|
+
`CLAUDECODE=true`, `CLAUDE_CODE_SESSION_ID=true`, `CODEX_THREAD_ID=false`.
|
|
60
|
+
This is shell output captured before Claude's interpretation of it.
|
|
61
|
+
- A subsequent feed-off run pinned to `da2ebbc` executed the same fixed probe
|
|
62
|
+
through the model's actual native Bash tool and separately through `!`.
|
|
63
|
+
Both actual command outputs showed the same three Claude/hub markers present
|
|
64
|
+
and `CODEX_THREAD_ID` absent. This closes the separate model-shell observation;
|
|
65
|
+
neither command printed environment values.
|
|
66
|
+
- The original harness accepted hub idle too early and stopped both last
|
|
67
|
+
review turns before a native final answer. Native transcripts independently
|
|
68
|
+
show two completed turns in feed-off and five in feed-own, with the last
|
|
69
|
+
assistant message still `tool_use` in each case. The board approvals are real;
|
|
70
|
+
complete final-turn verification and production supervision accounting were
|
|
71
|
+
incomplete for those captures. The harness now requires the current daemon instance, its session,
|
|
72
|
+
the fixture-specific transcript, a final `end_turn` and `turn_duration` after
|
|
73
|
+
the last actual review. Missing measurements remain unknown.
|
|
74
|
+
- The `da2ebbc` run verified the final native message UUID, message id,
|
|
75
|
+
`end_turn`, subsequent `turn_duration`, daemon instance, session and launcher.
|
|
76
|
+
Its complete transcript contains five completed turns and 21 unique usage
|
|
77
|
+
records, totalling 1,451,782 tokens including cached input. The daemon certified
|
|
78
|
+
only one native Stop, however. Its log refused the other completions as an
|
|
79
|
+
unavailable completed transcript message or completion predating the current
|
|
80
|
+
turn; the final refusal occurred before cleanup. A normal review receipt
|
|
81
|
+
remained queued. Actual task/file/final-answer evidence is valid, while full
|
|
82
|
+
runtime accounting and delivery settlement remain incomplete.
|
|
83
|
+
- The `6034dd9` read-only continuation preserved both approved tasks and exact
|
|
84
|
+
files. It completed one native turn with four unique usage records and 261,483
|
|
85
|
+
cached-inclusive tokens, but the matching daemon Stop remained absent. After
|
|
86
|
+
a bounded retry, the Stop handler logged unavailable completion at
|
|
87
|
+
01:39:04.087 UTC; the native stop summary/duration appeared at 01:39:04.095 UTC.
|
|
88
|
+
With no overlapping operator prompt, this strongly supports a transcript
|
|
89
|
+
visibility barrier while the native hook waits. The strict harness timed out
|
|
90
|
+
incomplete and stopped its owned processes. The final message's thinking/text
|
|
91
|
+
rows carried identical usage counters in these observed transcripts; this
|
|
92
|
+
observation does not replace the final text/duration requirement.
|
|
93
|
+
|
|
94
|
+
## Plain Claude and Kimi observations (2026-10-09)
|
|
95
|
+
|
|
96
|
+
- Plain Claude 2.1.295, launched with the candidate MCP configuration but no
|
|
97
|
+
development-channel flag, completed one native `hub_status` call with an
|
|
98
|
+
independent daemon Claude status audit. Its only native tool calls were
|
|
99
|
+
ToolSearch and that status call. A unique operator push was bridge-accepted,
|
|
100
|
+
but no native pushed user row or response appeared in the observed
|
|
101
|
+
20.075-second window. This bounded negative observation is not a universal
|
|
102
|
+
claim about channel support. Actual channel-enabled review/supervision pushes
|
|
103
|
+
were observed separately in the conductor cases. The plain probe and cleanup
|
|
104
|
+
took 56.724 seconds and made no model file, shell, task or settings changes.
|
|
105
|
+
- Real Kimi Code CLI 2.1.1 answered ACP `initialize` and created native session
|
|
106
|
+
`session_17eeb9dc-38d3-4685-b1aa-3db6a89b465c`. Its single prompt failed with
|
|
107
|
+
protocol error `-32000`, HTTP 403, for the managed account's weekly usage
|
|
108
|
+
limit before any requested tool event. No task-list/status result or ordinary
|
|
109
|
+
peer's conductor refusal was obtained. The owning CLI's safe provider list
|
|
110
|
+
showed only `managed:kimi-code`, type `kimi`, four models, OAuth, default
|
|
111
|
+
`kimi-code/k3`. No distinct configured provider was found. The reset time
|
|
112
|
+
remains unknown; no purchase, credential/configuration change or model retry
|
|
113
|
+
was attempted. Owned native processes and the fixture daemon were stopped.
|
|
114
|
+
|
|
115
|
+
## Release disposition and remaining bounded live probe
|
|
116
|
+
|
|
117
|
+
- On 2026-10-09 the user explicitly approved release 0.12.17 with only the Kimi native new-tool and ordinary-role refusal T0 evidence deferred. Kimi coverage remains unverified. This decision defers no other source/native gate and authorizes no purchase, credentials, provider/configuration change, account retry or authority relaxation.
|
|
118
|
+
- Kimi ACP MCP: after native provider access is available under an authorized account, request the same read-only task list/status and ordinary-role refusal probes in isolated sessions. Verify actual native tool events/results and the daemon reply before marking this prerequisite verified. The existing quota failure proves neither new-tool access nor refusal behavior.
|
|
119
|
+
|
|
120
|
+
## Verification limits
|
|
121
|
+
|
|
122
|
+
- Codex shell and MCP probes used the root agent's serial resource slot. Claude `!`, model shell, enabled-channel pushes and the bounded plain-session comparison were observed in subsequent serial native cases. Kimi new-tool access remains unverified because its real account rejected the prompt before tools.
|
|
123
|
+
- New unit and fake-adapter tests are prepared but have not been run by this worker. The root agent owns the final immutable-head checks.
|
package/docs/verified.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"documents": {
|
|
4
4
|
"README.md": {
|
|
5
|
-
"verifiedAgainst": "
|
|
5
|
+
"verifiedAgainst": "c3d1e22f9b200cbe3f484c9e73d30bad046c9221",
|
|
6
6
|
"paths": [
|
|
7
7
|
"package.json",
|
|
8
8
|
"src/",
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
]
|
|
12
12
|
},
|
|
13
13
|
"docs/security.md": {
|
|
14
|
-
"verifiedAgainst": "
|
|
14
|
+
"verifiedAgainst": "c3d1e22f9b200cbe3f484c9e73d30bad046c9221",
|
|
15
15
|
"paths": [
|
|
16
16
|
"src/",
|
|
17
17
|
"templates/"
|
|
18
18
|
]
|
|
19
19
|
},
|
|
20
20
|
"docs/operations.md": {
|
|
21
|
-
"verifiedAgainst": "
|
|
21
|
+
"verifiedAgainst": "c3d1e22f9b200cbe3f484c9e73d30bad046c9221",
|
|
22
22
|
"paths": [
|
|
23
23
|
"package.json",
|
|
24
24
|
"src/",
|
|
@@ -30,11 +30,12 @@
|
|
|
30
30
|
".github/workflows/check.yml",
|
|
31
31
|
"scripts/ci-gates.mjs",
|
|
32
32
|
"scripts/ci-reuse.mjs",
|
|
33
|
-
".github/workflows/release.yml"
|
|
33
|
+
".github/workflows/release.yml",
|
|
34
|
+
"scripts/smoke-conductor.ts"
|
|
34
35
|
]
|
|
35
36
|
},
|
|
36
37
|
"docs/quickstart.md": {
|
|
37
|
-
"verifiedAgainst": "
|
|
38
|
+
"verifiedAgainst": "c3d1e22f9b200cbe3f484c9e73d30bad046c9221",
|
|
38
39
|
"paths": [
|
|
39
40
|
"package.json",
|
|
40
41
|
"src/",
|
|
@@ -42,7 +43,7 @@
|
|
|
42
43
|
]
|
|
43
44
|
},
|
|
44
45
|
"docs/agent-notes/adapters.md": {
|
|
45
|
-
"verifiedAgainst": "
|
|
46
|
+
"verifiedAgainst": "20ad5e68fd1264fa43dd7426f60cb0c8211d6555",
|
|
46
47
|
"paths": [
|
|
47
48
|
"src/adapters/",
|
|
48
49
|
"src/pi/",
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
]
|
|
56
57
|
},
|
|
57
58
|
"docs/agent-notes/benchmarks.md": {
|
|
58
|
-
"verifiedAgainst": "
|
|
59
|
+
"verifiedAgainst": "8b7d7254f1bb5602ec5d658e7814e236f8fb43e7",
|
|
59
60
|
"paths": [
|
|
60
61
|
"scripts/benchmarks/",
|
|
61
62
|
"test/benchmarks/",
|
|
@@ -67,7 +68,7 @@
|
|
|
67
68
|
]
|
|
68
69
|
},
|
|
69
70
|
"docs/agent-notes/budget.md": {
|
|
70
|
-
"verifiedAgainst": "
|
|
71
|
+
"verifiedAgainst": "20ad5e68fd1264fa43dd7426f60cb0c8211d6555",
|
|
71
72
|
"paths": [
|
|
72
73
|
"src/hub/budget.ts",
|
|
73
74
|
"src/cli/statusline-tee.ts",
|
|
@@ -79,18 +80,19 @@
|
|
|
79
80
|
]
|
|
80
81
|
},
|
|
81
82
|
"docs/agent-notes/bus.md": {
|
|
82
|
-
"verifiedAgainst": "
|
|
83
|
+
"verifiedAgainst": "20ad5e68fd1264fa43dd7426f60cb0c8211d6555",
|
|
83
84
|
"paths": [
|
|
84
85
|
"src/hub/bus.ts",
|
|
85
86
|
"src/hub/envelope.ts",
|
|
86
87
|
"src/hub/limits.ts",
|
|
87
88
|
"src/hub/delivery-journal.ts",
|
|
88
89
|
"src/hub/inference.ts",
|
|
89
|
-
"src/hub/daemon.ts"
|
|
90
|
+
"src/hub/daemon.ts",
|
|
91
|
+
"src/hub/supervision.ts"
|
|
90
92
|
]
|
|
91
93
|
},
|
|
92
94
|
"docs/agent-notes/daemon.md": {
|
|
93
|
-
"verifiedAgainst": "
|
|
95
|
+
"verifiedAgainst": "20ad5e68fd1264fa43dd7426f60cb0c8211d6555",
|
|
94
96
|
"paths": [
|
|
95
97
|
"src/hub/daemon.ts",
|
|
96
98
|
"src/hub/control-client.ts",
|
|
@@ -107,11 +109,16 @@
|
|
|
107
109
|
"src/cli/recovery-package.ts",
|
|
108
110
|
"src/cli/facts-hook.ts",
|
|
109
111
|
"src/pi/process-signature.ts",
|
|
110
|
-
"src/hub/context-window.ts"
|
|
112
|
+
"src/hub/context-window.ts",
|
|
113
|
+
"src/hub/conductor.ts",
|
|
114
|
+
"src/cli/console.ts",
|
|
115
|
+
"src/cli/console-state.ts",
|
|
116
|
+
"src/cli/identity.ts",
|
|
117
|
+
"src/cli/identity-audit.ts"
|
|
111
118
|
]
|
|
112
119
|
},
|
|
113
120
|
"docs/agent-notes/local-worker.md": {
|
|
114
|
-
"verifiedAgainst": "
|
|
121
|
+
"verifiedAgainst": "8b7d7254f1bb5602ec5d658e7814e236f8fb43e7",
|
|
115
122
|
"paths": [
|
|
116
123
|
"src/adapters/local-worker.ts",
|
|
117
124
|
"src/local/",
|
|
@@ -120,7 +127,7 @@
|
|
|
120
127
|
]
|
|
121
128
|
},
|
|
122
129
|
"docs/agent-notes/models.md": {
|
|
123
|
-
"verifiedAgainst": "
|
|
130
|
+
"verifiedAgainst": "8b7d7254f1bb5602ec5d658e7814e236f8fb43e7",
|
|
124
131
|
"paths": [
|
|
125
132
|
"src/models/",
|
|
126
133
|
"src/hub/inference.ts",
|
|
@@ -129,7 +136,7 @@
|
|
|
129
136
|
]
|
|
130
137
|
},
|
|
131
138
|
"docs/agent-notes/tasks.md": {
|
|
132
|
-
"verifiedAgainst": "
|
|
139
|
+
"verifiedAgainst": "c3d1e22f9b200cbe3f484c9e73d30bad046c9221",
|
|
133
140
|
"paths": [
|
|
134
141
|
"src/hub/tasks.ts",
|
|
135
142
|
"src/hub/board.ts",
|
|
@@ -138,11 +145,13 @@
|
|
|
138
145
|
"src/hub/task-sweep.ts",
|
|
139
146
|
"src/cli/launch.ts",
|
|
140
147
|
"src/cli/main.ts",
|
|
141
|
-
"src/cli/preview.ts"
|
|
148
|
+
"src/cli/preview.ts",
|
|
149
|
+
"src/hub/conductor.ts",
|
|
150
|
+
"src/hub/supervision.ts"
|
|
142
151
|
]
|
|
143
152
|
},
|
|
144
153
|
"docs/agent-notes/tests.md": {
|
|
145
|
-
"verifiedAgainst": "
|
|
154
|
+
"verifiedAgainst": "2a274289e280252b264716da3a1112f67b0c278e",
|
|
146
155
|
"paths": [
|
|
147
156
|
"test/",
|
|
148
157
|
"scripts/check.sh",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-hub",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.17",
|
|
4
4
|
"description": "Channel between Claude Code and the agent-hub daemon: peer messages from Codex, Kimi and the local worker arrive as channel events; hub_send replies.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Young Joon Lee",
|
|
@@ -15291,8 +15291,8 @@ function projectContext(cwd, env = process.env) {
|
|
|
15291
15291
|
function stateDirFor(cwd) {
|
|
15292
15292
|
return projectContext(cwd).stateDir;
|
|
15293
15293
|
}
|
|
15294
|
-
var PROTOCOL =
|
|
15295
|
-
var RECOVERY_SOURCE_PROTOCOLS = [9, 10, 11, 12, 13, PROTOCOL];
|
|
15294
|
+
var PROTOCOL = 15;
|
|
15295
|
+
var RECOVERY_SOURCE_PROTOCOLS = [9, 10, 11, 12, 13, 14, PROTOCOL];
|
|
15296
15296
|
function readControl(stateDir) {
|
|
15297
15297
|
try {
|
|
15298
15298
|
const status = JSON.parse(readFileSync2(join2(stateDir, "status.json"), "utf8"));
|
|
@@ -15403,7 +15403,7 @@ class ControlClient {
|
|
|
15403
15403
|
// package.json
|
|
15404
15404
|
var package_default = {
|
|
15405
15405
|
name: "@staix/agent-hub",
|
|
15406
|
-
version: "0.12.
|
|
15406
|
+
version: "0.12.17",
|
|
15407
15407
|
description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
|
|
15408
15408
|
license: "MIT",
|
|
15409
15409
|
type: "module",
|
|
@@ -15503,7 +15503,18 @@ var TASK_TOOLS = [
|
|
|
15503
15503
|
tool("hub_remember", "Save a decision, finding, contract or fail to the memory all agents share (claude-mem); the other agents also get it with their next message. A fail is an approach you tried that does not work, and why: the most useful note, it stops the others spending their quota on it. Do not retry what a fail note rules out without new evidence. Conclusions worth recalling, not chatter.", { text: str, title: str, kind: { type: "string", enum: [...NOTE_KINDS] }, task: id }, ["text"])
|
|
15504
15504
|
];
|
|
15505
15505
|
var TASK_TOOL_NAMES = new Set(TASK_TOOLS.map((t) => t.name));
|
|
15506
|
+
var CONDUCTOR_TOOLS = [
|
|
15507
|
+
tool("hub_status", "Inspect public team state, holds, quota windows, task counts and pending approval peer/tool/age. Only the configured conductor may use this tool.", {}),
|
|
15508
|
+
tool("hub_task_show", "Read a task's public view and history. PII tasks remain stubs. Only the conductor may use this tool.", { id }, ["id"]),
|
|
15509
|
+
tool("hub_task_assign", "Move a task to another peer, as the conductor. Requires assign capability when the conductor has an explicit capabilities list.", { id, peer: str }, ["id", "peer"]),
|
|
15510
|
+
tool("hub_task_escalate", "Escalate a task through the normal task flow, as the conductor. Requires assign capability when explicitly listed.", { id }, ["id"]),
|
|
15511
|
+
tool("hub_peer_start", "Start local, kimi or headless pi. Claude, Codex and Pi TUI requests return a command for the person to run, without launching a terminal.", { peer: { type: "string", enum: ["local", "kimi", "pi", "claude", "codex"] }, mode: { type: "string", enum: ["headless", "tui"] } }, ["peer"]),
|
|
15512
|
+
tool("hub_peer_hold", "Hold a peer's deliveries as the conductor. This hold is separate from the person's hold and budget pauses.", { peer: str }, ["peer"]),
|
|
15513
|
+
tool("hub_peer_release", "Release only the conductor hold you placed. Never lifts a person's hold or a budget pause.", { peer: str }, ["peer"])
|
|
15514
|
+
];
|
|
15515
|
+
var CONDUCTOR_TOOL_NAMES = new Set(CONDUCTOR_TOOLS.map((t) => t.name));
|
|
15506
15516
|
var ROLE_TEXT = {
|
|
15517
|
+
conductor: "conductor: plan and split work into tasks with owners, watch the team with hub_status, move stalled work, and ensure every task is reviewed (review it yourself only if you also hold reviewer). Report results and open decisions to the person. Do not implement tasks you handed out. Never ask a peer to answer an approval; ask the person for human-only actions. You may release only holds you placed, never human holds or budget pauses.",
|
|
15507
15518
|
planner: "planner: break work into tasks with hub_task_propose (one outcome each, the right class, paths in refs, and after: [ids] for work that must wait for other tasks) instead of doing everything yourself.",
|
|
15508
15519
|
implementer: "implementer: accept tasks assigned to you, do them, and finish with hub_task_done (summary: what changed, why, and the check you ran with its result; refs). Decline what you cannot do. Before starting work nobody assigned you, claim it with hub_task_propose naming yourself as owner, with the paths in refs. With a claim or an accept, give a plan: the files, symbols and signatures you will change and where new code goes.",
|
|
15509
15520
|
verifier: "verifier: run the checks a task names and report what passed and what did not in hub_task_done.",
|
|
@@ -15690,7 +15701,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
15690
15701
|
inputSchema: { type: "object", properties: { delivery_id: { type: "string" }, delivery_generation: { type: "string" } }, required: ["delivery_id", "delivery_generation"], additionalProperties: false }
|
|
15691
15702
|
}
|
|
15692
15703
|
],
|
|
15693
|
-
...TASK_TOOLS
|
|
15704
|
+
...TASK_TOOLS,
|
|
15705
|
+
...CONDUCTOR_TOOLS
|
|
15694
15706
|
]
|
|
15695
15707
|
}));
|
|
15696
15708
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
@@ -15720,7 +15732,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
15720
15732
|
const sent = `sent to: ${res.targets.join(", ") || "(no other peers attached)"}`;
|
|
15721
15733
|
return text(typeof res.notice === "string" ? `${sent}; ${res.notice}` : sent);
|
|
15722
15734
|
}
|
|
15723
|
-
if (TASK_TOOL_NAMES.has(name)) {
|
|
15735
|
+
if (TASK_TOOL_NAMES.has(name) || CONDUCTOR_TOOL_NAMES.has(name)) {
|
|
15724
15736
|
if (!hub)
|
|
15725
15737
|
return text(offline());
|
|
15726
15738
|
const res = await hub.request({ t: "task", op: name, args: args ?? {} });
|
package/src/adapters/acp.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
import { renderDigest, replyAudience, replyParent, type Envelope, type PeerId } from "../hub/envelope.ts";
|
|
4
4
|
import { BasePeer } from "../hub/peers.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { peerChildEnv, stopOwnedProcess, trackGroup } from "../hub/child-process.ts";
|
|
6
6
|
|
|
7
7
|
export interface PermissionOption {
|
|
8
8
|
optionId: string;
|
|
@@ -135,7 +135,7 @@ export class AcpPeer extends BasePeer {
|
|
|
135
135
|
async start(): Promise<void> {
|
|
136
136
|
const [bin, ...args] = this.opts.cmd;
|
|
137
137
|
// Its own process group, stopped as a whole (#115, as Codex's in #113): an agent CLI may be a launcher with a native child.
|
|
138
|
-
const proc = spawn(bin!, args, { cwd: this.opts.cwd, env:
|
|
138
|
+
const proc = spawn(bin!, args, { cwd: this.opts.cwd, env: peerChildEnv(this.id, { ...process.env, ...(this.opts.env ?? {}) }), stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
139
139
|
this.proc = proc;
|
|
140
140
|
trackGroup(proc);
|
|
141
141
|
proc.on("error", (e) => this.down(`spawn failed: ${e.message}`));
|
|
@@ -8,7 +8,7 @@ import { readFileSync } from "node:fs";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { ControlClient, stateDirFor } from "../hub/control-client.ts";
|
|
10
10
|
import { VERSION } from "../version.ts";
|
|
11
|
-
import { DEFAULT_ROLES, roleContract, TASK_TOOL_NAMES, TASK_TOOLS } from "../hub/hub-tools.ts";
|
|
11
|
+
import { CONDUCTOR_TOOLS, CONDUCTOR_TOOL_NAMES, DEFAULT_ROLES, roleContract, TASK_TOOL_NAMES, TASK_TOOLS } from "../hub/hub-tools.ts";
|
|
12
12
|
import { frame, replyParent, sanitize, HUB_MESSAGE_INSTRUCTION, type Envelope } from "../hub/envelope.ts";
|
|
13
13
|
|
|
14
14
|
// A native session is pinned at launch. Unlike a new CLI invocation after `cd`,
|
|
@@ -201,6 +201,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
201
201
|
},
|
|
202
202
|
]),
|
|
203
203
|
...TASK_TOOLS,
|
|
204
|
+
...CONDUCTOR_TOOLS,
|
|
204
205
|
],
|
|
205
206
|
}));
|
|
206
207
|
|
|
@@ -225,7 +226,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
225
226
|
const sent = `sent to: ${res.targets.join(", ") || "(no other peers attached)"}`;
|
|
226
227
|
return text(typeof res.notice === "string" ? `${sent}; ${res.notice}` : sent);
|
|
227
228
|
}
|
|
228
|
-
if (TASK_TOOL_NAMES.has(name)) {
|
|
229
|
+
if (TASK_TOOL_NAMES.has(name) || CONDUCTOR_TOOL_NAMES.has(name)) {
|
|
229
230
|
if (!hub) return text(offline());
|
|
230
231
|
const res = await hub.request({ t: "task", op: name, args: args ?? {} });
|
|
231
232
|
return text(res.ok ? res.text : `error: ${res.error}`);
|
|
@@ -3,7 +3,7 @@ import type { Server, ServerWebSocket } from "bun";
|
|
|
3
3
|
import { renderDigest, replyAudience, replyParent, type Envelope, type PeerId } from "../hub/envelope.ts";
|
|
4
4
|
import { BasePeer } from "../hub/peers.ts";
|
|
5
5
|
import { codexContext, type ContextReading } from "../hub/context-window.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { peerChildEnv, stopOwnedProcess, trackGroup } from "../hub/child-process.ts";
|
|
7
7
|
|
|
8
8
|
export interface CodexOptions {
|
|
9
9
|
/** Port the TUI attaches to: `codex --enable tui_app_server --remote ws://127.0.0.1:<proxyPort>`. */
|
|
@@ -305,9 +305,10 @@ export class CodexPeer extends BasePeer {
|
|
|
305
305
|
throw new Error(`port ${port} already answers /healthz: an app-server the hub does not own is running (orphan from a crashed hub?)`);
|
|
306
306
|
}
|
|
307
307
|
let gone = "";
|
|
308
|
+
const env = peerChildEnv("codex", { ...process.env, ...(this.opts.env ?? {}) });
|
|
308
309
|
this.proc = spawn(this.opts.bin ?? "codex", ["app-server", "--listen", `ws://127.0.0.1:${port}`, ...(this.opts.extraArgs ?? [])], {
|
|
309
310
|
cwd: this.opts.cwd,
|
|
310
|
-
env
|
|
311
|
+
env,
|
|
311
312
|
stdio: ["ignore", "ignore", "pipe"],
|
|
312
313
|
detached: true, // its own process group, stopped as a whole (#113): `codex` is a launcher with a native child
|
|
313
314
|
});
|
|
@@ -4,7 +4,7 @@ import type { RouteLabelEvent, RouteTurnOutcome } from "../models/route/labels.t
|
|
|
4
4
|
import type { ToolObservation } from "../models/route/signals.ts";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { renderDigest, replyAudience, replyParent, STANDING_INSTRUCTION, USER, type Envelope, type EnvelopeOpts, type PeerId } from "../hub/envelope.ts";
|
|
7
|
-
import { TASK_TOOL_NAMES, TASK_TOOLS } from "../hub/hub-tools.ts";
|
|
7
|
+
import { CONDUCTOR_TOOL_NAMES, CONDUCTOR_TOOLS, TASK_TOOL_NAMES, TASK_TOOLS } from "../hub/hub-tools.ts";
|
|
8
8
|
import { BasePeer } from "../hub/peers.ts";
|
|
9
9
|
import { profile, proxyEnv, type SandboxNetwork } from "../local/sandbox.ts";
|
|
10
10
|
import { runTool, toolResultFailed, TOOL_SCHEMAS, touchedPaths, type ToolContext } from "../local/tools.ts";
|
|
@@ -98,7 +98,7 @@ export class LocalPeer extends BasePeer {
|
|
|
98
98
|
await this.requireBudget(envs, "model_calls");
|
|
99
99
|
if (generation !== this.turn) throw new Error("route belongs to an ended turn");
|
|
100
100
|
if (signal.aborted) throw new Error("turn cancelled before model request");
|
|
101
|
-
const tools = judge ? undefined : [...TOOL_SCHEMAS, ...(this.opts.taskTool ? TASK_TOOLS.map(asFunction) : [])];
|
|
101
|
+
const tools = judge ? undefined : [...TOOL_SCHEMAS, ...(this.opts.taskTool ? [...TASK_TOOLS, ...CONDUCTOR_TOOLS].map(asFunction) : [])];
|
|
102
102
|
const result = await this.opts.omni.chat({ model, messages, ...(tools ? { tools } : {}), ...(judge ? { max_tokens: maxTokens ?? 2048 } : {}) }, { signal, ...(pii ? { onCampusOnly: true } : {}) });
|
|
103
103
|
this.recordUsage(result, model);
|
|
104
104
|
if (generation !== this.turn) throw new Error("route belongs to an ended turn");
|
|
@@ -250,7 +250,7 @@ export class LocalPeer extends BasePeer {
|
|
|
250
250
|
await this.requireBudget(envs, "tool_calls");
|
|
251
251
|
if (turnSignal.aborted) throw new ExecutionBudgetStop(this.budgetStopReason || "turn cancelled before tool execution");
|
|
252
252
|
} catch (error) { clearInterval(alive); throw error; }
|
|
253
|
-
const running = TASK_TOOL_NAMES.has(name) && this.opts.taskTool ? this.opts.taskTool(name, safeParse(call.function.arguments), { pii: !!policy?.pii }).catch((e: Error) => `error: ${e.message}`) : runTool(name, call.function.arguments, ctx);
|
|
253
|
+
const running = (TASK_TOOL_NAMES.has(name) || CONDUCTOR_TOOL_NAMES.has(name)) && this.opts.taskTool ? this.opts.taskTool(name, safeParse(call.function.arguments), { pii: !!policy?.pii }).catch((e: Error) => `error: ${e.message}`) : runTool(name, call.function.arguments, ctx);
|
|
254
254
|
const output = await running.finally(() => clearInterval(alive));
|
|
255
255
|
if (turn !== this.turn) return "";
|
|
256
256
|
this.touch();
|
|
@@ -281,7 +281,7 @@ export class LocalPeer extends BasePeer {
|
|
|
281
281
|
});
|
|
282
282
|
},
|
|
283
283
|
sandboxProfile: this.sandboxProfile,
|
|
284
|
-
sandboxEnv: proxyEnv(this.opts.tools.bashNetwork ?? false),
|
|
284
|
+
sandboxEnv: { ...proxyEnv(this.opts.tools.bashNetwork ?? false), AGENTHUB_PEER_ID: this.id },
|
|
285
285
|
signal: turnSignal,
|
|
286
286
|
send: (text, to) => {
|
|
287
287
|
const refused = this.onMessage?.(text, pii ? reply : { inReplyTo: replyParent(envs), to: to?.length ? to : replyAudience(envs) });
|
|
@@ -334,7 +334,7 @@ export class LocalPeer extends BasePeer {
|
|
|
334
334
|
// A task turn asks for its class's route; a route needs the sidecar, which exists only when the worker was started with one.
|
|
335
335
|
const route = policy?.route ?? this.opts.route;
|
|
336
336
|
const fixedModel = policy?.fixedModel ?? this.opts.fixedModel;
|
|
337
|
-
const tools = [...TOOL_SCHEMAS, ...(this.opts.taskTool ? TASK_TOOLS.map(asFunction) : [])];
|
|
337
|
+
const tools = [...TOOL_SCHEMAS, ...(this.opts.taskTool ? [...TASK_TOOLS, ...CONDUCTOR_TOOLS].map(asFunction) : [])];
|
|
338
338
|
const signal = this.abort!.signal;
|
|
339
339
|
const messages: ChatMessage[] = [{ role: "system", content: system(this.opts.cwd, this.opts.preamble) }, ...this.history, ...turnMsgs];
|
|
340
340
|
const routed = await this.hubCall(route, messages, policy, signal);
|