@team-agent/installer 0.5.86 → 0.5.91

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 (39) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +104 -164
  4. package/crates/team-agent/src/cli/emit.rs +68 -87
  5. package/crates/team-agent/src/cli/mod.rs +1 -1
  6. package/crates/team-agent/src/cli/spec.rs +1 -4
  7. package/crates/team-agent/src/cli/status_port/brief.rs +187 -7
  8. package/crates/team-agent/src/cli/status_port/inbox.rs +42 -17
  9. package/crates/team-agent/src/cli/tests/lane_c.rs +34 -26
  10. package/crates/team-agent/src/cli/tests/main_preserved.rs +4 -70
  11. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +0 -68
  12. package/crates/team-agent/src/cli/types.rs +3 -16
  13. package/crates/team-agent/src/compiler.rs +4 -7
  14. package/crates/team-agent/src/coordinator/tests/tick_core.rs +5 -6
  15. package/crates/team-agent/src/coordinator/tick.rs +1 -16
  16. package/crates/team-agent/src/coordinator/types.rs +1 -1
  17. package/crates/team-agent/src/db/message_store.rs +10 -5
  18. package/crates/team-agent/src/fake_worker.rs +4 -4
  19. package/crates/team-agent/src/lifecycle/launch/identity.rs +39 -21
  20. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
  21. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  22. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +3 -3
  23. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +2 -2
  24. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +0 -10
  25. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -9
  26. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -26
  27. package/crates/team-agent/src/main.rs +7 -2
  28. package/crates/team-agent/src/mcp_server/mod.rs +1 -1
  29. package/crates/team-agent/src/messaging/mod.rs +2 -2
  30. package/crates/team-agent/src/messaging/results.rs +234 -386
  31. package/crates/team-agent/src/messaging/tests/runtime.rs +91 -135
  32. package/crates/team-agent/src/messaging/watchers.rs +1 -1
  33. package/crates/team-agent/src/model/enums.rs +17 -3
  34. package/crates/team-agent/src/model/spec.rs +8 -7
  35. package/crates/team-agent/src/provider/wire.rs +1 -1
  36. package/package.json +4 -4
  37. package/skills/team-agent/SKILL.md +3 -3
  38. package/skills/team-agent/command-coverage.json +2 -9
  39. package/skills/team-agent/references/team-agent-operator.md +661 -0
@@ -208,7 +208,7 @@ fn inbox_returns_stored_message_for_recipient() {
208
208
  let mid = store
209
209
  .create_message(None, "leader", "w1", "hello w1", None, true, None)
210
210
  .unwrap();
211
- let v = status_port::inbox(&ws, "w1", 20, None, true, None).expect("inbox");
211
+ let v = status_port::inbox(&ws, "w1", 20, None).expect("inbox");
212
212
  let messages = v["messages"].as_array().expect("messages array");
213
213
  assert_eq!(
214
214
  messages.len(),
@@ -223,30 +223,12 @@ fn inbox_returns_stored_message_for_recipient() {
223
223
  );
224
224
  assert_eq!(m["recipient"], json!("w1"));
225
225
  assert_eq!(m["sender"], json!("leader"));
226
- assert_eq!(m["content"], json!("hello w1"));
226
+ assert_eq!(m["summary"], json!("hello w1"));
227
227
  assert_eq!(
228
228
  m["status"],
229
229
  json!("accepted"),
230
230
  "create_message persists status='accepted'"
231
231
  );
232
- // NULL owner_team_id semantics: status.inbox() calls MessageStore.inbox(agent) with
233
- // owner_team_id=None (no team clause), so a NULL-owner message MUST surface for its recipient.
234
- assert_eq!(
235
- m["owner_team_id"],
236
- json!(null),
237
- "the stored message's owner_team_id is NULL and still returned"
238
- );
239
- // byte-faithful raw-row columns: requires_ack is the 0/1 INT; artifact_refs the literal text "[]".
240
- assert_eq!(
241
- m["requires_ack"],
242
- json!(1),
243
- "requires_ack is the 0/1 int, not a bool"
244
- );
245
- assert_eq!(
246
- m["artifact_refs"],
247
- json!("[]"),
248
- "artifact_refs is the raw text column, not parsed"
249
- );
250
232
  let _ = std::fs::remove_dir_all(&ws);
251
233
  }
252
234
  // ── BUG-2 (match scope) — inbox(agent) returns rows where sender==agent OR recipient==agent, and
@@ -265,11 +247,11 @@ fn inbox_matches_sender_or_recipient_and_excludes_others() {
265
247
  store
266
248
  .create_message(None, "leader", "w2", "unrelated to w2", None, true, None)
267
249
  .unwrap();
268
- let v = status_port::inbox(&ws, "w1", 20, None, true, None).expect("inbox");
250
+ let v = status_port::inbox(&ws, "w1", 20, None).expect("inbox");
269
251
  let messages = v["messages"].as_array().expect("messages array");
270
252
  let mut contents: Vec<String> = messages
271
253
  .iter()
272
- .map(|m| m["content"].as_str().unwrap().to_string())
254
+ .map(|m| m["summary"].as_str().unwrap().to_string())
273
255
  .collect();
274
256
  contents.sort();
275
257
  assert_eq!(
@@ -691,54 +673,6 @@ fn remove_agent_from_spec_refusal_is_not_success_envelope() {
691
673
  );
692
674
  }
693
675
  #[test]
694
- fn collect_uncollected_result_marks_db_and_outputs_result() {
695
- let ws = tmp_workspace();
696
- seed_collect_state(&ws);
697
- seed_uncollected_result(&ws, "res_collect_red");
698
- let out = json_output(
699
- cmd_collect(&CollectArgs {
700
- workspace: ws.clone(),
701
- result_file: None,
702
- json: true,
703
- team: None,
704
- })
705
- .unwrap(),
706
- );
707
- assert_eq!(out["ok"], json!(true));
708
- assert_eq!(
709
- out["collected_results"][0]["result_id"],
710
- json!("res_collect_red")
711
- );
712
- assert_eq!(out["collected_results"][0]["scope"], json!("task"));
713
- assert_eq!(
714
- out["results"],
715
- json!({"total": 1, "uncollected": 0, "collected": 1, "invalid": 0, "by_status": {}})
716
- );
717
- let store = crate::message_store::MessageStore::open(&ws).unwrap();
718
- let conn = crate::db::schema::open_db(store.db_path()).unwrap();
719
- let status: String = conn
720
- .query_row(
721
- "select status from results where result_id = 'res_collect_red'",
722
- [],
723
- |row| row.get(0),
724
- )
725
- .unwrap();
726
- assert_eq!(status, "collected");
727
- let state = read_state(&ws);
728
- assert_eq!(state["tasks"][0]["status"], json!("done"));
729
- assert_eq!(
730
- state["tasks"][0]["accepted_result_id"],
731
- json!("res_collect_red")
732
- );
733
- assert!(
734
- read_events(&ws)
735
- .iter()
736
- .any(|e| e["event"] == json!("collect.result")
737
- && e["result_id"] == json!("res_collect_red")),
738
- "collect must emit collect.result for the stored result"
739
- );
740
- }
741
- #[test]
742
676
  fn stuck_cancel_persists_suppression_and_stuck_list_reads_state() {
743
677
  let ws = tmp_workspace();
744
678
  seed_collect_state(&ws);
@@ -275,36 +275,6 @@ fn seed_team_spec(ws: &std::path::Path) {
275
275
  std::fs::write(ws.join("team.spec.yaml"), spec).unwrap();
276
276
  }
277
277
 
278
- fn seed_collect_runtime_state(ws: &std::path::Path) {
279
- crate::state::persist::save_runtime_state(
280
- ws,
281
- &json!({
282
- "active_team_key": "fake-e2e",
283
- "team_dir": ws.to_string_lossy().to_string(),
284
- "spec_path": ws.join("team.spec.yaml").to_string_lossy().to_string(),
285
- "session_name": "team-agent-fake-e2e",
286
- "leader": {"id": "leader"},
287
- "agents": {
288
- "fake_impl": {
289
- "status": "running",
290
- "provider": "fake",
291
- "role": "implementation_engineer",
292
- "window": "fake_impl",
293
- "owner_team_id": "fake-e2e"
294
- }
295
- },
296
- "tasks": [{
297
- "id": "task_impl",
298
- "title": "Fake implementation",
299
- "type": "implementation",
300
- "status": "pending",
301
- "assignee": "fake_impl"
302
- }]
303
- }),
304
- )
305
- .unwrap();
306
- }
307
-
308
278
  // ── sessions ── golden cli/parser.py:230 `cmd_sessions` -> runtime.sessions(ws). EXIT 0.
309
279
  // `team-agent sessions --workspace <ws> --json` on an empty ws ->
310
280
  // {"ok":true,"sessions":[],"workspace":"<ws>"} (--json sort_keys). RED: unrouted -> Error.
@@ -324,44 +294,6 @@ fn dispatch_routes_sessions_subcommand() {
324
294
  let _ = std::fs::remove_dir_all(&ws);
325
295
  }
326
296
 
327
- // ── collect ── golden parser.py:292 `cmd_collect` -> runtime.collect(ws). With a valid
328
- // team.spec.yaml present and nothing to collect -> EXIT 0, golden:
329
- // {"collected":[],"collected_results":[],"coordinator":{"ok":false,"status":"not_required"},
330
- // "delivered_messages":[],"invalid_results":[],"ok":true,"results":{...},"state_file":"<ws>/team_state.md"}
331
- // RED: unrouted -> Error.
332
- #[test]
333
- #[serial(env)]
334
- fn dispatch_routes_collect_with_spec() {
335
- let _env = EnvUnsetGuard::unset(&[
336
- "TEAM_AGENT_WORKSPACE",
337
- "TEAM_AGENT_TEAM_ID",
338
- "TEAM_AGENT_OWNER_TEAM_ID",
339
- "TEAM_AGENT_ACTIVE_TEAM",
340
- "TEAM_AGENT_ID",
341
- "TEAM_AGENT_LEADER_PANE_ID",
342
- "TEAM_AGENT_LEADER_SESSION_UUID",
343
- "TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
344
- "TEAM_AGENT_LEADER_PROVIDER",
345
- "TMUX",
346
- "TMUX_PANE",
347
- ]);
348
- let ws = tmp_workspace();
349
- let _cleanup = WorkspaceCleanup(ws.clone());
350
- seed_team_spec(&ws);
351
- seed_collect_runtime_state(&ws);
352
- let code = run(
353
- &cli_argv(&["collect", "--workspace", &ws.to_string_lossy(), "--json"]),
354
- &ws,
355
- );
356
- assert_eq!(
357
- code,
358
- ExitCode::Ok,
359
- "`collect` must ROUTE to cmd_collect (parser.py:292); with a valid spec golden exits 0 \
360
- {{collected,collected_results,coordinator,delivered_messages,invalid_results,ok,results,state_file}}; \
361
- today -> unknown-subcommand Error"
362
- );
363
- }
364
-
365
297
  // ── diagnose ── attached is host-registry authority, not state.json self-sign.
366
298
  // Seed leader_receiver=attached + a ~/.team-agent/leaders/ row for this workspace
367
299
  // + NO session_name + NO agents -> issues=[] -> EXIT 0.
@@ -44,10 +44,10 @@ pub enum CliError {
44
44
  /// state 解析失败(歧义/未找到 team 等)。透传 step 5。
45
45
  #[error("{0}")]
46
46
  State(#[from] crate::state::StateError),
47
- /// messaging 委派失败(send/collect/stuck)。透传 step 11。
47
+ /// messaging 委派失败(send/stuck)。透传 step 11。
48
48
  #[error("{0}")]
49
49
  Messaging(crate::messaging::MessagingError),
50
- /// I/O(cli-error 落盘、inbox 游标读写)。bug-084:写路径降级,不裸 panic。
50
+ /// I/O(cli-error 落盘)。bug-084:写路径降级,不裸 panic。
51
51
  #[error("io: {0}")]
52
52
  Io(#[from] std::io::Error),
53
53
  /// JSON 解析。
@@ -407,13 +407,12 @@ pub struct ApprovalsArgs {
407
407
  pub team: Option<String>,
408
408
  }
409
409
 
410
- /// `inbox`(`parser.py:217`)。`--since` ISO8601(claim-leader inbox_hint 复用)。
410
+ /// `inbox`(`parser.py:217`)。极简消息兜底查看器。
411
411
  #[derive(Debug, Clone, PartialEq, Eq)]
412
412
  pub struct InboxArgs {
413
413
  pub agent: String,
414
414
  pub workspace: PathBuf,
415
415
  pub limit: usize,
416
- pub since: Option<String>,
417
416
  pub json: bool,
418
417
  /// Stage 4: explicit `--team` scope.
419
418
  pub team: Option<String>,
@@ -682,18 +681,6 @@ pub struct ProfileArgs {
682
681
  pub json: bool,
683
682
  }
684
683
 
685
- /// `collect`(`parser.py:292`)。
686
- #[derive(Debug, Clone, PartialEq, Eq)]
687
- pub struct CollectArgs {
688
- pub workspace: PathBuf,
689
- pub result_file: Option<PathBuf>,
690
- pub json: bool,
691
- /// Stage 4: explicit `--team` scope. Destructive (consumes result
692
- /// envelopes); CLI dispatch refuses bare invocation in a
693
- /// multi-alive-team workspace.
694
- pub team: Option<String>,
695
- }
696
-
697
684
  /// Read-only lookup of all stored result envelopes for one case.
698
685
  #[derive(Debug, Clone, PartialEq, Eq)]
699
686
  pub struct ResultsArgs {
@@ -32,8 +32,8 @@ use crate::model::enums::{Provider, ProviderEffort};
32
32
  use crate::model::yaml::Value;
33
33
  use crate::model::{paths, spec, yaml, ModelError};
34
34
  use crate::provider::wire::{
35
- builtin_provider_model as wire_builtin_provider_model, is_claude_family,
36
- parse_canonical_provider, provider_model_keys,
35
+ builtin_provider_model as wire_builtin_provider_model, parse_canonical_provider,
36
+ provider_model_keys,
37
37
  };
38
38
 
39
39
  pub const IGNORED_OWNER_TEAM_ID_FIELD: &str = "owner_team_id";
@@ -602,12 +602,9 @@ fn compile_role_agent_with_mode(
602
602
  })
603
603
  .unwrap_or("");
604
604
  let provider_enum = parse_canonical_provider(provider_str).unwrap_or(Provider::Codex);
605
- if effort.is_claude_only()
606
- && !is_claude_family(provider_enum)
607
- && provider_enum != Provider::Pi
608
- {
605
+ if let Err(reason) = effort.resolve_for_provider(provider_enum) {
609
606
  return Err(ModelError::Validation(format!(
610
- "{}: effort '{}' is only supported by claude/claude_code (provider: {provider_str})",
607
+ "{}: {reason} (effort: {}; provider: {provider_str})",
611
608
  role_path.display(),
612
609
  effort.as_str()
613
610
  )));
@@ -82,7 +82,7 @@ fn tick_side_effect_order_is_the_fixed_sequence() {
82
82
  // startup_prompts -> runtime_prompts -> sync_health -> deliver_pending ->
83
83
  // fire_scheduled -> detect_stuck -> record_unknown_idle -> evaluate_takeover ->
84
84
  // detect_deadlocks -> detect_compaction -> detect_drift -> detect_api_errors ->
85
- // ATOMIC_save (bug-084 wrap) -> collect_results -> prune_dedupe_log.
85
+ // ATOMIC_save (bug-084 wrap) -> prune_dedupe_log.
86
86
  // The porter must push each step name into the injected recorder at its call site.
87
87
  let recorder: OrderRecorder = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
88
88
  let (coord, _calls) = coord_for_test(true, None, Some(std::sync::Arc::clone(&recorder)));
@@ -106,19 +106,18 @@ fn tick_side_effect_order_is_the_fixed_sequence() {
106
106
  "detect_drift",
107
107
  "detect_api_errors",
108
108
  "atomic_save",
109
- "collect_results",
110
109
  "prune_dedupe_log",
111
110
  ];
112
111
  assert_eq!(
113
112
  order, expected,
114
113
  "tick side-effect ORDER must match the fixed sequence"
115
114
  );
116
- // ATOMIC save is the LAST mutation before read-only collect/prune (bug-084 wrap point).
115
+ // ATOMIC save is the LAST mutation before the read-only prune (bug-084 wrap point).
117
116
  let save_idx = order.iter().position(|s| *s == "atomic_save").unwrap();
118
- let collect_idx = order.iter().position(|s| *s == "collect_results").unwrap();
117
+ let prune_idx = order.iter().position(|s| *s == "prune_dedupe_log").unwrap();
119
118
  assert!(
120
- save_idx < collect_idx,
121
- "save precedes collect (bug-084 wrap is the last mutation)"
119
+ save_idx < prune_idx,
120
+ "save precedes prune (bug-084 wrap is the last mutation)"
122
121
  );
123
122
  }
124
123
 
@@ -104,7 +104,7 @@ pub struct TickReport {
104
104
  pub session_drift: Vec<SessionDriftResult>,
105
105
  /// `detect_leader_api_errors`(`lifecycle.py:344`)——cross-dep step 11。
106
106
  pub api_errors: Vec<LeaderApiError>,
107
- /// `_collect_results_and_notify_watchers`(`lifecycle.py:364`)——degraded 时为空(未走到)。
107
+ /// Reserved result projection slot; report_result now finalizes directly.
108
108
  pub results: Vec<CollectedResult>,
109
109
  }
110
110
 
@@ -528,12 +528,6 @@ impl Coordinator {
528
528
  collections,
529
529
  ));
530
530
  }
531
- self.record_step(TickStepGroup::Delivery, "collect_results");
532
- collections.results =
533
- collect_results(crate::messaging::collect_results_and_notify_watchers(
534
- self.workspace.as_path(),
535
- &event_log,
536
- )?);
537
531
  self.record_step(TickStepGroup::Persist, "prune_dedupe_log");
538
532
  Ok(base_tick_report(true, false, None, Some(true), collections))
539
533
  }
@@ -1554,15 +1548,6 @@ fn empty_tick_report(
1554
1548
  base_tick_report(ok, stop, reason, persisted, TickCollections::default())
1555
1549
  }
1556
1550
 
1557
- fn collect_results(value: Value) -> Vec<CollectedResult> {
1558
- let Some(result_id) = value.get("result_id").and_then(Value::as_str) else {
1559
- return Vec::new();
1560
- };
1561
- vec![CollectedResult {
1562
- result_id: result_id.to_string(),
1563
- }]
1564
- }
1565
-
1566
1551
  struct ProviderTurnClassifier;
1567
1552
 
1568
1553
  impl TurnStateClassifier for ProviderTurnClassifier {
@@ -543,7 +543,7 @@ pub struct LeaderApiError {
543
543
  pub message: String,
544
544
  }
545
545
 
546
- /// `_collect_results_and_notify_watchers` 结果(`results.py:430`,step 11 拥有)。**PLACEHOLDER**。
546
+ /// Result projection reported by a coordinator tick. Report results now finalize inline.
547
547
  #[derive(Debug, Clone, PartialEq, Eq)]
548
548
  pub struct CollectedResult {
549
549
  pub result_id: String,
@@ -727,9 +727,11 @@ impl MessageStore {
727
727
  Ok(changed == 1)
728
728
  }
729
729
 
730
- /// Read inbox rows for an agent. This projection intentionally has no owner-team
731
- /// filter when the caller does not provide one: legacy/CLI inbox must surface
732
- /// NULL-owner messages stored for the agent.
730
+ /// Read ordinary inbox rows for an agent. Leader stage-result notifications are
731
+ /// delivered through the leader receiver, not exposed as mailbox fallback rows.
732
+ /// This projection intentionally has no owner-team filter when the caller does
733
+ /// not provide one: legacy/CLI inbox must surface NULL-owner messages stored for
734
+ /// the agent.
733
735
  pub fn inbox(
734
736
  &self,
735
737
  agent_id: &str,
@@ -744,7 +746,9 @@ impl MessageStore {
744
746
  status, content, artifact_refs, created_at, updated_at, delivered_at,
745
747
  acknowledged_at, error, delivery_attempts
746
748
  from messages
747
- where (sender = ?1 or recipient = ?1) and owner_team_id = ?3
749
+ where (sender = ?1 or recipient = ?1)
750
+ and owner_team_id = ?3
751
+ and coalesce(json_extract(presentation, '$.class'), 'message') != 'stage_result'
748
752
  order by created_at desc
749
753
  limit ?2"
750
754
  }
@@ -753,7 +757,8 @@ impl MessageStore {
753
757
  status, content, artifact_refs, created_at, updated_at, delivered_at,
754
758
  acknowledged_at, error, delivery_attempts
755
759
  from messages
756
- where sender = ?1 or recipient = ?1
760
+ where (sender = ?1 or recipient = ?1)
761
+ and coalesce(json_extract(presentation, '$.class'), 'message') != 'stage_result'
757
762
  order by created_at desc
758
763
  limit ?2"
759
764
  }
@@ -220,8 +220,8 @@ mod tests {
220
220
  let (task_id, agent_id, status, env) = stored_result(ws);
221
221
  assert_eq!(
222
222
  (task_id.as_str(), agent_id.as_str(), status.as_str()),
223
- ("t1", "w1", "success"),
224
- "stored result row must be task_id=t1 agent_id=w1 status=success"
223
+ ("t1", "w1", "collected"),
224
+ "stored result row must be task_id=t1 agent_id=w1 status=collected"
225
225
  );
226
226
  assert_eq!(
227
227
  env["schema_version"],
@@ -291,8 +291,8 @@ mod tests {
291
291
  let (task_id, agent_id, status, envelope) = stored_result(&ws);
292
292
  assert_eq!(
293
293
  (task_id.as_str(), agent_id.as_str(), status.as_str()),
294
- ("m1", "w1", "success"),
295
- "a direct-message result must use its message id for message-scope collection"
294
+ ("m1", "w1", "collected"),
295
+ "a direct-message result must use its message id for message-scope auto-finalization"
296
296
  );
297
297
  assert_ne!(
298
298
  envelope["task_id"],
@@ -9,7 +9,7 @@
9
9
  //! - name: transport_has_session
10
10
  //! what: 探测 tmux session 是否存在,探测本身出错一律当不存在
11
11
  //! - name: provider_effort_from_raw
12
- //! what: 解析 effort 并要求该 provider 支持,否则 None
12
+ //! what: 解析 effort,保留兼容忽略项并拒绝非法 provider 组合
13
13
  //! - name: quick_start_depth_guard
14
14
  //! what: 定出本次 quick-start 的父团队与层级,推不出父团队时拒绝
15
15
  //! - name: annotate_persisted_team_depth
@@ -135,24 +135,24 @@ pub(super) fn parse_auth_mode(raw: &str) -> Option<AuthMode> {
135
135
  /// purpose: 从原始字符串解析 effort,并要求当前 provider 支持它
136
136
  /// params:
137
137
  /// raw: effort 原文,None 或空白返回 None
138
- /// returns: 解析成功且该 provider 支持时返回该档位,否则 None
138
+ /// returns: 支持的档位或兼容忽略的 None;已知非法 provider/effort 组合返回错误
139
139
  /// ---
140
140
  /// 0.4.x provider effort MVP step 4: low-level from a raw string. Returns
141
- /// `Some(effort)` when the level parses AND the provider supports it.
141
+ /// `Some(effort)` for supported levels, None for legacy ignored values,
142
+ /// and an error for provider/effort combinations rejected by configuration.
142
143
  pub(crate) fn provider_effort_from_raw(
143
144
  raw: Option<&str>,
144
145
  provider: Provider,
145
- ) -> Option<ProviderEffort> {
146
- let raw = raw?.trim();
147
- if raw.is_empty() {
148
- return None;
149
- }
150
- let effort = ProviderEffort::parse(raw)?;
151
- if effort.is_supported_by(provider) {
152
- Some(effort)
153
- } else {
154
- None
155
- }
146
+ ) -> Result<Option<ProviderEffort>, LifecycleError> {
147
+ let Some(effort) = raw.and_then(ProviderEffort::parse) else {
148
+ return Ok(None);
149
+ };
150
+ effort.resolve_for_provider(provider).map_err(|reason| {
151
+ LifecycleError::RequirementUnmet(format!(
152
+ "{reason} (effort: {}; provider: {provider:?})",
153
+ effort.as_str()
154
+ ))
155
+ })
156
156
  }
157
157
 
158
158
  /// ---
@@ -171,7 +171,7 @@ pub(crate) fn provider_effort_event_payload(
171
171
  return None;
172
172
  }
173
173
  let effort = ProviderEffort::parse(raw)?;
174
- if effort.is_supported_by(provider) {
174
+ if !matches!(effort.resolve_for_provider(provider), Ok(None)) {
175
175
  return None;
176
176
  }
177
177
  Some(serde_json::json!({
@@ -221,7 +221,7 @@ pub(crate) fn extend_worker_env_unset_for_effort(
221
221
  pub(crate) fn provider_effort_for_spawn(
222
222
  agent: &crate::model::yaml::Value,
223
223
  provider: Provider,
224
- ) -> Option<ProviderEffort> {
224
+ ) -> Result<Option<ProviderEffort>, LifecycleError> {
225
225
  provider_effort_from_raw(agent.get("effort").and_then(|v| v.as_str()), provider)
226
226
  }
227
227
 
@@ -252,7 +252,7 @@ pub(crate) fn provider_effort_event_if_dropped(
252
252
  pub(crate) fn provider_effort_for_spawn_json(
253
253
  agent: &serde_json::Value,
254
254
  provider: Provider,
255
- ) -> Option<ProviderEffort> {
255
+ ) -> Result<Option<ProviderEffort>, LifecycleError> {
256
256
  provider_effort_from_raw(
257
257
  agent.get("effort").and_then(serde_json::Value::as_str),
258
258
  provider,
@@ -307,10 +307,16 @@ pub(super) fn quick_start_depth_guard(
307
307
  requested_team: Option<&str>,
308
308
  _strict_real_runtime: bool,
309
309
  ) -> Result<QuickStartDepth, LifecycleError> {
310
- let env_parent = std::env::var("TEAM_AGENT_OWNER_TEAM_ID")
311
- .ok()
312
- .map(|value| value.trim().to_string())
313
- .filter(|value| !value.is_empty());
310
+ // OWNER_TEAM_ID is injected into every worker process. It is parent
311
+ // context only when the target quick-start workspace is the same workspace
312
+ // that produced the environment; otherwise a worker launching a fresh
313
+ // external workspace would inherit a false nesting parent.
314
+ let env_parent = owner_context_matches_workspace(workspace).then(|| {
315
+ std::env::var("TEAM_AGENT_OWNER_TEAM_ID")
316
+ .ok()
317
+ .map(|value| value.trim().to_string())
318
+ .filter(|value| !value.is_empty())
319
+ }).flatten();
314
320
  let parent = env_parent;
315
321
  let Some(parent) = parent else {
316
322
  let state = crate::state::persist::load_runtime_state(workspace)
@@ -365,6 +371,18 @@ pub(super) fn quick_start_depth_guard(
365
371
  /// purpose: 从 runtime state 推断父团队键
366
372
  /// returns: 活跃团队键,且该团队确有 running 席位时返回它,否则 None
367
373
  /// ---
374
+ fn owner_context_matches_workspace(workspace: &Path) -> bool {
375
+ let Some(env_workspace) = std::env::var_os("TEAM_AGENT_WORKSPACE") else {
376
+ return false;
377
+ };
378
+ let env_workspace = PathBuf::from(env_workspace);
379
+ env_workspace == workspace
380
+ || std::fs::canonicalize(&env_workspace)
381
+ .ok()
382
+ .zip(std::fs::canonicalize(workspace).ok())
383
+ .is_some_and(|(env_path, target_path)| env_path == target_path)
384
+ }
385
+
368
386
  pub(super) fn infer_parent_team_from_active_state(state: &serde_json::Value) -> Option<String> {
369
387
  let active = explicit_active_team_key(state)?;
370
388
  let team = state
@@ -152,7 +152,7 @@ pub(super) fn spawn_agents(
152
152
  // 0.4.x provider effort MVP step 4 + 7: resolve effort and emit
153
153
  // unsupported warning event when the spec asked for an effort the
154
154
  // provider can't satisfy.
155
- let agent_effort = provider_effort_for_spawn(agent, provider);
155
+ let agent_effort = provider_effort_for_spawn(agent, provider)?;
156
156
  if let Some(event_value) = provider_effort_event_if_dropped(agent, provider, agent_id_raw) {
157
157
  let _ = crate::event_log::EventLog::new(workspace)
158
158
  .write("provider.effort_unsupported", event_value);
@@ -355,7 +355,7 @@ pub(super) fn spawn_agent_window(
355
355
  let command_model = profile_launch.command_overrides.model.as_deref().or(model);
356
356
  // 0.4.x provider effort MVP: restart/resume preserves effort from the
357
357
  // persisted agent JSON (state's agent["effort"] field, set by launch).
358
- let restart_effort = crate::lifecycle::launch::provider_effort_for_spawn_json(agent, provider);
358
+ let restart_effort = crate::lifecycle::launch::provider_effort_for_spawn_json(agent, provider)?;
359
359
  if let Some(event_value) = crate::lifecycle::launch::provider_effort_event_if_dropped_json(
360
360
  agent,
361
361
  provider,
@@ -99,17 +99,17 @@ fn stale_snapshot_cannot_flip_status_or_diagnose_ok_readiness() {
99
99
  let case = B0HideOneCase::new("status-diagnose-hide-one");
100
100
  let root = case.current_state();
101
101
  save_runtime_state(&case.workspace, &root).expect("seed current root state");
102
- let probe = brief_probe::install_trusted_nodeprobe(
102
+ let tmux = brief_probe::install_native_tmux(
103
103
  &case.workspace.join("probe"),
104
104
  NEW_ENDPOINT,
105
105
  SESSION,
106
106
  WORKER,
107
107
  "%new",
108
- "fake",
108
+ "bash",
109
109
  );
110
110
  let _path = case.env.with_env(
111
111
  "PATH",
112
- &brief_probe::path_with_probe(&probe, std::env::var_os("PATH").as_deref()),
112
+ &brief_probe::path_with_probe(&tmux, std::env::var_os("PATH").as_deref()),
113
113
  );
114
114
 
115
115
  let status_before = case.run_json(&[
@@ -255,8 +255,8 @@ fn t3_unknown_business_status_is_preserved_without_normalized_event() {
255
255
  let envelope: serde_json::Value =
256
256
  serde_json::from_str(&row.envelope).expect("T3-1 durable envelope JSON");
257
257
  assert_eq!(
258
- row.status, status,
259
- "T3-1 ingress: results.status must preserve the task-local value byte-for-byte"
258
+ row.status, "collected",
259
+ "T3-1 ingress: report_result auto-finalization must mark the durable row collected"
260
260
  );
261
261
  assert_eq!(
262
262
  envelope["status"],
@@ -410,16 +410,6 @@ fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_coll
410
410
  "status should normalize teamdir to project-root runtime state: {status:?}"
411
411
  );
412
412
 
413
- let collect = crate::cli::cmd_collect(&crate::cli::CollectArgs {
414
- result_file: None,
415
- workspace: team.clone(),
416
- json: true,
417
- team: None,
418
- });
419
- assert!(
420
- collect.is_ok(),
421
- "collect should normalize teamdir to the same project-root state/spec: {collect:?}"
422
- );
423
413
  }
424
414
 
425
415
  #[test]
@@ -1,8 +1,8 @@
1
1
  use super::launch_spawn::seed_healthy_coordinator;
2
2
  use super::*;
3
3
  use crate::cli::{
4
- cmd_collect, cmd_diagnose, cmd_send, cmd_status, lifecycle_port, CmdOutput, CmdResult,
5
- CollectArgs, DiagnoseArgs, SendArgs, StatusArgs,
4
+ cmd_diagnose, cmd_send, cmd_status, lifecycle_port, CmdOutput, CmdResult, DiagnoseArgs,
5
+ SendArgs, StatusArgs,
6
6
  };
7
7
  use crate::transport::test_support::OfflineTransport;
8
8
  use crate::transport::WindowName;
@@ -275,12 +275,6 @@ fn run_phase_golden(spec: PhaseGolden) -> Value {
275
275
  to_name: None,
276
276
  to_leader: None,
277
277
  });
278
- let collect = cmd_collect(&CollectArgs {
279
- workspace: workspace.clone(),
280
- result_file: None,
281
- json: true,
282
- team: Some(spec.team_key.to_string()),
283
- });
284
278
  let lifecycle_transport = codex_ready_transport()
285
279
  .with_session_present(true)
286
280
  .with_windows(vec![WindowName::new("w1"), WindowName::new("w2")]);
@@ -303,7 +297,6 @@ fn run_phase_golden(spec: PhaseGolden) -> Value {
303
297
  { "step": "status-json", "result": cmd_value(status_compact) },
304
298
  { "step": "status-detail-json", "result": cmd_value(status_detail) },
305
299
  { "step": "send", "result": cmd_value(send) },
306
- { "step": "collect", "result": cmd_value(collect) },
307
300
  { "step": "phase-lifecycle-op", "output": lifecycle },
308
301
  {
309
302
  "step": "shutdown-keep-logs",