@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
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -366,7 +366,7 @@ fn recovery_hint(team: &str, broken_class: &str, hint_action: &str) -> Value {
|
|
|
366
366
|
"hint_action": hint_action,
|
|
367
367
|
"dedupe_key": format!("{team}:{broken_class}"),
|
|
368
368
|
"action": format!(
|
|
369
|
-
"{hint_action} # alternatives: team-agent restart; team-agent claim-leader; team-agent quick-start; team-agent attach-leader"
|
|
369
|
+
"{hint_action} # alternatives: team-agent restart; team-agent claim-leader; team-agent takeover; team-agent quick-start; team-agent attach-leader"
|
|
370
370
|
),
|
|
371
371
|
})
|
|
372
372
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//! cli · emit — `emit`(--json vs 人读 dict 逐键)+ 顶层 `run` 调度(parser.py `main`)+
|
|
2
2
|
//! 人读标量/集合渲染(`human_value` / `json_dumps_like`)。
|
|
3
3
|
|
|
4
|
+
use super::spec::{command_spec, CommandKind, CommandTier, ALL_DISPATCH_KINDS, COMMAND_SPECS};
|
|
4
5
|
use super::*;
|
|
5
6
|
use std::io::Write as _;
|
|
6
7
|
|
|
@@ -77,6 +78,19 @@ fn emit_result(r: CmdResult) -> ExitCode {
|
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliError> {
|
|
81
|
+
let Some(spec) = command_spec(command) else {
|
|
82
|
+
return Ok(emit_unknown_subcommand_usage(command));
|
|
83
|
+
};
|
|
84
|
+
match spec.kind {
|
|
85
|
+
CommandKind::Dispatch(_) => {}
|
|
86
|
+
CommandKind::SpecOnlyAlias { .. } => {
|
|
87
|
+
eprintln!("{}", command_help(Some(command)));
|
|
88
|
+
return Ok(ExitCode::Usage);
|
|
89
|
+
}
|
|
90
|
+
CommandKind::LeaderPassthrough { .. } => {
|
|
91
|
+
return Ok(emit_unknown_subcommand_usage(command));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
80
94
|
match command {
|
|
81
95
|
"init" => cmd_init(&init_args(args, cwd)).map(emit_result),
|
|
82
96
|
"quick-start" => cmd_quick_start(&quick_start_args(args, cwd)?).map(emit_result),
|
|
@@ -216,6 +230,10 @@ const LEADER_PASSTHROUGH_COMMANDS: &[&str] = &["codex", "claude", "copilot"];
|
|
|
216
230
|
|
|
217
231
|
fn is_leader_passthrough_command(command: &str) -> bool {
|
|
218
232
|
LEADER_PASSTHROUGH_COMMANDS.contains(&command)
|
|
233
|
+
&& matches!(
|
|
234
|
+
command_spec(command).map(|spec| spec.kind),
|
|
235
|
+
Some(CommandKind::LeaderPassthrough { .. })
|
|
236
|
+
)
|
|
219
237
|
}
|
|
220
238
|
|
|
221
239
|
fn emit_missing_subcommand_usage() -> ExitCode {
|
|
@@ -228,7 +246,60 @@ fn emit_missing_subcommand_usage() -> ExitCode {
|
|
|
228
246
|
/// Used by the `--help` short-circuit gate so unknown commands keep falling through
|
|
229
247
|
/// to the argparse invalid-choice path.
|
|
230
248
|
fn is_known_subcommand(command: &str) -> bool {
|
|
231
|
-
|
|
249
|
+
command_spec(command).is_some_and(|spec| spec.command_help)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fn default_help() -> String {
|
|
253
|
+
let mut out = String::from("usage: team-agent <command> [options]\n");
|
|
254
|
+
append_help_section(
|
|
255
|
+
&mut out,
|
|
256
|
+
"Core",
|
|
257
|
+
&["quick-start", "send", "status", "collect"],
|
|
258
|
+
);
|
|
259
|
+
append_help_section(
|
|
260
|
+
&mut out,
|
|
261
|
+
"Lifecycle",
|
|
262
|
+
&[
|
|
263
|
+
"restart",
|
|
264
|
+
"shutdown",
|
|
265
|
+
"add-agent",
|
|
266
|
+
"start-agent",
|
|
267
|
+
"stop-agent",
|
|
268
|
+
"reset-agent",
|
|
269
|
+
],
|
|
270
|
+
);
|
|
271
|
+
append_help_section(&mut out, "Diagnose", &["diagnose"]);
|
|
272
|
+
append_help_section(
|
|
273
|
+
&mut out,
|
|
274
|
+
"Guided recovery",
|
|
275
|
+
&["claim-leader", "takeover", "attach-leader"],
|
|
276
|
+
);
|
|
277
|
+
out.push_str("\nProvider launchers:\n team-agent codex|claude|copilot ...\n");
|
|
278
|
+
out.push_str("\nRun `team-agent <command> --help` for command flags.");
|
|
279
|
+
out
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
fn append_help_section(out: &mut String, title: &str, names: &[&str]) {
|
|
283
|
+
out.push('\n');
|
|
284
|
+
out.push_str(title);
|
|
285
|
+
out.push_str(":\n");
|
|
286
|
+
for name in names {
|
|
287
|
+
if let Some(spec) = command_spec(name).filter(|spec| spec.default_help) {
|
|
288
|
+
out.push_str(&format!(" {:<13} {}\n", spec.name, spec.summary));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
fn compat_hidden_help(command: &str, usage: &str) -> String {
|
|
294
|
+
let Some(spec) = command_spec(command) else {
|
|
295
|
+
return usage.to_string();
|
|
296
|
+
};
|
|
297
|
+
if spec.tier != CommandTier::CompatHidden {
|
|
298
|
+
return usage.to_string();
|
|
299
|
+
}
|
|
300
|
+
let sunset = spec.sunset.unwrap_or("C2");
|
|
301
|
+
let action = spec.action.unwrap_or("use a supported command");
|
|
302
|
+
format!("{usage}\n\nstatus: hidden compatibility command\nsunset: {sunset}\naction: {action}")
|
|
232
303
|
}
|
|
233
304
|
|
|
234
305
|
/// Test-only public accessor for `command_help` — allows integration
|
|
@@ -250,28 +321,20 @@ pub fn __test_quick_start_args(
|
|
|
250
321
|
|
|
251
322
|
fn command_help(command: Option<&str>) -> String {
|
|
252
323
|
match command {
|
|
253
|
-
None =>
|
|
254
|
-
|
|
255
|
-
commands.extend_from_slice(DISPATCH_COMMANDS);
|
|
256
|
-
commands.extend_from_slice(SPEC_ONLY_HELP_COMMANDS);
|
|
257
|
-
format!(
|
|
258
|
-
"usage: team-agent <command> [options]\n\nCommands: {}\n\nRun `team-agent <command> --help` for command flags.",
|
|
259
|
-
commands.join(", ")
|
|
260
|
-
)
|
|
261
|
-
}
|
|
262
|
-
Some("init") => "usage: team-agent init [--workspace WORKSPACE] [--force] [--json]".to_string(),
|
|
324
|
+
None => default_help(),
|
|
325
|
+
Some("init") => compat_hidden_help("init", "usage: team-agent init [--workspace WORKSPACE] [--force] [--json]"),
|
|
263
326
|
Some("quick-start") => "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]\n\ndefaults: display_backend=adaptive; set display_backend: none in TEAM.md or pass --no-display to use one worker window per agent.\n\n--backend selects the worker transport (Phase 1d Batch 2): tmux (default on POSIX; unchanged behavior), conpty (Windows-native ConPTY worker transport; requires the shim binary and Windows host).".to_string(),
|
|
264
|
-
Some("start") => "usage: team-agent start [TEAMDIR] [--yes] [--fresh] [--json]"
|
|
327
|
+
Some("start") => compat_hidden_help("start", "usage: team-agent start [TEAMDIR] [--yes] [--fresh] [--json]"),
|
|
265
328
|
Some("compile") => "usage: team-agent compile --team TEAM [--out FILE] [--json]".to_string(),
|
|
266
329
|
Some("send") => "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]\n\nMVP: name-based cross-workspace addressing assumes trusted local caller; no auth gate.".to_string(),
|
|
267
|
-
Some("fallback-send-leader") => "usage: team-agent fallback-send-leader --workspace WORKSPACE --team TEAM --sender AGENT --message-id ID --content TEXT --primary-error ERROR [--task TASK] [--json]\n\nEmergency one-shot fallback only after a leader-bound MCP send transport failure; restart-agent after use."
|
|
268
|
-
Some("fallback-report-result") => "usage: team-agent fallback-report-result --workspace WORKSPACE --team TEAM --agent-id AGENT --task-id TASK --result-json JSON --primary-error ERROR [--json]\n\nEmergency one-shot fallback only after a report_result MCP transport failure; persists the result DB row, then restart-agent after use."
|
|
330
|
+
Some("fallback-send-leader") => compat_hidden_help("fallback-send-leader", "usage: team-agent fallback-send-leader --workspace WORKSPACE --team TEAM --sender AGENT --message-id ID --content TEXT --primary-error ERROR [--task TASK] [--json]\n\nEmergency one-shot fallback only after a leader-bound MCP send transport failure; restart-agent after use."),
|
|
331
|
+
Some("fallback-report-result") => compat_hidden_help("fallback-report-result", "usage: team-agent fallback-report-result --workspace WORKSPACE --team TEAM --agent-id AGENT --task-id TASK --result-json JSON --primary-error ERROR [--json]\n\nEmergency one-shot fallback only after a report_result MCP transport failure; persists the result DB row, then restart-agent after use."),
|
|
269
332
|
Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
|
|
270
333
|
Some("status") => "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]\n\n默认输出: worker,空闲|工作|错误;错误细分走 status --summary".to_string(),
|
|
271
|
-
Some("stop") => "usage: team-agent stop [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]"
|
|
334
|
+
Some("stop") => compat_hidden_help("stop", "usage: team-agent stop [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]"),
|
|
272
335
|
Some("shutdown") => "usage: team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]".to_string(),
|
|
273
336
|
Some("restart") => "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]".to_string(),
|
|
274
|
-
Some("restart-agent") => "usage: team-agent restart-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--discard-session] [--no-display] [--json]"
|
|
337
|
+
Some("restart-agent") => compat_hidden_help("restart-agent", "usage: team-agent restart-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--discard-session] [--no-display] [--json]"),
|
|
275
338
|
Some("reset-agent") => "usage: team-agent reset-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--discard-session] [--no-display] [--json]".to_string(),
|
|
276
339
|
Some("start-agent") => "usage: team-agent start-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--force] [--allow-fresh] [--no-display] [--json]".to_string(),
|
|
277
340
|
Some("stop-agent") => "usage: team-agent stop-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
|
|
@@ -322,9 +385,10 @@ fn emit_unknown_subcommand_usage(command: &str) -> ExitCode {
|
|
|
322
385
|
|
|
323
386
|
/// 在已知子命令里找与 `input` 最接近的一个(Levenshtein ≤ 阈值)。无足够接近者 → None。
|
|
324
387
|
fn nearest_subcommand(input: &str) -> Option<&'static str> {
|
|
325
|
-
let
|
|
326
|
-
|
|
327
|
-
|
|
388
|
+
let candidates = COMMAND_SPECS
|
|
389
|
+
.iter()
|
|
390
|
+
.filter(|spec| spec.suggestion_index)
|
|
391
|
+
.map(|spec| spec.name);
|
|
328
392
|
// 阈值随长度放宽,但短词收紧,避免 'x' 误配任何东西。
|
|
329
393
|
let max_distance = match input.chars().count() {
|
|
330
394
|
0..=3 => 1,
|
|
@@ -332,7 +396,6 @@ fn nearest_subcommand(input: &str) -> Option<&'static str> {
|
|
|
332
396
|
_ => 3,
|
|
333
397
|
};
|
|
334
398
|
candidates
|
|
335
|
-
.into_iter()
|
|
336
399
|
.map(|c| (c, levenshtein(input, c)))
|
|
337
400
|
.filter(|(_, d)| *d <= max_distance)
|
|
338
401
|
.min_by_key(|(_, d)| *d)
|
|
@@ -1630,69 +1693,158 @@ mod tests {
|
|
|
1630
1693
|
items.iter().map(|s| (*s).to_string()).collect()
|
|
1631
1694
|
}
|
|
1632
1695
|
|
|
1633
|
-
fn
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1696
|
+
fn visible_help_commands(help: &str) -> Vec<String> {
|
|
1697
|
+
help.lines()
|
|
1698
|
+
.filter_map(|line| {
|
|
1699
|
+
let trimmed = line.strip_prefix(" ")?;
|
|
1700
|
+
if trimmed.starts_with("team-agent ") {
|
|
1701
|
+
return None;
|
|
1702
|
+
}
|
|
1703
|
+
let command = trimmed.split_whitespace().next()?;
|
|
1704
|
+
command
|
|
1705
|
+
.chars()
|
|
1706
|
+
.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-')
|
|
1707
|
+
.then(|| command.to_string())
|
|
1708
|
+
})
|
|
1709
|
+
.collect()
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
#[test]
|
|
1713
|
+
fn command_specs_have_unique_names_and_valid_aliases() {
|
|
1714
|
+
let mut names = std::collections::BTreeSet::new();
|
|
1715
|
+
for spec in COMMAND_SPECS {
|
|
1716
|
+
assert!(
|
|
1717
|
+
names.insert(spec.name),
|
|
1718
|
+
"duplicate command spec `{}`",
|
|
1719
|
+
spec.name
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
for spec in COMMAND_SPECS {
|
|
1723
|
+
if let Some(alias_of) = spec.alias_of {
|
|
1724
|
+
assert!(
|
|
1725
|
+
names.contains(alias_of),
|
|
1726
|
+
"`{}` aliases missing command `{alias_of}`",
|
|
1727
|
+
spec.name
|
|
1728
|
+
);
|
|
1651
1729
|
}
|
|
1652
1730
|
}
|
|
1653
|
-
commands
|
|
1654
1731
|
}
|
|
1655
1732
|
|
|
1656
1733
|
#[test]
|
|
1657
|
-
fn
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1734
|
+
fn all_dispatch_kinds_have_exactly_one_spec() {
|
|
1735
|
+
for kind in ALL_DISPATCH_KINDS {
|
|
1736
|
+
let count = COMMAND_SPECS
|
|
1737
|
+
.iter()
|
|
1738
|
+
.filter(|spec| spec.kind == CommandKind::Dispatch(*kind))
|
|
1739
|
+
.count();
|
|
1740
|
+
assert_eq!(
|
|
1741
|
+
count, 1,
|
|
1742
|
+
"dispatch kind `{kind:?}` must appear in exactly one CommandSpec"
|
|
1663
1743
|
);
|
|
1664
1744
|
}
|
|
1665
|
-
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
#[test]
|
|
1748
|
+
fn known_command_gate_uses_specs() {
|
|
1749
|
+
for spec in COMMAND_SPECS.iter().filter(|spec| spec.command_help) {
|
|
1666
1750
|
assert!(
|
|
1667
|
-
|
|
1668
|
-
"
|
|
1751
|
+
is_known_subcommand(spec.name),
|
|
1752
|
+
"`{}` must be accepted by the known-command help gate",
|
|
1753
|
+
spec.name
|
|
1669
1754
|
);
|
|
1670
1755
|
}
|
|
1756
|
+
assert!(!is_known_subcommand("missing-c1-command"));
|
|
1757
|
+
}
|
|
1671
1758
|
|
|
1759
|
+
#[test]
|
|
1760
|
+
fn default_help_lists_only_default_help_specs() {
|
|
1672
1761
|
let top_help = command_help(None);
|
|
1673
|
-
|
|
1762
|
+
let visible = visible_help_commands(&top_help);
|
|
1763
|
+
for command in &visible {
|
|
1764
|
+
let spec = command_spec(command).expect("visible command must have a spec");
|
|
1674
1765
|
assert!(
|
|
1675
|
-
|
|
1676
|
-
"
|
|
1677
|
-
);
|
|
1678
|
-
let command_help = command_help(Some(command));
|
|
1679
|
-
assert!(
|
|
1680
|
-
command_help.contains("usage: team-agent") && command_help.contains(command),
|
|
1681
|
-
"`team-agent {command} --help` must show command-specific usage, got {command_help:?}"
|
|
1766
|
+
spec.default_help,
|
|
1767
|
+
"`{command}` appears in default help without default_help=true"
|
|
1682
1768
|
);
|
|
1683
1769
|
}
|
|
1684
|
-
for
|
|
1770
|
+
for spec in COMMAND_SPECS.iter().filter(|spec| spec.default_help) {
|
|
1685
1771
|
assert!(
|
|
1686
|
-
|
|
1687
|
-
"
|
|
1772
|
+
visible.iter().any(|command| command == spec.name),
|
|
1773
|
+
"`{}` has default_help=true but is missing from top-level help",
|
|
1774
|
+
spec.name
|
|
1688
1775
|
);
|
|
1689
1776
|
}
|
|
1777
|
+
assert!(
|
|
1778
|
+
visible.len() <= 15,
|
|
1779
|
+
"default visible command count must stay <= 15, got {visible:?}"
|
|
1780
|
+
);
|
|
1690
1781
|
assert!(
|
|
1691
1782
|
top_help.contains("copilot"),
|
|
1692
1783
|
"top-level leader passthrough help must list copilot"
|
|
1693
1784
|
);
|
|
1694
1785
|
}
|
|
1695
1786
|
|
|
1787
|
+
#[test]
|
|
1788
|
+
fn hidden_commands_not_in_default_help() {
|
|
1789
|
+
let top_help = command_help(None);
|
|
1790
|
+
let visible = visible_help_commands(&top_help);
|
|
1791
|
+
for command in [
|
|
1792
|
+
"fallback-send-leader",
|
|
1793
|
+
"fallback-report-result",
|
|
1794
|
+
"repair-state",
|
|
1795
|
+
"settle",
|
|
1796
|
+
"validate-result",
|
|
1797
|
+
"leaders",
|
|
1798
|
+
"doctor",
|
|
1799
|
+
"e2e",
|
|
1800
|
+
"peek",
|
|
1801
|
+
"coordinator",
|
|
1802
|
+
] {
|
|
1803
|
+
assert!(
|
|
1804
|
+
!visible.iter().any(|visible| visible == command),
|
|
1805
|
+
"`{command}` must stay hidden from default help"
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
#[test]
|
|
1811
|
+
fn compat_hidden_help_has_sunset_action() {
|
|
1812
|
+
for command in [
|
|
1813
|
+
"fallback-send-leader",
|
|
1814
|
+
"fallback-report-result",
|
|
1815
|
+
"stop",
|
|
1816
|
+
"restart-agent",
|
|
1817
|
+
"start",
|
|
1818
|
+
"init",
|
|
1819
|
+
] {
|
|
1820
|
+
let help = command_help(Some(command)).to_lowercase();
|
|
1821
|
+
assert!(help.contains("status: hidden compatibility command"));
|
|
1822
|
+
assert!(help.contains("sunset: c2"));
|
|
1823
|
+
assert!(help.contains("action:"));
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
#[test]
|
|
1828
|
+
fn observation_a_commands_have_terminal_tiers() {
|
|
1829
|
+
for (command, tier) in [
|
|
1830
|
+
("allow-peer-talk", CommandTier::Secondary),
|
|
1831
|
+
("approvals", CommandTier::Secondary),
|
|
1832
|
+
("profile", CommandTier::Secondary),
|
|
1833
|
+
("install-skill", CommandTier::Secondary),
|
|
1834
|
+
("init", CommandTier::CompatHidden),
|
|
1835
|
+
] {
|
|
1836
|
+
assert_eq!(command_spec(command).map(|spec| spec.tier), Some(tier));
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
#[test]
|
|
1841
|
+
fn suggestion_index_excludes_hidden_commands() {
|
|
1842
|
+
assert_eq!(nearest_subcommand("statu"), Some("status"));
|
|
1843
|
+
assert_eq!(nearest_subcommand("leader"), None);
|
|
1844
|
+
assert_eq!(nearest_subcommand("fallback-send-leade"), None);
|
|
1845
|
+
assert_eq!(nearest_subcommand("coordinato"), None);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1696
1848
|
#[test]
|
|
1697
1849
|
fn copilot_is_listed_as_leader_passthrough_candidate() {
|
|
1698
1850
|
assert!(command_help(None).contains("copilot"));
|
|
@@ -62,6 +62,7 @@ pub mod leaders;
|
|
|
62
62
|
pub mod named_address;
|
|
63
63
|
pub mod profile;
|
|
64
64
|
pub mod send;
|
|
65
|
+
pub(crate) mod spec;
|
|
65
66
|
pub mod status;
|
|
66
67
|
pub mod types;
|
|
67
68
|
|
|
@@ -4076,21 +4077,33 @@ pub mod leader_port {
|
|
|
4076
4077
|
"action": "rerun with --confirm to claim ownership of this team",
|
|
4077
4078
|
}));
|
|
4078
4079
|
}
|
|
4080
|
+
let state = crate::state::persist::load_runtime_state(workspace)
|
|
4081
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
4082
|
+
let Some(team_id) = resolve_owner_team_id(&state, team) else {
|
|
4083
|
+
return Ok(json!({
|
|
4084
|
+
"ok": false,
|
|
4085
|
+
"status": "refused",
|
|
4086
|
+
"reason": "team_target_unresolved",
|
|
4087
|
+
"team": team.unwrap_or(""),
|
|
4088
|
+
"hint": "specify an active team id",
|
|
4089
|
+
}));
|
|
4090
|
+
};
|
|
4091
|
+
let requested_team = team;
|
|
4092
|
+
let explicit_team = requested_team.filter(|team| !team.is_empty());
|
|
4093
|
+
let resolved_team = explicit_team.map(|_| team_id.as_str().to_string());
|
|
4079
4094
|
if !positive_caller_pane_env_present() {
|
|
4080
|
-
let state = crate::state::persist::load_runtime_state(workspace)
|
|
4081
|
-
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
4082
|
-
let team_id = resolve_owner_team_id(&state, team)
|
|
4083
|
-
.unwrap_or_else(|| TeamKey::new(crate::state::projection::team_state_key(&state)));
|
|
4084
4095
|
let bind = crate::leader::bind_owner_from_caller_pane(workspace, &team_id, None)
|
|
4085
4096
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
4086
4097
|
return Ok(owner_bind_value(bind));
|
|
4087
4098
|
}
|
|
4099
|
+
let team = resolved_team.as_deref();
|
|
4088
4100
|
let result = crate::leader::claim_leader(workspace, team, true)
|
|
4089
4101
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
4090
4102
|
let mut value = lease_value(result);
|
|
4103
|
+
insert_resolved_team(&mut value, requested_team, resolved_team.as_deref());
|
|
4091
4104
|
if value.get("ok").and_then(Value::as_bool) == Some(true) {
|
|
4092
|
-
emit_topology_convergence_event(workspace,
|
|
4093
|
-
register_after_binding_success(workspace,
|
|
4105
|
+
emit_topology_convergence_event(workspace, resolved_team.as_deref(), "takeover", &value);
|
|
4106
|
+
register_after_binding_success(workspace, resolved_team.as_deref(), "takeover", &mut value);
|
|
4094
4107
|
}
|
|
4095
4108
|
Ok(value)
|
|
4096
4109
|
}
|
|
@@ -4119,12 +4132,15 @@ pub mod leader_port {
|
|
|
4119
4132
|
}
|
|
4120
4133
|
return Ok(owner_bind_value(bind));
|
|
4121
4134
|
}
|
|
4122
|
-
let
|
|
4135
|
+
let explicit_team = team.filter(|team| !team.is_empty());
|
|
4136
|
+
let resolved_team = explicit_team.map(|_| team_id.as_str().to_string());
|
|
4137
|
+
let result = crate::leader::claim_leader(workspace, resolved_team.as_deref(), confirm)
|
|
4123
4138
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
4124
4139
|
let mut value = lease_value(result);
|
|
4140
|
+
insert_resolved_team(&mut value, team, resolved_team.as_deref());
|
|
4125
4141
|
if value.get("ok").and_then(Value::as_bool) == Some(true) {
|
|
4126
|
-
emit_topology_convergence_event(workspace,
|
|
4127
|
-
register_after_binding_success(workspace,
|
|
4142
|
+
emit_topology_convergence_event(workspace, resolved_team.as_deref(), "claim-leader", &value);
|
|
4143
|
+
register_after_binding_success(workspace, resolved_team.as_deref(), "claim-leader", &mut value);
|
|
4128
4144
|
}
|
|
4129
4145
|
Ok(value)
|
|
4130
4146
|
}
|
|
@@ -4275,16 +4291,14 @@ pub mod leader_port {
|
|
|
4275
4291
|
fn resolve_owner_team_id(state: &Value, team: Option<&str>) -> Option<TeamKey> {
|
|
4276
4292
|
match team.filter(|t| !t.is_empty()) {
|
|
4277
4293
|
Some(team_id) => {
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
} else {
|
|
4287
|
-
None
|
|
4294
|
+
match crate::state::projection::resolve_owner_team_id(state, team_id) {
|
|
4295
|
+
crate::state::projection::OwnerTeamResolution::Canonical(key)
|
|
4296
|
+
| crate::state::projection::OwnerTeamResolution::LegacyAlias {
|
|
4297
|
+
canonical: key,
|
|
4298
|
+
..
|
|
4299
|
+
} => Some(TeamKey::new(key)),
|
|
4300
|
+
crate::state::projection::OwnerTeamResolution::Unresolved { .. }
|
|
4301
|
+
| crate::state::projection::OwnerTeamResolution::Ambiguous { .. } => None,
|
|
4288
4302
|
}
|
|
4289
4303
|
}
|
|
4290
4304
|
None => Some(TeamKey::new(crate::state::projection::team_state_key(
|
|
@@ -4293,6 +4307,18 @@ pub mod leader_port {
|
|
|
4293
4307
|
}
|
|
4294
4308
|
}
|
|
4295
4309
|
|
|
4310
|
+
fn insert_resolved_team(value: &mut Value, requested: Option<&str>, resolved: Option<&str>) {
|
|
4311
|
+
let (Some(requested), Some(resolved)) = (requested.filter(|team| !team.is_empty()), resolved)
|
|
4312
|
+
else {
|
|
4313
|
+
return;
|
|
4314
|
+
};
|
|
4315
|
+
let Some(obj) = value.as_object_mut() else {
|
|
4316
|
+
return;
|
|
4317
|
+
};
|
|
4318
|
+
obj.insert("requested_team".to_string(), json!(requested));
|
|
4319
|
+
obj.insert("resolved_team".to_string(), json!(resolved));
|
|
4320
|
+
}
|
|
4321
|
+
|
|
4296
4322
|
fn positive_caller_pane_env_present() -> bool {
|
|
4297
4323
|
std::env::var("TMUX_PANE")
|
|
4298
4324
|
.ok()
|
|
@@ -32,7 +32,13 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
|
|
|
32
32
|
"--to-leader requires a non-empty message".to_string(),
|
|
33
33
|
));
|
|
34
34
|
}
|
|
35
|
-
let value = send_to_canonical_leader_target(
|
|
35
|
+
let value = send_to_canonical_leader_target(
|
|
36
|
+
&args.workspace,
|
|
37
|
+
to_leader,
|
|
38
|
+
&content,
|
|
39
|
+
&args.sender,
|
|
40
|
+
args.task.as_deref(),
|
|
41
|
+
)?;
|
|
36
42
|
return Ok(cmd_send_result(value, args.json));
|
|
37
43
|
}
|
|
38
44
|
if let Some(ref to_name) = args.to_name {
|
|
@@ -1055,10 +1061,10 @@ fn maybe_enqueue_offline_leader_mailbox(
|
|
|
1055
1061
|
// Owner-scope refusal: sender workspace == target workspace. Keep
|
|
1056
1062
|
// the actionable attach hint (owner sees status/diagnose copy that
|
|
1057
1063
|
// points at `attach-leader`).
|
|
1058
|
-
let sender_canonical =
|
|
1059
|
-
.unwrap_or_else(|_| sender_workspace.to_path_buf());
|
|
1060
|
-
let target_canonical =
|
|
1061
|
-
.unwrap_or_else(|_| target_workspace.clone());
|
|
1064
|
+
let sender_canonical =
|
|
1065
|
+
std::fs::canonicalize(sender_workspace).unwrap_or_else(|_| sender_workspace.to_path_buf());
|
|
1066
|
+
let target_canonical =
|
|
1067
|
+
std::fs::canonicalize(&target_workspace).unwrap_or_else(|_| target_workspace.clone());
|
|
1062
1068
|
if sender_canonical == target_canonical {
|
|
1063
1069
|
return Ok(None);
|
|
1064
1070
|
}
|
|
@@ -1084,10 +1090,7 @@ fn maybe_enqueue_offline_leader_mailbox(
|
|
|
1084
1090
|
&event_log,
|
|
1085
1091
|
)
|
|
1086
1092
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
1087
|
-
let message_id = outcome
|
|
1088
|
-
.message_id
|
|
1089
|
-
.clone()
|
|
1090
|
-
.unwrap_or_else(|| "".to_string());
|
|
1093
|
+
let message_id = outcome.message_id.clone().unwrap_or_else(|| "".to_string());
|
|
1091
1094
|
Ok(Some(json!({
|
|
1092
1095
|
"ok": true,
|
|
1093
1096
|
"status": "queued_until_leader_attach",
|
|
@@ -1342,7 +1345,7 @@ pub fn send_to_canonical_leader_target(
|
|
|
1342
1345
|
"channel": "leader_mailbox",
|
|
1343
1346
|
"delivered": false,
|
|
1344
1347
|
"message_status": "queued_until_leader_attach",
|
|
1345
|
-
"action": "run `team-agent leaders` to see registered leaders; retry with a qualified name",
|
|
1348
|
+
"action": "run `team-agent leaders` to see registered leaders; inspect queued leader messages with `team-agent inbox`; retry with a qualified name",
|
|
1346
1349
|
"registry_stale": false,
|
|
1347
1350
|
}));
|
|
1348
1351
|
}
|
|
@@ -1438,11 +1441,7 @@ pub fn send_to_canonical_leader_target(
|
|
|
1438
1441
|
// LIVE: canonical-validated. Delegate to the E6 --to-name path via a
|
|
1439
1442
|
// synthesized `<workspace>::<team_key>/leader` name so live inject +
|
|
1440
1443
|
// mailbox both go through one code path.
|
|
1441
|
-
let to_name = format!(
|
|
1442
|
-
"{}::{}/leader",
|
|
1443
|
-
entry.workspace.display(),
|
|
1444
|
-
entry.team_key
|
|
1445
|
-
);
|
|
1444
|
+
let to_name = format!("{}::{}/leader", entry.workspace.display(), entry.team_key);
|
|
1446
1445
|
let (resolved, transport) =
|
|
1447
1446
|
match crate::cli::named_address::resolve_name_for_cli(sender_workspace, &to_name) {
|
|
1448
1447
|
Ok(r) => r,
|
|
@@ -1455,10 +1454,7 @@ pub fn send_to_canonical_leader_target(
|
|
|
1455
1454
|
"resolved_via".to_string(),
|
|
1456
1455
|
serde_json::Value::String("host_leader_registry".to_string()),
|
|
1457
1456
|
);
|
|
1458
|
-
obj.insert(
|
|
1459
|
-
"delivered".to_string(),
|
|
1460
|
-
serde_json::Value::Bool(false),
|
|
1461
|
-
);
|
|
1457
|
+
obj.insert("delivered".to_string(), serde_json::Value::Bool(false));
|
|
1462
1458
|
}
|
|
1463
1459
|
return Ok(body);
|
|
1464
1460
|
}
|
|
@@ -1484,7 +1480,10 @@ pub fn send_to_canonical_leader_target(
|
|
|
1484
1480
|
// Honest delivered marker. `send_to_named_pane_direct` sets `ok`
|
|
1485
1481
|
// to whether physical inject verified — mirror that as
|
|
1486
1482
|
// `delivered`.
|
|
1487
|
-
let ok = obj
|
|
1483
|
+
let ok = obj
|
|
1484
|
+
.get("ok")
|
|
1485
|
+
.and_then(serde_json::Value::as_bool)
|
|
1486
|
+
.unwrap_or(false);
|
|
1488
1487
|
obj.insert("delivered".to_string(), serde_json::Value::Bool(ok));
|
|
1489
1488
|
}
|
|
1490
1489
|
Ok(value)
|