@team-agent/installer 0.2.11 → 0.3.1
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.
- package/Cargo.lock +744 -0
- package/Cargo.toml +34 -0
- package/crates/team-agent/Cargo.toml +33 -0
- package/crates/team-agent/src/cli/adapters.rs +1343 -0
- package/crates/team-agent/src/cli/diagnose.rs +554 -0
- package/crates/team-agent/src/cli/emit.rs +1204 -0
- package/crates/team-agent/src/cli/helpers.rs +88 -0
- package/crates/team-agent/src/cli/leader.rs +216 -0
- package/crates/team-agent/src/cli/mod.rs +1207 -0
- package/crates/team-agent/src/cli/profile.rs +306 -0
- package/crates/team-agent/src/cli/send.rs +215 -0
- package/crates/team-agent/src/cli/status.rs +179 -0
- package/crates/team-agent/src/cli/status_port.rs +502 -0
- package/crates/team-agent/src/cli/tests/base.rs +616 -0
- package/crates/team-agent/src/cli/tests/compile.rs +96 -0
- package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
- package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
- package/crates/team-agent/src/cli/tests/mod.rs +97 -0
- package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
- package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
- package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
- package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
- package/crates/team-agent/src/cli/types.rs +605 -0
- package/crates/team-agent/src/compiler/tests.rs +701 -0
- package/crates/team-agent/src/compiler.rs +489 -0
- package/crates/team-agent/src/coordinator/backoff.rs +153 -0
- package/crates/team-agent/src/coordinator/health.rs +557 -0
- package/crates/team-agent/src/coordinator/mod.rs +80 -0
- package/crates/team-agent/src/coordinator/orphan.rs +179 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
- package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
- package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
- package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
- package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
- package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
- package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
- package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
- package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
- package/crates/team-agent/src/coordinator/tick.rs +2032 -0
- package/crates/team-agent/src/coordinator/types.rs +584 -0
- package/crates/team-agent/src/db/migration.rs +716 -0
- package/crates/team-agent/src/db/mod.rs +23 -0
- package/crates/team-agent/src/db/schema.rs +378 -0
- package/crates/team-agent/src/event_log.rs +375 -0
- package/crates/team-agent/src/fake_worker.rs +253 -0
- package/crates/team-agent/src/leader/helpers.rs +190 -0
- package/crates/team-agent/src/leader/inject.rs +33 -0
- package/crates/team-agent/src/leader/lease.rs +1084 -0
- package/crates/team-agent/src/leader/mod.rs +99 -0
- package/crates/team-agent/src/leader/owner_bind.rs +292 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +526 -0
- package/crates/team-agent/src/leader/rediscover.rs +1101 -0
- package/crates/team-agent/src/leader/start.rs +273 -0
- package/crates/team-agent/src/leader/takeover.rs +235 -0
- package/crates/team-agent/src/leader/tests/basics.rs +183 -0
- package/crates/team-agent/src/leader/tests/byte_findings.rs +237 -0
- package/crates/team-agent/src/leader/tests/identity.rs +206 -0
- package/crates/team-agent/src/leader/tests/idle.rs +272 -0
- package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
- package/crates/team-agent/src/leader/tests/lease_claim.rs +410 -0
- package/crates/team-agent/src/leader/tests/mod.rs +125 -0
- package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
- package/crates/team-agent/src/leader/types.rs +489 -0
- package/crates/team-agent/src/lib.rs +85 -0
- package/crates/team-agent/src/lifecycle/display.rs +228 -0
- package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
- package/crates/team-agent/src/lifecycle/launch.rs +2109 -0
- package/crates/team-agent/src/lifecycle/mod.rs +62 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
- package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
- package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
- package/crates/team-agent/src/lifecycle/restart.rs +76 -0
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
- package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +985 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests.rs +27 -0
- package/crates/team-agent/src/lifecycle/types.rs +710 -0
- package/crates/team-agent/src/main.rs +41 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
- package/crates/team-agent/src/mcp_server/mod.rs +183 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
- package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
- package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
- package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +187 -0
- package/crates/team-agent/src/mcp_server/tests.rs +38 -0
- package/crates/team-agent/src/mcp_server/tools.rs +603 -0
- package/crates/team-agent/src/mcp_server/types.rs +421 -0
- package/crates/team-agent/src/mcp_server/wire.rs +468 -0
- package/crates/team-agent/src/message_store.rs +767 -0
- package/crates/team-agent/src/messaging/activity.rs +433 -0
- package/crates/team-agent/src/messaging/delivery.rs +743 -0
- package/crates/team-agent/src/messaging/helpers.rs +209 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +329 -0
- package/crates/team-agent/src/messaging/mod.rs +147 -0
- package/crates/team-agent/src/messaging/peers.rs +32 -0
- package/crates/team-agent/src/messaging/results.rs +553 -0
- package/crates/team-agent/src/messaging/scheduler.rs +344 -0
- package/crates/team-agent/src/messaging/selftest.rs +100 -0
- package/crates/team-agent/src/messaging/send.rs +578 -0
- package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
- package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
- package/crates/team-agent/src/messaging/trust.rs +192 -0
- package/crates/team-agent/src/messaging/types.rs +355 -0
- package/crates/team-agent/src/messaging/watchers.rs +591 -0
- package/crates/team-agent/src/model/enums.rs +311 -0
- package/crates/team-agent/src/model/errors.rs +17 -0
- package/crates/team-agent/src/model/ids.rs +155 -0
- package/crates/team-agent/src/model/mod.rs +22 -0
- package/crates/team-agent/src/model/paths.rs +228 -0
- package/crates/team-agent/src/model/permissions.rs +567 -0
- package/crates/team-agent/src/model/routing.rs +340 -0
- package/crates/team-agent/src/model/spec.rs +680 -0
- package/crates/team-agent/src/model/task_graph.rs +380 -0
- package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
- package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
- package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
- package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
- package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
- package/crates/team-agent/src/model/yaml/tests.rs +288 -0
- package/crates/team-agent/src/model/yaml.rs +800 -0
- package/crates/team-agent/src/packaging/install.rs +305 -0
- package/crates/team-agent/src/packaging/migrate.rs +30 -0
- package/crates/team-agent/src/packaging/mod.rs +82 -0
- package/crates/team-agent/src/packaging/repair.rs +24 -0
- package/crates/team-agent/src/packaging/tests.rs +829 -0
- package/crates/team-agent/src/packaging/types.rs +369 -0
- package/crates/team-agent/src/provider/adapter.rs +801 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
- package/crates/team-agent/src/provider/classify.rs +456 -0
- package/crates/team-agent/src/provider/faults.rs +136 -0
- package/crates/team-agent/src/provider/helpers.rs +41 -0
- package/crates/team-agent/src/provider/mod.rs +53 -0
- package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
- package/crates/team-agent/src/provider/tests/classify.rs +240 -0
- package/crates/team-agent/src/provider/tests/faults.rs +120 -0
- package/crates/team-agent/src/provider/tests/idle.rs +208 -0
- package/crates/team-agent/src/provider/tests/wire.rs +213 -0
- package/crates/team-agent/src/provider/tests.rs +31 -0
- package/crates/team-agent/src/provider/types.rs +424 -0
- package/crates/team-agent/src/state/identity.rs +659 -0
- package/crates/team-agent/src/state/mod.rs +58 -0
- package/crates/team-agent/src/state/owner_gate.rs +423 -0
- package/crates/team-agent/src/state/persist.rs +712 -0
- package/crates/team-agent/src/state/projection.rs +657 -0
- package/crates/team-agent/src/state/selector.rs +105 -0
- package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +765 -0
- package/crates/team-agent/src/tmux_backend.rs +810 -0
- package/crates/team-agent/src/transport/test_support.rs +252 -0
- package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
- package/crates/team-agent/src/transport/tests/mod.rs +199 -0
- package/crates/team-agent/src/transport/tests/wire.rs +527 -0
- package/crates/team-agent/src/transport.rs +774 -0
- package/npm/install.mjs +118 -112
- package/package.json +15 -13
- package/crates/team-agent-core/Cargo.toml +0 -12
- package/crates/team-agent-core/src/lib.rs +0 -332
- package/crates/team-agent-core/src/main.rs +0 -152
- package/pyproject.toml +0 -18
- package/scripts/install.py +0 -88
- package/scripts/run_regression_tests.py +0 -83
- package/src/team_agent/__init__.py +0 -3
- package/src/team_agent/__main__.py +0 -5
- package/src/team_agent/_legacy_pane_discovery.py +0 -186
- package/src/team_agent/abnormal_track.py +0 -253
- package/src/team_agent/approvals/__init__.py +0 -65
- package/src/team_agent/approvals/constants.py +0 -6
- package/src/team_agent/approvals/parsing.py +0 -176
- package/src/team_agent/approvals/runtime_prompts.py +0 -171
- package/src/team_agent/approvals/status.py +0 -176
- package/src/team_agent/cli/__init__.py +0 -137
- package/src/team_agent/cli/commands.py +0 -481
- package/src/team_agent/cli/e2e.py +0 -202
- package/src/team_agent/cli/helpers.py +0 -226
- package/src/team_agent/cli/parser.py +0 -540
- package/src/team_agent/compiler.py +0 -334
- package/src/team_agent/coordinator/__init__.py +0 -53
- package/src/team_agent/coordinator/__main__.py +0 -119
- package/src/team_agent/coordinator/lifecycle.py +0 -411
- package/src/team_agent/coordinator/metadata.py +0 -61
- package/src/team_agent/coordinator/paths.py +0 -17
- package/src/team_agent/diagnose/__init__.py +0 -48
- package/src/team_agent/diagnose/checks.py +0 -101
- package/src/team_agent/diagnose/comms.py +0 -213
- package/src/team_agent/diagnose/health.py +0 -241
- package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
- package/src/team_agent/diagnose/preflight.py +0 -194
- package/src/team_agent/diagnose/quick_start.py +0 -324
- package/src/team_agent/display/__init__.py +0 -92
- package/src/team_agent/display/adaptive.py +0 -511
- package/src/team_agent/display/backend.py +0 -46
- package/src/team_agent/display/close.py +0 -154
- package/src/team_agent/display/ghostty.py +0 -77
- package/src/team_agent/display/rebuild.py +0 -102
- package/src/team_agent/display/tiling.py +0 -156
- package/src/team_agent/display/worker_window.py +0 -114
- package/src/team_agent/display/workspace.py +0 -382
- package/src/team_agent/errors.py +0 -10
- package/src/team_agent/events.py +0 -84
- package/src/team_agent/fake_worker.py +0 -80
- package/src/team_agent/idle_predicate.py +0 -218
- package/src/team_agent/idle_takeover.py +0 -59
- package/src/team_agent/idle_takeover_wiring.py +0 -114
- package/src/team_agent/launch/__init__.py +0 -41
- package/src/team_agent/launch/bootstrap.py +0 -85
- package/src/team_agent/launch/config.py +0 -106
- package/src/team_agent/launch/core.py +0 -301
- package/src/team_agent/launch/requirements.py +0 -57
- package/src/team_agent/leader/__init__.py +0 -926
- package/src/team_agent/leader_binding.py +0 -183
- package/src/team_agent/lifecycle/__init__.py +0 -5
- package/src/team_agent/lifecycle/agents.py +0 -278
- package/src/team_agent/lifecycle/operations.py +0 -411
- package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
- package/src/team_agent/lifecycle/start.py +0 -363
- package/src/team_agent/mcp_server/__init__.py +0 -42
- package/src/team_agent/mcp_server/__main__.py +0 -7
- package/src/team_agent/mcp_server/contracts.py +0 -148
- package/src/team_agent/mcp_server/normalize.py +0 -257
- package/src/team_agent/mcp_server/server.py +0 -150
- package/src/team_agent/mcp_server/tools.py +0 -352
- package/src/team_agent/message_store/__init__.py +0 -23
- package/src/team_agent/message_store/agent_health.py +0 -113
- package/src/team_agent/message_store/core.py +0 -497
- package/src/team_agent/message_store/leader_notification_log.py +0 -198
- package/src/team_agent/message_store/result_watchers.py +0 -251
- package/src/team_agent/message_store/schema.py +0 -308
- package/src/team_agent/message_store/schema_migration.py +0 -448
- package/src/team_agent/messaging/__init__.py +0 -1
- package/src/team_agent/messaging/activity_detector.py +0 -262
- package/src/team_agent/messaging/delivery.py +0 -504
- package/src/team_agent/messaging/deps.py +0 -247
- package/src/team_agent/messaging/idle_alerts.py +0 -423
- package/src/team_agent/messaging/internal_delivery.py +0 -46
- package/src/team_agent/messaging/leader.py +0 -497
- package/src/team_agent/messaging/leader_api_errors.py +0 -216
- package/src/team_agent/messaging/leader_panes.py +0 -673
- package/src/team_agent/messaging/owner_bypass.py +0 -29
- package/src/team_agent/messaging/result_delivery.py +0 -539
- package/src/team_agent/messaging/results.py +0 -447
- package/src/team_agent/messaging/scheduler.py +0 -450
- package/src/team_agent/messaging/send.py +0 -532
- package/src/team_agent/messaging/session_drift.py +0 -94
- package/src/team_agent/messaging/tmux_io.py +0 -506
- package/src/team_agent/messaging/tmux_prompt.py +0 -338
- package/src/team_agent/messaging/trust_auto_answer.py +0 -52
- package/src/team_agent/orchestrator/__init__.py +0 -376
- package/src/team_agent/orchestrator/plan.py +0 -122
- package/src/team_agent/orchestrator/state.py +0 -128
- package/src/team_agent/paths.py +0 -45
- package/src/team_agent/permissions.py +0 -123
- package/src/team_agent/profiles/__init__.py +0 -82
- package/src/team_agent/profiles/constants.py +0 -19
- package/src/team_agent/profiles/core.py +0 -407
- package/src/team_agent/profiles/helpers.py +0 -69
- package/src/team_agent/profiles/provider_env.py +0 -188
- package/src/team_agent/profiles/smoke.py +0 -201
- package/src/team_agent/provider_cli/__init__.py +0 -43
- package/src/team_agent/provider_cli/adapter.py +0 -172
- package/src/team_agent/provider_cli/base.py +0 -48
- package/src/team_agent/provider_cli/claude.py +0 -503
- package/src/team_agent/provider_cli/codex.py +0 -336
- package/src/team_agent/provider_cli/copilot.py +0 -8
- package/src/team_agent/provider_cli/fake.py +0 -39
- package/src/team_agent/provider_cli/gemini.py +0 -95
- package/src/team_agent/provider_cli/opencode.py +0 -8
- package/src/team_agent/provider_cli/prompt.py +0 -62
- package/src/team_agent/provider_cli/registry.py +0 -18
- package/src/team_agent/provider_cli/unsupported.py +0 -32
- package/src/team_agent/provider_state/README.md +0 -78
- package/src/team_agent/provider_state/__init__.py +0 -91
- package/src/team_agent/provider_state/claude.py +0 -86
- package/src/team_agent/provider_state/codex.py +0 -84
- package/src/team_agent/provider_state/common.py +0 -207
- package/src/team_agent/provider_state/registry.py +0 -118
- package/src/team_agent/providers.py +0 -163
- package/src/team_agent/quality_gates.py +0 -104
- package/src/team_agent/restart/__init__.py +0 -34
- package/src/team_agent/restart/orchestration.py +0 -554
- package/src/team_agent/restart/selection.py +0 -89
- package/src/team_agent/restart/snapshot.py +0 -70
- package/src/team_agent/routing.py +0 -84
- package/src/team_agent/runtime.py +0 -1243
- package/src/team_agent/rust_core.py +0 -327
- package/src/team_agent/sessions/__init__.py +0 -25
- package/src/team_agent/sessions/capture.py +0 -144
- package/src/team_agent/sessions/inventory.py +0 -44
- package/src/team_agent/sessions/resume.py +0 -135
- package/src/team_agent/simple_yaml.py +0 -236
- package/src/team_agent/spec.py +0 -370
- package/src/team_agent/state.py +0 -693
- package/src/team_agent/status/__init__.py +0 -63
- package/src/team_agent/status/approvals.py +0 -52
- package/src/team_agent/status/compact.py +0 -158
- package/src/team_agent/status/constants.py +0 -18
- package/src/team_agent/status/inbox.py +0 -58
- package/src/team_agent/status/peek.py +0 -117
- package/src/team_agent/status/queries.py +0 -199
- package/src/team_agent/task_graph.py +0 -80
- package/src/team_agent/terminal.py +0 -57
- package/src/team_agent/wake.py +0 -58
- 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()
|
package/src/team_agent/spec.py
DELETED
|
@@ -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()
|