@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +24 -317
- package/crates/team-agent/src/cli/diagnose.rs +1 -1
- package/crates/team-agent/src/cli/emit.rs +3 -157
- package/crates/team-agent/src/cli/send.rs +0 -241
- package/crates/team-agent/src/cli/spec.rs +0 -16
- package/crates/team-agent/src/cli/tests/main_preserved.rs +0 -60
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +4 -54
- package/crates/team-agent/src/cli/tests/mod.rs +0 -2
- package/crates/team-agent/src/cli/types.rs +1 -64
- package/crates/team-agent/src/coordinator/health.rs +1 -1
- package/crates/team-agent/src/coordinator/tick.rs +36 -1
- package/crates/team-agent/src/db/agent_health_capture.rs +24 -0
- package/crates/team-agent/src/leader/lease.rs +40 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +14 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +27 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +34 -2
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +1 -1
- package/crates/team-agent/src/packaging/mod.rs +1 -1
- package/crates/team-agent/src/packaging/types.rs +1 -1
- package/package.json +4 -4
- package/skills/team-agent/references/recovery-runbook.md +5 -20
- package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +0 -374
- package/crates/team-agent/src/cli/tests/verb_settle.rs +0 -238
|
@@ -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 —
|
|
42
|
-
// emit.rs
|
|
43
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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 列缺失(可迁移)。`
|
|
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 {
|