@team-agent/installer 0.5.28 → 0.5.29
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/diagnose.rs +1 -1
- package/crates/team-agent/src/cli/emit.rs +211 -59
- package/crates/team-agent/src/cli/mod.rs +45 -19
- package/crates/team-agent/src/cli/send.rs +19 -20
- package/crates/team-agent/src/cli/spec.rs +222 -0
- package/crates/team-agent/src/cli/status.rs +29 -0
- package/crates/team-agent/src/cli/status_port.rs +36 -4
- package/crates/team-agent/src/leader/lease.rs +127 -27
- package/crates/team-agent/src/lifecycle/launch.rs +6 -1
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +85 -0
- package/crates/team-agent/src/state/mod.rs +1 -0
- package/crates/team-agent/src/state/repository.rs +387 -0
- package/crates/team-agent/src/topology.rs +130 -5
- package/package.json +4 -4
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
//! C1 command registry: one catalog for help visibility, known-command gates,
|
|
2
|
+
//! suggestion candidates, and command-tier governance.
|
|
3
|
+
|
|
4
|
+
#![allow(dead_code)]
|
|
5
|
+
|
|
6
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
7
|
+
pub(crate) enum CommandTier {
|
|
8
|
+
Core,
|
|
9
|
+
Guided,
|
|
10
|
+
Secondary,
|
|
11
|
+
DevInternal,
|
|
12
|
+
CompatHidden,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
16
|
+
pub(crate) enum CommandCategory {
|
|
17
|
+
Start,
|
|
18
|
+
Daily,
|
|
19
|
+
TeamLifecycle,
|
|
20
|
+
WorkerLifecycle,
|
|
21
|
+
GuidedRecovery,
|
|
22
|
+
Setup,
|
|
23
|
+
Observe,
|
|
24
|
+
Dev,
|
|
25
|
+
Compat,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
29
|
+
pub(crate) enum CommandKind {
|
|
30
|
+
Dispatch(DispatchKind),
|
|
31
|
+
LeaderPassthrough { provider: &'static str },
|
|
32
|
+
SpecOnlyAlias { alias_of: &'static str },
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
36
|
+
pub(crate) enum TokenUsage {
|
|
37
|
+
No,
|
|
38
|
+
Yes,
|
|
39
|
+
Conditional,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
43
|
+
pub(crate) enum DispatchKind {
|
|
44
|
+
Init,
|
|
45
|
+
QuickStart,
|
|
46
|
+
Compile,
|
|
47
|
+
Send,
|
|
48
|
+
FallbackSendLeader,
|
|
49
|
+
FallbackReportResult,
|
|
50
|
+
AllowPeerTalk,
|
|
51
|
+
Status,
|
|
52
|
+
Stop,
|
|
53
|
+
Shutdown,
|
|
54
|
+
Restart,
|
|
55
|
+
RestartAgent,
|
|
56
|
+
StartAgent,
|
|
57
|
+
StopAgent,
|
|
58
|
+
ResetAgent,
|
|
59
|
+
AddAgent,
|
|
60
|
+
ForkAgent,
|
|
61
|
+
RemoveAgent,
|
|
62
|
+
StuckList,
|
|
63
|
+
StuckCancel,
|
|
64
|
+
AcknowledgeIdle,
|
|
65
|
+
Takeover,
|
|
66
|
+
ClaimLeader,
|
|
67
|
+
AttachLeader,
|
|
68
|
+
AttachAppServerLeader,
|
|
69
|
+
Identity,
|
|
70
|
+
Approvals,
|
|
71
|
+
Inbox,
|
|
72
|
+
Doctor,
|
|
73
|
+
Watch,
|
|
74
|
+
Sessions,
|
|
75
|
+
Leaders,
|
|
76
|
+
Validate,
|
|
77
|
+
InstallSkill,
|
|
78
|
+
Profile,
|
|
79
|
+
ValidateResult,
|
|
80
|
+
Collect,
|
|
81
|
+
Settle,
|
|
82
|
+
RepairState,
|
|
83
|
+
Diagnose,
|
|
84
|
+
Preflight,
|
|
85
|
+
WaitReady,
|
|
86
|
+
E2e,
|
|
87
|
+
Peek,
|
|
88
|
+
Coordinator,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pub(crate) const ALL_DISPATCH_KINDS: &[DispatchKind] = &[
|
|
92
|
+
DispatchKind::Init,
|
|
93
|
+
DispatchKind::QuickStart,
|
|
94
|
+
DispatchKind::Compile,
|
|
95
|
+
DispatchKind::Send,
|
|
96
|
+
DispatchKind::FallbackSendLeader,
|
|
97
|
+
DispatchKind::FallbackReportResult,
|
|
98
|
+
DispatchKind::AllowPeerTalk,
|
|
99
|
+
DispatchKind::Status,
|
|
100
|
+
DispatchKind::Stop,
|
|
101
|
+
DispatchKind::Shutdown,
|
|
102
|
+
DispatchKind::Restart,
|
|
103
|
+
DispatchKind::RestartAgent,
|
|
104
|
+
DispatchKind::StartAgent,
|
|
105
|
+
DispatchKind::StopAgent,
|
|
106
|
+
DispatchKind::ResetAgent,
|
|
107
|
+
DispatchKind::AddAgent,
|
|
108
|
+
DispatchKind::ForkAgent,
|
|
109
|
+
DispatchKind::RemoveAgent,
|
|
110
|
+
DispatchKind::StuckList,
|
|
111
|
+
DispatchKind::StuckCancel,
|
|
112
|
+
DispatchKind::AcknowledgeIdle,
|
|
113
|
+
DispatchKind::Takeover,
|
|
114
|
+
DispatchKind::ClaimLeader,
|
|
115
|
+
DispatchKind::AttachLeader,
|
|
116
|
+
DispatchKind::AttachAppServerLeader,
|
|
117
|
+
DispatchKind::Identity,
|
|
118
|
+
DispatchKind::Approvals,
|
|
119
|
+
DispatchKind::Inbox,
|
|
120
|
+
DispatchKind::Doctor,
|
|
121
|
+
DispatchKind::Watch,
|
|
122
|
+
DispatchKind::Sessions,
|
|
123
|
+
DispatchKind::Leaders,
|
|
124
|
+
DispatchKind::Validate,
|
|
125
|
+
DispatchKind::InstallSkill,
|
|
126
|
+
DispatchKind::Profile,
|
|
127
|
+
DispatchKind::ValidateResult,
|
|
128
|
+
DispatchKind::Collect,
|
|
129
|
+
DispatchKind::Settle,
|
|
130
|
+
DispatchKind::RepairState,
|
|
131
|
+
DispatchKind::Diagnose,
|
|
132
|
+
DispatchKind::Preflight,
|
|
133
|
+
DispatchKind::WaitReady,
|
|
134
|
+
DispatchKind::E2e,
|
|
135
|
+
DispatchKind::Peek,
|
|
136
|
+
DispatchKind::Coordinator,
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
140
|
+
pub(crate) struct GovernanceNote {
|
|
141
|
+
pub(crate) decision: &'static str,
|
|
142
|
+
pub(crate) reason: &'static str,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
146
|
+
pub(crate) struct CommandSpec {
|
|
147
|
+
pub(crate) name: &'static str,
|
|
148
|
+
pub(crate) tier: CommandTier,
|
|
149
|
+
pub(crate) category: CommandCategory,
|
|
150
|
+
pub(crate) kind: CommandKind,
|
|
151
|
+
pub(crate) summary: &'static str,
|
|
152
|
+
pub(crate) usage: &'static str,
|
|
153
|
+
pub(crate) default_help: bool,
|
|
154
|
+
pub(crate) command_help: bool,
|
|
155
|
+
pub(crate) suggestion_index: bool,
|
|
156
|
+
pub(crate) token_usage: TokenUsage,
|
|
157
|
+
pub(crate) alias_of: Option<&'static str>,
|
|
158
|
+
pub(crate) sunset: Option<&'static str>,
|
|
159
|
+
pub(crate) action: Option<&'static str>,
|
|
160
|
+
pub(crate) governance: Option<GovernanceNote>,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#[rustfmt::skip]
|
|
164
|
+
pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
165
|
+
CommandSpec { name: "quick-start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::QuickStart), summary: "start or attach a team from TEAM.md", usage: "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
166
|
+
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "send a message/task", usage: "usage: team-agent send TARGET MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--targets AGENTS] [--to-name NAME] [--pane PANE] [--task TASK] [--sender SENDER] [--watch-result] [--requires-ack|--no-ack] [--no-wait] [--timeout SECONDS] [--confirm-human] [--message-id ID] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
167
|
+
CommandSpec { name: "status", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Status), summary: "show current team status", usage: "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
168
|
+
CommandSpec { name: "collect", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Collect), summary: "collect reported results", usage: "usage: team-agent collect [--workspace WORKSPACE] [--team TEAM] [--result-file FILE] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
169
|
+
CommandSpec { name: "restart", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Restart), summary: "restart the selected team", usage: "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
170
|
+
CommandSpec { name: "shutdown", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Shutdown), summary: "stop the selected team", usage: "usage: team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
171
|
+
CommandSpec { name: "add-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::AddAgent), summary: "add a worker", usage: "usage: team-agent add-agent AGENT --role-file FILE [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
172
|
+
CommandSpec { name: "start-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::StartAgent), summary: "start an existing worker", usage: "usage: team-agent start-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--force] [--allow-fresh] [--no-display] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
173
|
+
CommandSpec { name: "stop-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::StopAgent), summary: "stop a worker", usage: "usage: team-agent stop-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
174
|
+
CommandSpec { name: "reset-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::ResetAgent), summary: "reset a worker session", usage: "usage: team-agent reset-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--discard-session] [--no-display] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
175
|
+
CommandSpec { name: "diagnose", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Diagnose), summary: "inspect this workspace runtime and suggested repairs", usage: "usage: team-agent diagnose [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
176
|
+
CommandSpec { name: "claim-leader", tier: CommandTier::Guided, category: CommandCategory::GuidedRecovery, kind: CommandKind::Dispatch(DispatchKind::ClaimLeader), summary: "bind this pane when diagnose reports rebind_required", usage: "usage: team-agent claim-leader [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
177
|
+
CommandSpec { name: "takeover", tier: CommandTier::Guided, category: CommandCategory::GuidedRecovery, kind: CommandKind::Dispatch(DispatchKind::Takeover), summary: "replace a live leader binding with explicit consent", usage: "usage: team-agent takeover [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
178
|
+
CommandSpec { name: "attach-leader", tier: CommandTier::Guided, category: CommandCategory::GuidedRecovery, kind: CommandKind::Dispatch(DispatchKind::AttachLeader), summary: "bind an external leader pane", usage: "usage: team-agent attach-leader [--workspace WORKSPACE] [--team TEAM] [--pane PANE] [--provider PROVIDER] [--confirm] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
179
|
+
|
|
180
|
+
CommandSpec { name: "codex", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "codex" }, summary: "launch a Codex leader", usage: "usage: team-agent codex [provider args...]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, alias_of: None, sunset: None, action: None, governance: None },
|
|
181
|
+
CommandSpec { name: "claude", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "claude" }, summary: "launch a Claude leader", usage: "usage: team-agent claude [provider args...]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, alias_of: None, sunset: None, action: None, governance: None },
|
|
182
|
+
CommandSpec { name: "copilot", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "copilot" }, summary: "launch a Copilot leader", usage: "usage: team-agent copilot [provider args...]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, alias_of: None, sunset: None, action: None, governance: None },
|
|
183
|
+
|
|
184
|
+
CommandSpec { name: "init", tier: CommandTier::CompatHidden, category: CommandCategory::Compat, kind: CommandKind::Dispatch(DispatchKind::Init), summary: "legacy bootstrap command", usage: "usage: team-agent init [--workspace WORKSPACE] [--force] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: Some("quick-start"), sunset: Some("C2"), action: Some("use `team-agent quick-start`"), governance: None },
|
|
185
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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 },
|
|
200
|
+
CommandSpec { name: "remove-agent", tier: CommandTier::Secondary, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::RemoveAgent), summary: "remove a worker", usage: "usage: team-agent remove-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--from-spec] [--confirm] [--force] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
201
|
+
CommandSpec { name: "fork-agent", tier: CommandTier::Secondary, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::ForkAgent), summary: "fork a worker", usage: "usage: team-agent fork-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
202
|
+
CommandSpec { name: "allow-peer-talk", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::AllowPeerTalk), summary: "allow direct worker peer talk", usage: "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "capability/config surface" }) },
|
|
203
|
+
CommandSpec { name: "approvals", tier: CommandTier::Secondary, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Approvals), summary: "inspect provider approval state", usage: "usage: team-agent approvals [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: Some(GovernanceNote { decision: "secondary", reason: "approval diagnostics" }) },
|
|
204
|
+
CommandSpec { name: "profile", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::Profile), summary: "manage provider profiles", usage: "usage: team-agent profile COMMAND NAME [--workspace WORKSPACE] [--team TEAM] [--auth-mode MODE] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "provider/profile setup" }) },
|
|
205
|
+
CommandSpec { name: "install-skill", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::InstallSkill), summary: "install or uninstall Team Agent skills", usage: "usage: team-agent install-skill (--source DIR | --uninstall) [--target codex|claude|copilot|all] [--dest DIR] [--dry-run] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "capability setup" }) },
|
|
206
|
+
CommandSpec { name: "inbox", tier: CommandTier::Secondary, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Inbox), summary: "inspect queued messages", usage: "usage: team-agent inbox AGENT [--workspace WORKSPACE] [--team TEAM] [--limit N] [--since CURSOR] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
207
|
+
|
|
208
|
+
CommandSpec { name: "identity", tier: CommandTier::DevInternal, category: CommandCategory::Dev, kind: CommandKind::Dispatch(DispatchKind::Identity), summary: "inspect identity metadata", usage: "usage: team-agent identity [--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 },
|
|
209
|
+
CommandSpec { name: "watch", tier: CommandTier::DevInternal, category: CommandCategory::Dev, kind: CommandKind::Dispatch(DispatchKind::Watch), summary: "watch event logs", usage: "usage: team-agent watch [--workspace WORKSPACE] [--team TEAM]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
210
|
+
CommandSpec { name: "sessions", tier: CommandTier::DevInternal, category: CommandCategory::Dev, kind: CommandKind::Dispatch(DispatchKind::Sessions), summary: "inspect session inventory", usage: "usage: team-agent sessions [--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 },
|
|
211
|
+
CommandSpec { name: "compile", tier: CommandTier::DevInternal, category: CommandCategory::Dev, kind: CommandKind::Dispatch(DispatchKind::Compile), summary: "compile a team spec", usage: "usage: team-agent compile --team TEAM [--out FILE] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
212
|
+
CommandSpec { name: "validate", tier: CommandTier::DevInternal, category: CommandCategory::Dev, kind: CommandKind::Dispatch(DispatchKind::Validate), summary: "validate a team spec", usage: "usage: team-agent validate [SPEC] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
213
|
+
CommandSpec { name: "preflight", tier: CommandTier::DevInternal, category: CommandCategory::Dev, kind: CommandKind::Dispatch(DispatchKind::Preflight), summary: "run preflight checks", usage: "usage: team-agent preflight [TEAMDIR] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
214
|
+
CommandSpec { name: "wait-ready", tier: CommandTier::DevInternal, category: CommandCategory::Dev, kind: CommandKind::Dispatch(DispatchKind::WaitReady), summary: "wait for runtime readiness", usage: "usage: team-agent wait-ready [--workspace WORKSPACE] [--team TEAM] [--timeout SECONDS] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
215
|
+
CommandSpec { name: "e2e", tier: CommandTier::DevInternal, category: CommandCategory::Dev, kind: CommandKind::Dispatch(DispatchKind::E2e), summary: "run e2e harness", usage: "usage: team-agent e2e [--workspace WORKSPACE] [--providers LIST] [--real] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::Yes, alias_of: None, sunset: None, action: None, governance: None },
|
|
216
|
+
CommandSpec { name: "peek", tier: CommandTier::DevInternal, category: CommandCategory::Dev, kind: CommandKind::Dispatch(DispatchKind::Peek), summary: "capture low-level terminal output", usage: "usage: team-agent peek AGENT [--workspace WORKSPACE] [--tail N|--head N] [--search TEXT] [--allow-raw-screen] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
217
|
+
CommandSpec { name: "coordinator", tier: CommandTier::DevInternal, category: CommandCategory::Dev, kind: CommandKind::Dispatch(DispatchKind::Coordinator), summary: "run the coordinator daemon", usage: "usage: team-agent coordinator [--workspace WORKSPACE] [--once] [--tick-interval SECONDS]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
pub(crate) fn command_spec(name: &str) -> Option<&'static CommandSpec> {
|
|
221
|
+
COMMAND_SPECS.iter().find(|spec| spec.name == name)
|
|
222
|
+
}
|
|
@@ -32,6 +32,17 @@ pub fn agent_summary_counts(agents: &Value, health: &Value) -> SummaryCounts {
|
|
|
32
32
|
return counts;
|
|
33
33
|
};
|
|
34
34
|
for (agent_id, agent) in agent_map {
|
|
35
|
+
if let Some(bucket) = stale_agent_bucket(agent) {
|
|
36
|
+
match bucket {
|
|
37
|
+
SummaryBucket::Stopped => counts.stopped += 1,
|
|
38
|
+
SummaryBucket::Unknown => counts.unknown += 1,
|
|
39
|
+
SummaryBucket::Running
|
|
40
|
+
| SummaryBucket::Busy
|
|
41
|
+
| SummaryBucket::Idle
|
|
42
|
+
| SummaryBucket::Failed => {}
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
35
46
|
let raw = agent
|
|
36
47
|
.get("status")
|
|
37
48
|
.and_then(Value::as_str)
|
|
@@ -53,6 +64,21 @@ pub fn agent_summary_counts(agents: &Value, health: &Value) -> SummaryCounts {
|
|
|
53
64
|
counts
|
|
54
65
|
}
|
|
55
66
|
|
|
67
|
+
fn stale_agent_bucket(agent: &Value) -> Option<SummaryBucket> {
|
|
68
|
+
if agent.get("stale").and_then(Value::as_bool) != Some(true) {
|
|
69
|
+
return None;
|
|
70
|
+
}
|
|
71
|
+
if agent
|
|
72
|
+
.get("stale_reason")
|
|
73
|
+
.and_then(Value::as_str)
|
|
74
|
+
.is_some_and(|reason| !reason.is_empty())
|
|
75
|
+
{
|
|
76
|
+
Some(SummaryBucket::Stopped)
|
|
77
|
+
} else {
|
|
78
|
+
Some(SummaryBucket::Unknown)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
56
82
|
/// `_interaction_counts`(`commands.py:292-306`):遍历 agents 的 `interacted` 字段。
|
|
57
83
|
pub fn interaction_counts(agents: &Value) -> InteractionCounts {
|
|
58
84
|
let mut counts = InteractionCounts::default();
|
|
@@ -165,6 +191,9 @@ fn csv_agent_status(
|
|
|
165
191
|
agent: &Value,
|
|
166
192
|
health: Option<&serde_json::Map<String, Value>>,
|
|
167
193
|
) -> &'static str {
|
|
194
|
+
if stale_agent_bucket(agent).is_some() {
|
|
195
|
+
return "错误";
|
|
196
|
+
}
|
|
168
197
|
let raw = agent
|
|
169
198
|
.get("status")
|
|
170
199
|
.and_then(Value::as_str)
|
|
@@ -34,12 +34,13 @@ use rusqlite::params;
|
|
|
34
34
|
// backlog>0 才挂 down-hint(anti-nag)。auto-recovery 不做(user 已裁)。
|
|
35
35
|
let coordinator_running = coordinator_status_running(&health);
|
|
36
36
|
let undelivered_backlog = count_undelivered_backlog(&conn, owner_team_id)?;
|
|
37
|
+
let session_name = state.get("session_name").cloned().unwrap_or(Value::Null);
|
|
38
|
+
let tmux_present = tmux_session_present(workspace, state, session_name.as_str());
|
|
37
39
|
let runtime_block = build_runtime_status_block(
|
|
38
40
|
coordinator_running,
|
|
39
41
|
undelivered_backlog,
|
|
42
|
+
!tmux_present,
|
|
40
43
|
);
|
|
41
|
-
let session_name = state.get("session_name").cloned().unwrap_or(Value::Null);
|
|
42
|
-
let tmux_present = tmux_session_present(workspace, state, session_name.as_str());
|
|
43
44
|
let agents = enrich_agents(state.get("agents"), tmux_present);
|
|
44
45
|
let tasks = state
|
|
45
46
|
.get("tasks")
|
|
@@ -458,6 +459,9 @@ use rusqlite::params;
|
|
|
458
459
|
"stale_reason".to_string(),
|
|
459
460
|
Value::String(reason.to_string()),
|
|
460
461
|
);
|
|
462
|
+
if !tmux_session_present {
|
|
463
|
+
downgrade_stale_agent(&mut enriched);
|
|
464
|
+
}
|
|
461
465
|
}
|
|
462
466
|
out.insert(agent_id.clone(), Value::Object(enriched));
|
|
463
467
|
}
|
|
@@ -469,6 +473,25 @@ use rusqlite::params;
|
|
|
469
473
|
Value::Object(out)
|
|
470
474
|
}
|
|
471
475
|
|
|
476
|
+
fn downgrade_stale_agent(agent: &mut Map<String, Value>) {
|
|
477
|
+
let raw = agent
|
|
478
|
+
.get("status")
|
|
479
|
+
.and_then(Value::as_str)
|
|
480
|
+
.unwrap_or("")
|
|
481
|
+
.to_ascii_lowercase();
|
|
482
|
+
if matches!(raw.as_str(), "running" | "busy" | "working" | "idle") {
|
|
483
|
+
agent.insert("status".to_string(), Value::String("stopped".to_string()));
|
|
484
|
+
}
|
|
485
|
+
let worker_state = agent
|
|
486
|
+
.get("worker_state")
|
|
487
|
+
.and_then(Value::as_str)
|
|
488
|
+
.unwrap_or("")
|
|
489
|
+
.to_ascii_uppercase();
|
|
490
|
+
if matches!(worker_state.as_str(), "RUNNING" | "BUSY" | "PROBABLY_IDLE") {
|
|
491
|
+
agent.insert("worker_state".to_string(), Value::String("DEAD".to_string()));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
472
495
|
fn stale_reason_for_agent(agent: &Value, tmux_session_present: bool) -> Option<&'static str> {
|
|
473
496
|
let pane_dead = !tmux_session_present && agent_has_pane_fact(agent);
|
|
474
497
|
let process_dead =
|
|
@@ -1093,14 +1116,23 @@ use rusqlite::params;
|
|
|
1093
1116
|
/// B-5 / 036b N38 — status 出口的 runtime 块:把 coordinator_health 与
|
|
1094
1117
|
/// undelivered backlog 合体暴露。down-hint 只在【coordinator 不在跑 ∧ 有 backlog】
|
|
1095
1118
|
/// 两条件同时满足才挂(anti-nag);健康状态下不挂提示。auto-recovery 不做。
|
|
1096
|
-
fn build_runtime_status_block(
|
|
1119
|
+
fn build_runtime_status_block(
|
|
1120
|
+
coordinator_running: bool,
|
|
1121
|
+
undelivered: i64,
|
|
1122
|
+
tmux_session_missing: bool,
|
|
1123
|
+
) -> Value {
|
|
1097
1124
|
let mut runtime = serde_json::Map::new();
|
|
1098
1125
|
runtime.insert(
|
|
1099
1126
|
"coordinator".to_string(),
|
|
1100
1127
|
json!({"ok": coordinator_running}),
|
|
1101
1128
|
);
|
|
1102
1129
|
runtime.insert("undelivered".to_string(), json!(undelivered));
|
|
1103
|
-
if
|
|
1130
|
+
if tmux_session_missing {
|
|
1131
|
+
runtime.insert(
|
|
1132
|
+
"hint".to_string(),
|
|
1133
|
+
json!("tmux session missing — run team-agent restart"),
|
|
1134
|
+
);
|
|
1135
|
+
} else if !coordinator_running && undelivered > 0 {
|
|
1104
1136
|
runtime.insert(
|
|
1105
1137
|
"hint".to_string(),
|
|
1106
1138
|
json!(format!(
|
|
@@ -440,21 +440,17 @@ pub fn claim_leader(
|
|
|
440
440
|
.unwrap_or_default();
|
|
441
441
|
let raw_state = crate::state::persist::load_runtime_state(workspace)?;
|
|
442
442
|
let event_log = crate::event_log::EventLog::new(workspace);
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
let mut targets = crate::transport_factory::tmux_workspace_transport(workspace)
|
|
446
|
-
.list_targets()
|
|
447
|
-
.unwrap_or_default();
|
|
448
|
-
targets.extend(
|
|
449
|
-
crate::transport_factory::tmux_default_transport()
|
|
450
|
-
.list_targets()
|
|
451
|
-
.unwrap_or_default(),
|
|
452
|
-
);
|
|
453
|
-
let caller_pane_info = targets
|
|
443
|
+
let targets = claim_leader_targets(workspace, &raw_state);
|
|
444
|
+
let caller_candidate = targets
|
|
454
445
|
.iter()
|
|
455
|
-
.find(|target| target.pane_id.as_str() == caller);
|
|
456
|
-
let
|
|
457
|
-
|
|
446
|
+
.find(|target| target.info.pane_id.as_str() == caller);
|
|
447
|
+
let caller_pane_info = caller_candidate.map(|target| &target.info);
|
|
448
|
+
let caller_target = caller_candidate.and_then(|target| {
|
|
449
|
+
claim_target_from_pane_info(workspace, &target.info).map(|mut claim_target| {
|
|
450
|
+
claim_target.endpoint = target.endpoint.clone();
|
|
451
|
+
claim_target
|
|
452
|
+
})
|
|
453
|
+
});
|
|
458
454
|
let env_team = std::env::var("TEAM_AGENT_TEAM_ID")
|
|
459
455
|
.ok()
|
|
460
456
|
.filter(|team| !team.is_empty());
|
|
@@ -489,7 +485,7 @@ pub fn claim_leader(
|
|
|
489
485
|
} else {
|
|
490
486
|
raw_state
|
|
491
487
|
};
|
|
492
|
-
let liveness = AnyPaneLiveness::
|
|
488
|
+
let liveness = AnyPaneLiveness::from_claim_targets(&targets);
|
|
493
489
|
let result = claim_lease_no_incident_with_target(
|
|
494
490
|
workspace,
|
|
495
491
|
&mut state,
|
|
@@ -634,8 +630,23 @@ fn claim_lease_no_incident_with_target(
|
|
|
634
630
|
let bound_endpoint_matches_caller = bound_endpoint_matches_current_process(state);
|
|
635
631
|
if bound_pane_id.as_deref() == Some(caller_pane.as_str()) && bound_endpoint_matches_caller {
|
|
636
632
|
let current_endpoint = crate::tmux_backend::socket_name_from_tmux_env();
|
|
633
|
+
let observed_endpoint = caller_target.and_then(|target| target.endpoint.as_deref());
|
|
634
|
+
let convergence_candidate = observed_endpoint.or(current_endpoint.as_deref());
|
|
635
|
+
let candidate_source = if observed_endpoint.is_some() {
|
|
636
|
+
Some("observed_target_endpoint")
|
|
637
|
+
} else if current_endpoint.is_some() {
|
|
638
|
+
Some("fallback_tmux_env")
|
|
639
|
+
} else {
|
|
640
|
+
None
|
|
641
|
+
};
|
|
637
642
|
let (mut topology_convergence, converged) =
|
|
638
|
-
apply_endpoint_convergence(
|
|
643
|
+
apply_endpoint_convergence(
|
|
644
|
+
state,
|
|
645
|
+
team_id,
|
|
646
|
+
convergence_candidate,
|
|
647
|
+
candidate_source,
|
|
648
|
+
pre_epoch,
|
|
649
|
+
);
|
|
639
650
|
if converged {
|
|
640
651
|
write_claim_state(workspace, state, scoped_team, team)?;
|
|
641
652
|
topology_convergence = verify_persisted_topology_convergence(
|
|
@@ -796,7 +807,8 @@ fn claim_lease_no_incident_with_target(
|
|
|
796
807
|
}
|
|
797
808
|
let next_epoch = OwnerEpoch(pre_epoch.0.saturating_add(1));
|
|
798
809
|
let provider = caller_target.map_or_else(|| prior_provider(state), |target| target.provider);
|
|
799
|
-
let
|
|
810
|
+
let observed_endpoint = caller_target.and_then(|target| target.endpoint.clone());
|
|
811
|
+
let mut receiver = make_receiver(
|
|
800
812
|
provider,
|
|
801
813
|
&non_empty_caller_pane,
|
|
802
814
|
&identity.leader_session_uuid,
|
|
@@ -804,10 +816,26 @@ fn claim_lease_no_incident_with_target(
|
|
|
804
816
|
Discovery::ClaimLeader,
|
|
805
817
|
caller_target.and_then(|target| target.pane_info.clone()),
|
|
806
818
|
);
|
|
819
|
+
if let Some(endpoint) = observed_endpoint.as_ref() {
|
|
820
|
+
receiver.tmux_socket = Some(endpoint.clone());
|
|
821
|
+
}
|
|
807
822
|
let owner = make_owner(provider, &non_empty_caller_pane, &identity, next_epoch);
|
|
808
823
|
write_binding_to_state(state, &receiver, &owner)?;
|
|
824
|
+
let candidate_source = if observed_endpoint.is_some() {
|
|
825
|
+
Some("observed_target_endpoint")
|
|
826
|
+
} else if receiver.tmux_socket.is_some() {
|
|
827
|
+
Some("fallback_tmux_env")
|
|
828
|
+
} else {
|
|
829
|
+
None
|
|
830
|
+
};
|
|
809
831
|
let (mut topology_convergence, converged) =
|
|
810
|
-
apply_endpoint_convergence(
|
|
832
|
+
apply_endpoint_convergence(
|
|
833
|
+
state,
|
|
834
|
+
team_id,
|
|
835
|
+
receiver.tmux_socket.as_deref(),
|
|
836
|
+
candidate_source,
|
|
837
|
+
next_epoch,
|
|
838
|
+
);
|
|
811
839
|
write_claim_state(workspace, state, scoped_team, team)?;
|
|
812
840
|
if converged {
|
|
813
841
|
topology_convergence = verify_persisted_topology_convergence(
|
|
@@ -884,13 +912,17 @@ fn apply_endpoint_convergence(
|
|
|
884
912
|
state: &mut Value,
|
|
885
913
|
team_id: &TeamKey,
|
|
886
914
|
candidate_endpoint: Option<&str>,
|
|
915
|
+
candidate_source: Option<&'static str>,
|
|
887
916
|
owner_epoch: OwnerEpoch,
|
|
888
917
|
) -> (Option<Value>, bool) {
|
|
889
|
-
let
|
|
918
|
+
let explicit_candidate = candidate_endpoint
|
|
890
919
|
.filter(|endpoint| !endpoint.is_empty())
|
|
891
|
-
.map(str::to_string)
|
|
892
|
-
|
|
893
|
-
|
|
920
|
+
.map(str::to_string);
|
|
921
|
+
let (candidate_endpoint, candidate_source) = if let Some(endpoint) = explicit_candidate {
|
|
922
|
+
(endpoint, candidate_source.unwrap_or("candidate"))
|
|
923
|
+
} else if let Some(endpoint) = state_tmux_socket_candidate(state, team_id.as_str()) {
|
|
924
|
+
(endpoint, "fallback_state")
|
|
925
|
+
} else {
|
|
894
926
|
return (None, false);
|
|
895
927
|
};
|
|
896
928
|
match crate::topology::endpoint_convergence_decision(
|
|
@@ -903,6 +935,7 @@ fn apply_endpoint_convergence(
|
|
|
903
935
|
Some(json!({
|
|
904
936
|
"status": "unknown",
|
|
905
937
|
"new_tmux_endpoint": candidate_endpoint,
|
|
938
|
+
"candidate_source": candidate_source,
|
|
906
939
|
"owner_epoch": owner_epoch.0,
|
|
907
940
|
})),
|
|
908
941
|
false,
|
|
@@ -910,14 +943,17 @@ fn apply_endpoint_convergence(
|
|
|
910
943
|
crate::topology::EndpointConvergenceDecision::RefuseLiveOldEndpoint {
|
|
911
944
|
old_endpoint,
|
|
912
945
|
new_endpoint,
|
|
946
|
+
reason,
|
|
913
947
|
} => (
|
|
914
948
|
Some(json!({
|
|
915
949
|
"status": "not_converged_old_endpoint_live",
|
|
916
950
|
"old_tmux_endpoint": old_endpoint,
|
|
917
951
|
"new_tmux_endpoint": new_endpoint,
|
|
952
|
+
"reason": reason,
|
|
953
|
+
"candidate_source": candidate_source,
|
|
918
954
|
"owner_epoch": owner_epoch.0,
|
|
919
955
|
"action": format!(
|
|
920
|
-
"old tmux endpoint {old_endpoint}
|
|
956
|
+
"old tmux endpoint {old_endpoint} still has this team's session or pane tuple; run team-agent diagnose --json before retrying restart"
|
|
921
957
|
),
|
|
922
958
|
})),
|
|
923
959
|
false,
|
|
@@ -925,12 +961,14 @@ fn apply_endpoint_convergence(
|
|
|
925
961
|
crate::topology::EndpointConvergenceDecision::Converge {
|
|
926
962
|
old_endpoint,
|
|
927
963
|
new_endpoint,
|
|
964
|
+
reason,
|
|
928
965
|
} => {
|
|
929
966
|
let metadata = json!({
|
|
930
967
|
"status": "converged",
|
|
931
968
|
"old_tmux_endpoint": old_endpoint,
|
|
932
969
|
"new_tmux_endpoint": new_endpoint,
|
|
933
|
-
"reason":
|
|
970
|
+
"reason": reason,
|
|
971
|
+
"candidate_source": candidate_source,
|
|
934
972
|
"owner_epoch": owner_epoch.0,
|
|
935
973
|
});
|
|
936
974
|
write_endpoint_fields(state, team_id.as_str(), &new_endpoint);
|
|
@@ -1175,6 +1213,56 @@ struct LeaderClaimTarget {
|
|
|
1175
1213
|
leader_session_uuid: Option<crate::model::ids::LeaderSessionUuid>,
|
|
1176
1214
|
team_id: Option<String>,
|
|
1177
1215
|
pane_info: Option<PaneInfo>,
|
|
1216
|
+
endpoint: Option<String>,
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
#[derive(Clone)]
|
|
1220
|
+
struct ClaimLeaderTargetCandidate {
|
|
1221
|
+
info: PaneInfo,
|
|
1222
|
+
endpoint: Option<String>,
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
fn claim_leader_targets(workspace: &Path, state: &Value) -> Vec<ClaimLeaderTargetCandidate> {
|
|
1226
|
+
let mut targets = Vec::new();
|
|
1227
|
+
for endpoint in state_recorded_tmux_endpoints(state) {
|
|
1228
|
+
let backend = tmux_backend_for_endpoint(&endpoint);
|
|
1229
|
+
let resolved_endpoint = backend.tmux_endpoint();
|
|
1230
|
+
targets.extend(
|
|
1231
|
+
backend
|
|
1232
|
+
.list_targets()
|
|
1233
|
+
.unwrap_or_default()
|
|
1234
|
+
.into_iter()
|
|
1235
|
+
.map(|info| ClaimLeaderTargetCandidate {
|
|
1236
|
+
info,
|
|
1237
|
+
endpoint: resolved_endpoint.clone(),
|
|
1238
|
+
}),
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
let workspace_backend = crate::transport_factory::tmux_workspace_transport(workspace);
|
|
1242
|
+
let workspace_endpoint = workspace_backend.tmux_endpoint();
|
|
1243
|
+
targets.extend(
|
|
1244
|
+
workspace_backend
|
|
1245
|
+
.list_targets()
|
|
1246
|
+
.unwrap_or_default()
|
|
1247
|
+
.into_iter()
|
|
1248
|
+
.map(|info| ClaimLeaderTargetCandidate {
|
|
1249
|
+
info,
|
|
1250
|
+
endpoint: workspace_endpoint.clone(),
|
|
1251
|
+
}),
|
|
1252
|
+
);
|
|
1253
|
+
let default_backend = crate::transport_factory::tmux_default_transport();
|
|
1254
|
+
let default_endpoint = default_backend.tmux_endpoint();
|
|
1255
|
+
targets.extend(
|
|
1256
|
+
default_backend
|
|
1257
|
+
.list_targets()
|
|
1258
|
+
.unwrap_or_default()
|
|
1259
|
+
.into_iter()
|
|
1260
|
+
.map(|info| ClaimLeaderTargetCandidate {
|
|
1261
|
+
info,
|
|
1262
|
+
endpoint: default_endpoint.clone(),
|
|
1263
|
+
}),
|
|
1264
|
+
);
|
|
1265
|
+
targets
|
|
1178
1266
|
}
|
|
1179
1267
|
|
|
1180
1268
|
fn claim_target_from_pane_info(workspace: &Path, target: &PaneInfo) -> Option<LeaderClaimTarget> {
|
|
@@ -1191,6 +1279,7 @@ fn claim_target_from_pane_info(workspace: &Path, target: &PaneInfo) -> Option<Le
|
|
|
1191
1279
|
leader_session_uuid: target_leader_session_uuid(target),
|
|
1192
1280
|
team_id: target.leader_env.get("TEAM_AGENT_TEAM_ID").filter(|raw| !raw.is_empty()).cloned(),
|
|
1193
1281
|
pane_info: Some(target.clone()),
|
|
1282
|
+
endpoint: None,
|
|
1194
1283
|
})
|
|
1195
1284
|
}
|
|
1196
1285
|
|
|
@@ -1290,11 +1379,11 @@ struct AnyPaneLiveness {
|
|
|
1290
1379
|
}
|
|
1291
1380
|
|
|
1292
1381
|
impl AnyPaneLiveness {
|
|
1293
|
-
fn
|
|
1382
|
+
fn from_claim_targets(targets: &[ClaimLeaderTargetCandidate]) -> Self {
|
|
1294
1383
|
Self {
|
|
1295
1384
|
live_panes: targets
|
|
1296
1385
|
.iter()
|
|
1297
|
-
.map(|target| target.pane_id.as_str().to_string())
|
|
1386
|
+
.map(|target| target.info.pane_id.as_str().to_string())
|
|
1298
1387
|
.collect(),
|
|
1299
1388
|
}
|
|
1300
1389
|
}
|
|
@@ -1677,10 +1766,16 @@ fn save_claim_team_scoped_state(workspace: &Path, state: &Value, target_key: &st
|
|
|
1677
1766
|
.map(|_| crate::state::projection::team_state_key(&existing));
|
|
1678
1767
|
let existing_active_key = existing.get("active_team_key").and_then(Value::as_str);
|
|
1679
1768
|
let updates_active_team = existing_active_key == Some(target_key);
|
|
1769
|
+
let writes_endpoint_convergence = state
|
|
1770
|
+
.get("topology_convergence")
|
|
1771
|
+
.and_then(|marker| marker.get("status"))
|
|
1772
|
+
.and_then(Value::as_str)
|
|
1773
|
+
== Some("converged");
|
|
1680
1774
|
let mut merged = if existing_primary_key
|
|
1681
1775
|
.as_deref()
|
|
1682
1776
|
.is_none_or(|key| key == target_key)
|
|
1683
1777
|
|| updates_active_team
|
|
1778
|
+
|| writes_endpoint_convergence
|
|
1684
1779
|
{
|
|
1685
1780
|
value_object(state)
|
|
1686
1781
|
} else {
|
|
@@ -1697,7 +1792,12 @@ fn save_claim_team_scoped_state(workspace: &Path, state: &Value, target_key: &st
|
|
|
1697
1792
|
// / coordinator tick / promote sibling. `had_existing_teams` /
|
|
1698
1793
|
// primary/active key computations are retained as dead writes only
|
|
1699
1794
|
// via _ binding — they no longer gate any owner promote.
|
|
1700
|
-
let _ = (
|
|
1795
|
+
let _ = (
|
|
1796
|
+
had_existing_teams,
|
|
1797
|
+
existing_primary_key,
|
|
1798
|
+
existing_active_key,
|
|
1799
|
+
writes_endpoint_convergence,
|
|
1800
|
+
);
|
|
1701
1801
|
merged.insert("teams".to_string(), Value::Object(teams));
|
|
1702
1802
|
crate::state::persist::save_runtime_state(workspace, &Value::Object(merged))?;
|
|
1703
1803
|
Ok(())
|