@team-agent/installer 0.5.113 → 0.5.114
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 +252 -84
- package/Cargo.toml +3 -1
- package/crates/team-agent/Cargo.toml +3 -0
- package/crates/team-agent/src/cli/adapters.rs +28 -15
- package/crates/team-agent/src/cli/diagnose.rs +36 -13
- package/crates/team-agent/src/cli/emit.rs +55 -20
- package/crates/team-agent/src/cli/leader.rs +16 -0
- package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
- package/crates/team-agent/src/cli/mod.rs +152 -8
- package/crates/team-agent/src/cli/models.rs +68 -18
- package/crates/team-agent/src/cli/spec.rs +7 -1
- package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
- package/crates/team-agent/src/cli/tests/base.rs +88 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +2 -0
- package/crates/team-agent/src/compiler.rs +66 -18
- package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
- package/crates/team-agent/src/contract_runtime/config.rs +309 -0
- package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
- package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
- package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
- package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
- package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
- package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
- package/crates/team-agent/src/contract_runtime/registry.rs +30 -0
- package/crates/team-agent/src/coordinator/tick.rs +26 -8
- package/crates/team-agent/src/leader/mod.rs +1 -0
- package/crates/team-agent/src/leader/prompt.rs +906 -0
- package/crates/team-agent/src/leader/start.rs +260 -16
- package/crates/team-agent/src/leader/tests/mod.rs +2 -0
- package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +472 -388
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
- package/crates/team-agent/src/lifecycle/launch.rs +39 -28
- package/crates/team-agent/src/lifecycle/types.rs +4 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
- package/crates/team-agent/src/main.rs +6 -0
- package/crates/team-agent/src/messaging/delivery.rs +50 -1
- package/crates/team-agent/src/messaging/send.rs +10 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
- package/crates/team-agent/src/model/spec.rs +24 -12
- package/crates/team-agent/src/state/persist.rs +73 -0
- package/package.json +4 -4
|
@@ -28,6 +28,9 @@ chrono.workspace = true
|
|
|
28
28
|
libc.workspace = true
|
|
29
29
|
rusqlite.workspace = true # step 3 db(bundled SQLite,静态链接)
|
|
30
30
|
|
|
31
|
+
[target.'cfg(unix)'.dependencies]
|
|
32
|
+
team-agent-contract = { path = "../team-agent-contract" }
|
|
33
|
+
|
|
31
34
|
# 0.5.x Windows portability Batch 2/3: uses the same `windows = "0.61"`
|
|
32
35
|
# crate as `win-conpty-phase0` so the workspace shares a single
|
|
33
36
|
# Windows-API crate + already-locked Cargo.lock version.
|
|
@@ -7,6 +7,9 @@ use super::*;
|
|
|
7
7
|
|
|
8
8
|
/// `cmd_quick_start`(`commands.py:18`)。`--json` 或 `!ok` → 整 dict;否则 `result["summary"]`。
|
|
9
9
|
pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
|
|
10
|
+
#[cfg(unix)]
|
|
11
|
+
crate::contract_runtime::config::read_team(args)
|
|
12
|
+
.map_err(|error| CliError::Runtime(error.to_string()))?;
|
|
10
13
|
let mut value = lifecycle_port::quick_start(
|
|
11
14
|
&args.workspace,
|
|
12
15
|
&args.agents_dir,
|
|
@@ -158,7 +161,10 @@ fn quick_start_config_guidance(value: &mut Value, args: &QuickStartArgs) -> Opti
|
|
|
158
161
|
/// E13:quick-start "team 起了" 人类输出 = summary + attach 块。所有成功出口共用(别每分支手拷)。
|
|
159
162
|
/// attach_commands 缺/空 → 只 summary(向后兼容)。
|
|
160
163
|
fn quickstart_human(value: &Value) -> String {
|
|
161
|
-
let summary = value
|
|
164
|
+
let summary = value
|
|
165
|
+
.get("summary")
|
|
166
|
+
.and_then(Value::as_str)
|
|
167
|
+
.unwrap_or("Team started.");
|
|
162
168
|
let attach: Vec<&str> = value
|
|
163
169
|
.get("attach_commands")
|
|
164
170
|
.and_then(Value::as_array)
|
|
@@ -1088,19 +1094,13 @@ mod tests {
|
|
|
1088
1094
|
let value = json!({"summary": "quick-start complete"});
|
|
1089
1095
|
assert_eq!(
|
|
1090
1096
|
quickstart_human(&value),
|
|
1091
|
-
format!(
|
|
1092
|
-
"quick-start complete\n{}",
|
|
1093
|
-
crate::cli::QUICK_START_REMINDER
|
|
1094
|
-
)
|
|
1097
|
+
format!("quick-start complete\n{}", crate::cli::QUICK_START_REMINDER)
|
|
1095
1098
|
);
|
|
1096
1099
|
// 空数组也只显示人类摘要与下一步,不凭空编造连接方式。
|
|
1097
1100
|
let value2 = json!({"summary": "s", "attach_commands": []});
|
|
1098
1101
|
assert_eq!(
|
|
1099
1102
|
quickstart_human(&value2),
|
|
1100
|
-
format!(
|
|
1101
|
-
"s\n{}",
|
|
1102
|
-
crate::cli::QUICK_START_REMINDER
|
|
1103
|
-
)
|
|
1103
|
+
format!("s\n{}", crate::cli::QUICK_START_REMINDER)
|
|
1104
1104
|
);
|
|
1105
1105
|
}
|
|
1106
1106
|
|
|
@@ -1219,7 +1219,11 @@ mod tests {
|
|
|
1219
1219
|
|
|
1220
1220
|
#[test]
|
|
1221
1221
|
fn quick_start_human_preserves_typed_refusals_without_error_field() {
|
|
1222
|
-
for status in [
|
|
1222
|
+
for status in [
|
|
1223
|
+
"existing_runtime",
|
|
1224
|
+
"preflight_blocked",
|
|
1225
|
+
"leader_binding_refused",
|
|
1226
|
+
] {
|
|
1223
1227
|
let value = json!({
|
|
1224
1228
|
"ok": false,
|
|
1225
1229
|
"status": status,
|
|
@@ -1232,10 +1236,16 @@ mod tests {
|
|
|
1232
1236
|
});
|
|
1233
1237
|
let result = finish_quick_start(value.clone(), &quick_start_args(false)).unwrap();
|
|
1234
1238
|
assert_eq!(result.exit, ExitCode::Error);
|
|
1235
|
-
let CmdOutput::Human(text) = result.output else {
|
|
1236
|
-
|
|
1239
|
+
let CmdOutput::Human(text) = result.output else {
|
|
1240
|
+
panic!("expected human report")
|
|
1241
|
+
};
|
|
1242
|
+
let rendered: Value =
|
|
1243
|
+
serde_json::from_str(text.strip_prefix("quick-start report:\n").unwrap()).unwrap();
|
|
1237
1244
|
assert_eq!(rendered, value, "human projection lost fields for {status}");
|
|
1238
|
-
assert!(
|
|
1245
|
+
assert!(
|
|
1246
|
+
!text.contains("team-agent doctor"),
|
|
1247
|
+
"must not replace actual actions"
|
|
1248
|
+
);
|
|
1239
1249
|
let json_result = finish_quick_start(value.clone(), &quick_start_args(true)).unwrap();
|
|
1240
1250
|
assert_eq!(json_result.exit, ExitCode::Error);
|
|
1241
1251
|
assert_eq!(json_result.output, CmdOutput::Json(value));
|
|
@@ -1252,8 +1262,11 @@ mod tests {
|
|
|
1252
1262
|
});
|
|
1253
1263
|
let result = finish_quick_start(value.clone(), &quick_start_args(false)).unwrap();
|
|
1254
1264
|
assert_eq!(result.exit, ExitCode::Error);
|
|
1255
|
-
let CmdOutput::Human(text) = result.output else {
|
|
1256
|
-
|
|
1265
|
+
let CmdOutput::Human(text) = result.output else {
|
|
1266
|
+
panic!("expected human report")
|
|
1267
|
+
};
|
|
1268
|
+
let rendered: Value =
|
|
1269
|
+
serde_json::from_str(text.strip_prefix("quick-start report:\n").unwrap()).unwrap();
|
|
1257
1270
|
assert_eq!(rendered, value);
|
|
1258
1271
|
}
|
|
1259
1272
|
|
|
@@ -342,7 +342,13 @@ pub(crate) fn diagnose_runtime_for_workspace(
|
|
|
342
342
|
&mut repairs,
|
|
343
343
|
);
|
|
344
344
|
append_legacy_snapshot_issue(workspace, state, &mut issues);
|
|
345
|
-
append_coordinator_health_issue(
|
|
345
|
+
append_coordinator_health_issue(
|
|
346
|
+
workspace,
|
|
347
|
+
state,
|
|
348
|
+
coordinator_health,
|
|
349
|
+
&mut issues,
|
|
350
|
+
&mut repairs,
|
|
351
|
+
);
|
|
346
352
|
append_runtime_bindings_stale_after_boot_issue(workspace, state, &mut issues, &mut repairs);
|
|
347
353
|
(issues, repairs)
|
|
348
354
|
}
|
|
@@ -373,7 +379,9 @@ pub(crate) fn workspace_has_existing_team_runtime(
|
|
|
373
379
|
let terminal = team_state
|
|
374
380
|
.get("status")
|
|
375
381
|
.and_then(Value::as_str)
|
|
376
|
-
.is_some_and(|status|
|
|
382
|
+
.is_some_and(|status| {
|
|
383
|
+
matches!(status, "stopped" | "shutdown" | "archived" | "terminal")
|
|
384
|
+
});
|
|
377
385
|
if terminal {
|
|
378
386
|
return false;
|
|
379
387
|
}
|
|
@@ -695,14 +703,13 @@ pub(crate) fn append_registry_channel_unbound_to_report(
|
|
|
695
703
|
repairs.push(recovery_hint(&team_key, issue_id, repair));
|
|
696
704
|
object.insert("issues".to_string(), Value::Array(issues));
|
|
697
705
|
object.insert("suggested_repairs".to_string(), Value::Array(repairs));
|
|
698
|
-
let is_healthy_unattached_host =
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
.is_some_and(|checks| !checks.is_empty());
|
|
706
|
+
let is_healthy_unattached_host = class == crate::lifecycle::launch::LeaderBindingClass::Unbound
|
|
707
|
+
&& object.get("ok").and_then(Value::as_bool) == Some(true)
|
|
708
|
+
&& object
|
|
709
|
+
.get("profile_smoke")
|
|
710
|
+
.and_then(|profile| profile.get("checks"))
|
|
711
|
+
.and_then(Value::as_array)
|
|
712
|
+
.is_some_and(|checks| !checks.is_empty());
|
|
706
713
|
if !is_healthy_unattached_host {
|
|
707
714
|
object.insert("ok".to_string(), Value::Bool(false));
|
|
708
715
|
}
|
|
@@ -1054,8 +1061,7 @@ mod tests {
|
|
|
1054
1061
|
pane_pid: None,
|
|
1055
1062
|
leader_env: Default::default(),
|
|
1056
1063
|
}]);
|
|
1057
|
-
let Some((_, repair)) =
|
|
1058
|
-
live_leader_workspace_mismatch(&workspace, &state, &transport)
|
|
1064
|
+
let Some((_, repair)) = live_leader_workspace_mismatch(&workspace, &state, &transport)
|
|
1059
1065
|
else {
|
|
1060
1066
|
panic!("workspace mismatch repair required");
|
|
1061
1067
|
};
|
|
@@ -1109,7 +1115,8 @@ mod tests {
|
|
|
1109
1115
|
.as_nanos()
|
|
1110
1116
|
));
|
|
1111
1117
|
std::fs::create_dir_all(&workspace).unwrap();
|
|
1112
|
-
let state =
|
|
1118
|
+
let state =
|
|
1119
|
+
json!({"agents": {}, "tasks": [], "session_name": null, "active_team_key": null});
|
|
1113
1120
|
let mut issues = json!([]);
|
|
1114
1121
|
let mut repairs = json!([]);
|
|
1115
1122
|
append_registry_channel_unbound_issue(
|
|
@@ -1574,6 +1581,22 @@ pub(crate) fn provider_doctor_checks() -> Value {
|
|
|
1574
1581
|
}),
|
|
1575
1582
|
);
|
|
1576
1583
|
}
|
|
1584
|
+
#[cfg(unix)]
|
|
1585
|
+
for descriptor in crate::contract_runtime::registry::descriptors() {
|
|
1586
|
+
let identity = &descriptor.identity;
|
|
1587
|
+
// Inventory only: finding the launcher is not an engine, auth or readiness probe.
|
|
1588
|
+
providers.insert(
|
|
1589
|
+
identity.id.to_string(),
|
|
1590
|
+
json!({
|
|
1591
|
+
"auth": crate::provider::AuthHintStatus::Unknown,
|
|
1592
|
+
"command": identity.binary,
|
|
1593
|
+
"installed": crate::contract_runtime::discovery::executable(identity.binary).is_ok(),
|
|
1594
|
+
"version": "unknown",
|
|
1595
|
+
"probe_status": "not_run",
|
|
1596
|
+
"provider_probe_status": "not_run",
|
|
1597
|
+
}),
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1577
1600
|
Value::Object(providers)
|
|
1578
1601
|
}
|
|
1579
1602
|
|
|
@@ -68,6 +68,9 @@ pub fn run(argv: &[String], cwd: &Path) -> ExitCode {
|
|
|
68
68
|
if command == "route" {
|
|
69
69
|
return super::route::run(&argv[1..]);
|
|
70
70
|
}
|
|
71
|
+
if command == "leader-prompt" {
|
|
72
|
+
return super::leader_prompt::run(&argv[1..]);
|
|
73
|
+
}
|
|
71
74
|
if is_leader_passthrough_command(command) {
|
|
72
75
|
return match cmd_leader_passthrough(command, &argv[1..], cwd) {
|
|
73
76
|
Ok(result) => emit_result(result),
|
|
@@ -212,6 +215,9 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
|
|
|
212
215
|
if command == "route" {
|
|
213
216
|
return Ok(super::route::run(args));
|
|
214
217
|
}
|
|
218
|
+
if command == "leader-prompt" {
|
|
219
|
+
return Ok(super::leader_prompt::run(args));
|
|
220
|
+
}
|
|
215
221
|
// Keep the removed flag harmless for older scripts and persisted command
|
|
216
222
|
// lines; the default tmux backend is already the only runtime path.
|
|
217
223
|
let filtered_args: Vec<String> = args
|
|
@@ -290,6 +296,7 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
|
|
|
290
296
|
// delivery path — no separate route authority.
|
|
291
297
|
"leaders" => cmd_leaders(&leaders_args(args, cwd)?).map(emit_result),
|
|
292
298
|
"models" => cmd_models(&models_args(args)?).map(emit_result),
|
|
299
|
+
"kiro" => Ok(emit_result(cmd_kiro_leader(args))),
|
|
293
300
|
|
|
294
301
|
"install-skill" => cmd_install_skill(&install_skill_args(args)?).map(emit_result),
|
|
295
302
|
"profile" => cmd_profile(&profile_args(args, cwd)?).map(emit_result),
|
|
@@ -300,10 +307,7 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
|
|
|
300
307
|
|
|
301
308
|
// Script compatibility is deliberately outside the human catalog and suggestions.
|
|
302
309
|
fn is_machine_command(command: &str) -> bool {
|
|
303
|
-
matches!(
|
|
304
|
-
command,
|
|
305
|
-
"wait" | "coordinator" | "attach-app-server-leader"
|
|
306
|
-
)
|
|
310
|
+
matches!(command, "wait" | "coordinator" | "attach-app-server-leader")
|
|
307
311
|
}
|
|
308
312
|
|
|
309
313
|
fn dispatch_machine(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliError> {
|
|
@@ -365,7 +369,11 @@ pub(crate) fn default_help() -> String {
|
|
|
365
369
|
"Observation and collaboration",
|
|
366
370
|
&["leaders", "doctor", "approvals"],
|
|
367
371
|
);
|
|
368
|
-
append_help_section(
|
|
372
|
+
append_help_section(
|
|
373
|
+
&mut out,
|
|
374
|
+
"Configuration",
|
|
375
|
+
&["route", "leader-prompt", "profile", "install-skill"],
|
|
376
|
+
);
|
|
369
377
|
append_help_section(
|
|
370
378
|
&mut out,
|
|
371
379
|
"Guided recovery",
|
|
@@ -374,7 +382,7 @@ pub(crate) fn default_help() -> String {
|
|
|
374
382
|
append_help_section(
|
|
375
383
|
&mut out,
|
|
376
384
|
"Leader launch",
|
|
377
|
-
&["pi", "codex", "claude", "copilot", "grok", "cursor"],
|
|
385
|
+
&["pi", "codex", "claude", "copilot", "grok", "cursor", "kiro"],
|
|
378
386
|
);
|
|
379
387
|
out.push_str("\nCommand arguments and examples: team-agent <command> --help");
|
|
380
388
|
out
|
|
@@ -425,6 +433,9 @@ pub(super) fn command_help(command: Option<&str>) -> String {
|
|
|
425
433
|
if name == "route" {
|
|
426
434
|
return super::route::HELP.to_string();
|
|
427
435
|
}
|
|
436
|
+
if name == "leader-prompt" {
|
|
437
|
+
return super::leader_prompt::HELP.to_string();
|
|
438
|
+
}
|
|
428
439
|
let (details, examples, next) = match name {
|
|
429
440
|
"quick-start" => (
|
|
430
441
|
"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.",
|
|
@@ -473,6 +484,8 @@ pub(super) fn command_help(command: Option<&str>) -> String {
|
|
|
473
484
|
"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."),
|
|
474
485
|
"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.",
|
|
475
486
|
"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
|
+
"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 quick-start; worker support does not grant leader capability."),
|
|
476
489
|
_ => ("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.",
|
|
477
490
|
"", "Run team-agent quick-start in the opened leader's tool context, not in an ordinary worker conversation."),
|
|
478
491
|
};
|
|
@@ -486,12 +499,17 @@ pub(super) fn command_help(command: Option<&str>) -> String {
|
|
|
486
499
|
} else {
|
|
487
500
|
examples.to_string()
|
|
488
501
|
};
|
|
489
|
-
let scope = if
|
|
502
|
+
let scope = if name == "kiro" {
|
|
503
|
+
"Use --json for the structured capability refusal. Native arguments and attach options cannot enable this capability."
|
|
504
|
+
} else if matches!(spec.kind, CommandKind::LeaderPassthrough { .. }) {
|
|
490
505
|
"Use --json for structured output. --workspace/--team are not launcher options here; they pass to the native provider."
|
|
491
506
|
} else {
|
|
492
507
|
"Use --json for structured output. --workspace selects the project; supported commands use --team to select the team."
|
|
493
508
|
};
|
|
494
|
-
let mut out = format!(
|
|
509
|
+
let mut out = format!(
|
|
510
|
+
"Purpose:\n{}.\nUsage: {}\n\nOptions:\n{}\n{}\nExamples:\n{}\n\nNext Action:\n{}",
|
|
511
|
+
spec.summary, spec.usage, details, scope, examples, next
|
|
512
|
+
);
|
|
495
513
|
if name == "quick-start" {
|
|
496
514
|
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}"));
|
|
497
515
|
}
|
|
@@ -499,7 +517,9 @@ pub(super) fn command_help(command: Option<&str>) -> String {
|
|
|
499
517
|
out.push_str("\nDiscover models with team-agent models --provider cursor_agent.");
|
|
500
518
|
}
|
|
501
519
|
if name == "copilot" {
|
|
502
|
-
out.push_str(
|
|
520
|
+
out.push_str(
|
|
521
|
+
"\nCopilot has no team-agent models entrypoint; consult native provider help.",
|
|
522
|
+
);
|
|
503
523
|
}
|
|
504
524
|
out
|
|
505
525
|
}
|
|
@@ -1250,16 +1270,20 @@ fn warn_send_legacy_delivery_flags(args: &[String]) {
|
|
|
1250
1270
|
"--message-id",
|
|
1251
1271
|
];
|
|
1252
1272
|
let spec = command_spec("send");
|
|
1253
|
-
let sunset = spec
|
|
1254
|
-
|
|
1255
|
-
.
|
|
1256
|
-
|
|
1273
|
+
let sunset = spec
|
|
1274
|
+
.and_then(|spec| spec.sunset)
|
|
1275
|
+
.unwrap_or("a future compatibility release");
|
|
1276
|
+
let action = spec.and_then(|spec| spec.action).unwrap_or(
|
|
1277
|
+
"Use team-agent send <agent> 'task message'; select scope with --workspace/--team",
|
|
1278
|
+
);
|
|
1257
1279
|
for flag in FLAGS {
|
|
1258
1280
|
if args
|
|
1259
1281
|
.iter()
|
|
1260
1282
|
.any(|arg| arg == flag || arg.starts_with(&format!("{flag}=")))
|
|
1261
1283
|
{
|
|
1262
|
-
eprintln!(
|
|
1284
|
+
eprintln!(
|
|
1285
|
+
"Warning: {flag} is deprecated and will be removed in {sunset}. Next: {action}"
|
|
1286
|
+
);
|
|
1263
1287
|
}
|
|
1264
1288
|
}
|
|
1265
1289
|
}
|
|
@@ -2074,7 +2098,11 @@ mod tests {
|
|
|
2074
2098
|
.filter(|spec| spec.default_help)
|
|
2075
2099
|
.map(|spec| spec.name)
|
|
2076
2100
|
.collect();
|
|
2077
|
-
assert_eq!(
|
|
2101
|
+
assert_eq!(
|
|
2102
|
+
expected.len(),
|
|
2103
|
+
31,
|
|
2104
|
+
"the public catalog includes Leader prompt management and the Kiro capability gate"
|
|
2105
|
+
);
|
|
2078
2106
|
for spec_name in &expected {
|
|
2079
2107
|
assert!(
|
|
2080
2108
|
visible.iter().any(|command| command == spec_name),
|
|
@@ -2088,7 +2116,7 @@ mod tests {
|
|
|
2088
2116
|
expected_sorted.sort();
|
|
2089
2117
|
assert_eq!(
|
|
2090
2118
|
actual, expected_sorted,
|
|
2091
|
-
"default help must match the exact
|
|
2119
|
+
"default help must match the exact public spec set, not a slack threshold; got {visible:?}"
|
|
2092
2120
|
);
|
|
2093
2121
|
assert!(
|
|
2094
2122
|
top_help.contains("copilot"),
|
|
@@ -2145,7 +2173,10 @@ mod tests {
|
|
|
2145
2173
|
!is_machine_command(command),
|
|
2146
2174
|
"retired Machine route: {command}"
|
|
2147
2175
|
);
|
|
2148
|
-
assert!(
|
|
2176
|
+
assert!(
|
|
2177
|
+
!is_known_subcommand(command),
|
|
2178
|
+
"retired help gate: {command}"
|
|
2179
|
+
);
|
|
2149
2180
|
}
|
|
2150
2181
|
}
|
|
2151
2182
|
|
|
@@ -2332,9 +2363,13 @@ mod tests {
|
|
|
2332
2363
|
assert_english(&command_help(Some(spec.name)));
|
|
2333
2364
|
}
|
|
2334
2365
|
for text in [
|
|
2335
|
-
TEAM_TEMPLATE,
|
|
2336
|
-
|
|
2337
|
-
|
|
2366
|
+
TEAM_TEMPLATE,
|
|
2367
|
+
WORKER_TEMPLATE,
|
|
2368
|
+
HUMAN_NAVIGATION,
|
|
2369
|
+
crate::cli::route::HELP,
|
|
2370
|
+
crate::cli::COMMS_BOUNDARY_TEXT,
|
|
2371
|
+
crate::cli::QUICK_START_REMINDER,
|
|
2372
|
+
crate::cli::SEND_REMINDER,
|
|
2338
2373
|
crate::cli::STATUS_REMINDER,
|
|
2339
2374
|
] {
|
|
2340
2375
|
assert_english(text);
|
|
@@ -116,6 +116,22 @@ pub fn cmd_leader_passthrough(
|
|
|
116
116
|
Ok(CmdResult::from_json(value, as_json))
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/// Public capability gate, not a legacy passthrough or a native launch attempt.
|
|
120
|
+
pub(crate) fn cmd_kiro_leader(args: &[String]) -> CmdResult {
|
|
121
|
+
CmdResult::from_json(
|
|
122
|
+
json!({
|
|
123
|
+
"schema_version": "leader_launch.v1",
|
|
124
|
+
"ok": false,
|
|
125
|
+
"provider": "kiro",
|
|
126
|
+
"capability": "leader_launch",
|
|
127
|
+
"reason": "kiro_leader_not_admitted",
|
|
128
|
+
"error": "Kiro leader launching is not admitted by this candidate; no native process was started.",
|
|
129
|
+
"action": "Use provider: kiro in a worker role with team-agent quick-start. Worker support does not grant leader capability.",
|
|
130
|
+
}),
|
|
131
|
+
leader_launcher_json(args),
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
119
135
|
/// Closed-set verb → Provider. `None` = not a passthrough verb (must not become ClaudeCode).
|
|
120
136
|
pub(crate) fn leader_passthrough_provider(command: &str) -> Option<crate::model::enums::Provider> {
|
|
121
137
|
use crate::model::enums::Provider;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
//! Early, host-global command dispatch: never select or initialize a workspace.
|
|
2
|
+
use super::ExitCode;
|
|
3
|
+
use crate::leader::prompt::{self, Mutation, PromptError};
|
|
4
|
+
use serde_json::json;
|
|
5
|
+
use std::io::{self, Write};
|
|
6
|
+
use std::path::PathBuf;
|
|
7
|
+
|
|
8
|
+
pub(crate) const HELP: &str = "Purpose:\nPersist supplemental system instructions for future Team Agent Leader launches only.\nUsage: team-agent leader-prompt [show|set|append|clear] [--json] [--file PATH | -- TEXT]\n\nOptions:\nshow (default) prints the exact text; set replaces it; append joins old and new text with two newlines; clear removes it.\nset/append require exactly one TEXT token after --, or --file PATH containing UTF-8 text. No implicit stdin.\nEverything after -- is literal text, including --json and --help. Empty text and NUL are rejected.\nConfiguration: ~/.team-agent/leader-prompt.txt, independent of the workspace. Mutations do not echo the text.\nOnly new Pi, Claude, Codex and Grok Leaders apply it; Worker launches never read it. Cursor/Copilot are refused when configured.\nCodex merges explicit argv developer_instructions, but overrides that user/profile configuration slot; migrate its existing text here.\nNative core prompts and unrelated launch arguments remain unchanged. Attach does not update an existing session.\nExamples:\nteam-agent leader-prompt show\nteam-agent leader-prompt set -- 'Keep replies concise.'\nteam-agent leader-prompt clear\n\nNext Action:\nUse show to inspect the text, then start a new Leader with team-agent pi (or claude/codex/grok).\nset/append/clear affect the next new process, not running Leaders or their conversation history.";
|
|
9
|
+
|
|
10
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
11
|
+
enum Operation {
|
|
12
|
+
Show,
|
|
13
|
+
Set,
|
|
14
|
+
Append,
|
|
15
|
+
Clear,
|
|
16
|
+
}
|
|
17
|
+
impl Operation {
|
|
18
|
+
fn name(self) -> &'static str {
|
|
19
|
+
match self {
|
|
20
|
+
Self::Show => "show",
|
|
21
|
+
Self::Set => "set",
|
|
22
|
+
Self::Append => "append",
|
|
23
|
+
Self::Clear => "clear",
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
#[derive(Debug, Eq, PartialEq)]
|
|
28
|
+
enum Input {
|
|
29
|
+
Text(String),
|
|
30
|
+
File(PathBuf),
|
|
31
|
+
}
|
|
32
|
+
#[derive(Debug)]
|
|
33
|
+
struct Request {
|
|
34
|
+
operation: Operation,
|
|
35
|
+
input: Option<Input>,
|
|
36
|
+
json: bool,
|
|
37
|
+
help: bool,
|
|
38
|
+
}
|
|
39
|
+
fn usage() -> PromptError {
|
|
40
|
+
PromptError::new(
|
|
41
|
+
"leader_prompt_invalid_input",
|
|
42
|
+
"Invalid or incomplete leader-prompt arguments; use one input for set/append",
|
|
43
|
+
None,
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
fn parse(args: &[String]) -> Result<Request, PromptError> {
|
|
47
|
+
let end = args
|
|
48
|
+
.iter()
|
|
49
|
+
.position(|arg| arg == "--")
|
|
50
|
+
.unwrap_or(args.len());
|
|
51
|
+
let mut json = false;
|
|
52
|
+
let mut help = false;
|
|
53
|
+
let mut file = None;
|
|
54
|
+
let mut positional = None;
|
|
55
|
+
let mut i = 0;
|
|
56
|
+
while i < end {
|
|
57
|
+
match args[i].as_str() {
|
|
58
|
+
"--json" if !json => json = true,
|
|
59
|
+
"--help" | "-h" if !help => help = true,
|
|
60
|
+
"--stdin" => {
|
|
61
|
+
return Err(PromptError::new(
|
|
62
|
+
"leader_prompt_invalid_input",
|
|
63
|
+
"--stdin is not supported for leader-prompt; use -- TEXT or --file PATH instead",
|
|
64
|
+
None,
|
|
65
|
+
));
|
|
66
|
+
}
|
|
67
|
+
"--file" if file.is_none() && i + 1 < end => {
|
|
68
|
+
i += 1;
|
|
69
|
+
if matches!(args[i].as_str(), "--json" | "--help" | "-h" | "--file") {
|
|
70
|
+
return Err(usage());
|
|
71
|
+
}
|
|
72
|
+
file = Some(PathBuf::from(&args[i]));
|
|
73
|
+
}
|
|
74
|
+
token if !token.starts_with('-') && positional.is_none() => positional = Some(token),
|
|
75
|
+
_ => return Err(usage()),
|
|
76
|
+
}
|
|
77
|
+
i += 1;
|
|
78
|
+
}
|
|
79
|
+
let operation = match positional.unwrap_or("show") {
|
|
80
|
+
"show" => Operation::Show,
|
|
81
|
+
"set" => Operation::Set,
|
|
82
|
+
"append" => Operation::Append,
|
|
83
|
+
"clear" => Operation::Clear,
|
|
84
|
+
_ => return Err(usage()),
|
|
85
|
+
};
|
|
86
|
+
let text = if end < args.len() {
|
|
87
|
+
if args.len() != end + 2 {
|
|
88
|
+
return Err(usage());
|
|
89
|
+
}
|
|
90
|
+
Some(args[end + 1].clone())
|
|
91
|
+
} else {
|
|
92
|
+
None
|
|
93
|
+
};
|
|
94
|
+
if (file.is_some() && text.is_some()) || (help && (file.is_some() || text.is_some())) {
|
|
95
|
+
return Err(usage());
|
|
96
|
+
}
|
|
97
|
+
let input = file.map(Input::File).or_else(|| text.map(Input::Text));
|
|
98
|
+
if !help && (matches!(operation, Operation::Set | Operation::Append) != input.is_some()) {
|
|
99
|
+
return Err(usage());
|
|
100
|
+
}
|
|
101
|
+
Ok(Request {
|
|
102
|
+
operation,
|
|
103
|
+
input,
|
|
104
|
+
json,
|
|
105
|
+
help,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
fn execute(request: Request) -> Result<(), PromptError> {
|
|
109
|
+
let path = prompt::config_path()?;
|
|
110
|
+
if request.operation == Operation::Show {
|
|
111
|
+
let text = prompt::read(&path)?.unwrap_or_default();
|
|
112
|
+
if request.json {
|
|
113
|
+
println!(
|
|
114
|
+
"{}",
|
|
115
|
+
json!({"configured": !text.is_empty(), "config_path": path, "prompt": text})
|
|
116
|
+
);
|
|
117
|
+
} else {
|
|
118
|
+
// Unlike mutation diagnostics, explicit show must preserve every byte.
|
|
119
|
+
io::stdout()
|
|
120
|
+
.write_all(text.as_bytes())
|
|
121
|
+
.and_then(|_| io::stdout().flush())
|
|
122
|
+
.map_err(|error| PromptError::io("leader_prompt_write_failed", &path, error))?;
|
|
123
|
+
}
|
|
124
|
+
return Ok(());
|
|
125
|
+
}
|
|
126
|
+
let mutation = match request.operation {
|
|
127
|
+
Operation::Clear => Mutation::Clear,
|
|
128
|
+
_ => {
|
|
129
|
+
let text = match request.input {
|
|
130
|
+
Some(Input::Text(text)) => text,
|
|
131
|
+
Some(Input::File(input)) => std::fs::read_to_string(&input).map_err(|error| {
|
|
132
|
+
PromptError::io("leader_prompt_unreadable", &input, error).at_config(&path)
|
|
133
|
+
})?,
|
|
134
|
+
None => return Err(usage()),
|
|
135
|
+
};
|
|
136
|
+
if request.operation == Operation::Set {
|
|
137
|
+
Mutation::Set(text)
|
|
138
|
+
} else {
|
|
139
|
+
Mutation::Append(text)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
let configured = prompt::mutate(&path, mutation)?;
|
|
144
|
+
if request.json {
|
|
145
|
+
println!(
|
|
146
|
+
"{}",
|
|
147
|
+
json!({"operation": request.operation.name(), "configured": configured, "config_path": path})
|
|
148
|
+
);
|
|
149
|
+
} else {
|
|
150
|
+
println!(
|
|
151
|
+
"Leader prompt {}: configured={configured}; config={}",
|
|
152
|
+
request.operation.name(),
|
|
153
|
+
path.display()
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
Ok(())
|
|
157
|
+
}
|
|
158
|
+
pub(crate) fn run(args: &[String]) -> ExitCode {
|
|
159
|
+
let end = args
|
|
160
|
+
.iter()
|
|
161
|
+
.position(|arg| arg == "--")
|
|
162
|
+
.unwrap_or(args.len());
|
|
163
|
+
let as_json = args[..end].iter().any(|arg| arg == "--json");
|
|
164
|
+
let result = match parse(args) {
|
|
165
|
+
Ok(request) if request.help => {
|
|
166
|
+
println!("{HELP}");
|
|
167
|
+
return ExitCode::Ok;
|
|
168
|
+
}
|
|
169
|
+
Ok(request) => execute(request),
|
|
170
|
+
Err(error) => Err(error),
|
|
171
|
+
};
|
|
172
|
+
match result {
|
|
173
|
+
Ok(()) => ExitCode::Ok,
|
|
174
|
+
Err(error) => {
|
|
175
|
+
if as_json {
|
|
176
|
+
println!("{}", error.report());
|
|
177
|
+
} else {
|
|
178
|
+
eprintln!("{error}");
|
|
179
|
+
}
|
|
180
|
+
if error.reason == "leader_prompt_invalid_input" {
|
|
181
|
+
ExitCode::Usage
|
|
182
|
+
} else {
|
|
183
|
+
ExitCode::Error
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#[cfg(test)]
|
|
190
|
+
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
191
|
+
mod tests {
|
|
192
|
+
use super::*;
|
|
193
|
+
fn args(tokens: &[&str]) -> Vec<String> {
|
|
194
|
+
tokens.iter().map(|text| text.to_string()).collect()
|
|
195
|
+
}
|
|
196
|
+
#[test]
|
|
197
|
+
fn leader_prompt_controls_stop_at_delimiter() {
|
|
198
|
+
for text in ["--json", "--help", "--file", "--stdin", "multi\nline🙂", ""] {
|
|
199
|
+
let request = parse(&args(&["set", "--", text])).unwrap();
|
|
200
|
+
assert!(!request.json && !request.help);
|
|
201
|
+
assert_eq!(request.input, Some(Input::Text(text.into())));
|
|
202
|
+
}
|
|
203
|
+
assert!(parse(&args(&[])).unwrap().operation == Operation::Show);
|
|
204
|
+
let request = parse(&args(&["--json", "append", "--file", "path with 中文"])).unwrap();
|
|
205
|
+
assert_eq!(
|
|
206
|
+
request.input,
|
|
207
|
+
Some(Input::File(PathBuf::from("path with 中文")))
|
|
208
|
+
);
|
|
209
|
+
assert!(request.json);
|
|
210
|
+
}
|
|
211
|
+
#[test]
|
|
212
|
+
fn leader_prompt_stdin_control_reports_unsupported_input_with_usage_exit() {
|
|
213
|
+
for tokens in [
|
|
214
|
+
vec!["set", "--stdin"],
|
|
215
|
+
vec!["set", "--json", "--stdin"],
|
|
216
|
+
vec!["append", "--json", "--stdin"],
|
|
217
|
+
] {
|
|
218
|
+
let tokens = args(&tokens);
|
|
219
|
+
let error = parse(&tokens).unwrap_err();
|
|
220
|
+
assert_eq!(error.reason, "leader_prompt_invalid_input");
|
|
221
|
+
assert_eq!(
|
|
222
|
+
error.error,
|
|
223
|
+
"--stdin is not supported for leader-prompt; use -- TEXT or --file PATH instead"
|
|
224
|
+
);
|
|
225
|
+
assert!(error.config_path.is_none());
|
|
226
|
+
assert_eq!(error.report()["reason"], "leader_prompt_invalid_input");
|
|
227
|
+
assert_eq!(error.report()["error"], error.error);
|
|
228
|
+
assert!(error.to_string().contains("--stdin is not supported"));
|
|
229
|
+
assert_eq!(run(&tokens).code(), 2);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
#[test]
|
|
233
|
+
fn leader_prompt_stdin_file_value_is_not_reclassified_as_a_control() {
|
|
234
|
+
for operation in ["set", "append"] {
|
|
235
|
+
let request = parse(&args(&[operation, "--file", "--stdin"])).unwrap();
|
|
236
|
+
assert_eq!(request.input, Some(Input::File(PathBuf::from("--stdin"))));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
#[test]
|
|
240
|
+
fn leader_prompt_rejects_duplicate_missing_unknown_or_extra_controls() {
|
|
241
|
+
for tokens in [
|
|
242
|
+
vec!["set"],
|
|
243
|
+
vec!["append", "--"],
|
|
244
|
+
vec!["set", "--", "one", "two"],
|
|
245
|
+
vec!["set", "--file"],
|
|
246
|
+
vec!["set", "--file", "input", "--", "text"],
|
|
247
|
+
vec!["show", "--", "text"],
|
|
248
|
+
vec!["clear", "--file", "input"],
|
|
249
|
+
vec!["show", "--json", "--json"],
|
|
250
|
+
vec!["set", "--file", "one", "--file", "two"],
|
|
251
|
+
vec!["show", "--unknown"],
|
|
252
|
+
vec!["show", "clear"],
|
|
253
|
+
vec!["unknown", "--help"],
|
|
254
|
+
] {
|
|
255
|
+
assert_eq!(
|
|
256
|
+
parse(&args(&tokens)).unwrap_err().reason,
|
|
257
|
+
"leader_prompt_invalid_input"
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|