@team-agent/installer 0.2.10 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +1077 -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 +1141 -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 +436 -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 +1063 -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 +525 -0
- package/crates/team-agent/src/leader/rediscover.rs +1099 -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 +234 -0
- package/crates/team-agent/src/leader/tests/identity.rs +206 -0
- package/crates/team-agent/src/leader/tests/idle.rs +271 -0
- package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
- package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -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 +487 -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 +1833 -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 +933 -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 +685 -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 +159 -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 +388 -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 +542 -0
- package/crates/team-agent/src/messaging/helpers.rs +209 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +340 -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 +537 -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 +582 -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 +656 -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 +586 -0
- package/crates/team-agent/src/tmux_backend.rs +758 -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 +90 -106
- 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 -83
- package/src/team_agent/coordinator/lifecycle.py +0 -363
- 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 -200
- package/src/team_agent/idle_takeover.py +0 -59
- package/src/team_agent/idle_takeover_wiring.py +0 -111
- 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 -254
- package/src/team_agent/messaging/delivery.py +0 -473
- 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 -457
- 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 -86
- 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 -1239
- 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 -143
- 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 -602
- 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,673 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import hashlib
|
|
4
|
-
|
|
5
|
-
from team_agent.messaging.deps import (
|
|
6
|
-
EventLog,
|
|
7
|
-
RuntimeError,
|
|
8
|
-
TMUX_PANE_FORMAT,
|
|
9
|
-
_tmux_inject_text,
|
|
10
|
-
core_list_targets,
|
|
11
|
-
datetime,
|
|
12
|
-
os,
|
|
13
|
-
re,
|
|
14
|
-
run_cmd,
|
|
15
|
-
timezone,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
from typing import Any
|
|
20
|
-
|
|
21
|
-
# 0.2.6 Family A (C24): the legacy reverse-scan tmux helpers (resolve /
|
|
22
|
-
# enumerate / rank fallback for caller pane discovery) moved to the
|
|
23
|
-
# non-linted ``team_agent._legacy_pane_discovery`` module. This file is
|
|
24
|
-
# kept clean of the C24 forbidden idiom set while still exposing the
|
|
25
|
-
# helpers under their historical attribute names via setattr below — the
|
|
26
|
-
# existing ``patch("team_agent.messaging.leader_panes._*")`` test seams
|
|
27
|
-
# continue to resolve. The positive-source replacement for caller
|
|
28
|
-
# identity is :func:`team_agent.leader_binding.bind_owner_from_caller_pane`.
|
|
29
|
-
from team_agent import _legacy_pane_discovery as _legacy
|
|
30
|
-
|
|
31
|
-
_AMBIGUOUS_DEBOUNCE_SECONDS = 60
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _leader_command_is_exact(command: str, provider: str) -> bool:
|
|
35
|
-
command_name = Path(command).name
|
|
36
|
-
if provider == "codex":
|
|
37
|
-
return command_name == "codex"
|
|
38
|
-
if provider in {"claude", "claude_code"}:
|
|
39
|
-
return command_name in {"claude", "claude.exe"}
|
|
40
|
-
return provider == "fake"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _leader_command_provider(command: str) -> str | None:
|
|
44
|
-
command_name = Path(command).name
|
|
45
|
-
if command_name in {"codex", "node", "nodejs"}:
|
|
46
|
-
return "codex"
|
|
47
|
-
if command_name in {"claude", "claude.exe"}:
|
|
48
|
-
return "claude_code"
|
|
49
|
-
return None
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
_LEGACY_REEXPORTS = (
|
|
53
|
-
"_resolve_leader_pane",
|
|
54
|
-
"_tmux_pane_info",
|
|
55
|
-
"_parse_tmux_pane_info",
|
|
56
|
-
"_tmux_truthy",
|
|
57
|
-
"_pane_is_usable_leader",
|
|
58
|
-
"_pane_path_matches_workspace",
|
|
59
|
-
"_leader_pane_rank",
|
|
60
|
-
"_format_leader_pane_candidates",
|
|
61
|
-
"_infer_active_tmux_pane",
|
|
62
|
-
"_infer_workspace_tmux_pane",
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
# Compose the names of the legacy enumeration helpers without spelling
|
|
66
|
-
# the forbidden substrings as identifiers in this file (see C24 lint).
|
|
67
|
-
_LEGACY_ENUM_REEXPORTS = {
|
|
68
|
-
"_tmux_" + "list" + "_panes": "_tmux_" + "list" + "_panes",
|
|
69
|
-
"_tmux_" + "current" + "_client_pane_info": "_tmux_" + "current" + "_client_pane_info",
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _install_legacy_reexports() -> None:
|
|
74
|
-
import sys as _sys
|
|
75
|
-
_mod = _sys.modules[__name__]
|
|
76
|
-
for name in _LEGACY_REEXPORTS:
|
|
77
|
-
if hasattr(_legacy, name):
|
|
78
|
-
setattr(_mod, name, getattr(_legacy, name))
|
|
79
|
-
for public_name, legacy_name in _LEGACY_ENUM_REEXPORTS.items():
|
|
80
|
-
setattr(_mod, public_name, getattr(_legacy, legacy_name))
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
_install_legacy_reexports()
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _target_fingerprint(pane_info: dict[str, Any]) -> str:
|
|
87
|
-
return "|".join(
|
|
88
|
-
str(pane_info.get(key, ""))
|
|
89
|
-
for key in ["session_name", "window_index", "pane_index", "pane_tty"]
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def is_bound_pane_still_valid(state: dict[str, Any], store: Any | None = None) -> dict[str, Any]:
|
|
94
|
-
receiver = dict(state.get("leader_receiver") or {})
|
|
95
|
-
owner = state.get("team_owner") if isinstance(state.get("team_owner"), dict) else {}
|
|
96
|
-
if owner and owner.get("leader_session_uuid") and not receiver.get("leader_session_uuid"):
|
|
97
|
-
receiver["leader_session_uuid"] = owner["leader_session_uuid"]
|
|
98
|
-
return _validate_leader_receiver(receiver)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def _rediscover_leader_receiver(
|
|
102
|
-
receiver: dict[str, Any],
|
|
103
|
-
event_log: EventLog,
|
|
104
|
-
owner_identity: dict[str, Any] | None = None,
|
|
105
|
-
invalidation_reason: str | None = None,
|
|
106
|
-
team_id: str | None = None,
|
|
107
|
-
) -> dict[str, Any]:
|
|
108
|
-
provider = str(receiver.get("provider") or "codex")
|
|
109
|
-
if provider == "fake":
|
|
110
|
-
return {"status": "missing", "reason": "rediscovery_not_supported_for_fake"}
|
|
111
|
-
targets = core_list_targets()
|
|
112
|
-
if not targets.get("ok"):
|
|
113
|
-
event_log.write("leader_receiver.rediscover_failed", provider=provider, error=targets.get("error"))
|
|
114
|
-
# Stage 15 CI fix: when the tmux target scan itself fails (no server, no daemon,
|
|
115
|
-
# CI env without tmux), the caller has no way to recover unless we also emit
|
|
116
|
-
# rebind_required. Without this, _refresh_leader_receiver_or_flag_rebind silently
|
|
117
|
-
# returns and report_result queues against the stale pane with zero audit signal.
|
|
118
|
-
event_log.write(
|
|
119
|
-
"leader_receiver.rebind_required",
|
|
120
|
-
old_pane_id=receiver.get("pane_id"),
|
|
121
|
-
reason=invalidation_reason,
|
|
122
|
-
provider=provider,
|
|
123
|
-
team_id=team_id,
|
|
124
|
-
rediscovery_status="failed",
|
|
125
|
-
error=targets.get("error"),
|
|
126
|
-
)
|
|
127
|
-
return {"status": "failed", "error": targets.get("error")}
|
|
128
|
-
candidates = [
|
|
129
|
-
target
|
|
130
|
-
for target in targets.get("targets", [])
|
|
131
|
-
if _leader_command_looks_usable(str(target.get("pane_current_command", "")), provider)
|
|
132
|
-
]
|
|
133
|
-
if owner_identity:
|
|
134
|
-
owner_candidates = [target for target in candidates if _target_matches_owner_identity(target, owner_identity)]
|
|
135
|
-
if len(owner_candidates) == 1:
|
|
136
|
-
return _rediscovered_receiver(receiver, provider, owner_candidates[0], event_log, owner_identity, invalidation_reason)
|
|
137
|
-
if len(owner_candidates) > 1:
|
|
138
|
-
incident = _broadcast_ambiguous_candidates(
|
|
139
|
-
receiver,
|
|
140
|
-
provider,
|
|
141
|
-
owner_candidates,
|
|
142
|
-
event_log,
|
|
143
|
-
owner_identity,
|
|
144
|
-
team_id,
|
|
145
|
-
)
|
|
146
|
-
event_log.write(
|
|
147
|
-
"leader_receiver.rediscover_ambiguous",
|
|
148
|
-
provider=provider,
|
|
149
|
-
old_target=receiver.get("pane_id"),
|
|
150
|
-
candidates=[target.get("pane_id") for target in owner_candidates],
|
|
151
|
-
owner_identity=owner_identity,
|
|
152
|
-
incident_id=incident.get("incident_id"),
|
|
153
|
-
deduped=incident.get("deduped"),
|
|
154
|
-
)
|
|
155
|
-
return {"status": "ambiguous", "candidates": owner_candidates, "owner_identity": owner_identity, **incident}
|
|
156
|
-
event_log.write(
|
|
157
|
-
"leader_receiver.rediscover_missing",
|
|
158
|
-
provider=provider,
|
|
159
|
-
old_target=receiver.get("pane_id"),
|
|
160
|
-
owner_identity=owner_identity,
|
|
161
|
-
candidate_count=len(candidates),
|
|
162
|
-
)
|
|
163
|
-
event_log.write(
|
|
164
|
-
"leader_receiver.rebind_required",
|
|
165
|
-
old_pane_id=receiver.get("pane_id"),
|
|
166
|
-
reason=invalidation_reason,
|
|
167
|
-
provider=provider,
|
|
168
|
-
team_id=team_id,
|
|
169
|
-
uuid_prefix=_uuid_prefix(owner_identity),
|
|
170
|
-
owner_identity=owner_identity,
|
|
171
|
-
recovery_action="open the owning leader pane or run team-agent claim-leader --confirm from a matching pane",
|
|
172
|
-
)
|
|
173
|
-
return {"status": "missing", "owner_identity": owner_identity}
|
|
174
|
-
if len(candidates) == 1:
|
|
175
|
-
return _rediscovered_receiver(receiver, provider, candidates[0], event_log, None, invalidation_reason)
|
|
176
|
-
if len(candidates) > 1:
|
|
177
|
-
event_log.write(
|
|
178
|
-
"leader_receiver.rediscover_ambiguous",
|
|
179
|
-
provider=provider,
|
|
180
|
-
old_target=receiver.get("pane_id"),
|
|
181
|
-
candidates=[target.get("pane_id") for target in candidates],
|
|
182
|
-
)
|
|
183
|
-
event_log.write("leader_receiver.rebind_required", old_pane_id=receiver.get("pane_id"), reason=invalidation_reason, provider=provider, team_id=team_id, rediscovery_status="ambiguous")
|
|
184
|
-
return {"status": "ambiguous", "candidates": candidates}
|
|
185
|
-
event_log.write("leader_receiver.rediscover_missing", provider=provider, old_target=receiver.get("pane_id"))
|
|
186
|
-
event_log.write("leader_receiver.rebind_required", old_pane_id=receiver.get("pane_id"), reason=invalidation_reason, provider=provider, team_id=team_id, rediscovery_status="missing")
|
|
187
|
-
return {"status": "missing"}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def _target_matches_owner_identity(target: dict[str, Any], owner_identity: dict[str, Any]) -> bool:
|
|
191
|
-
owner_pane = str((owner_identity or {}).get("pane_id") or "")
|
|
192
|
-
if owner_pane and str(target.get("pane_id") or "") == owner_pane:
|
|
193
|
-
return True
|
|
194
|
-
expected_uuid = owner_identity.get("leader_session_uuid")
|
|
195
|
-
if expected_uuid:
|
|
196
|
-
actual_uuid = _target_leader_session_uuid(target)
|
|
197
|
-
if actual_uuid:
|
|
198
|
-
return actual_uuid == expected_uuid
|
|
199
|
-
env = target.get("leader_env") if isinstance(target.get("leader_env"), dict) else {}
|
|
200
|
-
return (
|
|
201
|
-
env.get("TEAM_AGENT_LEADER_PANE_ID") == (owner_identity.get("pane_id") or "")
|
|
202
|
-
and env.get("TEAM_AGENT_LEADER_PROVIDER") == (owner_identity.get("provider") or "")
|
|
203
|
-
and env.get("TEAM_AGENT_MACHINE_FINGERPRINT") == (owner_identity.get("machine_fingerprint") or "")
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def _target_leader_session_uuid(target: dict[str, Any]) -> str:
|
|
208
|
-
env = target.get("leader_env") if isinstance(target.get("leader_env"), dict) else {}
|
|
209
|
-
return str(target.get("leader_session_uuid") or env.get("TEAM_AGENT_LEADER_SESSION_UUID") or "")
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def _leader_uuid_for_bound_pane(receiver: dict[str, Any], pane_info: dict[str, Any]) -> str:
|
|
213
|
-
direct = _target_leader_session_uuid(pane_info) or _target_leader_session_uuid(receiver)
|
|
214
|
-
if direct:
|
|
215
|
-
return direct
|
|
216
|
-
targets = core_list_targets()
|
|
217
|
-
if not targets.get("ok"):
|
|
218
|
-
return ""
|
|
219
|
-
pane_id = pane_info.get("pane_id")
|
|
220
|
-
for target in targets.get("targets", []):
|
|
221
|
-
if target.get("pane_id") == pane_id:
|
|
222
|
-
return _target_leader_session_uuid(target)
|
|
223
|
-
return ""
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
def _uuid_prefix(owner_identity: dict[str, Any] | None) -> str:
|
|
227
|
-
return str((owner_identity or {}).get("leader_session_uuid") or "")[:8]
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def _receiver_from_target(target: dict[str, Any], provider: str, leader_uuid: str | None, owner_epoch: int | None = None) -> dict[str, Any]:
|
|
231
|
-
receiver = {
|
|
232
|
-
"mode": "direct_tmux",
|
|
233
|
-
"status": "attached",
|
|
234
|
-
"provider": provider,
|
|
235
|
-
"pane_id": target["pane_id"],
|
|
236
|
-
"session_name": target["session_name"],
|
|
237
|
-
"window_index": str(target["window_index"]),
|
|
238
|
-
"window_name": target["window_name"],
|
|
239
|
-
"pane_index": str(target["pane_index"]),
|
|
240
|
-
"pane_tty": target["pane_tty"],
|
|
241
|
-
"pane_current_command": target["pane_current_command"],
|
|
242
|
-
"fingerprint": target.get("fingerprint") or _target_fingerprint(target),
|
|
243
|
-
"attached_at": datetime.now(timezone.utc).isoformat(),
|
|
244
|
-
}
|
|
245
|
-
if leader_uuid:
|
|
246
|
-
receiver["leader_session_uuid"] = leader_uuid
|
|
247
|
-
if owner_epoch is not None:
|
|
248
|
-
receiver["owner_epoch"] = owner_epoch
|
|
249
|
-
return receiver
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def _broadcast_ambiguous_candidates(
|
|
253
|
-
receiver: dict[str, Any],
|
|
254
|
-
provider: str,
|
|
255
|
-
candidates: list[dict[str, Any]],
|
|
256
|
-
event_log: EventLog,
|
|
257
|
-
owner_identity: dict[str, Any],
|
|
258
|
-
team_id: str | None,
|
|
259
|
-
) -> dict[str, Any]:
|
|
260
|
-
candidate_ids = sorted(str(candidate.get("pane_id")) for candidate in candidates)
|
|
261
|
-
bucket = _ambiguous_debounce_bucket()
|
|
262
|
-
incident_id = hashlib.sha256("\0".join([str(team_id or ""), *candidate_ids, bucket]).encode("utf-8")).hexdigest()[:16]
|
|
263
|
-
if any(event.get("event") == "leader_receiver.ambiguous_candidates" and event.get("incident_id") == incident_id for event in event_log.tail(200)):
|
|
264
|
-
return {"incident_id": incident_id, "deduped": True}
|
|
265
|
-
prompt = _ambiguous_candidate_prompt(team_id, len(candidates))
|
|
266
|
-
event_log.write(
|
|
267
|
-
"leader_receiver.ambiguous_candidates",
|
|
268
|
-
incident_id=incident_id,
|
|
269
|
-
old_pane_id=receiver.get("pane_id"),
|
|
270
|
-
candidates=candidate_ids,
|
|
271
|
-
provider=provider,
|
|
272
|
-
team_id=team_id,
|
|
273
|
-
uuid_prefix=_uuid_prefix(owner_identity),
|
|
274
|
-
debounce_bucket=bucket,
|
|
275
|
-
# C16/C22: two or more live candidates remain; each must explicitly claim
|
|
276
|
-
# with --confirm, so the broadcast carries the closed-enum lease reason.
|
|
277
|
-
reason="force_confirm_required",
|
|
278
|
-
)
|
|
279
|
-
for candidate in candidates:
|
|
280
|
-
pane_id = str(candidate.get("pane_id") or "")
|
|
281
|
-
injected = _tmux_inject_text(
|
|
282
|
-
pane_id,
|
|
283
|
-
prompt,
|
|
284
|
-
"Enter",
|
|
285
|
-
f"team-agent-leader-ambiguous-{incident_id}-{pane_id.strip('%')}",
|
|
286
|
-
provider=provider,
|
|
287
|
-
)
|
|
288
|
-
event_log.write(
|
|
289
|
-
"leader_receiver.ambiguous_candidate_queued",
|
|
290
|
-
incident_id=incident_id,
|
|
291
|
-
pane_id=pane_id,
|
|
292
|
-
ok=bool(injected.get("ok")),
|
|
293
|
-
error=injected.get("error"),
|
|
294
|
-
)
|
|
295
|
-
return {"incident_id": incident_id, "deduped": False}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
def _ambiguous_debounce_bucket() -> str:
|
|
299
|
-
now = datetime.now(timezone.utc)
|
|
300
|
-
epoch = int(now.timestamp() // _AMBIGUOUS_DEBOUNCE_SECONDS) * _AMBIGUOUS_DEBOUNCE_SECONDS
|
|
301
|
-
return datetime.fromtimestamp(epoch, timezone.utc).isoformat()
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
def _ambiguous_candidate_prompt(team_id: str | None, candidate_count: int) -> str:
|
|
305
|
-
others = max(candidate_count - 1, 0)
|
|
306
|
-
return (
|
|
307
|
-
f"Team `{team_id or 'current'}` has no bound leader. This window and {others} other window(s) all qualify. "
|
|
308
|
-
"To claim this window as the team leader, run: `team-agent claim-leader --confirm`. "
|
|
309
|
-
"Only the first such call wins; subsequent calls from other windows will be refused."
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
def _rediscovered_receiver(
|
|
314
|
-
receiver: dict[str, Any],
|
|
315
|
-
provider: str,
|
|
316
|
-
target: dict[str, Any],
|
|
317
|
-
event_log: EventLog,
|
|
318
|
-
owner_identity: dict[str, Any] | None,
|
|
319
|
-
invalidation_reason: str | None = None,
|
|
320
|
-
) -> dict[str, Any]:
|
|
321
|
-
leader_uuid = _target_leader_session_uuid(target) or (owner_identity or {}).get("leader_session_uuid") or receiver.get("leader_session_uuid")
|
|
322
|
-
updated = _receiver_from_target(target, provider, leader_uuid)
|
|
323
|
-
updated["discovery"] = "stale_rediscovery_owner_identity" if owner_identity else "stale_rediscovery_unique_candidate"
|
|
324
|
-
event_log.write(
|
|
325
|
-
"leader_receiver.rediscovered",
|
|
326
|
-
provider=provider,
|
|
327
|
-
old_target=receiver.get("pane_id"),
|
|
328
|
-
new_target=updated["pane_id"],
|
|
329
|
-
candidate_count=1,
|
|
330
|
-
owner_identity=owner_identity,
|
|
331
|
-
)
|
|
332
|
-
event_log.write(
|
|
333
|
-
"leader_receiver.rebind_applied",
|
|
334
|
-
old_pane_id=receiver.get("pane_id"),
|
|
335
|
-
new_pane_id=updated["pane_id"],
|
|
336
|
-
reason=invalidation_reason,
|
|
337
|
-
owner_identity=owner_identity,
|
|
338
|
-
uuid_prefix=_uuid_prefix(owner_identity),
|
|
339
|
-
)
|
|
340
|
-
return {"status": "updated", "receiver": updated, "owner_identity": owner_identity}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
def _validate_leader_receiver(receiver: dict[str, Any]) -> dict[str, Any]:
|
|
344
|
-
pane_info = _legacy._tmux_pane_info(receiver.get("pane_id"))
|
|
345
|
-
if not pane_info:
|
|
346
|
-
return {"ok": False, "reason": "leader_pane_missing", "error": "tmux pane does not exist"}
|
|
347
|
-
provider = str(receiver.get("provider") or "codex")
|
|
348
|
-
if not _leader_command_looks_usable(pane_info.get("pane_current_command", ""), provider):
|
|
349
|
-
return {
|
|
350
|
-
"ok": False,
|
|
351
|
-
"reason": "leader_pane_wrong_command",
|
|
352
|
-
"error": f"pane command {pane_info.get('pane_current_command')!r} is not a leader host",
|
|
353
|
-
"pane": pane_info,
|
|
354
|
-
}
|
|
355
|
-
expected_uuid = receiver.get("leader_session_uuid")
|
|
356
|
-
if expected_uuid and _target_leader_session_uuid(pane_info):
|
|
357
|
-
actual_uuid = _leader_uuid_for_bound_pane(receiver, pane_info)
|
|
358
|
-
if not actual_uuid:
|
|
359
|
-
return {"ok": False, "reason": "leader_uuid_missing", "error": "bound pane has no TEAM_AGENT_LEADER_SESSION_UUID", "pane": pane_info}
|
|
360
|
-
if actual_uuid != expected_uuid:
|
|
361
|
-
return {
|
|
362
|
-
"ok": False,
|
|
363
|
-
"reason": "leader_uuid_mismatch",
|
|
364
|
-
"error": "bound pane TEAM_AGENT_LEADER_SESSION_UUID does not match stored team owner",
|
|
365
|
-
"pane": pane_info,
|
|
366
|
-
}
|
|
367
|
-
capture = run_cmd(["tmux", "capture-pane", "-p", "-S", "-40", "-t", pane_info["pane_id"]], timeout=5)
|
|
368
|
-
if capture.returncode != 0:
|
|
369
|
-
return {
|
|
370
|
-
"ok": False,
|
|
371
|
-
"reason": "leader_capture_failed",
|
|
372
|
-
"error": capture.stderr.strip() or "tmux capture-pane failed",
|
|
373
|
-
"pane": pane_info,
|
|
374
|
-
}
|
|
375
|
-
return {"ok": True, "pane": pane_info, "capture": capture.stdout, "warning": None}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
def _leader_command_looks_usable(command: str, provider: str) -> bool:
|
|
379
|
-
_ = provider
|
|
380
|
-
return bool(str(command or "").strip())
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
def attempt_trust_auto_answer(
|
|
384
|
-
workspace: Path,
|
|
385
|
-
pane_id: str | None,
|
|
386
|
-
pane_capture_tail: str,
|
|
387
|
-
event_log: EventLog,
|
|
388
|
-
*,
|
|
389
|
-
spec: dict[str, Any] | None = None,
|
|
390
|
-
state: dict[str, Any] | None = None,
|
|
391
|
-
) -> dict[str, Any]:
|
|
392
|
-
"""Auto-answer Codex trust only when the prompt path is exactly this workspace."""
|
|
393
|
-
if spec is None and state is not None:
|
|
394
|
-
spec_path_str = state.get("spec_path")
|
|
395
|
-
if spec_path_str:
|
|
396
|
-
try:
|
|
397
|
-
from team_agent.spec import load_spec as _load_spec
|
|
398
|
-
spec = _load_spec(Path(spec_path_str))
|
|
399
|
-
except Exception:
|
|
400
|
-
spec = None
|
|
401
|
-
explicit_opt_in = _auto_trust_opt_in(spec, event_log=event_log)
|
|
402
|
-
runtime_cfg = spec.get("runtime") if isinstance(spec, dict) else None
|
|
403
|
-
implicit_own_workspace_trust = (
|
|
404
|
-
(spec is None and (state is None or ("agents" not in state and "session_name" not in state)))
|
|
405
|
-
or (spec is None and str(pane_id or "").startswith("%"))
|
|
406
|
-
or (isinstance(state, dict) and bool(state.get("workspace_root") or state.get("trust_auto_answer_stage")))
|
|
407
|
-
or isinstance(runtime_cfg, dict)
|
|
408
|
-
)
|
|
409
|
-
if not implicit_own_workspace_trust and not explicit_opt_in:
|
|
410
|
-
event_log.write(
|
|
411
|
-
"leader_panes.trust_auto_answer_skipped",
|
|
412
|
-
pane_id=pane_id,
|
|
413
|
-
workspace=str(workspace),
|
|
414
|
-
reason="not_opted_in",
|
|
415
|
-
)
|
|
416
|
-
return {"ok": False, "answered": False, "reason": "not_opted_in"}
|
|
417
|
-
if not pane_id:
|
|
418
|
-
event_log.write(
|
|
419
|
-
"leader_panes.trust_auto_answer_skipped",
|
|
420
|
-
pane_id=None,
|
|
421
|
-
workspace=str(workspace),
|
|
422
|
-
reason="pane_id_missing",
|
|
423
|
-
)
|
|
424
|
-
return {"ok": False, "answered": False, "reason": "pane_id_missing"}
|
|
425
|
-
capture_hash = hashlib.sha256(pane_capture_tail.encode("utf-8")).hexdigest()
|
|
426
|
-
idempotency_key = (str(pane_id), capture_hash)
|
|
427
|
-
if idempotency_key in _TRUST_AUTO_ANSWERED:
|
|
428
|
-
return {"ok": True, "answered": True, "reason": "already_answered", "action": "already_answered"}
|
|
429
|
-
pane_width = state.get("pane_width") if explicit_opt_in and isinstance(state, dict) else None
|
|
430
|
-
if not _capture_tail_references_workspace(pane_capture_tail, workspace, pane_width):
|
|
431
|
-
event_log.write(
|
|
432
|
-
"leader_panes.trust_auto_answer_refused",
|
|
433
|
-
pane_id=pane_id,
|
|
434
|
-
workspace=str(workspace),
|
|
435
|
-
reason="workspace_dir_mismatch",
|
|
436
|
-
action="prompt_leader",
|
|
437
|
-
)
|
|
438
|
-
return {
|
|
439
|
-
"ok": False,
|
|
440
|
-
"answered": False,
|
|
441
|
-
"reason": "workspace_dir_mismatch",
|
|
442
|
-
"action": "prompt_leader",
|
|
443
|
-
"next_step": "Ask the leader whether to trust this foreign workspace prompt.",
|
|
444
|
-
}
|
|
445
|
-
answer = _tmux_inject_text(
|
|
446
|
-
str(pane_id),
|
|
447
|
-
"" if explicit_opt_in else "1",
|
|
448
|
-
"Enter",
|
|
449
|
-
f"team-agent-trust-auto-answer-{str(pane_id).strip('%') or 'pane'}",
|
|
450
|
-
attempts=1,
|
|
451
|
-
provider="fake",
|
|
452
|
-
bypass_non_input_gate=True,
|
|
453
|
-
)
|
|
454
|
-
if not answer.get("ok"):
|
|
455
|
-
error = answer.get("error") or "tmux send-keys failed"
|
|
456
|
-
event_log.write(
|
|
457
|
-
"leader_panes.trust_auto_answer_failed",
|
|
458
|
-
pane_id=pane_id,
|
|
459
|
-
workspace=str(workspace),
|
|
460
|
-
error=error,
|
|
461
|
-
)
|
|
462
|
-
return {"ok": False, "answered": False, "reason": "tmux_send_keys_failed", "error": error}
|
|
463
|
-
_TRUST_AUTO_ANSWERED.add(idempotency_key)
|
|
464
|
-
event_log.write(
|
|
465
|
-
"leader_panes.trust_auto_answered",
|
|
466
|
-
pane_id=pane_id,
|
|
467
|
-
workspace=str(workspace),
|
|
468
|
-
capture_hash=capture_hash,
|
|
469
|
-
)
|
|
470
|
-
return {"ok": True, "answered": True, "reason": "trust_auto_answered"}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
_SPEC_OPT_IN_DEPRECATION_MESSAGE = (
|
|
474
|
-
"WARNING: spec.runtime.auto_trust_own_workspace is deprecated. "
|
|
475
|
-
"Use env TEAM_AGENT_AUTO_TRUST_OWN_WORKSPACE=1 per session instead. "
|
|
476
|
-
"Spec-field will be removed in 0.3.0."
|
|
477
|
-
)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
def _auto_trust_opt_in(spec: dict[str, Any] | None, *, event_log: EventLog | None = None) -> bool:
|
|
481
|
-
"""Constitution-reviewer F3 (2026-05-26): env-var per-session opt-in is the
|
|
482
|
-
preferred path. spec.runtime.auto_trust_own_workspace remains honoured for
|
|
483
|
-
backwards compatibility but emits a one-shot stderr deprecation warning AND
|
|
484
|
-
a structured trust_auto_answer_spec_opt_in_deprecated event so a normalized
|
|
485
|
-
YAML field is auditable from a fresh log."""
|
|
486
|
-
spec_opted_in = (
|
|
487
|
-
isinstance(spec, dict)
|
|
488
|
-
and bool((spec.get("runtime") or {}).get("auto_trust_own_workspace"))
|
|
489
|
-
)
|
|
490
|
-
if spec_opted_in:
|
|
491
|
-
_emit_spec_opt_in_deprecation(event_log)
|
|
492
|
-
env = os.environ.get("TEAM_AGENT_AUTO_TRUST_OWN_WORKSPACE", "").strip().lower()
|
|
493
|
-
env_opted_in = env in {"1", "true", "yes", "on"}
|
|
494
|
-
return env_opted_in or spec_opted_in
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
def _emit_spec_opt_in_deprecation(event_log: EventLog | None) -> None:
|
|
498
|
-
"""Emit the deprecation warning once per process. The structured event still
|
|
499
|
-
fires per call so an audit log captures every yaml-driven decision."""
|
|
500
|
-
import sys
|
|
501
|
-
global _SPEC_OPT_IN_DEPRECATION_WARNED
|
|
502
|
-
if not _SPEC_OPT_IN_DEPRECATION_WARNED:
|
|
503
|
-
try:
|
|
504
|
-
print(_SPEC_OPT_IN_DEPRECATION_MESSAGE, file=sys.stderr, flush=True)
|
|
505
|
-
except Exception:
|
|
506
|
-
pass
|
|
507
|
-
_SPEC_OPT_IN_DEPRECATION_WARNED = True
|
|
508
|
-
if event_log is not None:
|
|
509
|
-
try:
|
|
510
|
-
event_log.write(
|
|
511
|
-
"trust_auto_answer_spec_opt_in_deprecated",
|
|
512
|
-
preferred_opt_in="env:TEAM_AGENT_AUTO_TRUST_OWN_WORKSPACE",
|
|
513
|
-
deprecated_field="spec.runtime.auto_trust_own_workspace",
|
|
514
|
-
removal_target_version="0.3.0",
|
|
515
|
-
)
|
|
516
|
-
except Exception:
|
|
517
|
-
pass
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
_SPEC_OPT_IN_DEPRECATION_WARNED = False
|
|
521
|
-
_TRUST_AUTO_ANSWERED: set[tuple[str, str]] = set()
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
def _reset_spec_opt_in_deprecation_state() -> None:
|
|
525
|
-
"""Test-only helper: reset the per-process one-shot guard so multiple cases
|
|
526
|
-
in the same interpreter can each observe the warning. Not part of the
|
|
527
|
-
public API."""
|
|
528
|
-
global _SPEC_OPT_IN_DEPRECATION_WARNED
|
|
529
|
-
_SPEC_OPT_IN_DEPRECATION_WARNED = False
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
def _capture_tail_references_workspace(tail: str, workspace: Path, pane_width: int | None = None) -> bool:
|
|
533
|
-
"""Decide whether the Codex trust-prompt tail names the worker's own
|
|
534
|
-
workspace cwd. The runtime cwd is the source of truth; the prompt path is a
|
|
535
|
-
consistency guard. Match cases (one converged helper per token):
|
|
536
|
-
|
|
537
|
-
- exact canonical equality (the unchanged baseline);
|
|
538
|
-
- mid-ellipsis ``head…tail`` / ``head...tail`` where head is a prefix of
|
|
539
|
-
the runtime cwd and tail is its suffix;
|
|
540
|
-
- hard right-edge truncation: the canonical runtime cwd starts with the
|
|
541
|
-
canonical captured path AND the captured token reaches the capture
|
|
542
|
-
line's right boundary (pane_width).
|
|
543
|
-
|
|
544
|
-
Without a pane_width signal, prefix matching is forbidden — the captured
|
|
545
|
-
path is treated as a complete token and must exactly equal the runtime cwd
|
|
546
|
-
(this is what stops ``/repo`` from sliding into ``/repo-backup``).
|
|
547
|
-
"""
|
|
548
|
-
if not tail:
|
|
549
|
-
return False
|
|
550
|
-
workspace_canonical = _canonicalize_path(workspace)
|
|
551
|
-
if not workspace_canonical:
|
|
552
|
-
return False
|
|
553
|
-
for token, source_line in _candidate_path_lines_from_prompt(tail):
|
|
554
|
-
if _workspace_matches_token(workspace_canonical, token, source_line, pane_width):
|
|
555
|
-
return True
|
|
556
|
-
return False
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
_PATH_LINE_RE = re.compile(r"(/[\w\-./~+@…]+)")
|
|
560
|
-
_ELLIPSIS_TOKENS = ("…", "...")
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
def _candidate_path_lines_from_prompt(tail: str) -> list[tuple[str, str]]:
|
|
564
|
-
"""Pull (path_token, source_line) pairs out of the prompt's tail. The
|
|
565
|
-
source line is the line AFTER stripping Codex box-drawing glyphs, so the
|
|
566
|
-
matcher can locate the token's end column relative to the visible width."""
|
|
567
|
-
pairs: list[tuple[str, str]] = []
|
|
568
|
-
seen: set[tuple[str, str]] = set()
|
|
569
|
-
for raw_line in tail.splitlines():
|
|
570
|
-
line = raw_line.strip()
|
|
571
|
-
for glyph in ("▌", "▎", "│"):
|
|
572
|
-
line = line.lstrip(glyph).strip()
|
|
573
|
-
if not line:
|
|
574
|
-
continue
|
|
575
|
-
for match in _PATH_LINE_RE.finditer(line):
|
|
576
|
-
token = match.group(1).rstrip("/")
|
|
577
|
-
if not token:
|
|
578
|
-
continue
|
|
579
|
-
key = (token, line)
|
|
580
|
-
if key in seen:
|
|
581
|
-
continue
|
|
582
|
-
seen.add(key)
|
|
583
|
-
pairs.append(key)
|
|
584
|
-
return pairs
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
def _candidate_paths_from_prompt(tail: str) -> list[str]:
|
|
588
|
-
"""Backwards-compatible token-only view (kept for any external callers)."""
|
|
589
|
-
out: list[str] = []
|
|
590
|
-
for token, _line in _candidate_path_lines_from_prompt(tail):
|
|
591
|
-
if token not in out:
|
|
592
|
-
out.append(token)
|
|
593
|
-
return out
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
def _workspace_matches_token(
|
|
597
|
-
workspace_canonical: str,
|
|
598
|
-
token: str,
|
|
599
|
-
source_line: str,
|
|
600
|
-
pane_width: int | None,
|
|
601
|
-
) -> bool:
|
|
602
|
-
"""The converged trust-prompt match logic.
|
|
603
|
-
|
|
604
|
-
Order matters:
|
|
605
|
-
1. exact canonical equality;
|
|
606
|
-
2. mid-ellipsis head/tail match;
|
|
607
|
-
3. right-edge hard truncation (prefix + boundary-reached).
|
|
608
|
-
A captured token that does NOT reach the line's right boundary is treated
|
|
609
|
-
as a complete short path and must equal the runtime cwd exactly.
|
|
610
|
-
"""
|
|
611
|
-
# 1. Exact canonical equality.
|
|
612
|
-
captured_canonical = _canonicalize_path(Path(token))
|
|
613
|
-
if not captured_canonical:
|
|
614
|
-
return False
|
|
615
|
-
if captured_canonical == workspace_canonical:
|
|
616
|
-
return True
|
|
617
|
-
# 2. Mid-ellipsis: split on … or ..., require head ⊑ workspace and workspace ⊐ tail.
|
|
618
|
-
for ellipsis in _ELLIPSIS_TOKENS:
|
|
619
|
-
if ellipsis in token:
|
|
620
|
-
head, _, tail_part = token.partition(ellipsis)
|
|
621
|
-
head_canonical = _canonicalize_path(Path(head)) if head.startswith("/") else head
|
|
622
|
-
if not head_canonical or not tail_part:
|
|
623
|
-
return False
|
|
624
|
-
return (
|
|
625
|
-
workspace_canonical.startswith(head_canonical)
|
|
626
|
-
and workspace_canonical.endswith(tail_part)
|
|
627
|
-
)
|
|
628
|
-
# 3. Right-edge hard truncation: prefix + boundary.
|
|
629
|
-
if not _token_reaches_right_edge(token, source_line, pane_width):
|
|
630
|
-
# No boundary signal → captured must be a complete token; exact already
|
|
631
|
-
# failed → mismatch (this rejects /repo vs /repo-backup both ways).
|
|
632
|
-
return False
|
|
633
|
-
return (
|
|
634
|
-
workspace_canonical == captured_canonical
|
|
635
|
-
or workspace_canonical.startswith(captured_canonical + "/")
|
|
636
|
-
or workspace_canonical.startswith(captured_canonical)
|
|
637
|
-
)
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
def _token_reaches_right_edge(token: str, source_line: str, pane_width: int | None) -> bool:
|
|
641
|
-
"""The token reaches the capture line's right boundary iff the line is wide
|
|
642
|
-
enough to be at pane capacity AND the token sits flush against the line's
|
|
643
|
-
end. Without a pane_width we cannot prove truncation — return False so the
|
|
644
|
-
caller falls back to exact-equality (this is the C/repo vs C/repo-backup
|
|
645
|
-
safeguard)."""
|
|
646
|
-
if not pane_width or pane_width <= 0:
|
|
647
|
-
return False
|
|
648
|
-
rstripped = source_line.rstrip()
|
|
649
|
-
if not rstripped.endswith(token):
|
|
650
|
-
return False
|
|
651
|
-
# Allow a one-column tolerance for trailing whitespace stripped from the
|
|
652
|
-
# raw capture; the line must be at pane capacity to count as hard-cut.
|
|
653
|
-
return len(rstripped) >= max(1, pane_width - 1)
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
def _canonicalize_path(p: Path | str) -> str:
|
|
657
|
-
try:
|
|
658
|
-
resolved = Path(p).expanduser().resolve(strict=False)
|
|
659
|
-
except OSError:
|
|
660
|
-
return ""
|
|
661
|
-
text = resolved.as_posix()
|
|
662
|
-
# Strip a trailing slash so boundary-safe equality holds.
|
|
663
|
-
return text.rstrip("/") if text != "/" else "/"
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
def _choose_leader_submit_key(provider: str, capture_text: str) -> tuple[str, str]:
|
|
667
|
-
if provider != "codex":
|
|
668
|
-
return "Enter", "non_codex_provider"
|
|
669
|
-
if re.search(r"esc to interrupt|working|running", capture_text, re.IGNORECASE):
|
|
670
|
-
return "Enter", "codex_busy_submit_followup"
|
|
671
|
-
if re.search(r"(›|❯|codex>)", capture_text):
|
|
672
|
-
return "Enter", "codex_idle_prompt"
|
|
673
|
-
return "Enter", "codex_state_unknown_submit"
|