@team-agent/installer 0.5.98 → 0.5.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +1 -163
  4. package/crates/team-agent/src/cli/diagnose.rs +3 -19
  5. package/crates/team-agent/src/cli/emit.rs +20 -204
  6. package/crates/team-agent/src/cli/mod.rs +138 -205
  7. package/crates/team-agent/src/cli/send/coordinator.rs +2 -2
  8. package/crates/team-agent/src/cli/spec.rs +41 -67
  9. package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -186
  10. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +9 -10
  11. package/crates/team-agent/src/cli/tests/run_delegation.rs +0 -3
  12. package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -1
  13. package/crates/team-agent/src/cli/triage.rs +1 -1
  14. package/crates/team-agent/src/cli/types.rs +0 -55
  15. package/crates/team-agent/src/compiler/tests.rs +5 -8
  16. package/crates/team-agent/src/compiler.rs +6 -14
  17. package/crates/team-agent/src/leader/lease.rs +1 -1
  18. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +1 -1
  19. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +1 -1
  20. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +5 -25
  21. package/crates/team-agent/src/lifecycle/launch/identity.rs +2 -18
  22. package/crates/team-agent/src/lifecycle/launch/layout.rs +7 -29
  23. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +1 -1
  24. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +1 -1
  25. package/crates/team-agent/src/lifecycle/launch/ownership.rs +1 -1
  26. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +7 -7
  27. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +12 -49
  28. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +3 -9
  29. package/crates/team-agent/src/lifecycle/launch/readiness.rs +1 -1
  30. package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -61
  31. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +2 -15
  32. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +3 -7
  33. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +1 -1
  34. package/crates/team-agent/src/lifecycle/launch.rs +3 -4
  35. package/crates/team-agent/src/lifecycle/mod.rs +0 -8
  36. package/crates/team-agent/src/lifecycle/restart/agent.rs +62 -8
  37. package/crates/team-agent/src/lifecycle/restart/common.rs +0 -32
  38. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +6 -15
  39. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +2 -2
  40. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +1 -1
  41. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +1 -2
  42. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +0 -1
  43. package/crates/team-agent/src/lifecycle/tests/core.rs +0 -213
  44. package/crates/team-agent/src/lifecycle/tests/issue238_codex_effort_contract.rs +96 -0
  45. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +1 -0
  46. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +31 -77
  47. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -2
  48. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +4 -8
  49. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +16 -9
  50. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +1 -1
  51. package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +0 -60
  52. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +3 -4
  53. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +2 -2
  54. package/crates/team-agent/src/lifecycle/tests.rs +1 -2
  55. package/crates/team-agent/src/lifecycle/types.rs +3 -133
  56. package/crates/team-agent/src/messaging/send.rs +2 -2
  57. package/crates/team-agent/src/model/enums.rs +21 -71
  58. package/crates/team-agent/src/model/spec.rs +5 -20
  59. package/crates/team-agent/src/provider/adapters/codex.rs +2 -2
  60. package/crates/team-agent/src/provider/types.rs +7 -6
  61. package/crates/team-agent/src/state/repository.rs +0 -5
  62. package/examples/team.spec.yaml +0 -1
  63. package/package.json +4 -4
  64. package/schemas/team.schema.json +2 -2
  65. package/skills/team-agent/SKILL.md +1 -1
  66. package/skills/team-agent/references/recovery-runbook.md +2 -2
  67. package/skills/team-agent/references/team-agent-operator.md +3 -10
  68. package/skills/team-agent/references/team-in-team.md +0 -6
  69. package/crates/team-agent/src/lifecycle/display.rs +0 -550
  70. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +0 -884
  71. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +0 -481
@@ -1,884 +0,0 @@
1
- #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
2
-
3
- #[path = "../../../tests/support/hermetic.rs"]
4
- mod hermetic_guard;
5
- #[allow(dead_code)]
6
- fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
7
-
8
- fn enter_hermetic(tag: &str) -> hermetic_guard::HermeticTestEnv {
9
- let env = hermetic_guard::HermeticTestEnv::enter(tag);
10
- env.scrub_tmux();
11
- env.assert_no_real_tmux();
12
- env
13
- }
14
-
15
- #[path = "../../../tests/support/composite_source.rs"]
16
- mod composite_source;
17
- use std::collections::BTreeMap;
18
- use std::path::{Path, PathBuf};
19
- use std::process::Command;
20
- use std::sync::atomic::{AtomicU64, Ordering};
21
- use std::sync::Mutex;
22
-
23
- use serde_json::{json, Value};
24
- use serial_test::serial;
25
- use team_agent::lifecycle::{quick_start_with_transport_in_workspace, restart_with_transport};
26
- use team_agent::state::persist::{load_runtime_state, save_runtime_state};
27
- use team_agent::transport::{
28
- AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
29
- InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, SessionName, SetEnvOutcome,
30
- SpawnResult, SubmitVerification, Target, Transport, TransportError, TurnVerification,
31
- WindowName,
32
- };
33
-
34
- fn bin() -> &'static str {
35
- crate::lifecycle::tests::test_binary_path()
36
- }
37
-
38
- #[test]
39
- #[serial(env)]
40
- fn claude_worker_spawn_argv_uses_compiled_prompt_without_tool_restrictions() {
41
- let _hermetic = enter_hermetic("core034-claude-argv");
42
- let _ancestry = EnvGuard::set(
43
- "TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON",
44
- "[\"/bin/zsh\"]",
45
- );
46
- let ws = tmp_dir("claude-argv");
47
- let team_dir = write_team(
48
- &ws,
49
- "worker-argv",
50
- "claude",
51
- None,
52
- &[AgentDoc {
53
- id: "worker_a",
54
- role: "Implementation Worker",
55
- provider: "claude",
56
- tools: &["fs_read", "fs_write", "fs_list", "execute_bash", "mcp_team"],
57
- body: "ROLE BODY SENTINEL: build the porcelain adapter without hiding failures.",
58
- }],
59
- );
60
- seed_healthy_coordinator(&ws);
61
- let transport = RecordingTransport::new();
62
-
63
- quick_start_with_transport_in_workspace(
64
- &ws,
65
- &team_dir,
66
- None,
67
- true,
68
- Some("workerargv"),
69
- &transport,
70
- )
71
- .expect("quick-start should reach the real lifecycle command-construction path");
72
-
73
- let spawn = transport.single_spawn();
74
- let prompt = flag_value(&spawn.argv, "--append-system-prompt").unwrap_or_default();
75
- let mut failures = Vec::new();
76
- for marker in [
77
- "worker_a",
78
- "Implementation Worker",
79
- "Team Agent Teammate Runtime Contract",
80
- "send_message(to='leader'",
81
- "report_result exactly once",
82
- "ROLE BODY SENTINEL",
83
- ] {
84
- if !prompt.contains(marker) {
85
- failures.push(format!("B2 missing compiled prompt marker {marker:?}"));
86
- }
87
- }
88
- if prompt == "Implementation Worker" {
89
- failures.push("B2 prompt collapsed to the bare role label".to_string());
90
- }
91
-
92
- if argv_has_flag(&spawn.argv, "--disallowedTools") {
93
- failures.push("B1 Claude argv must not derive --disallowedTools from tools metadata".to_string());
94
- }
95
- if argv_has_flag(&spawn.argv, "--allowedTools") {
96
- failures.push("B1 Claude argv must not derive --allowedTools from tools metadata".to_string());
97
- }
98
- assert!(
99
- failures.is_empty(),
100
- "B1/B2 worker argv contract failed:\n{}\nprompt={prompt:?}\nargv={:?}",
101
- failures.join("\n"),
102
- spawn.argv
103
- );
104
- }
105
-
106
- #[test]
107
- #[serial(env)]
108
- fn claude_control_role_does_not_derive_native_tool_restrictions() {
109
- let _hermetic = enter_hermetic("core034-claude-control");
110
- let _ancestry = EnvGuard::set(
111
- "TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON",
112
- "[\"/bin/zsh\"]",
113
- );
114
- let ws = tmp_dir("claude-control");
115
- let team_dir = write_team(
116
- &ws,
117
- "control-argv",
118
- "claude",
119
- None,
120
- &[AgentDoc {
121
- id: "control",
122
- role: "Control Worker",
123
- provider: "claude",
124
- tools: &["mcp_team"],
125
- body: "CONTROL BODY SENTINEL: route only through Team Agent MCP.",
126
- }],
127
- );
128
- seed_healthy_coordinator(&ws);
129
- let transport = RecordingTransport::new();
130
-
131
- quick_start_with_transport_in_workspace(
132
- &ws,
133
- &team_dir,
134
- None,
135
- true,
136
- Some("controlargv"),
137
- &transport,
138
- )
139
- .expect("quick-start should record the control worker command");
140
-
141
- let spawn = transport.single_spawn();
142
- assert!(
143
- !argv_has_flag(&spawn.argv, "--disallowedTools"),
144
- "B1 control role must not derive native-tool restrictions from tools metadata; argv={:?}",
145
- spawn.argv
146
- );
147
- assert!(
148
- !argv_has_flag(&spawn.argv, "--allowedTools"),
149
- "B1 control role must not derive an allowlist from tools metadata; argv={:?}",
150
- spawn.argv
151
- );
152
- }
153
-
154
- #[test]
155
- #[serial(env)]
156
- fn restart_claude_worker_does_not_derive_tool_restrictions_when_runtime_state_has_no_raw_tools() {
157
- let _hermetic = enter_hermetic("core034-restart-tools");
158
- let _ancestry = EnvGuard::set(
159
- "TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON",
160
- "[\"/bin/zsh\"]",
161
- );
162
- let ws = tmp_dir("claude-restart-tools");
163
- let team_dir = write_team(
164
- &ws,
165
- "restart-tools",
166
- "claude",
167
- Some("none"),
168
- &[AgentDoc {
169
- id: "worker_a",
170
- role: "Implementation Worker",
171
- provider: "claude",
172
- tools: &["fs_read", "fs_write", "fs_list", "execute_bash", "mcp_team"],
173
- body: "ROLE BODY SENTINEL: restart must keep declared native tools.",
174
- }],
175
- );
176
- seed_healthy_coordinator(&ws);
177
- quick_start_with_transport_in_workspace(
178
- &ws,
179
- &team_dir,
180
- None,
181
- true,
182
- Some("restarttools"),
183
- &RecordingTransport::new(),
184
- )
185
- .expect("quick-start should seed restart state");
186
- seed_running_resumable_state(&ws, "restarttools", "worker_a", "claude");
187
- remove_runtime_raw_tools(&ws, "restarttools", "worker_a");
188
-
189
- let restart_transport = RecordingTransport::new();
190
- let report = restart_with_transport(&ws, true, Some("restarttools"), &restart_transport)
191
- .expect("restart should reach worker command construction");
192
- let spawn = restart_transport.single_spawn();
193
- assert!(
194
- !argv_has_flag(&spawn.argv, "--disallowedTools"),
195
- "B1 restart must not derive native-tool restrictions from tools metadata; argv={:?} report={report:?}",
196
- spawn.argv
197
- );
198
- assert!(
199
- !argv_has_flag(&spawn.argv, "--allowedTools"),
200
- "B1 restart must not derive an allowlist from tools metadata; argv={:?} report={report:?}",
201
- spawn.argv
202
- );
203
- }
204
-
205
- #[test]
206
- fn worker_command_context_uses_single_prompt_without_permission_sources() {
207
- let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
208
- let src_root = crate_root.join("src");
209
- let launch = composite_source::composite_source("src/lifecycle/launch.rs");
210
- let restart_common = read_to_string(&src_root.join("lifecycle/restart/common.rs"));
211
- let restart_agent = read_to_string(&src_root.join("lifecycle/restart/agent.rs"));
212
- // 0.4.x decoupling step 2: claude argv builder moved to
213
- // provider/adapters/claude.rs. The adapter B1 grep guard now reads the
214
- // composite source (adapter.rs + every provider-local adapter file).
215
- let adapter = {
216
- let mut s = read_to_string(&src_root.join("provider/adapter.rs"));
217
- let adapters_dir = src_root.join("provider/adapters");
218
- if adapters_dir.is_dir() {
219
- for entry in std::fs::read_dir(&adapters_dir)
220
- .expect("read adapters dir")
221
- .flatten()
222
- {
223
- let p = entry.path();
224
- if p.extension().and_then(|e| e.to_str()) == Some("rs") {
225
- s.push('\n');
226
- s.push_str(&read_to_string(&p));
227
- }
228
- }
229
- }
230
- s
231
- };
232
- let all_src = read_rust_sources(&src_root);
233
- let command_sources = format!("{launch}\n{restart_common}\n{restart_agent}");
234
-
235
- assert!(
236
- all_src.contains("compile_worker_system_prompt")
237
- && all_src.contains("runtime_contract_section")
238
- && all_src.contains("identity_section")
239
- && all_src.contains("role_body")
240
- && all_src.contains("output_contract"),
241
- "B2 grep guard: worker command construction needs one compiled prompt helper with the CR-approved four sections \
242
- (runtime_contract_section, identity_section, role_body, output_contract)."
243
- );
244
- assert!(
245
- !command_sources.contains("system_prompt: role"),
246
- "B2 grep guard: launch/restart must not pass the raw role label as ProviderCommandContext.system_prompt."
247
- );
248
- assert!(
249
- all_src.matches("Team Agent Teammate Runtime Contract").count() == 1,
250
- "B2 grep guard: the common teammate runtime contract must have a single production source; count={}",
251
- all_src.matches("Team Agent Teammate Runtime Contract").count()
252
- );
253
- assert!(
254
- all_src.matches("529").count() <= 1 && all_src.matches("Overloaded").count() <= 1,
255
- "B2 grep guard: common 529/Overloaded slowdown wording belongs in the one runtime contract template, not scattered role/provider code."
256
- );
257
-
258
- assert!(
259
- !command_sources.contains("resolve_permissions")
260
- && !command_sources.contains("resolved_tool_strings"),
261
- "B1 grep guard: command construction must not derive argv restrictions from tools metadata."
262
- );
263
- assert!(
264
- !command_sources.contains("worker_tool_refs(agent_tool_strings")
265
- && !command_sources.contains("tools: &tool_refs"),
266
- "B1 grep guard: command construction must not raw-pass agent.tools/tool_refs into ProviderCommandContext."
267
- );
268
- assert!(
269
- !adapter.contains("--disallowedTools") && !adapter.contains("--allowedTools"),
270
- "B1 provider adapter must not derive Claude tool restrictions from tools metadata."
271
- );
272
- assert!(
273
- !adapter.contains("resolve_permissions"),
274
- "B1 grep guard: adapter.rs should stay a dumb argv renderer; provider-specific permission fallback guesses belong in the shared command-context helper."
275
- );
276
- }
277
-
278
- #[test]
279
- #[serial(env)]
280
- fn quick_start_default_display_adaptive_and_none_escape_hatch_are_observable() {
281
- let _hermetic = enter_hermetic("core034-display");
282
- let default_ws = tmp_dir("display-default");
283
- let default_team = write_team(
284
- &default_ws,
285
- "display-default-team",
286
- "fake",
287
- None,
288
- &[AgentDoc {
289
- id: "worker",
290
- role: "Fake Worker",
291
- provider: "fake",
292
- tools: &["mcp_team"],
293
- body: "Fake worker.",
294
- }],
295
- );
296
- seed_healthy_coordinator(&default_ws);
297
- let default_report = quick_start_with_transport_in_workspace(
298
- &default_ws,
299
- &default_team,
300
- None,
301
- true,
302
- Some("defaultdisplay"),
303
- &RecordingTransport::new(),
304
- )
305
- .expect("default quick-start should run with recording transport");
306
- let default_state = load_runtime_state(&default_ws).expect("load default display state");
307
-
308
- assert_eq!(
309
- state_display_backend(&default_state),
310
- Some("adaptive"),
311
- "adaptive layout contract: omitting display_backend in TEAM.md must persist display_backend=adaptive. \
312
- state={default_state}\nreport={default_report:?}"
313
- );
314
- assert!(
315
- format!("{default_report:?}").contains("display_backend")
316
- && format!("{default_report:?}").contains("adaptive"),
317
- "adaptive layout contract: quick-start report/JSON surface must visibly report display_backend=adaptive, not hide the default. report={default_report:?}"
318
- );
319
- // 0.3.28 Step 4b: per-agent adaptive `display` records removed in favour
320
- // of 1-window-per-agent layout. The state-root `display_backend=adaptive`
321
- // (asserted above) is the contract. Per-agent display dicts were the
322
- // pre-0.3.28 adaptive overlay bookkeeping; the architecture moved that to
323
- // `layout::overlay` (display overlay only fires for the leader-session
324
- // overlay path, not per worker).
325
- // Skip state_has_adaptive_display_records — replaced by:
326
- assert!(
327
- default_state.pointer("/agents/worker/window").is_some(),
328
- "0.3.28 Step 4b: each worker has its own window in the worker session. \
329
- state={default_state}"
330
- );
331
-
332
- let none_ws = tmp_dir("display-none");
333
- let none_team = write_team(
334
- &none_ws,
335
- "display-none-team",
336
- "fake",
337
- Some("none"),
338
- &[AgentDoc {
339
- id: "worker",
340
- role: "Fake Worker",
341
- provider: "fake",
342
- tools: &["mcp_team"],
343
- body: "Fake worker.",
344
- }],
345
- );
346
- seed_healthy_coordinator(&none_ws);
347
- let none_report = quick_start_with_transport_in_workspace(
348
- &none_ws,
349
- &none_team,
350
- None,
351
- true,
352
- Some("nonedisplay"),
353
- &RecordingTransport::new(),
354
- )
355
- .expect("none quick-start should run with recording transport");
356
- let none_state = load_runtime_state(&none_ws).expect("load none display state");
357
- assert_eq!(
358
- state_display_backend(&none_state),
359
- Some("none"),
360
- "adaptive layout contract: explicit display_backend=none remains the escape hatch. state={none_state}\nreport={none_report:?}"
361
- );
362
- assert!(
363
- !state_has_adaptive_display_records(&none_state),
364
- "explicit display_backend=none must not create adaptive display records. state={none_state}"
365
- );
366
- }
367
-
368
- #[test]
369
- #[serial(env)]
370
- fn quick_start_and_restart_emit_copyable_workspace_socket_attach_commands() {
371
- let _hermetic = enter_hermetic("core034-attach-command");
372
- let ws = tmp_dir("attach-command");
373
- let team = write_team(
374
- &ws,
375
- "attach-team",
376
- "fake",
377
- Some("none"),
378
- &[
379
- AgentDoc {
380
- id: "worker_a",
381
- role: "Fake Worker A",
382
- provider: "fake",
383
- tools: &["mcp_team"],
384
- body: "Fake worker A.",
385
- },
386
- AgentDoc {
387
- id: "worker_b",
388
- role: "Fake Worker B",
389
- provider: "fake",
390
- tools: &["mcp_team"],
391
- body: "Fake worker B.",
392
- },
393
- ],
394
- );
395
- seed_healthy_coordinator(&ws);
396
- let transport = RecordingTransport::new();
397
- let quick = quick_start_with_transport_in_workspace(
398
- &ws,
399
- &team,
400
- None,
401
- true,
402
- Some("attachcmd"),
403
- &transport,
404
- )
405
- .expect("quick-start should run with recording transport");
406
- let quick_text = format!("{quick:?}");
407
- let mut failures = Vec::new();
408
- if !report_has_attach_command(&quick_text, "team-attachcmd", "worker_a") {
409
- failures.push(format!(
410
- "quick-start missing `tmux -S <full-socket-path> attach -t team-attachcmd:worker_a`; report={quick:?}"
411
- ));
412
- }
413
- if !report_has_attach_command(&quick_text, "team-attachcmd", "worker_b") {
414
- failures.push(format!(
415
- "quick-start missing `tmux -S <full-socket-path> attach -t team-attachcmd:worker_b`; report={quick:?}"
416
- ));
417
- }
418
- seed_running_resumable_state(&ws, "attachcmd", "worker_a", "fake");
419
- seed_running_resumable_state(&ws, "attachcmd", "worker_b", "fake");
420
- let restart = restart_with_transport(&ws, true, Some("attachcmd"), &RecordingTransport::new())
421
- .expect("restart should run with recording transport");
422
- let restart_text = format!("{restart:?}");
423
- match &restart {
424
- team_agent::lifecycle::RestartReport::Restarted {
425
- attach_commands,
426
- next_actions,
427
- ..
428
- } => {
429
- if attach_commands.len() != 1 {
430
- failures.push(format!(
431
- "restart must emit exactly one attach command; report={restart:?}"
432
- ));
433
- }
434
- if !next_actions.is_empty() {
435
- failures.push(format!(
436
- "restart next_actions must not duplicate attach_commands; report={restart:?}"
437
- ));
438
- }
439
- if !report_has_session_attach_command(&attach_commands.join("\n"), "team-attachcmd") {
440
- failures.push(format!(
441
- "restart missing `tmux -S <full-socket-path> attach -t team-attachcmd`; report={restart:?}"
442
- ));
443
- }
444
- for worker in ["worker_a", "worker_b"] {
445
- if report_has_attach_command(&attach_commands.join("\n"), "team-attachcmd", worker)
446
- {
447
- failures.push(format!(
448
- "restart must not emit per-worker attach command for {worker}; report={restart:?}"
449
- ));
450
- }
451
- }
452
- }
453
- _ => failures.push(format!("restart should succeed; report={restart_text}")),
454
- }
455
- assert!(
456
- failures.is_empty(),
457
- "B3 attach-command contract: quick-start and restart report/JSON must include copyable workspace-socket attach commands.\n{}",
458
- failures.join("\n")
459
- );
460
- }
461
-
462
- #[test]
463
- fn display_default_none_is_documented_in_help_and_changelog() {
464
- let help = Command::new(bin())
465
- .args(["quick-start", "--help"])
466
- .output()
467
- .expect("run quick-start --help");
468
- let help_text = format!(
469
- "{}\n{}",
470
- String::from_utf8_lossy(&help.stdout),
471
- String::from_utf8_lossy(&help.stderr)
472
- );
473
- assert!(
474
- help_text.contains("display_backend")
475
- && help_text.to_lowercase().contains("default")
476
- && help_text.to_lowercase().contains("adaptive")
477
- && help_text.to_lowercase().contains("none"),
478
- "adaptive layout guard: quick-start help must disclose default adaptive and none escape hatch. help={help_text:?}"
479
- );
480
-
481
- let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
482
- .parent()
483
- .and_then(Path::parent)
484
- .unwrap()
485
- .to_path_buf();
486
- let docs = read_optional_changelogs(&repo_root);
487
- assert!(
488
- docs.contains("display_backend")
489
- && docs.to_lowercase().contains("adaptive")
490
- && docs.to_lowercase().contains("none"),
491
- "adaptive layout guard: CHANGELOG/docs must disclose default adaptive and none escape hatch. searched_root={} docs_excerpt={}",
492
- repo_root.display(),
493
- &docs.chars().take(500).collect::<String>()
494
- );
495
- }
496
-
497
- #[derive(Clone)]
498
- struct AgentDoc<'a> {
499
- id: &'a str,
500
- role: &'a str,
501
- provider: &'a str,
502
- tools: &'a [&'a str],
503
- body: &'a str,
504
- }
505
-
506
- #[derive(Debug, Clone)]
507
- struct RecordedSpawn {
508
- argv: Vec<String>,
509
- }
510
-
511
- #[derive(Default)]
512
- struct RecordingTransport {
513
- spawns: Mutex<Vec<RecordedSpawn>>,
514
- session_present: Mutex<bool>,
515
- }
516
-
517
- struct EnvGuard {
518
- key: &'static str,
519
- old: Option<String>,
520
- }
521
-
522
- impl EnvGuard {
523
- fn set(key: &'static str, value: &str) -> Self {
524
- let old = std::env::var(key).ok();
525
- std::env::set_var(key, value);
526
- Self { key, old }
527
- }
528
- }
529
-
530
- impl Drop for EnvGuard {
531
- fn drop(&mut self) {
532
- match self.old.take() {
533
- Some(value) => std::env::set_var(self.key, value),
534
- None => std::env::remove_var(self.key),
535
- }
536
- }
537
- }
538
-
539
- impl RecordingTransport {
540
- fn new() -> Self {
541
- Self::default()
542
- }
543
-
544
- fn single_spawn(&self) -> RecordedSpawn {
545
- let spawns = self.spawns.lock().unwrap();
546
- assert_eq!(
547
- spawns.len(),
548
- 1,
549
- "fixture should record one worker spawn; spawns={spawns:?}"
550
- );
551
- spawns[0].clone()
552
- }
553
-
554
- fn record_spawn(
555
- &self,
556
- session: &SessionName,
557
- window: &WindowName,
558
- argv: &[String],
559
- ) -> SpawnResult {
560
- let mut spawns = self.spawns.lock().unwrap();
561
- spawns.push(RecordedSpawn {
562
- argv: argv.to_vec(),
563
- });
564
- *self.session_present.lock().unwrap() = true;
565
- SpawnResult {
566
- pane_id: PaneId::new(format!("%{}", spawns.len())),
567
- session: session.clone(),
568
- window: window.clone(),
569
- child_pid: Some(10_000 + spawns.len() as u32),
570
- }
571
- }
572
- }
573
-
574
- impl Transport for RecordingTransport {
575
- fn kind(&self) -> BackendKind {
576
- BackendKind::Tmux
577
- }
578
-
579
- fn spawn_first(
580
- &self,
581
- session: &SessionName,
582
- window: &WindowName,
583
- argv: &[String],
584
- _cwd: &Path,
585
- _env: &BTreeMap<String, String>,
586
- ) -> Result<SpawnResult, TransportError> {
587
- Ok(self.record_spawn(session, window, argv))
588
- }
589
-
590
- fn spawn_into(
591
- &self,
592
- session: &SessionName,
593
- window: &WindowName,
594
- argv: &[String],
595
- _cwd: &Path,
596
- _env: &BTreeMap<String, String>,
597
- ) -> Result<SpawnResult, TransportError> {
598
- Ok(self.record_spawn(session, window, argv))
599
- }
600
-
601
- fn inject(
602
- &self,
603
- _target: &Target,
604
- _payload: &InjectPayload,
605
- _submit: Key,
606
- _bracketed: bool,
607
- ) -> Result<InjectReport, TransportError> {
608
- Ok(InjectReport {
609
- stage_reached: InjectStage::Submit,
610
- inject_verification: InjectVerification::CaptureContainsToken,
611
- submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
612
- turn_verification: TurnVerification::NotYetObserved,
613
- attempts: 1,
614
- submit_diagnostics: None,
615
- })
616
- }
617
-
618
- fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> {
619
- Ok(())
620
- }
621
-
622
- fn capture(
623
- &self,
624
- _target: &Target,
625
- range: CaptureRange,
626
- ) -> Result<CapturedText, TransportError> {
627
- Ok(CapturedText {
628
- text: "Claude Code\n> \nOpenAI Codex\ncodex>".to_string(),
629
- range,
630
- })
631
- }
632
-
633
- fn query(&self, _target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
634
- match field {
635
- PaneField::PaneWidth => Ok(Some("120".to_string())),
636
- _ => Ok(None),
637
- }
638
- }
639
-
640
- fn liveness(
641
- &self,
642
- _pane: &PaneId,
643
- ) -> Result<team_agent::transport::PaneLiveness, TransportError> {
644
- Ok(team_agent::transport::PaneLiveness::Live)
645
- }
646
-
647
- fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
648
- Ok(Vec::new())
649
- }
650
-
651
- fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
652
- Ok(*self.session_present.lock().unwrap())
653
- }
654
-
655
- fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
656
- Ok(Vec::new())
657
- }
658
-
659
- fn set_session_env(
660
- &self,
661
- _session: &SessionName,
662
- _key: &str,
663
- _value: &str,
664
- ) -> Result<SetEnvOutcome, TransportError> {
665
- Ok(SetEnvOutcome::Applied)
666
- }
667
-
668
- fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
669
- *self.session_present.lock().unwrap() = false;
670
- Ok(())
671
- }
672
-
673
- fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
674
- Ok(())
675
- }
676
-
677
- fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
678
- Ok(AttachOutcome::Attached)
679
- }
680
- }
681
-
682
- fn tmp_dir(tag: &str) -> PathBuf {
683
- static N: AtomicU64 = AtomicU64::new(0);
684
- let dir = std::env::temp_dir().join(format!(
685
- "ta-rs-core-034-{tag}-{}-{}",
686
- std::process::id(),
687
- N.fetch_add(1, Ordering::Relaxed)
688
- ));
689
- let _ = std::fs::remove_dir_all(&dir);
690
- std::fs::create_dir_all(&dir).unwrap();
691
- std::fs::canonicalize(dir).unwrap()
692
- }
693
-
694
- fn write_team(
695
- workspace: &Path,
696
- name: &str,
697
- provider: &str,
698
- display_backend: Option<&str>,
699
- agents: &[AgentDoc<'_>],
700
- ) -> PathBuf {
701
- let team = workspace.join(name);
702
- std::fs::create_dir_all(team.join("agents")).unwrap();
703
- let display = display_backend
704
- .map(|backend| format!("display_backend: {backend}\n"))
705
- .unwrap_or_default();
706
- std::fs::write(
707
- team.join("TEAM.md"),
708
- format!(
709
- "---\nname: {name}\nobjective: 0.3.4 core contract fixture.\nprovider: {provider}\n{display}---\n\nTeam fixture.\n"
710
- ),
711
- )
712
- .unwrap();
713
- for agent in agents {
714
- let tools = agent
715
- .tools
716
- .iter()
717
- .map(|tool| format!(" - {tool}\n"))
718
- .collect::<String>();
719
- std::fs::write(
720
- team.join("agents").join(format!("{}.md", agent.id)),
721
- format!(
722
- "---\nname: {}\nrole: {}\nprovider: {}\nmodel: fake\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n{}---\n\n{}\n",
723
- agent.id, agent.role, agent.provider, tools, agent.body
724
- ),
725
- )
726
- .unwrap();
727
- }
728
- team
729
- }
730
-
731
- fn seed_healthy_coordinator(workspace: &Path) {
732
- let workspace = team_agent::coordinator::WorkspacePath::new(workspace.to_path_buf());
733
- let pid = team_agent::coordinator::Pid::new(std::process::id());
734
- std::fs::create_dir_all(
735
- team_agent::coordinator::coordinator_pid_path(&workspace)
736
- .parent()
737
- .unwrap(),
738
- )
739
- .unwrap();
740
- team_agent::coordinator::write_coordinator_metadata(
741
- &workspace,
742
- pid,
743
- team_agent::coordinator::MetadataSource::Boot,
744
- )
745
- .expect("write coordinator metadata");
746
- std::fs::write(
747
- team_agent::coordinator::coordinator_pid_path(&workspace),
748
- pid.to_string(),
749
- )
750
- .expect("write coordinator pid");
751
- }
752
-
753
- fn seed_running_resumable_state(workspace: &Path, team: &str, agent: &str, provider: &str) {
754
- let mut state = load_runtime_state(workspace).expect("load quick-start state before restart");
755
- let agent_patch = json!({
756
- "status": "running",
757
- "provider": provider,
758
- "session_id": "11111111-2222-4333-8444-555555555555",
759
- "first_send_at": "2026-06-10T00:00:00+00:00",
760
- "window": agent,
761
- });
762
- merge_agent_patch(state.pointer_mut(&format!("/agents/{agent}")), &agent_patch);
763
- merge_agent_patch(
764
- state.pointer_mut(&format!("/teams/{team}/agents/{agent}")),
765
- &agent_patch,
766
- );
767
- save_runtime_state(workspace, &state).expect("save resumable state");
768
- }
769
-
770
- fn merge_agent_patch(target: Option<&mut Value>, patch: &Value) {
771
- let Some(target) = target.and_then(Value::as_object_mut) else {
772
- return;
773
- };
774
- for (key, value) in patch.as_object().unwrap() {
775
- target.insert(key.clone(), value.clone());
776
- }
777
- }
778
-
779
- fn remove_runtime_raw_tools(workspace: &Path, team: &str, agent: &str) {
780
- let mut state = load_runtime_state(workspace).expect("load state before stripping raw tools");
781
- for pointer in [
782
- format!("/agents/{agent}/tools"),
783
- format!("/teams/{team}/agents/{agent}/tools"),
784
- ] {
785
- if let Some(obj) = state
786
- .pointer_mut(pointer.trim_end_matches("/tools"))
787
- .and_then(Value::as_object_mut)
788
- {
789
- obj.remove("tools");
790
- obj.remove("role");
791
- }
792
- }
793
- save_runtime_state(workspace, &state).expect("save state after stripping raw tools");
794
- }
795
-
796
- fn flag_value(argv: &[String], flag: &str) -> Option<String> {
797
- argv.windows(2)
798
- .find_map(|pair| (pair[0] == flag).then(|| pair[1].clone()))
799
- }
800
-
801
- fn argv_has_flag(argv: &[String], flag: &str) -> bool {
802
- argv.iter().any(|arg| arg == flag)
803
- }
804
-
805
- fn state_display_backend(state: &Value) -> Option<&str> {
806
- state
807
- .get("display_backend")
808
- .and_then(Value::as_str)
809
- .or_else(|| {
810
- state
811
- .get("runtime")
812
- .and_then(|runtime| runtime.get("display_backend"))
813
- .and_then(Value::as_str)
814
- })
815
- }
816
-
817
- fn state_has_adaptive_display_records(state: &Value) -> bool {
818
- let agents = state
819
- .get("agents")
820
- .and_then(Value::as_object)
821
- .into_iter()
822
- .flat_map(|agents| agents.values());
823
- agents.into_iter().any(|agent| {
824
- agent
825
- .get("display")
826
- .and_then(|display| display.get("backend"))
827
- .and_then(Value::as_str)
828
- == Some("adaptive")
829
- })
830
- }
831
-
832
- fn report_has_attach_command(text: &str, expected_session: &str, expected_window: &str) -> bool {
833
- text.contains("tmux -S")
834
- && text.contains(" attach -t ")
835
- && text.contains(&format!("{expected_session}:{expected_window}"))
836
- }
837
-
838
- fn report_has_session_attach_command(text: &str, expected_session: &str) -> bool {
839
- text.contains("tmux -S")
840
- && text.contains(" attach -t ")
841
- && text.contains(&format!("attach -t {expected_session}"))
842
- }
843
-
844
- fn read_to_string(path: &Path) -> String {
845
- std::fs::read_to_string(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()))
846
- }
847
-
848
- fn read_rust_sources(root: &Path) -> String {
849
- let mut out = String::new();
850
- read_rust_sources_into(root, &mut out);
851
- out
852
- }
853
-
854
- fn read_rust_sources_into(root: &Path, out: &mut String) {
855
- let entries =
856
- std::fs::read_dir(root).unwrap_or_else(|e| panic!("read_dir {}: {e}", root.display()));
857
- for entry in entries {
858
- let path = entry.unwrap().path();
859
- if path.is_dir() {
860
- if path.file_name().and_then(|name| name.to_str()) == Some("tests") {
861
- continue;
862
- }
863
- read_rust_sources_into(&path, out);
864
- } else if path.extension().and_then(|s| s.to_str()) == Some("rs") {
865
- if path.file_name().and_then(|name| name.to_str()) == Some("tests.rs") {
866
- continue;
867
- }
868
- out.push_str(&read_to_string(&path));
869
- out.push('\n');
870
- }
871
- }
872
- }
873
-
874
- fn read_optional_changelogs(repo_root: &Path) -> String {
875
- let mut text = String::new();
876
- for name in ["CHANGELOG.md", "CHANGELOG", "README.md"] {
877
- let path = repo_root.join(name);
878
- if path.exists() {
879
- text.push_str(&read_to_string(&path));
880
- text.push('\n');
881
- }
882
- }
883
- text
884
- }