@team-agent/installer 0.2.11 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -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
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
use super::launch_spawn::{seed_healthy_coordinator, DELEG_ROLE_ALPHA, DELEG_ROLE_BRAVO};
|
|
3
|
+
|
|
4
|
+
// ═════════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
const DELEG_ROLE_ALPHA_COMPAT: &str = "---\nname: alpha\nrole: Alpha Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: compatible_api\nprofile: alpha-compat\ntools:\n - mcp_team\n---\n\nAlpha.\n";
|
|
7
|
+
|
|
8
|
+
type LaneKills = std::sync::Arc<std::sync::Mutex<Vec<String>>>;
|
|
9
|
+
pub(super) type LaneSpawns = std::sync::Arc<std::sync::Mutex<Vec<(String, Vec<String>)>>>;
|
|
10
|
+
|
|
11
|
+
/// Recording transport for Lane-A v2: `list_windows`/`list_targets` answer from a configurable window
|
|
12
|
+
/// set (golden's `_tmux_window_exists` primitive = `tmux list-windows`); `kill_window` + spawn_first/into
|
|
13
|
+
/// are RECORDED. Every other method returns a benign Ok (never panics) so stop/reset/remove/fork run
|
|
14
|
+
/// end-to-end in-process.
|
|
15
|
+
pub(super) struct LaneTransport {
|
|
16
|
+
session: String,
|
|
17
|
+
windows: Vec<String>,
|
|
18
|
+
killed: LaneKills,
|
|
19
|
+
spawns: LaneSpawns,
|
|
20
|
+
}
|
|
21
|
+
impl LaneTransport {
|
|
22
|
+
pub(super) fn new(session: &str, windows: &[&str]) -> Self {
|
|
23
|
+
Self {
|
|
24
|
+
session: session.to_string(),
|
|
25
|
+
windows: windows.iter().map(|w| (*w).to_string()).collect(),
|
|
26
|
+
killed: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
|
27
|
+
spawns: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
fn killed(&self) -> Vec<String> {
|
|
31
|
+
self.killed.lock().unwrap().clone()
|
|
32
|
+
}
|
|
33
|
+
fn spawns(&self) -> Vec<(String, Vec<String>)> {
|
|
34
|
+
self.spawns.lock().unwrap().clone()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
impl crate::transport::Transport for LaneTransport {
|
|
38
|
+
fn kind(&self) -> crate::transport::BackendKind {
|
|
39
|
+
crate::transport::BackendKind::Tmux
|
|
40
|
+
}
|
|
41
|
+
fn spawn_first(&self, session: &crate::transport::SessionName, window: &crate::transport::WindowName, argv: &[String], _c: &std::path::Path, _e: &std::collections::BTreeMap<String, String>) -> Result<crate::transport::SpawnResult, crate::transport::TransportError> {
|
|
42
|
+
self.spawns.lock().unwrap().push(("spawn_first".to_string(), argv.to_vec()));
|
|
43
|
+
Ok(crate::transport::SpawnResult { pane_id: crate::transport::PaneId::new(format!("%{}", window.as_str())), session: session.clone(), window: window.clone(), child_pid: None })
|
|
44
|
+
}
|
|
45
|
+
fn spawn_into(&self, session: &crate::transport::SessionName, window: &crate::transport::WindowName, argv: &[String], _c: &std::path::Path, _e: &std::collections::BTreeMap<String, String>) -> Result<crate::transport::SpawnResult, crate::transport::TransportError> {
|
|
46
|
+
self.spawns.lock().unwrap().push(("spawn_into".to_string(), argv.to_vec()));
|
|
47
|
+
Ok(crate::transport::SpawnResult { pane_id: crate::transport::PaneId::new(format!("%{}", window.as_str())), session: session.clone(), window: window.clone(), child_pid: None })
|
|
48
|
+
}
|
|
49
|
+
fn inject(&self, _t: &crate::transport::Target, _p: &crate::transport::InjectPayload, _s: crate::transport::Key, _b: bool) -> Result<crate::transport::InjectReport, crate::transport::TransportError> {
|
|
50
|
+
unimplemented!("LaneTransport::inject not reached by stop/reset/remove/fork")
|
|
51
|
+
}
|
|
52
|
+
fn send_keys(&self, _t: &crate::transport::Target, _k: &[crate::transport::Key]) -> Result<(), crate::transport::TransportError> {
|
|
53
|
+
Ok(())
|
|
54
|
+
}
|
|
55
|
+
fn capture(&self, _t: &crate::transport::Target, r: crate::transport::CaptureRange) -> Result<crate::transport::CapturedText, crate::transport::TransportError> {
|
|
56
|
+
Ok(crate::transport::CapturedText { text: String::new(), range: r })
|
|
57
|
+
}
|
|
58
|
+
fn query(&self, _t: &crate::transport::Target, _f: crate::transport::PaneField) -> Result<Option<String>, crate::transport::TransportError> {
|
|
59
|
+
Ok(None)
|
|
60
|
+
}
|
|
61
|
+
fn liveness(&self, _p: &crate::transport::PaneId) -> Result<crate::model::enums::PaneLiveness, crate::transport::TransportError> {
|
|
62
|
+
Ok(crate::model::enums::PaneLiveness::Unknown)
|
|
63
|
+
}
|
|
64
|
+
fn list_targets(&self) -> Result<Vec<crate::transport::PaneInfo>, crate::transport::TransportError> {
|
|
65
|
+
Ok(self
|
|
66
|
+
.windows
|
|
67
|
+
.iter()
|
|
68
|
+
.map(|w| crate::transport::PaneInfo {
|
|
69
|
+
pane_id: crate::transport::PaneId::new(format!("%{w}")),
|
|
70
|
+
session: crate::transport::SessionName::new(&self.session),
|
|
71
|
+
window_index: None,
|
|
72
|
+
window_name: Some(crate::transport::WindowName::new(w)),
|
|
73
|
+
pane_index: None,
|
|
74
|
+
tty: None,
|
|
75
|
+
current_command: None,
|
|
76
|
+
current_path: None,
|
|
77
|
+
active: false,
|
|
78
|
+
pane_pid: None,
|
|
79
|
+
leader_env: std::collections::BTreeMap::new(),
|
|
80
|
+
})
|
|
81
|
+
.collect())
|
|
82
|
+
}
|
|
83
|
+
fn has_session(&self, _s: &crate::transport::SessionName) -> Result<bool, crate::transport::TransportError> {
|
|
84
|
+
Ok(true)
|
|
85
|
+
}
|
|
86
|
+
fn list_windows(&self, s: &crate::transport::SessionName) -> Result<Vec<crate::transport::WindowName>, crate::transport::TransportError> {
|
|
87
|
+
if s.as_str() == self.session {
|
|
88
|
+
Ok(self.windows.iter().map(|w| crate::transport::WindowName::new(w.as_str())).collect())
|
|
89
|
+
} else {
|
|
90
|
+
Ok(Vec::new())
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
fn set_session_env(&self, _s: &crate::transport::SessionName, _k: &str, _v: &str) -> Result<crate::transport::SetEnvOutcome, crate::transport::TransportError> {
|
|
94
|
+
Ok(crate::transport::SetEnvOutcome::Applied)
|
|
95
|
+
}
|
|
96
|
+
fn kill_session(&self, _s: &crate::transport::SessionName) -> Result<(), crate::transport::TransportError> {
|
|
97
|
+
Ok(())
|
|
98
|
+
}
|
|
99
|
+
fn kill_window(&self, t: &crate::transport::Target) -> Result<(), crate::transport::TransportError> {
|
|
100
|
+
let name = match t {
|
|
101
|
+
crate::transport::Target::Pane(p) => p.as_str().to_string(),
|
|
102
|
+
crate::transport::Target::SessionWindow { session, window } => format!("{}:{}", session.as_str(), window.as_str()),
|
|
103
|
+
};
|
|
104
|
+
self.killed.lock().unwrap().push(name);
|
|
105
|
+
Ok(())
|
|
106
|
+
}
|
|
107
|
+
fn attach_session(&self, _s: &crate::transport::SessionName) -> Result<crate::transport::AttachOutcome, crate::transport::TransportError> {
|
|
108
|
+
Ok(crate::transport::AttachOutcome::Attached)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// 2-agent (alpha, bravo) compiled spec + custom `state.agents` map. session_name = "team-laneateam"
|
|
113
|
+
/// (the compiled runtime.session_name). ensure_owner_allowed passes (no team_owner).
|
|
114
|
+
fn lanea_ws_agents(agents: serde_json::Value) -> PathBuf {
|
|
115
|
+
let ws = temp_ws().join("laneav2");
|
|
116
|
+
std::fs::create_dir_all(ws.join("agents")).unwrap();
|
|
117
|
+
std::fs::write(ws.join("TEAM.md"), "---\nname: laneateam\nobjective: Lane A v2 probe.\nprovider: codex\n---\n\nteam.\n").unwrap();
|
|
118
|
+
std::fs::write(ws.join("agents").join("alpha.md"), DELEG_ROLE_ALPHA).unwrap();
|
|
119
|
+
std::fs::write(ws.join("agents").join("bravo.md"), DELEG_ROLE_BRAVO).unwrap();
|
|
120
|
+
let spec = crate::compiler::compile_team(&ws).expect("compile lane-A v2 team");
|
|
121
|
+
// Make `alpha` REMOVABLE (golden-valid): compile_team auto-wires EVERY agent into routing/tasks
|
|
122
|
+
// (default_assignee + route-<id>.assign_to + task.assignee), but golden remove_agent removes only from
|
|
123
|
+
// agents + startup_order then validate_spec RAISES on dangling refs (agents.py:94 / spec.py:341-346) —
|
|
124
|
+
// so a routed agent is NOT removable in golden. Re-point the *validated* refs at the STAYING agent
|
|
125
|
+
// `bravo`, so removing `alpha` (an unrouted, dynamic-style worker — the real fork->remove case) passes
|
|
126
|
+
// validate. (match-block `assignee` lists are not validated, so they may keep referencing alpha.)
|
|
127
|
+
let yaml = crate::model::yaml::dumps(&spec)
|
|
128
|
+
.replace("default_assignee: \"alpha\"", "default_assignee: \"bravo\"")
|
|
129
|
+
.replace("assign_to: \"alpha\"", "assign_to: \"bravo\"")
|
|
130
|
+
.replace("assignee: \"alpha\"", "assignee: \"bravo\"");
|
|
131
|
+
assert!(!yaml.contains("default_assignee: \"alpha\""), "fixture unroute: default_assignee still alpha");
|
|
132
|
+
assert!(!yaml.contains("assign_to: \"alpha\""), "fixture unroute: a routing rule still assign_to alpha");
|
|
133
|
+
assert!(!yaml.contains("assignee: \"alpha\""), "fixture unroute: task still assignee alpha");
|
|
134
|
+
std::fs::write(ws.join("team.spec.yaml"), yaml).unwrap();
|
|
135
|
+
crate::state::persist::save_runtime_state(&ws, &json!({ "session_name": "team-laneateam", "agents": agents })).unwrap();
|
|
136
|
+
ws
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// SINGLE-worker compiled spec (alpha only). Removing alpha leaves `agents: []` -> validate_spec FAILS
|
|
140
|
+
/// ("/agents: must be a non-empty list", spec.rs:273) -> a deterministic IN-TRY mid-remove failure that
|
|
141
|
+
/// drives the rollback path (golden agents.py:110 except).
|
|
142
|
+
fn lanea_one_agent_ws(alpha_status: &str) -> PathBuf {
|
|
143
|
+
let ws = temp_ws().join("lanea1");
|
|
144
|
+
std::fs::create_dir_all(ws.join("agents")).unwrap();
|
|
145
|
+
std::fs::write(ws.join("TEAM.md"), "---\nname: laneateam\nobjective: Lane A one-agent probe.\nprovider: codex\n---\n\nteam.\n").unwrap();
|
|
146
|
+
std::fs::write(ws.join("agents").join("alpha.md"), DELEG_ROLE_ALPHA).unwrap();
|
|
147
|
+
let spec = crate::compiler::compile_team(&ws).expect("compile 1-agent team");
|
|
148
|
+
std::fs::write(ws.join("team.spec.yaml"), crate::model::yaml::dumps(&spec)).unwrap();
|
|
149
|
+
crate::state::persist::save_runtime_state(&ws, &json!({ "session_name": "team-laneateam", "agents": { "alpha": { "status": alpha_status, "provider": "codex", "window": "alpha" } } })).unwrap();
|
|
150
|
+
ws
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Rewrite the compiled spec's `context.state_file` (default `team_state.md`) to `name`, so the
|
|
154
|
+
/// rollback team-state path divergence (capture honors spec.context.state_file; restore hardcodes
|
|
155
|
+
/// team_state.md) is observable. Asserts the rewrite took, so a format change can't silently mis-test.
|
|
156
|
+
fn set_context_state_file(ws: &std::path::Path, name: &str) {
|
|
157
|
+
let p = ws.join("team.spec.yaml");
|
|
158
|
+
let text = std::fs::read_to_string(&p).unwrap();
|
|
159
|
+
let replaced = text.replace("state_file: \"team_state.md\"", &format!("state_file: \"{name}\""));
|
|
160
|
+
assert_ne!(replaced, text, "expected to rewrite context.state_file in the compiled spec; got:\n{text}");
|
|
161
|
+
std::fs::write(&p, replaced).unwrap();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// Fork workspace: source `alpha` (role doc = `alpha_role`) + `bravo`, alpha seeded RUNNING with a
|
|
165
|
+
/// source `session_id` (so fork reaches the spec-mutation / native-fork gate). Seeded-healthy coordinator
|
|
166
|
+
/// (fork's start_coordinator -> AlreadyRunning, no real daemon). session_name = "team-laneateam".
|
|
167
|
+
fn fork_ws(alpha_role: &str) -> PathBuf {
|
|
168
|
+
let ws = temp_ws().join("forkv2");
|
|
169
|
+
std::fs::create_dir_all(ws.join("agents")).unwrap();
|
|
170
|
+
std::fs::write(ws.join("TEAM.md"), "---\nname: laneateam\nobjective: Fork v2 probe.\nprovider: codex\n---\n\nteam.\n").unwrap();
|
|
171
|
+
std::fs::write(ws.join("agents").join("alpha.md"), alpha_role).unwrap();
|
|
172
|
+
std::fs::write(ws.join("agents").join("bravo.md"), DELEG_ROLE_BRAVO).unwrap();
|
|
173
|
+
let spec = crate::compiler::compile_team(&ws).expect("compile fork team");
|
|
174
|
+
std::fs::write(ws.join("team.spec.yaml"), crate::model::yaml::dumps(&spec)).unwrap();
|
|
175
|
+
crate::state::persist::save_runtime_state(
|
|
176
|
+
&ws,
|
|
177
|
+
&json!({
|
|
178
|
+
"session_name": "team-laneateam",
|
|
179
|
+
"agents": {
|
|
180
|
+
"alpha": { "status": "running", "provider": "codex", "window": "alpha", "session_id": "sess-a" },
|
|
181
|
+
"bravo": { "status": "running", "provider": "codex", "window": "bravo" }
|
|
182
|
+
}
|
|
183
|
+
}),
|
|
184
|
+
)
|
|
185
|
+
.unwrap();
|
|
186
|
+
seed_healthy_coordinator(&ws);
|
|
187
|
+
ws
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── STOP #1 (stop-window-gate-1) [RED] — window ABSENT => stopped=false + NO kill ────────────────────
|
|
191
|
+
// Golden operations.py:81-99 gates `tmux kill-window` behind `if _tmux_window_exists(session, window)`;
|
|
192
|
+
// an absent window (already-stopped / never-started spec-known agent) skips the kill and returns
|
|
193
|
+
// {ok:true, status:"stopped", stopped:FALSE}. Rust kills UNCONDITIONALLY (restart.rs:181-183) and
|
|
194
|
+
// hardcodes stopped:true (:192). RED: LaneTransport with NO windows -> the porter's list_windows gate
|
|
195
|
+
// must skip the kill (killed empty) and set stopped=false; today kill_window IS called + stopped=true.
|
|
196
|
+
#[test]
|
|
197
|
+
fn lanea_stop_window_absent_returns_stopped_false_no_kill() {
|
|
198
|
+
let ws = lanea_ws_agents(json!({ "alpha": { "status": "running", "provider": "codex", "window": "alpha" } }));
|
|
199
|
+
let tx = LaneTransport::new("team-laneateam", &[]); // alpha's window is ABSENT
|
|
200
|
+
let report = stop_agent_with_transport(&ws, &aid("alpha"), None, &tx).expect("window-absent stop is a clean Ok, not an Err (golden returns stopped:false)");
|
|
201
|
+
assert!(
|
|
202
|
+
!report.stopped,
|
|
203
|
+
"golden operations.py:81-99: an ABSENT tmux window => stopped=FALSE (kill skipped); Rust hardcodes stopped:true"
|
|
204
|
+
);
|
|
205
|
+
assert!(
|
|
206
|
+
tx.killed().is_empty(),
|
|
207
|
+
"golden gates kill on _tmux_window_exists; an absent window must NOT be killed; Rust kills unconditionally, killed={:?}",
|
|
208
|
+
tx.killed()
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── STOP #1 companion [LOCK] — window PRESENT => stopped=true + kill the golden target ───────────────
|
|
213
|
+
// The OTHER side of the gate: when the window exists, golden kills `<session>:<window>` and returns
|
|
214
|
+
// stopped:true. GREEN today (Rust kills unconditionally), so this LOCKS the present-branch against the
|
|
215
|
+
// window-gate fix regressing it.
|
|
216
|
+
#[test]
|
|
217
|
+
fn lanea_stop_window_present_kills_and_stopped_true() {
|
|
218
|
+
let ws = lanea_ws_agents(json!({ "alpha": { "status": "running", "provider": "codex", "window": "alpha" } }));
|
|
219
|
+
let tx = LaneTransport::new("team-laneateam", &["alpha"]); // window present
|
|
220
|
+
let report = stop_agent_with_transport(&ws, &aid("alpha"), None, &tx).expect("present-window stop ok");
|
|
221
|
+
assert!(report.stopped, "a present window must be killed => stopped=true");
|
|
222
|
+
assert_eq!(
|
|
223
|
+
tx.killed(),
|
|
224
|
+
vec!["team-laneateam:alpha".to_string()],
|
|
225
|
+
"stop must kill exactly the golden target <session>:<window>; killed={:?}",
|
|
226
|
+
tx.killed()
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── STOP #2 (stop-display-noop-2) [RED] — close ghostty_workspace slot: persist display.status/pane_title
|
|
231
|
+
// Golden operations.py:88-92 -> display/close.py:84-85 relabels the slot: display["status"]="stopped",
|
|
232
|
+
// display["pane_title"]=f"stopped: {agent_id}", written back into the persisted agent entry. Rust never
|
|
233
|
+
// touches the display (mark_agent_stopped leaves it as-is) and hardcodes display_closed:false. RED: the
|
|
234
|
+
// persisted display.status/pane_title are the in-process observable.
|
|
235
|
+
#[test]
|
|
236
|
+
fn lanea_stop_ghostty_workspace_relabels_slot_to_stopped() {
|
|
237
|
+
let ws = lanea_ws_agents(json!({
|
|
238
|
+
"alpha": { "status": "running", "provider": "codex", "window": "alpha",
|
|
239
|
+
"display": { "backend": "ghostty_workspace", "pane_id": "%5", "linked_session": "disp-alpha", "status": "running", "pane_title": "alpha" } }
|
|
240
|
+
}));
|
|
241
|
+
let tx = LaneTransport::new("team-laneateam", &["alpha"]);
|
|
242
|
+
let _ = stop_agent_with_transport(&ws, &aid("alpha"), None, &tx).expect("stop ok");
|
|
243
|
+
let state = crate::state::persist::load_runtime_state(&ws).expect("load state");
|
|
244
|
+
assert_eq!(
|
|
245
|
+
state.pointer("/agents/alpha/display/status").and_then(serde_json::Value::as_str),
|
|
246
|
+
Some("stopped"),
|
|
247
|
+
"stop must relabel a ghostty_workspace slot: display.status='stopped' (close.py:84); Rust leaves it 'running'"
|
|
248
|
+
);
|
|
249
|
+
assert_eq!(
|
|
250
|
+
state.pointer("/agents/alpha/display/pane_title").and_then(serde_json::Value::as_str),
|
|
251
|
+
Some("stopped: alpha"),
|
|
252
|
+
"stop must set display.pane_title='stopped: <id>' (close.py:85); Rust never touches the display"
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── RESET #3 (reset-paused-restart-2) [RED] — reset of a PAUSED agent returns ok=true (NOT an Err) ───
|
|
257
|
+
// Golden operations.py:126-140: after discard, reset re-spawns via start_agent(force,allow_fresh).
|
|
258
|
+
// discard does NOT clear `paused`, so start_agent returns the refusal-shaped {ok:False,status:paused,
|
|
259
|
+
// reason:agent_paused} (start.py:101) WITHOUT raising; reset embeds it as `started` and returns the
|
|
260
|
+
// success envelope {ok:True, status:"running", started:{ok:False,...}, coordinator:None}. Rust maps
|
|
261
|
+
// StartAgentOutcome::Paused -> Err(RequirementUnmet "agent ... is paused") (restart.rs:251-253) — a hard
|
|
262
|
+
// error instead of golden's ok=true. RED: reset of a paused agent must be Ok, not Err.
|
|
263
|
+
#[test]
|
|
264
|
+
fn lanea_reset_paused_agent_returns_ok_not_err() {
|
|
265
|
+
let ws = lanea_ws_agents(json!({
|
|
266
|
+
"alpha": { "status": "running", "provider": "codex", "window": "alpha", "paused": true, "session_id": "sess-a" }
|
|
267
|
+
}));
|
|
268
|
+
let tx = LaneTransport::new("team-laneateam", &["alpha"]);
|
|
269
|
+
let result = reset_agent_with_transport(&ws, &aid("alpha"), true, false, None, &tx);
|
|
270
|
+
assert!(
|
|
271
|
+
result.is_ok(),
|
|
272
|
+
"golden operations.py:133-140: reset of a PAUSED agent returns the ok=true success envelope embedding \
|
|
273
|
+
started={{ok:false,status:paused,reason:agent_paused}}; Rust raises RequirementUnmet. Porter must add a \
|
|
274
|
+
ResetAgentOutcome variant carrying the paused `started` result. got {result:?}"
|
|
275
|
+
);
|
|
276
|
+
assert!(
|
|
277
|
+
!matches!(result, Ok(ResetAgentOutcome::Refused { .. })),
|
|
278
|
+
"discard_session=true was passed -> the DiscardSessionRequired refusal must NOT fire; got {result:?}"
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── REMOVE #4 (remove-dynamic-agent-refused-1) [RED] — dynamic agent removable WITHOUT from_spec ─────
|
|
283
|
+
// Golden agents.py:50-54: dynamic_agent = bool(agent_state.dynamic_role_file OR agent.forked_from);
|
|
284
|
+
// it only refuses when `not dynamic_agent and not (from_spec and confirm)`. A dynamic/forked agent is
|
|
285
|
+
// removable with from_spec=false. Rust unconditionally `if !from_spec -> RefusedFromSpecConfirm`
|
|
286
|
+
// (restart.rs:285) BEFORE even loading the spec/state -> wrongly refuses the dynamic agent. RED.
|
|
287
|
+
#[test]
|
|
288
|
+
fn lanea_remove_dynamic_agent_removable_without_from_spec() {
|
|
289
|
+
let ws = lanea_ws_agents(json!({
|
|
290
|
+
"alpha": { "status": "stopped", "provider": "codex", "window": "alpha", "dynamic_role_file": ".team/dynamic-role-files/alpha.md" },
|
|
291
|
+
"bravo": { "status": "stopped", "provider": "codex", "window": "bravo" }
|
|
292
|
+
}));
|
|
293
|
+
// make the dynamic role file exist (so removal resolves+deletes it cleanly under either path policy).
|
|
294
|
+
let dyn_dir = ws.join(".team").join("dynamic-role-files");
|
|
295
|
+
std::fs::create_dir_all(&dyn_dir).unwrap();
|
|
296
|
+
std::fs::write(dyn_dir.join("alpha.md"), "dynamic alpha role\n").unwrap();
|
|
297
|
+
let tx = LaneTransport::new("team-laneateam", &[]); // alpha not running
|
|
298
|
+
let result = remove_agent_with_transport(&ws, &aid("alpha"), false, true, None, &tx); // from_spec=FALSE
|
|
299
|
+
assert!(
|
|
300
|
+
!matches!(result, Ok(RemoveAgentOutcome::RefusedFromSpecConfirm { .. })),
|
|
301
|
+
"golden agents.py:50-54: a DYNAMIC agent (state.dynamic_role_file) is removable with from_spec=false; \
|
|
302
|
+
Rust wrongly returns RefusedFromSpecConfirm; got {result:?}"
|
|
303
|
+
);
|
|
304
|
+
let state = crate::state::persist::load_runtime_state(&ws).expect("load state");
|
|
305
|
+
assert!(
|
|
306
|
+
state.get("agents").and_then(serde_json::Value::as_object).is_some_and(|a| !a.contains_key("alpha")),
|
|
307
|
+
"the dynamic agent must actually be removed from state.agents; got {result:?}"
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── REMOVE #5 (remove-unknown-precedence-2) [RED] — unknown-worker raised BEFORE from_spec refusal ───
|
|
312
|
+
// Golden agents.py:41-54 loads the spec and runs _find_worker (raising "unknown worker agent id: <id>")
|
|
313
|
+
// BEFORE the from_spec/confirm refusal at :53. Rust checks `!from_spec` FIRST (restart.rs:285) and
|
|
314
|
+
// returns RefusedFromSpecConfirm without ever loading the spec -> a nonexistent agent is mis-reported as
|
|
315
|
+
// a from_spec refusal. RED: an unknown agent with from_spec=false must surface "unknown worker".
|
|
316
|
+
#[test]
|
|
317
|
+
fn lanea_remove_unknown_agent_precedes_from_spec_refusal() {
|
|
318
|
+
let ws = lanea_ws_agents(json!({ "alpha": { "status": "stopped", "provider": "codex", "window": "alpha" } }));
|
|
319
|
+
let tx = LaneTransport::new("team-laneateam", &[]);
|
|
320
|
+
let text = format!("{:?}", remove_agent_with_transport(&ws, &aid("ghost"), false, false, None, &tx));
|
|
321
|
+
assert!(
|
|
322
|
+
text.contains("unknown worker"),
|
|
323
|
+
"golden agents.py:41-54: the unknown-worker check precedes the from_spec refusal; an unknown agent must \
|
|
324
|
+
raise 'unknown worker agent id: ghost', NOT RefusedFromSpecConfirm; got {text}"
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ── REMOVE #8 (remove-team-state-md-content-5) [RED] — team_state.md is golden MARKDOWN, not JSON ────
|
|
329
|
+
// Golden state.py:625-686 write_team_state builds a Markdown doc ("# Team State", "## Objective",
|
|
330
|
+
// "## Agents" with one "- {id}: {role} on {provider} ({status})" per spec agent, etc.) from removed_spec.
|
|
331
|
+
// Rust write_team_state (restart.rs:925-941) writes serde_json::to_string_pretty(state) — raw JSON — and
|
|
332
|
+
// passes the ORIGINAL spec (so the removed agent would not be excluded). RED: after a successful remove,
|
|
333
|
+
// team_state.md must be the Markdown doc, list the REMAINING agent (bravo) and NOT the removed one.
|
|
334
|
+
#[test]
|
|
335
|
+
fn lanea_remove_writes_markdown_team_state_not_json() {
|
|
336
|
+
let ws = lanea_ws_agents(json!({
|
|
337
|
+
"alpha": { "status": "stopped", "provider": "codex", "window": "alpha" },
|
|
338
|
+
"bravo": { "status": "running", "provider": "codex", "window": "bravo" }
|
|
339
|
+
}));
|
|
340
|
+
let tx = LaneTransport::new("team-laneateam", &[]);
|
|
341
|
+
let _ = remove_agent_with_transport(&ws, &aid("alpha"), true, true, None, &tx).expect("remove ok");
|
|
342
|
+
let team_state = std::fs::read_to_string(ws.join("team_state.md")).expect("team_state.md written");
|
|
343
|
+
assert!(
|
|
344
|
+
team_state.starts_with("# Team State"),
|
|
345
|
+
"golden write_team_state emits a Markdown document starting '# Team State'; Rust dumps JSON; got:\n{team_state}"
|
|
346
|
+
);
|
|
347
|
+
assert!(
|
|
348
|
+
!team_state.trim_start().starts_with('{'),
|
|
349
|
+
"team_state.md must NOT be a JSON dump of runtime state; got:\n{team_state}"
|
|
350
|
+
);
|
|
351
|
+
assert!(team_state.contains("## Agents"), "golden has a '## Agents' section; got:\n{team_state}");
|
|
352
|
+
assert!(
|
|
353
|
+
team_state.contains("bravo: Bravo Worker on codex"),
|
|
354
|
+
"the '## Agents' section must list the remaining agent bravo (golden '- {{id}}: {{role}} on {{provider}} ({{status}})'); got:\n{team_state}"
|
|
355
|
+
);
|
|
356
|
+
assert!(
|
|
357
|
+
!team_state.contains("alpha: Alpha Worker"),
|
|
358
|
+
"the removed agent (alpha) must be EXCLUDED — golden writes removed_spec, not the original spec; got:\n{team_state}"
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── REMOVE #10 (remove-is-running-no-tmux-fallback-7) [RED] — is_running honors the tmux-window fallback
|
|
363
|
+
// Golden agents.py:247-252 _is_running returns True if status in {running,busy} OR
|
|
364
|
+
// (session_name AND _tmux_window_exists(session, window)). Rust agent_is_running (restart.rs:689-700)
|
|
365
|
+
// only checks status -> an agent with a STALE status ('idle') whose tmux window is still live is treated
|
|
366
|
+
// as not-running, so removal without --force is wrongly ALLOWED. RED: such an agent removed without
|
|
367
|
+
// force must be RefusedForceRequired (golden), not Removed.
|
|
368
|
+
#[test]
|
|
369
|
+
fn lanea_remove_is_running_honors_tmux_window_fallback() {
|
|
370
|
+
let ws = lanea_ws_agents(json!({
|
|
371
|
+
"alpha": { "status": "idle", "provider": "codex", "window": "alpha" }, // stale status, but window is live
|
|
372
|
+
"bravo": { "status": "stopped", "provider": "codex", "window": "bravo" }
|
|
373
|
+
}));
|
|
374
|
+
let tx = LaneTransport::new("team-laneateam", &["alpha"]); // alpha's tmux window EXISTS
|
|
375
|
+
let result = remove_agent_with_transport(&ws, &aid("alpha"), true, false, None, &tx); // force=FALSE
|
|
376
|
+
assert!(
|
|
377
|
+
matches!(result, Ok(RemoveAgentOutcome::RefusedForceRequired { .. })),
|
|
378
|
+
"golden agents.py:247-252: a stale-status agent whose tmux window is LIVE counts as running -> removal \
|
|
379
|
+
without --force is RefusedForceRequired; Rust drops the tmux fallback and allows it. Porter must thread \
|
|
380
|
+
the transport into agent_is_running. got {result:?}"
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── REMOVE #7 (remove-rollback-no-restart-running-4) [RED] — rollback RESTARTS the force-stopped worker
|
|
385
|
+
// Golden agents.py:78,219-223: after force-stopping a running worker, rollback (on any in-try failure)
|
|
386
|
+
// sets restore_running and calls start_agent(force=True, allow_fresh=True) to bring the worker back.
|
|
387
|
+
// Rust rollback (restart.rs:1023-1067) has no restore_running and never restarts -> a force-remove that
|
|
388
|
+
// stops a running agent then fails leaves it DEAD. RED: drive a deterministic in-try failure
|
|
389
|
+
// (1-worker team: removing alpha -> empty agents -> validate_spec fails AFTER the force-stop) and assert
|
|
390
|
+
// the transport recorded a re-spawn during rollback (the golden worker restart). Today: zero spawns.
|
|
391
|
+
#[test]
|
|
392
|
+
fn lanea_remove_rollback_restarts_force_stopped_worker() {
|
|
393
|
+
let ws = lanea_one_agent_ws("running"); // removing alpha -> agents:[] -> validate_spec FAILS post-stop
|
|
394
|
+
let tx = LaneTransport::new("team-laneateam", &["alpha"]);
|
|
395
|
+
let result = remove_agent_with_transport(&ws, &aid("alpha"), true, true, None, &tx); // from_spec+force
|
|
396
|
+
assert!(
|
|
397
|
+
result.is_err(),
|
|
398
|
+
"precondition: removing the only worker makes removed_spec invalid (validate_spec) -> the remove fails \
|
|
399
|
+
after the force-stop, triggering rollback; got {result:?}"
|
|
400
|
+
);
|
|
401
|
+
assert!(
|
|
402
|
+
tx.killed().contains(&"team-laneateam:alpha".to_string()),
|
|
403
|
+
"precondition: the running worker was force-stopped (window killed) before the failure; killed={:?}",
|
|
404
|
+
tx.killed()
|
|
405
|
+
);
|
|
406
|
+
assert!(
|
|
407
|
+
!tx.spawns().is_empty(),
|
|
408
|
+
"golden agents.py:219-223: rollback must RESTART the force-stopped worker (start_agent force=True) -> a \
|
|
409
|
+
re-spawn; Rust rollback never restarts, leaving the worker dead. Porter must thread the transport into \
|
|
410
|
+
RemoveRollback::restore. spawns={:?}",
|
|
411
|
+
tx.spawns()
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── REMOVE #11 (remove-dynamic-role-path-and-required-8, warn) [RED] — missing REQUIRED role file raises
|
|
416
|
+
// Golden agents.py:255-261 _remove_dynamic_role_file(path, required=True) RAISES "dynamic role file
|
|
417
|
+
// missing: <path>" when the state recorded a dynamic_role_file but it is absent. Rust hardcodes the
|
|
418
|
+
// default path and returns Ok(false) silently (restart.rs:951-953), losing the hard-fail+rollback. RED:
|
|
419
|
+
// a dynamic agent whose recorded role file is MISSING must raise, not silently complete the removal.
|
|
420
|
+
#[test]
|
|
421
|
+
fn lanea_remove_dynamic_role_file_missing_raises() {
|
|
422
|
+
let ws = lanea_ws_agents(json!({
|
|
423
|
+
"alpha": { "status": "stopped", "provider": "codex", "window": "alpha", "dynamic_role_file": ".team/dynamic-role-files/custom.md" }, // file NOT created
|
|
424
|
+
"bravo": { "status": "stopped", "provider": "codex", "window": "bravo" }
|
|
425
|
+
}));
|
|
426
|
+
let tx = LaneTransport::new("team-laneateam", &[]);
|
|
427
|
+
let text = format!("{:?}", remove_agent_with_transport(&ws, &aid("alpha"), true, true, None, &tx));
|
|
428
|
+
assert!(
|
|
429
|
+
text.contains("dynamic role file missing"),
|
|
430
|
+
"golden agents.py:259-260: a state-recorded dynamic_role_file that is MISSING must RAISE 'dynamic role \
|
|
431
|
+
file missing: <path>' (required=true); Rust returns Ok(false) silently and completes the remove. got {text}"
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ── REMOVE #9 (remove-rollback-team-state-path-6) [RED] — rollback restores via spec.context.state_file
|
|
436
|
+
// Golden agents.py:181-204 derives team_state_path = workspace / spec.context.state_file once and uses
|
|
437
|
+
// it for BOTH capture and restore. Rust capture honors spec.context.state_file (restart.rs:994-999) but
|
|
438
|
+
// restore HARDCODES workspace/team_state.md (:1031). With a custom state_file, rollback writes the
|
|
439
|
+
// captured content to the WRONG file. RED (deterministic in-try failure via the 1-worker validate-fail):
|
|
440
|
+
// after rollback, a SPURIOUS team_state.md must NOT exist (golden restores the custom file, never creates
|
|
441
|
+
// team_state.md). Today the hardcoded restore creates team_state.md.
|
|
442
|
+
#[test]
|
|
443
|
+
fn lanea_remove_rollback_restores_via_spec_state_file_path() {
|
|
444
|
+
let ws = lanea_one_agent_ws("stopped");
|
|
445
|
+
set_context_state_file(&ws, "custom_state.md");
|
|
446
|
+
std::fs::write(ws.join("custom_state.md"), "ORIGINAL CUSTOM TEAM STATE\n").unwrap(); // capture reads this
|
|
447
|
+
let tx = LaneTransport::new("team-laneateam", &[]);
|
|
448
|
+
let result = remove_agent_with_transport(&ws, &aid("alpha"), true, true, None, &tx);
|
|
449
|
+
assert!(result.is_err(), "precondition: 1-worker removal -> validate_spec fails -> rollback runs; got {result:?}");
|
|
450
|
+
assert!(
|
|
451
|
+
!ws.join("team_state.md").exists(),
|
|
452
|
+
"golden agents.py:200-204: rollback restores the spec-derived state_file (custom_state.md), it must NOT \
|
|
453
|
+
create the hardcoded team_state.md. Porter must capture team_state_path on the rollback struct and reuse \
|
|
454
|
+
it in restore."
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ── FORK (fork-dup-guard-misses-leader) [RED] — forking ONTO the leader id is 'already exists' ───────
|
|
459
|
+
// Golden operations.py:301-302 uses _find_agent (matches agents AND the leader, runtime.py:1055), so
|
|
460
|
+
// forking to as_agent_id == leader.id raises 'agent id already exists: <id>'. Rust find_spec_agent
|
|
461
|
+
// short-circuits to None for the leader id (launch.rs:507-515) -> the duplicate guard is SKIPPED and the
|
|
462
|
+
// fork proceeds against the leader id. RED: fork target == "leader" must be 'already exists'.
|
|
463
|
+
#[test]
|
|
464
|
+
fn lanea_fork_dup_target_leader_id_is_already_exists() {
|
|
465
|
+
let ws = fork_ws(DELEG_ROLE_ALPHA);
|
|
466
|
+
let tx = LaneTransport::new("team-laneateam", &[]);
|
|
467
|
+
let text = format!("{:?}", fork_agent_with_transport(&ws, &aid("alpha"), &aid("leader"), false, None, &tx));
|
|
468
|
+
assert!(
|
|
469
|
+
text.contains("already exists"),
|
|
470
|
+
"golden operations.py:301-302 (_find_agent matches the leader): forking ONTO the leader id must raise \
|
|
471
|
+
'agent id already exists: leader'; Rust skips the dup guard for the leader id and proceeds. got {text}"
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── FORK (fork-missing-tmux-window-guard) [RED] — window-already-exists guard BEFORE spec mutation ────
|
|
476
|
+
// Golden operations.py:310-312: after the session_id guard and BEFORE mutating the spec, raise
|
|
477
|
+
// 'tmux window already exists for fork target: {session}:{as_agent_id}' if the window exists. Rust has no
|
|
478
|
+
// such guard (launch.rs:439-440) -> it appends + writes the spec regardless. RED: a pre-existing window
|
|
479
|
+
// for the target must (a) raise that exact message and (b) leave the spec UNMUTATED (no fork agent).
|
|
480
|
+
#[test]
|
|
481
|
+
fn lanea_fork_window_already_exists_guard_before_spec_mutation() {
|
|
482
|
+
let ws = fork_ws(DELEG_ROLE_ALPHA);
|
|
483
|
+
let tx = LaneTransport::new("team-laneateam", &["newfork"]); // the target window already exists
|
|
484
|
+
let text = format!("{:?}", fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), false, None, &tx));
|
|
485
|
+
assert!(
|
|
486
|
+
text.contains("tmux window already exists for fork target: team-laneateam:newfork"),
|
|
487
|
+
"golden operations.py:310-312: a pre-existing target window must raise 'tmux window already exists for \
|
|
488
|
+
fork target: team-laneateam:newfork' BEFORE spec mutation; Rust has no guard. got {text}"
|
|
489
|
+
);
|
|
490
|
+
let spec_text = std::fs::read_to_string(ws.join("team.spec.yaml")).unwrap();
|
|
491
|
+
assert!(
|
|
492
|
+
!spec_text.contains("newfork"),
|
|
493
|
+
"the guard must fire BEFORE the spec is mutated; the spec must NOT contain the fork agent 'newfork'"
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ── FORK (fork-gate-error-text) [RED] + (fork-incomplete-rollback, adapter arm) — golden gate text + spec rollback
|
|
498
|
+
// Golden operations.py:329-330 raises f"{provider} does not support native session fork" when the native
|
|
499
|
+
// fork gate fails (auth_mode==compatible_api). Rust relies on adapter.fork() -> CapabilityUnsupported
|
|
500
|
+
// ("Codex:fork") (adapter.rs:310) -> a different observable. AND golden wraps the post-spec-write steps
|
|
501
|
+
// in try/except restoring the spec on ANY failure (operations.py:384-394); Rust writes the spec
|
|
502
|
+
// (launch.rs:443) then errors at adapter.fork (458-460) WITHOUT restoring it. RED on both: the message
|
|
503
|
+
// text AND the spec must be rolled back to not contain the fork agent.
|
|
504
|
+
#[test]
|
|
505
|
+
fn lanea_fork_gate_error_text_and_spec_rollback_on_adapter_arm() {
|
|
506
|
+
let ws = fork_ws(DELEG_ROLE_ALPHA_COMPAT); // source alpha auth_mode=compatible_api -> native fork unsupported
|
|
507
|
+
let tx = LaneTransport::new("team-laneateam", &[]);
|
|
508
|
+
let result = fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), false, None, &tx);
|
|
509
|
+
let text = format!("{result:?}");
|
|
510
|
+
assert!(
|
|
511
|
+
text.contains("codex does not support native session fork"),
|
|
512
|
+
"golden operations.py:329-330: the native-fork gate must raise 'codex does not support native session \
|
|
513
|
+
fork'; Rust surfaces the generic 'capability unsupported: Codex:fork'. got {text}"
|
|
514
|
+
);
|
|
515
|
+
let spec_text = std::fs::read_to_string(ws.join("team.spec.yaml")).unwrap();
|
|
516
|
+
assert!(
|
|
517
|
+
!spec_text.contains("newfork"),
|
|
518
|
+
"golden operations.py:384-394: on the gate failure the spec must be ROLLED BACK; Rust writes the spec \
|
|
519
|
+
then errors at adapter.fork without restoring it, leaving the fork agent 'newfork' in the spec"
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ── FORK (fork-report-session-id-is-pane-id) [RED] — report session_id is the captured id / None, not pane
|
|
524
|
+
// Golden operations.py:399,408 returns state['agents'][as_agent_id].get('session_id') — the captured
|
|
525
|
+
// provider session id (or None if capture missed, raise_on_missed=False). Rust sets
|
|
526
|
+
// session_id: Some(SessionId::new(spawn.pane_id)) (launch.rs:502) — the tmux pane id ('%newfork'),
|
|
527
|
+
// a different value kind. RED: the report session_id must NOT be the pane id (None, since the Rust fork
|
|
528
|
+
// path performs no session capture).
|
|
529
|
+
#[test]
|
|
530
|
+
fn lanea_fork_report_session_id_is_not_pane_id() {
|
|
531
|
+
let ws = fork_ws(DELEG_ROLE_ALPHA); // codex+subscription -> native fork supported -> full success path
|
|
532
|
+
let tx = LaneTransport::new("team-laneateam", &[]);
|
|
533
|
+
let report = fork_agent_with_transport(&ws, &aid("alpha"), &aid("newfork"), false, None, &tx).expect("fork ok (codex subscription supports fork)");
|
|
534
|
+
assert_ne!(
|
|
535
|
+
report.session_id,
|
|
536
|
+
Some(crate::provider::SessionId::new("%newfork")),
|
|
537
|
+
"golden operations.py:399,408: report.session_id is the captured provider session id / None, NEVER the \
|
|
538
|
+
tmux pane id; Rust returns Some(pane_id='%newfork')"
|
|
539
|
+
);
|
|
540
|
+
assert_eq!(
|
|
541
|
+
report.session_id, None,
|
|
542
|
+
"the Rust fork path captures no session -> report.session_id must be None (golden capture-missed), not the pane id"
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ── REMOVE #6/#12 (remove-rollback-no-agent-health-3 / remove-rollback-health-1) [SEAM #[ignore]] ────
|
|
547
|
+
// Golden _RemoveRollback captures `self.health = copy.deepcopy(store.agent_health().get(agent_id))`
|
|
548
|
+
// (agents.py:185) and restore() re-upserts it via _restore_agent_health (agents.py:215-218,268-278). The
|
|
549
|
+
// Rust RemoveRollback has NO health field and never restores it (restart.rs:972-1067). This is only
|
|
550
|
+
// observable when a step AFTER the agent_health delete fails — but Rust's only post-delete step is the
|
|
551
|
+
// snapshot, which golden runs OUTSIDE the rollback-protected region (agents.py:135). Exercising it
|
|
552
|
+
// golden-faithfully needs a production failure-injection seam at an in-try step after the delete (mirror
|
|
553
|
+
// the coordinator SaveHook). PORTER: add `health: Option<Value>` to RemoveRollback (capture the row
|
|
554
|
+
// before delete; restore re-upserts status||"IDLE"/last_output_at/context_usage_pct/current_task_id, or
|
|
555
|
+
// deletes if None) AND move save_team_runtime_snapshot OUTSIDE the rollback region (golden agents.py:135).
|
|
556
|
+
#[test]
|
|
557
|
+
#[ignore = "seam: agent_health rollback restore needs a failure-injection hook (post-delete, in-try) to \
|
|
558
|
+
exercise in-process; golden agents.py:185/215-218/268-278. Porter adds RemoveRollback.health + \
|
|
559
|
+
moves save_team_runtime_snapshot outside the rollback region."]
|
|
560
|
+
fn lanea_remove_rollback_restores_agent_health() {
|
|
561
|
+
// Golden contract (verified by reading agents.py): on a mid-remove failure after the agent_health
|
|
562
|
+
// row is deleted, rollback re-upserts the captured row so the health history is not lost.
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ── FORK (fork-incomplete-rollback) [SEAM #[ignore]] — post-spawn rollback arms ─────────────────────
|
|
566
|
+
// Golden operations.py:384-394 wraps spec-mutation..start_coordinator in try/except; on ANY failure it
|
|
567
|
+
// (1) kills the spawned tmux window if present, (2) adapter.cleanup_mcp, (3) restores old spec text, and
|
|
568
|
+
// (4) restores prior state. Rust only restores the spec on the spawn_into arm (launch.rs:481); the
|
|
569
|
+
// save_runtime_state (486-487) and start_coordinator (488-493) failure arms leave the spec mutated, the
|
|
570
|
+
// already-spawned window un-killed, and the state un-rolled-back; install_mcp/cleanup_mcp are absent.
|
|
571
|
+
// The adapter.fork arm IS covered HARD above (lanea_fork_gate_error_text_and_spec_rollback_on_adapter_arm).
|
|
572
|
+
// The post-SPAWN arms need a failure-injection seam after spawn_into (codex+subscription forks past
|
|
573
|
+
// adapter.fork, so the spawn succeeds and there is no in-process way to fail save/coordinator cleanly).
|
|
574
|
+
// PORTER: a Drop guard armed after the spec write, disarmed on success — kills the window, restores spec
|
|
575
|
+
// + state, runs cleanup_mcp on every post-write error arm.
|
|
576
|
+
#[test]
|
|
577
|
+
#[ignore = "seam: fork post-spawn rollback arms (save_runtime_state / start_coordinator failure) need a \
|
|
578
|
+
failure-injection hook after spawn_into; golden operations.py:384-394. Porter wires a Drop \
|
|
579
|
+
guard (kill window + restore spec/state + cleanup_mcp) armed after the spec write."]
|
|
580
|
+
fn lanea_fork_rollback_complete_on_post_spawn_failure() {
|
|
581
|
+
// Golden contract (operations.py:384-394): a post-spawn failure kills the spawned window, restores
|
|
582
|
+
// the old spec text + prior state, and runs cleanup_mcp before re-raising.
|
|
583
|
+
}
|