@team-agent/installer 0.5.42 → 0.5.44

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 (156) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +129 -54
  4. package/crates/team-agent/src/cli/diagnose.rs +1 -2
  5. package/crates/team-agent/src/cli/emit.rs +1 -7
  6. package/crates/team-agent/src/cli/helpers.rs +3 -1
  7. package/crates/team-agent/src/cli/leader.rs +2 -1
  8. package/crates/team-agent/src/cli/mod.rs +126 -16
  9. package/crates/team-agent/src/cli/named_address.rs +14 -5
  10. package/crates/team-agent/src/cli/profile.rs +19 -7
  11. package/crates/team-agent/src/cli/send.rs +9 -3
  12. package/crates/team-agent/src/cli/status.rs +8 -30
  13. package/crates/team-agent/src/cli/status_port.rs +1341 -1272
  14. package/crates/team-agent/src/cli/tests/base.rs +738 -660
  15. package/crates/team-agent/src/cli/tests/compile.rs +45 -18
  16. package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
  21. package/crates/team-agent/src/cli/tests/mod.rs +17 -7
  22. package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
  23. package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
  25. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
  26. package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
  27. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
  28. package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
  29. package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
  30. package/crates/team-agent/src/codex_app_server.rs +2 -5
  31. package/crates/team-agent/src/compiler/tests.rs +139 -33
  32. package/crates/team-agent/src/compiler.rs +55 -22
  33. package/crates/team-agent/src/conpty/backend.rs +23 -33
  34. package/crates/team-agent/src/coordinator/backoff.rs +2 -7
  35. package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
  36. package/crates/team-agent/src/coordinator/health.rs +46 -31
  37. package/crates/team-agent/src/coordinator/mod.rs +3 -3
  38. package/crates/team-agent/src/coordinator/orphan.rs +22 -10
  39. package/crates/team-agent/src/coordinator/steps/abnormal.rs +51 -56
  40. package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
  41. package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
  42. package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
  43. package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
  44. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
  45. package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
  46. package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
  47. package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
  48. package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
  49. package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
  50. package/crates/team-agent/src/db/message_store.rs +138 -30
  51. package/crates/team-agent/src/db/migration.rs +249 -61
  52. package/crates/team-agent/src/db/schema.rs +303 -82
  53. package/crates/team-agent/src/diagnose/comms.rs +9 -2
  54. package/crates/team-agent/src/diagnose/mod.rs +1 -3
  55. package/crates/team-agent/src/diagnose/orphans.rs +79 -61
  56. package/crates/team-agent/src/event_log.rs +70 -16
  57. package/crates/team-agent/src/layout/manager.rs +15 -4
  58. package/crates/team-agent/src/layout/mod.rs +4 -4
  59. package/crates/team-agent/src/layout/overlay.rs +10 -3
  60. package/crates/team-agent/src/layout/placement.rs +5 -1
  61. package/crates/team-agent/src/layout/recovery.rs +4 -2
  62. package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
  63. package/crates/team-agent/src/layout/sessions.rs +17 -9
  64. package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
  65. package/crates/team-agent/src/layout/worker_env.rs +87 -19
  66. package/crates/team-agent/src/leader/helpers.rs +7 -1
  67. package/crates/team-agent/src/leader/lease.rs +199 -89
  68. package/crates/team-agent/src/leader/owner_bind.rs +55 -22
  69. package/crates/team-agent/src/leader/provider_attribution.rs +25 -6
  70. package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
  71. package/crates/team-agent/src/leader/rediscover.rs +74 -25
  72. package/crates/team-agent/src/leader/registry.rs +1 -1
  73. package/crates/team-agent/src/leader/start.rs +75 -54
  74. package/crates/team-agent/src/leader/takeover.rs +46 -11
  75. package/crates/team-agent/src/leader/tests/basics.rs +320 -167
  76. package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
  77. package/crates/team-agent/src/leader/tests/identity.rs +428 -356
  78. package/crates/team-agent/src/leader/tests/idle.rs +285 -254
  79. package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
  80. package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
  81. package/crates/team-agent/src/leader/tests/mod.rs +115 -99
  82. package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
  83. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
  84. package/crates/team-agent/src/lib.rs +4 -4
  85. package/crates/team-agent/src/lifecycle/display.rs +7 -3
  86. package/crates/team-agent/src/lifecycle/launch.rs +55 -15
  87. package/crates/team-agent/src/lifecycle/mod.rs +9 -1
  88. package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
  89. package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
  90. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
  91. package/crates/team-agent/src/lifecycle/restart/common.rs +6 -2
  92. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
  93. package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
  94. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
  95. package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
  96. package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
  97. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
  98. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
  99. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
  100. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
  101. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
  102. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +3 -0
  103. package/crates/team-agent/src/lifecycle/tests.rs +2 -2
  104. package/crates/team-agent/src/main.rs +4 -4
  105. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
  106. package/crates/team-agent/src/mcp_server/mod.rs +11 -2
  107. package/crates/team-agent/src/mcp_server/types.rs +14 -3
  108. package/crates/team-agent/src/mcp_server/wire.rs +230 -68
  109. package/crates/team-agent/src/messaging/delivery.rs +20 -18
  110. package/crates/team-agent/src/messaging/helpers.rs +25 -4
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
  112. package/crates/team-agent/src/messaging/mod.rs +2 -3
  113. package/crates/team-agent/src/messaging/selftest.rs +46 -26
  114. package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
  115. package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
  116. package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
  117. package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
  118. package/crates/team-agent/src/messaging/trust.rs +25 -2
  119. package/crates/team-agent/src/messaging/watchers.rs +37 -9
  120. package/crates/team-agent/src/model/enums.rs +65 -16
  121. package/crates/team-agent/src/model/ids.rs +12 -3
  122. package/crates/team-agent/src/model/paths.rs +28 -7
  123. package/crates/team-agent/src/model/permissions.rs +176 -33
  124. package/crates/team-agent/src/model/routing.rs +66 -20
  125. package/crates/team-agent/src/model/spec.rs +365 -69
  126. package/crates/team-agent/src/model/task_graph.rs +36 -9
  127. package/crates/team-agent/src/model/yaml/tests.rs +24 -6
  128. package/crates/team-agent/src/model/yaml.rs +7 -6
  129. package/crates/team-agent/src/packaging/install.rs +23 -9
  130. package/crates/team-agent/src/packaging/migrate.rs +5 -7
  131. package/crates/team-agent/src/packaging/mod.rs +9 -1
  132. package/crates/team-agent/src/packaging/repair.rs +13 -6
  133. package/crates/team-agent/src/packaging/tests.rs +63 -16
  134. package/crates/team-agent/src/packaging/types.rs +22 -7
  135. package/crates/team-agent/src/platform/argv.rs +4 -1
  136. package/crates/team-agent/src/platform/file_lock.rs +22 -8
  137. package/crates/team-agent/src/platform/process.rs +54 -24
  138. package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
  139. package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
  140. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
  141. package/crates/team-agent/src/provider/classify.rs +127 -42
  142. package/crates/team-agent/src/provider/faults.rs +17 -5
  143. package/crates/team-agent/src/provider/helpers.rs +6 -5
  144. package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
  145. package/crates/team-agent/src/state/persist.rs +28 -0
  146. package/crates/team-agent/src/state/repository.rs +8 -3
  147. package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
  148. package/crates/team-agent/src/tmux_backend.rs +80 -44
  149. package/crates/team-agent/src/topology.rs +40 -20
  150. package/crates/team-agent/src/transport/test_support.rs +20 -23
  151. package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
  152. package/crates/team-agent/src/transport/tests/mod.rs +178 -187
  153. package/crates/team-agent/src/transport/tests/wire.rs +561 -525
  154. package/crates/team-agent/src/transport.rs +12 -17
  155. package/crates/team-agent/src/transport_factory.rs +29 -14
  156. package/package.json +4 -4
@@ -37,35 +37,35 @@ impl Drop for WorkspaceCleanup {
37
37
  }
38
38
  }
39
39
 
40
- // =========================================================================
41
- // WAVE-2 NON-SUB CHECKPOINT — missing CLI subcommands (ABSENT from cli/emit.rs dispatch).
42
- // emit.rs `dispatch` has NO arm for sessions, peek, collect, e2e, diagnose,
43
- // preflight, wait-ready -> they fall to `_ => Ok(ExitCode::Error)`. These REDs
44
- // assert the dispatch ROUTES each subcommand: `run([sub,...]) == ExitCode::Ok` for a golden
45
- // EXIT-0 scenario (today unrouted -> ExitCode::Error -> RED; green once the porter adds the
46
- // dispatch arm + handler). Golden exit codes + JSON shapes probed via `python3 -m team_agent <sub>`.
47
- //
48
- // OBSERVABILITY NOTE: `run()` exposes only ExitCode (Ok=0/Error=1); it prints via println!, which
49
- // libtest intercepts (thread-local capture), so an fd-level stdout byte-capture is unreliable
50
- // under `cargo test`. The exact golden --json byte-shape is therefore LOCKED in each doc-comment
51
- // as the porter's parity obligation; the in-process assertion is the routing (exit code). A
52
- // follow-up can byte-lock output once each handler exists as a callable `cmd_*`/`*_port` symbol.
53
- //
54
- // Only golden-EXIT-0, tmux-SAFE scenarios make a clean RED (an exit-1 scenario's Error is
55
- // indistinguishable from the unknown-subcommand Error -> false-green, forbidden). preflight/
56
- // wait-ready/e2e/peek cannot reach golden-exit-0 on CI without real tmux/providers/a live team,
57
- // so they are #[ignore] real-effect seams (documented shape), NOT false-green exit-1 asserts.
58
- // =========================================================================
40
+ // =========================================================================
41
+ // WAVE-2 NON-SUB CHECKPOINT — missing CLI subcommands (ABSENT from cli/emit.rs dispatch).
42
+ // emit.rs `dispatch` has NO arm for sessions, peek, collect, e2e, diagnose,
43
+ // preflight, wait-ready -> they fall to `_ => Ok(ExitCode::Error)`. These REDs
44
+ // assert the dispatch ROUTES each subcommand: `run([sub,...]) == ExitCode::Ok` for a golden
45
+ // EXIT-0 scenario (today unrouted -> ExitCode::Error -> RED; green once the porter adds the
46
+ // dispatch arm + handler). Golden exit codes + JSON shapes probed via `python3 -m team_agent <sub>`.
47
+ //
48
+ // OBSERVABILITY NOTE: `run()` exposes only ExitCode (Ok=0/Error=1); it prints via println!, which
49
+ // libtest intercepts (thread-local capture), so an fd-level stdout byte-capture is unreliable
50
+ // under `cargo test`. The exact golden --json byte-shape is therefore LOCKED in each doc-comment
51
+ // as the porter's parity obligation; the in-process assertion is the routing (exit code). A
52
+ // follow-up can byte-lock output once each handler exists as a callable `cmd_*`/`*_port` symbol.
53
+ //
54
+ // Only golden-EXIT-0, tmux-SAFE scenarios make a clean RED (an exit-1 scenario's Error is
55
+ // indistinguishable from the unknown-subcommand Error -> false-green, forbidden). preflight/
56
+ // wait-ready/e2e/peek cannot reach golden-exit-0 on CI without real tmux/providers/a live team,
57
+ // so they are #[ignore] real-effect seams (documented shape), NOT false-green exit-1 asserts.
58
+ // =========================================================================
59
59
 
60
- fn cli_argv(items: &[&str]) -> Vec<String> {
61
- items.iter().map(|s| (*s).to_string()).collect()
62
- }
60
+ fn cli_argv(items: &[&str]) -> Vec<String> {
61
+ items.iter().map(|s| (*s).to_string()).collect()
62
+ }
63
63
 
64
- /// The exact golden fake spec (cli/e2e.py `_fake_spec`, dumped via simple_yaml) with the literal
65
- /// `/WS` placeholder substituted to the real workspace — the minimal VALID team.spec.yaml that
66
- /// `collect` accepts. (load_spec rejects partial specs: requires
67
- /// communication/context/leader/routing/runtime/tasks.)
68
- const FAKE_SPEC_YAML: &str = r#"version: 1
64
+ /// The exact golden fake spec (cli/e2e.py `_fake_spec`, dumped via simple_yaml) with the literal
65
+ /// `/WS` placeholder substituted to the real workspace — the minimal VALID team.spec.yaml that
66
+ /// `collect` accepts. (load_spec rejects partial specs: requires
67
+ /// communication/context/leader/routing/runtime/tasks.)
68
+ const FAKE_SPEC_YAML: &str = r#"version: 1
69
69
  team:
70
70
  name: "fake-e2e"
71
71
  mode: "supervisor_worker"
@@ -163,100 +163,106 @@ tasks:
163
163
  risk: "low"
164
164
  "#;
165
165
 
166
- fn seed_team_spec(ws: &std::path::Path) {
167
- let spec = FAKE_SPEC_YAML.replace("/WS", &ws.to_string_lossy());
168
- std::fs::write(ws.join("team.spec.yaml"), spec).unwrap();
169
- }
166
+ fn seed_team_spec(ws: &std::path::Path) {
167
+ let spec = FAKE_SPEC_YAML.replace("/WS", &ws.to_string_lossy());
168
+ std::fs::write(ws.join("team.spec.yaml"), spec).unwrap();
169
+ }
170
170
 
171
- fn seed_collect_runtime_state(ws: &std::path::Path) {
172
- crate::state::persist::save_runtime_state(
173
- ws,
174
- &json!({
175
- "active_team_key": "fake-e2e",
176
- "team_dir": ws.to_string_lossy().to_string(),
177
- "spec_path": ws.join("team.spec.yaml").to_string_lossy().to_string(),
178
- "session_name": "team-agent-fake-e2e",
179
- "leader": {"id": "leader"},
180
- "agents": {
181
- "fake_impl": {
182
- "status": "running",
183
- "provider": "fake",
184
- "role": "implementation_engineer",
185
- "window": "fake_impl",
186
- "owner_team_id": "fake-e2e"
187
- }
188
- },
189
- "tasks": [{
190
- "id": "task_impl",
191
- "title": "Fake implementation",
192
- "type": "implementation",
193
- "status": "pending",
194
- "assignee": "fake_impl"
195
- }]
196
- }),
197
- )
198
- .unwrap();
199
- }
171
+ fn seed_collect_runtime_state(ws: &std::path::Path) {
172
+ crate::state::persist::save_runtime_state(
173
+ ws,
174
+ &json!({
175
+ "active_team_key": "fake-e2e",
176
+ "team_dir": ws.to_string_lossy().to_string(),
177
+ "spec_path": ws.join("team.spec.yaml").to_string_lossy().to_string(),
178
+ "session_name": "team-agent-fake-e2e",
179
+ "leader": {"id": "leader"},
180
+ "agents": {
181
+ "fake_impl": {
182
+ "status": "running",
183
+ "provider": "fake",
184
+ "role": "implementation_engineer",
185
+ "window": "fake_impl",
186
+ "owner_team_id": "fake-e2e"
187
+ }
188
+ },
189
+ "tasks": [{
190
+ "id": "task_impl",
191
+ "title": "Fake implementation",
192
+ "type": "implementation",
193
+ "status": "pending",
194
+ "assignee": "fake_impl"
195
+ }]
196
+ }),
197
+ )
198
+ .unwrap();
199
+ }
200
200
 
201
- // ── sessions ── golden cli/parser.py:230 `cmd_sessions` -> runtime.sessions(ws). EXIT 0.
202
- // `team-agent sessions --workspace <ws> --json` on an empty ws ->
203
- // {"ok":true,"sessions":[],"workspace":"<ws>"} (--json sort_keys). RED: unrouted -> Error.
204
- #[test]
205
- fn dispatch_routes_sessions_subcommand() {
206
- let ws = tmp_workspace();
207
- let code = run(&cli_argv(&["sessions", "--workspace", &ws.to_string_lossy(), "--json"]), &ws);
208
- assert_eq!(
201
+ // ── sessions ── golden cli/parser.py:230 `cmd_sessions` -> runtime.sessions(ws). EXIT 0.
202
+ // `team-agent sessions --workspace <ws> --json` on an empty ws ->
203
+ // {"ok":true,"sessions":[],"workspace":"<ws>"} (--json sort_keys). RED: unrouted -> Error.
204
+ #[test]
205
+ fn dispatch_routes_sessions_subcommand() {
206
+ let ws = tmp_workspace();
207
+ let code = run(
208
+ &cli_argv(&["sessions", "--workspace", &ws.to_string_lossy(), "--json"]),
209
+ &ws,
210
+ );
211
+ assert_eq!(
209
212
  code,
210
213
  ExitCode::Ok,
211
214
  "`sessions` must ROUTE to cmd_sessions (golden parser.py:230, exit 0 {{ok,sessions,workspace}}); \
212
215
  today it falls to the unknown-subcommand arm (emit.rs:77) -> Error"
213
216
  );
214
- let _ = std::fs::remove_dir_all(&ws);
215
- }
217
+ let _ = std::fs::remove_dir_all(&ws);
218
+ }
216
219
 
217
- // ── collect ── golden parser.py:292 `cmd_collect` -> runtime.collect(ws). With a valid
218
- // team.spec.yaml present and nothing to collect -> EXIT 0, golden:
219
- // {"collected":[],"collected_results":[],"coordinator":{"ok":false,"status":"not_required"},
220
- // "delivered_messages":[],"invalid_results":[],"ok":true,"results":{...},"state_file":"<ws>/team_state.md"}
221
- // RED: unrouted -> Error.
222
- #[test]
223
- #[serial(env)]
224
- fn dispatch_routes_collect_with_spec() {
225
- let _env = EnvUnsetGuard::unset(&[
226
- "TEAM_AGENT_WORKSPACE",
227
- "TEAM_AGENT_TEAM_ID",
228
- "TEAM_AGENT_OWNER_TEAM_ID",
229
- "TEAM_AGENT_ACTIVE_TEAM",
230
- "TEAM_AGENT_ID",
231
- "TEAM_AGENT_LEADER_PANE_ID",
232
- "TEAM_AGENT_LEADER_SESSION_UUID",
233
- "TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
234
- "TEAM_AGENT_LEADER_PROVIDER",
235
- "TMUX",
236
- "TMUX_PANE",
237
- ]);
238
- let ws = tmp_workspace();
239
- let _cleanup = WorkspaceCleanup(ws.clone());
240
- seed_team_spec(&ws);
241
- seed_collect_runtime_state(&ws);
242
- let code = run(&cli_argv(&["collect", "--workspace", &ws.to_string_lossy(), "--json"]), &ws);
243
- assert_eq!(
220
+ // ── collect ── golden parser.py:292 `cmd_collect` -> runtime.collect(ws). With a valid
221
+ // team.spec.yaml present and nothing to collect -> EXIT 0, golden:
222
+ // {"collected":[],"collected_results":[],"coordinator":{"ok":false,"status":"not_required"},
223
+ // "delivered_messages":[],"invalid_results":[],"ok":true,"results":{...},"state_file":"<ws>/team_state.md"}
224
+ // RED: unrouted -> Error.
225
+ #[test]
226
+ #[serial(env)]
227
+ fn dispatch_routes_collect_with_spec() {
228
+ let _env = EnvUnsetGuard::unset(&[
229
+ "TEAM_AGENT_WORKSPACE",
230
+ "TEAM_AGENT_TEAM_ID",
231
+ "TEAM_AGENT_OWNER_TEAM_ID",
232
+ "TEAM_AGENT_ACTIVE_TEAM",
233
+ "TEAM_AGENT_ID",
234
+ "TEAM_AGENT_LEADER_PANE_ID",
235
+ "TEAM_AGENT_LEADER_SESSION_UUID",
236
+ "TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
237
+ "TEAM_AGENT_LEADER_PROVIDER",
238
+ "TMUX",
239
+ "TMUX_PANE",
240
+ ]);
241
+ let ws = tmp_workspace();
242
+ let _cleanup = WorkspaceCleanup(ws.clone());
243
+ seed_team_spec(&ws);
244
+ seed_collect_runtime_state(&ws);
245
+ let code = run(
246
+ &cli_argv(&["collect", "--workspace", &ws.to_string_lossy(), "--json"]),
247
+ &ws,
248
+ );
249
+ assert_eq!(
244
250
  code,
245
251
  ExitCode::Ok,
246
252
  "`collect` must ROUTE to cmd_collect (parser.py:292); with a valid spec golden exits 0 \
247
253
  {{collected,collected_results,coordinator,delivered_messages,invalid_results,ok,results,state_file}}; \
248
254
  today -> unknown-subcommand Error"
249
255
  );
250
- }
256
+ }
251
257
 
252
- // ── diagnose ── golden parser.py:298 `cmd_diagnose` -> runtime.diagnose(ws) (diagnose/health.py:19).
253
- // ok:true only when there are zero issues. Seed leader_receiver=attached + NO session_name + NO
254
- // agents -> issues=[] -> EXIT 0, golden top keys (sort): event_log,issues,ok,runtime,suggested_repairs.
255
- // RED: unrouted -> Error.
256
- #[test]
257
- fn dispatch_routes_diagnose_healthy_leader() {
258
- let ws = tmp_workspace();
259
- std::fs::write(
258
+ // ── diagnose ── golden parser.py:298 `cmd_diagnose` -> runtime.diagnose(ws) (diagnose/health.py:19).
259
+ // ok:true only when there are zero issues. Seed leader_receiver=attached + NO session_name + NO
260
+ // agents -> issues=[] -> EXIT 0, golden top keys (sort): event_log,issues,ok,runtime,suggested_repairs.
261
+ // RED: unrouted -> Error.
262
+ #[test]
263
+ fn dispatch_routes_diagnose_healthy_leader() {
264
+ let ws = tmp_workspace();
265
+ std::fs::write(
260
266
  ws.join(".team").join("runtime").join("state.json"),
261
267
  serde_json::to_vec(&json!({
262
268
  "leader": {"id": "leader"},
@@ -265,165 +271,219 @@ tasks:
265
271
  .unwrap(),
266
272
  )
267
273
  .unwrap();
268
- let code = run(&cli_argv(&["diagnose", "--workspace", &ws.to_string_lossy(), "--json"]), &ws);
269
- assert_eq!(
274
+ let code = run(
275
+ &cli_argv(&["diagnose", "--workspace", &ws.to_string_lossy(), "--json"]),
276
+ &ws,
277
+ );
278
+ assert_eq!(
270
279
  code,
271
280
  ExitCode::Ok,
272
281
  "`diagnose` must ROUTE to cmd_diagnose (parser.py:298); a healthy (attached, no-session, \
273
282
  no-agent) state yields zero issues -> exit 0 {{event_log,issues,ok,runtime,suggested_repairs}}; \
274
283
  today -> unknown-subcommand Error"
275
284
  );
276
- let _ = std::fs::remove_dir_all(&ws);
277
- }
285
+ let _ = std::fs::remove_dir_all(&ws);
286
+ }
278
287
 
279
- // ── preflight (#[ignore] real-machine) ── golden parser.py:160 `cmd_preflight` -> runtime.preflight
280
- // (Path(args.team)). Validates a team role-doc dir: checks compile(TEAM.md)/tmux/ghostty/rust_core/
281
- // profile_dir; golden -> {"blockers":[...],"checks":[...],"details_log":...,"next_actions":[...],
282
- // "ok":bool,"summary":...}. Reaches ok:true (exit 0) ONLY with a valid TEAM.md + ghostty installed,
283
- // which CI cannot supply (ghostty absent -> blocker) -> not a clean in-process exit-0 RED.
284
- #[test]
285
- #[ignore = "real-machine: `preflight --team <dir>` needs a valid TEAM.md role-doc dir + ghostty to \
288
+ // ── preflight (#[ignore] real-machine) ── golden parser.py:160 `cmd_preflight` -> runtime.preflight
289
+ // (Path(args.team)). Validates a team role-doc dir: checks compile(TEAM.md)/tmux/ghostty/rust_core/
290
+ // profile_dir; golden -> {"blockers":[...],"checks":[...],"details_log":...,"next_actions":[...],
291
+ // "ok":bool,"summary":...}. Reaches ok:true (exit 0) ONLY with a valid TEAM.md + ghostty installed,
292
+ // which CI cannot supply (ghostty absent -> blocker) -> not a clean in-process exit-0 RED.
293
+ #[test]
294
+ #[ignore = "real-machine: `preflight --team <dir>` needs a valid TEAM.md role-doc dir + ghostty to \
286
295
  reach ok:true; on CI it golden-exits 1 (compile/ghostty blockers) which is \
287
296
  indistinguishable from the unknown-subcommand Error (false-green). Routes to cmd_preflight \
288
297
  (parser.py:160); shape {blockers,checks,details_log,next_actions,ok,summary}"]
289
- fn dispatch_routes_preflight_real_machine() {
290
- let ws = tmp_workspace();
291
- let code = run(&cli_argv(&["preflight", "--team", &ws.to_string_lossy(), "--json"]), &ws);
292
- assert_eq!(code, ExitCode::Ok, "preflight must ROUTE + exit 0 on a valid team dir");
293
- let _ = std::fs::remove_dir_all(&ws);
294
- }
298
+ fn dispatch_routes_preflight_real_machine() {
299
+ let ws = tmp_workspace();
300
+ let code = run(
301
+ &cli_argv(&["preflight", "--team", &ws.to_string_lossy(), "--json"]),
302
+ &ws,
303
+ );
304
+ assert_eq!(
305
+ code,
306
+ ExitCode::Ok,
307
+ "preflight must ROUTE + exit 0 on a valid team dir"
308
+ );
309
+ let _ = std::fs::remove_dir_all(&ws);
310
+ }
295
311
 
296
- // ── wait-ready (#[ignore] real-machine) ── golden parser.py:171 `cmd_wait_ready` -> runtime.wait_ready
297
- // (ws, timeout). Polls worker readiness; golden -> {"details_log":...,"next_actions":[...],"ok":bool,
298
- // "readiness":{cli_prompt_ready,mcp_ready,process_started,task_prompt_delivered},"summary":...}.
299
- // ok:true (exit 0) needs a LIVE ready team; CI has none -> golden-exits 1 (false-green vs unknown).
300
- #[test]
301
- #[ignore = "real-machine: `wait-ready` polls a LIVE team's readiness; with no workers golden-exits 1 \
312
+ // ── wait-ready (#[ignore] real-machine) ── golden parser.py:171 `cmd_wait_ready` -> runtime.wait_ready
313
+ // (ws, timeout). Polls worker readiness; golden -> {"details_log":...,"next_actions":[...],"ok":bool,
314
+ // "readiness":{cli_prompt_ready,mcp_ready,process_started,task_prompt_delivered},"summary":...}.
315
+ // ok:true (exit 0) needs a LIVE ready team; CI has none -> golden-exits 1 (false-green vs unknown).
316
+ #[test]
317
+ #[ignore = "real-machine: `wait-ready` polls a LIVE team's readiness; with no workers golden-exits 1 \
302
318
  (false-green vs unknown-subcommand Error). Routes to cmd_wait_ready (parser.py:171); shape \
303
319
  {details_log,next_actions,ok,readiness{cli_prompt_ready,mcp_ready,process_started,task_prompt_delivered},summary}"]
304
- fn dispatch_routes_wait_ready_real_machine() {
305
- let ws = tmp_workspace();
306
- let code = run(&cli_argv(&["wait-ready", "--workspace", &ws.to_string_lossy(), "--timeout", "1", "--json"]), &ws);
307
- assert_eq!(code, ExitCode::Ok, "wait-ready must ROUTE + exit 0 once the team is ready");
308
- let _ = std::fs::remove_dir_all(&ws);
309
- }
320
+ fn dispatch_routes_wait_ready_real_machine() {
321
+ let ws = tmp_workspace();
322
+ let code = run(
323
+ &cli_argv(&[
324
+ "wait-ready",
325
+ "--workspace",
326
+ &ws.to_string_lossy(),
327
+ "--timeout",
328
+ "1",
329
+ "--json",
330
+ ]),
331
+ &ws,
332
+ );
333
+ assert_eq!(
334
+ code,
335
+ ExitCode::Ok,
336
+ "wait-ready must ROUTE + exit 0 once the team is ready"
337
+ );
338
+ let _ = std::fs::remove_dir_all(&ws);
339
+ }
310
340
 
311
- // ── e2e --providers fake (#[ignore] real-machine) ── golden parser.py:449 `cmd_e2e` (cli/e2e.py:12).
312
- // `--providers fake` runs a REAL end-to-end: runtime.launch(spec, auto_approve=True) (spawns a tmux
313
- // team) -> send_message -> sleep -> collect -> shutdown. Result envelope (e2e.py:16,48-56):
314
- // {"workspace":str,"providers":{"fake":{"ok":bool,"launch":{...},"send":{...},"collect":{...},
315
- // "shutdown":{...}}},"ok":bool}. Real tmux spawn -> #[ignore] (NOT runnable in-process).
316
- #[test]
317
- #[ignore = "real-machine: `e2e --providers fake` calls runtime.launch -> spawns a REAL tmux team \
341
+ // ── e2e --providers fake (#[ignore] real-machine) ── golden parser.py:449 `cmd_e2e` (cli/e2e.py:12).
342
+ // `--providers fake` runs a REAL end-to-end: runtime.launch(spec, auto_approve=True) (spawns a tmux
343
+ // team) -> send_message -> sleep -> collect -> shutdown. Result envelope (e2e.py:16,48-56):
344
+ // {"workspace":str,"providers":{"fake":{"ok":bool,"launch":{...},"send":{...},"collect":{...},
345
+ // "shutdown":{...}}},"ok":bool}. Real tmux spawn -> #[ignore] (NOT runnable in-process).
346
+ #[test]
347
+ #[ignore = "real-machine: `e2e --providers fake` calls runtime.launch -> spawns a REAL tmux team \
318
348
  (send/collect/shutdown). Routes to cmd_e2e (parser.py:449 / e2e.py:12); envelope \
319
349
  {workspace,providers:{fake:{ok,launch,send,collect,shutdown}},ok}"]
320
- fn dispatch_routes_e2e_fake_real_machine() {
321
- let ws = tmp_workspace();
322
- let code = run(&cli_argv(&["e2e", "--providers", "fake", "--workspace", &ws.to_string_lossy(), "--json"]), &ws);
323
- assert_eq!(code, ExitCode::Ok, "e2e --providers fake must ROUTE + run the fake end-to-end (ok:true)");
324
- let _ = std::fs::remove_dir_all(&ws);
325
- }
350
+ fn dispatch_routes_e2e_fake_real_machine() {
351
+ let ws = tmp_workspace();
352
+ let code = run(
353
+ &cli_argv(&[
354
+ "e2e",
355
+ "--providers",
356
+ "fake",
357
+ "--workspace",
358
+ &ws.to_string_lossy(),
359
+ "--json",
360
+ ]),
361
+ &ws,
362
+ );
363
+ assert_eq!(
364
+ code,
365
+ ExitCode::Ok,
366
+ "e2e --providers fake must ROUTE + run the fake end-to-end (ok:true)"
367
+ );
368
+ let _ = std::fs::remove_dir_all(&ws);
369
+ }
326
370
 
327
- // ── peek (#[ignore] real-machine) ── golden parser.py:201 `cmd_peek` (commands.py:118). Requires
328
- // `--allow-raw-screen` (else TeamAgentError) + a mutually-exclusive --head/--tail/--search; then
329
- // runtime.peek captures a LIVE tmux pane (tmux capture-pane). Real tmux -> #[ignore]. Routes to
330
- // cmd_peek; --json returns the peek dict (text + capture metadata).
331
- #[test]
332
- #[ignore = "real-machine: `peek <agent> --tail N --allow-raw-screen` captures a LIVE tmux pane \
371
+ // ── peek (#[ignore] real-machine) ── golden parser.py:201 `cmd_peek` (commands.py:118). Requires
372
+ // `--allow-raw-screen` (else TeamAgentError) + a mutually-exclusive --head/--tail/--search; then
373
+ // runtime.peek captures a LIVE tmux pane (tmux capture-pane). Real tmux -> #[ignore]. Routes to
374
+ // cmd_peek; --json returns the peek dict (text + capture metadata).
375
+ #[test]
376
+ #[ignore = "real-machine: `peek <agent> --tail N --allow-raw-screen` captures a LIVE tmux pane \
333
377
  (tmux capture-pane); needs a running worker pane. Routes to cmd_peek (parser.py:201); \
334
378
  --json returns the peek dict (raw screen text + metadata)"]
335
- fn dispatch_routes_peek_real_machine() {
336
- let ws = tmp_workspace();
337
- let code = run(
338
- &cli_argv(&["peek", "fake_impl", "--workspace", &ws.to_string_lossy(), "--tail", "20", "--allow-raw-screen", "--json"]),
339
- &ws,
340
- );
341
- assert_eq!(code, ExitCode::Ok, "peek must ROUTE + capture the live pane (real machine)");
342
- let _ = std::fs::remove_dir_all(&ws);
343
- }
379
+ fn dispatch_routes_peek_real_machine() {
380
+ let ws = tmp_workspace();
381
+ let code = run(
382
+ &cli_argv(&[
383
+ "peek",
384
+ "fake_impl",
385
+ "--workspace",
386
+ &ws.to_string_lossy(),
387
+ "--tail",
388
+ "20",
389
+ "--allow-raw-screen",
390
+ "--json",
391
+ ]),
392
+ &ws,
393
+ );
394
+ assert_eq!(
395
+ code,
396
+ ExitCode::Ok,
397
+ "peek must ROUTE + capture the live pane (real machine)"
398
+ );
399
+ let _ = std::fs::remove_dir_all(&ws);
400
+ }
344
401
 
345
- // CONTRACT (shared-root, real-machine-driven; golden = correct-behavior baseline): wait_readiness
346
- // derives cli_prompt_ready from the LIFECYCLE status — an alive worker (status="running") IS
347
- // cli_prompt_ready (golden quick_start.py:173 `status ∈ {running, busy}`). Rust (diagnose.rs:280)
348
- // requires cli_prompt_ready flag / startup_prompts=="complete" / status=="ready" and does NOT accept
349
- // "running" → an alive fake worker never becomes ready → wait_ready times out. Same shared root as the
350
- // deferred_busy regression: lifecycle "running" is the authoritative alive/ready signal, not a
351
- // turn-level flag. (process_started/mcp_ready/task_prompt_delivered are satisfied here so `ready`
352
- // hinges solely on the cli_prompt_ready derivation.)
353
- #[test]
354
- fn contract_alive_worker_running_is_cli_prompt_ready_and_ready() {
355
- // A-5: missing leader_receiver no longer counts as attached; this contract's
356
- // subject is the cli_prompt_ready derivation, so the fixture carries an
357
- // attached receiver to keep `ready` hinging on it alone.
358
- let state = serde_json::json!({
359
- "leader_receiver": {"status": "attached", "pane_id": "%9"},
360
- "agents": {"w1": {
361
- "status": "running",
362
- "pane_id": "%1",
363
- "mcp_ready": true,
364
- "first_send_at": "2026-01-01T00:00:00Z"
365
- }}});
366
- let r = crate::cli::diagnose::wait_readiness(&state);
367
- assert_eq!(
402
+ // CONTRACT (shared-root, real-machine-driven; golden = correct-behavior baseline): wait_readiness
403
+ // derives cli_prompt_ready from the LIFECYCLE status — an alive worker (status="running") IS
404
+ // cli_prompt_ready (golden quick_start.py:173 `status ∈ {running, busy}`). Rust (diagnose.rs:280)
405
+ // requires cli_prompt_ready flag / startup_prompts=="complete" / status=="ready" and does NOT accept
406
+ // "running" → an alive fake worker never becomes ready → wait_ready times out. Same shared root as the
407
+ // deferred_busy regression: lifecycle "running" is the authoritative alive/ready signal, not a
408
+ // turn-level flag. (process_started/mcp_ready/task_prompt_delivered are satisfied here so `ready`
409
+ // hinges solely on the cli_prompt_ready derivation.)
410
+ #[test]
411
+ fn contract_alive_worker_running_is_cli_prompt_ready_and_ready() {
412
+ // A-5: missing leader_receiver no longer counts as attached; this contract's
413
+ // subject is the cli_prompt_ready derivation, so the fixture carries an
414
+ // attached receiver to keep `ready` hinging on it alone.
415
+ let state = serde_json::json!({
416
+ "leader_receiver": {"status": "attached", "pane_id": "%9"},
417
+ "agents": {"w1": {
418
+ "status": "running",
419
+ "pane_id": "%1",
420
+ "mcp_ready": true,
421
+ "first_send_at": "2026-01-01T00:00:00Z"
422
+ }}});
423
+ let r = crate::cli::diagnose::wait_readiness(&state);
424
+ assert_eq!(
368
425
  r.get("cli_prompt_ready").and_then(serde_json::Value::as_bool),
369
426
  Some(true),
370
427
  "CONTRACT: an alive worker (lifecycle status=running) is cli_prompt_ready (golden status∈{{running,busy}}); got {r:?}"
371
428
  );
372
- assert_eq!(
429
+ assert_eq!(
373
430
  r.get("ready").and_then(serde_json::Value::as_bool),
374
431
  Some(true),
375
432
  "status=running + pane_id + mcp_ready + first_send_at → all four readiness signals derive true → ready (no timeout); got {r:?}"
376
433
  );
377
- }
434
+ }
378
435
 
379
- // CONTRACT (real-machine wait_ready product FAIL @ 8ea5df5): a live fake quick-start worker times out
380
- // with process_started=false + mcp_ready=false. golden (quick_start.py:172/174):
381
- // process_started = bool(last["tmux_session_present"]) (the live tmux session exists)
382
- // mcp_ready = all(Path(agent["mcp_config"]).exists()) (each agent's mcp_config FILE exists)
383
- // The Rust launch (launch.rs:220-228) persists status="running" + mcp_config (path) but NO pane_id/pid
384
- // and NO mcp_ready flag. wait_readiness reads per-agent pane_id/pid for process_started (-> false) and an
385
- // mcp_ready FLAG for mcp_ready (-> false), so a live worker never becomes ready. The shared-root fix only
386
- // corrected cli_prompt_ready (status=running); these two signals still read the wrong source. This RED
387
- // uses the REALISTIC post-launch state (no synthetic pane_id / mcp_ready flag).
388
- #[test]
389
- fn contract_wait_ready_derives_process_started_from_session_and_mcp_ready_from_file() {
390
- let dir = std::env::temp_dir().join(format!(
391
- "ta-wr-{}-{}",
392
- std::process::id(),
393
- std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
394
- ));
395
- std::fs::create_dir_all(&dir).unwrap();
396
- let mcp = dir.join("mcp.json");
397
- std::fs::write(&mcp, "{}").unwrap();
398
- let state = serde_json::json!({
399
- "session_name": "ta-fake",
400
- "tmux_session_present": true, // golden status() top-level signal: the live tmux session exists
401
- // A-5: missing leader_receiver no longer counts as attached (see above).
402
- "leader_receiver": {"status": "attached", "pane_id": "%9"},
403
- "agents": { "w1": {
404
- "status": "running",
405
- "mcp_config": mcp.to_string_lossy(),
406
- "first_send_at": "2026-01-01T00:00:00Z"
407
- }}
408
- });
409
- let r = crate::cli::diagnose::wait_readiness(&state);
410
- assert_eq!(
436
+ // CONTRACT (real-machine wait_ready product FAIL @ 8ea5df5): a live fake quick-start worker times out
437
+ // with process_started=false + mcp_ready=false. golden (quick_start.py:172/174):
438
+ // process_started = bool(last["tmux_session_present"]) (the live tmux session exists)
439
+ // mcp_ready = all(Path(agent["mcp_config"]).exists()) (each agent's mcp_config FILE exists)
440
+ // The Rust launch (launch.rs:220-228) persists status="running" + mcp_config (path) but NO pane_id/pid
441
+ // and NO mcp_ready flag. wait_readiness reads per-agent pane_id/pid for process_started (-> false) and an
442
+ // mcp_ready FLAG for mcp_ready (-> false), so a live worker never becomes ready. The shared-root fix only
443
+ // corrected cli_prompt_ready (status=running); these two signals still read the wrong source. This RED
444
+ // uses the REALISTIC post-launch state (no synthetic pane_id / mcp_ready flag).
445
+ #[test]
446
+ fn contract_wait_ready_derives_process_started_from_session_and_mcp_ready_from_file() {
447
+ let dir = std::env::temp_dir().join(format!(
448
+ "ta-wr-{}-{}",
449
+ std::process::id(),
450
+ std::time::SystemTime::now()
451
+ .duration_since(std::time::UNIX_EPOCH)
452
+ .unwrap()
453
+ .as_nanos()
454
+ ));
455
+ std::fs::create_dir_all(&dir).unwrap();
456
+ let mcp = dir.join("mcp.json");
457
+ std::fs::write(&mcp, "{}").unwrap();
458
+ let state = serde_json::json!({
459
+ "session_name": "ta-fake",
460
+ "tmux_session_present": true, // golden status() top-level signal: the live tmux session exists
461
+ // A-5: missing leader_receiver no longer counts as attached (see above).
462
+ "leader_receiver": {"status": "attached", "pane_id": "%9"},
463
+ "agents": { "w1": {
464
+ "status": "running",
465
+ "mcp_config": mcp.to_string_lossy(),
466
+ "first_send_at": "2026-01-01T00:00:00Z"
467
+ }}
468
+ });
469
+ let r = crate::cli::diagnose::wait_readiness(&state);
470
+ assert_eq!(
411
471
  r.get("process_started").and_then(serde_json::Value::as_bool),
412
472
  Some(true),
413
473
  "CONTRACT: process_started derives from tmux_session_present (golden quick_start.py:172), NOT \
414
474
  per-agent pane_id/pid (the launch writes neither); got {r:?}"
415
475
  );
416
- assert_eq!(
476
+ assert_eq!(
417
477
  r.get("mcp_ready").and_then(serde_json::Value::as_bool),
418
478
  Some(true),
419
479
  "CONTRACT: mcp_ready derives from each agent's mcp_config FILE existence (golden quick_start.py:174), \
420
480
  NOT an mcp_ready flag (never set); got {r:?}"
421
481
  );
422
- assert_eq!(
482
+ assert_eq!(
423
483
  r.get("ready").and_then(serde_json::Value::as_bool),
424
484
  Some(true),
425
485
  "a live fake quick-start worker (session present + status running + mcp_config file + task sent) \
426
486
  must be ready, not timeout; got {r:?}"
427
487
  );
428
- let _ = std::fs::remove_dir_all(&dir);
429
- }
488
+ let _ = std::fs::remove_dir_all(&dir);
489
+ }