@team-agent/installer 0.5.60 → 0.5.62
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 +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +49 -3
- package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
- package/crates/team-agent/src/cli/diagnose.rs +184 -0
- package/crates/team-agent/src/cli/emit.rs +104 -21
- package/crates/team-agent/src/cli/helpers.rs +1 -0
- package/crates/team-agent/src/cli/leader.rs +14 -2
- package/crates/team-agent/src/cli/leaders.rs +2 -0
- package/crates/team-agent/src/cli/mod.rs +296 -8
- package/crates/team-agent/src/cli/named_address.rs +26 -2
- package/crates/team-agent/src/cli/profile.rs +2 -1
- package/crates/team-agent/src/cli/send/coordinator.rs +1 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +3 -0
- package/crates/team-agent/src/cli/send/persist.rs +1 -0
- package/crates/team-agent/src/cli/send/presentation.rs +3 -0
- package/crates/team-agent/src/cli/send/resolve.rs +206 -1
- package/crates/team-agent/src/cli/send.rs +2 -1
- package/crates/team-agent/src/cli/spec.rs +5 -1
- package/crates/team-agent/src/cli/status.rs +2 -1
- package/crates/team-agent/src/cli/status_port/compact.rs +1 -0
- package/crates/team-agent/src/cli/status_port/format.rs +1 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +1 -0
- package/crates/team-agent/src/cli/status_port/store.rs +7 -3
- package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
- package/crates/team-agent/src/cli/types.rs +36 -2
- package/crates/team-agent/src/communication_mode/mod.rs +54 -0
- package/crates/team-agent/src/compiler/tests.rs +5 -5
- package/crates/team-agent/src/compiler.rs +54 -1
- package/crates/team-agent/src/conpty/backend.rs +1 -0
- package/crates/team-agent/src/conpty/mod.rs +1 -0
- package/crates/team-agent/src/coordinator/backoff.rs +5 -8
- package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
- package/crates/team-agent/src/coordinator/health.rs +1 -0
- package/crates/team-agent/src/coordinator/mod.rs +2 -2
- package/crates/team-agent/src/coordinator/orphan.rs +4 -116
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +6 -5
- package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
- package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
- package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +0 -9
- package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
- package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -42
- package/crates/team-agent/src/coordinator/tick.rs +24 -28
- package/crates/team-agent/src/coordinator/types.rs +3 -176
- package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
- package/crates/team-agent/src/db/message_store.rs +47 -2
- package/crates/team-agent/src/db/migration.rs +5 -4
- package/crates/team-agent/src/db/mod.rs +1 -0
- package/crates/team-agent/src/db/schema.rs +1 -0
- package/crates/team-agent/src/diagnose/comms.rs +2 -1
- package/crates/team-agent/src/diagnose/mod.rs +1 -0
- package/crates/team-agent/src/diagnose/orphans.rs +1 -0
- package/crates/team-agent/src/fake_worker.rs +21 -1
- package/crates/team-agent/src/layout/manager.rs +3 -7
- package/crates/team-agent/src/layout/mod.rs +2 -2
- package/crates/team-agent/src/layout/overlay.rs +6 -1
- package/crates/team-agent/src/layout/placement.rs +1 -0
- package/crates/team-agent/src/layout/recovery.rs +3 -0
- package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
- package/crates/team-agent/src/layout/sessions.rs +8 -1
- package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
- package/crates/team-agent/src/layout/worker_env.rs +3 -0
- package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
- package/crates/team-agent/src/leader/helpers.rs +1 -0
- package/crates/team-agent/src/leader/incident.rs +1 -0
- package/crates/team-agent/src/leader/lease.rs +27 -22
- package/crates/team-agent/src/leader/mod.rs +2 -3
- package/crates/team-agent/src/leader/owner_bind.rs +13 -3
- package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
- package/crates/team-agent/src/leader/rediscover.rs +4 -17
- package/crates/team-agent/src/leader/registry.rs +3 -15
- package/crates/team-agent/src/leader/start.rs +1072 -186
- package/crates/team-agent/src/leader/takeover.rs +10 -232
- package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
- package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
- package/crates/team-agent/src/leader/tests/identity.rs +71 -59
- package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
- package/crates/team-agent/src/leader/tests/idle.rs +0 -193
- package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
- package/crates/team-agent/src/leader/tests/mod.rs +4 -41
- package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
- package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
- package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
- package/crates/team-agent/src/leader/types.rs +1 -28
- package/crates/team-agent/src/lib.rs +7 -0
- package/crates/team-agent/src/lifecycle/display.rs +7 -1
- package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -28
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch.rs +11 -12
- package/crates/team-agent/src/lifecycle/mod.rs +2 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +4 -4
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
- package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
- package/crates/team-agent/src/lifecycle/restart.rs +13 -11
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
- package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
- package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
- package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
- package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
- package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
- package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
- package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
- package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
- package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
- package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
- package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
- package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
- package/crates/team-agent/src/lifecycle/tests.rs +59 -0
- package/crates/team-agent/src/lifecycle/types.rs +0 -9
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
- package/crates/team-agent/src/mcp_server/mod.rs +1 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +5 -3
- package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
- package/crates/team-agent/src/mcp_server/tools.rs +101 -66
- package/crates/team-agent/src/mcp_server/types.rs +6 -18
- package/crates/team-agent/src/mcp_server/wire.rs +32 -26
- package/crates/team-agent/src/messaging/activity.rs +1 -0
- package/crates/team-agent/src/messaging/address.rs +1 -0
- package/crates/team-agent/src/messaging/delivery.rs +109 -41
- package/crates/team-agent/src/messaging/helpers.rs +2 -1
- package/crates/team-agent/src/messaging/leader_channel.rs +34 -10
- package/crates/team-agent/src/messaging/leader_receiver.rs +42 -24
- package/crates/team-agent/src/messaging/mod.rs +10 -14
- package/crates/team-agent/src/messaging/peers.rs +2 -0
- package/crates/team-agent/src/messaging/persist.rs +3 -1
- package/crates/team-agent/src/messaging/presentation.rs +111 -1
- package/crates/team-agent/src/messaging/results.rs +166 -17
- package/crates/team-agent/src/messaging/scheduler.rs +1 -0
- package/crates/team-agent/src/messaging/selftest.rs +1 -1
- package/crates/team-agent/src/messaging/send.rs +135 -89
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
- package/crates/team-agent/src/messaging/types.rs +7 -2
- package/crates/team-agent/src/messaging/watchers.rs +24 -13
- package/crates/team-agent/src/model/enums.rs +1 -0
- package/crates/team-agent/src/model/errors.rs +1 -0
- package/crates/team-agent/src/model/ids.rs +1 -0
- package/crates/team-agent/src/model/mod.rs +2 -0
- package/crates/team-agent/src/model/name_similarity.rs +1 -0
- package/crates/team-agent/src/model/pane_authority_refusal.rs +359 -0
- package/crates/team-agent/src/model/paths.rs +1 -0
- package/crates/team-agent/src/model/permissions.rs +3 -2
- package/crates/team-agent/src/model/routing.rs +1 -0
- package/crates/team-agent/src/model/spec.rs +17 -0
- package/crates/team-agent/src/model/task_graph.rs +1 -0
- package/crates/team-agent/src/model/yaml/tests.rs +1 -0
- package/crates/team-agent/src/model/yaml.rs +1 -0
- package/crates/team-agent/src/packaging/install.rs +9 -147
- package/crates/team-agent/src/packaging/migrate.rs +2 -0
- package/crates/team-agent/src/packaging/mod.rs +1 -0
- package/crates/team-agent/src/packaging/repair.rs +2 -0
- package/crates/team-agent/src/packaging/tests.rs +29 -218
- package/crates/team-agent/src/packaging/types.rs +8 -15
- package/crates/team-agent/src/platform/argv.rs +4 -0
- package/crates/team-agent/src/platform/errors.rs +10 -1
- package/crates/team-agent/src/platform/file_lock.rs +4 -157
- package/crates/team-agent/src/platform/mod.rs +0 -61
- package/crates/team-agent/src/platform/process.rs +1 -0
- package/crates/team-agent/src/provider/adapter.rs +1 -0
- package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
- package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
- package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
- package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
- package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
- package/crates/team-agent/src/provider/classify.rs +1 -0
- package/crates/team-agent/src/provider/faults.rs +1 -26
- package/crates/team-agent/src/provider/helpers.rs +1 -0
- package/crates/team-agent/src/provider/mod.rs +1 -1
- package/crates/team-agent/src/provider/session/capture.rs +41 -22
- package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
- package/crates/team-agent/src/provider/session/mod.rs +2 -4
- package/crates/team-agent/src/provider/session/resume.rs +2 -209
- package/crates/team-agent/src/provider/session_scan/claude.rs +93 -3
- package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +24 -7
- package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
- package/crates/team-agent/src/provider/session_scan.rs +6 -42
- package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
- package/crates/team-agent/src/provider/types.rs +1 -0
- package/crates/team-agent/src/provider/wire.rs +1 -0
- package/crates/team-agent/src/state/identity.rs +16 -5
- package/crates/team-agent/src/state/identity_keys.rs +1 -0
- package/crates/team-agent/src/state/mod.rs +2 -0
- package/crates/team-agent/src/state/owner_gate.rs +5 -0
- package/crates/team-agent/src/state/ownership.rs +10 -4
- package/crates/team-agent/src/state/paths.rs +12 -0
- package/crates/team-agent/src/state/persist.rs +9 -3
- package/crates/team-agent/src/state/projection.rs +15 -2
- package/crates/team-agent/src/state/repository/intent.rs +1 -0
- package/crates/team-agent/src/state/repository/tests.rs +1 -0
- package/crates/team-agent/src/state/repository.rs +7 -0
- package/crates/team-agent/src/state/selector.rs +1 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
- package/crates/team-agent/src/tmux_backend.rs +63 -0
- package/crates/team-agent/src/topology.rs +3 -0
- package/crates/team-agent/src/transport.rs +15 -0
- package/package.json +4 -4
- package/skills/team-agent/command-coverage.json +379 -0
- package/crates/team-agent/src/leader/inject.rs +0 -33
- package/crates/team-agent/src/provider/command.rs +0 -80
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
//!
|
|
1
2
|
//! step 5 · state — state.json 持久化 / identity / owner-gate / projection(真相源 `state.py`)。
|
|
2
3
|
//!
|
|
3
4
|
//! §6 step 5:原子写/锁/self-heal(**bug-084** 血泪)/ team projection / leader receiver identity /
|
|
@@ -32,6 +33,7 @@ pub mod selector;
|
|
|
32
33
|
use serde_json::Value;
|
|
33
34
|
use thiserror::Error;
|
|
34
35
|
|
|
36
|
+
///
|
|
35
37
|
/// Python 真值语义(`null`/`false`/`0`/`""`/`[]`/`{}` → false),`state` 模块共用。
|
|
36
38
|
pub(crate) fn json_truthy(v: &Value) -> bool {
|
|
37
39
|
match v {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
//!
|
|
1
2
|
//! owner-gate:trust own-vs-foreign(§11 / bug-064 / bug-082 血泪;真相源 `state.py`)。
|
|
2
3
|
//!
|
|
3
4
|
//! 三核心纯判定(集成 send_message/MCP 是 step 11/14):
|
|
@@ -37,6 +38,7 @@ pub struct CallerIdentity {
|
|
|
37
38
|
pub leader_session_uuid_source: String,
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
///
|
|
40
42
|
/// `check_team_owner`(`state.py:366`)的纯判定版:caller 身份 + 是否有 TEAM_AGENT_ID + liveness 注入。
|
|
41
43
|
/// 返回 `None`=允许(own / 无 owner / 死 owner pane 可接管);`Some(dict)`=拒绝(team_owner_mismatch)。
|
|
42
44
|
///
|
|
@@ -123,6 +125,7 @@ fn caller_to_value(c: &CallerIdentity) -> Value {
|
|
|
123
125
|
})
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
///
|
|
126
129
|
/// `worker_sender_bypasses_owner_gate`(`state.py:400`):worker peer-send 绕过 owner 门。
|
|
127
130
|
/// 返回 `Some(agent_id)`=绕过;`None`=不绕过(走 owner 门)。
|
|
128
131
|
pub fn worker_sender_bypasses_owner_gate(
|
|
@@ -160,6 +163,7 @@ pub fn worker_sender_bypasses_owner_gate(
|
|
|
160
163
|
Some(sender.to_string())
|
|
161
164
|
}
|
|
162
165
|
|
|
166
|
+
///
|
|
163
167
|
/// §11 realpath own-vs-foreign(`state.py:449`):**两边 canonicalize** 后全等才判 own。
|
|
164
168
|
/// macOS `/tmp → /private/tmp` 软链不对称由「两边 canonicalize」消除。canonicalize 失败(路径不存在)
|
|
165
169
|
/// → 退化为词法绝对比较(此时无软链可解,等价)。**禁 basename/startswith/子串/反推**。
|
|
@@ -187,6 +191,7 @@ fn lexical_abs(p: &Path) -> std::path::PathBuf {
|
|
|
187
191
|
}
|
|
188
192
|
}
|
|
189
193
|
|
|
194
|
+
///
|
|
190
195
|
/// `os.path.realpath` 等价(对抗 P0-B):全路径存在 → `canonicalize`;否则对**最长存在前缀**
|
|
191
196
|
/// canonicalize(解析其软链)再拼回缺失末段(规范化 `..`/`.`)。这复刻 Python「解析存在部分的
|
|
192
197
|
/// 软链 + 保留不存在末段」,修掉「父目录是软链 + 末段不存在 → 漏判 own」的 §11 血泪。
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
//!
|
|
1
2
|
//! Stage 2 of identity-boundary unified plan (architect direction 2026-06-23):
|
|
2
3
|
//! `state::ownership` — single read entry point for `team_owner` lookups.
|
|
3
4
|
//!
|
|
@@ -41,7 +42,7 @@ use crate::state::projection::{read_owner as projection_read_owner, team_state_k
|
|
|
41
42
|
/// owner value. CLI status / diagnose use it to surface legacy duplicate
|
|
42
43
|
/// warnings.
|
|
43
44
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
44
|
-
|
|
45
|
+
enum OwnershipSource {
|
|
45
46
|
/// Stage 5 canonical path: `.team/runtime/<team_key>/state.json` (not
|
|
46
47
|
/// yet emitted — placeholder for forward compatibility).
|
|
47
48
|
CanonicalPerTeamState,
|
|
@@ -57,11 +58,12 @@ pub enum OwnershipSource {
|
|
|
57
58
|
/// caller consumes (same shape as `state.get("team_owner")` returned today);
|
|
58
59
|
/// the `source` is diagnostic.
|
|
59
60
|
#[derive(Debug, Clone)]
|
|
60
|
-
|
|
61
|
+
struct OwnershipRead<'a> {
|
|
61
62
|
pub value: &'a Value,
|
|
62
63
|
pub source: OwnershipSource,
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
///
|
|
65
67
|
/// Stage 2 entry point: read the team owner for the given `team_key` from an
|
|
66
68
|
/// in-memory state value, honouring the legacy precedence. Returns `None` if
|
|
67
69
|
/// no source carries a non-empty, valid owner record.
|
|
@@ -73,7 +75,7 @@ pub struct OwnershipRead<'a> {
|
|
|
73
75
|
/// This deliberately reuses `projection::read_owner` so the pane-id validity
|
|
74
76
|
/// check (`%N` or all-digits) stays in one place. The repository's value-add
|
|
75
77
|
/// is the precedence ordering and the `OwnershipSource` tag.
|
|
76
|
-
|
|
78
|
+
fn read_owner_for_team<'a>(state: &'a Value, team_key: &str) -> Option<OwnershipRead<'a>> {
|
|
77
79
|
// Step 1 (Stage 5): canonical per-team state. Stage 2 does not consult
|
|
78
80
|
// a separate file yet — owner truth still lives in the in-memory state.
|
|
79
81
|
// Once Stage 5 wires `.team/runtime/<team_key>/state.json` as canonical,
|
|
@@ -109,6 +111,7 @@ pub fn read_owner_for_team<'a>(state: &'a Value, team_key: &str) -> Option<Owner
|
|
|
109
111
|
None
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
///
|
|
112
115
|
/// Convenience: just the JSON value, no diagnostic source. Returns `None`
|
|
113
116
|
/// when no owner is found. Stable across Stage 5 — only the lookup
|
|
114
117
|
/// implementation will move.
|
|
@@ -116,6 +119,7 @@ pub fn read_owner_value<'a>(state: &'a Value, team_key: &str) -> Option<&'a Valu
|
|
|
116
119
|
read_owner_for_team(state, team_key).map(|read| read.value)
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
///
|
|
119
123
|
/// Stage 3 (identity-boundary unified plan, architect direction 2026-06-23):
|
|
120
124
|
/// single write entry point for owner mutations. Pre-Stage-3, owner writes
|
|
121
125
|
/// were spread across 13 sites (claim/attach/readopt/managed-leader/quick-
|
|
@@ -187,6 +191,7 @@ pub fn write_owner(state: &mut Value, team_key: &str, record: OwnershipWrite) {
|
|
|
187
191
|
}
|
|
188
192
|
}
|
|
189
193
|
|
|
194
|
+
///
|
|
190
195
|
/// Stage 3 top-level cleanup (architect direction 2026-06-24,
|
|
191
196
|
/// .team/artifacts/stage3-toplevel-cleanup-fix.md): strip the legacy
|
|
192
197
|
/// top-level `team_owner / leader_receiver / owner_epoch` fields from a
|
|
@@ -202,12 +207,13 @@ pub fn write_owner(state: &mut Value, team_key: &str, record: OwnershipWrite) {
|
|
|
202
207
|
/// load-time path — legacy single-team states may still have only
|
|
203
208
|
/// top-level owner and no canonical `teams.<key>` owner, and a blind load
|
|
204
209
|
/// strip would lose ownership before migration.
|
|
205
|
-
|
|
210
|
+
fn strip_top_level_ownership(root: &mut serde_json::Map<String, Value>) {
|
|
206
211
|
for key in ["team_owner", "leader_receiver", "owner_epoch"] {
|
|
207
212
|
root.remove(key);
|
|
208
213
|
}
|
|
209
214
|
}
|
|
210
215
|
|
|
216
|
+
///
|
|
211
217
|
/// Stage 3 save-output canonical-aware strip (architect direction
|
|
212
218
|
/// 2026-06-24, .team/artifacts/stage3-save-strip-fix.md): conditional
|
|
213
219
|
/// version of `strip_top_level_ownership` used by `state::persist`'s
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
//!
|
|
1
2
|
//! Stage 0 of the identity-boundary unified plan (architect direction
|
|
2
3
|
//! 2026-06-23): `TeamScope` + `TeamRuntimePaths` foundation.
|
|
3
4
|
//!
|
|
@@ -42,6 +43,7 @@ pub struct TeamScope {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
impl TeamScope {
|
|
46
|
+
///
|
|
45
47
|
/// Construct a TeamScope from an already-resolved workspace + team_key.
|
|
46
48
|
/// Callers that have a display name must resolve it through the existing
|
|
47
49
|
/// `state::selector` machinery first; this constructor refuses an empty
|
|
@@ -66,6 +68,7 @@ impl TeamScope {
|
|
|
66
68
|
&self.team_key
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
///
|
|
69
72
|
/// Convenience: derive the path helper from this scope.
|
|
70
73
|
pub fn paths(&self) -> TeamRuntimePaths {
|
|
71
74
|
TeamRuntimePaths::for_scope(self)
|
|
@@ -110,6 +113,7 @@ impl TeamRuntimePaths {
|
|
|
110
113
|
&self.team_key
|
|
111
114
|
}
|
|
112
115
|
|
|
116
|
+
///
|
|
113
117
|
/// `.team/runtime/teams/<team_key>/` — the team's future B3 runtime
|
|
114
118
|
/// directory. It is not used as product authority until the B3 migration;
|
|
115
119
|
/// current callers must keep using `runtime_state_path`.
|
|
@@ -119,6 +123,7 @@ impl TeamRuntimePaths {
|
|
|
119
123
|
.join(&self.team_key)
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
///
|
|
122
127
|
/// Transitional runtime spec path. Mirrors the existing
|
|
123
128
|
/// `runtime_spec_path` helper while specs remain at
|
|
124
129
|
/// `.team/runtime/<team_key>/team.spec.yaml`.
|
|
@@ -126,6 +131,7 @@ impl TeamRuntimePaths {
|
|
|
126
131
|
runtime_spec_path(&self.workspace, &self.team_key)
|
|
127
132
|
}
|
|
128
133
|
|
|
134
|
+
///
|
|
129
135
|
/// `.team/runtime/teams/<team_key>/state.json` — the canonical per-team
|
|
130
136
|
/// state path that B3 will start writing. Not used by current product
|
|
131
137
|
/// code; callers must continue using `runtime_state_path` until B3
|
|
@@ -134,11 +140,13 @@ impl TeamRuntimePaths {
|
|
|
134
140
|
self.team_dir().join("state.json")
|
|
135
141
|
}
|
|
136
142
|
|
|
143
|
+
///
|
|
137
144
|
/// `.team/runtime/teams/<team_key>/coordinator.pid` — future sidecar location.
|
|
138
145
|
pub fn coordinator_pid_path(&self) -> PathBuf {
|
|
139
146
|
self.team_dir().join("coordinator.pid")
|
|
140
147
|
}
|
|
141
148
|
|
|
149
|
+
///
|
|
142
150
|
/// `.team/runtime/teams/<team_key>/coordinator.log` — future sidecar location.
|
|
143
151
|
pub fn coordinator_log_path(&self) -> PathBuf {
|
|
144
152
|
self.team_dir().join("coordinator.log")
|
|
@@ -180,6 +188,7 @@ pub enum CommandScope {
|
|
|
180
188
|
}
|
|
181
189
|
|
|
182
190
|
impl CommandScope {
|
|
191
|
+
///
|
|
183
192
|
/// Resolve the CLI's `--team` argument against the workspace state.
|
|
184
193
|
/// On any I/O error the empty case is returned — destructive commands
|
|
185
194
|
/// will run their own selector and surface the real error.
|
|
@@ -202,6 +211,7 @@ impl CommandScope {
|
|
|
202
211
|
}
|
|
203
212
|
}
|
|
204
213
|
|
|
214
|
+
///
|
|
205
215
|
/// Convert to an `Option<String>` for backward compatibility with
|
|
206
216
|
/// existing `args.team.as_deref()` call sites.
|
|
207
217
|
pub fn team_key(&self) -> Option<&str> {
|
|
@@ -211,11 +221,13 @@ impl CommandScope {
|
|
|
211
221
|
}
|
|
212
222
|
}
|
|
213
223
|
|
|
224
|
+
///
|
|
214
225
|
/// True iff there are 2+ alive teams and no explicit `--team`.
|
|
215
226
|
pub fn is_ambiguous(&self) -> bool {
|
|
216
227
|
matches!(self, Self::Ambiguous(_))
|
|
217
228
|
}
|
|
218
229
|
|
|
230
|
+
///
|
|
219
231
|
/// The candidate list when ambiguous; empty otherwise.
|
|
220
232
|
pub fn candidates(&self) -> &[String] {
|
|
221
233
|
match self {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
//!
|
|
1
2
|
//! state.json 持久化(bug-084 韧性;真相源 `state.py:save_runtime_state` + `_self_heal_runtime_state`
|
|
2
3
|
//! + `runtime.py:_runtime_lock`)。
|
|
3
4
|
//!
|
|
@@ -71,6 +72,7 @@ static RUNTIME_STATE_CACHE: LazyLock<Mutex<HashMap<PathBuf, Value>>> =
|
|
|
71
72
|
LazyLock::new(|| Mutex::new(HashMap::new()));
|
|
72
73
|
static TMP_SEQ: AtomicU64 = AtomicU64::new(0);
|
|
73
74
|
|
|
75
|
+
///
|
|
74
76
|
/// `state.py:41`。
|
|
75
77
|
pub fn runtime_state_path(workspace: &Path) -> PathBuf {
|
|
76
78
|
runtime_dir(workspace).join("state.json")
|
|
@@ -202,6 +204,7 @@ impl Drop for RuntimeLock {
|
|
|
202
204
|
}
|
|
203
205
|
}
|
|
204
206
|
|
|
207
|
+
///
|
|
205
208
|
/// `save_runtime_state`(bug-084)。`state` 是 state.json 的内存 Value(插入序保留)。
|
|
206
209
|
/// 注:Python 在此还调 `_migrate_state_identity`(identity slice 落地后接入;本 slice 不改 state 内容)。
|
|
207
210
|
pub fn save_runtime_state(workspace: &Path, state: &Value) -> Result<(), StateError> {
|
|
@@ -1135,10 +1138,11 @@ fn self_heal(
|
|
|
1135
1138
|
}
|
|
1136
1139
|
}
|
|
1137
1140
|
|
|
1141
|
+
///
|
|
1138
1142
|
/// `load_runtime_state`(本 slice 最小:读+parse+缓存;normalize/migration 待 identity slice)。
|
|
1139
1143
|
/// `normalize_agent_session_state`(`state.py:45`):为每个 agent dict 的 SESSION_STATE_FIELDS
|
|
1140
1144
|
/// setdefault None(缺则末尾插)。
|
|
1141
|
-
|
|
1145
|
+
fn normalize_agent_session_state(state: &mut Value) {
|
|
1142
1146
|
let Some(agents) = state.get_mut("agents").and_then(Value::as_object_mut) else {
|
|
1143
1147
|
return;
|
|
1144
1148
|
};
|
|
@@ -1151,12 +1155,13 @@ pub fn normalize_agent_session_state(state: &mut Value) {
|
|
|
1151
1155
|
}
|
|
1152
1156
|
}
|
|
1153
1157
|
|
|
1158
|
+
///
|
|
1154
1159
|
/// `_migrate_active_team_key`(`state.py:73`,0.2.6 Family B C6):legacy state 缺 active_team_key
|
|
1155
1160
|
/// 时 seed 一次。返回是否有改动。
|
|
1156
1161
|
///
|
|
1157
1162
|
/// 注:Python `seed if seed in teams or not teams else seed` 两支均为 `seed`(死三元),
|
|
1158
1163
|
/// 此处直接 `= seed`,与可观测行为一致。
|
|
1159
|
-
|
|
1164
|
+
fn migrate_active_team_key(state: &mut Value) -> bool {
|
|
1160
1165
|
if state
|
|
1161
1166
|
.as_object()
|
|
1162
1167
|
.is_some_and(|o| o.contains_key("active_team_key"))
|
|
@@ -1191,6 +1196,7 @@ pub fn migrate_active_team_key(state: &mut Value) -> bool {
|
|
|
1191
1196
|
true
|
|
1192
1197
|
}
|
|
1193
1198
|
|
|
1199
|
+
///
|
|
1194
1200
|
/// RM-039-STAT-001 second-round compat normalizer (architect verdict
|
|
1195
1201
|
/// 2026-06-22). When `active_team_key` names an existing `teams` entry
|
|
1196
1202
|
/// but root `team_key` is missing, set root `team_key = active_team_key`
|
|
@@ -1210,7 +1216,7 @@ pub fn migrate_active_team_key(state: &mut Value) -> bool {
|
|
|
1210
1216
|
/// * Does NOT touch any other field (no generic deep merge).
|
|
1211
1217
|
///
|
|
1212
1218
|
/// Returns `true` if state was mutated and the caller should persist.
|
|
1213
|
-
|
|
1219
|
+
fn migrate_team_key_to_match_active_team(state: &mut Value) -> bool {
|
|
1214
1220
|
let Some(obj) = state.as_object() else {
|
|
1215
1221
|
return false;
|
|
1216
1222
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
//!
|
|
1
2
|
//! projection:team 选择 / top-level 派生视图(真相源 `state.py`,0.2.6 Family B C6-C8)。
|
|
2
3
|
//!
|
|
3
4
|
//! `state.teams` 是唯一候选源(`status == "alive"`);顶层 `session_name`/`team_dir`/`agents`/`tasks`
|
|
@@ -24,6 +25,7 @@ use crate::state::persist::{
|
|
|
24
25
|
|
|
25
26
|
pub(crate) const CURRENT_TEAM_ALIAS: &str = "current";
|
|
26
27
|
|
|
28
|
+
///
|
|
27
29
|
/// `team_state_key`(`state.py:93`):从 team_dir(.name)/spec_path(.parent.name)派生 team key,
|
|
28
30
|
/// 跳过 `.team`/`runtime`;兜底 `session_name` 或 `"current"`。
|
|
29
31
|
pub fn team_state_key(state: &Value) -> String {
|
|
@@ -184,6 +186,7 @@ fn legacy_owner_team_aliases(entry: &Value) -> impl Iterator<Item = String> + '_
|
|
|
184
186
|
.map(str::to_string)
|
|
185
187
|
}
|
|
186
188
|
|
|
189
|
+
///
|
|
187
190
|
/// `compact_team_state`(`state.py:105`):剔除 `teams`(team entry 不嵌套全量 teams),保序。
|
|
188
191
|
pub fn compact_team_state(state: &Value) -> Value {
|
|
189
192
|
match state.as_object() {
|
|
@@ -208,6 +211,7 @@ pub fn state_is_managed_leader(state: &Value) -> bool {
|
|
|
208
211
|
!state_is_external_leader(state)
|
|
209
212
|
}
|
|
210
213
|
|
|
214
|
+
///
|
|
211
215
|
/// `merge_workspace_team_state`(`state.py:111`):把新启动的 team 并入既有 workspace state。
|
|
212
216
|
pub fn merge_workspace_team_state(existing: &Value, launched: &Value) -> Value {
|
|
213
217
|
let launched_key = team_state_key(launched);
|
|
@@ -239,6 +243,7 @@ pub fn merge_workspace_team_state(existing: &Value, launched: &Value) -> Value {
|
|
|
239
243
|
Value::Object(merged)
|
|
240
244
|
}
|
|
241
245
|
|
|
246
|
+
///
|
|
242
247
|
/// `team_state_candidates`(`state.py:131`):唯一候选源 = `state.teams` 中
|
|
243
248
|
/// `status=="alive"`(大小写不敏感;缺 status/空 视为 alive,但 0.5.26 起
|
|
244
249
|
/// legacy shutdown 残留「无 status + 全体 agents 处于终态」不再算 alive)。
|
|
@@ -263,6 +268,7 @@ pub fn team_state_candidates(state: &Value) -> Map<String, Value> {
|
|
|
263
268
|
out
|
|
264
269
|
}
|
|
265
270
|
|
|
271
|
+
///
|
|
266
272
|
/// 0.5.26 (`.team/artifacts/stale-team-saveconflict-locate.md` §7.1): 唯一 alive
|
|
267
273
|
/// 谓词。规则:
|
|
268
274
|
/// - 非 object → 排除;
|
|
@@ -348,8 +354,9 @@ pub(crate) fn agent_status_is_terminal(status: &str) -> bool {
|
|
|
348
354
|
)
|
|
349
355
|
}
|
|
350
356
|
|
|
357
|
+
///
|
|
351
358
|
/// `format_team_candidates`(`state.py:148`):候选摘要串(key 排序;agents 排序逗号连,空→`-`)。
|
|
352
|
-
|
|
359
|
+
fn format_team_candidates(team_states: &Map<String, Value>) -> String {
|
|
353
360
|
if team_states.is_empty() {
|
|
354
361
|
return "No team state was found.".to_string();
|
|
355
362
|
}
|
|
@@ -412,6 +419,7 @@ fn valid_owner_pane_id(pane_id: &str) -> bool {
|
|
|
412
419
|
pane_id.starts_with('%') || pane_id.chars().all(|ch| ch.is_ascii_digit())
|
|
413
420
|
}
|
|
414
421
|
|
|
422
|
+
///
|
|
415
423
|
/// `_project_top_level_view`(`state.py:167`,C8):把 `teams[team_key]` 投影成扁平顶层视图。
|
|
416
424
|
pub fn project_top_level_view(state: &Value, team_key: &str) -> Value {
|
|
417
425
|
// entry = _team_entry_from_state(...) or {} —— 空 dict 亦走 {} 分支(同内容)。
|
|
@@ -488,6 +496,7 @@ pub struct TeamScopeResolution {
|
|
|
488
496
|
pub state: Value,
|
|
489
497
|
}
|
|
490
498
|
|
|
499
|
+
///
|
|
491
500
|
/// Resolve a requested team selector once, returning both the canonical map key and its
|
|
492
501
|
/// projected state. Callers must carry `canonical_team_key` forward rather than deriving team
|
|
493
502
|
/// identity again from the projection or the original alias.
|
|
@@ -646,6 +655,7 @@ pub fn resolve_runtime_team_scope(
|
|
|
646
655
|
))
|
|
647
656
|
}
|
|
648
657
|
|
|
658
|
+
///
|
|
649
659
|
/// `select_runtime_state`(`state.py:193`):compatibility projection wrapper.
|
|
650
660
|
pub fn select_runtime_state(workspace: &Path, team: Option<&str>) -> Result<Value, StateError> {
|
|
651
661
|
resolve_runtime_team_scope(workspace, team).map(|resolved| resolved.state)
|
|
@@ -677,8 +687,9 @@ fn team_selector_matches(team: &str, key: &str, value: &Value) -> bool {
|
|
|
677
687
|
.is_some_and(|name| team == name)
|
|
678
688
|
}
|
|
679
689
|
|
|
690
|
+
///
|
|
680
691
|
/// `ambiguous_team_target_result`(`state.py:226`):无显式 team 且多候选 → 拒绝 dict;否则 None。
|
|
681
|
-
|
|
692
|
+
fn ambiguous_team_target_result(state: &Value) -> Option<Value> {
|
|
682
693
|
let alive = team_state_candidates(state);
|
|
683
694
|
let active = state
|
|
684
695
|
.get("active_team_key")
|
|
@@ -704,6 +715,7 @@ pub fn ambiguous_team_target_result(state: &Value) -> Option<Value> {
|
|
|
704
715
|
}))
|
|
705
716
|
}
|
|
706
717
|
|
|
718
|
+
///
|
|
707
719
|
/// `resolve_team_scoped_state`(`state.py:243`):返回 `(state, refusal)`,二者恰一为 Some。
|
|
708
720
|
/// `team=None` 且歧义 → refusal;否则 select,RuntimeError → `team_target_unresolved` refusal。
|
|
709
721
|
pub fn resolve_team_scoped_state(
|
|
@@ -732,6 +744,7 @@ pub fn resolve_team_scoped_state(
|
|
|
732
744
|
}
|
|
733
745
|
}
|
|
734
746
|
|
|
747
|
+
///
|
|
735
748
|
/// `save_team_scoped_state`(`state.py:594`):把 team-scoped(投影后)state 写回 workspace,**保全**
|
|
736
749
|
/// 多 team workspace 里其他 team 的持久态。单 team(磁盘无 `teams` 且 primary key == target)退化为
|
|
737
750
|
/// 纯 `save_runtime_state`(字节等价);多 team 时把本 team 落到 `teams[target_key]=compact(...)`,顶层
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
//!
|
|
1
2
|
//! StateRepository facade.
|
|
2
3
|
//!
|
|
3
4
|
//! Ref:
|
|
@@ -73,16 +74,19 @@ pub struct StateRepository<'a> {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
impl<'a> StateRepository<'a> {
|
|
77
|
+
///
|
|
76
78
|
/// Construct a repository bound to a workspace root.
|
|
77
79
|
pub fn new(workspace: &'a Path) -> Self {
|
|
78
80
|
Self { workspace }
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
///
|
|
81
84
|
/// Load the raw workspace state document.
|
|
82
85
|
pub fn load_workspace(&self) -> Result<Value, StateError> {
|
|
83
86
|
helper_load_workspace(self.workspace)
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
///
|
|
86
90
|
/// Load the canonical workspace document without running read-time
|
|
87
91
|
/// migrations. `None` preserves the legacy raw-reader distinction between
|
|
88
92
|
/// a missing file and a present empty/default document.
|
|
@@ -96,6 +100,7 @@ impl<'a> StateRepository<'a> {
|
|
|
96
100
|
serde_json::from_str(&text).map(Some).map_err(StateError::from)
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
///
|
|
99
104
|
/// Resolve a team-scoped projection using the existing projection selector.
|
|
100
105
|
/// `team_key = None` selects the ambient team the same way as pre-S1a.
|
|
101
106
|
pub fn load_team(&self, team_key: Option<&str>) -> Result<Value, StateError> {
|
|
@@ -110,6 +115,7 @@ impl<'a> StateRepository<'a> {
|
|
|
110
115
|
})
|
|
111
116
|
}
|
|
112
117
|
|
|
118
|
+
///
|
|
113
119
|
/// Persist `state` under the supplied intent. S1a dispatch is a pure
|
|
114
120
|
/// forward call to the same legacy helper family that the caller used
|
|
115
121
|
/// before S1a; the intent selects the family, not the merge semantics.
|
|
@@ -117,6 +123,7 @@ impl<'a> StateRepository<'a> {
|
|
|
117
123
|
route_direct(self.workspace, intent, state)
|
|
118
124
|
}
|
|
119
125
|
|
|
126
|
+
///
|
|
120
127
|
/// Persist `state` and let `reapply` re-materialize the write on
|
|
121
128
|
/// SaveConflict, matching the pre-S1a `_reapplying_after_conflict` helper
|
|
122
129
|
/// behavior for reapply-shaped intents.
|
|
@@ -146,6 +146,45 @@ fn backend_with_stdin(
|
|
|
146
146
|
)
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
#[test]
|
|
150
|
+
fn a21_spawn_command_too_long_is_structured_before_transport() {
|
|
151
|
+
let (backend, recorded) = backend_with(MockResp::Out(ok("%0\n")), Vec::new());
|
|
152
|
+
let session = SessionName::new("a21-session");
|
|
153
|
+
let window = WindowName::new("worker-long-role");
|
|
154
|
+
let argv = vec!["codex".to_string(), "x".repeat(20_000)];
|
|
155
|
+
|
|
156
|
+
let error = backend
|
|
157
|
+
.spawn_first(
|
|
158
|
+
&session,
|
|
159
|
+
&window,
|
|
160
|
+
&argv,
|
|
161
|
+
Path::new("/tmp/a21-workspace"),
|
|
162
|
+
&BTreeMap::new(),
|
|
163
|
+
)
|
|
164
|
+
.expect_err("oversized spawn must fail before tmux transport");
|
|
165
|
+
let text = error.to_string();
|
|
166
|
+
assert!(
|
|
167
|
+
text.contains("command_too_long"),
|
|
168
|
+
"structured error: {text}"
|
|
169
|
+
);
|
|
170
|
+
assert!(
|
|
171
|
+
text.contains("worker-long-role"),
|
|
172
|
+
"seat must be named: {text}"
|
|
173
|
+
);
|
|
174
|
+
assert!(
|
|
175
|
+
text.contains("actual_bytes="),
|
|
176
|
+
"actual length must be visible: {text}"
|
|
177
|
+
);
|
|
178
|
+
assert!(
|
|
179
|
+
text.contains("limit_bytes="),
|
|
180
|
+
"limit must be visible: {text}"
|
|
181
|
+
);
|
|
182
|
+
assert!(
|
|
183
|
+
recorded.lock().unwrap().is_empty(),
|
|
184
|
+
"tmux must not be called: {text}"
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
149
188
|
fn svec(items: &[&str]) -> Vec<String> {
|
|
150
189
|
items.iter().map(|s| (*s).to_string()).collect()
|
|
151
190
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
//!
|
|
1
2
|
//! Concrete tmux `Transport` backend (SKELETON) — the real executor that runs `tmux <argv>`.
|
|
2
3
|
//!
|
|
3
4
|
//! step 9 shipped the [`crate::transport::Transport`] trait + the pure tmux argv-builders
|
|
@@ -77,6 +78,11 @@ const COMMAND_TIMEOUT: Duration = Duration::from_secs(5);
|
|
|
77
78
|
const SPAWN_IDENTITY_TIMEOUT: Duration = Duration::from_millis(500);
|
|
78
79
|
const SPAWN_IDENTITY_POLL_INTERVAL: Duration = Duration::from_millis(25);
|
|
79
80
|
|
|
81
|
+
/// Keep the final tmux argv below the ~16 KiB command envelope observed on macOS.
|
|
82
|
+
/// This is checked after shell quoting and socket arguments are applied, before
|
|
83
|
+
/// the external tmux process is called.
|
|
84
|
+
pub const TMUX_SPAWN_COMMAND_LIMIT_BYTES: usize = 16_000;
|
|
85
|
+
|
|
80
86
|
impl CommandRunner for RealCommandRunner {
|
|
81
87
|
fn run(&self, argv: &[String]) -> Result<CommandOutput, std::io::Error> {
|
|
82
88
|
self.run_inner(argv, None)
|
|
@@ -390,6 +396,27 @@ impl TmuxBackend {
|
|
|
390
396
|
if self.socket.is_none() {
|
|
391
397
|
return;
|
|
392
398
|
}
|
|
399
|
+
// A server-level kill is only safe after the caller has removed every
|
|
400
|
+
// resource it owns. A non-empty server may carry a pre-existing team
|
|
401
|
+
// (or a leader owned by another launcher), so fail closed instead of
|
|
402
|
+
// turning an endpoint cleanup into a shared-server teardown. An
|
|
403
|
+
// inconclusive probe is also not proof of ownership.
|
|
404
|
+
let server_is_empty = match self.list_targets() {
|
|
405
|
+
Ok(targets) => targets.is_empty(),
|
|
406
|
+
Err(_) => false,
|
|
407
|
+
};
|
|
408
|
+
if !server_is_empty {
|
|
409
|
+
if let Some(workspace) = &self.event_workspace {
|
|
410
|
+
let _ = crate::event_log::EventLog::new(workspace).write(
|
|
411
|
+
"tmux.kill_server_skipped_nonempty_or_unknown",
|
|
412
|
+
serde_json::json!({
|
|
413
|
+
"reason": "server_ownership_not_proven",
|
|
414
|
+
"endpoint": self.tmux_endpoint(),
|
|
415
|
+
}),
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
393
420
|
let argv = self.tmux_argv(&["tmux".to_string(), "kill-server".to_string()]);
|
|
394
421
|
let _ = self.runner.run(&argv);
|
|
395
422
|
}
|
|
@@ -787,6 +814,16 @@ impl TmuxBackend {
|
|
|
787
814
|
first: bool,
|
|
788
815
|
) -> Result<SpawnResult, TransportError> {
|
|
789
816
|
let spawn_argv = tmux_spawn_argv(session, window, command, first);
|
|
817
|
+
self.validate_spawn_command(
|
|
818
|
+
&spawn_argv,
|
|
819
|
+
if first {
|
|
820
|
+
"tmux.new-session"
|
|
821
|
+
} else {
|
|
822
|
+
"tmux.new-window"
|
|
823
|
+
},
|
|
824
|
+
session,
|
|
825
|
+
window,
|
|
826
|
+
)?;
|
|
790
827
|
let output = self.run_spawn(&spawn_argv)?;
|
|
791
828
|
let pane = output.stdout.trim();
|
|
792
829
|
if pane.is_empty() {
|
|
@@ -897,6 +934,7 @@ impl TmuxBackend {
|
|
|
897
934
|
"-lc".to_string(),
|
|
898
935
|
command,
|
|
899
936
|
];
|
|
937
|
+
self.validate_spawn_command(&split_argv, "tmux.split-window", session, window)?;
|
|
900
938
|
let output = self.run_spawn(&split_argv)?;
|
|
901
939
|
let pane = output.stdout.trim();
|
|
902
940
|
if pane.is_empty() {
|
|
@@ -932,6 +970,31 @@ impl TmuxBackend {
|
|
|
932
970
|
}
|
|
933
971
|
}
|
|
934
972
|
|
|
973
|
+
fn validate_spawn_command(
|
|
974
|
+
&self,
|
|
975
|
+
argv: &[String],
|
|
976
|
+
segment: &str,
|
|
977
|
+
session: &SessionName,
|
|
978
|
+
window: &WindowName,
|
|
979
|
+
) -> Result<(), TransportError> {
|
|
980
|
+
let final_argv = self.tmux_argv(argv);
|
|
981
|
+
let actual_bytes = final_argv
|
|
982
|
+
.iter()
|
|
983
|
+
.map(|arg| arg.len().saturating_add(1))
|
|
984
|
+
.sum::<usize>();
|
|
985
|
+
if actual_bytes > TMUX_SPAWN_COMMAND_LIMIT_BYTES {
|
|
986
|
+
return Err(TransportError::CommandTooLong {
|
|
987
|
+
backend: BackendKind::Tmux,
|
|
988
|
+
segment: segment.to_string(),
|
|
989
|
+
session: session.as_str().to_string(),
|
|
990
|
+
window: window.as_str().to_string(),
|
|
991
|
+
actual_bytes,
|
|
992
|
+
limit_bytes: TMUX_SPAWN_COMMAND_LIMIT_BYTES,
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
Ok(())
|
|
996
|
+
}
|
|
997
|
+
|
|
935
998
|
fn run_spawn(&self, argv: &[String]) -> Result<CommandOutput, TransportError> {
|
|
936
999
|
let argv = self.tmux_argv(argv);
|
|
937
1000
|
let output = self
|
|
@@ -399,6 +399,9 @@ fn append_worker_pane_binding_issues(
|
|
|
399
399
|
let Some(agents) = state.get("agents").and_then(Value::as_object) else {
|
|
400
400
|
return;
|
|
401
401
|
};
|
|
402
|
+
if agents.is_empty() {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
402
405
|
let Ok(live_targets) = backend.list_targets() else {
|
|
403
406
|
return;
|
|
404
407
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
//!
|
|
1
2
|
//! step 9 · transport — 控制面 trait + typed payload(ROUND-0 SKELETON).
|
|
2
3
|
//!
|
|
3
4
|
//! 设计真相源:`docs/phase0/transport-backend-design.md` §1(Rust 草图:Target /
|
|
@@ -486,6 +487,20 @@ pub enum TransportError {
|
|
|
486
487
|
code: Option<i32>,
|
|
487
488
|
stderr: String,
|
|
488
489
|
},
|
|
490
|
+
/// The final tmux spawn argv is too large for the transport command envelope.
|
|
491
|
+
/// Keep this separate from `Subprocess` so callers do not have to parse a
|
|
492
|
+
/// provider-sized argv dump to learn the actionable limit.
|
|
493
|
+
#[error(
|
|
494
|
+
"command_too_long: backend={backend:?}; segment={segment}; session={session}; window={window}; actual_bytes={actual_bytes}; limit_bytes={limit_bytes}"
|
|
495
|
+
)]
|
|
496
|
+
CommandTooLong {
|
|
497
|
+
backend: BackendKind,
|
|
498
|
+
segment: String,
|
|
499
|
+
session: String,
|
|
500
|
+
window: String,
|
|
501
|
+
actual_bytes: usize,
|
|
502
|
+
limit_bytes: usize,
|
|
503
|
+
},
|
|
489
504
|
/// wezterm mux 连不上 / tmux server 不在。
|
|
490
505
|
#[error("mux unavailable on {backend:?}: {detail}")]
|
|
491
506
|
MuxUnavailable {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.62",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.62",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.62",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.62"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|