@team-agent/installer 0.5.106 → 0.5.108
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/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +1 -1
- package/crates/team-agent/src/cli/emit.rs +3 -3
- package/crates/team-agent/src/cli/mod.rs +87 -4
- package/crates/team-agent/src/cli/models.rs +252 -3
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/status_port/brief.rs +155 -37
- package/crates/team-agent/src/cli/tests/base.rs +112 -225
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +184 -0
- package/crates/team-agent/src/cli/tests/status_brief.rs +624 -0
- package/crates/team-agent/src/compiler/tests.rs +46 -59
- package/crates/team-agent/src/leader/tests/byte_findings.rs +1 -14
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +57 -173
- package/crates/team-agent/src/lifecycle/launch/pi_mcp_extension.js +226 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +0 -37
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +0 -9
- package/crates/team-agent/src/lifecycle/tests/fixtures/pi_native_mcp_host.mjs +71 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -66
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +0 -392
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +0 -5
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -361
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +1 -16
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +20 -112
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1 -12
- package/crates/team-agent/src/lifecycle/tests/pi_native_mcp_capability_red.rs +546 -0
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +11 -54
- package/crates/team-agent/src/messaging/tests/wave2.rs +2 -302
- package/crates/team-agent/src/model/name_similarity.rs +52 -67
- package/crates/team-agent/src/model/task_graph.rs +37 -124
- package/crates/team-agent/src/model/yaml/tests.rs +46 -165
- package/crates/team-agent/src/provider/model_catalog.rs +166 -3
- package/crates/team-agent/src/provider/testdata/cursor-list-models-native.stdout +250 -0
- package/crates/team-agent/src/provider/testdata/grok-models-authenticated.stdout +9 -0
- package/crates/team-agent/src/provider/testdata/grok-models-unauthenticated.stdout +7 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +0 -20
- package/crates/team-agent/src/provider/tests/classify.rs +106 -198
- package/crates/team-agent/src/tmux_backend/tests.rs +178 -501
- package/crates/team-agent/src/transport/tests/wire.rs +0 -55
- package/package.json +4 -4
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// The Rust materializer prepends teamMcpDefinition and teamMcpReceiptRoot as JSON.
|
|
2
|
+
// No package discovery, private extension events, settings writes or factory calls.
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { chmodSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { isAbsolute, join } from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
export default function teamAgentMcp(pi) {
|
|
9
|
+
// Keep full UUID entropy and all three native tool names below Pi's 64-character limit.
|
|
10
|
+
const alias = `team_${randomUUID().replaceAll("-", "")}`;
|
|
11
|
+
const wanted = teamMcpDefinition.includeTools;
|
|
12
|
+
const receipt = join(teamMcpReceiptRoot, `${alias}.json`);
|
|
13
|
+
let backend, surface, registration, timer, failure, bindingReady = false, stopped = false, starting = false;
|
|
14
|
+
let wireSuffix;
|
|
15
|
+
let nativeOwned = false;
|
|
16
|
+
const returned = new Set();
|
|
17
|
+
let frame = {
|
|
18
|
+
schema_version: "team-agent.pi-mcp.v1", alias,
|
|
19
|
+
api_shape: Object.fromEntries(["registerMcpServer", "unregisterMcpServer", "getCommands", "getAllTools", "getActiveTools"]
|
|
20
|
+
.map((name) => [name, typeof pi[name]])),
|
|
21
|
+
candidate: teamMcpDefinition.command, tools: wanted,
|
|
22
|
+
workspace: teamMcpDefinition.env.TEAM_AGENT_WORKSPACE,
|
|
23
|
+
agent_id: teamMcpDefinition.env.TEAM_AGENT_ID,
|
|
24
|
+
team_id: teamMcpDefinition.env.TEAM_AGENT_OWNER_TEAM_ID,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function publish(stage, extra = {}) {
|
|
28
|
+
frame = { ...frame, stage, backend, surface, ...extra };
|
|
29
|
+
const temp = `${receipt}.tmp`;
|
|
30
|
+
let written = false;
|
|
31
|
+
try {
|
|
32
|
+
writeFileSync(temp, JSON.stringify(frame) + "\n", { flag: "wx", mode: 0o600 });
|
|
33
|
+
written = true;
|
|
34
|
+
chmodSync(temp, 0o600);
|
|
35
|
+
renameSync(temp, receipt);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (written) {
|
|
38
|
+
try { unlinkSync(temp); } catch { /* only the exact temporary file we created */ }
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fail(ctx, stage, error) {
|
|
45
|
+
clearInterval(timer);
|
|
46
|
+
// Third-party error text can contain credentials: retain correlation, not its content.
|
|
47
|
+
const errorSha256 = createHash("sha256").update(String(error)).digest("hex");
|
|
48
|
+
failure = new Error(`Pi Team MCP unavailable at ${stage}; receipt ${receipt}`);
|
|
49
|
+
try { publish("Failed", { failed_stage: stage, error_sha256: errorSha256 }); }
|
|
50
|
+
catch { failure = new Error("Pi Team MCP failed and its owned receipt could not be written"); }
|
|
51
|
+
ctx.abort();
|
|
52
|
+
ctx.shutdown();
|
|
53
|
+
return failure;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function proxyAvailable() {
|
|
57
|
+
const tool = pi.getAllTools().find((tool) => tool.name === "mcp");
|
|
58
|
+
const properties = tool?.parameters?.properties;
|
|
59
|
+
return properties?.tool?.type === "string" && properties?.args
|
|
60
|
+
&& pi.getActiveTools().includes("mcp");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function directBinding() {
|
|
64
|
+
const namespace = `mcp__${alias}`;
|
|
65
|
+
const tools = pi.getAllTools().filter((tool) => tool.namespace?.name === namespace && tool.exposure !== "hidden");
|
|
66
|
+
const active = new Set(pi.getActiveTools());
|
|
67
|
+
const names = wanted.map((name) => `mcp__${alias}__${name}`);
|
|
68
|
+
if (tools.length !== wanted.length) return undefined;
|
|
69
|
+
if (!names.every((name) => active.has(name) && tools.some((tool) => tool.name === name && tool.exposure === "direct"))) return undefined;
|
|
70
|
+
return names;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function publicBridge(commands) {
|
|
74
|
+
const ownPath = realpathSync(fileURLToPath(import.meta.url));
|
|
75
|
+
const entries = new Set();
|
|
76
|
+
for (const command of commands) {
|
|
77
|
+
if (command.source !== "extension") continue;
|
|
78
|
+
const entry = command.sourceInfo?.path ?? command.path;
|
|
79
|
+
if (!entry || !isAbsolute(entry)) continue;
|
|
80
|
+
const canonical = realpathSync(entry);
|
|
81
|
+
if (canonical === ownPath) continue;
|
|
82
|
+
if (!statSync(canonical).isFile()) throw new Error("Loaded extension entry is not a regular file");
|
|
83
|
+
entries.add(canonical);
|
|
84
|
+
}
|
|
85
|
+
const bridges = [];
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
// Import only the public exports of already-loaded entries, never their default factory.
|
|
88
|
+
const module = await import(pathToFileURL(entry).href);
|
|
89
|
+
if (typeof module.registerMcpServer === "function") bridges.push({ entry, register: module.registerMcpServer });
|
|
90
|
+
}
|
|
91
|
+
if (bridges.length > 1) throw new Error("Ambiguous loaded MCP registration capabilities");
|
|
92
|
+
return bridges[0];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
96
|
+
if (starting || backend || failure || stopped) return;
|
|
97
|
+
starting = true;
|
|
98
|
+
try {
|
|
99
|
+
publish("ExecutableCatalogReady");
|
|
100
|
+
const native = typeof pi.registerMcpServer === "function" && typeof pi.unregisterMcpServer === "function";
|
|
101
|
+
const commands = typeof pi.getCommands === "function" ? pi.getCommands() : [];
|
|
102
|
+
const builtin = commands.some((command) => command.source === "extension" && command.sourceInfo?.path === "builtin:mcp");
|
|
103
|
+
const deadline = performance.now() + 30_000;
|
|
104
|
+
let loadingTimeout;
|
|
105
|
+
let bridge;
|
|
106
|
+
try {
|
|
107
|
+
if (!(native && builtin)) {
|
|
108
|
+
bridge = await Promise.race([
|
|
109
|
+
publicBridge(commands),
|
|
110
|
+
new Promise((_resolve, reject) => { loadingTimeout = setTimeout(() => reject(new Error("Public MCP capability loading timed out")), 30_000); }),
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
} finally { clearTimeout(loadingTimeout); }
|
|
114
|
+
if (stopped || failure) return;
|
|
115
|
+
if (bridge) {
|
|
116
|
+
backend = "public-bridge";
|
|
117
|
+
surface = "proxy";
|
|
118
|
+
registration = bridge.register({ pi, name: alias, definition: teamMcpDefinition });
|
|
119
|
+
if (!registration || typeof registration.dispose !== "function") throw new Error("Public MCP registration has no owned dispose handle");
|
|
120
|
+
publish("RegistrationSelected", { bridge_entry: bridge.entry });
|
|
121
|
+
} else if (native) {
|
|
122
|
+
backend = "native-api";
|
|
123
|
+
const { command, args, env, cwd } = teamMcpDefinition;
|
|
124
|
+
pi.registerMcpServer(alias, {
|
|
125
|
+
command, args, env, ...(cwd ? { cwd } : {}),
|
|
126
|
+
exposure: "hidden", toolExposure: Object.fromEntries(wanted.map((name) => [name, "direct"])),
|
|
127
|
+
});
|
|
128
|
+
nativeOwned = true;
|
|
129
|
+
publish("RegistrationSelected");
|
|
130
|
+
} else {
|
|
131
|
+
throw new Error("No native MCP API or loaded public MCP registration bridge");
|
|
132
|
+
}
|
|
133
|
+
// Never await connection here: later session_start handlers may own the consumer.
|
|
134
|
+
timer = setInterval(() => {
|
|
135
|
+
if (stopped || failure) return;
|
|
136
|
+
try {
|
|
137
|
+
if (performance.now() > deadline) {
|
|
138
|
+
fail(ctx, "tools-ready-timeout", new Error("MCP consumer did not expose the registered tools"));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const names = backend === "native-api" ? directBinding() : undefined;
|
|
142
|
+
if (names) {
|
|
143
|
+
surface = "direct";
|
|
144
|
+
bindingReady = true;
|
|
145
|
+
clearInterval(timer);
|
|
146
|
+
publish("ToolsAvailable", { actual_tool_names: names, evidence: "pi-runtime-tool-registry" });
|
|
147
|
+
} else if (!builtin && proxyAvailable()) {
|
|
148
|
+
surface = "proxy";
|
|
149
|
+
bindingReady = true;
|
|
150
|
+
clearInterval(timer);
|
|
151
|
+
publish("RegistrationSelected", { evidence: "proxy-present; owned MCP calls still pending" });
|
|
152
|
+
}
|
|
153
|
+
} catch (error) { fail(ctx, "tools-ready", error); }
|
|
154
|
+
}, 25);
|
|
155
|
+
} catch (error) { throw fail(ctx, "registration", error); }
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
async function waitForBinding() {
|
|
159
|
+
while (!bindingReady && !failure && !stopped) await new Promise((resolve) => setTimeout(resolve, 25));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// before_agent_start exceptions are diagnostic in Pi; input must be handled to stop a new run.
|
|
163
|
+
pi.on("input", async (_event, ctx) => {
|
|
164
|
+
await waitForBinding();
|
|
165
|
+
if (failure || stopped) {
|
|
166
|
+
ctx.shutdown();
|
|
167
|
+
return { action: "handled" };
|
|
168
|
+
}
|
|
169
|
+
return { action: "continue" };
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
173
|
+
await waitForBinding();
|
|
174
|
+
if (failure) throw failure;
|
|
175
|
+
if (stopped || !backend) throw fail(ctx, "wire-binding", new Error("No owned MCP registration"));
|
|
176
|
+
if (surface === "direct" && !directBinding()) throw fail(ctx, "wire-binding", new Error("Owned MCP tools became unreachable"));
|
|
177
|
+
if (surface === "proxy" && !proxyAvailable()) throw fail(ctx, "wire-binding", new Error("MCP proxy became unreachable"));
|
|
178
|
+
const examples = wanted.map((tool) => {
|
|
179
|
+
const args = tool === "send_message" ? { to: "leader", content: "..." }
|
|
180
|
+
: tool === "report_result" ? { summary: "..." } : {};
|
|
181
|
+
return surface === "direct"
|
|
182
|
+
? `- ${tool}: mcp__${alias}__${tool}; arguments ${JSON.stringify(args)}`
|
|
183
|
+
: `- ${tool}: mcp(${JSON.stringify({ tool: `${alias}_${tool}`, args })})`;
|
|
184
|
+
});
|
|
185
|
+
const base = wireSuffix && event.systemPrompt.endsWith(wireSuffix)
|
|
186
|
+
? event.systemPrompt.slice(0, -wireSuffix.length) : event.systemPrompt;
|
|
187
|
+
wireSuffix = "\n\n# Team Agent MCP wire binding\n"
|
|
188
|
+
+ "Use these actual tools for the logical Team Agent operations above. Do not pass sender, task_id or schema_version.\n"
|
|
189
|
+
+ examples.join("\n");
|
|
190
|
+
return { systemPrompt: base + wireSuffix };
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
pi.on("before_provider_request", (_event, ctx) => {
|
|
194
|
+
if (failure || stopped || !bindingReady) ctx.abort();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
pi.on("tool_result", (event, ctx) => {
|
|
198
|
+
if (stopped || failure) return;
|
|
199
|
+
const name = event.toolName === "mcp" ? event.input?.tool : event.toolName;
|
|
200
|
+
const tool = wanted.find((tool) => name === (surface === "direct" ? `mcp__${alias}__${tool}` : `${alias}_${tool}`));
|
|
201
|
+
if (!tool) return;
|
|
202
|
+
if (event.isError === false) returned.add(tool);
|
|
203
|
+
try {
|
|
204
|
+
publish(returned.size === wanted.length ? "ToolsVerified" : frame.stage, {
|
|
205
|
+
returned_tools: [...returned], last_call_is_error: event.isError ?? null,
|
|
206
|
+
evidence: "owned tool-result events; business success requires the original results",
|
|
207
|
+
});
|
|
208
|
+
} catch (error) { throw fail(ctx, "tool-receipt", error); }
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
212
|
+
if (stopped) return;
|
|
213
|
+
stopped = true;
|
|
214
|
+
clearInterval(timer);
|
|
215
|
+
const current = registration;
|
|
216
|
+
registration = undefined;
|
|
217
|
+
try {
|
|
218
|
+
if (nativeOwned) {
|
|
219
|
+
nativeOwned = false;
|
|
220
|
+
pi.unregisterMcpServer(alias);
|
|
221
|
+
}
|
|
222
|
+
await current?.dispose();
|
|
223
|
+
publish(failure ? "Failed" : frame.stage, { shutdown: true });
|
|
224
|
+
} catch (error) { throw fail(ctx, "shutdown", error); }
|
|
225
|
+
});
|
|
226
|
+
}
|
|
@@ -2788,43 +2788,6 @@ mod fresh_quick_start_leader_binding_tests {
|
|
|
2788
2788
|
let _ = std::fs::remove_dir_all(&workspace);
|
|
2789
2789
|
}
|
|
2790
2790
|
|
|
2791
|
-
#[test]
|
|
2792
|
-
fn attach_registry_or_readback_failure_rolls_back_exact_state_bytes() {
|
|
2793
|
-
assert_failure_rolls_back_this_attempt(
|
|
2794
|
-
"attach-failure",
|
|
2795
|
-
MockOps {
|
|
2796
|
-
attach_ok: false,
|
|
2797
|
-
..MockOps::default()
|
|
2798
|
-
},
|
|
2799
|
-
1,
|
|
2800
|
-
0,
|
|
2801
|
-
0,
|
|
2802
|
-
false,
|
|
2803
|
-
);
|
|
2804
|
-
assert_failure_rolls_back_this_attempt(
|
|
2805
|
-
"registry-failure",
|
|
2806
|
-
MockOps {
|
|
2807
|
-
register_ok: false,
|
|
2808
|
-
..MockOps::default()
|
|
2809
|
-
},
|
|
2810
|
-
1,
|
|
2811
|
-
1,
|
|
2812
|
-
0,
|
|
2813
|
-
true,
|
|
2814
|
-
);
|
|
2815
|
-
assert_failure_rolls_back_this_attempt(
|
|
2816
|
-
"readback-failure",
|
|
2817
|
-
MockOps {
|
|
2818
|
-
readback_ok: false,
|
|
2819
|
-
..MockOps::default()
|
|
2820
|
-
},
|
|
2821
|
-
1,
|
|
2822
|
-
1,
|
|
2823
|
-
1,
|
|
2824
|
-
true,
|
|
2825
|
-
);
|
|
2826
|
-
}
|
|
2827
|
-
|
|
2828
2791
|
#[test]
|
|
2829
2792
|
fn attach_failure_rolls_back_exact_state_bytes() {
|
|
2830
2793
|
assert_failure_rolls_back_this_attempt(
|
|
@@ -365,15 +365,6 @@ fn add_agent_joins_w2_into_running_roster_and_existing_session() {
|
|
|
365
365
|
);
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
// REAL-MACHINE e2e boundary (rt-host-a verifies): after `add-agent w2`, the coordinator delivers
|
|
369
|
-
// `send w2` and w2 reports a result (full round-trip). #[ignore] — needs a live tmux session + worker.
|
|
370
|
-
#[test]
|
|
371
|
-
#[ignore = "real-machine: add-agent then send w2 round-trips"]
|
|
372
|
-
fn add_agent_then_send_w2_round_trips() {
|
|
373
|
-
// The framework asserts: add-agent w2 -> w2 in the live session + roster -> `send w2 <msg>` ->
|
|
374
|
-
// coordinator delivers -> w2 emits a result_envelope (results row). Not unit-testable in-process.
|
|
375
|
-
}
|
|
376
|
-
|
|
377
368
|
#[derive(Clone)]
|
|
378
369
|
struct SocketRecordingRunner {
|
|
379
370
|
recorded: std::sync::Arc<std::sync::Mutex<Vec<Vec<String>>>>,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
|
|
4
|
+
const [wrapperPath, receiptPath, cwd] = process.argv.slice(2);
|
|
5
|
+
if (!wrapperPath || !receiptPath || !cwd) throw new Error("expected wrapper, receipt, and cwd");
|
|
6
|
+
|
|
7
|
+
const registrations = [];
|
|
8
|
+
const unregistrations = [];
|
|
9
|
+
const handlers = new Map();
|
|
10
|
+
const extensionCommands = [{
|
|
11
|
+
name: "mcp",
|
|
12
|
+
source: "extension",
|
|
13
|
+
sourceInfo: { path: "builtin:mcp", source: "builtin", scope: "user", origin: "top-level" },
|
|
14
|
+
}];
|
|
15
|
+
const pi = {
|
|
16
|
+
on(event, handler) {
|
|
17
|
+
const list = handlers.get(event) ?? [];
|
|
18
|
+
list.push(handler);
|
|
19
|
+
handlers.set(event, list);
|
|
20
|
+
},
|
|
21
|
+
getCommands() {
|
|
22
|
+
return extensionCommands;
|
|
23
|
+
},
|
|
24
|
+
getMcpServers() {
|
|
25
|
+
return registrations.map(({ name, config }) => ({ name, config, extensionPath: "<native-n1-fixture>" }));
|
|
26
|
+
},
|
|
27
|
+
getAllTools() {
|
|
28
|
+
return registrations.flatMap(({ name, config }) => Object.entries(config.toolExposure ?? {})
|
|
29
|
+
.map(([tool, exposure]) => ({ name: `mcp__${name}__${tool}`, namespace: { name: `mcp__${name}` }, exposure })));
|
|
30
|
+
},
|
|
31
|
+
getActiveTools() {
|
|
32
|
+
return this.getAllTools().filter((tool) => tool.exposure !== "hidden").map((tool) => tool.name);
|
|
33
|
+
},
|
|
34
|
+
registerMcpServer(name, config) {
|
|
35
|
+
registrations.push({ name, config });
|
|
36
|
+
},
|
|
37
|
+
unregisterMcpServer(name) {
|
|
38
|
+
unregistrations.push(name);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const extension = await import(pathToFileURL(wrapperPath).href);
|
|
43
|
+
if (typeof extension.default !== "function") throw new Error("generated wrapper has no default extension factory");
|
|
44
|
+
await extension.default(pi);
|
|
45
|
+
const context = {
|
|
46
|
+
cwd,
|
|
47
|
+
sessionManager: {
|
|
48
|
+
getSessionFile: () => undefined,
|
|
49
|
+
getLeafId: () => undefined,
|
|
50
|
+
getEntries: () => [],
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
for (const handler of handlers.get("session_start") ?? []) {
|
|
54
|
+
await handler({ type: "session_start" }, context);
|
|
55
|
+
}
|
|
56
|
+
if (registrations.length !== 1) throw new Error(`expected one native MCP registration, saw ${registrations.length}`);
|
|
57
|
+
const expectedTools = Object.keys(registrations[0].config.toolExposure ?? {})
|
|
58
|
+
.map((tool) => `mcp__${registrations[0].name}__${tool}`);
|
|
59
|
+
const deadline = Date.now() + 3_000;
|
|
60
|
+
while (!expectedTools.every((name) => pi.getActiveTools().includes(name))) {
|
|
61
|
+
if (Date.now() >= deadline) throw new Error(`native MCP tools never became active: ${pi.getActiveTools()}`);
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
63
|
+
}
|
|
64
|
+
const beforeAgentStart = handlers.get("before_agent_start")?.[0];
|
|
65
|
+
if (typeof beforeAgentStart !== "function") throw new Error("generated wrapper omitted before_agent_start wire binding");
|
|
66
|
+
const binding = await beforeAgentStart({ systemPrompt: "N1 fixture" }, context);
|
|
67
|
+
const wirePrompt = binding?.systemPrompt;
|
|
68
|
+
if (typeof wirePrompt !== "string" || !expectedTools.every((name) => wirePrompt.includes(name))) {
|
|
69
|
+
throw new Error(`native Team tool wire binding is incomplete: ${wirePrompt}`);
|
|
70
|
+
}
|
|
71
|
+
await writeFile(receiptPath, `${JSON.stringify({ registrations, unregistrations, activeTools: pi.getActiveTools(), expectedTools, wirePrompt }, null, 2)}\n`, "utf8");
|
|
@@ -1321,33 +1321,6 @@ fn lanea_stop_window_present_kills_and_stopped_true() {
|
|
|
1321
1321
|
);
|
|
1322
1322
|
}
|
|
1323
1323
|
|
|
1324
|
-
// ── STOP #2 (stop-display-noop-2) [RED] — close ghostty_workspace slot: persist display.status/pane_title
|
|
1325
|
-
// Golden operations.py:88-92 -> display/close.py:84-85 relabels the slot: display["status"]="stopped",
|
|
1326
|
-
// display["pane_title"]=f"stopped: {agent_id}", written back into the persisted agent entry. Rust never
|
|
1327
|
-
// touches the display (mark_agent_stopped leaves it as-is) and hardcodes display_closed:false. RED: the
|
|
1328
|
-
// persisted display.status/pane_title are the in-process observable.
|
|
1329
|
-
#[test]
|
|
1330
|
-
#[ignore = "legacy external display contract removed; runtime is silent tmux only"]
|
|
1331
|
-
fn lanea_stop_ghostty_workspace_relabels_slot_to_stopped() {
|
|
1332
|
-
let ws = lanea_ws_agents(json!({
|
|
1333
|
-
"alpha": { "status": "running", "provider": "codex", "window": "alpha",
|
|
1334
|
-
"display": { "backend": "ghostty_workspace", "pane_id": "%5", "linked_session": "disp-alpha", "status": "running", "pane_title": "alpha" } }
|
|
1335
|
-
}));
|
|
1336
|
-
let tx = LaneTransport::new("team-laneateam", &["alpha"]);
|
|
1337
|
-
let _ = stop_agent_with_transport(&ws, &aid("alpha"), None, &tx).expect("stop ok");
|
|
1338
|
-
let state = crate::state::persist::load_runtime_state(&ws).expect("load state");
|
|
1339
|
-
assert_eq!(
|
|
1340
|
-
state.pointer("/agents/alpha/display/status").and_then(serde_json::Value::as_str),
|
|
1341
|
-
Some("stopped"),
|
|
1342
|
-
"stop must relabel a ghostty_workspace slot: display.status='stopped' (close.py:84); Rust leaves it 'running'"
|
|
1343
|
-
);
|
|
1344
|
-
assert_eq!(
|
|
1345
|
-
state.pointer("/agents/alpha/display/pane_title").and_then(serde_json::Value::as_str),
|
|
1346
|
-
Some("stopped: alpha"),
|
|
1347
|
-
"stop must set display.pane_title='stopped: <id>' (close.py:85); Rust never touches the display"
|
|
1348
|
-
);
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
1324
|
// ── RESET #3 (reset-paused-restart-2) [RED] — reset of a PAUSED agent returns ok=true (NOT an Err) ───
|
|
1352
1325
|
// Golden operations.py:126-140: after discard, reset re-spawns via start_agent(force,allow_fresh).
|
|
1353
1326
|
// discard does NOT clear `paused`, so start_agent returns the refusal-shaped {ok:False,status:paused,
|
|
@@ -1851,42 +1824,3 @@ fn fresh_start_persists_boundary_from_before_transport_spawn() {
|
|
|
1851
1824
|
spawned_at={spawned_at}, transport_observed_at={transport_observed_at}"
|
|
1852
1825
|
);
|
|
1853
1826
|
}
|
|
1854
|
-
|
|
1855
|
-
// ── REMOVE #6/#12 (remove-rollback-no-agent-health-3 / remove-rollback-health-1) [SEAM #[ignore]] ────
|
|
1856
|
-
// Golden _RemoveRollback captures `self.health = copy.deepcopy(store.agent_health().get(agent_id))`
|
|
1857
|
-
// (agents.py:185) and restore() re-upserts it via _restore_agent_health (agents.py:215-218,268-278). The
|
|
1858
|
-
// Rust RemoveRollback has NO health field and never restores it (restart.rs:972-1067). This is only
|
|
1859
|
-
// observable when a step AFTER the agent_health delete fails — but Rust's only post-delete step is the
|
|
1860
|
-
// snapshot, which golden runs OUTSIDE the rollback-protected region (agents.py:135). Exercising it
|
|
1861
|
-
// golden-faithfully needs a production failure-injection seam at an in-try step after the delete (mirror
|
|
1862
|
-
// the coordinator SaveHook). PORTER: add `health: Option<Value>` to RemoveRollback (capture the row
|
|
1863
|
-
// before delete; restore re-upserts status||"IDLE"/last_output_at/context_usage_pct/current_task_id, or
|
|
1864
|
-
// deletes if None) AND move save_team_runtime_snapshot OUTSIDE the rollback region (golden agents.py:135).
|
|
1865
|
-
#[test]
|
|
1866
|
-
#[ignore = "seam: agent_health rollback restore needs a failure-injection hook (post-delete, in-try) to \
|
|
1867
|
-
exercise in-process; golden agents.py:185/215-218/268-278. Porter adds RemoveRollback.health + \
|
|
1868
|
-
moves save_team_runtime_snapshot outside the rollback region."]
|
|
1869
|
-
fn lanea_remove_rollback_restores_agent_health() {
|
|
1870
|
-
// Golden contract (verified by reading agents.py): on a mid-remove failure after the agent_health
|
|
1871
|
-
// row is deleted, rollback re-upserts the captured row so the health history is not lost.
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
// ── FORK (fork-incomplete-rollback) [SEAM #[ignore]] — post-spawn rollback arms ─────────────────────
|
|
1875
|
-
// Golden operations.py:384-394 wraps spec-mutation..start_coordinator in try/except; on ANY failure it
|
|
1876
|
-
// (1) kills the spawned tmux window if present, (2) adapter.cleanup_mcp, (3) restores old spec text, and
|
|
1877
|
-
// (4) restores prior state. Rust only restores the spec on the spawn_into arm (launch.rs:481); the
|
|
1878
|
-
// save_runtime_state (486-487) and start_coordinator (488-493) failure arms leave the spec mutated, the
|
|
1879
|
-
// already-spawned window un-killed, and the state un-rolled-back; install_mcp/cleanup_mcp are absent.
|
|
1880
|
-
// The adapter.fork_plan arm IS covered HARD above (lanea_fork_gate_error_text_and_spec_rollback_on_adapter_arm).
|
|
1881
|
-
// The post-SPAWN arms need a failure-injection seam after spawn_into (codex+subscription forks past
|
|
1882
|
-
// adapter.fork_plan, so the spawn succeeds and there is no in-process way to fail save/coordinator cleanly).
|
|
1883
|
-
// PORTER: a Drop guard armed after the spec write, disarmed on success — kills the window, restores spec
|
|
1884
|
-
// + state, runs cleanup_mcp on every post-write error arm.
|
|
1885
|
-
#[test]
|
|
1886
|
-
#[ignore = "seam: fork post-spawn rollback arms (save_runtime_state / start_coordinator failure) need a \
|
|
1887
|
-
failure-injection hook after spawn_into; golden operations.py:384-394. Porter wires a Drop \
|
|
1888
|
-
guard (kill window + restore spec/state + cleanup_mcp) armed after the spec write."]
|
|
1889
|
-
fn lanea_fork_rollback_complete_on_post_spawn_failure() {
|
|
1890
|
-
// Golden contract (operations.py:384-394): a post-spawn failure kills the spawned window, restores
|
|
1891
|
-
// the old spec text + prior state, and runs cleanup_mcp before re-raising.
|
|
1892
|
-
}
|