@team-agent/installer 0.4.1 → 0.4.2

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 (44) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +82 -22
  4. package/crates/team-agent/src/cli/diagnose.rs +6 -2
  5. package/crates/team-agent/src/cli/emit.rs +301 -38
  6. package/crates/team-agent/src/cli/mod.rs +7 -14
  7. package/crates/team-agent/src/cli/status_port.rs +12 -1
  8. package/crates/team-agent/src/cli/tests/base.rs +6 -2
  9. package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
  10. package/crates/team-agent/src/cli/tests/leader_watch.rs +5 -3
  11. package/crates/team-agent/src/cli/tests/main_preserved.rs +28 -2
  12. package/crates/team-agent/src/cli/tests/peer_allow.rs +19 -0
  13. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +72 -0
  14. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -3
  15. package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
  16. package/crates/team-agent/src/cli/types.rs +35 -2
  17. package/crates/team-agent/src/coordinator/runtime_detectors.rs +7 -2
  18. package/crates/team-agent/src/diagnose/comms.rs +10 -2
  19. package/crates/team-agent/src/leader/lease.rs +98 -19
  20. package/crates/team-agent/src/leader/rediscover/tests.rs +21 -7
  21. package/crates/team-agent/src/leader/rediscover.rs +16 -7
  22. package/crates/team-agent/src/leader/start.rs +25 -12
  23. package/crates/team-agent/src/leader/tests/byte_findings.rs +11 -2
  24. package/crates/team-agent/src/leader/tests/lease_claim.rs +248 -7
  25. package/crates/team-agent/src/lifecycle/launch.rs +116 -100
  26. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +5 -12
  27. package/crates/team-agent/src/lifecycle/tests/core.rs +1 -1
  28. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +16 -15
  29. package/crates/team-agent/src/lifecycle/types.rs +1 -1
  30. package/crates/team-agent/src/messaging/leader_receiver.rs +22 -8
  31. package/crates/team-agent/src/messaging/send.rs +21 -4
  32. package/crates/team-agent/src/provider/adapter.rs +22 -0
  33. package/crates/team-agent/src/provider/session/capture.rs +228 -0
  34. package/crates/team-agent/src/state/identity.rs +42 -12
  35. package/crates/team-agent/src/state/identity_keys.rs +134 -0
  36. package/crates/team-agent/src/state/mod.rs +8 -0
  37. package/crates/team-agent/src/state/owner_gate.rs +11 -1
  38. package/crates/team-agent/src/state/ownership.rs +556 -0
  39. package/crates/team-agent/src/state/paths.rs +358 -0
  40. package/crates/team-agent/src/state/persist.rs +43 -5
  41. package/crates/team-agent/src/state/projection.rs +13 -5
  42. package/crates/team-agent/src/tmux_backend.rs +12 -0
  43. package/package.json +4 -4
  44. package/skills/team-agent/SKILL.md +3 -3
@@ -93,9 +93,9 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
93
93
  }
94
94
  "status" => cmd_status_for_team(&status_args(args, cwd), parse_args(args).team.as_deref())
95
95
  .map(emit_result),
96
- "stop" => cmd_shutdown(&shutdown_args(args, cwd)).map(emit_result),
97
- "shutdown" => cmd_shutdown(&shutdown_args(args, cwd)).map(emit_result),
98
- "restart" => cmd_restart(&restart_args(args, cwd)).map(emit_result),
96
+ "stop" => cmd_shutdown(&shutdown_args(args, cwd)?).map(emit_result),
97
+ "shutdown" => cmd_shutdown(&shutdown_args(args, cwd)?).map(emit_result),
98
+ "restart" => cmd_restart(&restart_args(args, cwd)?).map(emit_result),
99
99
  "restart-agent" => cmd_reset_agent(&reset_agent_args(args, cwd)?).map(emit_result),
100
100
  "start-agent" => cmd_start_agent(&start_agent_args(args, cwd)?).map(emit_result),
101
101
  "stop-agent" => cmd_stop_agent(&stop_agent_args(args, cwd)?).map(emit_result),
@@ -127,7 +127,7 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
127
127
  }
128
128
  "validate-result" => cmd_validate_result(&validate_result_args(args)?).map(emit_result),
129
129
  "collect" => {
130
- cmd_collect_for_team(&collect_args(args, cwd), parse_args(args).team.as_deref())
130
+ cmd_collect_for_team(&collect_args(args, cwd)?, parse_args(args).team.as_deref())
131
131
  .map(emit_result)
132
132
  }
133
133
  "settle" => cmd_settle(&settle_args(args, cwd)).map(emit_result),
@@ -215,7 +215,7 @@ fn command_help(command: Option<&str>) -> String {
215
215
  )
216
216
  }
217
217
  Some("init") => "usage: team-agent init [--workspace WORKSPACE] [--force] [--json]".to_string(),
218
- Some("quick-start") => "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--fresh] [--no-display] [--json]\n\ndefaults: display_backend=adaptive; set display_backend: none in TEAM.md or pass --no-display to use one worker window per agent.".to_string(),
218
+ Some("quick-start") => "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--json]\n\ndefaults: display_backend=adaptive; set display_backend: none in TEAM.md or pass --no-display to use one worker window per agent.".to_string(),
219
219
  Some("start") => "usage: team-agent start [TEAMDIR] [--yes] [--fresh] [--json]".to_string(),
220
220
  Some("compile") => "usage: team-agent compile --team TEAM [--out FILE] [--json]".to_string(),
221
221
  Some("send") => "usage: team-agent send TARGET MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--targets AGENTS] [--task TASK] [--sender SENDER] [--watch-result] [--requires-ack|--no-ack] [--no-wait] [--timeout SECONDS] [--confirm-human] [--message-id ID] [--json]".to_string(),
@@ -234,28 +234,28 @@ fn command_help(command: Option<&str>) -> String {
234
234
  Some("fork-agent") => "usage: team-agent fork-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]".to_string(),
235
235
  Some("remove-agent") => "usage: team-agent remove-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--from-spec] [--confirm] [--force] [--json]".to_string(),
236
236
  Some("purge-agent") => "usage: team-agent purge-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--force] [--json]".to_string(),
237
- Some("stuck-list") => "usage: team-agent stuck-list [--workspace WORKSPACE] [--json]".to_string(),
237
+ Some("stuck-list") => "usage: team-agent stuck-list [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
238
238
  Some("stuck-cancel") => "usage: team-agent stuck-cancel AGENT [--workspace WORKSPACE] [--alert-type stuck|idle_fallback|cross_worker_deadlock|all] [--json]".to_string(),
239
239
  Some("acknowledge-idle") => "usage: team-agent acknowledge-idle [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
240
240
  Some("takeover") => "usage: team-agent takeover [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]".to_string(),
241
241
  Some("claim-leader") => "usage: team-agent claim-leader [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]".to_string(),
242
242
  Some("attach-leader") => "usage: team-agent attach-leader [--workspace WORKSPACE] [--team TEAM] [--pane PANE] [--provider PROVIDER] [--confirm] [--json]".to_string(),
243
243
  Some("identity") => "usage: team-agent identity [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
244
- Some("approvals") => "usage: team-agent approvals [AGENT] [--workspace WORKSPACE] [--json]".to_string(),
245
- Some("inbox") => "usage: team-agent inbox AGENT [--workspace WORKSPACE] [--limit N] [--since CURSOR] [--json]".to_string(),
244
+ Some("approvals") => "usage: team-agent approvals [AGENT] [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
245
+ Some("inbox") => "usage: team-agent inbox AGENT [--workspace WORKSPACE] [--team TEAM] [--limit N] [--since CURSOR] [--json]".to_string(),
246
246
  Some("doctor") => "usage: team-agent doctor [SPEC] [--workspace WORKSPACE] [--team TEAM] [--gate orphans|comms] [--comms] [--fix] [--fix-schema] [--cleanup-orphans] [--confirm] [--json]".to_string(),
247
247
  Some("watch") => "usage: team-agent watch [--workspace WORKSPACE] [--team TEAM]".to_string(),
248
- Some("sessions") => "usage: team-agent sessions [--workspace WORKSPACE] [--json]".to_string(),
248
+ Some("sessions") => "usage: team-agent sessions [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
249
249
  Some("validate") => "usage: team-agent validate [SPEC] [--json]".to_string(),
250
250
  Some("install-skill") => "usage: team-agent install-skill (--source DIR | --uninstall) [--target codex|claude|copilot|all] [--dest DIR] [--dry-run] [--json]".to_string(),
251
251
  Some("profile") => "usage: team-agent profile COMMAND NAME [--workspace WORKSPACE] [--team TEAM] [--auth-mode MODE] [--json]".to_string(),
252
252
  Some("validate-result") => "usage: team-agent validate-result [ENVELOPE] [--file FILE|--result JSON] [--json]".to_string(),
253
253
  Some("collect") => "usage: team-agent collect [--workspace WORKSPACE] [--team TEAM] [--result-file FILE] [--json]".to_string(),
254
254
  Some("settle") => "usage: team-agent settle [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
255
- Some("repair-state") => "usage: team-agent repair-state --task TASK --status STATUS [SUMMARY] [--assignee AGENT] [--workspace WORKSPACE] [--json]".to_string(),
256
- Some("diagnose") => "usage: team-agent diagnose [--workspace WORKSPACE] [--json]".to_string(),
255
+ Some("repair-state") => "usage: team-agent repair-state --task TASK --status STATUS [SUMMARY] [--assignee AGENT] [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
256
+ Some("diagnose") => "usage: team-agent diagnose [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
257
257
  Some("preflight") => "usage: team-agent preflight [TEAMDIR] [--json]".to_string(),
258
- Some("wait-ready") => "usage: team-agent wait-ready [--workspace WORKSPACE] [--timeout SECONDS] [--json]".to_string(),
258
+ Some("wait-ready") => "usage: team-agent wait-ready [--workspace WORKSPACE] [--team TEAM] [--timeout SECONDS] [--json]".to_string(),
259
259
  Some("e2e") => "usage: team-agent e2e [--workspace WORKSPACE] [--providers LIST] [--real] [--json]".to_string(),
260
260
  Some("peek") => "usage: team-agent peek AGENT [--workspace WORKSPACE] [--tail N|--head N] [--search TEXT] [--allow-raw-screen] [--json]".to_string(),
261
261
  Some("coordinator") => "usage: team-agent coordinator [--workspace WORKSPACE] [--once] [--tick-interval SECONDS]".to_string(),
@@ -613,7 +613,6 @@ struct ParsedArgs {
613
613
  team: Option<String>,
614
614
  json: bool,
615
615
  yes: bool,
616
- fresh: bool,
617
616
  name: Option<String>,
618
617
  team_id: Option<String>,
619
618
  targets: Option<String>,
@@ -685,7 +684,6 @@ fn parse_args(args: &[String]) -> ParsedArgs {
685
684
  "--team" => parsed.team = next_arg(args, &mut i),
686
685
  "--json" => parsed.json = true,
687
686
  "--yes" => parsed.yes = true,
688
- "--fresh" => parsed.fresh = true,
689
687
  "--name" => parsed.name = next_arg(args, &mut i),
690
688
  "--team-id" => parsed.team_id = next_arg(args, &mut i),
691
689
  "--targets" | "--target" | "--to" => parsed.targets = next_arg(args, &mut i),
@@ -804,6 +802,13 @@ fn required_pos(parsed: &ParsedArgs, index: usize, name: &str) -> Result<String,
804
802
  }
805
803
 
806
804
  fn quick_start_args(args: &[String], cwd: &Path) -> Result<QuickStartArgs, CliError> {
805
+ if has_arg(args, "--fresh") {
806
+ return Err(CliError::Usage(
807
+ "quick-start no longer accepts --fresh. Reset semantics moved to \
808
+ `team-agent restart --allow-fresh`, which requires explicit user \
809
+ confirmation.".to_string(),
810
+ ));
811
+ }
807
812
  let parsed = parse_args(args);
808
813
  let workspace = workspace(&parsed, cwd);
809
814
  let agents_dir = parsed
@@ -822,7 +827,6 @@ fn quick_start_args(args: &[String], cwd: &Path) -> Result<QuickStartArgs, CliEr
822
827
  name: parsed.name,
823
828
  team_id: parsed.team_id.or(parsed.team),
824
829
  yes: parsed.yes,
825
- fresh: parsed.fresh,
826
830
  no_display: parsed.no_display,
827
831
  json: parsed.json,
828
832
  })
@@ -949,6 +953,31 @@ fn fallback_report_result_args(
949
953
  })
950
954
  }
951
955
 
956
+ /// Stage 4 of identity-boundary unified plan (architect direction
957
+ /// 2026-06-24, .team/artifacts/identity-boundary-unified-plan.md §2 Stage
958
+ /// 4): destructive command ambiguity gate. When the workspace has 2+
959
+ /// alive teams and the caller did not pass `--team`, refuse with a
960
+ /// usage error listing the candidates so the operator picks explicitly.
961
+ /// Single-team workspaces (the 0.4.x baseline) are unaffected — the
962
+ /// `CommandScope::resolve` helper returns `Resolved` and this function
963
+ /// is a no-op.
964
+ fn refuse_if_multi_alive_team_missing_scope(
965
+ command: &str,
966
+ workspace: &Path,
967
+ requested_team: Option<&str>,
968
+ ) -> Result<(), CliError> {
969
+ let scope = crate::state::paths::CommandScope::resolve(workspace, requested_team);
970
+ if scope.is_ambiguous() {
971
+ let candidates = scope.candidates().join(", ");
972
+ return Err(CliError::Usage(format!(
973
+ "{command}: workspace has multiple alive teams ({candidates}); \
974
+ pass `--team <key>` to choose one (refusing to default to any \
975
+ single team — Stage 4 identity-boundary contract)"
976
+ )));
977
+ }
978
+ Ok(())
979
+ }
980
+
952
981
  fn allow_peer_talk_args(args: &[String], cwd: &Path) -> Result<AllowPeerTalkArgs, CliError> {
953
982
  let parsed = parse_args(args);
954
983
  Ok(AllowPeerTalkArgs {
@@ -956,6 +985,7 @@ fn allow_peer_talk_args(args: &[String], cwd: &Path) -> Result<AllowPeerTalkArgs
956
985
  b: required_pos(&parsed, 1, "b")?,
957
986
  workspace: workspace(&parsed, cwd),
958
987
  json: parsed.json,
988
+ team: parsed.team,
959
989
  })
960
990
  }
961
991
 
@@ -967,6 +997,7 @@ fn status_args(args: &[String], cwd: &Path) -> StatusArgs {
967
997
  detail: parsed.detail,
968
998
  summary: parsed.summary,
969
999
  json: parsed.json,
1000
+ team: parsed.team,
970
1001
  }
971
1002
  }
972
1003
 
@@ -984,6 +1015,7 @@ fn approvals_args(args: &[String], cwd: &Path) -> ApprovalsArgs {
984
1015
  agent: parsed.positionals.first().cloned(),
985
1016
  workspace: workspace(&parsed, cwd),
986
1017
  json: parsed.json,
1018
+ team: parsed.team,
987
1019
  }
988
1020
  }
989
1021
 
@@ -995,6 +1027,7 @@ fn inbox_args(args: &[String], cwd: &Path) -> Result<InboxArgs, CliError> {
995
1027
  limit: parsed.limit.unwrap_or(20),
996
1028
  since: parsed.since,
997
1029
  json: parsed.json,
1030
+ team: parsed.team,
998
1031
  })
999
1032
  }
1000
1033
 
@@ -1042,29 +1075,33 @@ fn identity_args(args: &[String], cwd: &Path) -> IdentityArgs {
1042
1075
  }
1043
1076
  }
1044
1077
 
1045
- fn shutdown_args(args: &[String], cwd: &Path) -> ShutdownArgs {
1078
+ fn shutdown_args(args: &[String], cwd: &Path) -> Result<ShutdownArgs, CliError> {
1046
1079
  let parsed = parse_args(args);
1047
- ShutdownArgs {
1048
- workspace: workspace(&parsed, cwd),
1080
+ let workspace = workspace(&parsed, cwd);
1081
+ refuse_if_multi_alive_team_missing_scope("shutdown", &workspace, parsed.team.as_deref())?;
1082
+ Ok(ShutdownArgs {
1083
+ workspace,
1049
1084
  team: parsed.team,
1050
1085
  keep_logs: parsed.keep_logs,
1051
1086
  json: parsed.json,
1052
- }
1087
+ })
1053
1088
  }
1054
1089
 
1055
- fn restart_args(args: &[String], cwd: &Path) -> RestartArgs {
1090
+ fn restart_args(args: &[String], cwd: &Path) -> Result<RestartArgs, CliError> {
1056
1091
  let parsed = parse_args(args);
1057
- RestartArgs {
1058
- workspace: parsed
1059
- .positionals
1060
- .first()
1061
- .map(PathBuf::from)
1062
- .unwrap_or_else(|| workspace(&parsed, cwd)),
1092
+ let workspace = parsed
1093
+ .positionals
1094
+ .first()
1095
+ .map(PathBuf::from)
1096
+ .unwrap_or_else(|| workspace(&parsed, cwd));
1097
+ refuse_if_multi_alive_team_missing_scope("restart", &workspace, parsed.team.as_deref())?;
1098
+ Ok(RestartArgs {
1099
+ workspace,
1063
1100
  team: parsed.team,
1064
1101
  allow_fresh: parsed.allow_fresh,
1065
1102
  session_converge_deadline_ms: parsed.session_converge_deadline_ms,
1066
1103
  json: parsed.json,
1067
- }
1104
+ })
1068
1105
  }
1069
1106
 
1070
1107
  fn start_agent_args(args: &[String], cwd: &Path) -> Result<StartAgentArgs, CliError> {
@@ -1092,9 +1129,11 @@ fn stop_agent_args(args: &[String], cwd: &Path) -> Result<StopAgentArgs, CliErro
1092
1129
 
1093
1130
  fn reset_agent_args(args: &[String], cwd: &Path) -> Result<ResetAgentArgs, CliError> {
1094
1131
  let parsed = parse_args(args);
1132
+ let workspace = workspace(&parsed, cwd);
1133
+ refuse_if_multi_alive_team_missing_scope("reset-agent", &workspace, parsed.team.as_deref())?;
1095
1134
  Ok(ResetAgentArgs {
1096
1135
  agent: required_pos(&parsed, 0, "agent")?,
1097
- workspace: workspace(&parsed, cwd),
1136
+ workspace,
1098
1137
  team: parsed.team,
1099
1138
  discard_session: parsed.discard_session,
1100
1139
  no_display: parsed.no_display,
@@ -1133,9 +1172,11 @@ fn fork_agent_args(args: &[String], cwd: &Path) -> Result<ForkAgentArgs, CliErro
1133
1172
 
1134
1173
  fn remove_agent_args(args: &[String], cwd: &Path) -> Result<RemoveAgentArgs, CliError> {
1135
1174
  let parsed = parse_args(args);
1175
+ let workspace = workspace(&parsed, cwd);
1176
+ refuse_if_multi_alive_team_missing_scope("remove-agent", &workspace, parsed.team.as_deref())?;
1136
1177
  Ok(RemoveAgentArgs {
1137
1178
  agent: required_pos(&parsed, 0, "agent")?,
1138
- workspace: workspace(&parsed, cwd),
1179
+ workspace,
1139
1180
  team: parsed.team,
1140
1181
  from_spec: parsed.from_spec,
1141
1182
  confirm: parsed.confirm,
@@ -1149,16 +1190,24 @@ fn stuck_list_args(args: &[String], cwd: &Path) -> StuckListArgs {
1149
1190
  StuckListArgs {
1150
1191
  workspace: workspace(&parsed, cwd),
1151
1192
  json: parsed.json,
1193
+ team: parsed.team,
1152
1194
  }
1153
1195
  }
1154
1196
 
1155
1197
  fn stuck_cancel_args(args: &[String], cwd: &Path) -> Result<StuckCancelArgs, CliError> {
1156
1198
  let parsed = parse_args(args);
1199
+ let workspace = workspace(&parsed, cwd);
1200
+ refuse_if_multi_alive_team_missing_scope(
1201
+ "stuck-cancel",
1202
+ &workspace,
1203
+ parsed.team.as_deref(),
1204
+ )?;
1157
1205
  Ok(StuckCancelArgs {
1158
1206
  agent: required_pos(&parsed, 0, "agent")?,
1159
- workspace: workspace(&parsed, cwd),
1207
+ workspace,
1160
1208
  alert_type: alert_type(parsed.alert_type.as_deref())?,
1161
1209
  json: parsed.json,
1210
+ team: parsed.team,
1162
1211
  })
1163
1212
  }
1164
1213
 
@@ -1212,6 +1261,7 @@ fn sessions_args(args: &[String], cwd: &Path) -> SessionsArgs {
1212
1261
  SessionsArgs {
1213
1262
  workspace: workspace(&parsed, cwd),
1214
1263
  json: parsed.json,
1264
+ team: parsed.team,
1215
1265
  }
1216
1266
  }
1217
1267
 
@@ -1250,13 +1300,16 @@ fn validate_result_args(args: &[String]) -> Result<ValidateResultArgs, CliError>
1250
1300
  })
1251
1301
  }
1252
1302
 
1253
- fn collect_args(args: &[String], cwd: &Path) -> CollectArgs {
1303
+ fn collect_args(args: &[String], cwd: &Path) -> Result<CollectArgs, CliError> {
1254
1304
  let parsed = parse_args(args);
1255
- CollectArgs {
1256
- workspace: workspace(&parsed, cwd),
1305
+ let workspace = workspace(&parsed, cwd);
1306
+ refuse_if_multi_alive_team_missing_scope("collect", &workspace, parsed.team.as_deref())?;
1307
+ Ok(CollectArgs {
1308
+ workspace,
1257
1309
  result_file: parsed.result_file,
1258
1310
  json: parsed.json,
1259
- }
1311
+ team: parsed.team,
1312
+ })
1260
1313
  }
1261
1314
 
1262
1315
  fn settle_args(args: &[String], cwd: &Path) -> SettleArgs {
@@ -1270,8 +1323,10 @@ fn settle_args(args: &[String], cwd: &Path) -> SettleArgs {
1270
1323
 
1271
1324
  fn repair_state_args(args: &[String], cwd: &Path) -> Result<RepairStateArgs, CliError> {
1272
1325
  let parsed = parse_args(args);
1326
+ let workspace = workspace(&parsed, cwd);
1327
+ refuse_if_multi_alive_team_missing_scope("repair-state", &workspace, parsed.team.as_deref())?;
1273
1328
  Ok(RepairStateArgs {
1274
- workspace: workspace(&parsed, cwd),
1329
+ workspace,
1275
1330
  task_id: parsed
1276
1331
  .task
1277
1332
  .ok_or_else(|| CliError::Usage("missing --task".to_string()))?,
@@ -1281,6 +1336,7 @@ fn repair_state_args(args: &[String], cwd: &Path) -> Result<RepairStateArgs, Cli
1281
1336
  .ok_or_else(|| CliError::Usage("missing --status".to_string()))?,
1282
1337
  summary: option_value(args, "--summary").or_else(|| parsed.positionals.first().cloned()),
1283
1338
  json: parsed.json,
1339
+ team: parsed.team,
1284
1340
  })
1285
1341
  }
1286
1342
 
@@ -1308,6 +1364,7 @@ fn diagnose_args(args: &[String], cwd: &Path) -> DiagnoseArgs {
1308
1364
  DiagnoseArgs {
1309
1365
  workspace: workspace(&parsed, cwd),
1310
1366
  json: parsed.json,
1367
+ team: parsed.team,
1311
1368
  }
1312
1369
  }
1313
1370
 
@@ -1331,6 +1388,7 @@ fn wait_ready_args(args: &[String], cwd: &Path) -> WaitReadyArgs {
1331
1388
  workspace: workspace(&parsed, cwd),
1332
1389
  timeout: parsed.timeout.unwrap_or(60.0),
1333
1390
  json: parsed.json,
1391
+ team: parsed.team,
1334
1392
  }
1335
1393
  }
1336
1394
 
@@ -1529,7 +1587,7 @@ mod tests {
1529
1587
  for (command, flags) in [
1530
1588
  (
1531
1589
  "quick-start",
1532
- &["--workspace", "--team-id", "--yes", "--fresh", "--json"][..],
1590
+ &["--workspace", "--team-id", "--yes", "--json"][..],
1533
1591
  ),
1534
1592
  (
1535
1593
  "send",
@@ -1638,11 +1696,19 @@ mod tests {
1638
1696
  "collect",
1639
1697
  &["--workspace", "--team", "--result-file", "--json"][..],
1640
1698
  ),
1699
+ ("stuck-list", &["--workspace", "--team", "--json"][..]),
1700
+ ("approvals", &["--workspace", "--team", "--json"][..]),
1701
+ (
1702
+ "inbox",
1703
+ &["--workspace", "--team", "--limit", "--since", "--json"][..],
1704
+ ),
1705
+ ("sessions", &["--workspace", "--team", "--json"][..]),
1641
1706
  (
1642
1707
  "repair-state",
1643
- &["--task", "--status", "--assignee", "--workspace", "--json"][..],
1708
+ &["--task", "--status", "--assignee", "--workspace", "--team", "--json"][..],
1644
1709
  ),
1645
- ("wait-ready", &["--workspace", "--timeout", "--json"][..]),
1710
+ ("diagnose", &["--workspace", "--team", "--json"][..]),
1711
+ ("wait-ready", &["--workspace", "--team", "--timeout", "--json"][..]),
1646
1712
  (
1647
1713
  "peek",
1648
1714
  &[
@@ -1667,6 +1733,10 @@ mod tests {
1667
1733
  );
1668
1734
  }
1669
1735
  }
1736
+ assert!(
1737
+ !command_help(Some("quick-start")).contains("--fresh"),
1738
+ "quick-start help must not advertise removed reset semantics"
1739
+ );
1670
1740
  }
1671
1741
 
1672
1742
  #[test]
@@ -1724,4 +1794,197 @@ mod tests {
1724
1794
  assert_eq!(levenshtein("status", "status"), 0);
1725
1795
  assert_eq!(levenshtein("statu", "status"), 1);
1726
1796
  }
1797
+
1798
+ // ─── Stage 4: multi-team ambiguity refusal for destructive commands ───
1799
+
1800
+ fn seed_two_alive_teams_in(ws: &std::path::Path) {
1801
+ crate::state::persist::save_runtime_state(
1802
+ ws,
1803
+ &serde_json::json!({
1804
+ "teams": {
1805
+ "alpha": {"status": "alive"},
1806
+ "beta": {"status": "alive"},
1807
+ },
1808
+ }),
1809
+ )
1810
+ .unwrap();
1811
+ }
1812
+
1813
+ #[test]
1814
+ fn refuse_helper_passes_on_single_alive_team() {
1815
+ let ws = tmp_workspace();
1816
+ crate::state::persist::save_runtime_state(
1817
+ &ws,
1818
+ &serde_json::json!({"teams": {"alpha": {"status": "alive"}}}),
1819
+ )
1820
+ .unwrap();
1821
+ assert!(
1822
+ refuse_if_multi_alive_team_missing_scope("stuck-cancel", &ws, None).is_ok(),
1823
+ "single-alive-team workspace must not trigger the ambiguity refusal"
1824
+ );
1825
+ }
1826
+
1827
+ #[test]
1828
+ fn refuse_helper_passes_when_explicit_team_provided_even_in_multi_alive() {
1829
+ let ws = tmp_workspace();
1830
+ seed_two_alive_teams_in(&ws);
1831
+ assert!(
1832
+ refuse_if_multi_alive_team_missing_scope("collect", &ws, Some("alpha")).is_ok(),
1833
+ "explicit --team alpha must bypass the ambiguity gate"
1834
+ );
1835
+ }
1836
+
1837
+ #[test]
1838
+ fn refuse_helper_refuses_when_multi_alive_team_and_no_explicit_team() {
1839
+ let ws = tmp_workspace();
1840
+ seed_two_alive_teams_in(&ws);
1841
+ let err = refuse_if_multi_alive_team_missing_scope("repair-state", &ws, None)
1842
+ .expect_err("multi-alive-team must refuse without --team");
1843
+ let message = err.to_string();
1844
+ assert!(
1845
+ message.contains("multiple alive teams"),
1846
+ "refusal must name the ambiguity; got: {message}"
1847
+ );
1848
+ assert!(
1849
+ message.contains("alpha") && message.contains("beta"),
1850
+ "refusal must list candidate teams; got: {message}"
1851
+ );
1852
+ assert!(
1853
+ message.contains("repair-state"),
1854
+ "refusal must name the command for diagnostic clarity; got: {message}"
1855
+ );
1856
+ }
1857
+
1858
+ #[test]
1859
+ fn stuck_cancel_args_builder_refuses_on_multi_alive_team() {
1860
+ let ws = tmp_workspace();
1861
+ seed_two_alive_teams_in(&ws);
1862
+ let argv = cli_argv(&[
1863
+ "worker_a",
1864
+ "--workspace",
1865
+ &ws.to_string_lossy(),
1866
+ ]);
1867
+ let err = stuck_cancel_args(&argv, &ws).expect_err("must refuse");
1868
+ assert!(
1869
+ err.to_string().contains("multiple alive teams"),
1870
+ "stuck-cancel args builder must surface the refusal; got: {err}"
1871
+ );
1872
+ }
1873
+
1874
+ #[test]
1875
+ fn collect_args_builder_refuses_on_multi_alive_team() {
1876
+ let ws = tmp_workspace();
1877
+ seed_two_alive_teams_in(&ws);
1878
+ let argv = cli_argv(&["--workspace", &ws.to_string_lossy()]);
1879
+ let err = collect_args(&argv, &ws).expect_err("must refuse");
1880
+ assert!(
1881
+ err.to_string().contains("multiple alive teams"),
1882
+ "collect args builder must surface the refusal; got: {err}"
1883
+ );
1884
+ }
1885
+
1886
+ #[test]
1887
+ fn repair_state_args_builder_refuses_on_multi_alive_team_before_task_validation() {
1888
+ let ws = tmp_workspace();
1889
+ seed_two_alive_teams_in(&ws);
1890
+ // The ambiguity gate must fire BEFORE `--task` / `--status` validation
1891
+ // so the operator sees the multi-team confusion before any other
1892
+ // usage error.
1893
+ let argv = cli_argv(&["--workspace", &ws.to_string_lossy()]);
1894
+ let err = repair_state_args(&argv, &ws).expect_err("must refuse");
1895
+ let message = err.to_string();
1896
+ assert!(
1897
+ message.contains("multiple alive teams"),
1898
+ "ambiguity gate must precede --task/--status validation; got: {message}"
1899
+ );
1900
+ }
1901
+
1902
+ #[test]
1903
+ fn shutdown_args_builder_refuses_on_multi_alive_team() {
1904
+ let ws = tmp_workspace();
1905
+ seed_two_alive_teams_in(&ws);
1906
+ let argv = cli_argv(&["--workspace", &ws.to_string_lossy()]);
1907
+ let err = shutdown_args(&argv, &ws).expect_err("must refuse");
1908
+ assert!(
1909
+ err.to_string().contains("multiple alive teams"),
1910
+ "shutdown args builder must surface the refusal; got: {err}"
1911
+ );
1912
+ }
1913
+
1914
+ #[test]
1915
+ fn restart_args_builder_refuses_on_multi_alive_team() {
1916
+ let ws = tmp_workspace();
1917
+ seed_two_alive_teams_in(&ws);
1918
+ let argv = cli_argv(&[&ws.to_string_lossy()]);
1919
+ let err = restart_args(&argv, &ws).expect_err("must refuse");
1920
+ assert!(
1921
+ err.to_string().contains("multiple alive teams"),
1922
+ "restart args builder must surface the refusal; got: {err}"
1923
+ );
1924
+ }
1925
+
1926
+ #[test]
1927
+ fn reset_agent_args_builder_refuses_on_multi_alive_team_before_agent_validation() {
1928
+ let ws = tmp_workspace();
1929
+ seed_two_alive_teams_in(&ws);
1930
+ let argv = cli_argv(&["--workspace", &ws.to_string_lossy()]);
1931
+ let err = reset_agent_args(&argv, &ws).expect_err("must refuse");
1932
+ assert!(
1933
+ err.to_string().contains("multiple alive teams"),
1934
+ "reset-agent args builder must surface the refusal; got: {err}"
1935
+ );
1936
+ }
1937
+
1938
+ #[test]
1939
+ fn remove_agent_args_builder_refuses_on_multi_alive_team_before_agent_validation() {
1940
+ let ws = tmp_workspace();
1941
+ seed_two_alive_teams_in(&ws);
1942
+ let argv = cli_argv(&["--workspace", &ws.to_string_lossy()]);
1943
+ let err = remove_agent_args(&argv, &ws).expect_err("must refuse");
1944
+ assert!(
1945
+ err.to_string().contains("multiple alive teams"),
1946
+ "remove-agent args builder must surface the refusal; got: {err}"
1947
+ );
1948
+ }
1949
+
1950
+ // ──────────── Stage QR: quick-start/restart separation ────────────
1951
+ // Design doc: .team/artifacts/quickstart-restart-separation-design.md
1952
+
1953
+ #[test]
1954
+ fn quick_start_refuses_fresh_flag_with_restart_guidance() {
1955
+ // QR contract: `--fresh` is gone from quick-start. The flag is
1956
+ // not advertised or carried in QuickStartArgs, but scripts that
1957
+ // still pass it get a clear redirect to restart --allow-fresh.
1958
+ let ws = tmp_workspace();
1959
+ let argv = cli_argv(&[
1960
+ "--workspace",
1961
+ &ws.to_string_lossy(),
1962
+ "--fresh",
1963
+ ]);
1964
+ let err = quick_start_args(&argv, &ws).expect_err("must refuse --fresh");
1965
+ let message = err.to_string();
1966
+ assert!(
1967
+ message.contains("no longer accepts --fresh"),
1968
+ "QR: refusal must say --fresh is gone; got: {message}"
1969
+ );
1970
+ assert!(
1971
+ message.contains("restart --allow-fresh"),
1972
+ "QR: refusal must redirect to `restart --allow-fresh`; got: {message}"
1973
+ );
1974
+ }
1975
+
1976
+ #[test]
1977
+ fn quick_start_without_fresh_flag_still_builds_args() {
1978
+ // Without --fresh, args build normally (the initial-creation path).
1979
+ let ws = tmp_workspace();
1980
+ let argv = cli_argv(&[
1981
+ "--workspace",
1982
+ &ws.to_string_lossy(),
1983
+ ]);
1984
+ let args = quick_start_args(&argv, &ws).expect("must build");
1985
+ assert_eq!(args.workspace, ws);
1986
+ // No `fresh` field anymore — the struct must compile and round-trip
1987
+ // without it.
1988
+ let _ = args.no_display;
1989
+ }
1727
1990
  }
@@ -117,7 +117,6 @@ pub mod lifecycle_port {
117
117
  name: Option<&str>,
118
118
  team_id: Option<&str>,
119
119
  yes: bool,
120
- fresh: bool,
121
120
  open_display: bool,
122
121
  ) -> Result<Value, CliError> {
123
122
  match crate::lifecycle::quick_start_in_workspace_with_display(
@@ -125,7 +124,6 @@ pub mod lifecycle_port {
125
124
  agents_dir,
126
125
  name,
127
126
  yes,
128
- fresh,
129
127
  team_id,
130
128
  open_display,
131
129
  ) {
@@ -3745,18 +3743,13 @@ pub mod leader_port {
3745
3743
  }
3746
3744
 
3747
3745
  fn team_owner_value(state: &Value, team_id: &TeamKey) -> Option<Value> {
3748
- state
3749
- .get("teams")
3750
- .and_then(|teams| teams.get(team_id.as_str()))
3751
- .and_then(|team| team.get("team_owner"))
3752
- .cloned()
3753
- .or_else(|| {
3754
- if crate::state::projection::team_state_key(state) == team_id.as_str() {
3755
- state.get("team_owner").cloned()
3756
- } else {
3757
- None
3758
- }
3759
- })
3746
+ // Stage 2 (identity-boundary unified plan, architect direction
3747
+ // 2026-06-23): route through the ownership repository so all owner
3748
+ // reads see the same precedence (teams.<key> > top-level when
3749
+ // team_state_key matches). The repository preserves the pre-Stage-2
3750
+ // semantics of this helper and adds an `OwnershipSource` tag that
3751
+ // diagnose/status surfaces can consume in later stages.
3752
+ crate::state::ownership::read_owner_value(state, team_id.as_str()).cloned()
3760
3753
  }
3761
3754
 
3762
3755
  fn family_a_owner_value(
@@ -265,6 +265,16 @@ use rusqlite::params;
265
265
  pub fn approvals(workspace: &Path, agent: Option<&str>, as_json: bool) -> Result<Value, CliError> {
266
266
  let _ = as_json;
267
267
  let state = read_runtime_state(workspace);
268
+ approvals_scoped(workspace, &state, agent, as_json)
269
+ }
270
+
271
+ pub fn approvals_scoped(
272
+ workspace: &Path,
273
+ state: &Value,
274
+ agent: Option<&str>,
275
+ as_json: bool,
276
+ ) -> Result<Value, CliError> {
277
+ let _ = as_json;
268
278
  let session = state.get("session_name").and_then(Value::as_str).filter(|s| !s.is_empty());
269
279
  let mut approvals = Vec::new();
270
280
  if let (Some(session), Some(agents)) = (session, state.get("agents").and_then(Value::as_object)) {
@@ -333,12 +343,13 @@ use rusqlite::params;
333
343
  limit: usize,
334
344
  since: Option<&str>,
335
345
  as_json: bool,
346
+ owner_team_id: Option<&str>,
336
347
  ) -> Result<Value, CliError> {
337
348
  let _ = as_json;
338
349
  let store = crate::message_store::MessageStore::open(workspace)
339
350
  .map_err(|e| CliError::Runtime(e.to_string()))?;
340
351
  let mut messages = store
341
- .inbox(agent, limit, None)
352
+ .inbox(agent, limit, owner_team_id)
342
353
  .map_err(|e| CliError::Runtime(e.to_string()))?;
343
354
  if let Some(cutoff) = since.and_then(parse_rfc3339) {
344
355
  messages.retain(|message| {
@@ -455,11 +455,13 @@ latest result: none";
455
455
  let payload = err.to_payload(Path::new("/tmp/cli-error-123.log"), "quick-start");
456
456
  assert_eq!(payload.reason.as_deref(), Some("tmux_session_name_conflict"));
457
457
  assert_eq!(payload.session_name.as_deref(), Some("my-team"));
458
- // E8 (N38): quick-start 撞已有 runtime 引导到 restart(resume),明确 --fresh 会丢上下文。
458
+ // E8 (N38): quick-start 撞已有 runtime 引导到 restart(resume);
459
+ // context reset is only through restart --allow-fresh with explicit consent.
459
460
  assert_eq!(
460
461
  payload.action,
461
462
  "tmux session `my-team` already exists. It may be your own existing team. \
462
- To resume it use `team-agent restart` (NOT --fresh, which discards context). \
463
+ To resume it use `team-agent restart`. \
464
+ If recovery is impossible, use `team-agent restart --allow-fresh` only after explicit context-loss consent. \
463
465
  Only if you want a separate team, change `name:` in TEAM.md and run quick-start again. \
464
466
  Never terminate existing tmux sessions from quick-start."
465
467
  );
@@ -635,6 +637,7 @@ Truncated: more fallback entries available; run team-agent inbox leader";
635
637
  detail: false,
636
638
  summary: true,
637
639
  json: true,
640
+ team: None,
638
641
  };
639
642
  let err = cmd_status(&args).unwrap_err();
640
643
  assert!(
@@ -652,6 +655,7 @@ Truncated: more fallback entries available; run team-agent inbox leader";
652
655
  detail: false,
653
656
  summary: true,
654
657
  json: false,
658
+ team: None,
655
659
  };
656
660
  let err = cmd_status(&args).unwrap_err();
657
661
  assert!(