@team-agent/installer 0.5.119 → 0.5.121

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.
Files changed (73) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +46 -1
  4. package/README.zh.md +46 -1
  5. package/crates/team-agent/src/cli/adapters.rs +11 -0
  6. package/crates/team-agent/src/cli/arguments.rs +401 -0
  7. package/crates/team-agent/src/cli/emit.rs +241 -142
  8. package/crates/team-agent/src/cli/intent/runtime.rs +1175 -0
  9. package/crates/team-agent/src/cli/intent/runtime_tests.rs +698 -0
  10. package/crates/team-agent/src/cli/intent/tests.rs +654 -0
  11. package/crates/team-agent/src/cli/intent.rs +642 -0
  12. package/crates/team-agent/src/cli/mod.rs +53 -6
  13. package/crates/team-agent/src/cli/named_address.rs +10 -1
  14. package/crates/team-agent/src/cli/spec.rs +22 -10
  15. package/crates/team-agent/src/cli/status_port/brief.rs +117 -16
  16. package/crates/team-agent/src/cli/tests/base.rs +6 -1
  17. package/crates/team-agent/src/cli/tests/parser_safety.rs +383 -0
  18. package/crates/team-agent/src/cli/tests/run_delegation.rs +63 -19
  19. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +56 -0
  20. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +3 -2
  21. package/crates/team-agent/src/cli/tests/verb_profile.rs +3 -1
  22. package/crates/team-agent/src/contract_runtime/backend.rs +1 -1
  23. package/crates/team-agent/src/db/message_store.rs +65 -0
  24. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +8 -0
  25. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +111 -4
  26. package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +244 -82
  27. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +166 -77
  28. package/crates/team-agent/src/lifecycle/launch.rs +29 -0
  29. package/crates/team-agent/src/lifecycle/restart/common.rs +7 -1
  30. package/crates/team-agent/src/lifecycle/restart.rs +17 -0
  31. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -3
  32. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +5 -15
  33. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -3
  34. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +2 -3
  35. package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +10 -2
  36. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +8 -1
  37. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +3 -2
  38. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +6 -9
  39. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +7 -5
  40. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +64 -19
  41. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +5 -2
  42. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +34 -10
  43. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +2 -4
  44. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +3 -4
  45. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +5 -4
  46. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +2 -4
  47. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +78 -36
  48. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +8 -4
  49. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +6 -12
  50. package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +4 -0
  51. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1 -3
  52. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +7 -0
  53. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +47 -4
  54. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -3
  55. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -1
  56. package/crates/team-agent/src/lifecycle/types.rs +10 -0
  57. package/crates/team-agent/src/mcp_server/mod.rs +2 -0
  58. package/crates/team-agent/src/mcp_server/readiness.rs +432 -0
  59. package/crates/team-agent/src/mcp_server/readiness_tests.rs +242 -0
  60. package/crates/team-agent/src/mcp_server/wire.rs +96 -11
  61. package/crates/team-agent/src/messaging/delivery.rs +62 -0
  62. package/crates/team-agent/src/messaging/tests/dup_inject.rs +41 -1
  63. package/crates/team-agent/src/provider/adapter.rs +7 -0
  64. package/crates/team-agent/src/tmux_backend/tests.rs +37 -0
  65. package/crates/team-agent/src/tmux_backend.rs +8 -1
  66. package/package.json +4 -4
  67. package/skills/team-agent/SKILL.md +71 -54
  68. package/skills/team-agent/command-coverage.json +283 -13
  69. package/skills/team-agent/docs/reference/team-agent-operator.md +220 -0
  70. package/skills/team-agent/references/bug-as-artifact-flow.md +1 -1
  71. package/skills/team-agent/references/recovery-runbook.md +61 -212
  72. package/skills/team-agent/references/team-agent-operator.md +181 -48
  73. package/skills/team-agent/references/team-in-team.md +47 -80
package/Cargo.lock CHANGED
@@ -727,7 +727,7 @@ dependencies = [
727
727
 
728
728
  [[package]]
729
729
  name = "team-agent"
730
- version = "0.5.119"
730
+ version = "0.5.121"
731
731
  dependencies = [
732
732
  "anyhow",
733
733
  "chrono",
package/Cargo.toml CHANGED
@@ -11,7 +11,7 @@ exclude = ["crates/team-agent-contract"]
11
11
 
12
12
  [workspace.package]
13
13
  edition = "2021"
14
- version = "0.5.119"
14
+ version = "0.5.121"
15
15
  license = "AGPL-3.0"
16
16
  rust-version = "1.95"
17
17
 
package/README.md CHANGED
@@ -20,7 +20,7 @@ Then:
20
20
  - Teammates message each other directly (frontend asks backend for the API schema — no need to go through you)
21
21
  - You only talk to the lead; the lead reports progress and only escalates when there's a real decision
22
22
 
23
- No config files. No new UI to learn. If you can chat with Claude, you can run a team.
23
+ No new dashboard or orchestration language: the Leader prepares team files from your approved plan. If you can chat with Claude, you can run a team.
24
24
 
25
25
  ## Install
26
26
 
@@ -36,6 +36,51 @@ team-agent claude
36
36
 
37
37
  Two steps. Everything else happens in the conversation.
38
38
 
39
+ ## Everyday CLI: start, stop, restart
40
+
41
+ Check the selected binary with `team-agent start --help` and `team-agent team --help`. If these are missing, obtain an authorized supporting build; a version string alone is not a feature guarantee. Lifecycle commands run in the authorized native **Leader's tool context**, not an ordinary worker's pane.
42
+
43
+ The Leader prepares `TEAM.md` and `agents/*.md` once, recording the user's provider and boolean permission-bypass choices. For example:
44
+
45
+ ```text
46
+ .team/current/
47
+ TEAM.md
48
+ agents/reviewer.md
49
+ ```
50
+
51
+ With that complete source, operate on an explicit workspace/team:
52
+
53
+ ```sh
54
+ team-agent start --from .team/current --workspace . --team project-team
55
+ team-agent start --workspace . --team project-team
56
+ team-agent send reviewer "Review the change and report the main risk" --workspace . --team project-team
57
+ team-agent inbox leader --workspace . --team project-team -n 3
58
+ team-agent stop reviewer --workspace . --team project-team
59
+ team-agent start reviewer --workspace . --team project-team
60
+ team-agent restart reviewer --workspace . --team project-team
61
+ team-agent stop --workspace . --team project-team --json
62
+ team-agent start --workspace . --team project-team
63
+ team-agent team restart --workspace . --team project-team --json
64
+ ```
65
+
66
+ | Command | Intent and boundary |
67
+ |---|---|
68
+ | `start` | Create once from complete configuration; later activate the selected non-paused roster. Verified live/drift-free instances stay unchanged |
69
+ | `start AGENT` | Wake a stopped seat, prove a live no-op, or add a RoleOnly seat from its complete file. A new ID needs explicit provider/bypass decisions |
70
+ | `stop [AGENT]` | Stop one agent or the selected team, retaining saved associations; team stop reports owned residuals |
71
+ | `restart AGENT` | Preflight, confirmed stop, then start that agent; no implicit role patches or reset |
72
+ | `team restart` | Deliberate team restart; preserve explicitly stopped/paused seats |
73
+ | `team status` / `status` | Read the concise node projection; diagnose with `doctor` |
74
+ | `agent add/remove/reset/clone/fork` | Explicit operations with their original confirmation/capability gates; reset requires `--discard-session`, fork requires `--as NEW_ID` |
75
+
76
+ New agent/team restart defaults to `--session auto`: resume when proven, otherwise only an existing legal fresh fallback. **Auto is not a promise of preserved memory.** For strict recovery use `team-agent team restart --session preserve --workspace . --team project-team`; known unavailable continuation refuses before stopping. Unknown or partial outcomes are not permission to reset or retry blindly. `--dry-run` on start/restart has no lifecycle effects, and no eligible targets is a nonzero refusal.
77
+
78
+ No-op does not synchronize changed role bodies/settings or prove business readiness. Stop before applying worker changes. A queued send is not the reply. Verify actual results and, after final `stop`, empty scoped residual resources; never clean up sibling teams or shared servers. Team sources use `--from DIR`/explicit paths; `reviewer.v2` is an agent ID, and `agent restart ID` disambiguates a same-named directory. Native unsupported capabilities remain refusals; `--in-place` fork is not implemented.
79
+
80
+ ### Compatibility bridges
81
+
82
+ Existing automation may keep `quick-start`, `shutdown` and suffixed `*-agent` commands. They remain supported but are not the daily instructions above. Old bare/explicit-path `restart` retains preserve/default receipts; new `team restart` defaults to auto. Legacy single-seat start refuses a live seat, whereas new start can prove a no-op: do not mechanically rename scripts or assume byte-identical JSON. See the [operation skill](skills/team-agent/SKILL.md) and [operator reference](skills/team-agent/references/team-agent-operator.md).
83
+
39
84
  ## What you can say
40
85
 
41
86
  Team building and management is all natural language. Some real examples:
package/README.zh.md CHANGED
@@ -20,7 +20,7 @@
20
20
  - 队友之间直接互发消息对齐(前端问后端要接口定义,不用经过你)
21
21
  - 你只跟队长说话,队长汇总进度、有分歧才来问你
22
22
 
23
- 整个过程没有配置文件,没有新的界面要学。会跟 Claude 聊天,就会带团队。
23
+ 不用学习新的编排界面或语言:队长按你批准的方案准备团队文件。会跟 Claude 聊天,就会带团队。
24
24
 
25
25
  ## 安装
26
26
 
@@ -36,6 +36,51 @@ team-agent claude
36
36
 
37
37
  就这两步。后面发生的一切都在对话里。
38
38
 
39
+ ## 日常 CLI:start、stop、restart
40
+
41
+ 先核对实际二进制的 `team-agent start --help` 与 `team-agent team --help`。缺少这些入口时,应取得经授权的支持版本,不能仅凭版本号断定功能存在。生命周期命令由合法原生 **Leader 的工具上下文**执行,普通 Worker 不能据此升权。
42
+
43
+ 队长一次性准备 `TEAM.md` 与 `agents/*.md`,明确记录用户选择的 Provider 和布尔型权限绕过策略,例如:
44
+
45
+ ```text
46
+ .team/current/
47
+ TEAM.md
48
+ agents/reviewer.md
49
+ ```
50
+
51
+ 配置完整后,显式选定工作区和团队:
52
+
53
+ ```sh
54
+ team-agent start --from .team/current --workspace . --team project-team
55
+ team-agent start --workspace . --team project-team
56
+ team-agent send reviewer "检查修改并回报主要风险" --workspace . --team project-team
57
+ team-agent inbox leader --workspace . --team project-team -n 3
58
+ team-agent stop reviewer --workspace . --team project-team
59
+ team-agent start reviewer --workspace . --team project-team
60
+ team-agent restart reviewer --workspace . --team project-team
61
+ team-agent stop --workspace . --team project-team --json
62
+ team-agent start --workspace . --team project-team
63
+ team-agent team restart --workspace . --team project-team --json
64
+ ```
65
+
66
+ | 命令 | 意图与边界 |
67
+ |---|---|
68
+ | `start` | 完整配置首次建队;后续激活所选非 paused 席位;已确证存活且无配置漂移的实例保持不变 |
69
+ | `start AGENT` | 唤醒停止席位、确证存活 no-op,或从完整角色文件加入 RoleOnly 席位;全新 ID 必须明确 Provider/bypass |
70
+ | `stop [AGENT]` | 停单席或所选整队,保留会话关联;整队停止返回自有残留报告 |
71
+ | `restart AGENT` | 先预检、确认停止后再启动该席位;不偷偷改角色或 reset |
72
+ | `team restart` | 明确重启整队,保留显式停止和 paused 席位 |
73
+ | `team status` / `status` | 读取简要节点投影;深入诊断用 `doctor` |
74
+ | `agent add/remove/reset/clone/fork` | 保留原确认/能力门禁;reset 要求 `--discard-session`,fork 要求 `--as NEW_ID` |
75
+
76
+ 新单席/整队重启默认 `--session auto`:有确证就续接,否则仅允许原引擎支持的合法 fresh。**auto 不是记忆不丢失的保证。** 严格恢复用 `team-agent team restart --session preserve --workspace . --team project-team`;已知无法续接时停止前拒绝。unknown/partial 不是 reset 或盲目重试许可。start/restart 的 `--dry-run` 不产生生命周期效果;没有可操作目标时非零拒绝,不伪报启动成功。
77
+
78
+ no-op 不表示角色正文/参数修改已生效,也不表示业务就绪;修改运行席位前先 stop。消息已排队不是回包。最终 stop 后核验所选团队残留为空,不能清理兄弟团队或共享服务器。团队目录用 `--from DIR`/明确路径选择;`reviewer.v2` 仍是节点 ID,同名目录歧义用 `agent restart ID` 消除。原生 Provider 不支持的能力必须拒绝,fork 的 `--in-place` 尚未实现。
79
+
80
+ ### 旧命令仅作兼容桥接
81
+
82
+ 既有自动化仍可保留 `quick-start`、`shutdown` 与 `*-agent`,但新日常指引采用上面的直觉命令。旧裸/显式路径 `restart` 保持 preserve 默认和原回执;新 `team restart` 默认 auto。旧单席 start 拒绝运行节点,新 start 可以确证 no-op:不要机械改名脚本或假设 JSON 逐字节等价。详见[操作 Skill](skills/team-agent/SKILL.md)与[操作参考](skills/team-agent/references/team-agent-operator.md)。
83
+
39
84
  ## 你可以说什么
40
85
 
41
86
  组队和管队全是自然语言,举几个真实的例子:
@@ -724,6 +724,11 @@ fn unified_default_doctor_report(args: &DoctorArgs, mut value: Value) -> Value {
724
724
  );
725
725
  merge_report_array(&mut value, "issues", &issues);
726
726
  merge_report_array(&mut value, "suggested_repairs", &repairs);
727
+ let brief = crate::cli::status_port::status_brief_scoped(
728
+ &selected.run_workspace, &state, None,
729
+ );
730
+ let observations = brief["nodes"].as_array().into_iter().flatten()
731
+ .filter(|node| node.get("observation_reason").is_some()).cloned().collect::<Vec<_>>();
727
732
  let run_workspace = selected.run_workspace.to_string_lossy().to_string();
728
733
  runtime = json!({
729
734
  "status": "present",
@@ -733,6 +738,7 @@ fn unified_default_doctor_report(args: &DoctorArgs, mut value: Value) -> Value {
733
738
  "session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
734
739
  "leader_receiver": state.get("leader_receiver").cloned().unwrap_or(Value::Null),
735
740
  "agent_count": state.get("agents").and_then(Value::as_object).map_or(0, serde_json::Map::len),
741
+ "observations": observations,
736
742
  "message_count": count_dir_entries(&selected.run_workspace.join(".team").join("messages")),
737
743
  "result_count": count_dir_entries(&selected.run_workspace.join(".team").join("results")),
738
744
  });
@@ -740,6 +746,11 @@ fn unified_default_doctor_report(args: &DoctorArgs, mut value: Value) -> Value {
740
746
  }
741
747
  Err(error) => {
742
748
  runtime["status"] = Value::String("unresolved".to_string());
749
+ runtime["observation_reason"] = json!("runtime_selection_failed");
750
+ runtime["next_action"] = json!(format!(
751
+ "team-agent status --workspace {} --team {} --json; verify the intended workspace/team selector before lifecycle changes",
752
+ shell_quote(&requested_workspace), shell_quote(team_key),
753
+ ));
743
754
  append_issue_repair(
744
755
  &mut value,
745
756
  "runtime_selection_failed",
@@ -0,0 +1,401 @@
1
+ //! Pure control-argument grammar. Native/route payloads keep their own parsers.
2
+ use super::CliError;
3
+ use std::collections::BTreeSet;
4
+
5
+ #[derive(Clone, Copy)]
6
+ enum ValueKind {
7
+ Switch,
8
+ Token,
9
+ Path,
10
+ Payload,
11
+ Query,
12
+ Count,
13
+ Seconds,
14
+ PositiveSeconds,
15
+ Choice(&'static [&'static str]),
16
+ FoldedChoice(&'static [&'static str]),
17
+ }
18
+ use ValueKind::*;
19
+
20
+ struct Schema {
21
+ flags: &'static [(&'static str, ValueKind)],
22
+ max_positionals: usize,
23
+ workspace: bool,
24
+ team: bool,
25
+ }
26
+
27
+ fn schema(command: &str) -> Option<Schema> {
28
+ let (flags, max_positionals): (&'static [(&'static str, ValueKind)], usize) = match command {
29
+ "intent-start" | "start" => (
30
+ &[
31
+ ("--from", Path),
32
+ ("--session", Choice(&["auto", "preserve"])),
33
+ ("--dry-run", Switch),
34
+ ("--allow-fresh", Switch),
35
+ ("--provider", Token),
36
+ ("--model", Token),
37
+ ("--effort", Token),
38
+ ("--bypass", Choice(&["true", "false"])),
39
+ ("--prompt", Payload),
40
+ ("--profile", Token),
41
+ ("--role-file", Path),
42
+ ],
43
+ 1,
44
+ ),
45
+ "intent-stop" | "stop" => (&[], 1),
46
+ "intent-restart" => (
47
+ &[
48
+ ("--session", Choice(&["auto", "preserve"])),
49
+ ("--dry-run", Switch),
50
+ ("--allow-fresh", Switch),
51
+ ("--session-converge-deadline", Seconds),
52
+ ("--detail", Switch),
53
+ ],
54
+ 1,
55
+ ),
56
+ "quick-start" => (
57
+ &[
58
+ ("--name", Token),
59
+ ("--team-id", Token),
60
+ ("--yes", Switch),
61
+ ("--backend", FoldedChoice(&["tmux", "conpty"])),
62
+ ("--detail", Switch),
63
+ ("--no-display", Switch),
64
+ ],
65
+ 1,
66
+ ),
67
+ "send" => (
68
+ &[
69
+ ("--targets", Token),
70
+ ("--pane", Token),
71
+ ("--to-name", Token),
72
+ ("--to-leader", Token),
73
+ ("--mailbox", Switch),
74
+ (
75
+ "--presentation-sink",
76
+ Choice(&["leader", "casefile", "silent"]),
77
+ ),
78
+ ("--message-class", Token),
79
+ ("--case-id", Token),
80
+ ("--task", Token),
81
+ ("--watch-result", Switch),
82
+ ("--requires-ack", Switch),
83
+ ("--no-ack", Switch),
84
+ ("--no-wait", Switch),
85
+ ("--timeout", Seconds),
86
+ ("--confirm-human", Switch),
87
+ ("--message-id", Token),
88
+ ],
89
+ usize::MAX,
90
+ ),
91
+ "status" => (&[("--detail", Switch), ("--summary", Switch)], 1),
92
+ "models" => (&[("--provider", Token), ("--search", Query)], usize::MAX),
93
+ "inbox" => (&[("--limit", Count)], 1),
94
+ "shutdown" => (&[("--keep-logs", Switch)], 0),
95
+ "restart" => (
96
+ &[
97
+ ("--allow-fresh", Switch),
98
+ ("--session-converge-deadline", Seconds),
99
+ ("--detail", Switch),
100
+ ("--no-display", Switch),
101
+ ],
102
+ 1,
103
+ ),
104
+ "start-agent" | "add-agent" => (
105
+ &[
106
+ ("--provider", Token),
107
+ ("--model", Token),
108
+ ("--effort", Token),
109
+ ("--bypass", Choice(&["true", "false"])),
110
+ ("--prompt", Payload),
111
+ ("--profile", Token),
112
+ ("--role-file", Path),
113
+ ("--force", Switch),
114
+ ("--allow-fresh", Switch),
115
+ ],
116
+ 1,
117
+ ),
118
+ "stop-agent" | "approvals" => (&[], 1),
119
+ "reset-agent" => (&[("--discard-session", Switch)], 1),
120
+ "clone-agent" | "fork-agent" => (&[("--as", Token), ("--label", Token)], 1),
121
+ "remove-agent" => (
122
+ &[
123
+ ("--from-spec", Switch),
124
+ ("--confirm", Switch),
125
+ ("--force", Switch),
126
+ ],
127
+ 1,
128
+ ),
129
+ "doctor" => (
130
+ &[
131
+ ("--gate", Token), // Preserve the doctor's typed unknown_gate refusal.
132
+ ("--comms", Switch),
133
+ ("--fix", Switch),
134
+ ("--fix-schema", Switch),
135
+ ("--cleanup-orphans", Switch),
136
+ ("--confirm", Switch),
137
+ ],
138
+ 1,
139
+ ),
140
+ "leaders" => (
141
+ &[
142
+ ("--all", Switch),
143
+ ("--stale", Switch),
144
+ ("--prune", Switch),
145
+ ("--dry-run", Switch),
146
+ ("--search", Token),
147
+ ],
148
+ 1,
149
+ ),
150
+ "profile" => (
151
+ &[
152
+ ("--auth-mode", Token),
153
+ ("--proxy-mode", Choice(&["direct", "inherit"])),
154
+ ],
155
+ 2,
156
+ ),
157
+ "install-skill" => (
158
+ &[
159
+ ("--target", Choice(&["all", "codex", "claude", "copilot"])),
160
+ ("--source", Path),
161
+ ("--dest", Path),
162
+ ("--uninstall", Switch),
163
+ ("--dry-run", Switch),
164
+ ],
165
+ 0,
166
+ ),
167
+ "claim-leader" => (&[("--confirm", Switch), ("--detail", Switch)], 0),
168
+ "takeover" => (&[("--confirm", Switch)], 0),
169
+ "attach-leader" => (
170
+ &[
171
+ ("--pane", Token),
172
+ ("--provider", Token),
173
+ ("--confirm", Switch),
174
+ ],
175
+ 0,
176
+ ),
177
+ "attach-app-server-leader" => (&[("--socket", Path), ("--thread-id", Token)], 0),
178
+ "wait" => (&[("--task", Token)], 0),
179
+ "coordinator" => (
180
+ &[("--once", Switch), ("--tick-interval", PositiveSeconds)],
181
+ 0,
182
+ ),
183
+ "kiro" => (&[], 0),
184
+ _ => return None,
185
+ };
186
+ Some(Schema {
187
+ flags,
188
+ max_positionals,
189
+ workspace: !matches!(command, "models" | "leaders" | "install-skill" | "kiro"),
190
+ team: !matches!(
191
+ command,
192
+ "models" | "leaders" | "install-skill" | "kiro" | "wait" | "quick-start"
193
+ ),
194
+ })
195
+ }
196
+
197
+ fn canonical_flag<'a>(command: &str, flag: &'a str) -> &'a str {
198
+ match (command, flag) {
199
+ (_, "-h") => "--help",
200
+ ("inbox", "-n") => "--limit",
201
+ ("quick-start", "--team") => "--team-id",
202
+ ("send", "--target" | "--to") => "--targets",
203
+ _ => flag,
204
+ }
205
+ }
206
+
207
+ fn option(schema: &Schema, command: &str, flag: &str) -> Option<ValueKind> {
208
+ match flag {
209
+ "--help" | "--json" | "--no-display" => Some(Switch),
210
+ "--workspace" if schema.workspace => Some(Path),
211
+ "--team" if schema.team => Some(Token),
212
+ "--role-file" if command == "start-agent" => None,
213
+ "--allow-fresh" if command == "add-agent" => None,
214
+ _ => schema
215
+ .flags
216
+ .iter()
217
+ .find(|(name, _)| *name == flag)
218
+ .map(|(_, kind)| *kind),
219
+ }
220
+ }
221
+
222
+ fn valid_value(kind: ValueKind, value: &str) -> bool {
223
+ if value.trim().is_empty() {
224
+ return matches!(kind, Query);
225
+ }
226
+ match kind {
227
+ Count => value.parse::<usize>().is_ok(),
228
+ Seconds | PositiveSeconds => value.parse::<f64>().is_ok_and(|number| {
229
+ number.is_finite()
230
+ && number >= 0.0
231
+ && number <= u64::MAX as f64 / 1000.0
232
+ && (!matches!(kind, PositiveSeconds) || number > 0.0)
233
+ }),
234
+ Choice(values) => values.contains(&value),
235
+ FoldedChoice(values) => values.contains(&value.trim().to_ascii_lowercase().as_str()),
236
+ _ => true,
237
+ }
238
+ }
239
+
240
+ pub(super) struct CheckedArgs {
241
+ pub(super) tokens: Vec<String>,
242
+ pub(super) help: bool,
243
+ }
244
+
245
+ /// Validate only control tokens. Values retain everything after the first '='.
246
+ /// A free --help wins before filesystem/owner checks; a consumed/literal one never does.
247
+ pub(super) fn parse(command: &str, args: &[String]) -> Result<CheckedArgs, CliError> {
248
+ let schema = schema(command)
249
+ .ok_or_else(|| CliError::Usage(format!("no argument schema for {command}")))?;
250
+ let mut tokens = Vec::new();
251
+ let mut seen = BTreeSet::new();
252
+ let mut positionals = 0usize;
253
+ let mut error = None;
254
+ let mut help = false;
255
+ let mut i = 0;
256
+ while i < args.len() {
257
+ let raw = &args[i];
258
+ if raw == "--" {
259
+ positionals += args.len() - i - 1;
260
+ tokens.extend_from_slice(&args[i..]);
261
+ break;
262
+ }
263
+ if !raw.starts_with('-') {
264
+ positionals += 1;
265
+ tokens.push(raw.clone());
266
+ i += 1;
267
+ continue;
268
+ }
269
+ let (key, inline) = raw
270
+ .split_once('=')
271
+ .map_or((raw.as_str(), None), |(key, value)| (key, Some(value)));
272
+ let flag = canonical_flag(command, key);
273
+ let Some(kind) = option(&schema, command, flag) else {
274
+ error.get_or_insert_with(|| format!("unknown option for {command}: {key}"));
275
+ i += 1;
276
+ continue;
277
+ };
278
+ if flag == "--help" && inline.is_none() {
279
+ help = true;
280
+ }
281
+ if !seen.insert(flag.to_string()) {
282
+ error.get_or_insert_with(|| format!("duplicate option: {flag}"));
283
+ }
284
+ if matches!(kind, Switch) {
285
+ if inline.is_some() {
286
+ error.get_or_insert_with(|| format!("{flag} does not take a value"));
287
+ }
288
+ // Explicit historical compatibility flag, never inferred from an unknown
289
+ // option and never removed from a consumed or post-delimiter payload.
290
+ if flag != "--no-display" {
291
+ tokens.push(flag.to_string());
292
+ }
293
+ } else {
294
+ let value = if let Some(value) = inline {
295
+ Some(value)
296
+ } else {
297
+ args.get(i + 1)
298
+ .filter(|value| matches!(kind, Payload | Query) || !value.starts_with('-'))
299
+ .map(|value| {
300
+ i += 1;
301
+ value.as_str()
302
+ })
303
+ };
304
+ match value {
305
+ Some(value) if valid_value(kind, value) => tokens.push(format!("{flag}={value}")),
306
+ Some(_) => {
307
+ error.get_or_insert_with(|| format!("invalid value for {flag}"));
308
+ }
309
+ None => {
310
+ error.get_or_insert_with(|| format!("missing value for {flag}"));
311
+ }
312
+ }
313
+ }
314
+ i += 1;
315
+ }
316
+ if help {
317
+ return Ok(CheckedArgs { tokens, help });
318
+ }
319
+ if positionals > schema.max_positionals {
320
+ error.get_or_insert_with(|| format!("too many positional arguments for {command}"));
321
+ }
322
+ for pair in [
323
+ ["--detail", "--summary"],
324
+ ["--all", "--stale"],
325
+ ["--requires-ack", "--no-ack"],
326
+ ] {
327
+ if pair.iter().all(|flag| seen.contains(*flag)) {
328
+ error.get_or_insert_with(|| format!("{} conflicts with {}", pair[0], pair[1]));
329
+ }
330
+ }
331
+ if command == "send"
332
+ && ["--targets", "--pane", "--to-name", "--to-leader"]
333
+ .iter()
334
+ .filter(|flag| seen.contains(**flag))
335
+ .count()
336
+ > 1
337
+ {
338
+ error.get_or_insert_with(|| "choose one recipient addressing mode".to_string());
339
+ }
340
+ if command == "start-agent" && seen.contains("--force") {
341
+ error = Some("start-agent does not support --force; use stop-agent AGENT followed by start-agent AGENT for a safe restart".to_string());
342
+ }
343
+ if let Some(error) = error {
344
+ return Err(CliError::Usage(error));
345
+ }
346
+ Ok(CheckedArgs { tokens, help })
347
+ }
348
+
349
+ /// Rendering switches must not be inferred from a consumed value or literal payload.
350
+ pub(super) fn has_flag(command: &str, args: &[String], needle: &str) -> bool {
351
+ if matches!(command, "team" | "agent") {
352
+ if let Some((operation, args)) = args.split_first() {
353
+ let command = match operation.as_str() {
354
+ "start" => "intent-start",
355
+ "stop" => "intent-stop",
356
+ "restart" => "intent-restart",
357
+ "status" if command == "team" => "status",
358
+ "add" => "add-agent",
359
+ "remove" => "remove-agent",
360
+ "reset" => "reset-agent",
361
+ "clone" => "clone-agent",
362
+ "fork" => "fork-agent",
363
+ _ => return false,
364
+ };
365
+ return has_flag(command, args, needle);
366
+ }
367
+ }
368
+ let Some(schema) = schema(command) else {
369
+ // Dedicated native parsers retain their pre-delimiter rendering switch.
370
+ return args
371
+ .iter()
372
+ .take_while(|arg| arg.as_str() != "--")
373
+ .any(|arg| arg == needle);
374
+ };
375
+ let mut i = 0;
376
+ while i < args.len() {
377
+ let raw = &args[i];
378
+ if raw == "--" {
379
+ break;
380
+ }
381
+ let (key, inline) = raw
382
+ .split_once('=')
383
+ .map_or((raw.as_str(), None), |(key, value)| (key, Some(value)));
384
+ let flag = canonical_flag(command, key);
385
+ if flag == needle && inline.is_none() {
386
+ return true;
387
+ }
388
+ if let Some(kind) = option(&schema, command, flag) {
389
+ if !matches!(kind, Switch) && inline.is_none() {
390
+ if args
391
+ .get(i + 1)
392
+ .is_some_and(|value| matches!(kind, Payload | Query) || !value.starts_with('-'))
393
+ {
394
+ i += 1;
395
+ }
396
+ }
397
+ }
398
+ i += 1;
399
+ }
400
+ false
401
+ }