@team-agent/installer 0.5.0 → 0.5.2
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 +118 -9
- package/Cargo.toml +2 -2
- package/crates/team-agent/Cargo.toml +1 -0
- package/crates/team-agent/src/app_server_test_support.rs +258 -0
- package/crates/team-agent/src/cli/adapters.rs +32 -3
- package/crates/team-agent/src/cli/attach_app_server_leader.rs +16 -0
- package/crates/team-agent/src/cli/diagnose.rs +14 -5
- package/crates/team-agent/src/cli/emit.rs +111 -3
- package/crates/team-agent/src/cli/mod.rs +72 -25
- package/crates/team-agent/src/cli/named_address.rs +975 -0
- package/crates/team-agent/src/cli/send.rs +200 -1
- package/crates/team-agent/src/cli/status_port.rs +44 -6
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +586 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +21 -0
- package/crates/team-agent/src/codex_app_server.rs +488 -0
- package/crates/team-agent/src/conpty/backend.rs +710 -0
- package/crates/team-agent/src/conpty/mod.rs +32 -0
- package/crates/team-agent/src/coordinator/backoff.rs +45 -6
- package/crates/team-agent/src/diagnose/orphans.rs +11 -3
- package/crates/team-agent/src/leader/lease.rs +144 -7
- package/crates/team-agent/src/leader/owner_bind.rs +5 -2
- package/crates/team-agent/src/leader/start.rs +18 -5
- package/crates/team-agent/src/leader/tests/lease_api.rs +172 -0
- package/crates/team-agent/src/lib.rs +23 -0
- package/crates/team-agent/src/lifecycle/launch.rs +127 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +70 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +90 -0
- package/crates/team-agent/src/messaging/delivery.rs +329 -9
- package/crates/team-agent/src/messaging/leader_receiver.rs +17 -138
- package/crates/team-agent/src/messaging/mod.rs +2 -2
- package/crates/team-agent/src/messaging/results.rs +78 -21
- package/crates/team-agent/src/messaging/tests/runtime.rs +232 -0
- package/crates/team-agent/src/tmux_backend.rs +31 -0
- package/crates/team-agent/src/transport_factory.rs +632 -0
- package/package.json +4 -4
|
@@ -2835,16 +2835,91 @@ pub fn quick_start_in_workspace_with_display(
|
|
|
2835
2835
|
yes: bool,
|
|
2836
2836
|
team_id: Option<&str>,
|
|
2837
2837
|
open_display: bool,
|
|
2838
|
+
) -> Result<QuickStartReport, LifecycleError> {
|
|
2839
|
+
// Legacy entrypoint: no `--backend` override. Preserves the exact
|
|
2840
|
+
// tmux behavior we shipped in Phase 1c (byte-equivalent tmux path
|
|
2841
|
+
// when no explicit backend is asked for).
|
|
2842
|
+
quick_start_in_workspace_with_display_and_backend(
|
|
2843
|
+
workspace,
|
|
2844
|
+
agents_dir,
|
|
2845
|
+
name,
|
|
2846
|
+
yes,
|
|
2847
|
+
team_id,
|
|
2848
|
+
open_display,
|
|
2849
|
+
None,
|
|
2850
|
+
)
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
/// 0.5.x Phase 1d Batch 2: quick-start with an optional
|
|
2854
|
+
/// `--backend <tmux|conpty>` override. When `backend` is `None` or
|
|
2855
|
+
/// `Some("tmux")` the transport is the same one the legacy entrypoint
|
|
2856
|
+
/// built (byte-equivalent to Phase 1c), so tmux users see no
|
|
2857
|
+
/// behavioral change.
|
|
2858
|
+
///
|
|
2859
|
+
/// When `backend` is `Some("conpty")`, this call routes through the
|
|
2860
|
+
/// factory. On a host without a live shim client (i.e. every
|
|
2861
|
+
/// non-Windows host today), the resulting `ConPtyBackend` degrades
|
|
2862
|
+
/// its spawn/inject/capture calls to `TransportError::MuxUnavailable`
|
|
2863
|
+
/// honestly — MUST-NOT-13 + CR C-1 ①. Users see a real "conpty
|
|
2864
|
+
/// unavailable" error rather than a silent tmux fallback.
|
|
2865
|
+
pub fn quick_start_in_workspace_with_display_and_backend(
|
|
2866
|
+
workspace: &Path,
|
|
2867
|
+
agents_dir: &Path,
|
|
2868
|
+
name: Option<&str>,
|
|
2869
|
+
yes: bool,
|
|
2870
|
+
team_id: Option<&str>,
|
|
2871
|
+
open_display: bool,
|
|
2872
|
+
backend: Option<&str>,
|
|
2838
2873
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
2839
2874
|
let workspace = explicit_quick_start_workspace(workspace);
|
|
2840
|
-
|
|
2875
|
+
// Default / `tmux` literal: preserve the existing tmux path
|
|
2876
|
+
// BYTE-FOR-BYTE. This is the CR §Batch 2 Verification anchor:
|
|
2877
|
+
// `quick-start` without `--backend` produces byte-equivalent tmux
|
|
2878
|
+
// behavior.
|
|
2879
|
+
let literal = backend.map(str::trim);
|
|
2880
|
+
let is_tmux_or_default = matches!(literal, None | Some("") | Some("tmux") | Some("TMUX"));
|
|
2881
|
+
if is_tmux_or_default {
|
|
2882
|
+
let transport = quick_start_tmux_backend(&workspace);
|
|
2883
|
+
return quick_start_with_transport_in_workspace_with_display(
|
|
2884
|
+
&workspace,
|
|
2885
|
+
agents_dir,
|
|
2886
|
+
name,
|
|
2887
|
+
yes,
|
|
2888
|
+
team_id,
|
|
2889
|
+
&transport,
|
|
2890
|
+
open_display,
|
|
2891
|
+
);
|
|
2892
|
+
}
|
|
2893
|
+
// Explicit non-tmux backend: route through the factory. Parse the
|
|
2894
|
+
// literal, then delegate. On unsupported literals or refused
|
|
2895
|
+
// preconditions we return `LifecycleError` — never silently pick
|
|
2896
|
+
// tmux (CR C-1 ①/②).
|
|
2897
|
+
let requested = crate::transport_factory::RequestedTransportBackend::parse_literal(
|
|
2898
|
+
literal.unwrap_or_default(),
|
|
2899
|
+
)
|
|
2900
|
+
.ok_or_else(|| {
|
|
2901
|
+
LifecycleError::TeamSelect(format!(
|
|
2902
|
+
"unsupported --backend literal {literal:?}; expected `tmux` or `conpty` \
|
|
2903
|
+
(Phase 1d does not auto-map `pty` to conpty — CR C-1 ②)"
|
|
2904
|
+
))
|
|
2905
|
+
})?;
|
|
2906
|
+
let team_key_for_factory = team_id;
|
|
2907
|
+
let input = crate::transport_factory::TransportFactoryInput::new(
|
|
2908
|
+
&workspace,
|
|
2909
|
+
crate::transport_factory::TransportPurpose::Launch,
|
|
2910
|
+
)
|
|
2911
|
+
.with_team_key(team_key_for_factory)
|
|
2912
|
+
.with_explicit_backend(Some(requested));
|
|
2913
|
+
let resolved = crate::transport_factory::resolve_transport(input)
|
|
2914
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
2915
|
+
// Hand the boxed backend down as `&dyn Transport`.
|
|
2841
2916
|
quick_start_with_transport_in_workspace_with_display(
|
|
2842
2917
|
&workspace,
|
|
2843
2918
|
agents_dir,
|
|
2844
2919
|
name,
|
|
2845
2920
|
yes,
|
|
2846
2921
|
team_id,
|
|
2847
|
-
|
|
2922
|
+
&*resolved.backend,
|
|
2848
2923
|
open_display,
|
|
2849
2924
|
)
|
|
2850
2925
|
}
|
|
@@ -3068,7 +3143,16 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
3068
3143
|
let resolved_spec_path =
|
|
3069
3144
|
std::fs::canonicalize(&spec_path).unwrap_or_else(|_| spec_path.clone());
|
|
3070
3145
|
let mut state = initial_runtime_state(&spec, &resolved_spec_path, &workspace, agents_dir);
|
|
3071
|
-
|
|
3146
|
+
// 0.5.x Phase 1d hot-path 接线(裁决1 msg_76e1d98202b8): use the
|
|
3147
|
+
// generic annotator that writes `state.transport = { kind, source }`
|
|
3148
|
+
// for every backend AND (for tmux) preserves the existing
|
|
3149
|
+
// `tmux_endpoint`/`tmux_socket`/`tmux_socket_source` fields via the
|
|
3150
|
+
// inner `annotate_runtime_tmux_endpoint` call. Source is threaded
|
|
3151
|
+
// from the caller-side factory selection when available; the
|
|
3152
|
+
// legacy quick-start entrypoint currently passes `None` (defaults
|
|
3153
|
+
// to "unknown" in the payload) — Phase 2 refactor will thread
|
|
3154
|
+
// ResolvedTransport.source through the launch signature.
|
|
3155
|
+
annotate_runtime_transport(&mut state, transport, &workspace, None);
|
|
3072
3156
|
save_launched_team_state_for_key(&workspace, &state, Some(&state_team_key))?;
|
|
3073
3157
|
annotate_persisted_team_depth(
|
|
3074
3158
|
&workspace,
|
|
@@ -3164,6 +3248,46 @@ pub fn quick_start_with_transport_in_workspace_with_display(
|
|
|
3164
3248
|
/// the persisted endpoint synchronized with the transport they actually used,
|
|
3165
3249
|
/// closing the silent socket-drift gap** (single state-save path; no parallel
|
|
3166
3250
|
/// "annotate after spawn" race with coordinator).
|
|
3251
|
+
/// 0.5.x Phase 1d Batch 2: generic runtime-transport annotator.
|
|
3252
|
+
///
|
|
3253
|
+
/// Writes `state.transport = { kind, source }` for every backend
|
|
3254
|
+
/// (kind = wire string `"tmux"` | `"conpty"`; source = wire string
|
|
3255
|
+
/// from `ResolvedTransport.source` when known, else `"unknown"`).
|
|
3256
|
+
///
|
|
3257
|
+
/// For tmux, ALSO forwards to `annotate_runtime_tmux_endpoint` so the
|
|
3258
|
+
/// existing `tmux_endpoint` / `tmux_socket` / `tmux_socket_source`
|
|
3259
|
+
/// fields remain populated byte-equivalent to today's shape.
|
|
3260
|
+
///
|
|
3261
|
+
/// The tmux-specific fields are NOT written for ConPTY (CR C-4: no
|
|
3262
|
+
/// tmux-only fields under a conpty state; keeps compact status
|
|
3263
|
+
/// stable). ConPTY discovery fields (`pipe_name`, `shim_pid`) are
|
|
3264
|
+
/// added in Batch 3 when the shim boot path lands; this function just
|
|
3265
|
+
/// pins the top-level `transport.kind`/`source`.
|
|
3266
|
+
pub fn annotate_runtime_transport(
|
|
3267
|
+
state: &mut serde_json::Value,
|
|
3268
|
+
transport: &dyn Transport,
|
|
3269
|
+
workspace: &Path,
|
|
3270
|
+
source: Option<&str>,
|
|
3271
|
+
) {
|
|
3272
|
+
use crate::transport::BackendKind;
|
|
3273
|
+
let kind_wire = match transport.kind() {
|
|
3274
|
+
BackendKind::Tmux => "tmux",
|
|
3275
|
+
BackendKind::WezTerm => "wezterm",
|
|
3276
|
+
BackendKind::ConPty => "conpty",
|
|
3277
|
+
};
|
|
3278
|
+
if let Some(obj) = state.as_object_mut() {
|
|
3279
|
+
let transport_block = serde_json::json!({
|
|
3280
|
+
"kind": kind_wire,
|
|
3281
|
+
"source": source.unwrap_or("unknown"),
|
|
3282
|
+
});
|
|
3283
|
+
obj.insert("transport".to_string(), transport_block);
|
|
3284
|
+
}
|
|
3285
|
+
// Tmux: preserve the existing tmux endpoint annotation shape.
|
|
3286
|
+
if matches!(transport.kind(), BackendKind::Tmux) {
|
|
3287
|
+
annotate_runtime_tmux_endpoint(state, transport, workspace);
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3167
3291
|
pub(crate) fn annotate_runtime_tmux_endpoint(
|
|
3168
3292
|
state: &mut serde_json::Value,
|
|
3169
3293
|
transport: &dyn Transport,
|
|
@@ -467,6 +467,22 @@ pub(super) fn start_coordinator_for_workspace(workspace: &Path) -> Result<bool,
|
|
|
467
467
|
/// only within `lifecycle::restart`. Sharing the resolver across the lifecycle
|
|
468
468
|
/// module is the correct ownership: restart/add/fork all need the SAME socket
|
|
469
469
|
/// the live team uses, and duplicating the lookup invited drift.**
|
|
470
|
+
/// 0.5.x Phase 1d Batch 1: this legacy resolver stays tmux-only. It
|
|
471
|
+
/// still returns a concrete `TmuxBackend` because the 6 caller sites
|
|
472
|
+
/// (rebuild/agent/remove/launch) currently call `TmuxBackend`-specific
|
|
473
|
+
/// methods (`.tmux_endpoint()`, `.for_workspace()`, etc.). But it now
|
|
474
|
+
/// routes through the factory so its **selection semantics** match
|
|
475
|
+
/// the new `lifecycle_worker_transport_for_selected_state` API:
|
|
476
|
+
///
|
|
477
|
+
/// - If `state.transport.kind` is `"conpty"`, this legacy tmux-typed
|
|
478
|
+
/// resolver **fails-closed** with `TransportBackendKindMismatch`
|
|
479
|
+
/// rather than silently returning a tmux backend that would
|
|
480
|
+
/// spawn/inject into the wrong pane universe. That satisfies CR C-1
|
|
481
|
+
/// fail-closed while the migration of caller sites to the generic
|
|
482
|
+
/// `_transport_for_selected_state` API completes.
|
|
483
|
+
/// - Otherwise (default/legacy tmux endpoint), behavior is preserved
|
|
484
|
+
/// byte-for-byte through
|
|
485
|
+
/// `tmux_backend_for_runtime_state_or_workspace`.
|
|
470
486
|
pub(crate) fn lifecycle_worker_tmux_backend_for_selected_state(
|
|
471
487
|
run_workspace: &Path,
|
|
472
488
|
team: Option<&str>,
|
|
@@ -485,12 +501,66 @@ pub(crate) fn lifecycle_worker_tmux_backend_for_selected_state(
|
|
|
485
501
|
.unwrap_or("");
|
|
486
502
|
return Err(LifecycleError::TeamSelect(format!("{reason}: {detail}")));
|
|
487
503
|
}
|
|
504
|
+
// CR C-1 fail-closed: state with `transport.kind=conpty` must NOT
|
|
505
|
+
// silently downgrade to a tmux backend just because the caller
|
|
506
|
+
// asked for the tmux-typed variant. Callers that see this refusal
|
|
507
|
+
// are expected to migrate to `lifecycle_worker_transport_for_selected_state`
|
|
508
|
+
// during Batch 2/3.
|
|
509
|
+
if let Some(state_ref) = state.as_ref() {
|
|
510
|
+
if let Some(kind) = state_ref.pointer("/transport/kind").and_then(|v| v.as_str()) {
|
|
511
|
+
if kind.eq_ignore_ascii_case("conpty") {
|
|
512
|
+
return Err(LifecycleError::TeamSelect(format!(
|
|
513
|
+
"backend_kind_mismatch: state.transport.kind={kind:?} but the legacy \
|
|
514
|
+
tmux-typed lifecycle resolver was invoked; caller must migrate to \
|
|
515
|
+
`lifecycle_worker_transport_for_selected_state` (Phase 1d Batch 2/3)"
|
|
516
|
+
)));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
488
520
|
Ok(state
|
|
489
521
|
.as_ref()
|
|
490
522
|
.map(|state| lifecycle_worker_tmux_backend_for_state(run_workspace, state))
|
|
491
523
|
.unwrap_or_else(|| crate::tmux_backend::TmuxBackend::for_workspace(run_workspace)))
|
|
492
524
|
}
|
|
493
525
|
|
|
526
|
+
/// 0.5.x Phase 1d Batch 1: new generic-typed lifecycle resolver.
|
|
527
|
+
///
|
|
528
|
+
/// Same team-selection semantics as the legacy tmux-typed variant, but
|
|
529
|
+
/// returns a `ResolvedTransport` from the factory so callers that can
|
|
530
|
+
/// hold `Box<dyn Transport>` (or the resolved `BackendKind`) work
|
|
531
|
+
/// against both tmux AND conpty state. Caller migration to this API
|
|
532
|
+
/// happens across Batch 2-5.
|
|
533
|
+
///
|
|
534
|
+
/// Fail-closed rules follow the factory (C-1 ×2, C-2 CLI-vs-state,
|
|
535
|
+
/// C-3 N38 notices via `ResolvedTransport.notices`).
|
|
536
|
+
pub(crate) fn lifecycle_worker_transport_for_selected_state(
|
|
537
|
+
run_workspace: &Path,
|
|
538
|
+
team: Option<&str>,
|
|
539
|
+
) -> Result<crate::transport_factory::ResolvedTransport, LifecycleError> {
|
|
540
|
+
let (state, refusal) = crate::state::projection::resolve_team_scoped_state(run_workspace, team)
|
|
541
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
542
|
+
if let Some(refusal) = refusal {
|
|
543
|
+
let reason = refusal
|
|
544
|
+
.get("reason")
|
|
545
|
+
.and_then(|v| v.as_str())
|
|
546
|
+
.unwrap_or("team_target_unresolved");
|
|
547
|
+
let detail = refusal
|
|
548
|
+
.get("error")
|
|
549
|
+
.or_else(|| refusal.get("message"))
|
|
550
|
+
.and_then(|v| v.as_str())
|
|
551
|
+
.unwrap_or("");
|
|
552
|
+
return Err(LifecycleError::TeamSelect(format!("{reason}: {detail}")));
|
|
553
|
+
}
|
|
554
|
+
let input = crate::transport_factory::TransportFactoryInput::new(
|
|
555
|
+
run_workspace,
|
|
556
|
+
crate::transport_factory::TransportPurpose::LifecycleWorker,
|
|
557
|
+
)
|
|
558
|
+
.with_team_key(team)
|
|
559
|
+
.with_state(state.as_ref());
|
|
560
|
+
crate::transport_factory::resolve_transport(input)
|
|
561
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))
|
|
562
|
+
}
|
|
563
|
+
|
|
494
564
|
pub(super) fn lifecycle_worker_tmux_backend_for_state(
|
|
495
565
|
run_workspace: &Path,
|
|
496
566
|
state: &serde_json::Value,
|
|
@@ -2693,6 +2693,13 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
|
|
|
2693
2693
|
"tasks",
|
|
2694
2694
|
"display_backend",
|
|
2695
2695
|
"is_external_leader",
|
|
2696
|
+
// 0.5.x Phase 1d hot-path 接线 (裁决1 msg_76e1d98202b8):
|
|
2697
|
+
// `transport = { kind, source }` inserted here by
|
|
2698
|
+
// `annotate_runtime_transport`. Kind is `"tmux"` for the
|
|
2699
|
+
// default tmux path (byte-equivalent behavior + new metadata
|
|
2700
|
+
// field); source is `"unknown"` until Phase 2 threads
|
|
2701
|
+
// `ResolvedTransport.source` through the launch call site.
|
|
2702
|
+
"transport",
|
|
2696
2703
|
// 0.4.0 refactor: `team_key` added as top-level topology marker
|
|
2697
2704
|
// alongside active_team_key (refactor-modular-architecture). The
|
|
2698
2705
|
// owner / leader_receiver / owner_epoch invariants below still
|
|
@@ -11,6 +11,20 @@ use std::collections::{BTreeMap, BTreeSet};
|
|
|
11
11
|
use std::path::{Path, PathBuf};
|
|
12
12
|
|
|
13
13
|
const ANCESTRY_ENV: &str = "TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON";
|
|
14
|
+
const CALLER_IDENTITY_ENVS: &[&str] = &[
|
|
15
|
+
"TMUX",
|
|
16
|
+
"TMUX_PANE",
|
|
17
|
+
"TEAM_AGENT_LEADER_PANE_ID",
|
|
18
|
+
"TEAM_AGENT_LEADER_SESSION_UUID",
|
|
19
|
+
"TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
|
|
20
|
+
"TEAM_AGENT_LEADER_PROVIDER",
|
|
21
|
+
"TEAM_AGENT_MACHINE_FINGERPRINT",
|
|
22
|
+
"TEAM_AGENT_WORKSPACE",
|
|
23
|
+
"TEAM_AGENT_TEAM_ID",
|
|
24
|
+
"TEAM_AGENT_OWNER_TEAM_ID",
|
|
25
|
+
"TEAM_AGENT_ACTIVE_TEAM",
|
|
26
|
+
"TEAM_AGENT_ID",
|
|
27
|
+
];
|
|
14
28
|
|
|
15
29
|
#[test]
|
|
16
30
|
#[serial_test::serial(env)]
|
|
@@ -141,6 +155,10 @@ struct PhaseGolden {
|
|
|
141
155
|
|
|
142
156
|
fn run_phase_golden(spec: PhaseGolden) -> Value {
|
|
143
157
|
let _permission_mode = EnvVarGuard::set(ANCESTRY_ENV, "[]");
|
|
158
|
+
let _caller_identity = CALLER_IDENTITY_ENVS
|
|
159
|
+
.iter()
|
|
160
|
+
.map(|key| EnvVarGuard::unset(key))
|
|
161
|
+
.collect::<Vec<_>>();
|
|
144
162
|
let team = two_worker_team_dir();
|
|
145
163
|
let workspace = team.parent().expect("workspace").to_path_buf();
|
|
146
164
|
seed_healthy_coordinator(&workspace);
|
|
@@ -186,6 +204,7 @@ fn run_phase_golden(spec: PhaseGolden) -> Value {
|
|
|
186
204
|
json: true,
|
|
187
205
|
message_id: Some("phase-golden-message".to_string()),
|
|
188
206
|
pane: None,
|
|
207
|
+
to_name: None,
|
|
189
208
|
});
|
|
190
209
|
let collect = cmd_collect(&CollectArgs {
|
|
191
210
|
workspace: workspace.clone(),
|
|
@@ -394,6 +413,14 @@ impl EnvVarGuard {
|
|
|
394
413
|
}
|
|
395
414
|
Self { key, previous }
|
|
396
415
|
}
|
|
416
|
+
|
|
417
|
+
fn unset(key: &'static str) -> Self {
|
|
418
|
+
let previous = std::env::var(key).ok();
|
|
419
|
+
unsafe {
|
|
420
|
+
std::env::remove_var(key);
|
|
421
|
+
}
|
|
422
|
+
Self { key, previous }
|
|
423
|
+
}
|
|
397
424
|
}
|
|
398
425
|
|
|
399
426
|
impl Drop for EnvVarGuard {
|
|
@@ -531,6 +558,15 @@ fn normalize_path_string(text: &str, ctx: &NormalizeCtx) -> String {
|
|
|
531
558
|
|
|
532
559
|
fn normalize_team_agent_binary_path(text: &str) -> String {
|
|
533
560
|
let mut out = text.to_string();
|
|
561
|
+
// 0.5.0 hermetic 教训「环境路径类 token 化」的直接延伸
|
|
562
|
+
// (leader msg_6ee04cf5aee8):归一化改为结构判据,不绑路径前缀。
|
|
563
|
+
// 用户 CARGO_TARGET_DIR 可以指到任意目录(默认 `<repo>/target`,
|
|
564
|
+
// 全局改道时是 `/Volumes/nvme/cargo-target`,CI 时可能是别的)。
|
|
565
|
+
// 结构判据:任意绝对路径下的 `/deps/team_agent-<hex>`(可选 `.exe`)
|
|
566
|
+
// → `<TEAM_AGENT_BIN>`。同时保留旧 marker 兼容(处理旧固定字符串
|
|
567
|
+
// 出现在 output 里,以及 `debug/team-agent` / `release/team-agent`
|
|
568
|
+
// 这两个非 `/deps/` 分支)。
|
|
569
|
+
out = normalize_deps_team_agent_by_structure(&out);
|
|
534
570
|
for marker in [
|
|
535
571
|
"/target/debug/deps/team_agent-",
|
|
536
572
|
"/target/debug/team-agent",
|
|
@@ -541,6 +577,60 @@ fn normalize_team_agent_binary_path(text: &str) -> String {
|
|
|
541
577
|
out
|
|
542
578
|
}
|
|
543
579
|
|
|
580
|
+
/// Structural (path-prefix-independent) normalization for
|
|
581
|
+
/// `/…/deps/team_agent-<hex>(.exe)?` occurrences. Scans the input for
|
|
582
|
+
/// every `/deps/team_agent-` marker and rewrites the containing
|
|
583
|
+
/// absolute path token to `<TEAM_AGENT_BIN>`, no matter which
|
|
584
|
+
/// `CARGO_TARGET_DIR` produced it.
|
|
585
|
+
fn normalize_deps_team_agent_by_structure(text: &str) -> String {
|
|
586
|
+
const MARKER: &str = "/deps/team_agent-";
|
|
587
|
+
let mut out = String::with_capacity(text.len());
|
|
588
|
+
let bytes = text.as_bytes();
|
|
589
|
+
let mut i = 0usize;
|
|
590
|
+
while i < bytes.len() {
|
|
591
|
+
let Some(rel) = text[i..].find(MARKER) else {
|
|
592
|
+
out.push_str(&text[i..]);
|
|
593
|
+
break;
|
|
594
|
+
};
|
|
595
|
+
let marker_idx = i + rel;
|
|
596
|
+
// Locate the start of the absolute path token: walk back from
|
|
597
|
+
// `marker_idx` while the byte is not one of a small set of
|
|
598
|
+
// delimiters (quote, whitespace, `[`, `(`, `,`, `:` after
|
|
599
|
+
// whitespace). The absolute-path anchor MUST also start with
|
|
600
|
+
// `/` — if the containing token is not absolute, skip
|
|
601
|
+
// rewriting so we don't eat unrelated text.
|
|
602
|
+
let path_start = text[..marker_idx]
|
|
603
|
+
.rfind(|c: char| matches!(c, '"' | '\'' | ' ' | '[' | '(' | ',' | '\n' | '\t'))
|
|
604
|
+
.map(|idx| idx + 1)
|
|
605
|
+
.unwrap_or(0);
|
|
606
|
+
// Confirm the token starts with `/` (absolute path).
|
|
607
|
+
if !text[path_start..].starts_with('/') {
|
|
608
|
+
// Not an absolute path — leave alone.
|
|
609
|
+
out.push_str(&text[i..marker_idx + MARKER.len()]);
|
|
610
|
+
i = marker_idx + MARKER.len();
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
// Extend the end past the hex hash + optional `.exe`. Same
|
|
614
|
+
// continuation rule as `replace_path_with_marker`: alnum + `-`
|
|
615
|
+
// + `_` + `.`.
|
|
616
|
+
let mut path_end = marker_idx + MARKER.len();
|
|
617
|
+
while path_end < text.len() {
|
|
618
|
+
let ch = text[path_end..].chars().next().expect("char at boundary");
|
|
619
|
+
if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
|
|
620
|
+
path_end += ch.len_utf8();
|
|
621
|
+
} else {
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
// Emit everything up to the path start, then the token, then
|
|
626
|
+
// continue past the eaten span.
|
|
627
|
+
out.push_str(&text[i..path_start]);
|
|
628
|
+
out.push_str("<TEAM_AGENT_BIN>");
|
|
629
|
+
i = path_end;
|
|
630
|
+
}
|
|
631
|
+
out
|
|
632
|
+
}
|
|
633
|
+
|
|
544
634
|
fn replace_path_with_marker(mut text: String, marker: &str, token: &str) -> String {
|
|
545
635
|
let mut search_from = 0;
|
|
546
636
|
while let Some(offset) = text[search_from..].find(marker) {
|