@team-agent/installer 0.5.29 → 0.5.32

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.
@@ -615,120 +615,6 @@ fn named_target_kind_wire(kind: crate::cli::named_address::NamedTargetKind) -> &
615
615
  }
616
616
  }
617
617
 
618
- pub fn cmd_fallback_send_leader(args: &FallbackSendLeaderArgs) -> Result<CmdResult, CliError> {
619
- if let Some(value) = fallback_business_refusal(&args.primary_error, args.json) {
620
- return Ok(value);
621
- }
622
- let selected = crate::state::selector::resolve_active_team(
623
- &args.workspace,
624
- args.team.as_deref(),
625
- crate::state::selector::SelectorMode::RuntimeOnly,
626
- )?;
627
- let target = MessageTarget::Single("leader".to_string());
628
- let opts = SendOptions {
629
- task_id: args.task.as_ref().map(|task| TaskId::new(task.clone())),
630
- route_task_id: false,
631
- sender: args.sender.clone(),
632
- requires_ack: false,
633
- wait_visible: false,
634
- block_until_delivered: false,
635
- team: Some(TeamKey::new(selected.team_key.clone())),
636
- message_id: Some(args.message_id.clone()),
637
- ..SendOptions::default()
638
- };
639
- let primary = messaging::send_message(&selected.run_workspace, &target, &args.content, &opts);
640
- let message_id = match &primary {
641
- Ok(outcome) => outcome
642
- .message_id
643
- .clone()
644
- .unwrap_or_else(|| args.message_id.clone()),
645
- Err(_) => args.message_id.clone(),
646
- };
647
- if let Ok(outcome) = &primary {
648
- if is_business_refusal_outcome(outcome) {
649
- let value = json!({
650
- "ok": false,
651
- "status": "refused",
652
- "reason": "business_reject",
653
- "primary_error": args.primary_error,
654
- "message_id": outcome.message_id,
655
- "action": "N38 fallback refused: business rule refusals must not use fallback pane delivery",
656
- });
657
- return Ok(CmdResult::from_json(value, args.json));
658
- }
659
- }
660
-
661
- let state = selected_state_with_active_key(&selected);
662
- let event_log = crate::event_log::EventLog::new(&selected.run_workspace);
663
- let primary_error = match primary {
664
- Ok(outcome) if primary_delivery_succeeded(outcome.status) => {
665
- let mut value = delivery_outcome_json(&outcome, &target, &args.content, &opts);
666
- if let Some(obj) = value.as_object_mut() {
667
- obj.insert("fallback_used".to_string(), json!(false));
668
- obj.insert("primary_error".to_string(), json!(args.primary_error));
669
- }
670
- return Ok(CmdResult::from_json(value, args.json));
671
- }
672
- Ok(outcome) => format!(
673
- "{}; fallback_cli_primary_status={}",
674
- args.primary_error,
675
- delivery_status_wire(outcome.status)
676
- ),
677
- Err(error) => format!("{}; fallback_cli_primary_error={error}", args.primary_error),
678
- };
679
- let outcome = messaging::deliver_to_leader_fallback_pane(
680
- &selected.run_workspace,
681
- &state,
682
- &message_id,
683
- None,
684
- &args.content,
685
- false,
686
- Some(&primary_error),
687
- &event_log,
688
- )?;
689
- let mut value = delivery_outcome_json(&outcome, &target, &args.content, &opts);
690
- if let Some(obj) = value.as_object_mut() {
691
- obj.insert("primary_error".to_string(), json!(args.primary_error));
692
- obj.insert("delivered_via".to_string(), json!("fallback_pane"));
693
- obj.insert(
694
- "next_action".to_string(),
695
- json!("run team-agent restart-agent to refresh the worker MCP transport"),
696
- );
697
- }
698
- Ok(CmdResult::from_json(value, args.json))
699
- }
700
-
701
- pub fn cmd_fallback_report_result(args: &FallbackReportResultArgs) -> Result<CmdResult, CliError> {
702
- if let Some(value) = fallback_business_refusal(&args.primary_error, args.json) {
703
- return Ok(value);
704
- }
705
- let selected = crate::state::selector::resolve_active_team(
706
- &args.workspace,
707
- args.team.as_deref(),
708
- crate::state::selector::SelectorMode::RuntimeOnly,
709
- )?;
710
- let envelope = fallback_result_envelope(args)?;
711
- let value = messaging::report_result_for_owner_team_with_primary_error(
712
- &selected.run_workspace,
713
- &envelope,
714
- Some(&selected.team_key),
715
- Some(&args.primary_error),
716
- )?;
717
- let mut value = value;
718
- if let Some(obj) = value.as_object_mut() {
719
- obj.insert("primary_error".to_string(), json!(args.primary_error));
720
- obj.insert(
721
- "fallback_protocol".to_string(),
722
- json!("fallback-report-result"),
723
- );
724
- obj.insert(
725
- "next_action".to_string(),
726
- json!("run team-agent restart-agent to refresh the worker MCP transport"),
727
- );
728
- }
729
- Ok(CmdResult::from_json(value, args.json))
730
- }
731
-
732
618
  fn routing_ambiguous_value(
733
619
  workspace: &Path,
734
620
  args: &SendArgs,
@@ -783,69 +669,6 @@ fn selected_state_with_active_key(selected: &crate::state::selector::SelectedTea
783
669
  state
784
670
  }
785
671
 
786
- fn fallback_result_envelope(args: &FallbackReportResultArgs) -> Result<Value, CliError> {
787
- let mut envelope: Value = serde_json::from_str(&args.result_json)?;
788
- let Some(obj) = envelope.as_object_mut() else {
789
- return Err(CliError::Usage(
790
- "--result-json must be a JSON object".to_string(),
791
- ));
792
- };
793
- obj.entry("schema_version".to_string())
794
- .or_insert_with(|| json!("result_envelope_v1"));
795
- obj.entry("task_id".to_string())
796
- .or_insert_with(|| json!(args.task_id));
797
- obj.entry("agent_id".to_string())
798
- .or_insert_with(|| json!(args.agent_id));
799
- obj.entry("status".to_string())
800
- .or_insert_with(|| json!("success"));
801
- obj.entry("summary".to_string())
802
- .or_insert_with(|| json!("completed"));
803
- for key in ["changes", "tests", "risks", "artifacts", "next_actions"] {
804
- obj.entry(key.to_string()).or_insert_with(|| json!([]));
805
- }
806
- Ok(envelope)
807
- }
808
-
809
- fn fallback_business_refusal(primary_error: &str, as_json: bool) -> Option<CmdResult> {
810
- is_business_reject_text(primary_error).then(|| {
811
- CmdResult::from_json(
812
- json!({
813
- "ok": false,
814
- "status": "refused",
815
- "reason": "business_reject",
816
- "primary_error": primary_error,
817
- "action": "N38 fallback refused: business rule refusals must not use fallback pane delivery",
818
- }),
819
- as_json,
820
- )
821
- })
822
- }
823
-
824
- fn is_business_refusal_outcome(outcome: &DeliveryOutcome) -> bool {
825
- matches!(
826
- outcome.reason,
827
- Some(
828
- DeliveryRefusal::TargetNotInTeam
829
- | DeliveryRefusal::HumanConfirmationRequired
830
- | DeliveryRefusal::MissingPermissions
831
- | DeliveryRefusal::UnknownRecipient
832
- | DeliveryRefusal::TeamOwnerMismatch
833
- | DeliveryRefusal::Ambiguous
834
- | DeliveryRefusal::RecipientPaneInNonInputMode
835
- | DeliveryRefusal::SessionDrift
836
- | DeliveryRefusal::RoutingAmbiguous
837
- | DeliveryRefusal::EmptyTargetList
838
- )
839
- )
840
- }
841
-
842
- fn primary_delivery_succeeded(status: DeliveryStatus) -> bool {
843
- matches!(
844
- status,
845
- DeliveryStatus::Delivered | DeliveryStatus::AlreadyDelivered
846
- )
847
- }
848
-
849
672
  fn initial_delivery_allows_watch(status: DeliveryStatus) -> bool {
850
673
  matches!(
851
674
  status,
@@ -888,27 +711,6 @@ fn observe_initial_delivery_for_watch(
888
711
  .map_err(CliError::from)
889
712
  }
890
713
 
891
- fn is_business_reject_text(error: &str) -> bool {
892
- let lower = error.to_ascii_lowercase();
893
- [
894
- "peer_not_in_scope",
895
- "target_not_in_team",
896
- "permission denied",
897
- "missing_permissions",
898
- "human_confirmation_required",
899
- "unknown_recipient",
900
- "routing_ambiguous",
901
- "quota",
902
- "rate limit",
903
- "rate_limit",
904
- "blacklist",
905
- "blacklisted",
906
- "forbidden",
907
- ]
908
- .iter()
909
- .any(|needle| lower.contains(needle))
910
- }
911
-
912
714
  /// `_send_target`(`commands.py:181-184`):`--to` comma-split fanout / `target` 单值 / None。
913
715
  pub fn send_target(targets: Option<&str>, target: Option<&str>) -> MessageTarget {
914
716
  if let Some(targets) = targets.filter(|s| !s.is_empty()) {
@@ -1488,46 +1290,3 @@ pub fn send_to_canonical_leader_target(
1488
1290
  }
1489
1291
  Ok(value)
1490
1292
  }
1491
-
1492
- #[cfg(test)]
1493
- mod e23_tests {
1494
- use super::*;
1495
-
1496
- #[test]
1497
- fn fallback_error_classifier_allows_transport_and_primary_bugs() {
1498
- for error in [
1499
- "Transport closed",
1500
- "Connection refused",
1501
- "Broken pipe",
1502
- "EOF on transport",
1503
- "MCP timeout after 5s",
1504
- "internal assertion failed: unwrap on Err",
1505
- "primary_delivery_error: serialize failed",
1506
- ] {
1507
- assert!(
1508
- !is_business_reject_text(error),
1509
- "failure should be fallback-eligible, not classified as a business refusal: {error}"
1510
- );
1511
- }
1512
- }
1513
-
1514
- #[test]
1515
- fn fallback_error_classifier_blocks_business_refusals() {
1516
- for error in [
1517
- "peer_not_in_scope",
1518
- "target_not_in_team",
1519
- "permission denied",
1520
- "missing_permissions",
1521
- "human_confirmation_required",
1522
- "unknown_recipient",
1523
- "quota exceeded",
1524
- "rate_limit",
1525
- "blacklisted target",
1526
- ] {
1527
- assert!(
1528
- is_business_reject_text(error),
1529
- "business refusal must not use fallback pane delivery: {error}"
1530
- );
1531
- }
1532
- }
1533
- }
@@ -45,8 +45,6 @@ pub(crate) enum DispatchKind {
45
45
  QuickStart,
46
46
  Compile,
47
47
  Send,
48
- FallbackSendLeader,
49
- FallbackReportResult,
50
48
  AllowPeerTalk,
51
49
  Status,
52
50
  Stop,
@@ -76,10 +74,7 @@ pub(crate) enum DispatchKind {
76
74
  Validate,
77
75
  InstallSkill,
78
76
  Profile,
79
- ValidateResult,
80
77
  Collect,
81
- Settle,
82
- RepairState,
83
78
  Diagnose,
84
79
  Preflight,
85
80
  WaitReady,
@@ -93,8 +88,6 @@ pub(crate) const ALL_DISPATCH_KINDS: &[DispatchKind] = &[
93
88
  DispatchKind::QuickStart,
94
89
  DispatchKind::Compile,
95
90
  DispatchKind::Send,
96
- DispatchKind::FallbackSendLeader,
97
- DispatchKind::FallbackReportResult,
98
91
  DispatchKind::AllowPeerTalk,
99
92
  DispatchKind::Status,
100
93
  DispatchKind::Stop,
@@ -124,10 +117,7 @@ pub(crate) const ALL_DISPATCH_KINDS: &[DispatchKind] = &[
124
117
  DispatchKind::Validate,
125
118
  DispatchKind::InstallSkill,
126
119
  DispatchKind::Profile,
127
- DispatchKind::ValidateResult,
128
120
  DispatchKind::Collect,
129
- DispatchKind::Settle,
130
- DispatchKind::RepairState,
131
121
  DispatchKind::Diagnose,
132
122
  DispatchKind::Preflight,
133
123
  DispatchKind::WaitReady,
@@ -185,15 +175,9 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
185
175
  CommandSpec { name: "start", tier: CommandTier::CompatHidden, category: CommandCategory::Compat, kind: CommandKind::SpecOnlyAlias { alias_of: "quick-start" }, summary: "legacy start alias", usage: "usage: team-agent start [TEAMDIR] [--yes] [--fresh] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::Conditional, alias_of: Some("quick-start"), sunset: Some("C2"), action: Some("use `team-agent quick-start` or `team-agent restart`"), governance: None },
186
176
  CommandSpec { name: "stop", tier: CommandTier::CompatHidden, category: CommandCategory::Compat, kind: CommandKind::Dispatch(DispatchKind::Stop), summary: "legacy shutdown alias", usage: "usage: team-agent stop [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: Some("shutdown"), sunset: Some("C2"), action: Some("use `team-agent shutdown`"), governance: None },
187
177
  CommandSpec { name: "restart-agent", tier: CommandTier::CompatHidden, category: CommandCategory::Compat, kind: CommandKind::Dispatch(DispatchKind::RestartAgent), summary: "legacy reset-agent alias", usage: "usage: team-agent restart-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--discard-session] [--no-display] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::Conditional, alias_of: Some("reset-agent"), sunset: Some("C2"), action: Some("use `team-agent reset-agent`"), governance: None },
188
- CommandSpec { name: "fallback-send-leader", tier: CommandTier::CompatHidden, category: CommandCategory::Compat, kind: CommandKind::Dispatch(DispatchKind::FallbackSendLeader), summary: "legacy leader send fallback", usage: "usage: team-agent fallback-send-leader --workspace WORKSPACE --team TEAM --sender AGENT --message-id ID --content TEXT --primary-error ERROR [--task TASK] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: Some("C2"), action: Some("use `team-agent send` and `team-agent diagnose`"), governance: None },
189
- CommandSpec { name: "fallback-report-result", tier: CommandTier::CompatHidden, category: CommandCategory::Compat, kind: CommandKind::Dispatch(DispatchKind::FallbackReportResult), summary: "legacy report_result fallback", usage: "usage: team-agent fallback-report-result --workspace WORKSPACE --team TEAM --agent-id AGENT --task-id TASK --result-json JSON --primary-error ERROR [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: Some("C2"), action: Some("use `team-agent collect` or MCP `report_result`"), governance: None },
190
- CommandSpec { name: "settle", tier: CommandTier::CompatHidden, category: CommandCategory::Compat, kind: CommandKind::Dispatch(DispatchKind::Settle), summary: "legacy settle helper", usage: "usage: team-agent settle [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: Some("C2"), action: Some("use `team-agent collect`"), governance: None },
191
- CommandSpec { name: "validate-result", tier: CommandTier::CompatHidden, category: CommandCategory::Compat, kind: CommandKind::Dispatch(DispatchKind::ValidateResult), summary: "legacy result validation helper", usage: "usage: team-agent validate-result [ENVELOPE] [--file FILE|--result JSON] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: Some("C2"), action: Some("use report_result validation through normal `team-agent collect` flow"), governance: None },
192
178
  CommandSpec { name: "stuck-list", tier: CommandTier::CompatHidden, category: CommandCategory::Compat, kind: CommandKind::Dispatch(DispatchKind::StuckList), summary: "legacy stuck alert listing", usage: "usage: team-agent stuck-list [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: Some("C2"), action: Some("use `team-agent diagnose`"), governance: None },
193
179
  CommandSpec { name: "stuck-cancel", tier: CommandTier::CompatHidden, category: CommandCategory::Compat, kind: CommandKind::Dispatch(DispatchKind::StuckCancel), summary: "legacy stuck alert suppression", usage: "usage: team-agent stuck-cancel AGENT [--workspace WORKSPACE] [--alert-type stuck|idle_fallback|cross_worker_deadlock|all] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: Some("C2"), action: Some("use the concrete notification action or `team-agent diagnose`"), governance: None },
194
180
  CommandSpec { name: "acknowledge-idle", tier: CommandTier::CompatHidden, category: CommandCategory::Compat, kind: CommandKind::Dispatch(DispatchKind::AcknowledgeIdle), summary: "legacy idle acknowledgement", usage: "usage: team-agent acknowledge-idle [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: Some("C2"), action: Some("use the concrete idle notification action or `team-agent diagnose`"), governance: None },
195
-
196
- CommandSpec { name: "repair-state", tier: CommandTier::Guided, category: CommandCategory::GuidedRecovery, kind: CommandKind::Dispatch(DispatchKind::RepairState), summary: "guided state repair helper", usage: "usage: team-agent repair-state --task TASK --status STATUS [SUMMARY] [--assignee AGENT] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
197
181
  CommandSpec { name: "leaders", tier: CommandTier::Secondary, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Leaders), summary: "inspect registered host leaders", usage: "usage: team-agent leaders [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
198
182
  CommandSpec { name: "doctor", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::Doctor), summary: "run host maintenance checks", usage: "usage: team-agent doctor [SPEC] [--workspace WORKSPACE] [--team TEAM] [--gate orphans|comms] [--comms] [--fix] [--fix-schema] [--cleanup-orphans] [--confirm] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
199
183
  CommandSpec { name: "attach-app-server-leader", tier: CommandTier::Guided, category: CommandCategory::GuidedRecovery, kind: CommandKind::Dispatch(DispatchKind::AttachAppServerLeader), summary: "bind an app-server leader receiver", usage: "usage: team-agent attach-app-server-leader [--workspace WORKSPACE] [--team TEAM] --socket unix:///path.sock --thread-id THREAD_ID [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
@@ -597,27 +597,6 @@ fn seed_team_spec(ws: &std::path::Path) {
597
597
  );
598
598
  }
599
599
  #[test]
600
- fn validate_result_file_good_and_inline_garbage_are_distinct() {
601
- let ws = tmp_workspace();
602
- let envelope_path = ws.join("result.json");
603
- std::fs::write(&envelope_path, valid_result_envelope().to_string()).unwrap();
604
- let good = run(
605
- &cli_argv(&["validate-result", "--file", &envelope_path.to_string_lossy(), "--json"]),
606
- &ws,
607
- );
608
- assert_eq!(
609
- good,
610
- ExitCode::Ok,
611
- "Python cmd_validate_result accepts --file and returns {{ok:true,task_id,agent_id,status}} for a valid envelope"
612
- );
613
- let garbage = run(&cli_argv(&["validate-result", "{garbage", "--json"]), &ws);
614
- assert_eq!(
615
- garbage,
616
- ExitCode::Error,
617
- "garbage JSON must be invalid, not indistinguishable from the good-envelope path"
618
- );
619
- }
620
- #[test]
621
600
  fn collect_uncollected_result_marks_db_and_outputs_result() {
622
601
  let ws = tmp_workspace();
623
602
  seed_collect_state(&ws);
@@ -784,42 +763,3 @@ fn seed_team_spec(ws: &std::path::Path) {
784
763
  "acknowledge-idle must emit coordinator.idle_acknowledged"
785
764
  );
786
765
  }
787
- #[test]
788
- fn repair_state_done_persists_after_status_and_summary() {
789
- let ws = tmp_workspace();
790
- seed_collect_state(&ws);
791
- let out = json_output(
792
- cmd_repair_state(&RepairStateArgs {
793
- workspace: ws.clone(),
794
- task_id: "task_impl".to_string(),
795
- assignee: None,
796
- status: "done".to_string(),
797
- summary: Some("manual repair accepted".to_string()),
798
- json: true,
799
- team: None,
800
- })
801
- .expect("repair-state should not fail on a valid runtime state"),
802
- );
803
- assert_eq!(out["ok"], json!(true));
804
- assert_eq!(
805
- out["after"]["status"],
806
- json!("done"),
807
- "repair-state --status done must return after.status=done; ok:true with null after fields is a false success"
808
- );
809
- assert_eq!(out["after"]["assignee"], json!("fake_impl"));
810
- assert_eq!(out["after"]["last_result_summary"], json!("manual repair accepted"));
811
- let state = read_state(&ws);
812
- let task = state["tasks"]
813
- .as_array()
814
- .unwrap()
815
- .iter()
816
- .find(|task| task["id"] == json!("task_impl"))
817
- .unwrap();
818
- assert_eq!(
819
- task["status"],
820
- json!("done"),
821
- "repair-state --status done must persist the task status, not only emit a success envelope"
822
- );
823
- assert_eq!(task["last_result_summary"], json!("manual repair accepted"));
824
- let _ = std::fs::remove_dir_all(&ws);
825
- }
@@ -38,9 +38,9 @@ impl Drop for WorkspaceCleanup {
38
38
  }
39
39
 
40
40
  // =========================================================================
41
- // WAVE-2 NON-SUB CHECKPOINT — 9 MISSING CLI subcommands (ABSENT from cli/emit.rs dispatch).
42
- // emit.rs:53-78 `dispatch` has NO arm for: sessions, peek, collect, e2e, diagnose, repair-state,
43
- // validate-result, preflight, wait-ready -> they fall to `_ => Ok(ExitCode::Error)`. These REDs
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
44
  // assert the dispatch ROUTES each subcommand: `run([sub,...]) == ExitCode::Ok` for a golden
45
45
  // EXIT-0 scenario (today unrouted -> ExitCode::Error -> RED; green once the porter adds the
46
46
  // dispatch arm + handler). Golden exit codes + JSON shapes probed via `python3 -m team_agent <sub>`.
@@ -63,7 +63,7 @@ impl Drop for WorkspaceCleanup {
63
63
 
64
64
  /// The exact golden fake spec (cli/e2e.py `_fake_spec`, dumped via simple_yaml) with the literal
65
65
  /// `/WS` placeholder substituted to the real workspace — the minimal VALID team.spec.yaml that
66
- /// `collect`/`repair-state` (load_spec) accept. (load_spec rejects partial specs: requires
66
+ /// `collect` accepts. (load_spec rejects partial specs: requires
67
67
  /// communication/context/leader/routing/runtime/tasks.)
68
68
  const FAKE_SPEC_YAML: &str = r#"version: 1
69
69
  team:
@@ -214,22 +214,6 @@ tasks:
214
214
  let _ = std::fs::remove_dir_all(&ws);
215
215
  }
216
216
 
217
- // ── validate-result ── golden parser.py:312 `cmd_validate_result` (commands.py:206). A FULL valid
218
- // result_envelope_v1 -> {"agent_id":"a1","ok":true,"status":"success","task_id":"t1"} EXIT 0.
219
- // RED: unrouted -> Error. (A partial envelope golden-exits 1 — that would be false-green, so the
220
- // RED uses the complete envelope that golden accepts.)
221
- #[test]
222
- fn dispatch_routes_validate_result_valid_envelope() {
223
- let envelope = r#"{"schema_version":"result_envelope_v1","task_id":"t1","agent_id":"a1","status":"success","summary":"done","artifacts":[],"changes":[],"tests":[],"risks":[],"next_actions":[]}"#;
224
- let code = run(&cli_argv(&["validate-result", envelope, "--json"]), std::path::Path::new("."));
225
- assert_eq!(
226
- code,
227
- ExitCode::Ok,
228
- "`validate-result <valid envelope> --json` must ROUTE to cmd_validate_result (parser.py:312) \
229
- and exit 0 with {{agent_id,ok,status,task_id}}; today -> unknown-subcommand Error"
230
- );
231
- }
232
-
233
217
  // ── collect ── golden parser.py:292 `cmd_collect` -> runtime.collect(ws). With a valid
234
218
  // team.spec.yaml present and nothing to collect -> EXIT 0, golden:
235
219
  // {"collected":[],"collected_results":[],"coordinator":{"ok":false,"status":"not_required"},
@@ -265,40 +249,6 @@ tasks:
265
249
  );
266
250
  }
267
251
 
268
- // ── repair-state ── golden parser.py:303 `cmd_repair_state` -> runtime.repair_state (quick_start.py:285).
269
- // `--task <id>` required; `--status` must be in TASK_STATUSES{blocked,cancelled,done,failed,
270
- // needs_retry,pending,ready,running}. With a seeded task + --status done -> EXIT 0:
271
- // {"after":{...},"before":{...},"ok":true,"state_file":"<ws>/team_state.md","task_id":"fake_impl"}
272
- // RED: unrouted -> Error.
273
- #[test]
274
- fn dispatch_routes_repair_state_with_task() {
275
- let ws = tmp_workspace();
276
- seed_team_spec(&ws);
277
- std::fs::write(
278
- ws.join(".team").join("runtime").join("state.json"),
279
- serde_json::to_vec(&json!({
280
- "leader": {"id": "leader"},
281
- "tasks": [{"id": "fake_impl", "title": "impl", "status": "open", "assignee": "fake_impl", "type": "implementation"}],
282
- }))
283
- .unwrap(),
284
- )
285
- .unwrap();
286
- let code = run(
287
- &cli_argv(&[
288
- "repair-state", "--workspace", &ws.to_string_lossy(),
289
- "--task", "fake_impl", "--status", "done", "--summary", "ok", "--json",
290
- ]),
291
- &ws,
292
- );
293
- assert_eq!(
294
- code,
295
- ExitCode::Ok,
296
- "`repair-state --task <id> --status done` must ROUTE to cmd_repair_state (parser.py:303) and \
297
- exit 0 {{after,before,ok,state_file,task_id}}; today -> unknown-subcommand Error"
298
- );
299
- let _ = std::fs::remove_dir_all(&ws);
300
- }
301
-
302
252
  // ── diagnose ── golden parser.py:298 `cmd_diagnose` -> runtime.diagnose(ws) (diagnose/health.py:19).
303
253
  // ok:true only when there are zero issues. Seed leader_receiver=attached + NO session_name + NO
304
254
  // agents -> issues=[] -> EXIT 0, golden top keys (sort): event_log,issues,ok,runtime,suggested_repairs.
@@ -88,12 +88,10 @@ mod lane_c;
88
88
  mod leader_watch;
89
89
  mod missing_subcommands;
90
90
  mod named_address;
91
- mod repair_state_byte_lock;
92
91
  mod peer_allow;
93
92
  mod run_delegation;
94
93
  mod status_send;
95
94
  mod verb_validate;
96
- mod verb_settle;
97
95
  mod verb_profile;
98
96
  mod verb_install_skill;
99
97
  mod main_preserved;
@@ -49,7 +49,7 @@ pub enum CliError {
49
49
  /// I/O(cli-error 落盘、inbox 游标读写)。bug-084:写路径降级,不裸 panic。
50
50
  #[error("io: {0}")]
51
51
  Io(#[from] std::io::Error),
52
- /// JSON 解析(`validate-result` 读 envelope)。
52
+ /// JSON 解析。
53
53
  #[error("json: {0}")]
54
54
  Json(#[from] serde_json::Error),
55
55
  }
@@ -341,33 +341,6 @@ pub struct SendArgs {
341
341
  pub to_leader: Option<String>,
342
342
  }
343
343
 
344
- /// E23 worker-side emergency fallback for `team_orchestrator.send_message`
345
- /// transport failures. This is not a general control-plane send path.
346
- #[derive(Debug, Clone, PartialEq, Eq)]
347
- pub struct FallbackSendLeaderArgs {
348
- pub workspace: PathBuf,
349
- pub team: Option<String>,
350
- pub sender: String,
351
- pub task: Option<String>,
352
- pub message_id: String,
353
- pub content: String,
354
- pub primary_error: String,
355
- pub json: bool,
356
- }
357
-
358
- /// E23 worker-side emergency fallback for `team_orchestrator.report_result`
359
- /// transport failures. It must still persist through the results DB.
360
- #[derive(Debug, Clone, PartialEq, Eq)]
361
- pub struct FallbackReportResultArgs {
362
- pub workspace: PathBuf,
363
- pub team: Option<String>,
364
- pub agent_id: String,
365
- pub task_id: String,
366
- pub result_json: String,
367
- pub primary_error: String,
368
- pub json: bool,
369
- }
370
-
371
344
  /// `allow-peer-talk`(`parser.py`): allow direct peer communication between two agents.
372
345
  #[derive(Debug, Clone, PartialEq, Eq)]
373
346
  pub struct AllowPeerTalkArgs {
@@ -649,15 +622,6 @@ pub struct ProfileArgs {
649
622
  pub json: bool,
650
623
  }
651
624
 
652
- /// `validate-result`(`parser.py:312`)。
653
- #[derive(Debug, Clone, PartialEq, Eq)]
654
- pub struct ValidateResultArgs {
655
- pub envelope: Option<String>,
656
- pub file: Option<PathBuf>,
657
- pub result: Option<String>,
658
- pub json: bool,
659
- }
660
-
661
625
  /// `collect`(`parser.py:292`)。
662
626
  #[derive(Debug, Clone, PartialEq, Eq)]
663
627
  pub struct CollectArgs {
@@ -670,33 +634,6 @@ pub struct CollectArgs {
670
634
  pub team: Option<String>,
671
635
  }
672
636
 
673
- /// `settle`(`parser.py:177`)。
674
- #[derive(Debug, Clone, PartialEq, Eq)]
675
- pub struct SettleArgs {
676
- pub workspace: PathBuf,
677
- /// Bug #6 (prerelease 0.4.0 gate review): explicit team scope. When
678
- /// `Some`, settle scopes collect/status/team-state to this team via
679
- /// `resolve_active_team(workspace, Some(team), ...)`. When `None`,
680
- /// the selector falls back to top-level `active_team_key` (legacy).
681
- pub team: Option<String>,
682
- pub json: bool,
683
- }
684
-
685
- /// `repair-state`(`parser.py:303`)。
686
- #[derive(Debug, Clone, PartialEq, Eq)]
687
- pub struct RepairStateArgs {
688
- pub workspace: PathBuf,
689
- pub task_id: String,
690
- pub assignee: Option<String>,
691
- pub status: String,
692
- pub summary: Option<String>,
693
- pub json: bool,
694
- /// Stage 4: explicit `--team` scope. Destructive (rewrites task state);
695
- /// CLI dispatch refuses bare invocation in a multi-alive-team
696
- /// workspace.
697
- pub team: Option<String>,
698
- }
699
-
700
637
  /// `diagnose`(`parser.py:298`) runtime health report, distinct from `doctor`.
701
638
  #[derive(Debug, Clone, PartialEq, Eq)]
702
639
  pub struct DiagnoseArgs {
@@ -853,7 +853,7 @@ pub(crate) fn message_store_schema_health(workspace: &WorkspacePath) -> SchemaHe
853
853
  error: Some(SchemaError::InitFailed {
854
854
  message: e.to_string(),
855
855
  }),
856
- action: Some("run team-agent repair-state --schema".to_string()),
856
+ action: Some("run team-agent doctor --fix-schema --json".to_string()),
857
857
  },
858
858
  }
859
859
  }
@@ -1319,7 +1319,7 @@ impl Coordinator {
1319
1319
  }
1320
1320
 
1321
1321
  /// `message_store_schema_health`(`lifecycle.py:197`)。DB 列兼容门:区分 pre-init 必需列缺失
1322
- /// (拒启)vs migratable 列缺失(可迁移)。`advanced repair-state --schema` 用其 action hint。
1322
+ /// (拒启)vs migratable 列缺失(可迁移)。`doctor --fix-schema` 用其 action hint。
1323
1323
  pub fn schema_health(&self) -> SchemaHealth {
1324
1324
  // A-8: the gate must inspect the REAL team.db (Python lifecycle.py:197+
1325
1325
  // message_store_schema_health); a hardcoded ok:true left the card §89
@@ -1807,6 +1807,31 @@ fn parse_rfc3339_utc(raw: &str) -> Option<chrono::DateTime<chrono::Utc>> {
1807
1807
  .map(|value| value.with_timezone(&chrono::Utc))
1808
1808
  }
1809
1809
 
1810
+ /// 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
1811
+ /// return true when the rollout file's modification time is not newer than
1812
+ /// the agent's current `spawned_at`. Used by `jsonl_activity_for_agent` to
1813
+ /// refuse classifying a pre-spawn transcript tail into fresh-cohort activity.
1814
+ /// Missing `spawned_at`, missing mtime, or unparseable timestamp → false
1815
+ /// (do not block classification; the caller keeps the pre-0.5.32 behavior).
1816
+ fn jsonl_older_than_spawn_boundary(agent: &Value, metadata: &std::fs::Metadata) -> bool {
1817
+ let Some(spawned_at) = agent
1818
+ .get("spawned_at")
1819
+ .and_then(Value::as_str)
1820
+ .and_then(parse_rfc3339_utc)
1821
+ else {
1822
+ return false;
1823
+ };
1824
+ let Ok(mtime) = metadata.modified() else {
1825
+ return false;
1826
+ };
1827
+ let Ok(mtime_since_epoch) = mtime.duration_since(std::time::UNIX_EPOCH) else {
1828
+ return false;
1829
+ };
1830
+ let mtime_utc =
1831
+ chrono::DateTime::<chrono::Utc>::from(std::time::UNIX_EPOCH + mtime_since_epoch);
1832
+ mtime_utc <= spawned_at
1833
+ }
1834
+
1810
1835
  fn matching_capture_pane_info(
1811
1836
  agent: &Value,
1812
1837
  session: &crate::transport::SessionName,
@@ -1902,6 +1927,16 @@ fn jsonl_activity_for_agent(agent: &Value) -> Option<crate::messaging::AgentActi
1902
1927
  let metadata = std::fs::metadata(&rollout_path).ok()?;
1903
1928
  let size = metadata.len();
1904
1929
  let mtime_ns = crate::coordinator::steps::abnormal::metadata_mtime_ns(&metadata)?;
1930
+ // 0.5.32 (`.team/artifacts/restart-resumed-stale-activity-locate.md` §5):
1931
+ // freshness gate — a rollout file whose last modification is older than
1932
+ // the agent's current `spawned_at` cannot describe post-spawn behavior.
1933
+ // Refusing to classify (return None) here keeps stale pre-restart
1934
+ // Working tails from re-poisoning a fresh cohort's activity/worker_state;
1935
+ // pane fallback in the caller is still free to produce a fresh
1936
+ // classification when a live pane observation exists.
1937
+ if jsonl_older_than_spawn_boundary(agent, &metadata) {
1938
+ return None;
1939
+ }
1905
1940
  if let Ok(cache) = jsonl_activity_cache().lock() {
1906
1941
  if let Some(entry) = cache.get(&rollout_path) {
1907
1942
  if entry.size == size && entry.mtime_ns == mtime_ns {