@team-agent/installer 0.4.10 → 0.5.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 +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +1 -0
- package/crates/team-agent/src/cli/diagnose.rs +2 -15
- package/crates/team-agent/src/cli/emit.rs +62 -4
- package/crates/team-agent/src/compiler.rs +11 -20
- package/crates/team-agent/src/coordinator/orphan.rs +2 -2
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +5 -4
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +1776 -2
- package/crates/team-agent/src/coordinator/steps/mod.rs +19 -0
- package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +78 -7
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +195 -0
- package/crates/team-agent/src/coordinator/tick.rs +31 -932
- package/crates/team-agent/src/diagnose/orphans.rs +66 -8
- package/crates/team-agent/src/layout/worker_window_helpers.rs +5 -7
- package/crates/team-agent/src/leader/provider_attribution.rs +7 -5
- package/crates/team-agent/src/leader/rediscover.rs +1 -11
- package/crates/team-agent/src/leader/start.rs +169 -25
- package/crates/team-agent/src/lifecycle/launch/plan.rs +19 -8
- package/crates/team-agent/src/lifecycle/launch.rs +135 -58
- package/crates/team-agent/src/lifecycle/lock.rs +302 -0
- package/crates/team-agent/src/lifecycle/mod.rs +1 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -11
- package/crates/team-agent/src/lifecycle/restart/agent.rs +436 -121
- package/crates/team-agent/src/lifecycle/restart/common.rs +108 -17
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -32
- package/crates/team-agent/src/lifecycle/restart/remove.rs +16 -2
- package/crates/team-agent/src/lifecycle/restart/selection.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +193 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +321 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +54 -15
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +802 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +648 -0
- package/crates/team-agent/src/lifecycle/tests.rs +3 -0
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +24 -34
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +24 -9
- package/crates/team-agent/src/mcp_server/tools.rs +157 -68
- package/crates/team-agent/src/messaging/activity.rs +43 -18
- package/crates/team-agent/src/messaging/delivery.rs +161 -97
- package/crates/team-agent/src/messaging/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +22 -1
- package/crates/team-agent/src/messaging/results.rs +34 -9
- package/crates/team-agent/src/messaging/scheduler.rs +52 -9
- package/crates/team-agent/src/model/spec.rs +10 -6
- package/crates/team-agent/src/provider/adapter.rs +10 -1038
- package/crates/team-agent/src/provider/adapters/claude.rs +3 -8
- package/crates/team-agent/src/provider/adapters/copilot.rs +2 -5
- package/crates/team-agent/src/provider/classify.rs +23 -48
- package/crates/team-agent/src/provider/command.rs +16 -7
- package/crates/team-agent/src/provider/faults.rs +93 -53
- package/crates/team-agent/src/provider/session/capture.rs +4 -15
- package/crates/team-agent/src/provider/session_scan/claude.rs +309 -0
- package/crates/team-agent/src/provider/session_scan/codex.rs +40 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +350 -0
- package/crates/team-agent/src/provider/session_scan/copilot.rs +202 -0
- package/crates/team-agent/src/provider/session_scan.rs +65 -27
- package/crates/team-agent/src/provider/tests/faults.rs +80 -0
- package/crates/team-agent/src/provider/types.rs +33 -1
- package/crates/team-agent/src/provider/wire.rs +171 -1
- package/crates/team-agent/src/state/identity.rs +242 -57
- package/crates/team-agent/src/state/identity_keys.rs +9 -12
- package/crates/team-agent/src/state/mod.rs +5 -1
- package/crates/team-agent/src/state/owner_gate.rs +59 -15
- package/crates/team-agent/src/state/ownership.rs +12 -11
- package/crates/team-agent/src/state/paths.rs +13 -6
- package/crates/team-agent/src/state/persist.rs +671 -128
- package/crates/team-agent/src/state/projection.rs +240 -49
- package/crates/team-agent/src/state/selector.rs +11 -5
- package/crates/team-agent/src/tmux_backend/tests.rs +51 -2
- package/crates/team-agent/src/tmux_backend.rs +42 -5
- package/crates/team-agent/src/transport/test_support.rs +55 -6
- package/package.json +4 -4
|
@@ -503,20 +503,43 @@ fn is_provider_or_mcp_workspace_command(command: &str) -> bool {
|
|
|
503
503
|
}
|
|
504
504
|
|
|
505
505
|
fn parse_workspace_arg(command: &str) -> Option<PathBuf> {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
506
|
+
for (start, _) in command.match_indices("--workspace") {
|
|
507
|
+
let boundary_before = start == 0
|
|
508
|
+
|| command
|
|
509
|
+
.get(..start)
|
|
510
|
+
.and_then(|prefix| prefix.chars().next_back())
|
|
511
|
+
.is_some_and(char::is_whitespace);
|
|
512
|
+
if !boundary_before {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
let after = &command[start + "--workspace".len()..];
|
|
516
|
+
if let Some(value) = after.strip_prefix('=') {
|
|
517
|
+
return parse_workspace_value(value);
|
|
512
518
|
}
|
|
513
|
-
if
|
|
514
|
-
return
|
|
519
|
+
if after.chars().next().is_some_and(char::is_whitespace) {
|
|
520
|
+
return parse_workspace_value(after);
|
|
515
521
|
}
|
|
516
522
|
}
|
|
517
523
|
None
|
|
518
524
|
}
|
|
519
525
|
|
|
526
|
+
fn parse_workspace_value(value: &str) -> Option<PathBuf> {
|
|
527
|
+
let value = value.trim_start();
|
|
528
|
+
if value.is_empty() {
|
|
529
|
+
return None;
|
|
530
|
+
}
|
|
531
|
+
let value = value
|
|
532
|
+
.split_once(" --")
|
|
533
|
+
.map(|(workspace, _)| workspace)
|
|
534
|
+
.unwrap_or(value)
|
|
535
|
+
.trim_end();
|
|
536
|
+
if value.is_empty() {
|
|
537
|
+
None
|
|
538
|
+
} else {
|
|
539
|
+
Some(PathBuf::from(value))
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
520
543
|
fn classify_workspace_orphan(workspace: &Path, pid: Pid) -> Option<OrphanReason> {
|
|
521
544
|
if !workspace.is_absolute() {
|
|
522
545
|
return None;
|
|
@@ -656,3 +679,38 @@ fn reason_key(reason: &OrphanReason) -> &'static str {
|
|
|
656
679
|
OrphanReason::PidNotRunning => "pid_not_running",
|
|
657
680
|
}
|
|
658
681
|
}
|
|
682
|
+
|
|
683
|
+
#[cfg(test)]
|
|
684
|
+
mod tests {
|
|
685
|
+
use super::*;
|
|
686
|
+
|
|
687
|
+
#[test]
|
|
688
|
+
fn parse_workspace_arg_keeps_trailing_path_with_spaces() {
|
|
689
|
+
let command = "/tmp/team-agent coordinator --workspace /Volumes/projects/My Agent";
|
|
690
|
+
|
|
691
|
+
assert_eq!(
|
|
692
|
+
parse_workspace_arg(command),
|
|
693
|
+
Some(PathBuf::from("/Volumes/projects/My Agent"))
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
#[test]
|
|
698
|
+
fn parse_workspace_arg_stops_at_next_flag() {
|
|
699
|
+
let command = "/tmp/team-agent coordinator --workspace /tmp/My Agent --once";
|
|
700
|
+
|
|
701
|
+
assert_eq!(
|
|
702
|
+
parse_workspace_arg(command),
|
|
703
|
+
Some(PathBuf::from("/tmp/My Agent"))
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
#[test]
|
|
708
|
+
fn parse_workspace_arg_supports_equals_form_with_spaces() {
|
|
709
|
+
let command = "/tmp/team-agent coordinator --workspace=/tmp/My Agent --once";
|
|
710
|
+
|
|
711
|
+
assert_eq!(
|
|
712
|
+
parse_workspace_arg(command),
|
|
713
|
+
Some(PathBuf::from("/tmp/My Agent"))
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
//! 0.3.28 layout step 2 — small
|
|
1
|
+
//! 0.3.28 layout step 2 — small window grammar helper for layout-layer naming.
|
|
2
2
|
//!
|
|
3
|
-
//!
|
|
4
|
-
//!
|
|
5
|
-
//!
|
|
6
|
-
//! authoritative provider→string mapping).
|
|
3
|
+
//! Command/UI grammar, not provider identity parsing: leader window names
|
|
4
|
+
//! intentionally collapse `claude_code` to `claude` and use executable-style
|
|
5
|
+
//! labels such as `gemini`.
|
|
7
6
|
|
|
8
7
|
use crate::model::enums::Provider;
|
|
9
8
|
|
|
10
|
-
/// Window name used inside the leader session
|
|
11
|
-
/// wire identifier (e.g. `claude`, `codex`, `copilot`, `fake`). Mirrors
|
|
9
|
+
/// Window name used inside the leader session. Mirrors
|
|
12
10
|
/// Python's `leader_window_name = provider` (see
|
|
13
11
|
/// `runtime/0.2.11/src/team_agent/leader/__init__.py:114-131`).
|
|
14
12
|
pub fn provider_window_name(provider: Provider) -> &'static str {
|
|
@@ -8,7 +8,9 @@ use crate::transport::PaneInfo;
|
|
|
8
8
|
|
|
9
9
|
use super::helpers::{parse_provider, provider_wire};
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// Command grammar, not provider identity parsing: these are process argv/current-command
|
|
12
|
+
// needles observed in tmux panes.
|
|
13
|
+
const COMMAND_GRAMMAR_PROVIDER_COMMANDS: &[(&str, Provider)] = &[
|
|
12
14
|
("claude", Provider::ClaudeCode),
|
|
13
15
|
("claude.exe", Provider::ClaudeCode),
|
|
14
16
|
("codex", Provider::Codex),
|
|
@@ -37,7 +39,7 @@ fn attribute_pane_provider_with_process(
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
pub(crate) fn attribute_command_provider(command: &str) -> Option<Provider> {
|
|
40
|
-
|
|
42
|
+
provider_from_command_text(command)
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
pub(crate) fn command_matches_provider(provider: Provider, command: &str) -> bool {
|
|
@@ -58,7 +60,7 @@ fn provider_from_env(env: &BTreeMap<String, String>) -> Option<Provider> {
|
|
|
58
60
|
fn provider_from_pid_argv(pid: u32) -> Option<Provider> {
|
|
59
61
|
process_command_line(pid)
|
|
60
62
|
.as_deref()
|
|
61
|
-
.and_then(
|
|
63
|
+
.and_then(provider_from_command_text)
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
fn provider_from_pid_env(pid: u32) -> Option<Provider> {
|
|
@@ -67,9 +69,9 @@ fn provider_from_pid_env(pid: u32) -> Option<Provider> {
|
|
|
67
69
|
.and_then(provider_from_env_text)
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
fn
|
|
72
|
+
fn provider_from_command_text(text: &str) -> Option<Provider> {
|
|
71
73
|
let lower = text.to_ascii_lowercase();
|
|
72
|
-
|
|
74
|
+
COMMAND_GRAMMAR_PROVIDER_COMMANDS
|
|
73
75
|
.iter()
|
|
74
76
|
.find_map(|(needle, provider)| lower.contains(needle).then_some(*provider))
|
|
75
77
|
}
|
|
@@ -7,6 +7,7 @@ use serde_json::{json, Value};
|
|
|
7
7
|
|
|
8
8
|
use crate::model::ids::{LeaderSessionUuid, OwnerEpoch};
|
|
9
9
|
use crate::provider::Provider;
|
|
10
|
+
use crate::provider::wire::provider_wire;
|
|
10
11
|
use crate::transport::{PaneId, PaneInfo, SessionName, Transport, WindowName};
|
|
11
12
|
|
|
12
13
|
use super::helpers::{get_path_str, now_ts, parse_provider, prefix, resolve_workspace_for_hash, sha1_hex_prefix};
|
|
@@ -902,17 +903,6 @@ fn uuid_prefix(uuid: Option<&LeaderSessionUuid>) -> String {
|
|
|
902
903
|
uuid.map_or_else(String::new, |value| prefix(value.as_str(), 8).to_string())
|
|
903
904
|
}
|
|
904
905
|
|
|
905
|
-
fn provider_wire(provider: Provider) -> &'static str {
|
|
906
|
-
match provider {
|
|
907
|
-
Provider::Claude => "claude",
|
|
908
|
-
Provider::ClaudeCode => "claude_code",
|
|
909
|
-
Provider::Codex => "codex",
|
|
910
|
-
Provider::Copilot => "copilot",
|
|
911
|
-
Provider::GeminiCli => "gemini_cli",
|
|
912
|
-
Provider::Fake => "fake",
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
|
|
916
906
|
fn event_provider(identity: &OwnerIdentity) -> &'static str {
|
|
917
907
|
provider_wire(identity.provider.unwrap_or(Provider::Codex))
|
|
918
908
|
}
|
|
@@ -61,12 +61,20 @@ pub fn leader_start_plan(
|
|
|
61
61
|
// `managed_team_session_name(identity) = team-<team_id>` which is the
|
|
62
62
|
// worker session — that co-location is the structural root of
|
|
63
63
|
// E49/E51/E53/E57-3/E60.
|
|
64
|
+
// 0.4.10+ mirror-session fix v2 (option B): managed-mode session name
|
|
65
|
+
// gets a per-launch nonce so each `team-agent <provider>` entry in the
|
|
66
|
+
// same workspace creates its OWN tmux session — matching the user
|
|
67
|
+
// expectation that `tmux new-session + claude` is independent every
|
|
68
|
+
// time. Pre-fix the managed path used the workspace-keyed name (same
|
|
69
|
+
// session for every entry) and silently attached the second client →
|
|
70
|
+
// UI mirror. The external/attach paths keep the stable workspace-keyed
|
|
71
|
+
// name (per-launch nonce would break `--attach-session` reattach).
|
|
64
72
|
let session_name = if external_path {
|
|
65
73
|
attach_session
|
|
66
74
|
.cloned()
|
|
67
75
|
.or_else(|| Some(leader_session_name(provider, workspace)))
|
|
68
76
|
} else {
|
|
69
|
-
Some(
|
|
77
|
+
Some(managed_leader_session_name(provider, workspace))
|
|
70
78
|
};
|
|
71
79
|
let in_tmux = std::env::var_os("TMUX").is_some();
|
|
72
80
|
if !in_tmux {
|
|
@@ -266,6 +274,42 @@ pub fn leader_session_name(provider: Provider, workspace: &Path) -> SessionName
|
|
|
266
274
|
))
|
|
267
275
|
}
|
|
268
276
|
|
|
277
|
+
/// 0.4.10+ mirror-session fix v2: managed-mode session name with a
|
|
278
|
+
/// per-launch nonce.
|
|
279
|
+
///
|
|
280
|
+
/// Format: `team-agent-leader-<provider>-<folder>-<hash>-<nonce>`.
|
|
281
|
+
/// `nonce` = `<pid_hex>-<epoch_nanos_hex>`. The pid distinguishes
|
|
282
|
+
/// concurrent launches; the epoch_nanos distinguishes sequential ones.
|
|
283
|
+
///
|
|
284
|
+
/// Each `team-agent <provider>` entry in the managed (non-tmux) path gets
|
|
285
|
+
/// its OWN session, matching the user expectation that `tmux new-session
|
|
286
|
+
/// + claude` is independent every time. The workspace-keyed prefix is
|
|
287
|
+
/// preserved so existing leader-session-anchored protections
|
|
288
|
+
/// (`LEADER_SESSION_PREFIX` matchers in shutdown/cli/mod.rs) still
|
|
289
|
+
/// classify these sessions as leader sessions.
|
|
290
|
+
///
|
|
291
|
+
/// External / attach paths keep the stable `leader_session_name` —
|
|
292
|
+
/// per-launch nonce would break `--attach-session <name>` reattach
|
|
293
|
+
/// semantics.
|
|
294
|
+
pub fn managed_leader_session_name(provider: Provider, workspace: &Path) -> SessionName {
|
|
295
|
+
let resolved = resolve_workspace_for_hash(workspace);
|
|
296
|
+
let folder_raw = resolved
|
|
297
|
+
.file_name()
|
|
298
|
+
.and_then(|n| n.to_str())
|
|
299
|
+
.unwrap_or("workspace");
|
|
300
|
+
let folder = sanitize_session_folder(folder_raw);
|
|
301
|
+
let hash = sha1_hex_prefix(resolved.to_string_lossy().as_bytes(), 8);
|
|
302
|
+
let pid = std::process::id();
|
|
303
|
+
let epoch_nanos = std::time::SystemTime::now()
|
|
304
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
305
|
+
.map(|d| d.as_nanos())
|
|
306
|
+
.unwrap_or(0);
|
|
307
|
+
SessionName::new(format!(
|
|
308
|
+
"{LEADER_SESSION_PREFIX}{}-{folder}-{hash}-{pid:x}-{epoch_nanos:x}",
|
|
309
|
+
provider_wire(provider)
|
|
310
|
+
))
|
|
311
|
+
}
|
|
312
|
+
|
|
269
313
|
fn start_argv(
|
|
270
314
|
mode: LeaderStartMode,
|
|
271
315
|
provider: Provider,
|
|
@@ -423,31 +467,14 @@ fn ensure_managed_leader_pane(
|
|
|
423
467
|
plan: &LeaderStartPlan,
|
|
424
468
|
workspace: &Path,
|
|
425
469
|
) -> Result<SpawnResult, LeaderError> {
|
|
470
|
+
// 0.4.10+ mirror-session fix v2 (option B): each managed launch gets
|
|
471
|
+
// its OWN session name (via `managed_leader_session_name`'s nonce).
|
|
472
|
+
// The historical "session already exists → reuse pane" branch is
|
|
473
|
+
// structurally unreachable now (the per-launch session never
|
|
474
|
+
// preexists). If a caller bypasses the nonce path and reuses a name,
|
|
475
|
+
// tmux's own duplicate-session error will surface — better than
|
|
476
|
+
// silently attaching a second client.
|
|
426
477
|
if transport.has_session(session).unwrap_or(false) {
|
|
427
|
-
if transport
|
|
428
|
-
.list_windows(session)
|
|
429
|
-
.unwrap_or_default()
|
|
430
|
-
.iter()
|
|
431
|
-
.any(|existing| existing.as_str() == window.as_str())
|
|
432
|
-
{
|
|
433
|
-
if let Some(existing) = transport
|
|
434
|
-
.list_targets()
|
|
435
|
-
.unwrap_or_default()
|
|
436
|
-
.into_iter()
|
|
437
|
-
.find(|pane| {
|
|
438
|
-
pane.session.as_str() == session.as_str()
|
|
439
|
-
&& pane.window_name.as_ref().map(WindowName::as_str)
|
|
440
|
-
== Some(window.as_str())
|
|
441
|
-
})
|
|
442
|
-
{
|
|
443
|
-
return Ok(SpawnResult {
|
|
444
|
-
pane_id: existing.pane_id,
|
|
445
|
-
session: session.clone(),
|
|
446
|
-
window: window.clone(),
|
|
447
|
-
child_pid: existing.pane_pid,
|
|
448
|
-
});
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
478
|
// 0.4.x (CR C-1 + C-2): leader env_unset reuses the worker
|
|
452
479
|
// provider_env_unsets (single source of truth) + spawn through the
|
|
453
480
|
// leader shell wrapper so provider exit returns to a shell, not
|
|
@@ -1586,4 +1613,121 @@ mod tests {
|
|
|
1586
1613
|
assert_eq!(sent[0].0, Target::Pane(PaneId::new("%0")));
|
|
1587
1614
|
assert_eq!(sent[0].1, vec![Key::Enter]);
|
|
1588
1615
|
}
|
|
1616
|
+
|
|
1617
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1618
|
+
// 0.4.10+ mirror-session fix v2 (option B: independent session per launch).
|
|
1619
|
+
//
|
|
1620
|
+
// managed_leader_session_name appends a per-launch nonce so each
|
|
1621
|
+
// `team-agent <provider>` entry in the same workspace creates its own
|
|
1622
|
+
// tmux session, matching `tmux new-session + claude` semantics.
|
|
1623
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1624
|
+
|
|
1625
|
+
#[test]
|
|
1626
|
+
fn managed_leader_session_name_carries_leader_prefix() {
|
|
1627
|
+
// Protection invariant: every session name returned by the
|
|
1628
|
+
// managed path must still match LEADER_SESSION_PREFIX so the
|
|
1629
|
+
// shutdown / cli/mod.rs prefix matchers still classify it as a
|
|
1630
|
+
// leader session.
|
|
1631
|
+
let workspace = std::env::temp_dir().join(format!(
|
|
1632
|
+
"ta_rs_mgr_prefix_{}",
|
|
1633
|
+
std::process::id()
|
|
1634
|
+
));
|
|
1635
|
+
let _ = std::fs::create_dir_all(&workspace);
|
|
1636
|
+
let name = super::managed_leader_session_name(Provider::ClaudeCode, &workspace);
|
|
1637
|
+
assert!(
|
|
1638
|
+
name.as_str().starts_with(super::super::LEADER_SESSION_PREFIX),
|
|
1639
|
+
"managed session name must carry LEADER_SESSION_PREFIX (`{}`); \
|
|
1640
|
+
got `{}`",
|
|
1641
|
+
super::super::LEADER_SESSION_PREFIX,
|
|
1642
|
+
name.as_str()
|
|
1643
|
+
);
|
|
1644
|
+
assert!(
|
|
1645
|
+
name.as_str().contains("claude_code"),
|
|
1646
|
+
"managed session name must include provider wire; got `{}`",
|
|
1647
|
+
name.as_str()
|
|
1648
|
+
);
|
|
1649
|
+
let _ = std::fs::remove_dir_all(&workspace);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
#[test]
|
|
1653
|
+
fn managed_leader_session_name_is_unique_per_call() {
|
|
1654
|
+
// Two consecutive calls must return DIFFERENT names — the
|
|
1655
|
+
// per-launch nonce defeats the mirror-session bug.
|
|
1656
|
+
let workspace = std::env::temp_dir().join(format!(
|
|
1657
|
+
"ta_rs_mgr_unique_{}",
|
|
1658
|
+
std::process::id()
|
|
1659
|
+
));
|
|
1660
|
+
let _ = std::fs::create_dir_all(&workspace);
|
|
1661
|
+
let a = super::managed_leader_session_name(Provider::ClaudeCode, &workspace);
|
|
1662
|
+
// Sleep > 1ns to ensure epoch_nanos advances even on coarse clocks.
|
|
1663
|
+
std::thread::sleep(std::time::Duration::from_millis(1));
|
|
1664
|
+
let b = super::managed_leader_session_name(Provider::ClaudeCode, &workspace);
|
|
1665
|
+
assert_ne!(
|
|
1666
|
+
a.as_str(),
|
|
1667
|
+
b.as_str(),
|
|
1668
|
+
"two managed launches in the same workspace must get \
|
|
1669
|
+
DIFFERENT session names (mirror-session fix v2); got \
|
|
1670
|
+
`{}` vs `{}`",
|
|
1671
|
+
a.as_str(),
|
|
1672
|
+
b.as_str()
|
|
1673
|
+
);
|
|
1674
|
+
let _ = std::fs::remove_dir_all(&workspace);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
#[test]
|
|
1678
|
+
fn leader_session_name_is_stable_across_calls() {
|
|
1679
|
+
// External / attach paths must keep the stable workspace-keyed
|
|
1680
|
+
// name — `--attach-session <name>` reattach would break if the
|
|
1681
|
+
// name varied per call.
|
|
1682
|
+
let workspace = std::env::temp_dir().join(format!(
|
|
1683
|
+
"ta_rs_lsn_stable_{}",
|
|
1684
|
+
std::process::id()
|
|
1685
|
+
));
|
|
1686
|
+
let _ = std::fs::create_dir_all(&workspace);
|
|
1687
|
+
let a = super::leader_session_name(Provider::ClaudeCode, &workspace);
|
|
1688
|
+
let b = super::leader_session_name(Provider::ClaudeCode, &workspace);
|
|
1689
|
+
assert_eq!(
|
|
1690
|
+
a.as_str(),
|
|
1691
|
+
b.as_str(),
|
|
1692
|
+
"leader_session_name must be deterministic per workspace \
|
|
1693
|
+
(external/attach paths depend on stability); got `{}` vs `{}`",
|
|
1694
|
+
a.as_str(),
|
|
1695
|
+
b.as_str()
|
|
1696
|
+
);
|
|
1697
|
+
let _ = std::fs::remove_dir_all(&workspace);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/// CR C-5 source grep guard: managed mode passes the nonce session
|
|
1701
|
+
/// name through `leader_start_plan` so the per-launch independence
|
|
1702
|
+
/// reaches `ensure_managed_leader_pane`. External/attach paths must
|
|
1703
|
+
/// keep `leader_session_name`.
|
|
1704
|
+
#[test]
|
|
1705
|
+
fn managed_path_uses_nonce_session_name_grep_guard() {
|
|
1706
|
+
let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
|
1707
|
+
let start_rs = manifest.join("src").join("leader").join("start.rs");
|
|
1708
|
+
let contents =
|
|
1709
|
+
std::fs::read_to_string(&start_rs).expect("read leader/start.rs");
|
|
1710
|
+
// leader_start_plan must dispatch to managed_leader_session_name
|
|
1711
|
+
// on the managed path.
|
|
1712
|
+
let start = contents
|
|
1713
|
+
.find("pub fn leader_start_plan(")
|
|
1714
|
+
.expect("leader_start_plan must exist");
|
|
1715
|
+
let end = contents[start + 1..]
|
|
1716
|
+
.find("\npub fn ")
|
|
1717
|
+
.or_else(|| contents[start + 1..].find("\nfn "))
|
|
1718
|
+
.map(|p| start + 1 + p)
|
|
1719
|
+
.unwrap_or(contents.len());
|
|
1720
|
+
let body = &contents[start..end];
|
|
1721
|
+
assert!(
|
|
1722
|
+
body.contains("managed_leader_session_name(provider, workspace)"),
|
|
1723
|
+
"leader_start_plan must call managed_leader_session_name on \
|
|
1724
|
+
the managed path (mirror-session fix v2); body excerpt: {body}"
|
|
1725
|
+
);
|
|
1726
|
+
// The external_path branch must still use the stable name.
|
|
1727
|
+
assert!(
|
|
1728
|
+
body.contains("leader_session_name(provider, workspace)"),
|
|
1729
|
+
"leader_start_plan must keep leader_session_name on the \
|
|
1730
|
+
external/attach paths; body excerpt: {body}"
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1589
1733
|
}
|
|
@@ -15,9 +15,8 @@ pub fn start_plan(
|
|
|
15
15
|
plan_path.display()
|
|
16
16
|
)));
|
|
17
17
|
}
|
|
18
|
-
let text = std::fs::read_to_string(plan_path)
|
|
19
|
-
LifecycleError::InvalidPlan(format!("{}: {e}", plan_path.display()))
|
|
20
|
-
})?;
|
|
18
|
+
let text = std::fs::read_to_string(plan_path)
|
|
19
|
+
.map_err(|e| LifecycleError::InvalidPlan(format!("{}: {e}", plan_path.display())))?;
|
|
21
20
|
let plan = yaml::loads(&text).map_err(|e| LifecycleError::InvalidPlan(e.to_string()))?;
|
|
22
21
|
let plan_id = plan_id_from_plan(plan_path, &plan)?;
|
|
23
22
|
let state_path = plan_state_path(workspace, &plan_id);
|
|
@@ -66,7 +65,8 @@ pub fn handle_report_result(
|
|
|
66
65
|
return Ok(PlanProgress::NoMatch);
|
|
67
66
|
};
|
|
68
67
|
for entry in entries {
|
|
69
|
-
let entry =
|
|
68
|
+
let entry =
|
|
69
|
+
entry.map_err(|e| LifecycleError::InvalidPlan(format!("read plan dir: {e}")))?;
|
|
70
70
|
let path = entry.path();
|
|
71
71
|
if path.extension().and_then(|s| s.to_str()) != Some("json") {
|
|
72
72
|
continue;
|
|
@@ -112,10 +112,19 @@ fn plan_stages(plan: &Value) -> Result<Vec<PlanStage>, LifecycleError> {
|
|
|
112
112
|
.transpose()?;
|
|
113
113
|
stages.push(PlanStage {
|
|
114
114
|
id,
|
|
115
|
-
assignee: raw
|
|
116
|
-
|
|
115
|
+
assignee: raw
|
|
116
|
+
.get("assignee")
|
|
117
|
+
.and_then(Value::as_str)
|
|
118
|
+
.map(AgentId::new),
|
|
119
|
+
prompt: raw
|
|
120
|
+
.get("prompt")
|
|
121
|
+
.and_then(Value::as_str)
|
|
122
|
+
.map(str::to_string),
|
|
117
123
|
on_result: condition,
|
|
118
|
-
status: raw
|
|
124
|
+
status: raw
|
|
125
|
+
.get("status")
|
|
126
|
+
.and_then(Value::as_str)
|
|
127
|
+
.map(str::to_string),
|
|
119
128
|
});
|
|
120
129
|
}
|
|
121
130
|
Ok(stages)
|
|
@@ -222,6 +231,8 @@ fn json_scalar_to_string(value: &serde_json::Value) -> Option<String> {
|
|
|
222
231
|
serde_json::Value::String(s) => Some(s.clone()),
|
|
223
232
|
serde_json::Value::Bool(b) => Some(b.to_string()),
|
|
224
233
|
serde_json::Value::Number(n) => Some(n.to_string()),
|
|
225
|
-
serde_json::Value::Null | serde_json::Value::Array(_) | serde_json::Value::Object(_) =>
|
|
234
|
+
serde_json::Value::Null | serde_json::Value::Array(_) | serde_json::Value::Object(_) => {
|
|
235
|
+
None
|
|
236
|
+
}
|
|
226
237
|
}
|
|
227
238
|
}
|