@team-agent/installer 0.5.42 → 0.5.44

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 (156) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +129 -54
  4. package/crates/team-agent/src/cli/diagnose.rs +1 -2
  5. package/crates/team-agent/src/cli/emit.rs +1 -7
  6. package/crates/team-agent/src/cli/helpers.rs +3 -1
  7. package/crates/team-agent/src/cli/leader.rs +2 -1
  8. package/crates/team-agent/src/cli/mod.rs +126 -16
  9. package/crates/team-agent/src/cli/named_address.rs +14 -5
  10. package/crates/team-agent/src/cli/profile.rs +19 -7
  11. package/crates/team-agent/src/cli/send.rs +9 -3
  12. package/crates/team-agent/src/cli/status.rs +8 -30
  13. package/crates/team-agent/src/cli/status_port.rs +1341 -1272
  14. package/crates/team-agent/src/cli/tests/base.rs +738 -660
  15. package/crates/team-agent/src/cli/tests/compile.rs +45 -18
  16. package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
  21. package/crates/team-agent/src/cli/tests/mod.rs +17 -7
  22. package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
  23. package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
  25. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
  26. package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
  27. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
  28. package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
  29. package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
  30. package/crates/team-agent/src/codex_app_server.rs +2 -5
  31. package/crates/team-agent/src/compiler/tests.rs +139 -33
  32. package/crates/team-agent/src/compiler.rs +55 -22
  33. package/crates/team-agent/src/conpty/backend.rs +23 -33
  34. package/crates/team-agent/src/coordinator/backoff.rs +2 -7
  35. package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
  36. package/crates/team-agent/src/coordinator/health.rs +46 -31
  37. package/crates/team-agent/src/coordinator/mod.rs +3 -3
  38. package/crates/team-agent/src/coordinator/orphan.rs +22 -10
  39. package/crates/team-agent/src/coordinator/steps/abnormal.rs +51 -56
  40. package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
  41. package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
  42. package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
  43. package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
  44. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
  45. package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
  46. package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
  47. package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
  48. package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
  49. package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
  50. package/crates/team-agent/src/db/message_store.rs +138 -30
  51. package/crates/team-agent/src/db/migration.rs +249 -61
  52. package/crates/team-agent/src/db/schema.rs +303 -82
  53. package/crates/team-agent/src/diagnose/comms.rs +9 -2
  54. package/crates/team-agent/src/diagnose/mod.rs +1 -3
  55. package/crates/team-agent/src/diagnose/orphans.rs +79 -61
  56. package/crates/team-agent/src/event_log.rs +70 -16
  57. package/crates/team-agent/src/layout/manager.rs +15 -4
  58. package/crates/team-agent/src/layout/mod.rs +4 -4
  59. package/crates/team-agent/src/layout/overlay.rs +10 -3
  60. package/crates/team-agent/src/layout/placement.rs +5 -1
  61. package/crates/team-agent/src/layout/recovery.rs +4 -2
  62. package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
  63. package/crates/team-agent/src/layout/sessions.rs +17 -9
  64. package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
  65. package/crates/team-agent/src/layout/worker_env.rs +87 -19
  66. package/crates/team-agent/src/leader/helpers.rs +7 -1
  67. package/crates/team-agent/src/leader/lease.rs +199 -89
  68. package/crates/team-agent/src/leader/owner_bind.rs +55 -22
  69. package/crates/team-agent/src/leader/provider_attribution.rs +25 -6
  70. package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
  71. package/crates/team-agent/src/leader/rediscover.rs +74 -25
  72. package/crates/team-agent/src/leader/registry.rs +1 -1
  73. package/crates/team-agent/src/leader/start.rs +75 -54
  74. package/crates/team-agent/src/leader/takeover.rs +46 -11
  75. package/crates/team-agent/src/leader/tests/basics.rs +320 -167
  76. package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
  77. package/crates/team-agent/src/leader/tests/identity.rs +428 -356
  78. package/crates/team-agent/src/leader/tests/idle.rs +285 -254
  79. package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
  80. package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
  81. package/crates/team-agent/src/leader/tests/mod.rs +115 -99
  82. package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
  83. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
  84. package/crates/team-agent/src/lib.rs +4 -4
  85. package/crates/team-agent/src/lifecycle/display.rs +7 -3
  86. package/crates/team-agent/src/lifecycle/launch.rs +55 -15
  87. package/crates/team-agent/src/lifecycle/mod.rs +9 -1
  88. package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
  89. package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
  90. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
  91. package/crates/team-agent/src/lifecycle/restart/common.rs +6 -2
  92. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
  93. package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
  94. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
  95. package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
  96. package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
  97. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
  98. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
  99. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
  100. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
  101. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
  102. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +3 -0
  103. package/crates/team-agent/src/lifecycle/tests.rs +2 -2
  104. package/crates/team-agent/src/main.rs +4 -4
  105. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
  106. package/crates/team-agent/src/mcp_server/mod.rs +11 -2
  107. package/crates/team-agent/src/mcp_server/types.rs +14 -3
  108. package/crates/team-agent/src/mcp_server/wire.rs +230 -68
  109. package/crates/team-agent/src/messaging/delivery.rs +20 -18
  110. package/crates/team-agent/src/messaging/helpers.rs +25 -4
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
  112. package/crates/team-agent/src/messaging/mod.rs +2 -3
  113. package/crates/team-agent/src/messaging/selftest.rs +46 -26
  114. package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
  115. package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
  116. package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
  117. package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
  118. package/crates/team-agent/src/messaging/trust.rs +25 -2
  119. package/crates/team-agent/src/messaging/watchers.rs +37 -9
  120. package/crates/team-agent/src/model/enums.rs +65 -16
  121. package/crates/team-agent/src/model/ids.rs +12 -3
  122. package/crates/team-agent/src/model/paths.rs +28 -7
  123. package/crates/team-agent/src/model/permissions.rs +176 -33
  124. package/crates/team-agent/src/model/routing.rs +66 -20
  125. package/crates/team-agent/src/model/spec.rs +365 -69
  126. package/crates/team-agent/src/model/task_graph.rs +36 -9
  127. package/crates/team-agent/src/model/yaml/tests.rs +24 -6
  128. package/crates/team-agent/src/model/yaml.rs +7 -6
  129. package/crates/team-agent/src/packaging/install.rs +23 -9
  130. package/crates/team-agent/src/packaging/migrate.rs +5 -7
  131. package/crates/team-agent/src/packaging/mod.rs +9 -1
  132. package/crates/team-agent/src/packaging/repair.rs +13 -6
  133. package/crates/team-agent/src/packaging/tests.rs +63 -16
  134. package/crates/team-agent/src/packaging/types.rs +22 -7
  135. package/crates/team-agent/src/platform/argv.rs +4 -1
  136. package/crates/team-agent/src/platform/file_lock.rs +22 -8
  137. package/crates/team-agent/src/platform/process.rs +54 -24
  138. package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
  139. package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
  140. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
  141. package/crates/team-agent/src/provider/classify.rs +127 -42
  142. package/crates/team-agent/src/provider/faults.rs +17 -5
  143. package/crates/team-agent/src/provider/helpers.rs +6 -5
  144. package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
  145. package/crates/team-agent/src/state/persist.rs +28 -0
  146. package/crates/team-agent/src/state/repository.rs +8 -3
  147. package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
  148. package/crates/team-agent/src/tmux_backend.rs +80 -44
  149. package/crates/team-agent/src/topology.rs +40 -20
  150. package/crates/team-agent/src/transport/test_support.rs +20 -23
  151. package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
  152. package/crates/team-agent/src/transport/tests/mod.rs +178 -187
  153. package/crates/team-agent/src/transport/tests/wire.rs +561 -525
  154. package/crates/team-agent/src/transport.rs +12 -17
  155. package/crates/team-agent/src/transport_factory.rs +29 -14
  156. package/package.json +4 -4
@@ -36,10 +36,26 @@ const RESULT_REQUIRED: &[&str] = &[
36
36
 
37
37
  /// `RESULT_COLLECTION_SCHEMAS`(`spec.py:93-99`),有序:(field, required, allowed)。
38
38
  const RESULT_COLLECTIONS: &[(&str, &[&str], &[&str])] = &[
39
- ("changes", &["path", "kind", "description"], &["path", "kind", "description"]),
40
- ("tests", &["command", "status"], &["command", "status", "detail"]),
41
- ("risks", &["severity", "description"], &["severity", "description"]),
42
- ("artifacts", &["path", "description"], &["path", "description"]),
39
+ (
40
+ "changes",
41
+ &["path", "kind", "description"],
42
+ &["path", "kind", "description"],
43
+ ),
44
+ (
45
+ "tests",
46
+ &["command", "status"],
47
+ &["command", "status", "detail"],
48
+ ),
49
+ (
50
+ "risks",
51
+ &["severity", "description"],
52
+ &["severity", "description"],
53
+ ),
54
+ (
55
+ "artifacts",
56
+ &["path", "description"],
57
+ &["path", "description"],
58
+ ),
43
59
  ("next_actions", &["description"], &["description"]),
44
60
  ];
45
61
 
@@ -61,7 +77,13 @@ pub fn validate_result_envelope(envelope: &Value) -> Result<(), ModelError> {
61
77
  }
62
78
 
63
79
  /// `spec._check_keys`:非 object → "must be an object";否则 missing(排序)+ unknown(排序)。
64
- fn check_keys(obj: &Value, path: &str, required: &[&str], allowed: &[&str], errors: &mut Vec<String>) {
80
+ fn check_keys(
81
+ obj: &Value,
82
+ path: &str,
83
+ required: &[&str],
84
+ allowed: &[&str],
85
+ errors: &mut Vec<String>,
86
+ ) {
65
87
  let Some(map) = obj.as_object() else {
66
88
  errors.push(format!("{path}: must be an object"));
67
89
  return;
@@ -146,7 +168,11 @@ fn result_schema_errors(envelope: &Value) -> Vec<String> {
146
168
  &["passed", "failed", "not_run", "skipped"],
147
169
  "invalid test status",
148
170
  )),
149
- "risks" => Some(("severity", &["low", "medium", "high"], "invalid risk severity")),
171
+ "risks" => Some((
172
+ "severity",
173
+ &["low", "medium", "high"],
174
+ "invalid risk severity",
175
+ )),
150
176
  _ => None,
151
177
  };
152
178
  if let Some((key, valid, msg)) = enum_field {
@@ -174,14 +200,35 @@ fn result_schema_errors(envelope: &Value) -> Vec<String> {
174
200
  // 对齐 Python `spec.validate_spec`(golden 由真相源双跑锁死)。
175
201
 
176
202
  const ROOT_KEYS: &[&str] = &[
177
- "version", "team", "leader", "agents", "routing", "communication", "runtime", "context", "tasks",
203
+ "version",
204
+ "team",
205
+ "leader",
206
+ "agents",
207
+ "routing",
208
+ "communication",
209
+ "runtime",
210
+ "context",
211
+ "tasks",
178
212
  ];
179
213
  const AUTH_MODES: &[&str] = &["subscription", "official_api", "compatible_api"];
180
214
  const VALID_DISPLAY_BACKENDS: &[&str] = &[
181
- "none", "tmux_attach", "iterm", "ghostty", "ghostty_window", "ghostty_workspace", "adaptive",
215
+ "none",
216
+ "tmux_attach",
217
+ "iterm",
218
+ "ghostty",
219
+ "ghostty_window",
220
+ "ghostty_workspace",
221
+ "adaptive",
182
222
  ];
183
223
  const TASK_STATUS_STRS: &[&str] = &[
184
- "pending", "ready", "running", "blocked", "needs_retry", "done", "failed", "cancelled",
224
+ "pending",
225
+ "ready",
226
+ "running",
227
+ "blocked",
228
+ "needs_retry",
229
+ "done",
230
+ "failed",
231
+ "cancelled",
185
232
  ];
186
233
 
187
234
  /// `spec.validate_spec`:basic schema + semantic 校验。失败 → `ModelError::Validation`,
@@ -192,7 +239,11 @@ pub fn validate_spec(spec: &Yaml, base_dir: &Path) -> Result<(), ModelError> {
192
239
  if errors.is_empty() {
193
240
  return Ok(());
194
241
  }
195
- let joined = errors.iter().map(|m| format!("- {m}")).collect::<Vec<_>>().join("\n");
242
+ let joined = errors
243
+ .iter()
244
+ .map(|m| format!("- {m}"))
245
+ .collect::<Vec<_>>()
246
+ .join("\n");
196
247
  Err(ModelError::Validation(format!(
197
248
  "team.spec.yaml validation failed:\n{joined}"
198
249
  )))
@@ -210,7 +261,13 @@ fn is_map_some(v: Option<&Yaml>) -> bool {
210
261
  }
211
262
 
212
263
  /// `spec._check_keys` 的 yaml::Value 版。
213
- fn check_keys_y(obj: Option<&Yaml>, path: &str, required: &[&str], allowed: &[&str], errors: &mut Vec<String>) {
264
+ fn check_keys_y(
265
+ obj: Option<&Yaml>,
266
+ path: &str,
267
+ required: &[&str],
268
+ allowed: &[&str],
269
+ errors: &mut Vec<String>,
270
+ ) {
214
271
  let Some(map) = obj.and_then(Yaml::as_map) else {
215
272
  errors.push(format!("{path}: must be an object"));
216
273
  return;
@@ -251,7 +308,13 @@ fn basic_schema_errors(spec: &Yaml) -> Vec<String> {
251
308
  // 0.4.x provider effort MVP step 3: provider_effort is allowed but not required.
252
309
  let team_required = &["name", "mode", "objective", "workspace"];
253
310
  let team_allowed = &["name", "mode", "objective", "workspace", "provider_effort"];
254
- check_keys_y(spec.get("team"), "/team", team_required, team_allowed, &mut e);
311
+ check_keys_y(
312
+ spec.get("team"),
313
+ "/team",
314
+ team_required,
315
+ team_allowed,
316
+ &mut e,
317
+ );
255
318
  if let Some(team) = spec.get("team") {
256
319
  if let Some(raw) = team.get("provider_effort").and_then(Yaml::as_str) {
257
320
  if crate::model::enums::ProviderEffort::parse(raw).is_none() {
@@ -261,13 +324,26 @@ fn basic_schema_errors(spec: &Yaml) -> Vec<String> {
261
324
  }
262
325
  }
263
326
  }
264
- let mode = spec.get("team").and_then(|t| t.get("mode")).and_then(Yaml::as_str);
327
+ let mode = spec
328
+ .get("team")
329
+ .and_then(|t| t.get("mode"))
330
+ .and_then(Yaml::as_str);
265
331
  if !matches!(mode, Some("supervisor_worker" | "swarm_limited")) {
266
332
  e.push("/team/mode: invalid mode".to_string());
267
333
  }
268
334
  let leader_keys = &["id", "role", "provider", "model", "tools", "context_policy"];
269
- check_keys_y(spec.get("leader"), "/leader", leader_keys, leader_keys, &mut e);
270
- let cp_keys = &["keep_user_thread", "receive_worker_outputs", "max_worker_result_tokens"];
335
+ check_keys_y(
336
+ spec.get("leader"),
337
+ "/leader",
338
+ leader_keys,
339
+ leader_keys,
340
+ &mut e,
341
+ );
342
+ let cp_keys = &[
343
+ "keep_user_thread",
344
+ "receive_worker_outputs",
345
+ "max_worker_result_tokens",
346
+ ];
271
347
  check_keys_y(
272
348
  spec.get("leader").and_then(|l| l.get("context_policy")),
273
349
  "/leader/context_policy",
@@ -300,13 +376,35 @@ fn basic_schema_errors(spec: &Yaml) -> Vec<String> {
300
376
 
301
377
  fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
302
378
  let req = &[
303
- "id", "role", "provider", "model", "working_directory", "system_prompt", "tools",
304
- "permission_mode", "preferred_for", "avoid_for", "output_contract",
379
+ "id",
380
+ "role",
381
+ "provider",
382
+ "model",
383
+ "working_directory",
384
+ "system_prompt",
385
+ "tools",
386
+ "permission_mode",
387
+ "preferred_for",
388
+ "avoid_for",
389
+ "output_contract",
305
390
  ];
306
391
  let allowed = &[
307
- "id", "role", "provider", "model", "working_directory", "system_prompt", "tools",
308
- "permission_mode", "preferred_for", "avoid_for", "output_contract", "paused", "auth_mode",
309
- "profile", "credential_ref", "forked_from",
392
+ "id",
393
+ "role",
394
+ "provider",
395
+ "model",
396
+ "working_directory",
397
+ "system_prompt",
398
+ "tools",
399
+ "permission_mode",
400
+ "preferred_for",
401
+ "avoid_for",
402
+ "output_contract",
403
+ "paused",
404
+ "auth_mode",
405
+ "profile",
406
+ "credential_ref",
407
+ "forked_from",
310
408
  // 0.4.x provider effort MVP step 3: per-agent effort override (resolved at compile).
311
409
  "effort",
312
410
  ];
@@ -333,18 +431,47 @@ fn check_agent(agent: &Yaml, path: &str, errors: &mut Vec<String>) {
333
431
  Some(_) => {}
334
432
  }
335
433
  }
336
- check_keys_y(agent.get("system_prompt"), &format!("{path}/system_prompt"), &["inline", "file"], &["inline", "file"], errors);
434
+ check_keys_y(
435
+ agent.get("system_prompt"),
436
+ &format!("{path}/system_prompt"),
437
+ &["inline", "file"],
438
+ &["inline", "file"],
439
+ errors,
440
+ );
337
441
  check_list_y(agent.get("tools"), &format!("{path}/tools"), errors);
338
- check_list_y(agent.get("preferred_for"), &format!("{path}/preferred_for"), errors);
442
+ check_list_y(
443
+ agent.get("preferred_for"),
444
+ &format!("{path}/preferred_for"),
445
+ errors,
446
+ );
339
447
  check_list_y(agent.get("avoid_for"), &format!("{path}/avoid_for"), errors);
340
- check_keys_y(agent.get("output_contract"), &format!("{path}/output_contract"), &["format", "required_fields"], &["format", "required_fields"], errors);
341
- if agent.get("output_contract").and_then(|o| o.get("format")).and_then(Yaml::as_str) != Some("result_envelope_v1") {
342
- errors.push(format!("{path}/output_contract/format: must be result_envelope_v1"));
448
+ check_keys_y(
449
+ agent.get("output_contract"),
450
+ &format!("{path}/output_contract"),
451
+ &["format", "required_fields"],
452
+ &["format", "required_fields"],
453
+ errors,
454
+ );
455
+ if agent
456
+ .get("output_contract")
457
+ .and_then(|o| o.get("format"))
458
+ .and_then(Yaml::as_str)
459
+ != Some("result_envelope_v1")
460
+ {
461
+ errors.push(format!(
462
+ "{path}/output_contract/format: must be result_envelope_v1"
463
+ ));
343
464
  }
344
465
  }
345
466
 
346
467
  fn check_routing(routing: Option<&Yaml>, errors: &mut Vec<String>) {
347
- check_keys_y(routing, "/routing", &["default_assignee", "rules"], &["default_assignee", "rules"], errors);
468
+ check_keys_y(
469
+ routing,
470
+ "/routing",
471
+ &["default_assignee", "rules"],
472
+ &["default_assignee", "rules"],
473
+ errors,
474
+ );
348
475
  if !is_map_some(routing) {
349
476
  return;
350
477
  }
@@ -353,8 +480,15 @@ fn check_routing(routing: Option<&Yaml>, errors: &mut Vec<String>) {
353
480
  return;
354
481
  };
355
482
  for (idx, rule) in rules.iter().enumerate() {
356
- check_keys_y(Some(rule), &format!("/routing/rules/{idx}"), &["id", "assign_to", "priority"], &["id", "when", "match", "assign_to", "priority"], errors);
357
- let has_clause = rule.get("when").is_some_and(Yaml::is_truthy) || rule.get("match").is_some_and(Yaml::is_truthy);
483
+ check_keys_y(
484
+ Some(rule),
485
+ &format!("/routing/rules/{idx}"),
486
+ &["id", "assign_to", "priority"],
487
+ &["id", "when", "match", "assign_to", "priority"],
488
+ errors,
489
+ );
490
+ let has_clause = rule.get("when").is_some_and(Yaml::is_truthy)
491
+ || rule.get("match").is_some_and(Yaml::is_truthy);
358
492
  if rule.is_map() && !has_clause {
359
493
  errors.push(format!("/routing/rules/{idx}: must include when or match"));
360
494
  }
@@ -362,27 +496,64 @@ fn check_routing(routing: Option<&Yaml>, errors: &mut Vec<String>) {
362
496
  }
363
497
 
364
498
  fn check_communication(comm: Option<&Yaml>, errors: &mut Vec<String>) {
365
- let req = &["protocol", "topology", "worker_to_worker", "ack_timeout_sec", "result_format", "message_store"];
499
+ let req = &[
500
+ "protocol",
501
+ "topology",
502
+ "worker_to_worker",
503
+ "ack_timeout_sec",
504
+ "result_format",
505
+ "message_store",
506
+ ];
366
507
  check_keys_y(comm, "/communication", req, req, errors);
367
508
  if !is_map_some(comm) {
368
509
  return;
369
510
  }
370
- if !matches!(comm.and_then(|c| c.get("protocol")).and_then(Yaml::as_str), Some("mcp_inbox" | "file_bus")) {
511
+ if !matches!(
512
+ comm.and_then(|c| c.get("protocol")).and_then(Yaml::as_str),
513
+ Some("mcp_inbox" | "file_bus")
514
+ ) {
371
515
  errors.push("/communication/protocol: invalid protocol".to_string());
372
516
  }
373
- if comm.and_then(|c| c.get("result_format")).and_then(Yaml::as_str) != Some("result_envelope_v1") {
517
+ if comm
518
+ .and_then(|c| c.get("result_format"))
519
+ .and_then(Yaml::as_str)
520
+ != Some("result_envelope_v1")
521
+ {
374
522
  errors.push("/communication/result_format: must be result_envelope_v1".to_string());
375
523
  }
376
- check_keys_y(comm.and_then(|c| c.get("message_store")), "/communication/message_store", &["sqlite", "mirror_files"], &["sqlite", "mirror_files"], errors);
524
+ check_keys_y(
525
+ comm.and_then(|c| c.get("message_store")),
526
+ "/communication/message_store",
527
+ &["sqlite", "mirror_files"],
528
+ &["sqlite", "mirror_files"],
529
+ errors,
530
+ );
377
531
  }
378
532
 
379
533
  fn check_runtime(runtime: Option<&Yaml>, errors: &mut Vec<String>) {
380
- let req = &["backend", "session_name", "auto_launch", "require_user_approval_before_launch", "max_active_agents", "startup_order"];
534
+ let req = &[
535
+ "backend",
536
+ "session_name",
537
+ "auto_launch",
538
+ "require_user_approval_before_launch",
539
+ "max_active_agents",
540
+ "startup_order",
541
+ ];
381
542
  let allowed = &[
382
- "backend", "session_name", "auto_launch", "require_user_approval_before_launch",
383
- "max_active_agents", "startup_order", "display_backend", "dangerous_auto_approve",
384
- "auto_attach_leader", "fast", "tick_interval_sec", "push_min_interval_sec",
385
- "stuck_timeout_sec", "auto_trust_own_workspace",
543
+ "backend",
544
+ "session_name",
545
+ "auto_launch",
546
+ "require_user_approval_before_launch",
547
+ "max_active_agents",
548
+ "startup_order",
549
+ "display_backend",
550
+ "dangerous_auto_approve",
551
+ "auto_attach_leader",
552
+ "fast",
553
+ "tick_interval_sec",
554
+ "push_min_interval_sec",
555
+ "stuck_timeout_sec",
556
+ "auto_trust_own_workspace",
386
557
  ];
387
558
  check_keys_y(runtime, "/runtime", req, allowed, errors);
388
559
  if !is_map_some(runtime) {
@@ -393,7 +564,10 @@ fn check_runtime(runtime: Option<&Yaml>, errors: &mut Vec<String>) {
393
564
  errors.push("/runtime/backend: invalid backend".to_string());
394
565
  }
395
566
  if let Some(db) = get("display_backend") {
396
- if !db.as_str().is_some_and(|s| VALID_DISPLAY_BACKENDS.contains(&s)) {
567
+ if !db
568
+ .as_str()
569
+ .is_some_and(|s| VALID_DISPLAY_BACKENDS.contains(&s))
570
+ {
397
571
  errors.push("/runtime/display_backend: invalid display backend".to_string());
398
572
  }
399
573
  }
@@ -410,23 +584,56 @@ fn check_context(context: Option<&Yaml>, errors: &mut Vec<String>) {
410
584
  let req = &["state_file", "artifact_dir", "log_dir", "summarization"];
411
585
  check_keys_y(context, "/context", req, req, errors);
412
586
  if is_map_some(context) {
413
- check_keys_y(context.and_then(|c| c.get("summarization")), "/context/summarization", &["worker_full_logs", "state_update"], &["worker_full_logs", "state_update"], errors);
587
+ check_keys_y(
588
+ context.and_then(|c| c.get("summarization")),
589
+ "/context/summarization",
590
+ &["worker_full_logs", "state_update"],
591
+ &["worker_full_logs", "state_update"],
592
+ errors,
593
+ );
414
594
  }
415
595
  }
416
596
 
417
597
  fn check_task(task: &Yaml, path: &str, errors: &mut Vec<String>) {
418
- let req = &["id", "title", "type", "assignee", "deps", "acceptance", "status"];
598
+ let req = &[
599
+ "id",
600
+ "title",
601
+ "type",
602
+ "assignee",
603
+ "deps",
604
+ "acceptance",
605
+ "status",
606
+ ];
419
607
  let allowed = &[
420
- "id", "title", "type", "assignee", "deps", "acceptance", "status", "description",
421
- "requires_tools", "files", "risk", "retry_limit", "human_confirmation",
608
+ "id",
609
+ "title",
610
+ "type",
611
+ "assignee",
612
+ "deps",
613
+ "acceptance",
614
+ "status",
615
+ "description",
616
+ "requires_tools",
617
+ "files",
618
+ "risk",
619
+ "retry_limit",
620
+ "human_confirmation",
422
621
  ];
423
622
  check_keys_y(Some(task), path, req, allowed, errors);
424
623
  if !task.is_map() {
425
624
  return;
426
625
  }
427
626
  check_list_y(task.get("deps"), &format!("{path}/deps"), errors);
428
- check_list_y(task.get("acceptance"), &format!("{path}/acceptance"), errors);
429
- if !task.get("status").and_then(Yaml::as_str).is_some_and(|s| TASK_STATUS_STRS.contains(&s)) {
627
+ check_list_y(
628
+ task.get("acceptance"),
629
+ &format!("{path}/acceptance"),
630
+ errors,
631
+ );
632
+ if !task
633
+ .get("status")
634
+ .and_then(Yaml::as_str)
635
+ .is_some_and(|s| TASK_STATUS_STRS.contains(&s))
636
+ {
430
637
  errors.push(format!("{path}/status: invalid task status"));
431
638
  }
432
639
  }
@@ -441,7 +648,10 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
441
648
  // duplicate agent id(集合含 None 复刻 Python `{a.get("id") ...}`)。
442
649
  // SMOKE-1 N38 失败可解释性:不仅报"有重复",还点出**哪个 id 重复**(可能多个),
443
650
  // 给 operator 直接定位线索;locate.md §"Smallest likely code touch" item 3。
444
- let id_set: HashSet<Option<&str>> = map_agents.iter().map(|a| a.get("id").and_then(Yaml::as_str)).collect();
651
+ let id_set: HashSet<Option<&str>> = map_agents
652
+ .iter()
653
+ .map(|a| a.get("id").and_then(Yaml::as_str))
654
+ .collect();
445
655
  if id_set.len() != map_agents.len() {
446
656
  let mut seen: HashSet<&str> = HashSet::new();
447
657
  let mut duplicates: Vec<&str> = Vec::new();
@@ -466,7 +676,10 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
466
676
  }
467
677
  }
468
678
  // all_ids:present 的 agent id + leader id(若 truthy)。
469
- let mut all_ids: HashSet<&str> = map_agents.iter().filter_map(|a| a.get("id").and_then(Yaml::as_str)).collect();
679
+ let mut all_ids: HashSet<&str> = map_agents
680
+ .iter()
681
+ .filter_map(|a| a.get("id").and_then(Yaml::as_str))
682
+ .collect();
470
683
  if let Some(lid) = leader.and_then(|l| l.get("id")).and_then(Yaml::as_str) {
471
684
  if !lid.is_empty() {
472
685
  all_ids.insert(lid);
@@ -478,7 +691,10 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
478
691
  .and_then(Yaml::as_str)
479
692
  .is_some_and(|p| parse_canonical_provider(p).is_some())
480
693
  {
481
- e.push(format!("/leader/provider: unknown provider {}", py_repr(leader_provider)));
694
+ e.push(format!(
695
+ "/leader/provider: unknown provider {}",
696
+ py_repr(leader_provider)
697
+ ));
482
698
  }
483
699
 
484
700
  for (idx, agent) in agents.iter().enumerate() {
@@ -487,60 +703,123 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
487
703
  .and_then(Yaml::as_str)
488
704
  .is_some_and(|p| parse_canonical_provider(p).is_some())
489
705
  {
490
- e.push(format!("/agents/{idx}/provider: unknown provider {}", py_repr(provider)));
706
+ e.push(format!(
707
+ "/agents/{idx}/provider: unknown provider {}",
708
+ py_repr(provider)
709
+ ));
491
710
  }
492
711
  if let Some(auth) = agent.get("auth_mode") {
493
- if !matches!(auth, Yaml::Null) && !auth.as_str().is_some_and(|a| AUTH_MODES.contains(&a)) {
494
- e.push(format!("/agents/{idx}/auth_mode: unknown auth_mode {}", py_repr(Some(auth))));
712
+ if !matches!(auth, Yaml::Null)
713
+ && !auth.as_str().is_some_and(|a| AUTH_MODES.contains(&a))
714
+ {
715
+ e.push(format!(
716
+ "/agents/{idx}/auth_mode: unknown auth_mode {}",
717
+ py_repr(Some(auth))
718
+ ));
495
719
  }
496
720
  }
497
- if let Some(f) = agent.get("system_prompt").and_then(|sp| sp.get("file")).filter(|p| p.is_truthy()).and_then(Yaml::as_str) {
721
+ if let Some(f) = agent
722
+ .get("system_prompt")
723
+ .and_then(|sp| sp.get("file"))
724
+ .filter(|p| p.is_truthy())
725
+ .and_then(Yaml::as_str)
726
+ {
498
727
  let candidate = Path::new(f);
499
- let full = if candidate.is_absolute() { candidate.to_path_buf() } else { base_dir.join(candidate) };
728
+ let full = if candidate.is_absolute() {
729
+ candidate.to_path_buf()
730
+ } else {
731
+ base_dir.join(candidate)
732
+ };
500
733
  if !full.exists() {
501
- e.push(format!("/agents/{idx}/system_prompt/file: file not found: {}", full.display()));
734
+ e.push(format!(
735
+ "/agents/{idx}/system_prompt/file: file not found: {}",
736
+ full.display()
737
+ ));
502
738
  }
503
739
  }
504
- let tools: Vec<&str> = agent.get("tools").and_then(Yaml::as_list).unwrap_or(&[]).iter().filter_map(Yaml::as_str).collect();
740
+ let tools: Vec<&str> = agent
741
+ .get("tools")
742
+ .and_then(Yaml::as_list)
743
+ .unwrap_or(&[])
744
+ .iter()
745
+ .filter_map(Yaml::as_str)
746
+ .collect();
505
747
  for tool in permissions::expand_tool_strings(tools) {
506
748
  if !permissions::is_canonical_tool(&tool) {
507
- e.push(format!("/agents/{idx}/tools: unknown tool {}", py_repr_str(&tool)));
749
+ e.push(format!(
750
+ "/agents/{idx}/tools: unknown tool {}",
751
+ py_repr_str(&tool)
752
+ ));
508
753
  }
509
754
  }
510
755
  }
511
756
 
512
- let leader_tools: Vec<&str> = leader.and_then(|l| l.get("tools")).and_then(Yaml::as_list).unwrap_or(&[]).iter().filter_map(Yaml::as_str).collect();
757
+ let leader_tools: Vec<&str> = leader
758
+ .and_then(|l| l.get("tools"))
759
+ .and_then(Yaml::as_list)
760
+ .unwrap_or(&[])
761
+ .iter()
762
+ .filter_map(Yaml::as_str)
763
+ .collect();
513
764
  for tool in permissions::expand_tool_strings(leader_tools) {
514
765
  if !permissions::is_canonical_tool(&tool) {
515
- e.push(format!("/leader/tools: unknown tool {}", py_repr_str(&tool)));
766
+ e.push(format!(
767
+ "/leader/tools: unknown tool {}",
768
+ py_repr_str(&tool)
769
+ ));
516
770
  }
517
771
  }
518
772
 
519
773
  let routing = spec.get("routing");
520
- if let Some(da) = routing.and_then(|r| r.get("default_assignee")).and_then(Yaml::as_str) {
774
+ if let Some(da) = routing
775
+ .and_then(|r| r.get("default_assignee"))
776
+ .and_then(Yaml::as_str)
777
+ {
521
778
  if !da.is_empty() && !all_ids.contains(da) {
522
- e.push(format!("/routing/default_assignee: unknown agent {}", py_repr_str(da)));
779
+ e.push(format!(
780
+ "/routing/default_assignee: unknown agent {}",
781
+ py_repr_str(da)
782
+ ));
523
783
  }
524
784
  }
525
- let rules = routing.and_then(|r| r.get("rules")).and_then(Yaml::as_list).unwrap_or(&[]);
785
+ let rules = routing
786
+ .and_then(|r| r.get("rules"))
787
+ .and_then(Yaml::as_list)
788
+ .unwrap_or(&[]);
526
789
  for (idx, rule) in rules.iter().enumerate() {
527
790
  let target = rule.get("assign_to");
528
- if !target.and_then(Yaml::as_str).is_some_and(|t| all_ids.contains(t)) {
529
- e.push(format!("/routing/rules/{idx}/assign_to: unknown agent {}", py_repr(target)));
791
+ if !target
792
+ .and_then(Yaml::as_str)
793
+ .is_some_and(|t| all_ids.contains(t))
794
+ {
795
+ e.push(format!(
796
+ "/routing/rules/{idx}/assign_to: unknown agent {}",
797
+ py_repr(target)
798
+ ));
530
799
  }
531
800
  }
532
801
 
533
802
  let tasks: &[Yaml] = spec.get("tasks").and_then(Yaml::as_list).unwrap_or(&[]);
534
- let task_ids: HashSet<&str> = tasks.iter().filter(|t| t.is_map()).filter_map(|t| t.get("id").and_then(Yaml::as_str)).collect();
803
+ let task_ids: HashSet<&str> = tasks
804
+ .iter()
805
+ .filter(|t| t.is_map())
806
+ .filter_map(|t| t.get("id").and_then(Yaml::as_str))
807
+ .collect();
535
808
  for (idx, task) in tasks.iter().enumerate() {
536
809
  if let Some(a) = task.get("assignee").and_then(Yaml::as_str) {
537
810
  if !a.is_empty() && !all_ids.contains(a) {
538
- e.push(format!("/tasks/{idx}/assignee: unknown agent {}", py_repr_str(a)));
811
+ e.push(format!(
812
+ "/tasks/{idx}/assignee: unknown agent {}",
813
+ py_repr_str(a)
814
+ ));
539
815
  }
540
816
  }
541
817
  for dep in task.get("deps").and_then(Yaml::as_list).unwrap_or(&[]) {
542
818
  if !dep.as_str().is_some_and(|d| task_ids.contains(d)) {
543
- e.push(format!("/tasks/{idx}/deps: unknown dependency {}", py_repr(Some(dep))));
819
+ e.push(format!(
820
+ "/tasks/{idx}/deps: unknown dependency {}",
821
+ py_repr(Some(dep))
822
+ ));
544
823
  }
545
824
  }
546
825
  }
@@ -549,14 +828,27 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
549
828
  let nodes: Vec<TaskNode> = tasks
550
829
  .iter()
551
830
  .filter_map(|t| {
552
- let id = t.get("id").and_then(Yaml::as_str).filter(|s| !s.is_empty())?;
553
- let deps: Vec<TaskId> = t.get("deps").and_then(Yaml::as_list).unwrap_or(&[]).iter().filter_map(|d| d.as_str().map(TaskId::from)).collect();
831
+ let id = t
832
+ .get("id")
833
+ .and_then(Yaml::as_str)
834
+ .filter(|s| !s.is_empty())?;
835
+ let deps: Vec<TaskId> = t
836
+ .get("deps")
837
+ .and_then(Yaml::as_list)
838
+ .unwrap_or(&[])
839
+ .iter()
840
+ .filter_map(|d| d.as_str().map(TaskId::from))
841
+ .collect();
554
842
  Some(TaskNode::new(TaskId::from(id), deps, TaskStatus::Pending))
555
843
  })
556
844
  .collect();
557
845
  let cycle = find_dependency_cycle(&nodes);
558
846
  if !cycle.is_empty() {
559
- let chain = cycle.iter().map(TaskId::as_str).collect::<Vec<_>>().join(" -> ");
847
+ let chain = cycle
848
+ .iter()
849
+ .map(TaskId::as_str)
850
+ .collect::<Vec<_>>()
851
+ .join(" -> ");
560
852
  e.push(format!("/tasks: dependency cycle detected: {chain}"));
561
853
  }
562
854
  e
@@ -564,7 +856,11 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
564
856
 
565
857
  /// Python `repr()` of a string(选引号 + 转义),用于 `unknown X 'name'`。
566
858
  fn py_repr_str(s: &str) -> String {
567
- let quote = if s.contains('\'') && !s.contains('"') { '"' } else { '\'' };
859
+ let quote = if s.contains('\'') && !s.contains('"') {
860
+ '"'
861
+ } else {
862
+ '\''
863
+ };
568
864
  let mut out = String::new();
569
865
  out.push(quote);
570
866
  for c in s.chars() {