@team-agent/installer 0.2.11 → 0.3.0

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 (326) hide show
  1. package/Cargo.lock +744 -0
  2. package/Cargo.toml +34 -0
  3. package/crates/team-agent/Cargo.toml +33 -0
  4. package/crates/team-agent/src/cli/adapters.rs +1343 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +554 -0
  6. package/crates/team-agent/src/cli/emit.rs +1077 -0
  7. package/crates/team-agent/src/cli/helpers.rs +88 -0
  8. package/crates/team-agent/src/cli/leader.rs +216 -0
  9. package/crates/team-agent/src/cli/mod.rs +1141 -0
  10. package/crates/team-agent/src/cli/profile.rs +306 -0
  11. package/crates/team-agent/src/cli/send.rs +215 -0
  12. package/crates/team-agent/src/cli/status.rs +179 -0
  13. package/crates/team-agent/src/cli/status_port.rs +502 -0
  14. package/crates/team-agent/src/cli/tests/base.rs +616 -0
  15. package/crates/team-agent/src/cli/tests/compile.rs +96 -0
  16. package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
  21. package/crates/team-agent/src/cli/tests/mod.rs +97 -0
  22. package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
  23. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
  25. package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
  27. package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
  28. package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
  29. package/crates/team-agent/src/cli/types.rs +605 -0
  30. package/crates/team-agent/src/compiler/tests.rs +701 -0
  31. package/crates/team-agent/src/compiler.rs +489 -0
  32. package/crates/team-agent/src/coordinator/backoff.rs +153 -0
  33. package/crates/team-agent/src/coordinator/health.rs +436 -0
  34. package/crates/team-agent/src/coordinator/mod.rs +80 -0
  35. package/crates/team-agent/src/coordinator/orphan.rs +179 -0
  36. package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
  37. package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
  38. package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
  39. package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
  40. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
  41. package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
  42. package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
  43. package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
  44. package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
  45. package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
  46. package/crates/team-agent/src/coordinator/tick.rs +2032 -0
  47. package/crates/team-agent/src/coordinator/types.rs +584 -0
  48. package/crates/team-agent/src/db/migration.rs +716 -0
  49. package/crates/team-agent/src/db/mod.rs +23 -0
  50. package/crates/team-agent/src/db/schema.rs +378 -0
  51. package/crates/team-agent/src/event_log.rs +375 -0
  52. package/crates/team-agent/src/fake_worker.rs +253 -0
  53. package/crates/team-agent/src/leader/helpers.rs +190 -0
  54. package/crates/team-agent/src/leader/inject.rs +33 -0
  55. package/crates/team-agent/src/leader/lease.rs +1063 -0
  56. package/crates/team-agent/src/leader/mod.rs +99 -0
  57. package/crates/team-agent/src/leader/owner_bind.rs +292 -0
  58. package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1099 -0
  60. package/crates/team-agent/src/leader/start.rs +273 -0
  61. package/crates/team-agent/src/leader/takeover.rs +235 -0
  62. package/crates/team-agent/src/leader/tests/basics.rs +183 -0
  63. package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +271 -0
  66. package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
  67. package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
  68. package/crates/team-agent/src/leader/tests/mod.rs +125 -0
  69. package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
  70. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
  71. package/crates/team-agent/src/leader/types.rs +487 -0
  72. package/crates/team-agent/src/lib.rs +85 -0
  73. package/crates/team-agent/src/lifecycle/display.rs +228 -0
  74. package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
  75. package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
  76. package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
  77. package/crates/team-agent/src/lifecycle/mod.rs +62 -0
  78. package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
  79. package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
  80. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
  81. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
  82. package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
  83. package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
  84. package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
  85. package/crates/team-agent/src/lifecycle/restart.rs +76 -0
  86. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
  88. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
  89. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
  90. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
  91. package/crates/team-agent/src/lifecycle/tests.rs +27 -0
  92. package/crates/team-agent/src/lifecycle/types.rs +685 -0
  93. package/crates/team-agent/src/main.rs +41 -0
  94. package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
  95. package/crates/team-agent/src/mcp_server/mod.rs +183 -0
  96. package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
  97. package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
  98. package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
  99. package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
  100. package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
  101. package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
  102. package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
  103. package/crates/team-agent/src/mcp_server/tests.rs +38 -0
  104. package/crates/team-agent/src/mcp_server/tools.rs +603 -0
  105. package/crates/team-agent/src/mcp_server/types.rs +421 -0
  106. package/crates/team-agent/src/mcp_server/wire.rs +388 -0
  107. package/crates/team-agent/src/message_store.rs +767 -0
  108. package/crates/team-agent/src/messaging/activity.rs +433 -0
  109. package/crates/team-agent/src/messaging/delivery.rs +542 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
  112. package/crates/team-agent/src/messaging/mod.rs +147 -0
  113. package/crates/team-agent/src/messaging/peers.rs +32 -0
  114. package/crates/team-agent/src/messaging/results.rs +537 -0
  115. package/crates/team-agent/src/messaging/scheduler.rs +344 -0
  116. package/crates/team-agent/src/messaging/selftest.rs +100 -0
  117. package/crates/team-agent/src/messaging/send.rs +582 -0
  118. package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
  119. package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
  121. package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
  122. package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
  123. package/crates/team-agent/src/messaging/trust.rs +192 -0
  124. package/crates/team-agent/src/messaging/types.rs +355 -0
  125. package/crates/team-agent/src/messaging/watchers.rs +591 -0
  126. package/crates/team-agent/src/model/enums.rs +311 -0
  127. package/crates/team-agent/src/model/errors.rs +17 -0
  128. package/crates/team-agent/src/model/ids.rs +155 -0
  129. package/crates/team-agent/src/model/mod.rs +22 -0
  130. package/crates/team-agent/src/model/paths.rs +228 -0
  131. package/crates/team-agent/src/model/permissions.rs +567 -0
  132. package/crates/team-agent/src/model/routing.rs +340 -0
  133. package/crates/team-agent/src/model/spec.rs +680 -0
  134. package/crates/team-agent/src/model/task_graph.rs +380 -0
  135. package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
  136. package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
  137. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
  138. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
  139. package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
  140. package/crates/team-agent/src/model/yaml/tests.rs +288 -0
  141. package/crates/team-agent/src/model/yaml.rs +800 -0
  142. package/crates/team-agent/src/packaging/install.rs +305 -0
  143. package/crates/team-agent/src/packaging/migrate.rs +30 -0
  144. package/crates/team-agent/src/packaging/mod.rs +82 -0
  145. package/crates/team-agent/src/packaging/repair.rs +24 -0
  146. package/crates/team-agent/src/packaging/tests.rs +829 -0
  147. package/crates/team-agent/src/packaging/types.rs +369 -0
  148. package/crates/team-agent/src/provider/adapter.rs +801 -0
  149. package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
  150. package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
  151. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
  152. package/crates/team-agent/src/provider/classify.rs +456 -0
  153. package/crates/team-agent/src/provider/faults.rs +136 -0
  154. package/crates/team-agent/src/provider/helpers.rs +41 -0
  155. package/crates/team-agent/src/provider/mod.rs +53 -0
  156. package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
  157. package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
  158. package/crates/team-agent/src/provider/tests/classify.rs +240 -0
  159. package/crates/team-agent/src/provider/tests/faults.rs +120 -0
  160. package/crates/team-agent/src/provider/tests/idle.rs +208 -0
  161. package/crates/team-agent/src/provider/tests/wire.rs +213 -0
  162. package/crates/team-agent/src/provider/tests.rs +31 -0
  163. package/crates/team-agent/src/provider/types.rs +424 -0
  164. package/crates/team-agent/src/state/identity.rs +656 -0
  165. package/crates/team-agent/src/state/mod.rs +58 -0
  166. package/crates/team-agent/src/state/owner_gate.rs +423 -0
  167. package/crates/team-agent/src/state/persist.rs +712 -0
  168. package/crates/team-agent/src/state/projection.rs +657 -0
  169. package/crates/team-agent/src/state/selector.rs +105 -0
  170. package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
  171. package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
  172. package/crates/team-agent/src/tmux_backend.rs +758 -0
  173. package/crates/team-agent/src/transport/test_support.rs +252 -0
  174. package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
  175. package/crates/team-agent/src/transport/tests/mod.rs +199 -0
  176. package/crates/team-agent/src/transport/tests/wire.rs +527 -0
  177. package/crates/team-agent/src/transport.rs +774 -0
  178. package/npm/install.mjs +90 -106
  179. package/package.json +15 -13
  180. package/crates/team-agent-core/Cargo.toml +0 -12
  181. package/crates/team-agent-core/src/lib.rs +0 -332
  182. package/crates/team-agent-core/src/main.rs +0 -152
  183. package/pyproject.toml +0 -18
  184. package/scripts/install.py +0 -88
  185. package/scripts/run_regression_tests.py +0 -83
  186. package/src/team_agent/__init__.py +0 -3
  187. package/src/team_agent/__main__.py +0 -5
  188. package/src/team_agent/_legacy_pane_discovery.py +0 -186
  189. package/src/team_agent/abnormal_track.py +0 -253
  190. package/src/team_agent/approvals/__init__.py +0 -65
  191. package/src/team_agent/approvals/constants.py +0 -6
  192. package/src/team_agent/approvals/parsing.py +0 -176
  193. package/src/team_agent/approvals/runtime_prompts.py +0 -171
  194. package/src/team_agent/approvals/status.py +0 -176
  195. package/src/team_agent/cli/__init__.py +0 -137
  196. package/src/team_agent/cli/commands.py +0 -481
  197. package/src/team_agent/cli/e2e.py +0 -202
  198. package/src/team_agent/cli/helpers.py +0 -226
  199. package/src/team_agent/cli/parser.py +0 -540
  200. package/src/team_agent/compiler.py +0 -334
  201. package/src/team_agent/coordinator/__init__.py +0 -53
  202. package/src/team_agent/coordinator/__main__.py +0 -119
  203. package/src/team_agent/coordinator/lifecycle.py +0 -411
  204. package/src/team_agent/coordinator/metadata.py +0 -61
  205. package/src/team_agent/coordinator/paths.py +0 -17
  206. package/src/team_agent/diagnose/__init__.py +0 -48
  207. package/src/team_agent/diagnose/checks.py +0 -101
  208. package/src/team_agent/diagnose/comms.py +0 -213
  209. package/src/team_agent/diagnose/health.py +0 -241
  210. package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
  211. package/src/team_agent/diagnose/preflight.py +0 -194
  212. package/src/team_agent/diagnose/quick_start.py +0 -324
  213. package/src/team_agent/display/__init__.py +0 -92
  214. package/src/team_agent/display/adaptive.py +0 -511
  215. package/src/team_agent/display/backend.py +0 -46
  216. package/src/team_agent/display/close.py +0 -154
  217. package/src/team_agent/display/ghostty.py +0 -77
  218. package/src/team_agent/display/rebuild.py +0 -102
  219. package/src/team_agent/display/tiling.py +0 -156
  220. package/src/team_agent/display/worker_window.py +0 -114
  221. package/src/team_agent/display/workspace.py +0 -382
  222. package/src/team_agent/errors.py +0 -10
  223. package/src/team_agent/events.py +0 -84
  224. package/src/team_agent/fake_worker.py +0 -80
  225. package/src/team_agent/idle_predicate.py +0 -218
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -114
  228. package/src/team_agent/launch/__init__.py +0 -41
  229. package/src/team_agent/launch/bootstrap.py +0 -85
  230. package/src/team_agent/launch/config.py +0 -106
  231. package/src/team_agent/launch/core.py +0 -301
  232. package/src/team_agent/launch/requirements.py +0 -57
  233. package/src/team_agent/leader/__init__.py +0 -926
  234. package/src/team_agent/leader_binding.py +0 -183
  235. package/src/team_agent/lifecycle/__init__.py +0 -5
  236. package/src/team_agent/lifecycle/agents.py +0 -278
  237. package/src/team_agent/lifecycle/operations.py +0 -411
  238. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
  239. package/src/team_agent/lifecycle/start.py +0 -363
  240. package/src/team_agent/mcp_server/__init__.py +0 -42
  241. package/src/team_agent/mcp_server/__main__.py +0 -7
  242. package/src/team_agent/mcp_server/contracts.py +0 -148
  243. package/src/team_agent/mcp_server/normalize.py +0 -257
  244. package/src/team_agent/mcp_server/server.py +0 -150
  245. package/src/team_agent/mcp_server/tools.py +0 -352
  246. package/src/team_agent/message_store/__init__.py +0 -23
  247. package/src/team_agent/message_store/agent_health.py +0 -113
  248. package/src/team_agent/message_store/core.py +0 -497
  249. package/src/team_agent/message_store/leader_notification_log.py +0 -198
  250. package/src/team_agent/message_store/result_watchers.py +0 -251
  251. package/src/team_agent/message_store/schema.py +0 -308
  252. package/src/team_agent/message_store/schema_migration.py +0 -448
  253. package/src/team_agent/messaging/__init__.py +0 -1
  254. package/src/team_agent/messaging/activity_detector.py +0 -262
  255. package/src/team_agent/messaging/delivery.py +0 -504
  256. package/src/team_agent/messaging/deps.py +0 -247
  257. package/src/team_agent/messaging/idle_alerts.py +0 -423
  258. package/src/team_agent/messaging/internal_delivery.py +0 -46
  259. package/src/team_agent/messaging/leader.py +0 -497
  260. package/src/team_agent/messaging/leader_api_errors.py +0 -216
  261. package/src/team_agent/messaging/leader_panes.py +0 -673
  262. package/src/team_agent/messaging/owner_bypass.py +0 -29
  263. package/src/team_agent/messaging/result_delivery.py +0 -539
  264. package/src/team_agent/messaging/results.py +0 -447
  265. package/src/team_agent/messaging/scheduler.py +0 -450
  266. package/src/team_agent/messaging/send.py +0 -532
  267. package/src/team_agent/messaging/session_drift.py +0 -94
  268. package/src/team_agent/messaging/tmux_io.py +0 -506
  269. package/src/team_agent/messaging/tmux_prompt.py +0 -338
  270. package/src/team_agent/messaging/trust_auto_answer.py +0 -52
  271. package/src/team_agent/orchestrator/__init__.py +0 -376
  272. package/src/team_agent/orchestrator/plan.py +0 -122
  273. package/src/team_agent/orchestrator/state.py +0 -128
  274. package/src/team_agent/paths.py +0 -45
  275. package/src/team_agent/permissions.py +0 -123
  276. package/src/team_agent/profiles/__init__.py +0 -82
  277. package/src/team_agent/profiles/constants.py +0 -19
  278. package/src/team_agent/profiles/core.py +0 -407
  279. package/src/team_agent/profiles/helpers.py +0 -69
  280. package/src/team_agent/profiles/provider_env.py +0 -188
  281. package/src/team_agent/profiles/smoke.py +0 -201
  282. package/src/team_agent/provider_cli/__init__.py +0 -43
  283. package/src/team_agent/provider_cli/adapter.py +0 -172
  284. package/src/team_agent/provider_cli/base.py +0 -48
  285. package/src/team_agent/provider_cli/claude.py +0 -503
  286. package/src/team_agent/provider_cli/codex.py +0 -336
  287. package/src/team_agent/provider_cli/copilot.py +0 -8
  288. package/src/team_agent/provider_cli/fake.py +0 -39
  289. package/src/team_agent/provider_cli/gemini.py +0 -95
  290. package/src/team_agent/provider_cli/opencode.py +0 -8
  291. package/src/team_agent/provider_cli/prompt.py +0 -62
  292. package/src/team_agent/provider_cli/registry.py +0 -18
  293. package/src/team_agent/provider_cli/unsupported.py +0 -32
  294. package/src/team_agent/provider_state/README.md +0 -78
  295. package/src/team_agent/provider_state/__init__.py +0 -91
  296. package/src/team_agent/provider_state/claude.py +0 -86
  297. package/src/team_agent/provider_state/codex.py +0 -84
  298. package/src/team_agent/provider_state/common.py +0 -207
  299. package/src/team_agent/provider_state/registry.py +0 -118
  300. package/src/team_agent/providers.py +0 -163
  301. package/src/team_agent/quality_gates.py +0 -104
  302. package/src/team_agent/restart/__init__.py +0 -34
  303. package/src/team_agent/restart/orchestration.py +0 -554
  304. package/src/team_agent/restart/selection.py +0 -89
  305. package/src/team_agent/restart/snapshot.py +0 -70
  306. package/src/team_agent/routing.py +0 -84
  307. package/src/team_agent/runtime.py +0 -1243
  308. package/src/team_agent/rust_core.py +0 -327
  309. package/src/team_agent/sessions/__init__.py +0 -25
  310. package/src/team_agent/sessions/capture.py +0 -144
  311. package/src/team_agent/sessions/inventory.py +0 -44
  312. package/src/team_agent/sessions/resume.py +0 -135
  313. package/src/team_agent/simple_yaml.py +0 -236
  314. package/src/team_agent/spec.py +0 -370
  315. package/src/team_agent/state.py +0 -693
  316. package/src/team_agent/status/__init__.py +0 -63
  317. package/src/team_agent/status/approvals.py +0 -52
  318. package/src/team_agent/status/compact.py +0 -158
  319. package/src/team_agent/status/constants.py +0 -18
  320. package/src/team_agent/status/inbox.py +0 -58
  321. package/src/team_agent/status/peek.py +0 -117
  322. package/src/team_agent/status/queries.py +0 -199
  323. package/src/team_agent/task_graph.py +0 -80
  324. package/src/team_agent/terminal.py +0 -57
  325. package/src/team_agent/wake.py +0 -58
  326. package/src/team_agent/watch/__init__.py +0 -145
@@ -1,236 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import ast
4
- import json
5
- from typing import Any
6
-
7
-
8
- def loads(text: str) -> Any:
9
- stripped = text.lstrip()
10
- if stripped.startswith("{") or stripped.startswith("["):
11
- return json.loads(text)
12
- lines = text.splitlines()
13
- value, index = _parse_block(lines, 0, 0)
14
- while index < len(lines) and not _content(lines[index]):
15
- index += 1
16
- if index != len(lines):
17
- raise ValueError(f"unexpected content at line {index + 1}: {lines[index]}")
18
- return value
19
-
20
-
21
- def dumps(value: Any, indent: int = 0) -> str:
22
- lines = _dump(value, indent)
23
- return "\n".join(lines) + "\n"
24
-
25
-
26
- def _parse_block(lines: list[str], index: int, indent: int) -> tuple[Any, int]:
27
- index = _skip_blank(lines, index)
28
- if index >= len(lines):
29
- return None, index
30
- current_indent = _indent(lines[index])
31
- if current_indent < indent:
32
- return None, index
33
- if _stripped(lines[index]).startswith("- "):
34
- return _parse_list(lines, index, current_indent)
35
- return _parse_dict(lines, index, current_indent)
36
-
37
-
38
- def _parse_dict(lines: list[str], index: int, indent: int) -> tuple[dict[str, Any], int]:
39
- obj: dict[str, Any] = {}
40
- while index < len(lines):
41
- if not _content(lines[index]):
42
- index += 1
43
- continue
44
- line_indent = _indent(lines[index])
45
- if line_indent < indent:
46
- break
47
- if line_indent > indent:
48
- raise ValueError(f"unexpected indentation at line {index + 1}: {lines[index]}")
49
- stripped = _stripped(lines[index])
50
- if stripped.startswith("- "):
51
- break
52
- key, raw = _split_key_value(stripped, index)
53
- if raw == "|":
54
- value, index = _parse_block_scalar(lines, index + 1, indent + 2)
55
- elif raw == "":
56
- value, index = _parse_block(lines, index + 1, indent + 2)
57
- else:
58
- value = _parse_scalar(raw)
59
- index += 1
60
- obj[key] = value
61
- return obj, index
62
-
63
-
64
- def _parse_list(lines: list[str], index: int, indent: int) -> tuple[list[Any], int]:
65
- items: list[Any] = []
66
- while index < len(lines):
67
- if not _content(lines[index]):
68
- index += 1
69
- continue
70
- line_indent = _indent(lines[index])
71
- if line_indent < indent:
72
- break
73
- if line_indent != indent:
74
- raise ValueError(f"unexpected list indentation at line {index + 1}: {lines[index]}")
75
- stripped = _stripped(lines[index])
76
- if not stripped.startswith("- "):
77
- break
78
- item_text = stripped[2:].strip()
79
- if item_text == "":
80
- value, index = _parse_block(lines, index + 1, indent + 2)
81
- items.append(value)
82
- continue
83
- if _looks_like_key_value(item_text):
84
- key, raw = _split_key_value(item_text, index)
85
- item: dict[str, Any] = {}
86
- if raw == "|":
87
- value, next_index = _parse_block_scalar(lines, index + 1, indent + 2)
88
- elif raw == "":
89
- value, next_index = _parse_block(lines, index + 1, indent + 2)
90
- else:
91
- value = _parse_scalar(raw)
92
- next_index = index + 1
93
- item[key] = value
94
- if next_index < len(lines) and _indent(lines[next_index]) == indent + 2:
95
- extra, next_index = _parse_dict(lines, next_index, indent + 2)
96
- item.update(extra)
97
- items.append(item)
98
- index = next_index
99
- else:
100
- items.append(_parse_scalar(item_text))
101
- index += 1
102
- return items, index
103
-
104
-
105
- def _parse_block_scalar(lines: list[str], index: int, indent: int) -> tuple[str, int]:
106
- block: list[str] = []
107
- while index < len(lines):
108
- if not lines[index].strip():
109
- block.append("")
110
- index += 1
111
- continue
112
- line_indent = _indent(lines[index])
113
- if line_indent < indent:
114
- break
115
- block.append(lines[index][indent:])
116
- index += 1
117
- return "\n".join(block).rstrip() + "\n", index
118
-
119
-
120
- def _parse_scalar(raw: str) -> Any:
121
- if raw in {"null", "Null", "NULL", "~"}:
122
- return None
123
- if raw in {"true", "True", "TRUE"}:
124
- return True
125
- if raw in {"false", "False", "FALSE"}:
126
- return False
127
- try:
128
- return int(raw)
129
- except ValueError:
130
- pass
131
- if raw.startswith("[") and raw.endswith("]"):
132
- try:
133
- return ast.literal_eval(raw)
134
- except (SyntaxError, ValueError):
135
- return raw
136
- if raw == "{}":
137
- return {}
138
- if (raw.startswith('"') and raw.endswith('"')) or (raw.startswith("'") and raw.endswith("'")):
139
- try:
140
- return ast.literal_eval(raw)
141
- except (SyntaxError, ValueError):
142
- return raw[1:-1]
143
- return raw
144
-
145
-
146
- def _dump(value: Any, indent: int) -> list[str]:
147
- pad = " " * indent
148
- if isinstance(value, dict):
149
- lines: list[str] = []
150
- for key, item in value.items():
151
- if item == []:
152
- lines.append(f"{pad}{key}: []")
153
- elif item == {}:
154
- lines.append(f"{pad}{key}: {{}}")
155
- elif isinstance(item, (dict, list)):
156
- lines.append(f"{pad}{key}:")
157
- lines.extend(_dump(item, indent + 2))
158
- elif isinstance(item, str) and "\n" in item:
159
- lines.append(f"{pad}{key}: |")
160
- for block_line in item.rstrip("\n").splitlines():
161
- lines.append(f"{pad} {block_line}")
162
- else:
163
- lines.append(f"{pad}{key}: {_format_scalar(item)}")
164
- return lines
165
- if isinstance(value, list):
166
- lines = []
167
- for item in value:
168
- if isinstance(item, dict):
169
- if not item:
170
- lines.append(f"{pad}- {{}}")
171
- continue
172
- first = True
173
- for key, child in item.items():
174
- prefix = "- " if first else " "
175
- if child == []:
176
- lines.append(f"{pad}{prefix}{key}: []")
177
- elif child == {}:
178
- lines.append(f"{pad}{prefix}{key}: {{}}")
179
- elif isinstance(child, (dict, list)):
180
- lines.append(f"{pad}{prefix}{key}:")
181
- lines.extend(_dump(child, indent + 4))
182
- else:
183
- lines.append(f"{pad}{prefix}{key}: {_format_scalar(child)}")
184
- first = False
185
- elif isinstance(item, list):
186
- lines.append(f"{pad}-")
187
- lines.extend(_dump(item, indent + 2))
188
- else:
189
- lines.append(f"{pad}- {_format_scalar(item)}")
190
- return lines
191
- return [f"{pad}{_format_scalar(value)}"]
192
-
193
-
194
- def _format_scalar(value: Any) -> str:
195
- if value is None:
196
- return "null"
197
- if value is True:
198
- return "true"
199
- if value is False:
200
- return "false"
201
- if isinstance(value, int):
202
- return str(value)
203
- return json.dumps(str(value), ensure_ascii=False)
204
-
205
-
206
- def _split_key_value(stripped: str, index: int) -> tuple[str, str]:
207
- if ":" not in stripped:
208
- raise ValueError(f"expected key: value at line {index + 1}")
209
- key, raw = stripped.split(":", 1)
210
- return key.strip(), raw.strip()
211
-
212
-
213
- def _looks_like_key_value(text: str) -> bool:
214
- if ":" not in text:
215
- return False
216
- key = text.split(":", 1)[0]
217
- return bool(key) and all(ch.isalnum() or ch in "_-" for ch in key)
218
-
219
-
220
- def _content(line: str) -> bool:
221
- stripped = line.strip()
222
- return bool(stripped) and not stripped.startswith("#")
223
-
224
-
225
- def _skip_blank(lines: list[str], index: int) -> int:
226
- while index < len(lines) and not _content(lines[index]):
227
- index += 1
228
- return index
229
-
230
-
231
- def _indent(line: str) -> int:
232
- return len(line) - len(line.lstrip(" "))
233
-
234
-
235
- def _stripped(line: str) -> str:
236
- return line.strip()
@@ -1,370 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from pathlib import Path
4
- from typing import Any
5
-
6
- from team_agent.errors import ValidationError
7
- from team_agent.display.backend import VALID_DISPLAY_BACKENDS
8
- from team_agent.permissions import CANONICAL_TOOLS, expand_tools
9
- from team_agent.profiles import AUTH_MODES
10
- from team_agent.simple_yaml import loads
11
- from team_agent.task_graph import find_dependency_cycle
12
-
13
- SUPPORTED_PROVIDERS = {"claude", "claude_code", "codex", "gemini_cli", "fake"}
14
-
15
-
16
- def load_yaml(path: Path) -> dict[str, Any]:
17
- try:
18
- data = loads(path.read_text(encoding="utf-8"))
19
- except OSError as exc:
20
- raise ValidationError(f"Cannot read {path}: {exc}") from exc
21
- except ValueError as exc:
22
- raise ValidationError(f"Invalid YAML in {path}: {exc}") from exc
23
- if not isinstance(data, dict):
24
- raise ValidationError(f"{path} must contain a YAML object")
25
- return data
26
-
27
-
28
- def load_spec(path: Path) -> dict[str, Any]:
29
- spec = load_yaml(path)
30
- validate_spec(spec, base_dir=path.parent)
31
- _emit_load_time_deprecations(spec, path)
32
- return spec
33
-
34
-
35
- def _emit_load_time_deprecations(spec: dict[str, Any], path: Path) -> None:
36
- """Stage 7 S7 (2026-05-27): deprecation signals attached to the spec field
37
- itself must fire when the YAML is read, not lazily inside the trust-prompt
38
- code path. A user with the deprecated field in team.spec.yaml needs to see
39
- the warning even when startup never reaches attempt_trust_auto_answer.
40
-
41
- The leader-panes helper owns the one-shot stderr guard + the structured
42
- audit event, so we reuse it. EventLog points at the WORKSPACE ROOT (not
43
- the spec file's directory) so a quick-start layout that stores the spec
44
- under <workspace>/.team/current/team.spec.yaml still routes the audit
45
- event into the single canonical <workspace>/.team/logs/events.jsonl
46
- instead of a doubled <workspace>/.team/current/.team/logs/events.jsonl
47
- nesting.
48
- """
49
- runtime = spec.get("runtime")
50
- if not isinstance(runtime, dict):
51
- return
52
- if not bool(runtime.get("auto_trust_own_workspace")):
53
- return
54
- # Local import keeps the spec module free of messaging-layer coupling at
55
- # import time; only YAMLs that opt into the deprecated field pay the cost.
56
- from team_agent.events import EventLog
57
- from team_agent.messaging.leader_panes import _emit_spec_opt_in_deprecation
58
- _emit_spec_opt_in_deprecation(EventLog(_resolve_workspace_root(path)))
59
-
60
-
61
- def _resolve_workspace_root(spec_path: Path) -> Path:
62
- """Find the workspace root that owns this spec.
63
-
64
- A workspace root is the directory whose `.team/` subdirectory holds the
65
- runtime state, logs, artifacts, and (for quick-start layouts) the spec
66
- itself under `.team/current/`. We climb from the spec file's parent
67
- looking for the first ancestor that has a `.team/` child. If no ancestor
68
- qualifies (fresh workspace before init, or a spec deliberately placed
69
- outside any team workspace), we fall back to `spec_path.parent` which is
70
- the legacy single-layout behaviour.
71
-
72
- Implementation note: we use real filesystem evidence (`(dir/.team).is_dir()`)
73
- rather than path-string parsing so the resolver works correctly even when
74
- workspace paths legitimately contain a `.team` segment.
75
- """
76
- direct_parent = spec_path.parent
77
- if (direct_parent / ".team").is_dir():
78
- return direct_parent
79
- for ancestor in direct_parent.parents:
80
- if (ancestor / ".team").is_dir():
81
- return ancestor
82
- return direct_parent
83
-
84
-
85
- def validate_spec(spec: dict[str, Any], base_dir: Path | None = None) -> None:
86
- messages = _basic_schema_errors(spec)
87
- messages.extend(_semantic_errors(spec, base_dir or Path.cwd()))
88
- if messages:
89
- joined = "\n".join(f"- {m}" for m in messages)
90
- raise ValidationError(f"team.spec.yaml validation failed:\n{joined}")
91
-
92
-
93
- RESULT_COLLECTION_SCHEMAS: dict[str, tuple[set[str], set[str]]] = {
94
- "changes": ({"path", "kind", "description"}, {"path", "kind", "description"}),
95
- "tests": ({"command", "status"}, {"command", "status", "detail"}),
96
- "risks": ({"severity", "description"}, {"severity", "description"}),
97
- "artifacts": ({"path", "description"}, {"path", "description"}),
98
- "next_actions": ({"description"}, {"description"}),
99
- }
100
-
101
-
102
- def validate_result_envelope(envelope: dict[str, Any]) -> None:
103
- errors = _result_schema_errors(envelope)
104
- if errors:
105
- joined = "\n".join(f"- {error}" for error in errors)
106
- raise ValidationError(f"result_envelope_v1 validation failed:\n{joined}")
107
-
108
-
109
- def _basic_schema_errors(spec: dict[str, Any]) -> list[str]:
110
- errors: list[str] = []
111
- root_keys = {"version", "team", "leader", "agents", "routing", "communication", "runtime", "context", "tasks"}
112
- _check_keys(spec, "/", root_keys, root_keys, errors)
113
- if spec.get("version") != 1:
114
- errors.append("/version: must equal 1")
115
- _check_keys(spec.get("team"), "/team", {"name", "mode", "objective", "workspace"}, {"name", "mode", "objective", "workspace"}, errors)
116
- if spec.get("team", {}).get("mode") not in {"supervisor_worker", "swarm_limited"}:
117
- errors.append("/team/mode: invalid mode")
118
- _check_keys(
119
- spec.get("leader"),
120
- "/leader",
121
- {"id", "role", "provider", "model", "tools", "context_policy"},
122
- {"id", "role", "provider", "model", "tools", "context_policy"},
123
- errors,
124
- )
125
- _check_context_policy(spec.get("leader", {}).get("context_policy"), errors)
126
- if not isinstance(spec.get("agents"), list) or not spec.get("agents"):
127
- errors.append("/agents: must be a non-empty list")
128
- else:
129
- for idx, agent in enumerate(spec["agents"]):
130
- _check_agent(agent, f"/agents/{idx}", errors)
131
- _check_routing(spec.get("routing"), errors)
132
- _check_communication(spec.get("communication"), errors)
133
- _check_runtime(spec.get("runtime"), errors)
134
- _check_context(spec.get("context"), errors)
135
- if not isinstance(spec.get("tasks"), list):
136
- errors.append("/tasks: must be a list")
137
- else:
138
- for idx, task in enumerate(spec["tasks"]):
139
- _check_task(task, f"/tasks/{idx}", errors)
140
- return errors
141
-
142
-
143
- def _result_schema_errors(envelope: Any) -> list[str]:
144
- errors: list[str] = []
145
- required = {"schema_version", "task_id", "agent_id", "status", "summary", "changes", "tests", "risks", "artifacts", "next_actions"}
146
- _check_keys(envelope, "/", required, required, errors)
147
- if not isinstance(envelope, dict):
148
- return errors
149
- if envelope.get("schema_version") != "result_envelope_v1":
150
- errors.append("/schema_version: must be result_envelope_v1")
151
- for field in ["task_id", "agent_id", "summary"]:
152
- if field in envelope and not isinstance(envelope[field], str):
153
- errors.append(f"/{field}: must be a string")
154
- elif field in envelope and not envelope[field]:
155
- errors.append(f"/{field}: must not be empty")
156
- if envelope.get("status") not in {"success", "blocked", "failed", "partial"}:
157
- errors.append("/status: invalid result status")
158
- if "schema" in envelope:
159
- errors.append("/schema: use schema_version, not schema")
160
- for field, (item_required, item_allowed) in RESULT_COLLECTION_SCHEMAS.items():
161
- if field not in envelope:
162
- continue
163
- value = envelope[field]
164
- if not isinstance(value, list):
165
- errors.append(f"/{field}: must be a list")
166
- continue
167
- for idx, item in enumerate(value):
168
- item_path = f"/{field}/{idx}"
169
- _check_keys(item, item_path, item_required, item_allowed, errors)
170
- if not isinstance(item, dict):
171
- continue
172
- if field == "changes" and item.get("kind") not in {"created", "modified", "deleted", "observed"}:
173
- errors.append(f"{item_path}/kind: invalid change kind")
174
- if field == "tests" and item.get("status") not in {"passed", "failed", "not_run", "skipped"}:
175
- errors.append(f"{item_path}/status: invalid test status")
176
- if field == "risks" and item.get("severity") not in {"low", "medium", "high"}:
177
- errors.append(f"{item_path}/severity: invalid risk severity")
178
- for key, child in item.items():
179
- if key in item_allowed and not isinstance(child, str):
180
- errors.append(f"{item_path}/{key}: must be a string")
181
- return errors
182
-
183
-
184
- def _check_agent(agent: Any, path: str, errors: list[str]) -> None:
185
- required = {"id", "role", "provider", "model", "working_directory", "system_prompt", "tools", "permission_mode", "preferred_for", "avoid_for", "output_contract"}
186
- allowed = required | {"paused", "auth_mode", "profile", "credential_ref", "forked_from"}
187
- _check_keys(agent, path, required, allowed, errors)
188
- if not isinstance(agent, dict):
189
- return
190
- _check_keys(agent.get("system_prompt"), f"{path}/system_prompt", {"inline", "file"}, {"inline", "file"}, errors)
191
- _check_list(agent.get("tools"), f"{path}/tools", errors)
192
- _check_list(agent.get("preferred_for"), f"{path}/preferred_for", errors)
193
- _check_list(agent.get("avoid_for"), f"{path}/avoid_for", errors)
194
- _check_keys(agent.get("output_contract"), f"{path}/output_contract", {"format", "required_fields"}, {"format", "required_fields"}, errors)
195
- if agent.get("output_contract", {}).get("format") != "result_envelope_v1":
196
- errors.append(f"{path}/output_contract/format: must be result_envelope_v1")
197
-
198
-
199
- def _check_context_policy(policy: Any, errors: list[str]) -> None:
200
- _check_keys(
201
- policy,
202
- "/leader/context_policy",
203
- {"keep_user_thread", "receive_worker_outputs", "max_worker_result_tokens"},
204
- {"keep_user_thread", "receive_worker_outputs", "max_worker_result_tokens"},
205
- errors,
206
- )
207
-
208
-
209
- def _check_routing(routing: Any, errors: list[str]) -> None:
210
- _check_keys(routing, "/routing", {"default_assignee", "rules"}, {"default_assignee", "rules"}, errors)
211
- if not isinstance(routing, dict):
212
- return
213
- if not isinstance(routing.get("rules"), list):
214
- errors.append("/routing/rules: must be a list")
215
- return
216
- for idx, rule in enumerate(routing["rules"]):
217
- allowed = {"id", "when", "match", "assign_to", "priority"}
218
- required = {"id", "assign_to", "priority"}
219
- _check_keys(rule, f"/routing/rules/{idx}", required, allowed, errors)
220
- if isinstance(rule, dict) and not (rule.get("when") or rule.get("match")):
221
- errors.append(f"/routing/rules/{idx}: must include when or match")
222
-
223
-
224
- def _check_communication(comm: Any, errors: list[str]) -> None:
225
- required = {"protocol", "topology", "worker_to_worker", "ack_timeout_sec", "result_format", "message_store"}
226
- _check_keys(comm, "/communication", required, required, errors)
227
- if not isinstance(comm, dict):
228
- return
229
- if comm.get("protocol") not in {"mcp_inbox", "file_bus"}:
230
- errors.append("/communication/protocol: invalid protocol")
231
- if comm.get("result_format") != "result_envelope_v1":
232
- errors.append("/communication/result_format: must be result_envelope_v1")
233
- _check_keys(comm.get("message_store"), "/communication/message_store", {"sqlite", "mirror_files"}, {"sqlite", "mirror_files"}, errors)
234
-
235
-
236
- def _check_runtime(runtime: Any, errors: list[str]) -> None:
237
- required = {"backend", "session_name", "auto_launch", "require_user_approval_before_launch", "max_active_agents", "startup_order"}
238
- allowed = required | {"display_backend"} | {
239
- "dangerous_auto_approve",
240
- "auto_attach_leader",
241
- "fast",
242
- "tick_interval_sec",
243
- "push_min_interval_sec",
244
- "stuck_timeout_sec",
245
- # Gap 29 / F3 deprecation (2026-05-26): accept the legacy spec opt-in so
246
- # YAMLs that still set it validate and the deprecation warning + structured
247
- # event in messaging/leader_panes.py can fire. The preferred per-session
248
- # opt-in is the env var TEAM_AGENT_AUTO_TRUST_OWN_WORKSPACE; this spec
249
- # field will be removed in 0.3.0.
250
- "auto_trust_own_workspace",
251
- }
252
- _check_keys(runtime, "/runtime", required, allowed, errors)
253
- if not isinstance(runtime, dict):
254
- return
255
- if runtime.get("backend") not in {"tmux", "pty"}:
256
- errors.append("/runtime/backend: invalid backend")
257
- if "display_backend" in runtime and runtime.get("display_backend") not in VALID_DISPLAY_BACKENDS:
258
- errors.append("/runtime/display_backend: invalid display backend")
259
- if "dangerous_auto_approve" in runtime and not isinstance(runtime["dangerous_auto_approve"], bool):
260
- errors.append("/runtime/dangerous_auto_approve: must be a boolean")
261
- if "auto_trust_own_workspace" in runtime and not isinstance(runtime["auto_trust_own_workspace"], bool):
262
- errors.append("/runtime/auto_trust_own_workspace: must be a boolean")
263
- _check_list(runtime.get("startup_order"), "/runtime/startup_order", errors)
264
-
265
-
266
- def _check_context(context: Any, errors: list[str]) -> None:
267
- required = {"state_file", "artifact_dir", "log_dir", "summarization"}
268
- _check_keys(context, "/context", required, required, errors)
269
- if isinstance(context, dict):
270
- _check_keys(context.get("summarization"), "/context/summarization", {"worker_full_logs", "state_update"}, {"worker_full_logs", "state_update"}, errors)
271
-
272
-
273
- def _check_task(task: Any, path: str, errors: list[str]) -> None:
274
- required = {"id", "title", "type", "assignee", "deps", "acceptance", "status"}
275
- allowed = required | {"description", "requires_tools", "files", "risk", "retry_limit", "human_confirmation"}
276
- _check_keys(task, path, required, allowed, errors)
277
- if not isinstance(task, dict):
278
- return
279
- _check_list(task.get("deps"), f"{path}/deps", errors)
280
- _check_list(task.get("acceptance"), f"{path}/acceptance", errors)
281
- if task.get("status") not in {"pending", "ready", "running", "blocked", "needs_retry", "done", "failed", "cancelled"}:
282
- errors.append(f"{path}/status: invalid task status")
283
-
284
-
285
- def _check_keys(obj: Any, path: str, required: set[str], allowed: set[str], errors: list[str]) -> None:
286
- if not isinstance(obj, dict):
287
- errors.append(f"{path}: must be an object")
288
- return
289
- missing = sorted(required - set(obj))
290
- for key in missing:
291
- errors.append(f"{path.rstrip('/')}/{key}: missing required field")
292
- unknown = sorted(set(obj) - allowed)
293
- for key in unknown:
294
- errors.append(f"{path.rstrip('/')}/{key}: unknown field")
295
-
296
-
297
- def _check_list(value: Any, path: str, errors: list[str]) -> None:
298
- if not isinstance(value, list):
299
- errors.append(f"{path}: must be a list")
300
-
301
-
302
- def _semantic_errors(spec: dict[str, Any], base_dir: Path) -> list[str]:
303
- errors: list[str] = []
304
- leader = spec.get("leader", {})
305
- agents = spec.get("agents", [])
306
- agent_ids = {a.get("id") for a in agents if isinstance(a, dict)}
307
- all_ids = set(agent_ids)
308
- if len(agent_ids) != len([a for a in agents if isinstance(a, dict)]):
309
- errors.append("/agents: duplicate agent id")
310
- if leader.get("id"):
311
- all_ids.add(leader["id"])
312
-
313
- for path, provider in [("/leader/provider", leader.get("provider"))]:
314
- if provider not in SUPPORTED_PROVIDERS:
315
- errors.append(f"{path}: unknown provider {provider!r}")
316
- for idx, agent in enumerate(agents):
317
- provider = agent.get("provider")
318
- if provider not in SUPPORTED_PROVIDERS:
319
- errors.append(f"/agents/{idx}/provider: unknown provider {provider!r}")
320
- auth_mode = agent.get("auth_mode")
321
- if auth_mode is not None and auth_mode not in AUTH_MODES:
322
- errors.append(f"/agents/{idx}/auth_mode: unknown auth_mode {auth_mode!r}")
323
- prompt_file = agent.get("system_prompt", {}).get("file")
324
- if prompt_file:
325
- candidate = Path(prompt_file)
326
- if not candidate.is_absolute():
327
- candidate = base_dir / candidate
328
- if not candidate.exists():
329
- errors.append(f"/agents/{idx}/system_prompt/file: file not found: {candidate}")
330
- for tool in expand_tools(agent.get("tools", [])):
331
- if tool not in CANONICAL_TOOLS:
332
- errors.append(f"/agents/{idx}/tools: unknown tool {tool!r}")
333
-
334
- leader_tools = leader.get("tools", [])
335
- for tool in expand_tools(leader_tools):
336
- if tool not in CANONICAL_TOOLS:
337
- errors.append(f"/leader/tools: unknown tool {tool!r}")
338
-
339
- routing = spec.get("routing", {})
340
- default_assignee = routing.get("default_assignee")
341
- if default_assignee and default_assignee not in all_ids:
342
- errors.append(f"/routing/default_assignee: unknown agent {default_assignee!r}")
343
- for idx, rule in enumerate(routing.get("rules", [])):
344
- target = rule.get("assign_to")
345
- if target not in all_ids:
346
- errors.append(f"/routing/rules/{idx}/assign_to: unknown agent {target!r}")
347
-
348
- tasks = spec.get("tasks", [])
349
- task_ids = {t.get("id") for t in tasks if isinstance(t, dict)}
350
- for idx, task in enumerate(tasks):
351
- assignee = task.get("assignee")
352
- if assignee and assignee not in all_ids:
353
- errors.append(f"/tasks/{idx}/assignee: unknown agent {assignee!r}")
354
- for dep in task.get("deps", []):
355
- if dep not in task_ids:
356
- errors.append(f"/tasks/{idx}/deps: unknown dependency {dep!r}")
357
-
358
- cycle = find_dependency_cycle(tasks)
359
- if cycle:
360
- errors.append(f"/tasks: dependency cycle detected: {' -> '.join(cycle)}")
361
- return errors
362
-
363
-
364
- def workspace_from_spec(spec: dict[str, Any], spec_path: Path | None = None) -> Path:
365
- raw = spec.get("team", {}).get("workspace") or "."
366
- path = Path(raw)
367
- if path.is_absolute():
368
- return path
369
- base = spec_path.parent if spec_path else Path.cwd()
370
- return (base / path).resolve()