@team-agent/installer 0.5.100 → 0.5.102

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 (63) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/diagnose.rs +29 -7
  4. package/crates/team-agent/src/cli/mod.rs +24 -8
  5. package/crates/team-agent/src/cli/tests/main_preserved.rs +0 -74
  6. package/crates/team-agent/src/compiler/tests.rs +20 -23
  7. package/crates/team-agent/src/compiler.rs +52 -65
  8. package/crates/team-agent/src/coordinator/tests/s3_bounded.rs +16 -48
  9. package/crates/team-agent/src/leader/start.rs +1 -14
  10. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +211 -16
  11. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +1 -0
  12. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +15 -5
  13. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +89 -6
  14. package/crates/team-agent/src/lifecycle/launch/role_source.rs +168 -32
  15. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
  16. package/crates/team-agent/src/lifecycle/launch.rs +11 -3
  17. package/crates/team-agent/src/lifecycle/profile_launch.rs +23 -8
  18. package/crates/team-agent/src/lifecycle/restart/agent.rs +2 -2
  19. package/crates/team-agent/src/lifecycle/restart/avatar.rs +1806 -0
  20. package/crates/team-agent/src/lifecycle/restart/common.rs +115 -2
  21. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +8 -2
  22. package/crates/team-agent/src/lifecycle/restart/selection.rs +14 -2
  23. package/crates/team-agent/src/lifecycle/restart.rs +11 -4
  24. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +0 -28
  25. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +11 -9
  26. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +8 -401
  27. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +2 -2
  28. package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1398 -0
  29. package/crates/team-agent/src/lifecycle/tests.rs +1 -0
  30. package/crates/team-agent/src/lifecycle/types.rs +10 -2
  31. package/crates/team-agent/src/lifecycle/worker_command_context.rs +22 -8
  32. package/crates/team-agent/src/mcp_server/helpers.rs +0 -16
  33. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +2 -7
  34. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -127
  35. package/crates/team-agent/src/mcp_server/mod.rs +7 -12
  36. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -132
  37. package/crates/team-agent/src/mcp_server/tests/send.rs +1 -93
  38. package/crates/team-agent/src/mcp_server/tests/tools.rs +0 -61
  39. package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -51
  40. package/crates/team-agent/src/mcp_server/tests.rs +4 -200
  41. package/crates/team-agent/src/mcp_server/tools.rs +5 -396
  42. package/crates/team-agent/src/mcp_server/types.rs +3 -36
  43. package/crates/team-agent/src/mcp_server/wire.rs +2 -223
  44. package/crates/team-agent/src/messaging/leader_receiver.rs +1 -1
  45. package/crates/team-agent/src/messaging/mod.rs +2 -2
  46. package/crates/team-agent/src/messaging/scheduler.rs +2 -205
  47. package/crates/team-agent/src/messaging/tests/basic.rs +0 -31
  48. package/crates/team-agent/src/messaging/tests/main_preserved.rs +0 -73
  49. package/crates/team-agent/src/messaging/tests/runtime.rs +0 -26
  50. package/crates/team-agent/src/messaging/types.rs +0 -20
  51. package/crates/team-agent/src/model/spec.rs +15 -18
  52. package/crates/team-agent/src/provider/types.rs +2 -0
  53. package/crates/team-agent/src/state/persist.rs +58 -2
  54. package/crates/team-agent/src/state/projection.rs +5 -1
  55. package/crates/team-agent/src/state/repository/tests.rs +1 -16
  56. package/crates/team-agent/src/state/repository.rs +126 -37
  57. package/crates/team-agent/src/transport/test_support.rs +23 -3
  58. package/package.json +4 -4
  59. package/skills/team-agent/SKILL.md +18 -47
  60. package/skills/team-agent/command-coverage.json +7 -0
  61. package/skills/team-agent/references/team-agent-operator.md +28 -118
  62. package/skills/team-agent/references/team-in-team.md +4 -19
  63. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +0 -484
package/Cargo.lock CHANGED
@@ -581,7 +581,7 @@ dependencies = [
581
581
 
582
582
  [[package]]
583
583
  name = "team-agent"
584
- version = "0.5.100"
584
+ version = "0.5.102"
585
585
  dependencies = [
586
586
  "anyhow",
587
587
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.100"
12
+ version = "0.5.102"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -94,6 +94,14 @@ pub(crate) fn classify_tmux_server_error(error_text: &str) -> &'static str {
94
94
  }
95
95
 
96
96
  pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value, Value) {
97
+ diagnose_runtime_with_workspace(None, state, backend)
98
+ }
99
+
100
+ fn diagnose_runtime_with_workspace(
101
+ workspace: Option<&std::path::Path>,
102
+ state: &Value,
103
+ backend: &dyn Transport,
104
+ ) -> (Value, Value) {
97
105
  let mut issues = Vec::new();
98
106
  let mut repairs = Vec::new();
99
107
 
@@ -248,12 +256,25 @@ pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value
248
256
  .and_then(Value::as_str)
249
257
  .filter(|path| !path.is_empty())
250
258
  .map(crate::provider::RolloutPath::new);
251
- let identity_probe =
252
- crate::lifecycle::restart::session_identity_probe_for_agent(
253
- &crate::model::ids::AgentId::new(agent_id.clone()),
254
- provider,
255
- rollout_path.as_ref(),
256
- );
259
+ let agent_identity = crate::model::ids::AgentId::new(agent_id.clone());
260
+ let identity_probe = workspace
261
+ .map(|workspace| {
262
+ crate::lifecycle::restart::session_identity_probe_for_agent_with_fork_ancestry(
263
+ workspace,
264
+ state,
265
+ &agent_identity,
266
+ agent_state,
267
+ provider,
268
+ rollout_path.as_ref(),
269
+ )
270
+ })
271
+ .unwrap_or_else(|| {
272
+ crate::lifecycle::restart::session_identity_probe_for_agent(
273
+ &agent_identity,
274
+ provider,
275
+ rollout_path.as_ref(),
276
+ )
277
+ });
257
278
  if identity_probe.identity_ok == Some(false) {
258
279
  let issue = format!("session_identity_mismatch:{agent_id}");
259
280
  issues.push(json!(issue));
@@ -304,7 +325,8 @@ pub(crate) fn diagnose_runtime_for_workspace(
304
325
  selected_team_key: Option<&str>,
305
326
  coordinator_health: &crate::coordinator::HealthReport,
306
327
  ) -> (Value, Value) {
307
- let (mut issues, mut repairs) = diagnose_runtime(state, backend);
328
+ let (mut issues, mut repairs) =
329
+ diagnose_runtime_with_workspace(Some(workspace), state, backend);
308
330
  append_live_leader_workspace_mismatch_issue(
309
331
  workspace,
310
332
  state,
@@ -2926,14 +2926,30 @@ pub mod lifecycle_port {
2926
2926
  let source = crate::model::ids::AgentId::new(source_agent);
2927
2927
  let dest = crate::model::ids::AgentId::new(as_agent_id);
2928
2928
  match crate::lifecycle::fork_agent(workspace, &source, &dest, label, true, team) {
2929
- Ok(report) => Ok(json!({
2930
- "ok": true,
2931
- "source_agent_id": report.source_agent_id.as_str(),
2932
- "new_agent_id": report.new_agent_id.as_str(),
2933
- "session_id": report.session_id.as_ref().map(|session| session.as_str()),
2934
- "new_session_id": report.session_id.as_ref().map(|session| session.as_str()),
2935
- "backing_state": report.backing_state,
2936
- })),
2929
+ Ok(report) => {
2930
+ let mut value = json!({
2931
+ "ok": true,
2932
+ "source_agent_id": report.source_agent_id.as_str(),
2933
+ "new_agent_id": report.new_agent_id.as_str(),
2934
+ "session_id": report.session_id.as_ref().map(|session| session.as_str()),
2935
+ "new_session_id": report.session_id.as_ref().map(|session| session.as_str()),
2936
+ "backing_state": report.backing_state,
2937
+ });
2938
+ if let (Some(details), Some(object)) = (report.pi_fork, value.as_object_mut()) {
2939
+ object.insert("status".to_string(), json!("forked"));
2940
+ object.insert("target".to_string(), json!(report.new_agent_id.as_str()));
2941
+ object.insert(
2942
+ "source_session_id".to_string(),
2943
+ json!(details.source_session_id.as_str()),
2944
+ );
2945
+ object.insert("session_id".to_string(), json!(details.session_id.as_str()));
2946
+ object.insert(
2947
+ "backing_path".to_string(),
2948
+ json!(details.backing_path.0.to_string_lossy().to_string()),
2949
+ );
2950
+ }
2951
+ Ok(value)
2952
+ }
2937
2953
  Err(e) => Ok(error_value(e)),
2938
2954
  }
2939
2955
  }
@@ -414,31 +414,6 @@ fn valid_result_envelope() -> serde_json::Value {
414
414
  "next_actions": []
415
415
  })
416
416
  }
417
- fn seed_collect_state(ws: &std::path::Path) {
418
- seed_team_spec(ws);
419
- crate::state::persist::save_runtime_state(
420
- ws,
421
- &json!({
422
- "agents": {"fake_impl": {"status": "idle"}},
423
- "tasks": [{
424
- "id": "task_impl",
425
- "title": "Fake implementation",
426
- "type": "implementation",
427
- "assignee": "fake_impl",
428
- "deps": [],
429
- "acceptance": ["fake result collected"],
430
- "status": "pending",
431
- "requires_tools": [],
432
- "files": [],
433
- "risk": "low"
434
- }],
435
- "session_name": Value::Null,
436
- "active_team_key": Value::Null,
437
- "spec_path": ws.join("team.spec.yaml").to_string_lossy()
438
- }),
439
- )
440
- .unwrap();
441
- }
442
417
  fn seed_uncollected_result(ws: &std::path::Path, result_id: &str) {
443
418
  let store = crate::message_store::MessageStore::open(ws).unwrap();
444
419
  let conn = crate::db::schema::open_db(store.db_path()).unwrap();
@@ -450,18 +425,6 @@ fn seed_uncollected_result(ws: &std::path::Path, result_id: &str) {
450
425
  )
451
426
  .unwrap();
452
427
  }
453
- fn read_state(ws: &std::path::Path) -> serde_json::Value {
454
- serde_json::from_str(
455
- &std::fs::read_to_string(crate::state::persist::runtime_state_path(ws)).unwrap(),
456
- )
457
- .unwrap()
458
- }
459
- fn read_events(ws: &std::path::Path) -> Vec<serde_json::Value> {
460
- crate::event_log::EventLog::new(ws).tail(50).unwrap()
461
- }
462
- fn seeded_team_key(ws: &std::path::Path) -> String {
463
- ws.file_name().unwrap().to_string_lossy().to_string()
464
- }
465
428
  fn json_output(result: CmdResult) -> serde_json::Value {
466
429
  match result.output {
467
430
  CmdOutput::Json(v) => v,
@@ -590,40 +553,3 @@ fn remove_agent_from_spec_refusal_is_not_success_envelope() {
590
553
  "refused remove-agent must not delete the spec-defined agent"
591
554
  );
592
555
  }
593
- #[test]
594
- fn stuck_cancel_persists_suppression_and_stuck_list_reads_state() {
595
- let ws = tmp_workspace();
596
- seed_collect_state(&ws);
597
- let out = messaging::stuck_cancel(&ws, "fake_impl", None, "leader").unwrap();
598
- let team_key = seeded_team_key(&ws);
599
- assert_eq!(out["ok"], json!(true));
600
- assert_eq!(
601
- out["alert_types"],
602
- json!(["cross_worker_deadlock", "idle_fallback", "stuck"])
603
- );
604
- assert!(
605
- out["suppressed"]["idle_fallback"]["snapshot"]["assigned_task_ids"]
606
- .as_array()
607
- .unwrap()
608
- .contains(&json!("task_impl"))
609
- );
610
- let state = read_state(&ws);
611
- assert_eq!(
612
- state["coordinator"]["suppressed_idle_alerts"][&team_key]["fake_impl"]["idle_fallback"]
613
- ["suppressed_by"],
614
- json!("leader")
615
- );
616
- assert!(
617
- read_events(&ws)
618
- .iter()
619
- .any(|e| e["event"] == json!("coordinator.idle_alert_suppressed")
620
- && e["agent_id"] == json!("fake_impl")),
621
- "stuck_cancel must write coordinator.idle_alert_suppressed"
622
- );
623
- let listed = messaging::stuck_list(&ws).unwrap();
624
- assert_eq!(
625
- listed["suppressed_idle_alerts"]["fake_impl"]["stuck"]["suppressed_by"],
626
- json!("leader"),
627
- "stuck-list must read the persisted state mirror, not return a hard-coded empty list"
628
- );
629
- }
@@ -163,25 +163,21 @@ fn front_matter_body_lstrip_strips_only_newlines() {
163
163
  }
164
164
 
165
165
  #[test]
166
- fn front_matter_unterminated_errors() {
167
- let p = write_tmp("unterminated.md", "---\nname: x\n");
168
- let err = read_front_matter(&p).unwrap_err();
169
- assert!(
170
- err.to_string().contains("unterminated front matter"),
171
- "got: {err}"
172
- );
166
+ fn front_matter_unterminated_falls_back_to_plain_text() {
167
+ let text = "---\nname: x\n";
168
+ let p = write_tmp("unterminated.md", text);
169
+ let (meta, body) = read_front_matter(&p).expect("unterminated front matter is plain text");
170
+ assert_eq!(meta, Value::Map(Vec::new()));
171
+ assert_eq!(body, text);
173
172
  }
174
173
 
175
174
  #[test]
176
- fn front_matter_non_object_errors() {
177
- // A YAML list in the block → "front matter must be a YAML object" (compiler.py:183-184).
178
- let p = write_tmp("list.md", "---\n- a\n- b\n---\nbody\n");
179
- let err = read_front_matter(&p).unwrap_err();
180
- assert!(
181
- err.to_string()
182
- .contains("front matter must be a YAML object"),
183
- "got: {err}"
184
- );
175
+ fn front_matter_non_object_falls_back_to_plain_text() {
176
+ let text = "---\n- a\n- b\n---\nbody\n";
177
+ let p = write_tmp("list.md", text);
178
+ let (meta, body) = read_front_matter(&p).expect("non-object front matter is plain text");
179
+ assert_eq!(meta, Value::Map(Vec::new()));
180
+ assert_eq!(body, text);
185
181
  }
186
182
 
187
183
  // ───────────────────────────── compile_team: full dict parity ─────────────────────────────
@@ -395,7 +391,7 @@ const ROLE_MISSING_PROVIDER: &str = "\
395
391
  ---
396
392
  name: implementer
397
393
  role: Implementation Engineer
398
- model: gpt-5.5
394
+ model: team-agent/qwen3.8-27b
399
395
  auth_mode: subscription
400
396
  dangerously_skip_permissions: false
401
397
  tools:
@@ -406,13 +402,14 @@ Implement bounded tasks.
406
402
  ";
407
403
 
408
404
  #[test]
409
- fn compile_missing_required_provider_field_errors() {
405
+ fn compile_missing_provider_defaults_to_pi() {
410
406
  let team = build_team(TEAM_BASE, &[("implementer.md", ROLE_MISSING_PROVIDER)], &[]);
411
- let err = compile_team(&team).unwrap_err();
412
- assert!(
413
- err.to_string()
414
- .contains("missing front matter field provider"),
415
- "got: {err}"
407
+ let spec = compile_team(&team).expect("an omitted provider defaults to Pi");
408
+ let agent = agent0(&spec);
409
+ assert_eq!(agent.get("provider"), Some(&Value::Str("pi".to_string())));
410
+ assert_eq!(
411
+ agent.get("model"),
412
+ Some(&Value::Str("team-agent/qwen3.8-27b".to_string()))
416
413
  );
417
414
  }
418
415
 
@@ -44,48 +44,38 @@ pub struct IgnoredTeamField {
44
44
  pub value: String,
45
45
  }
46
46
 
47
- /// `compiler._read_front_matter` (compiler.py:173-185).
48
- ///
49
- /// Reads `path` (UTF-8). If the text does not start with `"---\n"`, returns
50
- /// `({}, full_text)`. Otherwise splits on the first `"\n---"` after byte 4:
51
- /// unterminated (no closing marker) → `ValidationError "{path}: unterminated
52
- /// front matter"`; the front-matter block is parsed via `simple_yaml.loads`
53
- /// (empty block → `{}`); a non-dict block → `ValidationError "{path}: front
54
- /// matter must be a YAML object"`. The body is everything after the closing
55
- /// marker, `lstrip("\n")` (leading NEWLINES only — not other whitespace).
47
+ /// Read optional YAML front matter; malformed or non-object headers are plain text.
56
48
  pub fn read_front_matter(path: &Path) -> Result<(Value, String), ModelError> {
57
49
  let text = fs::read_to_string(path)
58
50
  .map_err(|e| ModelError::Runtime(format!("{}: {e}", path.display())))?;
59
51
  let text = text.replace("\r\n", "\n").replace('\r', "\n");
52
+ let empty_meta = || Value::Map(Vec::new());
60
53
  let Some(rest) = text.strip_prefix("---\n") else {
61
- return Ok((Value::Map(Vec::new()), text));
54
+ return Ok((empty_meta(), text));
62
55
  };
63
- let Some(close) = rest.find("\n---") else {
64
- return Err(ModelError::Validation(format!(
65
- "{}: unterminated front matter",
66
- path.display()
67
- )));
56
+
57
+ let mut offset = 0;
58
+ let mut close = None;
59
+ for line in rest.split_inclusive('\n') {
60
+ if line.strip_suffix('\n').unwrap_or(line) == "---" {
61
+ close = Some(offset);
62
+ break;
63
+ }
64
+ offset += line.len();
65
+ }
66
+ let Some(close) = close else {
67
+ return Ok((empty_meta(), text));
68
68
  };
69
- let raw_meta = rest.get(..close).ok_or_else(|| {
70
- ModelError::Validation(format!("{}: unterminated front matter", path.display()))
71
- })?;
72
- let after_meta = rest.get(close..).ok_or_else(|| {
73
- ModelError::Validation(format!("{}: unterminated front matter", path.display()))
74
- })?;
75
- let after_marker = after_meta.strip_prefix("\n---").ok_or_else(|| {
76
- ModelError::Validation(format!("{}: unterminated front matter", path.display()))
77
- })?;
69
+ let raw_meta = &rest[..close];
78
70
  let meta = if raw_meta.trim().is_empty() {
79
- Value::Map(Vec::new())
71
+ empty_meta()
80
72
  } else {
81
- yaml::loads(raw_meta)?
73
+ match yaml::loads(raw_meta) {
74
+ Ok(meta) if meta.is_map() => meta,
75
+ _ => return Ok((empty_meta(), text)),
76
+ }
82
77
  };
83
- if !meta.is_map() {
84
- return Err(ModelError::Validation(format!(
85
- "{}: front matter must be a YAML object",
86
- path.display()
87
- )));
88
- }
78
+ let after_marker = &rest[close + 3..];
89
79
  Ok((meta, after_marker.trim_start_matches('\n').to_string()))
90
80
  }
91
81
 
@@ -161,7 +151,7 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
161
151
  let workspace = paths::team_workspace(team_dir)?;
162
152
  let workspace_s = workspace.display().to_string();
163
153
  let team_name =
164
- string_field(&team_meta, "name").unwrap_or_else(|| team_dir_parent_name(team_dir));
154
+ string_field(&team_meta, "name").unwrap_or_else(|| workspace_dir_name(&workspace));
165
155
  let objective = string_field(&team_meta, "objective")
166
156
  .or_else(|| non_empty_trimmed(&team_body))
167
157
  .unwrap_or_else(|| "Team Agent document-driven team.".to_string());
@@ -487,9 +477,23 @@ fn compile_role_agent_with_mode(
487
477
  let (meta, body) = read_front_matter(role_path)?;
488
478
  let communication_mode =
489
479
  communication_mode_field(&meta, role_path)?.unwrap_or(team_communication_mode);
490
- let id = required_string(&meta, role_path, "name")?;
491
- let role = required_string(&meta, role_path, "role")?;
492
- let provider = required_string(&meta, role_path, "provider")?;
480
+ let id = string_field(&meta, "agent_id")
481
+ .or_else(|| string_field(&meta, "name"))
482
+ .filter(|id| !id.trim().is_empty())
483
+ .or_else(|| {
484
+ role_path
485
+ .file_stem()
486
+ .and_then(|stem| stem.to_str())
487
+ .filter(|stem| !stem.is_empty())
488
+ .map(ToString::to_string)
489
+ })
490
+ .unwrap_or_else(|| "worker".to_string());
491
+ let role = string_field(&meta, "role")
492
+ .filter(|role| !role.trim().is_empty())
493
+ .unwrap_or_else(|| id.clone());
494
+ let provider = string_field(&meta, "provider")
495
+ .filter(|provider| !provider.trim().is_empty())
496
+ .unwrap_or_else(|| "pi".to_string());
493
497
  require_explicit_grok_role_model(&meta, role_path, &provider)?;
494
498
  require_explicit_cursor_role_model(&meta, role_path, &provider)?;
495
499
  validate_pi_role_fields(&meta, role_path, &provider)?;
@@ -498,7 +502,7 @@ fn compile_role_agent_with_mode(
498
502
  string_field(&meta, "model")
499
503
  .filter(|value| !value.trim().is_empty())
500
504
  .map(Value::Str)
501
- .unwrap_or(Value::Null)
505
+ .unwrap_or_else(|| Value::Str("openai-codex/gpt-6-luna".to_string()))
502
506
  } else {
503
507
  resolve_model(&meta, team_meta, &provider)
504
508
  };
@@ -532,11 +536,9 @@ fn compile_role_agent_with_mode(
532
536
  ("file", Value::Null),
533
537
  ]),
534
538
  ),
535
- // 0.5.66 bypass 单源:compiler 透传角色 md 的 `dangerously_skip_permissions`
536
- // (必填 bool,spec 校验保证存在)。取代旧"恒发 permission_mode: restricted"。
537
539
  (
538
540
  "dangerously_skip_permissions",
539
- Value::Bool(required_dangerously_skip_permissions(&meta, role_path)?),
541
+ Value::Bool(optional_dangerously_skip_permissions(&meta)),
540
542
  ),
541
543
  (
542
544
  "communication_mode",
@@ -555,6 +557,9 @@ fn compile_role_agent_with_mode(
555
557
  ]),
556
558
  ),
557
559
  ];
560
+ if let Some(label) = string_field(&meta, "label") {
561
+ agent_items.insert(1, ("label", Value::Str(label)));
562
+ }
558
563
  if let Some(profile) = string_field(&meta, "profile") {
559
564
  agent_items.push(("profile", Value::Str(profile)));
560
565
  }
@@ -572,6 +577,7 @@ fn compile_role_agent_with_mode(
572
577
  })?;
573
578
  Some(parsed)
574
579
  }
580
+ _ if is_pi => Some(ProviderEffort::Max),
575
581
  _ => None,
576
582
  };
577
583
  let team_effort = match string_field(team_meta, "provider_effort") {
@@ -668,15 +674,6 @@ fn string_field(meta: &Value, key: &str) -> Option<String> {
668
674
  .map(ToString::to_string)
669
675
  }
670
676
 
671
- fn required_string(meta: &Value, path: &Path, key: &str) -> Result<String, ModelError> {
672
- string_field(meta, key).ok_or_else(|| {
673
- ModelError::Validation(format!(
674
- "{}: missing front matter field {key}",
675
- path.display()
676
- ))
677
- })
678
- }
679
-
680
677
  /// 缺 model 时框架会填内建默认(grok 上是 grok-4),席位因此静默拿到
681
678
  /// 与角色文件无关的模型和上下文窗口,而 argv 看起来完全正常。
682
679
  /// 角色文件必须自己写死;内建默认、team 级默认、CLI 全局默认都是隐式来源。
@@ -746,19 +743,10 @@ fn validate_pi_role_fields(meta: &Value, path: &Path, provider: &str) -> Result<
746
743
  Ok(())
747
744
  }
748
745
 
749
- /// 0.5.66 bypass 单源:角色 md 的 `dangerously_skip_permissions` 必填 bool。
750
- /// 缺 = 编译失败(消息含硬串);非 bool = 同 fail-loud。
751
- fn required_dangerously_skip_permissions(meta: &Value, path: &Path) -> Result<bool, ModelError> {
746
+ fn optional_dangerously_skip_permissions(meta: &Value) -> bool {
752
747
  match meta.get("dangerously_skip_permissions") {
753
- Some(Value::Bool(value)) => Ok(*value),
754
- Some(_) => Err(ModelError::Validation(format!(
755
- "{}: front matter field dangerously_skip_permissions must be a boolean. This field must be declared explicitly; it controls whether the agent launches with permission prompts bypassed.",
756
- path.display()
757
- ))),
758
- None => Err(ModelError::Validation(format!(
759
- "{}: missing front matter field dangerously_skip_permissions. This field must be declared explicitly; it controls whether the agent launches with permission prompts bypassed.",
760
- path.display()
761
- ))),
748
+ Some(Value::Bool(value)) => *value,
749
+ _ => true,
762
750
  }
763
751
  }
764
752
 
@@ -880,10 +868,9 @@ fn non_empty_trimmed(text: &str) -> Option<String> {
880
868
  }
881
869
  }
882
870
 
883
- fn team_dir_parent_name(team_dir: &Path) -> String {
884
- team_dir
885
- .parent()
886
- .and_then(Path::file_name)
871
+ fn workspace_dir_name(workspace: &Path) -> String {
872
+ workspace
873
+ .file_name()
887
874
  .and_then(|s| s.to_str())
888
875
  .filter(|s| !s.is_empty())
889
876
  .unwrap_or("team")
@@ -105,12 +105,7 @@ fn s3_process_worker() {
105
105
  assert_eq!(line.trim(), "commit");
106
106
  });
107
107
  }
108
- let tools = crate::mcp_server::TeamOrchestratorTools::with_identity(
109
- &dir, Some(crate::model::ids::AgentId::new("leader")), team.map(crate::model::ids::TeamKey::new),
110
- );
111
108
  match action.as_str() {
112
- "assign" => { tools.assign_task(&payload, Some("bounded contract")).unwrap(); }
113
- "note" => { println!("S3 note={:?}", tools.update_state(payload.as_str().unwrap()).unwrap()); }
114
109
  "collect" => {
115
110
  let output = crate::messaging::results::collect_for_team(&dir, Some(&dir.join("result.json")), false, team).unwrap();
116
111
  assert_eq!(output["ok"], true);
@@ -152,10 +147,7 @@ fn s3_tick_preserves_tasks_notes_and_siblings() {
152
147
  seed(dir, false);
153
148
  let hook: SaveHook = Box::new(|ws, observed| {
154
149
  assert_eq!(observed["agents"]["w1"]["worker_state"], "DEAD", "tick must produce a real observation delta");
155
- write(ws.as_path(), "assign", Some("T1"), task("new-task"));
156
- write(ws.as_path(), "note", Some("T1"), json!("concurrent note"));
157
150
  write(ws.as_path(), "collect", Some("T1"), Value::Null);
158
- write(ws.as_path(), "note", Some("T2"), json!("sibling note"));
159
151
  write(ws.as_path(), "sibling", None, Value::Null);
160
152
  let latest = disk(ws.as_path());
161
153
  assert_eq!(latest["teams"]["T1"]["agents"]["w1"]["pane_id"], observed["agents"]["w1"]["pane_id"]);
@@ -166,11 +158,11 @@ fn s3_tick_preserves_tasks_notes_and_siblings() {
166
158
  assert!(report.ok, "tick must save its observation: {report:?}");
167
159
  let state = disk(dir);
168
160
  let team = &state["teams"]["T1"];
161
+ assert_eq!(team["tasks"][0]["id"], "original");
169
162
  assert_eq!(team["tasks"][0]["accepted_result_id"], "result-original");
170
- assert!(team["tasks"].as_array().unwrap().iter().any(|row| row["id"] == "new-task"));
171
- assert_eq!(team["notes"], json!(["original note", "concurrent note"]));
163
+ assert_eq!(team["notes"], json!(["original note"]));
172
164
  assert_eq!(team["agents"]["w1"]["worker_state"], "DEAD");
173
- assert_eq!(state["teams"]["T2"]["notes"], json!(["original note", "sibling note"]));
165
+ assert_eq!(state["teams"]["T2"]["notes"], json!(["original note"]));
174
166
  assert_eq!(state["teams"]["T3"]["notes"], json!(["third"]));
175
167
  assert_eq!(state["tasks"], team["tasks"]);
176
168
  assert_eq!(StateRepository::new(dir).load_team(Some("T1")).unwrap()["tasks"], team["tasks"]);
@@ -179,50 +171,26 @@ fn s3_tick_preserves_tasks_notes_and_siblings() {
179
171
  }
180
172
 
181
173
  #[test]
182
- fn s3_business_writers_commit_once_on_latest() {
183
- isolated("s3_business_writers_commit_once_on_latest", |root| {
184
- for (i, (left, right)) in [
185
- (("assign", task("left")), ("assign", task("right"))),
186
- (("note", json!("same note")), ("note", json!("same note"))),
187
- (("note", json!("same note")), ("assign", task("right"))),
188
- (("collect", Value::Null), ("assign", task("right"))),
189
- (("collect", Value::Null), ("note", json!("same note"))),
190
- ].into_iter().enumerate() {
191
- let dir = root.join(i.to_string());
192
- seed(&dir, false);
193
- let waiting = start_writer(&dir, left.0, Some("T1"), left.1, true);
194
- write(&dir, right.0, Some("T1"), right.1);
195
- finish(waiting, true);
196
- let state = disk(&dir);
197
- let team = &state["teams"]["T1"];
198
- if left.0 == "assign" { assert!(team["tasks"].as_array().unwrap().iter().any(|task| task["id"] == "left")); }
199
- if right.0 == "assign" { assert!(team["tasks"].as_array().unwrap().iter().any(|task| task["id"] == "right")); }
200
- if left.0 == "collect" { assert_eq!(team["tasks"][0]["accepted_result_id"], "result-original"); }
201
- let notes = usize::from(left.0 == "note") + usize::from(right.0 == "note");
202
- assert_eq!(team["notes"].as_array().unwrap().len(), 1 + notes);
203
- if left.0 == "note" && right.0 == "assign" {
204
- let markdown = std::fs::read_to_string(dir.join(".team/runtime/T1/team_state.md")).unwrap();
205
- assert!(markdown.contains("right"), "note must render the committed task view: {markdown}");
206
- }
207
- if left.0 == "collect" {
208
- write(&dir, "assign", Some("T1"), json!({"id": "original", "assignee": "w1", "title": "edited"}));
209
- assert_eq!(disk(&dir)["teams"]["T1"]["tasks"][0]["accepted_result_id"], "result-original");
210
- }
211
- println!("S3 pair {i} canonical={team}");
212
- }
174
+ fn s3_result_collection_commit_preserves_concurrent_sibling() {
175
+ isolated("s3_result_collection_commit_preserves_concurrent_sibling", |dir| {
176
+ seed(dir, false);
177
+ let waiting = start_writer(&dir, "collect", Some("T1"), Value::Null, true);
178
+ write(&dir, "sibling", None, Value::Null);
179
+ finish(waiting, true);
180
+ let state = disk(&dir);
181
+ assert_eq!(state["teams"]["T1"]["tasks"][0]["accepted_result_id"], "result-original");
182
+ assert_eq!(state["teams"]["T3"]["notes"], json!(["third"]));
183
+ assert_eq!(state["teams"]["T2"]["notes"], json!(["original note"]));
213
184
  });
214
185
  }
215
186
 
216
187
  #[test]
217
- fn s3_legacy_note_and_collection_keep_independent_updates() {
218
- isolated("s3_legacy_note_and_collection_keep_independent_updates", |dir| {
188
+ fn s3_legacy_result_collection_preserves_root_shape() {
189
+ isolated("s3_legacy_result_collection_preserves_root_shape", |dir| {
219
190
  seed(dir, true);
220
- let waiting = start_writer(dir, "collect", None, Value::Null, true);
221
- write(dir, "note", None, json!("legacy note"));
222
- finish(waiting, true);
191
+ write(dir, "collect", None, Value::Null);
223
192
  let state = disk(dir);
224
193
  assert_eq!(state["tasks"][0]["accepted_result_id"], "result-original");
225
- assert_eq!(state["notes"], json!(["original note", "legacy note"]));
226
194
  assert!(state.get("teams").is_none(), "legacy single Team shape remains valid");
227
195
  });
228
196
  }
@@ -304,20 +304,7 @@ fn leader_start_plan_with_ambient_authority(
304
304
  identity.team_id.as_str(),
305
305
  );
306
306
  let prompt = crate::lifecycle::worker_command_context::compile_pi_leader_system_prompt();
307
- let team_mcp_tools = [
308
- "assign_task",
309
- "send_message",
310
- "update_state",
311
- "get_team_status",
312
- "stop_agent",
313
- "reset_agent",
314
- "add_agent",
315
- "clone_agent",
316
- "fork_agent",
317
- "request_human",
318
- "stuck_list",
319
- "stuck_cancel",
320
- ];
307
+ let team_mcp_tools = ["send_message", "report_result", "get_team_status"];
321
308
  crate::lifecycle::launch::pi_mcp::materialize_pi_plan(
322
309
  crate::lifecycle::launch::pi_mcp::PiMaterializeRequest {
323
310
  workspace,