@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/README.md +46 -1
- package/README.zh.md +46 -1
- package/crates/team-agent/src/cli/adapters.rs +11 -0
- package/crates/team-agent/src/cli/arguments.rs +401 -0
- package/crates/team-agent/src/cli/emit.rs +241 -142
- package/crates/team-agent/src/cli/intent/runtime.rs +1175 -0
- package/crates/team-agent/src/cli/intent/runtime_tests.rs +698 -0
- package/crates/team-agent/src/cli/intent/tests.rs +654 -0
- package/crates/team-agent/src/cli/intent.rs +642 -0
- package/crates/team-agent/src/cli/mod.rs +53 -6
- package/crates/team-agent/src/cli/named_address.rs +10 -1
- package/crates/team-agent/src/cli/spec.rs +22 -10
- package/crates/team-agent/src/cli/status_port/brief.rs +117 -16
- package/crates/team-agent/src/cli/tests/base.rs +6 -1
- package/crates/team-agent/src/cli/tests/parser_safety.rs +383 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +63 -19
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +56 -0
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +3 -2
- package/crates/team-agent/src/cli/tests/verb_profile.rs +3 -1
- package/crates/team-agent/src/contract_runtime/backend.rs +1 -1
- package/crates/team-agent/src/db/message_store.rs +65 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +8 -0
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +111 -4
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +244 -82
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +166 -77
- package/crates/team-agent/src/lifecycle/launch.rs +29 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +7 -1
- package/crates/team-agent/src/lifecycle/restart.rs +17 -0
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +5 -15
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +10 -2
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +8 -1
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +6 -9
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +7 -5
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +64 -19
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +5 -2
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +34 -10
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +2 -4
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +3 -4
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +5 -4
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +2 -4
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +78 -36
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +8 -4
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +6 -12
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +4 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +47 -4
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -1
- package/crates/team-agent/src/lifecycle/types.rs +10 -0
- package/crates/team-agent/src/mcp_server/mod.rs +2 -0
- package/crates/team-agent/src/mcp_server/readiness.rs +432 -0
- package/crates/team-agent/src/mcp_server/readiness_tests.rs +242 -0
- package/crates/team-agent/src/mcp_server/wire.rs +96 -11
- package/crates/team-agent/src/messaging/delivery.rs +62 -0
- package/crates/team-agent/src/messaging/tests/dup_inject.rs +41 -1
- package/crates/team-agent/src/provider/adapter.rs +7 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +37 -0
- package/crates/team-agent/src/tmux_backend.rs +8 -1
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +71 -54
- package/skills/team-agent/command-coverage.json +283 -13
- package/skills/team-agent/docs/reference/team-agent-operator.md +220 -0
- package/skills/team-agent/references/bug-as-artifact-flow.md +1 -1
- package/skills/team-agent/references/recovery-runbook.md +61 -212
- package/skills/team-agent/references/team-agent-operator.md +181 -48
- package/skills/team-agent/references/team-in-team.md +47 -80
|
@@ -57,14 +57,6 @@ pub fn run(argv: &[String], cwd: &Path) -> ExitCode {
|
|
|
57
57
|
println!("{}", default_help());
|
|
58
58
|
return ExitCode::Ok;
|
|
59
59
|
};
|
|
60
|
-
if is_machine_command(command)
|
|
61
|
-
&& argv[1..]
|
|
62
|
-
.iter()
|
|
63
|
-
.any(|arg| matches!(arg.as_str(), "-h" | "--help"))
|
|
64
|
-
{
|
|
65
|
-
eprintln!("{HUMAN_NAVIGATION}");
|
|
66
|
-
return ExitCode::Usage;
|
|
67
|
-
}
|
|
68
60
|
if command == "route" {
|
|
69
61
|
return super::route::run(&argv[1..]);
|
|
70
62
|
}
|
|
@@ -85,21 +77,26 @@ pub fn run(argv: &[String], cwd: &Path) -> ExitCode {
|
|
|
85
77
|
println!("team-agent {}", env!("CARGO_PKG_VERSION"));
|
|
86
78
|
return ExitCode::Ok;
|
|
87
79
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.skip(1)
|
|
99
|
-
.any(|arg| matches!(arg.as_str(), "-h" | "--help"))
|
|
80
|
+
if let Some(result) = super::intent::run(command, &argv[1..], cwd) {
|
|
81
|
+
return match result {
|
|
82
|
+
Ok(result) => emit_result(result),
|
|
83
|
+
Err(error) => emit_cli_error_for_command(command, &argv[1..], cwd, &error),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// Classify syntax/scope errors before dispatch; contextual status refusals
|
|
87
|
+
// retain their existing stderr-only contract. Resolve the scope only once.
|
|
88
|
+
if is_machine_command(command)
|
|
89
|
+
|| matches!(command_spec(command).map(|spec| spec.kind), Some(CommandKind::Dispatch(_)))
|
|
100
90
|
{
|
|
101
|
-
|
|
102
|
-
|
|
91
|
+
let checked = match prepare_control_args(command, &argv[1..], cwd) {
|
|
92
|
+
Ok(checked) => checked,
|
|
93
|
+
Err(error) => return emit_parser_usage(command, &argv[1..], &error),
|
|
94
|
+
};
|
|
95
|
+
let error_args = checked.tokens.clone();
|
|
96
|
+
return match dispatch_prepared(command, checked, cwd) {
|
|
97
|
+
Ok(exit) => exit,
|
|
98
|
+
Err(error) => emit_cli_error_for_command(command, &error_args, cwd, &error),
|
|
99
|
+
};
|
|
103
100
|
}
|
|
104
101
|
match dispatch(command, &argv[1..], cwd) {
|
|
105
102
|
Ok(exit) => exit,
|
|
@@ -136,7 +133,7 @@ fn emit_cli_error_for_command(
|
|
|
136
133
|
},
|
|
137
134
|
_ => error.to_string(),
|
|
138
135
|
};
|
|
139
|
-
if
|
|
136
|
+
if super::arguments::has_flag(command, args, "--json") {
|
|
140
137
|
let payload = error.to_payload(Path::new(""), command);
|
|
141
138
|
let mut value = serde_json::to_value(payload)
|
|
142
139
|
.unwrap_or_else(|_| serde_json::json!({"ok": false, "error": error.to_string()}));
|
|
@@ -152,11 +149,18 @@ fn emit_cli_error_for_command(
|
|
|
152
149
|
ExitCode::Usage
|
|
153
150
|
} else if command == "status" {
|
|
154
151
|
emit_status_cli_error(error)
|
|
152
|
+
} else if matches!(error, CliError::Usage(_)) {
|
|
153
|
+
emit_parser_usage(command, args, error)
|
|
155
154
|
} else {
|
|
156
155
|
emit_cli_error(command, args, cwd, error)
|
|
157
156
|
}
|
|
158
157
|
}
|
|
159
158
|
|
|
159
|
+
#[cfg(test)]
|
|
160
|
+
pub(crate) fn __test_runtime_error_emitter(command: &str, cwd: &Path, error: &CliError) -> ExitCode {
|
|
161
|
+
emit_cli_error(command, &[], cwd, error)
|
|
162
|
+
}
|
|
163
|
+
|
|
160
164
|
fn emit_status_cli_error(error: &CliError) -> ExitCode {
|
|
161
165
|
let normalized = normalize_cli_error(error);
|
|
162
166
|
let payload_error = normalized.as_ref().unwrap_or(error);
|
|
@@ -218,33 +222,34 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
|
|
|
218
222
|
if command == "leader-prompt" {
|
|
219
223
|
return Ok(super::leader_prompt::run(args));
|
|
220
224
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
let filtered_args: Vec<String> = args
|
|
224
|
-
.iter()
|
|
225
|
-
.filter(|arg| arg.as_str() != "--no-display")
|
|
226
|
-
.cloned()
|
|
227
|
-
.collect();
|
|
228
|
-
let args = filtered_args.as_slice();
|
|
229
|
-
if is_machine_command(command) {
|
|
230
|
-
if args
|
|
231
|
-
.iter()
|
|
232
|
-
.any(|arg| matches!(arg.as_str(), "-h" | "--help"))
|
|
233
|
-
{
|
|
234
|
-
eprintln!("{HUMAN_NAVIGATION}");
|
|
235
|
-
return Ok(ExitCode::Usage);
|
|
236
|
-
}
|
|
237
|
-
return dispatch_machine(command, args, cwd);
|
|
225
|
+
if let Some(result) = super::intent::run(command, args, cwd) {
|
|
226
|
+
return result.map(emit_result);
|
|
238
227
|
}
|
|
239
|
-
let
|
|
228
|
+
let machine = is_machine_command(command);
|
|
229
|
+
if !machine && !matches!(command_spec(command).map(|spec| spec.kind), Some(CommandKind::Dispatch(_))) {
|
|
240
230
|
return Ok(emit_unknown_subcommand_usage(command));
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
231
|
+
}
|
|
232
|
+
dispatch_prepared(command, prepare_control_args(command, args, cwd)?, cwd)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fn prepare_control_args(command: &str, args: &[String], cwd: &Path) -> Result<super::arguments::CheckedArgs, CliError> {
|
|
236
|
+
let mut checked = super::arguments::parse(command, args)?;
|
|
237
|
+
if !checked.help { checked.tokens = resolve_command_scope(command, checked.tokens, cwd)?; }
|
|
238
|
+
Ok(checked)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
fn dispatch_prepared(command: &str, checked: super::arguments::CheckedArgs, cwd: &Path) -> Result<ExitCode, CliError> {
|
|
242
|
+
let machine = is_machine_command(command);
|
|
243
|
+
if checked.help {
|
|
244
|
+
if machine {
|
|
245
|
+
eprintln!("{HUMAN_NAVIGATION}");
|
|
246
|
+
return Ok(ExitCode::Usage);
|
|
246
247
|
}
|
|
248
|
+
println!("{}", command_help(Some(command)));
|
|
249
|
+
return Ok(ExitCode::Ok);
|
|
247
250
|
}
|
|
251
|
+
let args = checked.tokens.as_slice();
|
|
252
|
+
if machine { return dispatch_machine(command, args, cwd); }
|
|
248
253
|
if args.iter().all(|arg| arg == "--json")
|
|
249
254
|
&& matches!(
|
|
250
255
|
command,
|
|
@@ -344,17 +349,18 @@ fn is_known_subcommand(command: &str) -> bool {
|
|
|
344
349
|
}
|
|
345
350
|
|
|
346
351
|
pub(crate) fn default_help() -> String {
|
|
347
|
-
let mut out = String::from("Team Agent: create a team, send tasks, read replies and shut down safely.\n\n1. Prepare agents\n
|
|
352
|
+
let mut out = String::from("Team Agent: create a team, send tasks, read replies and shut down safely.\n\n1. Prepare agents\n Prepare TEAM.md and agents/*.md with explicit provider and bypass decisions; use --from for an external source.\n2. Open a leader and create the team\n With Pi installed and signed in, run team-agent pi.\n Run team-agent start in that leader's tool context. Verified live members stay unchanged; stopped members are started.\n Use team restart for a deliberate restart (session auto); legacy bare restart retains preserve. Do not take over or delete an existing team.\n3. Send tasks and read replies\n team-agent send worker 'Calculate 245 * 37 and reply to the leader.'\n team-agent inbox leader -n 3\n team-agent status\n Sending is not completion; wait for an actual reply.\n4. Diagnose and stop\n team-agent doctor --workspace .\n team-agent stop --workspace . --json\n Verify this team's residual resources are empty, then diagnose the selected workspace.\n\nSelect workspace/team scope with --workspace/--team. Discover model names with models.\nWindows ConPTY requires a Windows host and an installed shim; tmux commands are not a generic Windows path.\n");
|
|
348
353
|
append_help_section(
|
|
349
354
|
&mut out,
|
|
350
355
|
"Getting started",
|
|
351
|
-
&["quick-start", "send", "status", "models", "inbox"],
|
|
356
|
+
&["start", "quick-start", "send", "status", "models", "inbox"],
|
|
352
357
|
);
|
|
353
|
-
append_help_section(&mut out, "Team lifecycle", &["restart", "shutdown"]);
|
|
358
|
+
append_help_section(&mut out, "Team lifecycle", &["stop", "team", "restart", "shutdown"]);
|
|
354
359
|
append_help_section(
|
|
355
360
|
&mut out,
|
|
356
361
|
"Agent lifecycle",
|
|
357
362
|
&[
|
|
363
|
+
"agent",
|
|
358
364
|
"add-agent",
|
|
359
365
|
"start-agent",
|
|
360
366
|
"stop-agent",
|
|
@@ -424,6 +430,9 @@ pub(super) const WORKER_TEMPLATE: &str = "---\nname: worker\nrole: assistant\npr
|
|
|
424
430
|
const HUMAN_NAVIGATION: &str = "Use team-agent --help to list available commands.";
|
|
425
431
|
|
|
426
432
|
pub(super) fn command_help(command: Option<&str>) -> String {
|
|
433
|
+
if let Some(name) = command.filter(|name| matches!(*name, "start" | "stop" | "team" | "agent")) {
|
|
434
|
+
return super::intent::help(name);
|
|
435
|
+
}
|
|
427
436
|
let Some(name) = command else {
|
|
428
437
|
return default_help();
|
|
429
438
|
};
|
|
@@ -440,7 +449,7 @@ pub(super) fn command_help(command: Option<&str>) -> String {
|
|
|
440
449
|
"quick-start" => (
|
|
441
450
|
"TEAMDIR contains TEAM.md and agents/; --workspace selects the runtime project.\n--name sets the team name; --team/--team-id selects the team; --yes retains compatibility; --detail includes full diagnostics.\nProviders must be installed and signed in. Declare provider/model/effort in role files; permission bypass defaults to false.\n--backend tmux is for POSIX; conpty requires a Windows host and an installed ConPTY shim.",
|
|
442
451
|
"team-agent quick-start\nteam-agent quick-start ./roles --workspace ./project\nteam-agent quick-start . --json",
|
|
443
|
-
"
|
|
452
|
+
"Compatibility bridge only. In the authorized Leader context, prefer team-agent start --from TEAMDIR for complete new configuration and team-agent start for later activation. Check models and native sign-in first; inspect status and actual replies after starting."),
|
|
444
453
|
"send" => ("<agent> is an agent name from status; MESSAGE is the task text.\n--mailbox stores a message without injecting it into the current conversation; default delivery targets the agent conversation.",
|
|
445
454
|
"team-agent send worker 'Calculate 245 * 37 and reply to the leader.'\nteam-agent send worker 'Review the changes' --workspace . --team help-demo\nteam-agent send worker 'Read this when next available' --mailbox",
|
|
446
455
|
"Acceptance is not delivery or completion. Wait for an actual reply; use team-agent inbox leader -n 3. Diagnose with status/doctor rather than repeated sends."),
|
|
@@ -449,25 +458,25 @@ pub(super) fn command_help(command: Option<&str>) -> String {
|
|
|
449
458
|
"status" => ("Optional <agent> selects one agent. --summary/--detail retain compatibility and do not add fields.\nNine fields: name/provider/model/effort/runtime_status/activity/health/session_name/tmux_command.\nRuntime, activity and health are distinct. unknown means insufficient evidence; null means unset. No default model is guessed.",
|
|
450
459
|
"team-agent status\nteam-agent status worker\nteam-agent status --workspace . --team help-demo --json", "Read replies with team-agent inbox leader -n 3; diagnose problems with team-agent doctor --workspace ."),
|
|
451
460
|
"models" => ("--provider selects the provider, default pi. QUERY or --search performs keyword search; they are mutually exclusive.\nUse cursor for the contract-native Cursor catalog; cursor_agent retains the legacy entrypoint. Copilot has no model discovery entrypoint here. Results come from the native catalog, not guesses.",
|
|
452
|
-
"team-agent models --provider pi\nteam-agent models --provider pi --search luna\nteam-agent models --provider cursor --json", "Use the exact returned model
|
|
453
|
-
"restart" => ("
|
|
454
|
-
"team-agent restart
|
|
455
|
-
"shutdown" => ("
|
|
456
|
-
"team-agent shutdown --workspace .\nteam-agent shutdown --workspace . --team help-demo --json", "Verify this team's owned residuals are empty
|
|
457
|
-
"add-agent" => ("<agent> names the new
|
|
458
|
-
"team-agent add-agent reviewer --provider pi --model openai-codex/gpt-6-luna --bypass false --prompt 'Review tasks'\nteam-agent add-agent reviewer --role-file ./agents/reviewer.md", "
|
|
459
|
-
"start-agent" => ("
|
|
460
|
-
"team-agent start-agent worker\nteam-agent start-agent worker --model openai-codex/gpt-6-luna", "
|
|
461
|
-
"stop-agent" => ("
|
|
462
|
-
"team-agent stop-agent worker\nteam-agent stop-agent worker --workspace . --team help-demo --json", "Verify stopped
|
|
463
|
-
"reset-agent" => ("
|
|
464
|
-
"team-agent reset-agent worker --discard-session\nteam-agent reset-agent worker --discard-session --workspace . --team help-demo --json", "
|
|
465
|
-
"clone-agent" => ("<agent> is the source; --as names the new agent; --label sets a readable label.\
|
|
466
|
-
"team-agent clone-agent worker --as reviewer\nteam-agent clone-agent worker --as reviewer --label 'Review agent' --workspace .", "Check the new agent with status, then send
|
|
467
|
-
"fork-agent" => ("<agent> is the source; --as names the new agent; --label sets a readable label.\nRequires
|
|
468
|
-
"team-agent fork-agent worker --as experiment\nteam-agent fork-agent worker --as experiment --workspace . --team help-demo --json", "
|
|
469
|
-
"remove-agent" => ("
|
|
470
|
-
"team-agent remove-agent reviewer --confirm\nteam-agent remove-agent reviewer --from-spec --confirm --workspace . --team help-demo --json", "
|
|
461
|
+
"team-agent models --provider pi\nteam-agent models --provider pi --search luna\nteam-agent models --provider cursor --json", "Use the exact returned model in a role file or team-agent start AGENT --model MODEL; stop a running seat before changing its settings."),
|
|
462
|
+
"restart" => ("restart AGENT and team restart default to --session auto; bare/explicit-path restart keeps preserve.\n--session preserve requires continuation evidence; auto permits only the original legal fresh fallback, not guaranteed saved memory.\n--dry-run plans without lifecycle effects. --allow-fresh is the legacy authorized opt-in; --session-converge-deadline is in seconds.",
|
|
463
|
+
"team-agent restart worker --dry-run --json\nteam-agent restart worker --session preserve --workspace . --dry-run --json", "Use team-agent team restart for deliberate team recovery; choose --session preserve when context must be retained. Team restart leaves explicitly stopped/paused seats alone; team-agent start intentionally activates the non-paused roster. Inspect partial/unknown outcomes with status/doctor, never blind reset or retry."),
|
|
464
|
+
"shutdown" => ("Compatibility bridge: stop only the selected workspace/team. Logs are retained by default; --keep-logs remains compatible.\n--json reports scope, degradation and owned residuals. Success does not mean other teams were shut down.",
|
|
465
|
+
"team-agent shutdown --workspace .\nteam-agent shutdown --workspace . --team help-demo --json", "Prefer team-agent stop --workspace . --team KEY --json. Verify this team's owned residuals are empty; use team-agent start for authorized later activation. Do not clean up other teams or shared servers."),
|
|
466
|
+
"add-agent" => ("Compatibility add-only entry. <agent> names the new seat; --provider/--model/--effort select intentional launch settings.\n--bypass requires true|false; --prompt sets responsibilities; --profile selects a local profile.\nProvider and bypass must be explicit in arguments or --role-file. Conflicting settings are refused before writes. --force requires explicit authorization, a positively dead old pane and no live cohort.",
|
|
467
|
+
"team-agent add-agent reviewer --provider pi --model openai-codex/gpt-6-luna --bypass false --prompt 'Review tasks'\nteam-agent add-agent reviewer --role-file ./agents/reviewer.md", "Prefer team-agent start reviewer --role-file FILE for add-or-wake, or team-agent agent add reviewer for strict new-seat behavior. Keep provider/bypass explicit; check status and send a task after actual startup."),
|
|
468
|
+
"start-agent" => ("Compatibility entry starts an existing stopped seat only; a running seat is refused.\n--provider/--model/--effort/--bypass select intentional settings; --prompt replaces permanent instructions and --profile selects local authentication.\nA different provider is refused. Use --allow-fresh only after explicit authorization to lose saved context.",
|
|
469
|
+
"team-agent start-agent worker\nteam-agent start-agent worker --model openai-codex/gpt-6-luna", "Prefer team-agent start worker for daily add-or-wake or verified live no-op. Use team-agent stop worker before applying changed settings; inspect status and send a task afterward. A no-op is not role synchronization or business readiness."),
|
|
470
|
+
"stop-agent" => ("Compatibility single-agent stop; configuration and saved session associations are retained, not removed.",
|
|
471
|
+
"team-agent stop-agent worker\nteam-agent stop-agent worker --workspace . --team help-demo --json", "Prefer team-agent stop worker. Verify the stopped result; intentionally wake with team-agent start worker, then check status before sending new work."),
|
|
472
|
+
"reset-agent" => ("Compatibility entry requires explicit --discard-session. Stops the old process, clears the saved association and attempts fresh startup. Paused agents stay paused; not all native history is deleted.",
|
|
473
|
+
"team-agent reset-agent worker --discard-session\nteam-agent reset-agent worker --discard-session --workspace . --team help-demo --json", "Prefer team-agent agent reset worker --discard-session only after explicit context-loss approval. Never use reset as a resume-refusal fallback; check the actual result/status before sending new work."),
|
|
474
|
+
"clone-agent" => ("Compatibility entry: <agent> is the source; --as names the new agent; --label sets a readable label.\nCopies configuration, not a complete conversation.",
|
|
475
|
+
"team-agent clone-agent worker --as reviewer\nteam-agent clone-agent worker --as reviewer --label 'Review agent' --workspace .", "Prefer team-agent agent clone worker --as reviewer. Check the new agent with status, then send it an independent task; original capability gates still apply."),
|
|
476
|
+
"fork-agent" => ("Compatibility entry: <agent> is the source; --as names the new agent; --label sets a readable label.\nRequires captured context and admitted new-seat fork capability. Unsupported providers refuse without fallback.",
|
|
477
|
+
"team-agent fork-agent worker --as experiment\nteam-agent fork-agent worker --as experiment --workspace . --team help-demo --json", "Prefer team-agent agent fork worker --as experiment where admitted. Inspect status before sending work; finish with team-agent stop experiment or the explicit agent remove operation."),
|
|
478
|
+
"remove-agent" => ("Compatibility entry requires --confirm. Spec-defined agents also require --from-spec; dynamic agents do not.\nDeletes only the managed role copy, not external user files. Stop a running agent first.\n--force stops/removes a running agent only with explicit authorization and the original safety checks.",
|
|
479
|
+
"team-agent remove-agent reviewer --confirm\nteam-agent remove-agent reviewer --from-spec --confirm --workspace . --team help-demo --json", "Prefer team-agent agent remove reviewer --confirm with any required --from-spec. Save replies first and verify the resulting scope/status; do not force-clean other agents."),
|
|
471
480
|
"leaders" => ("Lists live leaders by default; --all includes stale registrations and --stale selects stale entries.\nQUERY/--search filters workspace/team/name. --prune removes verified retired registrations; preview with --dry-run.",
|
|
472
481
|
"team-agent leaders\nteam-agent leaders --all --json\nteam-agent leaders --prune --dry-run", "Select --workspace/--team from the reported scope. Registry cleanup is not process shutdown."),
|
|
473
482
|
"doctor" => ("Read-only diagnosis by default; SPEC is an optional configuration path.\n--comms/--gate comms checks channels, not actual replies; --gate orphans checks residual resources.\nUse --fix/--fix-schema/--cleanup-orphans only within authorized scope. Dangerous repairs require --confirm.",
|
|
@@ -475,19 +484,19 @@ pub(super) fn command_help(command: Option<&str>) -> String {
|
|
|
475
484
|
"approvals" => ("Optional <agent> selects one agent. Observes permission prompts without granting approval.",
|
|
476
485
|
"team-agent approvals\nteam-agent approvals worker --json", "Verify the requested permission, handle it in the actual provider prompt, then check status."),
|
|
477
486
|
"profile" => ("init creates a profile; doctor checks it; show displays settings. NAME is the profile name.\n--auth-mode selects authentication; --proxy-mode direct bypasses proxies and inherit uses the environment proxy.\nProfiles belong to the selected workspace, not automatically to an isolated --team. Do not expose credentials.",
|
|
478
|
-
"team-agent profile init local --auth-mode subscription\nteam-agent profile doctor local\nteam-agent profile show local", "After validation,
|
|
487
|
+
"team-agent profile init local --auth-mode subscription\nteam-agent profile doctor local\nteam-agent profile show local", "After validation, use team-agent start AGENT --profile local; stop a running seat before changing settings. Use native sign-in when needed, never copy external credentials."),
|
|
479
488
|
"install-skill" => ("--source must be a verified skill directory; --target selects codex/claude/copilot/all.\n--dest overrides the destination; --dry-run previews; --uninstall removes the target skill. Installation is not required to create a team.",
|
|
480
489
|
"team-agent install-skill --source \"$SKILL_DIR\" --target codex --dry-run\nteam-agent install-skill --source \"$SKILL_DIR\" --target codex", "Set SKILL_DIR to a verified directory and inspect the preview before installation. Do not overwrite unknown user files."),
|
|
481
490
|
"claim-leader" => ("Use only when doctor recommends leader registration and this terminal is the actual leader.\n--confirm authorizes registration; it does not open a new provider.",
|
|
482
491
|
"team-agent claim-leader --workspace . --json\nteam-agent claim-leader --workspace . --team help-demo --confirm --json", "Verify doctor findings and terminal ownership first. A refusal without confirmation is not successful registration."),
|
|
483
|
-
"takeover" => ("Use only when doctor recommends takeover and the user has authorized it. This changes leader ownership.\nWith a usable team terminal, takeover may execute even without --confirm; it is not a read-only preview. Use restart for normal recovery.",
|
|
492
|
+
"takeover" => ("Use only when doctor recommends takeover and the user has authorized it. This changes leader ownership.\nWith a usable team terminal, takeover may execute even without --confirm; it is not a read-only preview. Use team restart --session preserve for normal context-preserving recovery.",
|
|
484
493
|
"team-agent takeover --workspace . --team help-demo --confirm\nteam-agent takeover --workspace . --team help-demo --confirm --json", "Verify leader/team ownership before confirmation. Check status/doctor afterwards; do not take over someone else's team."),
|
|
485
494
|
"attach-leader" => ("PANE must be a verified leader terminal, not a guessed pane id.\n--provider selects the actual provider; --confirm authorizes attachment to an existing leader. Use only when doctor recommends it.",
|
|
486
495
|
"team-agent attach-leader --pane \"$PANE\" --provider pi --workspace .\nteam-agent attach-leader --pane \"$PANE\" --provider pi --workspace . --team help-demo --confirm --json", "Set PANE to the verified terminal. Confirm attachment with doctor before sending a task."),
|
|
487
496
|
"kiro" => ("Kiro leader launching is not admitted by this candidate. This entry returns the stable capability refusal kiro_leader_not_admitted (exit 1).\nNo native process, leader binding, attach or provider fallback is attempted. --help/-h is read-only and exits 0.",
|
|
488
|
-
"team-agent kiro\nteam-agent kiro --json", "Use provider: kiro in a worker role with team-agent
|
|
497
|
+
"team-agent kiro\nteam-agent kiro --json", "Use provider: kiro in a complete worker role source with team-agent start --from TEAMDIR; worker support does not grant Leader capability."),
|
|
489
498
|
_ => ("Install the provider and complete native sign-in first. The current directory is the workspace.\nUse --attach-existing/--confirm only after verifying leader ownership and receiving authorization; --attach-session selects a verified session.\n--external-leader selects an external leader; --allow-nested-attach requires explicit authorization for nested terminals.\nArguments after -- pass through unchanged. Bare --help/-h displays this help without starting the provider.",
|
|
490
|
-
"", "
|
|
499
|
+
"", "Prepare complete TEAM.md/roles with explicit user choices, then run team-agent start in the opened Leader's tool context, never an ordinary worker conversation."),
|
|
491
500
|
};
|
|
492
501
|
let examples = if matches!(spec.kind, CommandKind::LeaderPassthrough { .. }) {
|
|
493
502
|
let native = if name == "cursor" { "agent" } else { name };
|
|
@@ -513,6 +522,9 @@ pub(super) fn command_help(command: Option<&str>) -> String {
|
|
|
513
522
|
if name == "quick-start" {
|
|
514
523
|
out.push_str(&format!("\n\nMinimal two-file configuration (create manually; not written automatically):\nTEAM.md:\n{TEAM_TEMPLATE}\nagents/worker.md:\n{WORKER_TEMPLATE}"));
|
|
515
524
|
}
|
|
525
|
+
if name == "restart" {
|
|
526
|
+
out.push_str("\nRestart forms: restart AGENT selects one registered agent (default session auto); use agent restart ID to disambiguate a same-named directory. Legacy bare or explicit-path restart retains preserve and original receipts. New team restart defaults to auto. auto never means reset; native capability refusals remain authoritative.");
|
|
527
|
+
}
|
|
516
528
|
if name == "cursor" {
|
|
517
529
|
out.push_str("\nDiscover models with team-agent models --provider cursor_agent.");
|
|
518
530
|
}
|
|
@@ -612,9 +624,14 @@ fn install_skill_args(args: &[String]) -> Result<InstallSkillArgs, CliError> {
|
|
|
612
624
|
|
|
613
625
|
/// 取 `--flag <value>` 的值(用于 install-skill 的 --source/--dest,parse_args 不覆盖的旗标)。
|
|
614
626
|
fn flag_value(args: &[String], flag: &str) -> Option<String> {
|
|
615
|
-
args.iter()
|
|
616
|
-
|
|
617
|
-
|
|
627
|
+
for (index, argument) in args.iter().enumerate() {
|
|
628
|
+
if argument == "--" { break; }
|
|
629
|
+
if argument == flag { return args.get(index + 1).cloned(); }
|
|
630
|
+
if let Some((key, value)) = argument.split_once('=') {
|
|
631
|
+
if key == flag { return Some(value.to_string()); }
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
None
|
|
618
635
|
}
|
|
619
636
|
|
|
620
637
|
/// `team-agent install-skill`(RED-1 单源):repo `skills/team-agent` → `~/.codex|.claude|.copilot`。
|
|
@@ -685,7 +702,21 @@ fn resolve_path(path: &Path) -> PathBuf {
|
|
|
685
702
|
std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
|
|
686
703
|
}
|
|
687
704
|
|
|
705
|
+
fn emit_parser_usage(command: &str, args: &[String], error: &CliError) -> ExitCode {
|
|
706
|
+
if super::arguments::has_flag(command, args, "--json") {
|
|
707
|
+
let mut payload = error.to_payload(Path::new(""), command);
|
|
708
|
+
payload.action = format!("Use team-agent {command} --help to correct the command arguments.");
|
|
709
|
+
if let Ok(value) = serde_json::to_value(payload) {
|
|
710
|
+
println!("{}", python_compact_json(&crate::redaction::redact_external_value(&value)));
|
|
711
|
+
}
|
|
712
|
+
} else {
|
|
713
|
+
eprintln!("error: {}\nUse team-agent {command} --help.", crate::redaction::redact_external_text(&error.to_string()));
|
|
714
|
+
}
|
|
715
|
+
ExitCode::Usage
|
|
716
|
+
}
|
|
717
|
+
|
|
688
718
|
fn emit_cli_error(command: &str, args: &[String], cwd: &Path, error: &CliError) -> ExitCode {
|
|
719
|
+
if matches!(error, CliError::Usage(_)) { return emit_parser_usage(command, args, error); }
|
|
689
720
|
let parsed = parse_args(args);
|
|
690
721
|
let workspace = workspace(&parsed, cwd);
|
|
691
722
|
let log_path = cli_error_log_path(&workspace);
|
|
@@ -699,7 +730,7 @@ fn emit_cli_error(command: &str, args: &[String], cwd: &Path, error: &CliError)
|
|
|
699
730
|
let mut payload = payload_error.to_payload(&log_path, command);
|
|
700
731
|
payload.error = safe_error;
|
|
701
732
|
if let CliError::AddressRefusal { error, action, .. } = payload_error {
|
|
702
|
-
if !
|
|
733
|
+
if !super::arguments::has_flag(command, args, "--json") {
|
|
703
734
|
payload.error = crate::redaction::redact_external_text(error);
|
|
704
735
|
payload.action = action.clone();
|
|
705
736
|
}
|
|
@@ -742,7 +773,7 @@ fn emit_cli_error(command: &str, args: &[String], cwd: &Path, error: &CliError)
|
|
|
742
773
|
.map(|value| crate::redaction::redact_external_text(&value))
|
|
743
774
|
.collect()
|
|
744
775
|
});
|
|
745
|
-
if
|
|
776
|
+
if super::arguments::has_flag(command, args, "--json") {
|
|
746
777
|
if let Ok(value) = serde_json::to_value(payload) {
|
|
747
778
|
println!("{}", python_compact_json(&value));
|
|
748
779
|
}
|
|
@@ -820,7 +851,7 @@ fn python_compact_json(value: &Value) -> String {
|
|
|
820
851
|
}
|
|
821
852
|
|
|
822
853
|
fn has_arg(args: &[String], needle: &str) -> bool {
|
|
823
|
-
args.iter().any(|arg| arg == needle)
|
|
854
|
+
args.iter().take_while(|arg| arg.as_str() != "--").any(|arg| arg == needle)
|
|
824
855
|
}
|
|
825
856
|
|
|
826
857
|
#[derive(Debug, Default)]
|
|
@@ -894,6 +925,19 @@ struct ParsedArgs {
|
|
|
894
925
|
}
|
|
895
926
|
|
|
896
927
|
fn parse_args(args: &[String]) -> ParsedArgs {
|
|
928
|
+
let mut expanded = Vec::new();
|
|
929
|
+
let mut literal = false;
|
|
930
|
+
for argument in args {
|
|
931
|
+
if argument == "--" { literal = true; }
|
|
932
|
+
if !literal && argument.starts_with("--") {
|
|
933
|
+
if let Some((flag, value)) = argument.split_once('=') {
|
|
934
|
+
expanded.extend([flag.to_string(), value.to_string()]);
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
expanded.push(argument.clone());
|
|
939
|
+
}
|
|
940
|
+
let args = expanded.as_slice();
|
|
897
941
|
let mut parsed = ParsedArgs::default();
|
|
898
942
|
let mut i = 0usize;
|
|
899
943
|
while i < args.len() {
|
|
@@ -901,6 +945,10 @@ fn parse_args(args: &[String]) -> ParsedArgs {
|
|
|
901
945
|
break;
|
|
902
946
|
};
|
|
903
947
|
match arg.as_str() {
|
|
948
|
+
"--" => {
|
|
949
|
+
parsed.positionals.extend_from_slice(&args[i + 1..]);
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
904
952
|
"--workspace" => {
|
|
905
953
|
parsed.workspace = next_arg(args, &mut i).map(PathBuf::from);
|
|
906
954
|
}
|
|
@@ -1022,7 +1070,7 @@ fn next_arg(args: &[String], index: &mut usize) -> Option<String> {
|
|
|
1022
1070
|
args.get(*index).cloned()
|
|
1023
1071
|
}
|
|
1024
1072
|
|
|
1025
|
-
fn parse_seconds_ms(raw: &str) -> Option<u64> {
|
|
1073
|
+
pub(super) fn parse_seconds_ms(raw: &str) -> Option<u64> {
|
|
1026
1074
|
let seconds = raw.parse::<f64>().ok()?;
|
|
1027
1075
|
if seconds.is_finite() && seconds >= 0.0 {
|
|
1028
1076
|
Some((seconds * 1000.0).round() as u64)
|
|
@@ -1031,10 +1079,54 @@ fn parse_seconds_ms(raw: &str) -> Option<u64> {
|
|
|
1031
1079
|
}
|
|
1032
1080
|
}
|
|
1033
1081
|
|
|
1034
|
-
fn parse_cli_provider(raw:
|
|
1035
|
-
let raw = raw.unwrap_or("codex");
|
|
1082
|
+
fn parse_cli_provider(raw: &str) -> Result<crate::provider::Provider, CliError> {
|
|
1036
1083
|
serde_json::from_value::<crate::provider::Provider>(serde_json::json!(raw))
|
|
1037
|
-
.map_err(|_| CliError::
|
|
1084
|
+
.map_err(|_| CliError::Usage(format!("unknown provider: {raw}")))
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
pub(super) fn explicit_path(value: &str) -> bool {
|
|
1088
|
+
value == "." || value == ".." || value.contains('/') || value.contains('\\')
|
|
1089
|
+
|| Path::new(value).is_absolute()
|
|
1090
|
+
|| (value.as_bytes().get(1) == Some(&b':') && value.as_bytes().first().is_some_and(u8::is_ascii_alphabetic))
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
fn checked_workspace(path: &Path, cwd: &Path) -> Result<PathBuf, CliError> {
|
|
1094
|
+
let path = resolve_cli_path(cwd, path);
|
|
1095
|
+
let canonical = std::fs::canonicalize(&path).map_err(|_| CliError::Usage(format!(
|
|
1096
|
+
"workspace does not exist or is unavailable: {}", path.display()
|
|
1097
|
+
)))?;
|
|
1098
|
+
if !canonical.is_dir() {
|
|
1099
|
+
return Err(CliError::Usage(format!("workspace is not a directory: {}", path.display())));
|
|
1100
|
+
}
|
|
1101
|
+
Ok(canonical)
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
fn resolve_command_scope(command: &str, mut args: Vec<String>, cwd: &Path) -> Result<Vec<String>, CliError> {
|
|
1105
|
+
let mut selected = None;
|
|
1106
|
+
for argument in &mut args {
|
|
1107
|
+
if argument == "--" { break; }
|
|
1108
|
+
if let Some(path) = argument.strip_prefix("--workspace=") {
|
|
1109
|
+
let canonical = checked_workspace(Path::new(path), cwd)?;
|
|
1110
|
+
*argument = format!("--workspace={}", canonical.display());
|
|
1111
|
+
selected = Some(canonical);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
if command == "restart" {
|
|
1115
|
+
let index = args.iter().position(|arg| arg == "--")
|
|
1116
|
+
.and_then(|index| (index + 1 < args.len()).then_some(index + 1))
|
|
1117
|
+
.or_else(|| args.iter().position(|arg| !arg.starts_with('-')));
|
|
1118
|
+
if let Some(index) = index {
|
|
1119
|
+
if !explicit_path(&args[index]) {
|
|
1120
|
+
return Err(CliError::Usage("bare restart targets are ambiguous; use restart --workspace PATH or restart ./PATH for a team".to_string()));
|
|
1121
|
+
}
|
|
1122
|
+
let canonical = checked_workspace(Path::new(&args[index]), cwd)?;
|
|
1123
|
+
if selected.as_ref().is_some_and(|selected| selected != &canonical) {
|
|
1124
|
+
return Err(CliError::Usage("restart WORKSPACE conflicts with --workspace".to_string()));
|
|
1125
|
+
}
|
|
1126
|
+
args[index] = canonical.to_string_lossy().into_owned();
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
Ok(args)
|
|
1038
1130
|
}
|
|
1039
1131
|
|
|
1040
1132
|
fn workspace(parsed: &ParsedArgs, cwd: &Path) -> PathBuf {
|
|
@@ -1279,6 +1371,7 @@ fn warn_send_legacy_delivery_flags(args: &[String]) {
|
|
|
1279
1371
|
for flag in FLAGS {
|
|
1280
1372
|
if args
|
|
1281
1373
|
.iter()
|
|
1374
|
+
.take_while(|arg| arg.as_str() != "--")
|
|
1282
1375
|
.any(|arg| arg == flag || arg.starts_with(&format!("{flag}=")))
|
|
1283
1376
|
{
|
|
1284
1377
|
eprintln!(
|
|
@@ -1289,52 +1382,7 @@ fn warn_send_legacy_delivery_flags(args: &[String]) {
|
|
|
1289
1382
|
}
|
|
1290
1383
|
|
|
1291
1384
|
fn validate_send_flags(args: &[String]) -> Result<(), CliError> {
|
|
1292
|
-
|
|
1293
|
-
"--workspace",
|
|
1294
|
-
"--team",
|
|
1295
|
-
"--targets",
|
|
1296
|
-
"--target",
|
|
1297
|
-
"--to",
|
|
1298
|
-
"--to-name",
|
|
1299
|
-
"--to-leader",
|
|
1300
|
-
"--pane",
|
|
1301
|
-
"--task",
|
|
1302
|
-
"--watch-result",
|
|
1303
|
-
"--requires-ack",
|
|
1304
|
-
"--no-ack",
|
|
1305
|
-
"--no-wait",
|
|
1306
|
-
"--timeout",
|
|
1307
|
-
"--confirm-human",
|
|
1308
|
-
"--message-id",
|
|
1309
|
-
"--mailbox",
|
|
1310
|
-
"--presentation-sink",
|
|
1311
|
-
"--message-class",
|
|
1312
|
-
"--case-id",
|
|
1313
|
-
"--json",
|
|
1314
|
-
"-h",
|
|
1315
|
-
"--help",
|
|
1316
|
-
];
|
|
1317
|
-
const ALLOWED_PREFIXES: &[&str] = &[
|
|
1318
|
-
"--team=",
|
|
1319
|
-
"--pane=",
|
|
1320
|
-
"--to-name=",
|
|
1321
|
-
"--to-leader=",
|
|
1322
|
-
"--presentation-sink=",
|
|
1323
|
-
"--message-class=",
|
|
1324
|
-
"--case-id=",
|
|
1325
|
-
];
|
|
1326
|
-
if let Some(flag) = args.iter().find(|arg| {
|
|
1327
|
-
arg.starts_with('-')
|
|
1328
|
-
&& !ALLOWED.contains(&arg.as_str())
|
|
1329
|
-
&& !ALLOWED_PREFIXES
|
|
1330
|
-
.iter()
|
|
1331
|
-
.any(|prefix| arg.starts_with(prefix))
|
|
1332
|
-
}) {
|
|
1333
|
-
return Err(CliError::Usage(format!(
|
|
1334
|
-
"unrecognized argument for `send`: {flag}"
|
|
1335
|
-
)));
|
|
1336
|
-
}
|
|
1337
|
-
Ok(())
|
|
1385
|
+
super::arguments::parse("send", args).map(|_| ())
|
|
1338
1386
|
}
|
|
1339
1387
|
|
|
1340
1388
|
/// Stage 4 of identity-boundary unified plan (architect direction
|
|
@@ -1445,7 +1493,9 @@ fn attach_leader_args(args: &[String], cwd: &Path) -> Result<AttachLeaderArgs, C
|
|
|
1445
1493
|
.pane
|
|
1446
1494
|
.filter(|pane| !pane.is_empty())
|
|
1447
1495
|
.map(crate::transport::PaneId::new),
|
|
1448
|
-
provider: parse_cli_provider(parsed.provider.as_deref()
|
|
1496
|
+
provider: parse_cli_provider(parsed.provider.as_deref()
|
|
1497
|
+
.filter(|provider| !provider.trim().is_empty())
|
|
1498
|
+
.ok_or_else(|| CliError::Usage("attach-leader requires --provider <name>".to_string()))?)?,
|
|
1449
1499
|
confirm: parsed.confirm,
|
|
1450
1500
|
json: parsed.json,
|
|
1451
1501
|
})
|
|
@@ -1485,11 +1535,12 @@ fn shutdown_args(args: &[String], cwd: &Path) -> Result<ShutdownArgs, CliError>
|
|
|
1485
1535
|
|
|
1486
1536
|
fn restart_args(args: &[String], cwd: &Path) -> Result<RestartArgs, CliError> {
|
|
1487
1537
|
let parsed = parse_args(args);
|
|
1488
|
-
let workspace = parsed
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1538
|
+
let workspace = if let Some(path) = parsed.positionals.first() {
|
|
1539
|
+
if !explicit_path(path) {
|
|
1540
|
+
return Err(CliError::Usage("bare restart targets are ambiguous; use restart --workspace PATH or restart ./PATH for a team".to_string()));
|
|
1541
|
+
}
|
|
1542
|
+
resolve_cli_path(cwd, Path::new(path))
|
|
1543
|
+
} else { workspace(&parsed, cwd) };
|
|
1493
1544
|
refuse_if_multi_alive_team_missing_scope("restart", &workspace, parsed.team.as_deref())?;
|
|
1494
1545
|
Ok(RestartArgs {
|
|
1495
1546
|
workspace,
|
|
@@ -1502,6 +1553,41 @@ fn restart_args(args: &[String], cwd: &Path) -> Result<RestartArgs, CliError> {
|
|
|
1502
1553
|
}
|
|
1503
1554
|
|
|
1504
1555
|
// Keep strict role CLI parsing separate from the legacy shared parser.
|
|
1556
|
+
pub(super) fn intent_source_workspace(source: &Path, cwd: &Path) -> Result<PathBuf, CliError> {
|
|
1557
|
+
quick_start_args(&[source.to_string_lossy().into_owned()], cwd).map(|args| args.workspace)
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
pub(super) fn intent_role_patch(args: &[String]) -> Result<crate::lifecycle::role_config::RoleConfigPatch, CliError> {
|
|
1561
|
+
role_agent_args(args, false).map(|(_, patch)| patch)
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
/// Explicit object aliases reuse the same syntax checks, typed builders and handlers.
|
|
1565
|
+
pub(super) fn intent_alias(command: &str, args: &[String], cwd: &Path) -> Result<CmdResult, CliError> {
|
|
1566
|
+
let mut checked = prepare_control_args(command, args, cwd)?;
|
|
1567
|
+
if checked.help { return Ok(CmdResult::human(command_help(Some(command)))); }
|
|
1568
|
+
for token in &mut checked.tokens {
|
|
1569
|
+
if let Some(path) = token.strip_prefix("--role-file=") {
|
|
1570
|
+
let path = Path::new(path);
|
|
1571
|
+
let path = if path.is_absolute() { path.to_path_buf() } else { cwd.join(path) };
|
|
1572
|
+
let path = path.canonicalize().map_err(|e| CliError::Usage(format!("invalid explicit role file: {e}")))?;
|
|
1573
|
+
*token = format!("--role-file={}", path.display());
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
let args = checked.tokens.as_slice();
|
|
1577
|
+
match command {
|
|
1578
|
+
"status" => {
|
|
1579
|
+
let parsed = status_args(args, cwd);
|
|
1580
|
+
cmd_status_for_team(&parsed, parsed.team.as_deref())
|
|
1581
|
+
}
|
|
1582
|
+
"add-agent" => cmd_add_agent(&add_agent_args(args, cwd)?),
|
|
1583
|
+
"remove-agent" => cmd_remove_agent(&remove_agent_args(args, cwd)?),
|
|
1584
|
+
"reset-agent" => cmd_reset_agent(&reset_agent_args(args, cwd)?),
|
|
1585
|
+
"clone-agent" => cmd_clone_agent(&clone_agent_args(args, cwd)?),
|
|
1586
|
+
"fork-agent" => cmd_fork_agent(&fork_agent_args(args, cwd)?),
|
|
1587
|
+
_ => Err(CliError::Usage("unsupported lifecycle alias".into())),
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1505
1591
|
fn role_agent_args(
|
|
1506
1592
|
args: &[String],
|
|
1507
1593
|
add: bool,
|
|
@@ -1551,7 +1637,7 @@ fn role_agent_args(
|
|
|
1551
1637
|
} else {
|
|
1552
1638
|
index += 1;
|
|
1553
1639
|
args.get(index)
|
|
1554
|
-
.filter(|value| !value.starts_with("--"))
|
|
1640
|
+
.filter(|value| flag == "--prompt" || !value.starts_with("--"))
|
|
1555
1641
|
.ok_or_else(|| CliError::Usage(format!("missing value for {flag}")))?
|
|
1556
1642
|
.clone()
|
|
1557
1643
|
};
|
|
@@ -1671,6 +1757,9 @@ mod role_cli_tests {
|
|
|
1671
1757
|
|
|
1672
1758
|
fn start_agent_args(args: &[String], cwd: &Path) -> Result<StartAgentArgs, CliError> {
|
|
1673
1759
|
let (parsed, role_config) = role_agent_args(args, false)?;
|
|
1760
|
+
if parsed.force {
|
|
1761
|
+
return Err(CliError::Usage("start-agent does not support --force; use stop-agent AGENT followed by start-agent AGENT for a safe restart".to_string()));
|
|
1762
|
+
}
|
|
1674
1763
|
Ok(StartAgentArgs {
|
|
1675
1764
|
role_config,
|
|
1676
1765
|
agent: required_pos(&parsed, 0, "agent")?,
|
|
@@ -2100,8 +2189,8 @@ mod tests {
|
|
|
2100
2189
|
.collect();
|
|
2101
2190
|
assert_eq!(
|
|
2102
2191
|
expected.len(),
|
|
2103
|
-
|
|
2104
|
-
"the public catalog
|
|
2192
|
+
35,
|
|
2193
|
+
"the public catalog adds the four canonical lifecycle surfaces"
|
|
2105
2194
|
);
|
|
2106
2195
|
for spec_name in &expected {
|
|
2107
2196
|
assert!(
|
|
@@ -2554,11 +2643,15 @@ mod tests {
|
|
|
2554
2643
|
#[test]
|
|
2555
2644
|
fn send_pane_still_rejects_to_and_empty_message() {
|
|
2556
2645
|
let cwd = tmp_workspace();
|
|
2557
|
-
let
|
|
2646
|
+
let err = send_args(
|
|
2558
2647
|
&cli_argv(&["--pane", "%1596", "--to", "worker", "hello"]),
|
|
2559
2648
|
&cwd,
|
|
2560
|
-
)
|
|
2561
|
-
.
|
|
2649
|
+
).unwrap_err();
|
|
2650
|
+
assert!(matches!(err, CliError::Usage(ref message) if message.contains("one recipient addressing mode")));
|
|
2651
|
+
// The public parser now refuses first; the typed handler must still
|
|
2652
|
+
// reject the same conflict if a non-CLI caller constructs it directly.
|
|
2653
|
+
let mut args = send_args(&cli_argv(&["--pane", "%1596", "hello"]), &cwd).unwrap();
|
|
2654
|
+
args.targets = Some("worker".to_string());
|
|
2562
2655
|
let err = cmd_send(&args).unwrap_err();
|
|
2563
2656
|
assert!(
|
|
2564
2657
|
matches!(err, CliError::Usage(ref message) if message.contains("--pane") && message.contains("--to") && message.contains("cannot") && message.contains("combined")),
|
|
@@ -2630,7 +2723,9 @@ mod tests {
|
|
|
2630
2723
|
// 'statu' typo → status; 'add-agen' → add-agent.
|
|
2631
2724
|
assert_eq!(nearest_subcommand("statu"), Some("status"));
|
|
2632
2725
|
assert_eq!(nearest_subcommand("add-agen"), Some("add-agent"));
|
|
2633
|
-
|
|
2726
|
+
// Presentation migration: the new canonical short verb is now a
|
|
2727
|
+
// prefix candidate in the unchanged shared similarity ranking.
|
|
2728
|
+
assert_eq!(nearest_subcommand("start-agnet"), Some("start"));
|
|
2634
2729
|
}
|
|
2635
2730
|
|
|
2636
2731
|
#[test]
|
|
@@ -2766,3 +2861,7 @@ mod tests {
|
|
|
2766
2861
|
// without it.
|
|
2767
2862
|
}
|
|
2768
2863
|
}
|
|
2864
|
+
|
|
2865
|
+
#[cfg(test)]
|
|
2866
|
+
#[path = "tests/parser_safety.rs"]
|
|
2867
|
+
mod parser_safety_tests;
|