@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,218 +0,0 @@
|
|
|
1
|
-
"""Provider-neutral take-over reminder predicate (Gap 32 §3).
|
|
2
|
-
|
|
3
|
-
Consumes already-classified node states only. Contains no provider knowledge.
|
|
4
|
-
Rules: arm only after a worker has opened a turn (C1); fire one neutral ping when
|
|
5
|
-
every node is idle for a monotonic debounce window (C2/C11); idle_interrupted
|
|
6
|
-
counts as idle but is annotated (C12); re-arm on a real turn-open edge (C3).
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
from typing import Any
|
|
12
|
-
|
|
13
|
-
_IDLE_STATES = {"idle", "idle_interrupted"}
|
|
14
|
-
_DELEGATED_STATES = {"working", "blocked_on_human", "idle_interrupted", "abnormal"}
|
|
15
|
-
|
|
16
|
-
_ARM_KEY = "opened_worker_turn_since_ack"
|
|
17
|
-
_SUPPRESS_KEY = "suppressed"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def evaluate_takeover_reminder(
|
|
21
|
-
nodes: list[dict[str, Any]],
|
|
22
|
-
*,
|
|
23
|
-
monitor_state: dict[str, Any] | None,
|
|
24
|
-
now_monotonic: float,
|
|
25
|
-
debounce_seconds: float,
|
|
26
|
-
suspend_intervals: list[tuple[float, float]] | None = None,
|
|
27
|
-
event_sink: Any = None,
|
|
28
|
-
) -> dict[str, Any]:
|
|
29
|
-
state = dict(monitor_state or {})
|
|
30
|
-
state.setdefault(_ARM_KEY, False)
|
|
31
|
-
state.setdefault(_SUPPRESS_KEY, False)
|
|
32
|
-
state.setdefault("all_idle_since", None)
|
|
33
|
-
state.setdefault("pinged_for_episode", None)
|
|
34
|
-
|
|
35
|
-
# C1: a WORKER turn-open (working / blocked / interrupted / faulted) is the
|
|
36
|
-
# only thing that arms the watch. Leader-only activity never arms it.
|
|
37
|
-
for node in nodes:
|
|
38
|
-
if _role(node) == "leader":
|
|
39
|
-
continue
|
|
40
|
-
if node.get("state") in _DELEGATED_STATES:
|
|
41
|
-
state[_ARM_KEY] = True
|
|
42
|
-
|
|
43
|
-
# Any non-idle node blocks the ping; report which kind (C5 unknown / C14 working).
|
|
44
|
-
for node in nodes:
|
|
45
|
-
node_state = node.get("state")
|
|
46
|
-
if node_state not in _IDLE_STATES:
|
|
47
|
-
state["all_idle_since"] = None
|
|
48
|
-
state["pinged_for_episode"] = None
|
|
49
|
-
return _result(False, None, f"node_{node_state or 'unknown'}", _interrupted(nodes), state, event_sink=event_sink, node=node)
|
|
50
|
-
|
|
51
|
-
if not nodes:
|
|
52
|
-
return _result(False, None, "no_nodes", [], state, event_sink=event_sink)
|
|
53
|
-
|
|
54
|
-
if state.get("all_idle_since") is None:
|
|
55
|
-
state["all_idle_since"] = now_monotonic
|
|
56
|
-
state["pinged_for_episode"] = None
|
|
57
|
-
elapsed = _active_elapsed(state["all_idle_since"], now_monotonic, suspend_intervals)
|
|
58
|
-
interrupted = _interrupted(nodes)
|
|
59
|
-
|
|
60
|
-
if not state.get(_ARM_KEY):
|
|
61
|
-
return _result(False, None, "not_armed_no_worker_turn", interrupted, state, event_sink=event_sink)
|
|
62
|
-
if state.get(_SUPPRESS_KEY):
|
|
63
|
-
return _result(False, None, "acknowledged", interrupted, state, event_sink=event_sink)
|
|
64
|
-
if elapsed < debounce_seconds:
|
|
65
|
-
return _result(False, None, "debounce_active", interrupted, state, event_sink=event_sink)
|
|
66
|
-
if state.get("pinged_for_episode") == state.get("all_idle_since"):
|
|
67
|
-
return _result(False, None, "already_pinged_this_episode", interrupted, state, event_sink=event_sink)
|
|
68
|
-
|
|
69
|
-
state["pinged_for_episode"] = state["all_idle_since"]
|
|
70
|
-
message = _neutral_message(len(nodes), elapsed, interrupted)
|
|
71
|
-
_emit(event_sink, "idle_takeover.ping", nodes=len(nodes), elapsed_seconds=int(elapsed), interrupted=[i["node_id"] for i in interrupted])
|
|
72
|
-
return _result(True, message, "all_idle_debounce_elapsed", interrupted, state, event_sink=event_sink)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def record_turn_open_after_delivery(
|
|
76
|
-
monitor_state: dict[str, Any] | None,
|
|
77
|
-
*,
|
|
78
|
-
node_id: str,
|
|
79
|
-
turn_id: str | None,
|
|
80
|
-
delivered_message_id: str | None,
|
|
81
|
-
now_monotonic: float,
|
|
82
|
-
event_sink: Any = None,
|
|
83
|
-
) -> dict[str, Any]:
|
|
84
|
-
"""A delivered inbound message produced a real turn-open edge (C3).
|
|
85
|
-
|
|
86
|
-
Re-arms a previously acknowledged watch so delivered-but-unprocessed work
|
|
87
|
-
can never leave it permanently suppressed. Returns the updated monitor_state
|
|
88
|
-
directly (with the re-arm flags set).
|
|
89
|
-
"""
|
|
90
|
-
state = dict(monitor_state or {})
|
|
91
|
-
state[_ARM_KEY] = True
|
|
92
|
-
state[_SUPPRESS_KEY] = False
|
|
93
|
-
state["all_idle_since"] = None
|
|
94
|
-
state["pinged_for_episode"] = None
|
|
95
|
-
state["last_turn_open"] = {
|
|
96
|
-
"node_id": node_id,
|
|
97
|
-
"turn_id": turn_id,
|
|
98
|
-
"delivered_message_id": delivered_message_id,
|
|
99
|
-
"at": now_monotonic,
|
|
100
|
-
}
|
|
101
|
-
state["ok"] = True
|
|
102
|
-
state["rearmed"] = True
|
|
103
|
-
_emit(event_sink, "idle_takeover.turn_open_rearmed", node_id=node_id, turn_id=turn_id, delivered_message_id=delivered_message_id)
|
|
104
|
-
return state
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def _role(node: dict[str, Any]) -> str:
|
|
108
|
-
return str(node.get("role") or ("leader" if node.get("is_leader") else "worker"))
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def _interrupted(nodes: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
112
|
-
return [
|
|
113
|
-
{
|
|
114
|
-
"node_id": n.get("node_id"),
|
|
115
|
-
"node": n.get("node_id"),
|
|
116
|
-
"state": "idle_interrupted",
|
|
117
|
-
"reason": "interrupted",
|
|
118
|
-
"interrupted": True,
|
|
119
|
-
"kind": "interrupted",
|
|
120
|
-
"type": "interrupted",
|
|
121
|
-
"annotation": "interrupted",
|
|
122
|
-
}
|
|
123
|
-
for n in nodes
|
|
124
|
-
if n.get("state") == "idle_interrupted"
|
|
125
|
-
]
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _active_elapsed(start: float, now: float, suspend_intervals: list[tuple[float, float]] | None) -> float:
|
|
129
|
-
elapsed = max(0.0, now - start)
|
|
130
|
-
if not suspend_intervals:
|
|
131
|
-
return elapsed
|
|
132
|
-
# C11: clip each window to [start, now], then MERGE overlapping/duplicate
|
|
133
|
-
# windows before subtracting, so an overlap is never counted twice.
|
|
134
|
-
clipped: list[tuple[float, float]] = []
|
|
135
|
-
for interval in suspend_intervals:
|
|
136
|
-
try:
|
|
137
|
-
s, e = float(interval[0]), float(interval[1])
|
|
138
|
-
except (TypeError, ValueError, IndexError):
|
|
139
|
-
continue
|
|
140
|
-
lo = max(s, start)
|
|
141
|
-
hi = min(e, now)
|
|
142
|
-
if hi > lo:
|
|
143
|
-
clipped.append((lo, hi))
|
|
144
|
-
suspended = 0.0
|
|
145
|
-
for lo, hi in _merge_intervals(clipped):
|
|
146
|
-
suspended += hi - lo
|
|
147
|
-
return max(0.0, elapsed - suspended)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def _merge_intervals(intervals: list[tuple[float, float]]) -> list[tuple[float, float]]:
|
|
151
|
-
if not intervals:
|
|
152
|
-
return []
|
|
153
|
-
ordered = sorted(intervals)
|
|
154
|
-
merged: list[tuple[float, float]] = [ordered[0]]
|
|
155
|
-
for lo, hi in ordered[1:]:
|
|
156
|
-
last_lo, last_hi = merged[-1]
|
|
157
|
-
if lo <= last_hi: # overlapping or touching → merge
|
|
158
|
-
merged[-1] = (last_lo, max(last_hi, hi))
|
|
159
|
-
else:
|
|
160
|
-
merged.append((lo, hi))
|
|
161
|
-
return merged
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def _neutral_message(node_count: int, elapsed: float, interrupted: list[dict[str, Any]]) -> str:
|
|
165
|
-
minutes = max(1, int(round(elapsed / 60.0)))
|
|
166
|
-
base = (
|
|
167
|
-
f"All nodes idle: {node_count} team nodes have had every turn closed for "
|
|
168
|
-
f"about {minutes} min. If this idle state is intentional, run "
|
|
169
|
-
f"team-agent acknowledge-idle to confirm it."
|
|
170
|
-
)
|
|
171
|
-
if interrupted:
|
|
172
|
-
ids = ", ".join(str(i["node_id"]) for i in interrupted)
|
|
173
|
-
base += f" Interrupted nodes: {ids}."
|
|
174
|
-
return base
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def _result(
|
|
178
|
-
should_ping: bool,
|
|
179
|
-
message: str | None,
|
|
180
|
-
reason: str,
|
|
181
|
-
annotations: list[dict[str, Any]],
|
|
182
|
-
state: dict[str, Any],
|
|
183
|
-
*,
|
|
184
|
-
event_sink: Any = None,
|
|
185
|
-
node: dict[str, Any] | None = None,
|
|
186
|
-
) -> dict[str, Any]:
|
|
187
|
-
if not should_ping and state.get("last_no_ping_reason") != reason:
|
|
188
|
-
state["last_no_ping_reason"] = reason
|
|
189
|
-
_emit(
|
|
190
|
-
event_sink,
|
|
191
|
-
"idle_takeover.no_ping",
|
|
192
|
-
reason=reason,
|
|
193
|
-
node_id=(node or {}).get("node_id"),
|
|
194
|
-
armed=bool(state.get(_ARM_KEY)),
|
|
195
|
-
)
|
|
196
|
-
return {
|
|
197
|
-
"should_ping": should_ping,
|
|
198
|
-
"message": message,
|
|
199
|
-
"reason": reason,
|
|
200
|
-
"annotations": list(annotations),
|
|
201
|
-
"interrupted_nodes": [a.get("node_id") for a in annotations],
|
|
202
|
-
"interrupted": [a.get("node_id") for a in annotations],
|
|
203
|
-
"monitor_state": state,
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def _emit(event_sink: Any, name: str, **fields: Any) -> None:
|
|
208
|
-
if event_sink is None:
|
|
209
|
-
return
|
|
210
|
-
try:
|
|
211
|
-
event_sink(name, fields)
|
|
212
|
-
except TypeError:
|
|
213
|
-
try:
|
|
214
|
-
event_sink({"event": name, **fields})
|
|
215
|
-
except Exception:
|
|
216
|
-
pass
|
|
217
|
-
except Exception:
|
|
218
|
-
pass
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
"""Gap 32 idle/take-over public facade.
|
|
2
|
-
|
|
3
|
-
Thin surface that the runtime + acceptance contract import. Provider dispatch
|
|
4
|
-
lives in ``provider_state``; the predicate / abnormal / wake logic lives in the
|
|
5
|
-
provider-neutral modules. This module only wires them together.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
from typing import Any
|
|
11
|
-
|
|
12
|
-
from team_agent.abnormal_track import detect_whole_team_gone, process_abnormal_records
|
|
13
|
-
from team_agent.idle_predicate import evaluate_takeover_reminder, record_turn_open_after_delivery
|
|
14
|
-
from team_agent.provider_state import read_turn_state
|
|
15
|
-
from team_agent.provider_state.registry import get_provider_registry
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def classify_provider_turn_state(
|
|
19
|
-
provider: str,
|
|
20
|
-
session_log_text: str,
|
|
21
|
-
*,
|
|
22
|
-
process: Any = None,
|
|
23
|
-
file_silence_seconds: float = 0,
|
|
24
|
-
registry: Any = None,
|
|
25
|
-
event_sink: Any = None,
|
|
26
|
-
) -> dict[str, Any]:
|
|
27
|
-
"""Classify one node's turn state from its provider session-log text."""
|
|
28
|
-
result = read_turn_state(
|
|
29
|
-
provider,
|
|
30
|
-
session_log_text,
|
|
31
|
-
process=process,
|
|
32
|
-
file_silence_seconds=file_silence_seconds,
|
|
33
|
-
registry=registry,
|
|
34
|
-
)
|
|
35
|
-
if event_sink is not None and result.get("state") in {"unknown", "abnormal"}:
|
|
36
|
-
_emit(event_sink, "idle_takeover.classify", provider=provider, state=result.get("state"), reason=result.get("reason"))
|
|
37
|
-
return result
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
__all__ = [
|
|
41
|
-
"classify_provider_turn_state",
|
|
42
|
-
"evaluate_takeover_reminder",
|
|
43
|
-
"record_turn_open_after_delivery",
|
|
44
|
-
"process_abnormal_records",
|
|
45
|
-
"detect_whole_team_gone",
|
|
46
|
-
"get_provider_registry",
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _emit(event_sink: Any, name: str, **fields: Any) -> None:
|
|
51
|
-
try:
|
|
52
|
-
event_sink(name, fields)
|
|
53
|
-
except TypeError:
|
|
54
|
-
try:
|
|
55
|
-
event_sink({"event": name, **fields})
|
|
56
|
-
except Exception:
|
|
57
|
-
pass
|
|
58
|
-
except Exception:
|
|
59
|
-
pass
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
"""Gap 32 runtime wiring: drive the file-fact idle/takeover reminder from the
|
|
2
|
-
coordinator tick. This is the glue that replaces the legacy screen-scrape
|
|
3
|
-
`detect_idle_fallbacks` path — it classifies each node from its provider
|
|
4
|
-
session-log file (never the pane) and runs the provider-neutral predicate.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import time
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import Any
|
|
12
|
-
|
|
13
|
-
_TAIL_BYTES = 131072
|
|
14
|
-
_DEBOUNCE_SECONDS = 60.0
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
IDLE_DEBOUNCE_SECONDS = _DEBOUNCE_SECONDS
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def build_idle_nodes(state: dict[str, Any]) -> list[dict[str, Any]]:
|
|
21
|
-
"""Classify every live node from its provider session-log file fact (never
|
|
22
|
-
the pane screen, never message-row status). The leader is read via its own
|
|
23
|
-
transcript when its path is tracked (C13), else omitted rather than guessed.
|
|
24
|
-
"""
|
|
25
|
-
from team_agent.provider_state import read_turn_state
|
|
26
|
-
|
|
27
|
-
nodes: list[dict[str, Any]] = []
|
|
28
|
-
for agent_id, agent_state in (state.get("agents") or {}).items():
|
|
29
|
-
if str(agent_state.get("status") or "") in {"stopped", "paused"}:
|
|
30
|
-
continue
|
|
31
|
-
provider = str(agent_state.get("provider") or "")
|
|
32
|
-
classification = read_turn_state(provider, _read_session_tail(agent_state.get("rollout_path")))
|
|
33
|
-
nodes.append({
|
|
34
|
-
"node_id": agent_id,
|
|
35
|
-
"role": "worker",
|
|
36
|
-
"state": classification.get("state"),
|
|
37
|
-
"turn_id": classification.get("turn_id"),
|
|
38
|
-
"annotations": classification.get("annotations"),
|
|
39
|
-
"provider": provider,
|
|
40
|
-
"auth_mode": agent_state.get("auth_mode"),
|
|
41
|
-
"rollout_path": agent_state.get("rollout_path"),
|
|
42
|
-
})
|
|
43
|
-
leader_node = _leader_node(state)
|
|
44
|
-
if leader_node is not None:
|
|
45
|
-
nodes.append(leader_node)
|
|
46
|
-
return nodes
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def push_idle_reminder(workspace: Path, state: dict[str, Any], event_log: Any, result: dict[str, Any]) -> None:
|
|
50
|
-
"""Deliver the one neutral take-over reminder to the leader when the
|
|
51
|
-
predicate fired. No-op otherwise."""
|
|
52
|
-
if not result.get("should_ping"):
|
|
53
|
-
return
|
|
54
|
-
from team_agent.messaging.internal_delivery import deliver_stored_message
|
|
55
|
-
from team_agent.state import team_state_key
|
|
56
|
-
|
|
57
|
-
leader_id = (state.get("leader") or {}).get("id") or "leader"
|
|
58
|
-
try:
|
|
59
|
-
deliver_stored_message(
|
|
60
|
-
workspace,
|
|
61
|
-
leader_id,
|
|
62
|
-
result["message"],
|
|
63
|
-
sender="coordinator",
|
|
64
|
-
requires_ack=False,
|
|
65
|
-
wait_visible=False,
|
|
66
|
-
team=team_state_key(state),
|
|
67
|
-
)
|
|
68
|
-
except Exception as exc:
|
|
69
|
-
event_log.write("idle_takeover.push_failed", error=str(exc))
|
|
70
|
-
event_log.write(
|
|
71
|
-
"idle_takeover.reminder",
|
|
72
|
-
interrupted=result.get("interrupted_nodes"),
|
|
73
|
-
reason=result.get("reason"),
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def _leader_node(state: dict[str, Any]) -> dict[str, Any] | None:
|
|
78
|
-
"""Best-effort leader node from its own provider transcript (C13). If the
|
|
79
|
-
leader's session file path is not tracked, the leader is omitted rather than
|
|
80
|
-
guessed — the predicate then evaluates the workers (the leader is the ping
|
|
81
|
-
recipient and acts on the reminder regardless)."""
|
|
82
|
-
from team_agent.provider_state import read_turn_state
|
|
83
|
-
|
|
84
|
-
leader = state.get("leader") if isinstance(state.get("leader"), dict) else {}
|
|
85
|
-
receiver = state.get("leader_receiver") if isinstance(state.get("leader_receiver"), dict) else {}
|
|
86
|
-
path = leader.get("rollout_path") or receiver.get("rollout_path")
|
|
87
|
-
provider = str(leader.get("provider") or receiver.get("provider") or "")
|
|
88
|
-
if not path or not provider:
|
|
89
|
-
return None
|
|
90
|
-
classification = read_turn_state(provider, _read_session_tail(path))
|
|
91
|
-
return {
|
|
92
|
-
"node_id": leader.get("id") or "leader",
|
|
93
|
-
"role": "leader",
|
|
94
|
-
"state": classification.get("state"),
|
|
95
|
-
"turn_id": classification.get("turn_id"),
|
|
96
|
-
"annotations": classification.get("annotations"),
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _read_session_tail(path: Any, max_bytes: int = _TAIL_BYTES) -> str:
|
|
101
|
-
if not path:
|
|
102
|
-
return ""
|
|
103
|
-
try:
|
|
104
|
-
p = Path(str(path))
|
|
105
|
-
size = p.stat().st_size
|
|
106
|
-
with p.open("rb") as handle:
|
|
107
|
-
if size > max_bytes:
|
|
108
|
-
handle.seek(size - max_bytes)
|
|
109
|
-
# drop a possibly-partial first line after seeking mid-file
|
|
110
|
-
handle.readline()
|
|
111
|
-
data = handle.read()
|
|
112
|
-
return data.decode("utf-8", errors="ignore")
|
|
113
|
-
except (OSError, ValueError):
|
|
114
|
-
return ""
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from team_agent.launch.bootstrap import (
|
|
4
|
-
attach_team_profile_dirs,
|
|
5
|
-
compile_team_dir_spec,
|
|
6
|
-
init_workspace,
|
|
7
|
-
is_team_doc_dir,
|
|
8
|
-
spec_team_dir,
|
|
9
|
-
tmux_session_conflict_error,
|
|
10
|
-
validate_file,
|
|
11
|
-
)
|
|
12
|
-
from team_agent.launch.config import (
|
|
13
|
-
DANGEROUS_LEADER_FLAGS,
|
|
14
|
-
command_has_flag,
|
|
15
|
-
detect_inherited_dangerous_permissions,
|
|
16
|
-
effective_runtime_config,
|
|
17
|
-
process_ancestry,
|
|
18
|
-
process_info,
|
|
19
|
-
requires_direct_leader_receiver,
|
|
20
|
-
)
|
|
21
|
-
from team_agent.launch.core import launch
|
|
22
|
-
from team_agent.launch.requirements import ensure_agent_start_requirements
|
|
23
|
-
|
|
24
|
-
__all__ = [
|
|
25
|
-
"DANGEROUS_LEADER_FLAGS",
|
|
26
|
-
"attach_team_profile_dirs",
|
|
27
|
-
"command_has_flag",
|
|
28
|
-
"compile_team_dir_spec",
|
|
29
|
-
"detect_inherited_dangerous_permissions",
|
|
30
|
-
"effective_runtime_config",
|
|
31
|
-
"ensure_agent_start_requirements",
|
|
32
|
-
"init_workspace",
|
|
33
|
-
"is_team_doc_dir",
|
|
34
|
-
"launch",
|
|
35
|
-
"process_ancestry",
|
|
36
|
-
"process_info",
|
|
37
|
-
"requires_direct_leader_receiver",
|
|
38
|
-
"spec_team_dir",
|
|
39
|
-
"tmux_session_conflict_error",
|
|
40
|
-
"validate_file",
|
|
41
|
-
]
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
from team_agent.events import EventLog
|
|
7
|
-
from team_agent.simple_yaml import dumps
|
|
8
|
-
from team_agent.spec import load_spec, workspace_from_spec
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def init_workspace(workspace: Path, force: bool = False) -> dict[str, Path]:
|
|
12
|
-
from team_agent.runtime import ensure_workspace_dirs
|
|
13
|
-
from team_agent.paths import example_path, template_path
|
|
14
|
-
|
|
15
|
-
ensure_workspace_dirs(workspace)
|
|
16
|
-
team_dir = workspace / ".team" / "current"
|
|
17
|
-
team_dir.mkdir(parents=True, exist_ok=True)
|
|
18
|
-
spec_path = team_dir / "team.spec.yaml"
|
|
19
|
-
state_path = workspace / "team_state.md"
|
|
20
|
-
if spec_path.exists() and not force:
|
|
21
|
-
from team_agent.runtime import RuntimeError
|
|
22
|
-
raise RuntimeError(f"{spec_path} already exists; pass --force to overwrite")
|
|
23
|
-
spec_path.write_text(example_path("team.spec.yaml").read_text(encoding="utf-8"), encoding="utf-8")
|
|
24
|
-
if not state_path.exists() or force:
|
|
25
|
-
state_path.write_text(template_path("team_state.md").read_text(encoding="utf-8"), encoding="utf-8")
|
|
26
|
-
EventLog(workspace).write("init", spec_path=str(spec_path), state_path=str(state_path))
|
|
27
|
-
return {"spec": spec_path, "state": state_path}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def validate_file(spec_path: Path) -> dict[str, Any]:
|
|
31
|
-
if spec_path.is_dir():
|
|
32
|
-
from team_agent.compiler import compile_team
|
|
33
|
-
|
|
34
|
-
result = compile_team(spec_path)
|
|
35
|
-
spec = result["spec"]
|
|
36
|
-
return {
|
|
37
|
-
"ok": True,
|
|
38
|
-
"type": "team_dir",
|
|
39
|
-
"workspace": str(Path(spec["team"]["workspace"]).resolve()),
|
|
40
|
-
"team": spec["team"]["name"],
|
|
41
|
-
"agents": [agent["id"] for agent in spec.get("agents", [])],
|
|
42
|
-
}
|
|
43
|
-
spec = load_spec(spec_path)
|
|
44
|
-
workspace = workspace_from_spec(spec, spec_path)
|
|
45
|
-
return {"ok": True, "workspace": str(workspace), "team": spec["team"]["name"]}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def tmux_session_conflict_error(session_name: str) -> str:
|
|
49
|
-
return (
|
|
50
|
-
f"tmux session already exists: {session_name}. "
|
|
51
|
-
"Startup will not terminate existing tmux sessions because they may belong to active teams. "
|
|
52
|
-
"Use a different team name or runtime.session_name and start again."
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def spec_team_dir(spec_path: Path, workspace: Path) -> Path:
|
|
57
|
-
spec_dir = spec_path.resolve().parent
|
|
58
|
-
if spec_dir.parent.name == ".team":
|
|
59
|
-
return spec_dir
|
|
60
|
-
return workspace.resolve() / ".team" / "current"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def is_team_doc_dir(team_dir: Path) -> bool:
|
|
64
|
-
return (team_dir / "TEAM.md").exists() and (team_dir / "agents").is_dir()
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def compile_team_dir_spec(team_dir: Path, workspace: Path) -> dict[str, Any]:
|
|
68
|
-
from team_agent.compiler import compile_team
|
|
69
|
-
|
|
70
|
-
spec_path = team_dir / "team.spec.yaml"
|
|
71
|
-
compiled = compile_team(team_dir, spec_path)
|
|
72
|
-
if compiled["spec"].get("context", {}).get("state_file") == "team_state.md":
|
|
73
|
-
state_file = str(team_dir.relative_to(workspace) / "team_state.md") if team_dir.is_relative_to(workspace) else "team_state.md"
|
|
74
|
-
compiled["spec"]["context"]["state_file"] = state_file
|
|
75
|
-
spec_path.write_text(dumps(compiled["spec"]), encoding="utf-8")
|
|
76
|
-
return compiled
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def attach_team_profile_dirs(spec: dict[str, Any], spec_path: Path, workspace: Path | None = None, team_dir: Path | None = None) -> None:
|
|
80
|
-
workspace = workspace.resolve() if workspace else workspace_from_spec(spec, spec_path)
|
|
81
|
-
team_dir = team_dir.resolve() if team_dir else spec_team_dir(spec_path, workspace)
|
|
82
|
-
profiles_dir = team_dir / "profiles"
|
|
83
|
-
for agent in spec.get("agents", []):
|
|
84
|
-
if isinstance(agent, dict) and agent.get("profile"):
|
|
85
|
-
agent["_profile_dir"] = str(profiles_dir)
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import re
|
|
5
|
-
import subprocess
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
DANGEROUS_LEADER_FLAGS = (
|
|
10
|
-
("claude", "--dangerously-skip-permissions"),
|
|
11
|
-
("claude", "--dangerously-skip-permission"),
|
|
12
|
-
("codex", "--dangerously-bypass-approvals-and-sandbox"),
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def effective_runtime_config(runtime_cfg: dict[str, Any]) -> dict[str, Any]:
|
|
17
|
-
# Route via the runtime alias surface so tests patching
|
|
18
|
-
# team_agent.runtime._detect_inherited_dangerous_permissions still take effect.
|
|
19
|
-
from team_agent.runtime import _detect_inherited_dangerous_permissions
|
|
20
|
-
effective = dict(runtime_cfg)
|
|
21
|
-
if effective.get("dangerous_auto_approve"):
|
|
22
|
-
effective["dangerous_auto_approve_source"] = "runtime_config"
|
|
23
|
-
effective["dangerous_auto_approve_inherited"] = False
|
|
24
|
-
return effective
|
|
25
|
-
inherited = _detect_inherited_dangerous_permissions()
|
|
26
|
-
if not inherited.get("enabled"):
|
|
27
|
-
effective["dangerous_auto_approve"] = False
|
|
28
|
-
effective["dangerous_auto_approve_source"] = "disabled"
|
|
29
|
-
effective["dangerous_auto_approve_inherited"] = False
|
|
30
|
-
return effective
|
|
31
|
-
effective["dangerous_auto_approve"] = True
|
|
32
|
-
effective["dangerous_auto_approve_source"] = "leader_process"
|
|
33
|
-
effective["dangerous_auto_approve_inherited"] = True
|
|
34
|
-
effective["dangerous_auto_approve_provider"] = inherited.get("provider")
|
|
35
|
-
effective["dangerous_auto_approve_flag"] = inherited.get("flag")
|
|
36
|
-
return effective
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def requires_direct_leader_receiver(spec: dict[str, Any], runtime_cfg: dict[str, Any]) -> bool:
|
|
40
|
-
if runtime_cfg.get("require_leader_receiver") is not None:
|
|
41
|
-
return bool(runtime_cfg.get("require_leader_receiver"))
|
|
42
|
-
return any(agent.get("provider") != "fake" for agent in spec.get("agents", []))
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def detect_inherited_dangerous_permissions() -> dict[str, Any]:
|
|
46
|
-
# Route via runtime alias so existing patches of
|
|
47
|
-
# team_agent.runtime._process_ancestry take effect at call time.
|
|
48
|
-
from team_agent.runtime import _process_ancestry
|
|
49
|
-
for proc in _process_ancestry(os.getpid()):
|
|
50
|
-
command = str(proc.get("command") or "")
|
|
51
|
-
for provider, flag in DANGEROUS_LEADER_FLAGS:
|
|
52
|
-
if command_has_flag(command, flag):
|
|
53
|
-
return {
|
|
54
|
-
"enabled": True,
|
|
55
|
-
"provider": provider,
|
|
56
|
-
"flag": flag,
|
|
57
|
-
"pid": proc.get("pid"),
|
|
58
|
-
}
|
|
59
|
-
return {"enabled": False}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def command_has_flag(command: str, flag: str) -> bool:
|
|
63
|
-
return re.search(rf"(?<!\S){re.escape(flag)}(?!\S)", command) is not None
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def process_ancestry(pid: int, max_depth: int = 12) -> list[dict[str, Any]]:
|
|
67
|
-
ancestry: list[dict[str, Any]] = []
|
|
68
|
-
current = pid
|
|
69
|
-
seen: set[int] = set()
|
|
70
|
-
for _ in range(max_depth):
|
|
71
|
-
if current in seen or current <= 0:
|
|
72
|
-
break
|
|
73
|
-
seen.add(current)
|
|
74
|
-
info = process_info(current)
|
|
75
|
-
if not info:
|
|
76
|
-
break
|
|
77
|
-
ancestry.append(info)
|
|
78
|
-
parent = info.get("ppid")
|
|
79
|
-
if not isinstance(parent, int) or parent <= 1 or parent == current:
|
|
80
|
-
break
|
|
81
|
-
current = parent
|
|
82
|
-
return ancestry
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def process_info(pid: int) -> dict[str, Any] | None:
|
|
86
|
-
try:
|
|
87
|
-
proc = subprocess.run(
|
|
88
|
-
["ps", "-p", str(pid), "-o", "ppid=", "-o", "command="],
|
|
89
|
-
text=True,
|
|
90
|
-
capture_output=True,
|
|
91
|
-
timeout=2,
|
|
92
|
-
check=False,
|
|
93
|
-
)
|
|
94
|
-
except (OSError, subprocess.TimeoutExpired):
|
|
95
|
-
return None
|
|
96
|
-
if proc.returncode != 0:
|
|
97
|
-
return None
|
|
98
|
-
line = proc.stdout.strip()
|
|
99
|
-
if not line:
|
|
100
|
-
return None
|
|
101
|
-
parts = line.split(None, 1)
|
|
102
|
-
try:
|
|
103
|
-
ppid = int(parts[0])
|
|
104
|
-
except (IndexError, ValueError):
|
|
105
|
-
return None
|
|
106
|
-
return {"pid": pid, "ppid": ppid, "command": parts[1] if len(parts) > 1 else ""}
|