@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.
Files changed (46) hide show
  1. package/Cargo.lock +252 -84
  2. package/Cargo.toml +3 -1
  3. package/crates/team-agent/Cargo.toml +3 -0
  4. package/crates/team-agent/src/cli/adapters.rs +28 -15
  5. package/crates/team-agent/src/cli/diagnose.rs +36 -13
  6. package/crates/team-agent/src/cli/emit.rs +55 -20
  7. package/crates/team-agent/src/cli/leader.rs +16 -0
  8. package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
  9. package/crates/team-agent/src/cli/mod.rs +152 -8
  10. package/crates/team-agent/src/cli/models.rs +68 -18
  11. package/crates/team-agent/src/cli/spec.rs +7 -1
  12. package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
  13. package/crates/team-agent/src/cli/tests/base.rs +88 -0
  14. package/crates/team-agent/src/cli/tests/status_send.rs +2 -0
  15. package/crates/team-agent/src/compiler.rs +66 -18
  16. package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
  17. package/crates/team-agent/src/contract_runtime/config.rs +309 -0
  18. package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
  19. package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
  20. package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
  21. package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
  22. package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
  23. package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
  24. package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
  25. package/crates/team-agent/src/contract_runtime/registry.rs +30 -0
  26. package/crates/team-agent/src/coordinator/tick.rs +26 -8
  27. package/crates/team-agent/src/leader/mod.rs +1 -0
  28. package/crates/team-agent/src/leader/prompt.rs +906 -0
  29. package/crates/team-agent/src/leader/start.rs +260 -16
  30. package/crates/team-agent/src/leader/tests/mod.rs +2 -0
  31. package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
  32. package/crates/team-agent/src/leader/types.rs +6 -0
  33. package/crates/team-agent/src/lib.rs +4 -0
  34. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +472 -388
  35. package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
  36. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
  37. package/crates/team-agent/src/lifecycle/launch.rs +39 -28
  38. package/crates/team-agent/src/lifecycle/types.rs +4 -1
  39. package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
  40. package/crates/team-agent/src/main.rs +6 -0
  41. package/crates/team-agent/src/messaging/delivery.rs +50 -1
  42. package/crates/team-agent/src/messaging/send.rs +10 -0
  43. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
  44. package/crates/team-agent/src/model/spec.rs +24 -12
  45. package/crates/team-agent/src/state/persist.rs +73 -0
  46. 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.get("summary").and_then(Value::as_str).unwrap_or("Team started.");
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 ["existing_runtime", "preflight_blocked", "leader_binding_refused"] {
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 { panic!("expected human report") };
1236
- let rendered: Value = serde_json::from_str(text.strip_prefix("quick-start report:\n").unwrap()).unwrap();
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!(!text.contains("team-agent doctor"), "must not replace actual actions");
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 { panic!("expected human report") };
1256
- let rendered: Value = serde_json::from_str(text.strip_prefix("quick-start report:\n").unwrap()).unwrap();
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(workspace, state, coordinator_health, &mut issues, &mut repairs);
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| matches!(status, "stopped" | "shutdown" | "archived" | "terminal"));
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
- class == crate::lifecycle::launch::LeaderBindingClass::Unbound
700
- && object.get("ok").and_then(Value::as_bool) == Some(true)
701
- && object
702
- .get("profile_smoke")
703
- .and_then(|profile| profile.get("checks"))
704
- .and_then(Value::as_array)
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 = json!({"agents": {}, "tasks": [], "session_name": null, "active_team_key": null});
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(&mut out, "Configuration", &["route", "profile", "install-skill"]);
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 matches!(spec.kind, CommandKind::LeaderPassthrough { .. }) {
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!("Purpose:\n{}.\nUsage: {}\n\nOptions:\n{}\n{}\nExamples:\n{}\n\nNext Action:\n{}", spec.summary, spec.usage, details, scope, examples, next);
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("\nCopilot has no team-agent models entrypoint; consult native provider help.");
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.and_then(|spec| spec.sunset).unwrap_or("a future compatibility release");
1254
- let action = spec
1255
- .and_then(|spec| spec.action)
1256
- .unwrap_or("Use team-agent send <agent> 'task message'; select scope with --workspace/--team");
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!("Warning: {flag} is deprecated and will be removed in {sunset}. Next: {action}");
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!(expected.len(), 29, "the public catalog is Human29");
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 Human29 public spec set, not a slack threshold; got {visible:?}"
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!(!is_known_subcommand(command), "retired help gate: {command}");
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, WORKER_TEMPLATE, HUMAN_NAVIGATION,
2336
- crate::cli::route::HELP, crate::cli::COMMS_BOUNDARY_TEXT,
2337
- crate::cli::QUICK_START_REMINDER, crate::cli::SEND_REMINDER,
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
+ }