@team-agent/installer 0.5.27 → 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.
@@ -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(coordinator_running: bool, undelivered: i64) -> Value {
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 !coordinator_running && undelivered > 0 {
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
- // Phase 1d Batch 6: factory tmux channel helpers for grep-visibility.
444
- // Tmux-only leader-claim path (caller pane enumeration).
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 caller_target =
457
- caller_pane_info.and_then(|target| claim_target_from_pane_info(workspace, target));
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::from_targets(&targets);
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(state, team_id, current_endpoint.as_deref(), pre_epoch);
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 receiver = make_receiver(
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(state, team_id, receiver.tmux_socket.as_deref(), next_epoch);
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 Some(candidate_endpoint) = candidate_endpoint
918
+ let explicit_candidate = candidate_endpoint
890
919
  .filter(|endpoint| !endpoint.is_empty())
891
- .map(str::to_string)
892
- .or_else(|| state_tmux_socket_candidate(state, team_id.as_str()))
893
- else {
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} is still live; run team-agent diagnose --json and clean that endpoint before retrying restart"
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": "old_endpoint_dead",
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 from_targets(targets: &[PaneInfo]) -> Self {
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 _ = (had_existing_teams, existing_primary_key, existing_active_key);
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(())