@team-agent/installer 0.5.118 → 0.5.120

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 (72) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +46 -1
  4. package/README.zh.md +46 -1
  5. package/crates/team-agent/src/cli/adapters.rs +11 -0
  6. package/crates/team-agent/src/cli/arguments.rs +401 -0
  7. package/crates/team-agent/src/cli/emit.rs +241 -142
  8. package/crates/team-agent/src/cli/intent/runtime.rs +1139 -0
  9. package/crates/team-agent/src/cli/intent/runtime_tests.rs +584 -0
  10. package/crates/team-agent/src/cli/intent/tests.rs +654 -0
  11. package/crates/team-agent/src/cli/intent.rs +642 -0
  12. package/crates/team-agent/src/cli/mod.rs +49 -5
  13. package/crates/team-agent/src/cli/named_address.rs +10 -1
  14. package/crates/team-agent/src/cli/spec.rs +22 -10
  15. package/crates/team-agent/src/cli/status_port/brief.rs +117 -16
  16. package/crates/team-agent/src/cli/tests/base.rs +6 -1
  17. package/crates/team-agent/src/cli/tests/parser_safety.rs +383 -0
  18. package/crates/team-agent/src/cli/tests/run_delegation.rs +63 -19
  19. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +3 -2
  20. package/crates/team-agent/src/cli/tests/verb_profile.rs +3 -1
  21. package/crates/team-agent/src/contract_runtime/backend.rs +1 -1
  22. package/crates/team-agent/src/db/message_store.rs +10 -3
  23. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +166 -77
  24. package/crates/team-agent/src/lifecycle/launch.rs +28 -0
  25. package/crates/team-agent/src/lifecycle/restart.rs +17 -0
  26. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -3
  27. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +5 -15
  28. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -3
  29. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +2 -3
  30. package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +10 -2
  31. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +8 -1
  32. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +3 -2
  33. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +6 -9
  34. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +7 -5
  35. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +4 -2
  36. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +5 -2
  37. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +4 -3
  38. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +2 -4
  39. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +3 -4
  40. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +5 -4
  41. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +2 -4
  42. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +78 -36
  43. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +8 -4
  44. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +6 -12
  45. package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +4 -0
  46. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1 -3
  47. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +7 -0
  48. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -3
  49. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -1
  50. package/crates/team-agent/src/lifecycle/types.rs +10 -0
  51. package/crates/team-agent/src/messaging/delivery.rs +132 -39
  52. package/crates/team-agent/src/messaging/delivery_consumption_tests.rs +223 -0
  53. package/crates/team-agent/src/messaging/enter_rescue.rs +182 -154
  54. package/crates/team-agent/src/messaging/enter_rescue_tests.rs +344 -277
  55. package/crates/team-agent/src/messaging/mod.rs +1 -1
  56. package/crates/team-agent/src/tmux_backend/enter_rescue.rs +325 -0
  57. package/crates/team-agent/src/tmux_backend/enter_rescue_tests.rs +432 -0
  58. package/crates/team-agent/src/tmux_backend/native_input.rs +19 -0
  59. package/crates/team-agent/src/tmux_backend/tests.rs +37 -0
  60. package/crates/team-agent/src/tmux_backend.rs +35 -1
  61. package/crates/team-agent/src/transport/enter_rescue.rs +166 -0
  62. package/crates/team-agent/src/transport/native_input.rs +5 -1
  63. package/crates/team-agent/src/transport/test_support.rs +25 -0
  64. package/crates/team-agent/src/transport.rs +34 -0
  65. package/package.json +4 -4
  66. package/skills/team-agent/SKILL.md +71 -54
  67. package/skills/team-agent/command-coverage.json +283 -13
  68. package/skills/team-agent/docs/reference/team-agent-operator.md +220 -0
  69. package/skills/team-agent/references/bug-as-artifact-flow.md +1 -1
  70. package/skills/team-agent/references/recovery-runbook.md +61 -212
  71. package/skills/team-agent/references/team-agent-operator.md +181 -48
  72. package/skills/team-agent/references/team-in-team.md +47 -80
@@ -0,0 +1,654 @@
1
+ #![allow(clippy::unwrap_used, clippy::panic)]
2
+ use super::*;
3
+ use std::cell::RefCell;
4
+
5
+ pub(super) struct Sandbox(pub PathBuf);
6
+ impl Sandbox {
7
+ pub(super) fn new() -> Self {
8
+ use std::sync::atomic::{AtomicU64, Ordering};
9
+ static NEXT: AtomicU64 = AtomicU64::new(0);
10
+ let path = std::env::temp_dir().join(format!(
11
+ "ta-cli-phase1-{}-{}",
12
+ std::process::id(),
13
+ NEXT.fetch_add(1, Ordering::Relaxed)
14
+ ));
15
+ std::fs::create_dir(&path).unwrap();
16
+ Self(path.canonicalize().unwrap())
17
+ }
18
+ pub(super) fn path(&self) -> &Path {
19
+ &self.0
20
+ }
21
+ }
22
+ impl Drop for Sandbox {
23
+ fn drop(&mut self) {
24
+ let _ = std::fs::remove_dir_all(&self.0);
25
+ }
26
+ }
27
+
28
+ fn args(values: &[&str]) -> Vec<String> {
29
+ values.iter().map(|v| v.to_string()).collect()
30
+ }
31
+ pub(super) fn intent(verb: Verb) -> Intent {
32
+ Intent {
33
+ surface: Surface::Canonical,
34
+ verb,
35
+ subject: Subject::Agent(AgentId::new("one")),
36
+ workspace: PathBuf::from("/fixture"),
37
+ pinned_workspace: true,
38
+ team: Some("pinned".into()),
39
+ session: SessionPolicy::Preserve,
40
+ session_converge_deadline_ms: None,
41
+ dry_run: false,
42
+ json: true,
43
+ role_file: None,
44
+ patch: RoleConfigPatch::default(),
45
+ }
46
+ }
47
+ fn plan(verb: Verb, actions: &[&str]) -> Plan {
48
+ let intent = intent(verb);
49
+ let scope = ResolvedScope {
50
+ workspace: intent.workspace.clone(),
51
+ team: intent.team.clone(),
52
+ };
53
+ let steps = actions
54
+ .iter()
55
+ .map(|action| match *action {
56
+ "stop" => Step::StopAgent(StopAgentArgs {
57
+ agent: "one".into(),
58
+ workspace: scope.workspace.clone(),
59
+ team: scope.team.clone(),
60
+ json: true,
61
+ }),
62
+ _ => Step::StartAgent(StartAgentArgs {
63
+ agent: "one".into(),
64
+ workspace: scope.workspace.clone(),
65
+ team: scope.team.clone(),
66
+ force: false,
67
+ allow_fresh: false,
68
+ json: true,
69
+ role_config: RoleConfigPatch::default(),
70
+ }),
71
+ })
72
+ .collect();
73
+ Plan {
74
+ intent,
75
+ scope,
76
+ steps,
77
+ skipped: vec![],
78
+ no_eligible: None,
79
+ files: vec![],
80
+ expected: BTreeMap::new(),
81
+ topology: None,
82
+ }
83
+ }
84
+ #[test]
85
+ fn registered_truth_table_keeps_physics_configuration_and_intent_separate() {
86
+ use Observation::*;
87
+ use Verb::*;
88
+ for (verb, obs, expected) in [
89
+ (Start, Live, vec![]),
90
+ (Start, StoppedVerified, vec!["start"]),
91
+ (Start, Unstarted, vec!["start"]),
92
+ (Stop, Live, vec!["stop"]),
93
+ (Stop, StoppedVerified, vec![]),
94
+ (Restart, Live, vec!["stop", "start"]),
95
+ (Restart, StoppedVerified, vec!["start"]),
96
+ ] {
97
+ assert_eq!(
98
+ seat_actions(verb, obs, false, false, false, false).unwrap(),
99
+ expected
100
+ );
101
+ }
102
+ for verb in [Start, Stop, Restart] {
103
+ assert!(seat_actions(verb, Unknown, false, false, false, false).is_err());
104
+ assert!(seat_actions(verb, Live, true, false, false, false).is_err());
105
+ }
106
+ assert!(seat_actions(Start, Live, false, false, true, false).is_err());
107
+ assert!(seat_actions(Start, Live, false, true, false, false).is_err());
108
+ assert!(seat_actions(Restart, Unstarted, false, false, false, false).is_err());
109
+ assert!(seat_actions(Start, Live, true, false, false, true)
110
+ .unwrap()
111
+ .is_empty());
112
+ assert!(seat_actions(Restart, Live, false, true, false, true)
113
+ .unwrap()
114
+ .is_empty());
115
+ assert_eq!(
116
+ seat_actions(Start, StoppedVerified, false, true, false, true).unwrap(),
117
+ ["start"]
118
+ );
119
+ }
120
+ #[test]
121
+ fn explicit_stop_uses_existing_conservative_interpretation() {
122
+ for value in [json!(true), Value::Null, json!("false"), json!(0)] {
123
+ let row = json!({"explicit_stop":value});
124
+ assert!(crate::state::projection::agent_explicit_stop(&row));
125
+ }
126
+ for row in [json!({}), json!({"explicit_stop":false})] {
127
+ assert!(!crate::state::projection::agent_explicit_stop(&row));
128
+ }
129
+ }
130
+ #[test]
131
+ fn restart_executes_exact_pinned_stop_start_and_preserves_original_receipts() {
132
+ let calls = RefCell::new(Vec::new());
133
+ let result = execute_plan(
134
+ plan(Verb::Restart, &["stop", "start"]),
135
+ &mut |step| {
136
+ calls.borrow_mut().push(step.name());
137
+ Ok(CmdResult::from_json(
138
+ json!({"ok":true,"stopped":true,"start_mode":"resumed","original":17}),
139
+ true,
140
+ ))
141
+ },
142
+ &mut |p, s, after_stop| {
143
+ assert_eq!(p.scope.team.as_deref(), Some("pinned"));
144
+ assert_eq!(s.agent(), Some("one"));
145
+ assert_eq!(after_stop, matches!(s, Step::StartAgent(_)));
146
+ Ok(())
147
+ },
148
+ )
149
+ .unwrap();
150
+ assert_eq!(*calls.borrow(), ["stop_agent", "start_agent"]);
151
+ let CmdOutput::Json(value) = result.output else {
152
+ panic!()
153
+ };
154
+ assert_eq!(value["steps"][1]["receipt"]["original"], 17);
155
+ assert_eq!(value["steps"][1]["mode"], "resumed");
156
+ }
157
+ #[test]
158
+ fn stop_failure_never_starts_and_spawn_failure_preserves_partial_uncertainty() {
159
+ for fail_at in [0, 1] {
160
+ let mut calls = Vec::new();
161
+ let result = execute_plan(
162
+ plan(Verb::Restart, &["stop", "start"]),
163
+ &mut |step| {
164
+ let n = calls.len();
165
+ calls.push(step.name());
166
+ Ok(CmdResult::from_json(
167
+ json!({"ok":n!=fail_at,"stopped":true,"error":"original refusal"}),
168
+ true,
169
+ ))
170
+ },
171
+ &mut |_, _, _| Ok(()),
172
+ )
173
+ .unwrap();
174
+ assert_eq!(result.exit, ExitCode::Error);
175
+ assert_eq!(calls.len(), fail_at + 1);
176
+ let CmdOutput::Json(value) = result.output else {
177
+ panic!()
178
+ };
179
+ assert_eq!(
180
+ value["status"],
181
+ if fail_at == 0 { "failed" } else { "partial" }
182
+ );
183
+ assert_eq!(
184
+ value["steps"][fail_at]["receipt"]["error"],
185
+ "original refusal"
186
+ );
187
+ }
188
+ }
189
+ #[test]
190
+ fn owner_or_incarnation_change_interrupts_even_best_effort_start() {
191
+ let mut calls = 0;
192
+ let mut checks = 0;
193
+ let result = execute_plan(
194
+ plan(Verb::Start, &["start", "start"]),
195
+ &mut |_| {
196
+ calls += 1;
197
+ Ok(CmdResult::from_json(json!({"ok":true}), true))
198
+ },
199
+ &mut |_, _, _| {
200
+ checks += 1;
201
+ if checks == 2 {
202
+ Err(refusal("owner changed"))
203
+ } else {
204
+ Ok(())
205
+ }
206
+ },
207
+ )
208
+ .unwrap();
209
+ assert_eq!(calls, 1);
210
+ assert_eq!(result.exit, ExitCode::Error);
211
+ }
212
+ #[test]
213
+ fn role_change_between_stop_and_start_refuses_without_rollback_claim() {
214
+ let mut calls = 0;
215
+ let result = execute_plan(
216
+ plan(Verb::Restart, &["stop", "start"]),
217
+ &mut |_| {
218
+ calls += 1;
219
+ Ok(CmdResult::from_json(
220
+ json!({"ok":true,"stopped":true}),
221
+ true,
222
+ ))
223
+ },
224
+ &mut |_, _, after_stop| {
225
+ if after_stop {
226
+ Err(refusal("role changed"))
227
+ } else {
228
+ Ok(())
229
+ }
230
+ },
231
+ )
232
+ .unwrap();
233
+ assert_eq!(calls, 1);
234
+ let CmdOutput::Json(value) = result.output else {
235
+ panic!()
236
+ };
237
+ assert_eq!(value["status"], "partial");
238
+ assert_eq!(value["last_confirmed_effect"], "original_instance_stopped");
239
+ assert_eq!(value["current_state"], "unknown");
240
+ }
241
+ #[test]
242
+ fn issue337_failed_handler_preserves_confirmed_and_uncertain_effects() {
243
+ for (coordinator, workers, confirmed) in [
244
+ ("not_attempted", json!(["worker"]), true),
245
+ ("attempted_unknown", json!([]), false),
246
+ ] {
247
+ let result = execute_plan(
248
+ plan(Verb::Start, &["start"]),
249
+ &mut |_| Ok(CmdResult::from_json(json!({
250
+ "ok": false, "status": "partial", "error": "original failure",
251
+ "effects": {"worker_panes_started": workers, "coordinator_start": coordinator},
252
+ }), true)),
253
+ &mut |_, _, _| Ok(()),
254
+ ).unwrap();
255
+ assert_eq!(result.exit, ExitCode::Error);
256
+ let CmdOutput::Json(value) = result.output else {
257
+ panic!()
258
+ };
259
+ assert_eq!(value["status"], "partial");
260
+ assert_eq!(value["steps"][0]["receipt"]["error"], "original failure");
261
+ assert_eq!(
262
+ value["effects"][0]["effects"]["coordinator_start"],
263
+ coordinator
264
+ );
265
+ assert_eq!(!value["last_confirmed_effect"].is_null(), confirmed);
266
+ let recovery = value["recovery"].as_str().unwrap();
267
+ assert!(recovery.contains("--workspace"));
268
+ assert!(recovery.contains("--team"));
269
+ assert!(recovery.contains("doctor"));
270
+ assert!(!recovery.contains("claim-leader"));
271
+ }
272
+ }
273
+
274
+ #[test]
275
+ fn issue337_zero_effect_refusal_is_explicit_at_top_without_hiding_prior_uncertainty() {
276
+ for prior_unknown in [false, true] {
277
+ let mut call = 0;
278
+ let actions = if prior_unknown {
279
+ vec!["start", "start"]
280
+ } else {
281
+ vec!["start"]
282
+ };
283
+ let result = execute_plan(
284
+ plan(Verb::Start, &actions),
285
+ &mut |_| {
286
+ call += 1;
287
+ let receipt = if prior_unknown && call == 1 {
288
+ json!({"ok":false,"error":"unclassified handler failure"})
289
+ } else {
290
+ json!({"ok":false,"status":"preflight_blocked",
291
+ "effects":{"worker_panes_started":[],"coordinator_start":"not_attempted"},
292
+ "next_actions":["run from the intended live Leader pane; no process was started"]})
293
+ };
294
+ Ok(CmdResult::from_json(receipt, true))
295
+ },
296
+ &mut |_, _, _| Ok(()),
297
+ ).unwrap();
298
+ let CmdOutput::Json(value) = result.output else {
299
+ panic!()
300
+ };
301
+ assert_eq!(value["status"], "failed");
302
+ assert_eq!(
303
+ value["current_state"],
304
+ if prior_unknown {
305
+ "unknown"
306
+ } else {
307
+ "unchanged"
308
+ }
309
+ );
310
+ assert!(value["last_confirmed_effect"].is_null());
311
+ assert_eq!(
312
+ value["effects"][0]["effects"]["coordinator_start"],
313
+ "not_attempted"
314
+ );
315
+ let recovery = value["recovery"].as_str().unwrap();
316
+ if prior_unknown {
317
+ assert!(recovery.contains("doctor"));
318
+ assert!(!recovery.contains("no process was started"));
319
+ } else {
320
+ assert!(recovery.contains("live Leader pane"));
321
+ }
322
+ }
323
+ }
324
+
325
+ #[test]
326
+ fn dry_run_never_calls_handler_or_execution_admission() {
327
+ let mut plan = plan(Verb::Restart, &["stop", "start"]);
328
+ plan.intent.dry_run = true;
329
+ let result = execute_plan(plan, &mut |_| panic!("dry-run executed"), &mut |_, _, _| {
330
+ panic!("dry-run acquired execution admission")
331
+ })
332
+ .unwrap();
333
+ let CmdOutput::Json(value) = result.output else {
334
+ panic!()
335
+ };
336
+ assert_eq!(value["dry_run"], true);
337
+ assert_eq!(value["steps"].as_array().unwrap().len(), 2);
338
+ assert!(!value["deferred"].as_array().unwrap().is_empty());
339
+ }
340
+ #[test]
341
+ fn lexical_paths_never_use_dot_or_directory_existence_as_agent_inference() {
342
+ for id in ["reviewer", "reviewer.v2", "worker-1"] {
343
+ assert!(!path_syntax(id));
344
+ }
345
+ for path in [
346
+ ".",
347
+ "..",
348
+ "./worker",
349
+ "/tmp/worker",
350
+ "a/b",
351
+ "a\\b",
352
+ "C:\\team",
353
+ ] {
354
+ assert!(path_syntax(path));
355
+ }
356
+ }
357
+ #[test]
358
+ fn grammar_is_strict_and_payload_values_are_not_controls() {
359
+ for (schema, values) in [
360
+ ("intent-start", vec!["one", "--force"]),
361
+ ("intent-start", vec!["--from"]),
362
+ ("intent-stop", vec!["one", "two"]),
363
+ ("intent-stop", vec!["--session", "auto"]),
364
+ ("intent-restart", vec!["one", "--model", "wrong"]),
365
+ ("intent-restart", vec!["--session", "fresh"]),
366
+ (
367
+ "intent-start",
368
+ vec!["--workspace=missing", "--workspace=other"],
369
+ ),
370
+ ] {
371
+ assert!(super::super::arguments::parse(schema, &args(&values)).is_err());
372
+ }
373
+ let checked =
374
+ super::super::arguments::parse("intent-start", &args(&["one", "--prompt", "--help"]))
375
+ .unwrap();
376
+ assert!(!checked.help);
377
+ assert!(checked.tokens.contains(&"--prompt=--help".into()));
378
+ }
379
+ #[test]
380
+ fn frozen_legacy_restart_is_not_rerouted() {
381
+ let cwd = std::env::temp_dir();
382
+ for values in [
383
+ vec![],
384
+ vec!["--workspace", "/unread-by-bridge"],
385
+ vec!["./old-path"],
386
+ vec!["--allow-fresh"],
387
+ vec!["--help"],
388
+ ] {
389
+ assert!(run("restart", &args(&values), &cwd).is_none());
390
+ }
391
+ for values in [vec!["--session", "auto", "--help"], vec!["one", "--help"]] {
392
+ assert!(run("restart", &args(&values), &cwd).unwrap().is_ok());
393
+ }
394
+ }
395
+ #[test]
396
+ fn canonical_help_and_invalid_flags_do_not_create_runtime() {
397
+ let root = Sandbox::new();
398
+ for command in ["start", "stop", "team", "agent"] {
399
+ assert_eq!(
400
+ run(command, &args(&["--help"]), root.path())
401
+ .unwrap()
402
+ .unwrap()
403
+ .exit,
404
+ ExitCode::Ok
405
+ );
406
+ }
407
+ assert!(run("start", &args(&["--workspaec", "x"]), root.path())
408
+ .unwrap()
409
+ .is_err());
410
+ assert!(run("stop", &args(&["./one"]), root.path())
411
+ .unwrap()
412
+ .is_err());
413
+ assert!(!root.path().join(".team").exists());
414
+ }
415
+
416
+ #[test]
417
+ fn delimiter_and_consumed_prompt_are_never_rendering_or_scope_controls() {
418
+ let checked =
419
+ super::super::arguments::parse("intent-start", &args(&["--", "--workspace=foreign"]))
420
+ .unwrap();
421
+ let (pos, flags) = options(&checked.tokens);
422
+ assert_eq!(pos, ["--workspace=foreign"]);
423
+ assert!(flags.is_empty());
424
+ assert!(!super::super::arguments::has_flag(
425
+ "start",
426
+ &args(&["one", "--prompt", "--json"]),
427
+ "--json"
428
+ ));
429
+ assert!(!super::super::arguments::has_flag(
430
+ "agent",
431
+ &args(&["start", "one", "--prompt", "--json"]),
432
+ "--json"
433
+ ));
434
+ }
435
+
436
+ #[test]
437
+ fn unconfirmed_stop_never_authorizes_a_restart_start_step() {
438
+ for receipt in [json!({"ok":true,"stopped":false}), json!({"ok":true})] {
439
+ let mut calls = 0;
440
+ let result = execute_plan(
441
+ plan(Verb::Restart, &["stop", "start"]),
442
+ &mut |_| {
443
+ calls += 1;
444
+ Ok(CmdResult::from_json(receipt.clone(), true))
445
+ },
446
+ &mut |_, _, _| Ok(()),
447
+ )
448
+ .unwrap();
449
+ assert_eq!(calls, 1);
450
+ assert_eq!(result.exit, ExitCode::Error);
451
+ let CmdOutput::Json(value) = result.output else {
452
+ panic!()
453
+ };
454
+ assert_eq!(value["steps"][0]["phase"], "stop_not_confirmed");
455
+ assert_eq!(value["status"], "failed");
456
+ }
457
+ }
458
+
459
+ #[test]
460
+ fn syntax_session_matrix_and_directory_ambiguity_are_deterministic() {
461
+ let root = Sandbox::new();
462
+ std::fs::create_dir(root.path().join("worker")).unwrap();
463
+ for (command, verb, input, agent, team, expected) in [
464
+ (
465
+ "start",
466
+ Verb::Start,
467
+ vec!["worker"],
468
+ false,
469
+ false,
470
+ SessionPolicy::Preserve,
471
+ ),
472
+ (
473
+ "agent",
474
+ Verb::Restart,
475
+ vec!["worker"],
476
+ true,
477
+ false,
478
+ SessionPolicy::Auto,
479
+ ),
480
+ (
481
+ "team",
482
+ Verb::Restart,
483
+ vec![],
484
+ false,
485
+ true,
486
+ SessionPolicy::Auto,
487
+ ),
488
+ (
489
+ "restart",
490
+ Verb::Restart,
491
+ vec![],
492
+ false,
493
+ false,
494
+ SessionPolicy::Preserve,
495
+ ),
496
+ (
497
+ "restart",
498
+ Verb::Restart,
499
+ vec!["--session=auto"],
500
+ false,
501
+ false,
502
+ SessionPolicy::Auto,
503
+ ),
504
+ (
505
+ "agent",
506
+ Verb::Restart,
507
+ vec!["worker", "--session=preserve"],
508
+ true,
509
+ false,
510
+ SessionPolicy::Preserve,
511
+ ),
512
+ (
513
+ "start",
514
+ Verb::Start,
515
+ vec!["worker", "--allow-fresh"],
516
+ false,
517
+ false,
518
+ SessionPolicy::Auto,
519
+ ),
520
+ ] {
521
+ let intent =
522
+ lifecycle_intent(command, verb, &args(&input), root.path(), agent, team).unwrap();
523
+ assert_eq!(intent.session, expected);
524
+ if input.first() == Some(&"worker") {
525
+ assert!(matches!(intent.subject, Subject::Agent(_)));
526
+ }
527
+ }
528
+ assert!(lifecycle_intent(
529
+ "restart",
530
+ Verb::Restart,
531
+ &args(&["worker"]),
532
+ root.path(),
533
+ false,
534
+ false
535
+ )
536
+ .is_err());
537
+ assert!(lifecycle_intent(
538
+ "restart",
539
+ Verb::Restart,
540
+ &args(&["--session=preserve", "--allow-fresh"]),
541
+ root.path(),
542
+ false,
543
+ false
544
+ )
545
+ .is_err());
546
+ assert!(lifecycle_intent(
547
+ "start",
548
+ Verb::Start,
549
+ &args(&["worker", "--from=worker"]),
550
+ root.path(),
551
+ false,
552
+ false
553
+ )
554
+ .is_err());
555
+ std::fs::create_dir(root.path().join("agents")).unwrap();
556
+ std::fs::write(root.path().join("TEAM.md"), "---\nname: caller\n---\n").unwrap();
557
+ let path_intent = lifecycle_intent(
558
+ "restart",
559
+ Verb::Restart,
560
+ &args(&["./worker", "--dry-run"]),
561
+ root.path(),
562
+ false,
563
+ false,
564
+ )
565
+ .unwrap();
566
+ assert_eq!(path_intent.workspace, root.path().join("worker"));
567
+ assert!(path_intent.pinned_workspace);
568
+ let role = root.path().join("role.md");
569
+ std::fs::write(&role, "fixture").unwrap();
570
+ let tokens = vec![
571
+ "one".into(),
572
+ format!("--workspace={}", root.path().join("worker").display()),
573
+ "--role-file=role.md".into(),
574
+ ];
575
+ let intent =
576
+ lifecycle_intent("start", Verb::Start, &tokens, root.path(), false, false).unwrap();
577
+ assert_eq!(intent.role_file, Some(role));
578
+ assert!(lifecycle_intent(
579
+ "start",
580
+ Verb::Start,
581
+ &args(&["--from=role.md"]),
582
+ root.path(),
583
+ false,
584
+ false
585
+ )
586
+ .is_err());
587
+ assert!(lifecycle_intent(
588
+ "start",
589
+ Verb::Start,
590
+ &args(&["one", "--role-file=worker"]),
591
+ root.path(),
592
+ false,
593
+ false
594
+ )
595
+ .is_err());
596
+ }
597
+
598
+ #[test]
599
+ fn no_eligible_is_scoped_nonzero_with_skipped_and_zero_handlers_even_in_dry_run() {
600
+ for dry_run in [false, true] {
601
+ let mut p = plan(Verb::Start, &["start"]);
602
+ p.intent.dry_run = dry_run;
603
+ p.no_eligible = Some("all selected seats are paused");
604
+ p.skipped.push(json!({"agent":"one","reason":"paused"}));
605
+ let result = execute_plan(p, &mut |_| panic!("blocked handler"), &mut |_, _, _| {
606
+ panic!("blocked recheck")
607
+ })
608
+ .unwrap();
609
+ assert_eq!(result.exit, ExitCode::Error);
610
+ let CmdOutput::Json(value) = result.output else {
611
+ panic!()
612
+ };
613
+ assert_eq!(value["scope"]["team"], "pinned");
614
+ assert_eq!(value["status"], "no_eligible_agents");
615
+ assert_eq!(value["phase"], "planning");
616
+ assert_eq!(value["skipped"][0]["agent"], "one");
617
+ assert_eq!(value["skipped"][0]["reason"], "paused");
618
+ assert_eq!(value["steps"], json!([]));
619
+ assert_eq!(value["dry_run"], dry_run);
620
+ }
621
+ }
622
+
623
+ #[test]
624
+ fn accepted_pending_creation_keeps_original_receipt_and_actual_canonical_team() {
625
+ let mut p = plan(Verb::Start, &[]);
626
+ p.scope.team = None;
627
+ p.steps.push(Step::CreateTeam(QuickStartArgs {
628
+ workspace: PathBuf::from("/fixture"),
629
+ agents_dir: PathBuf::from("/fixture"),
630
+ name: None,
631
+ team_id: None,
632
+ yes: false,
633
+ json: true,
634
+ detail: true,
635
+ backend: None,
636
+ }));
637
+ let result = execute_plan(
638
+ p,
639
+ &mut |_| {
640
+ Ok(CmdResult::from_json(
641
+ json!({"ok":true,"status":"pending_tool_load","ready":false,"team":"actual-key"}),
642
+ true,
643
+ ))
644
+ },
645
+ &mut |_, _, _| Ok(()),
646
+ )
647
+ .unwrap();
648
+ let CmdOutput::Json(value) = result.output else {
649
+ panic!()
650
+ };
651
+ assert_eq!(value["status"], "pending");
652
+ assert_eq!(value["scope"]["team"], "actual-key");
653
+ assert_eq!(value["steps"][0]["receipt"]["ready"], false);
654
+ }