@team-agent/installer 0.5.66 → 0.5.69

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 (178) hide show
  1. package/Cargo.lock +8 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -0
  4. package/crates/team-agent/src/cli/adapters.rs +5 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +156 -0
  6. package/crates/team-agent/src/cli/emit.rs +6 -5
  7. package/crates/team-agent/src/cli/grok_slot.rs +299 -0
  8. package/crates/team-agent/src/cli/leader.rs +22 -7
  9. package/crates/team-agent/src/cli/leaders.rs +125 -1
  10. package/crates/team-agent/src/cli/mod.rs +20 -0
  11. package/crates/team-agent/src/cli/send/presentation.rs +7 -1
  12. package/crates/team-agent/src/cli/spec.rs +3 -0
  13. package/crates/team-agent/src/cli/status_port/compact.rs +28 -0
  14. package/crates/team-agent/src/cli/status_port/snapshot.rs +25 -1
  15. package/crates/team-agent/src/cli/tests/base.rs +26 -3
  16. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +129 -19
  17. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +387 -6
  18. package/crates/team-agent/src/cli/tests/status_send.rs +79 -10
  19. package/crates/team-agent/src/communication_mode/mod.rs +6 -4
  20. package/crates/team-agent/src/compiler.rs +121 -5
  21. package/crates/team-agent/src/coordinator/backoff.rs +55 -0
  22. package/crates/team-agent/src/coordinator/conpty_shim.rs +82 -0
  23. package/crates/team-agent/src/coordinator/health.rs +173 -0
  24. package/crates/team-agent/src/coordinator/mod.rs +31 -0
  25. package/crates/team-agent/src/coordinator/orphan.rs +31 -0
  26. package/crates/team-agent/src/coordinator/runtime_detectors.rs +31 -0
  27. package/crates/team-agent/src/coordinator/runtime_observation.rs +25 -0
  28. package/crates/team-agent/src/coordinator/steps/abnormal.rs +59 -0
  29. package/crates/team-agent/src/coordinator/steps/delivery.rs +10 -0
  30. package/crates/team-agent/src/coordinator/steps/health_sync.rs +10 -0
  31. package/crates/team-agent/src/coordinator/steps/mod.rs +22 -0
  32. package/crates/team-agent/src/coordinator/steps/persist.rs +10 -0
  33. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +10 -0
  34. package/crates/team-agent/src/coordinator/steps/session_gate.rs +10 -0
  35. package/crates/team-agent/src/coordinator/tick.rs +133 -22
  36. package/crates/team-agent/src/coordinator/types.rs +49 -0
  37. package/crates/team-agent/src/db/message_store.rs +39 -5
  38. package/crates/team-agent/src/layout/worker_env.rs +20 -3
  39. package/crates/team-agent/src/layout/worker_window_helpers.rs +3 -0
  40. package/crates/team-agent/src/leader/provider_attribution.rs +53 -0
  41. package/crates/team-agent/src/leader/registry.rs +65 -0
  42. package/crates/team-agent/src/leader/start.rs +973 -75
  43. package/crates/team-agent/src/lifecycle/display.rs +56 -0
  44. package/crates/team-agent/src/lifecycle/helpers.rs +57 -0
  45. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +398 -26
  46. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +80 -0
  47. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +60 -0
  48. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +101 -11
  49. package/crates/team-agent/src/lifecycle/launch/cursor_create_chat.rs +229 -0
  50. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +548 -0
  51. package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +120 -0
  52. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +208 -456
  53. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +24 -0
  54. package/crates/team-agent/src/lifecycle/launch/grok_per_seat.rs +260 -0
  55. package/crates/team-agent/src/lifecycle/launch/identity.rs +146 -0
  56. package/crates/team-agent/src/lifecycle/launch/layout.rs +60 -0
  57. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +112 -0
  58. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +482 -1
  59. package/crates/team-agent/src/lifecycle/launch/ownership.rs +40 -0
  60. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +830 -0
  61. package/crates/team-agent/src/lifecycle/launch/plan.rs +31 -0
  62. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +66 -0
  63. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +87 -3
  64. package/crates/team-agent/src/lifecycle/launch/readiness.rs +85 -38
  65. package/crates/team-agent/src/lifecycle/launch/role_source.rs +44 -44
  66. package/crates/team-agent/src/lifecycle/launch/spawn.rs +127 -25
  67. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +140 -0
  68. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +109 -0
  69. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +214 -1
  70. package/crates/team-agent/src/lifecycle/launch.rs +107 -29
  71. package/crates/team-agent/src/lifecycle/lock.rs +42 -1
  72. package/crates/team-agent/src/lifecycle/mod.rs +11 -0
  73. package/crates/team-agent/src/lifecycle/pane_input_lock.rs +161 -0
  74. package/crates/team-agent/src/lifecycle/profile_launch.rs +99 -0
  75. package/crates/team-agent/src/lifecycle/profile_smoke.rs +52 -1
  76. package/crates/team-agent/src/lifecycle/restart/agent.rs +214 -160
  77. package/crates/team-agent/src/lifecycle/restart/common.rs +513 -37
  78. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +27 -0
  79. package/crates/team-agent/src/lifecycle/restart/preflight.rs +25 -0
  80. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +106 -0
  81. package/crates/team-agent/src/lifecycle/restart/remove.rs +233 -6
  82. package/crates/team-agent/src/lifecycle/restart/selection.rs +72 -0
  83. package/crates/team-agent/src/lifecycle/restart/team_state.rs +22 -0
  84. package/crates/team-agent/src/lifecycle/restart.rs +29 -0
  85. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -3
  86. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +24 -3
  87. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +309 -0
  88. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +150 -157
  89. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +2 -2
  90. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -0
  91. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +464 -0
  92. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +247 -0
  93. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +353 -0
  94. package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_g0.stdout.txt +222 -0
  95. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +787 -0
  96. package/crates/team-agent/src/lifecycle/tests/gate_fixtures.rs +562 -0
  97. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +239 -0
  98. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +485 -0
  99. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +250 -0
  100. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +293 -0
  101. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
  102. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +63 -38
  103. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -2
  104. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +16 -8
  105. package/crates/team-agent/src/lifecycle/tests/mcp_tool_name_format_red.rs +69 -0
  106. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +20 -19
  107. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +97 -61
  108. package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +97 -0
  109. package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +128 -0
  110. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +207 -0
  111. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +838 -0
  112. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +320 -0
  113. package/crates/team-agent/src/lifecycle/tests/pi_lifecycle_doctor_red.rs +98 -0
  114. package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +216 -0
  115. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +3 -0
  116. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +40 -7
  117. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +59 -1
  118. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +69 -26
  119. package/crates/team-agent/src/lifecycle/tests.rs +30 -13
  120. package/crates/team-agent/src/lifecycle/types.rs +61 -0
  121. package/crates/team-agent/src/lifecycle/worker_command_context.rs +143 -12
  122. package/crates/team-agent/src/mcp_server/tests/scoped.rs +100 -1
  123. package/crates/team-agent/src/mcp_server/tests.rs +196 -2
  124. package/crates/team-agent/src/messaging/delivery.rs +430 -41
  125. package/crates/team-agent/src/messaging/helpers.rs +2 -0
  126. package/crates/team-agent/src/messaging/leader_receiver.rs +53 -1
  127. package/crates/team-agent/src/messaging/results.rs +4 -0
  128. package/crates/team-agent/src/messaging/send.rs +34 -0
  129. package/crates/team-agent/src/messaging/tests/dup_inject.rs +406 -0
  130. package/crates/team-agent/src/messaging/tests/e23.rs +9 -0
  131. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +66 -0
  132. package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
  133. package/crates/team-agent/src/messaging/types.rs +4 -1
  134. package/crates/team-agent/src/model/enums.rs +17 -6
  135. package/crates/team-agent/src/model/permissions.rs +3 -0
  136. package/crates/team-agent/src/os_probe.rs +73 -2
  137. package/crates/team-agent/src/provider/adapter.rs +259 -6
  138. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +89 -0
  139. package/crates/team-agent/src/provider/adapters/grok.rs +157 -0
  140. package/crates/team-agent/src/provider/adapters/mod.rs +3 -0
  141. package/crates/team-agent/src/provider/adapters/pi.rs +152 -0
  142. package/crates/team-agent/src/provider/bypass_flags.rs +47 -2
  143. package/crates/team-agent/src/provider/classify.rs +4 -2
  144. package/crates/team-agent/src/provider/faults.rs +2 -1
  145. package/crates/team-agent/src/provider/mod.rs +11 -0
  146. package/crates/team-agent/src/provider/session/capture.rs +158 -20
  147. package/crates/team-agent/src/provider/session/context_fork/claude.rs +38 -1
  148. package/crates/team-agent/src/provider/session/context_fork/codex.rs +72 -1
  149. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +57 -1
  150. package/crates/team-agent/src/provider/session/context_fork.rs +78 -3
  151. package/crates/team-agent/src/provider/session/mod.rs +18 -0
  152. package/crates/team-agent/src/provider/session/resume.rs +57 -0
  153. package/crates/team-agent/src/provider/session_scan/claude.rs +125 -1
  154. package/crates/team-agent/src/provider/session_scan/codex.rs +94 -1
  155. package/crates/team-agent/src/provider/session_scan/common.rs +205 -2
  156. package/crates/team-agent/src/provider/session_scan/copilot.rs +33 -1
  157. package/crates/team-agent/src/provider/session_scan/cursor.rs +538 -0
  158. package/crates/team-agent/src/provider/session_scan/grok.rs +288 -0
  159. package/crates/team-agent/src/provider/session_scan/pi.rs +172 -0
  160. package/crates/team-agent/src/provider/session_scan.rs +12 -0
  161. package/crates/team-agent/src/provider/submit_now.rs +94 -0
  162. package/crates/team-agent/src/provider/tests/adapter.rs +384 -0
  163. package/crates/team-agent/src/provider/types.rs +4 -2
  164. package/crates/team-agent/src/provider/wire.rs +29 -1
  165. package/crates/team-agent/src/state/persist.rs +301 -8
  166. package/crates/team-agent/src/state/repository.rs +5 -0
  167. package/crates/team-agent/src/tmux_backend/tests.rs +1737 -42
  168. package/crates/team-agent/src/tmux_backend.rs +1116 -75
  169. package/crates/team-agent/src/transport/tests/wire.rs +28 -2
  170. package/crates/team-agent/src/transport.rs +248 -1
  171. package/npm/install.mjs +129 -73
  172. package/package.json +4 -4
  173. package/skills/team-agent/SKILL.md +32 -239
  174. package/skills/team-agent/command-coverage.json +31 -0
  175. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +0 -59
  176. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +0 -488
  177. package/crates/team-agent/src/lifecycle/launch/fork_pending.rs +0 -109
  178. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +0 -447
@@ -0,0 +1,128 @@
1
+ use std::path::Path;
2
+
3
+ use crate::model::enums::ProviderEffort;
4
+ use crate::provider::adapters::pi::{build_pi_command_argv, PiCommandRequest, PiSessionSelector};
5
+
6
+ fn request(session: PiSessionSelector<'static>) -> PiCommandRequest<'static> {
7
+ PiCommandRequest {
8
+ executable: Path::new("/verified/pi"),
9
+ extension: Path::new("/workspace/.team/runtime/pi/team-a/worker-a/team-mcp.ts"),
10
+ model: "team-agent/qwen3.8-27b",
11
+ effort: ProviderEffort::High,
12
+ system_prompt: "frozen worker prompt",
13
+ tool_categories: &["mcp_team", "fs_read", "fs_write", "execute_bash"],
14
+ session_dir: Path::new("/workspace/.team/runtime/pi/team-a/worker-a/sessions"),
15
+ session,
16
+ agent_id: "worker-a",
17
+ }
18
+ }
19
+
20
+ #[test]
21
+ fn pi_command_is_exact_regular_tui_and_has_no_ambient_flags() {
22
+ let fresh = build_pi_command_argv(request(PiSessionSelector::Fresh {
23
+ session_id: "da8c3622-2378-4d05-a26c-e826a6ef6d63",
24
+ }))
25
+ .expect("fresh Pi command");
26
+ assert_eq!(
27
+ fresh,
28
+ [
29
+ "/verified/pi",
30
+ "--no-extensions",
31
+ "-e",
32
+ "/workspace/.team/runtime/pi/team-a/worker-a/team-mcp.ts",
33
+ "--no-approve",
34
+ "--no-context-files",
35
+ "--no-skills",
36
+ "--no-prompt-templates",
37
+ "--tui-mode",
38
+ "regular",
39
+ "--model",
40
+ "team-agent/qwen3.8-27b",
41
+ "--thinking",
42
+ "high",
43
+ "--system-prompt",
44
+ "frozen worker prompt",
45
+ "--tools",
46
+ "bash,edit,mcp,read,write",
47
+ "--session-dir",
48
+ "/workspace/.team/runtime/pi/team-a/worker-a/sessions",
49
+ "--session-id",
50
+ "da8c3622-2378-4d05-a26c-e826a6ef6d63",
51
+ "--name",
52
+ "worker-a",
53
+ ]
54
+ .map(str::to_string)
55
+ );
56
+
57
+ let resume = build_pi_command_argv(request(PiSessionSelector::Resume {
58
+ path: Path::new(
59
+ "/workspace/.team/runtime/pi/team-a/worker-a/sessions/2026/08/session.jsonl",
60
+ ),
61
+ }))
62
+ .expect("resume Pi command");
63
+ assert!(resume.windows(2).any(|pair| {
64
+ pair == [
65
+ "--session",
66
+ "/workspace/.team/runtime/pi/team-a/worker-a/sessions/2026/08/session.jsonl",
67
+ ]
68
+ }));
69
+ assert!(!resume.iter().any(|arg| arg == "--session-id"));
70
+
71
+ for argv in [&fresh, &resume] {
72
+ for forbidden in [
73
+ "rpc",
74
+ "--mode",
75
+ "--print",
76
+ "-p",
77
+ "-r",
78
+ "--resume",
79
+ "-c",
80
+ "--continue",
81
+ "--mcp-config",
82
+ "--provider",
83
+ "--fork",
84
+ "--workspace",
85
+ "--cwd",
86
+ "--rules",
87
+ "--always-approve",
88
+ "--force",
89
+ "--trust",
90
+ "--sandbox",
91
+ ] {
92
+ assert!(
93
+ !argv.iter().any(|arg| arg == forbidden),
94
+ "materializer-owned Pi argv must not inherit {forbidden}: {argv:?}"
95
+ );
96
+ }
97
+ }
98
+ }
99
+
100
+ #[test]
101
+ fn pi_command_uses_no_context_no_extensions_no_approve() {
102
+ let argv = build_pi_command_argv(request(PiSessionSelector::Fresh {
103
+ session_id: "76f682b1-ecc3-4586-850f-ab2e2bb04cb3",
104
+ }))
105
+ .expect("fresh Pi command");
106
+
107
+ for required in [
108
+ "--no-context-files",
109
+ "--no-extensions",
110
+ "--no-approve",
111
+ "--no-skills",
112
+ "--no-prompt-templates",
113
+ ] {
114
+ assert_eq!(
115
+ argv.iter().filter(|arg| arg.as_str() == required).count(),
116
+ 1,
117
+ "required isolation flag must occur once: {required}; argv={argv:?}"
118
+ );
119
+ }
120
+ assert!(argv.windows(2).any(|pair| {
121
+ pair == [
122
+ "-e",
123
+ "/workspace/.team/runtime/pi/team-a/worker-a/team-mcp.ts",
124
+ ]
125
+ }));
126
+ assert_eq!(argv.iter().filter(|arg| arg.as_str() == "-e").count(), 1);
127
+ assert!(!argv.iter().any(|arg| arg == "--approve"));
128
+ }
@@ -0,0 +1,207 @@
1
+ use std::path::Path;
2
+ use std::sync::atomic::{AtomicU32, Ordering};
3
+
4
+ use crate::compiler::{compile_role_agent, CompiledRole};
5
+ use crate::lifecycle::launch::pi_mcp::{parse_pi_list_models_table, select_exact_pi_model};
6
+ use crate::model::enums::ProviderEffort;
7
+ use crate::model::yaml::Value;
8
+ use crate::model::ModelError;
9
+ use crate::provider::adapters::pi::{
10
+ build_pi_command_argv, pi_tool_mapping, PiCommandRequest, PiSessionSelector, PiToolMapping,
11
+ };
12
+
13
+ const CATALOG: &[u8] = include_bytes!("fixtures/pi_list_models_g0.stdout.txt");
14
+ static NEXT_ROLE: AtomicU32 = AtomicU32::new(0);
15
+
16
+ fn compile_pi_role(front_matter: &str) -> Result<CompiledRole, ModelError> {
17
+ let seq = NEXT_ROLE.fetch_add(1, Ordering::Relaxed);
18
+ let root = std::env::temp_dir().join(format!(
19
+ "team-agent-pi-compiler-{}-{seq}",
20
+ std::process::id()
21
+ ));
22
+ std::fs::create_dir_all(&root).expect("create role fixture root");
23
+ let role = root.join("pi-worker.md");
24
+ std::fs::write(
25
+ &role,
26
+ format!("---\n{front_matter}\n---\nPi worker contract.\n"),
27
+ )
28
+ .expect("write role fixture");
29
+ let result = compile_role_agent(&role, &Value::Map(Vec::new()), "/workspace");
30
+ std::fs::remove_dir_all(root).expect("remove role fixture root");
31
+ result
32
+ }
33
+
34
+ fn valid_role_with(extra: &str) -> String {
35
+ format!(
36
+ "name: pi-worker\nrole: developer\nprovider: pi\nmodel: team-agent/qwen3.8-27b\nauth_mode: subscription\neffort: medium\ntools:\n - mcp_team\n - fs_read\n - fs_list\n - fs_write\n - execute_bash\ndangerously_skip_permissions: true\n{extra}"
37
+ )
38
+ }
39
+
40
+ fn compile_error(result: Result<CompiledRole, ModelError>) -> ModelError {
41
+ match result {
42
+ Err(error) => error,
43
+ Ok(_) => panic!("incomplete Pi role must refuse"),
44
+ }
45
+ }
46
+
47
+ fn command(tools: &[&str], effort: ProviderEffort, model: &str) -> Result<Vec<String>, String> {
48
+ build_pi_command_argv(PiCommandRequest {
49
+ executable: Path::new("/verified/pi"),
50
+ extension: Path::new("/workspace/.team/runtime/pi/t1/pi-worker/team-mcp.ts"),
51
+ model,
52
+ effort,
53
+ system_prompt: "Pi worker contract.",
54
+ tool_categories: tools,
55
+ session_dir: Path::new("/workspace/.team/runtime/pi/t1/pi-worker/sessions"),
56
+ session: PiSessionSelector::Fresh {
57
+ session_id: "c5ddf218-24a5-4e74-960c-ab6606ea7e8c",
58
+ },
59
+ agent_id: "pi-worker",
60
+ })
61
+ .map_err(|error| error.to_string())
62
+ }
63
+
64
+ #[test]
65
+ fn pi_role_requires_qualified_model_explicit_effort_and_mcp_team() {
66
+ let positive = compile_pi_role(&valid_role_with(""));
67
+ assert!(positive.is_ok(), "fully explicit Pi role must compile");
68
+
69
+ let missing_model = valid_role_with("").replace("model: team-agent/qwen3.8-27b\n", "");
70
+ let missing_effort = valid_role_with("").replace("effort: medium\n", "");
71
+ let missing_mcp = valid_role_with("").replace(" - mcp_team\n", "");
72
+ let unqualified =
73
+ valid_role_with("").replace("model: team-agent/qwen3.8-27b", "model: qwen3.8-27b");
74
+
75
+ for (label, role) in [
76
+ ("model", missing_model),
77
+ ("effort", missing_effort),
78
+ ("mcp_team", missing_mcp),
79
+ ("qualified exact model", unqualified),
80
+ ] {
81
+ let error = compile_error(compile_pi_role(&role));
82
+ assert!(
83
+ error.to_string().to_ascii_lowercase().contains(label),
84
+ "error must name {label}; got {error}"
85
+ );
86
+ }
87
+
88
+ let models = parse_pi_list_models_table(CATALOG).expect("catalog fixture");
89
+ assert!(select_exact_pi_model(&models, "team-agent/qwen3.8-27b").is_ok());
90
+ assert!(select_exact_pi_model(&models, "foo/bar").is_err());
91
+ }
92
+
93
+ #[test]
94
+ fn pi_role_requires_intrinsic_unrestricted_ack_true() {
95
+ let compiled = compile_pi_role(&valid_role_with("")).expect("true acknowledgement");
96
+ assert_eq!(
97
+ compiled.agent.get("dangerously_skip_permissions"),
98
+ Some(&Value::Bool(true))
99
+ );
100
+
101
+ let argv = command(
102
+ &["mcp_team"],
103
+ ProviderEffort::Medium,
104
+ "team-agent/qwen3.8-27b",
105
+ )
106
+ .expect("true acknowledgement maps to intrinsic unrestricted mode");
107
+ for forbidden in [
108
+ "--approve",
109
+ "--always-approve",
110
+ "--force",
111
+ "--trust",
112
+ "--sandbox",
113
+ "dangerous_auto_approve",
114
+ ] {
115
+ assert!(
116
+ !argv.iter().any(|arg| arg == forbidden),
117
+ "Pi acknowledgement must not fabricate a bypass flag {forbidden}: {argv:?}"
118
+ );
119
+ }
120
+
121
+ let false_ack = valid_role_with("").replace(
122
+ "dangerously_skip_permissions: true",
123
+ "dangerously_skip_permissions: false",
124
+ );
125
+ let error = compile_error(compile_pi_role(&false_ack));
126
+ let text = error.to_string();
127
+ assert!(
128
+ text.contains("dangerously_skip_permissions") && text.contains("true"),
129
+ "got {text}"
130
+ );
131
+ }
132
+
133
+ #[test]
134
+ fn pi_max_effort_is_supported_without_model_suffix() {
135
+ let max_role = valid_role_with("").replace("effort: medium", "effort: max");
136
+ compile_pi_role(&max_role).expect("Pi supports explicit max effort");
137
+
138
+ let argv =
139
+ command(&["mcp_team"], ProviderEffort::Max, "team-agent/qwen3.8-27b").expect("max Pi argv");
140
+ assert!(
141
+ argv.windows(2).any(|pair| pair == ["--thinking", "max"]),
142
+ "max must map to --thinking max: {argv:?}"
143
+ );
144
+ assert!(
145
+ argv.windows(2)
146
+ .any(|pair| pair == ["--model", "team-agent/qwen3.8-27b"]),
147
+ "model must remain exact and unsuffixed: {argv:?}"
148
+ );
149
+ assert!(!argv.iter().any(|arg| arg.ends_with(":max")));
150
+
151
+ let models = parse_pi_list_models_table(CATALOG).expect("catalog fixture");
152
+ assert!(select_exact_pi_model(&models, "team-agent/qwen3.8-27b:max").is_err());
153
+ }
154
+
155
+ #[test]
156
+ fn pi_tools_allowlist_keeps_mcp_and_rejects_unknown_categories() {
157
+ assert_eq!(pi_tool_mapping("mcp_team"), PiToolMapping::Mcp);
158
+ assert_eq!(
159
+ pi_tool_mapping("fs_read"),
160
+ PiToolMapping::Builtin(&["read"])
161
+ );
162
+ assert_eq!(
163
+ pi_tool_mapping("fs_list"),
164
+ PiToolMapping::Builtin(&["grep", "find", "ls"])
165
+ );
166
+ assert_eq!(
167
+ pi_tool_mapping("fs_write"),
168
+ PiToolMapping::Builtin(&["edit", "write"])
169
+ );
170
+ assert_eq!(
171
+ pi_tool_mapping("execute_bash"),
172
+ PiToolMapping::Builtin(&["bash"])
173
+ );
174
+
175
+ for unsupported in ["git_diff", "network", "provider_builtin", "not_a_tool"] {
176
+ assert_eq!(
177
+ pi_tool_mapping(unsupported),
178
+ PiToolMapping::Unsupported,
179
+ "unknown categories must not silently broaden to bash"
180
+ );
181
+ assert!(
182
+ command(
183
+ &["mcp_team", unsupported],
184
+ ProviderEffort::Medium,
185
+ "team-agent/qwen3.8-27b"
186
+ )
187
+ .is_err(),
188
+ "unsupported tool must refuse the command: {unsupported}"
189
+ );
190
+ }
191
+
192
+ let argv = command(
193
+ &["mcp_team", "fs_write", "fs_read", "fs_list", "execute_bash"],
194
+ ProviderEffort::Medium,
195
+ "team-agent/qwen3.8-27b",
196
+ )
197
+ .expect("known allowlist");
198
+ let tools = argv
199
+ .windows(2)
200
+ .find(|pair| pair[0] == "--tools")
201
+ .map(|pair| pair[1].as_str())
202
+ .expect("--tools value");
203
+ assert_eq!(
204
+ tools, "bash,edit,find,grep,ls,mcp,read,write",
205
+ "tools must be sorted and deduplicated"
206
+ );
207
+ }