@team-agent/installer 0.4.0 → 0.4.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.
Files changed (45) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +82 -22
  4. package/crates/team-agent/src/cli/diagnose.rs +6 -2
  5. package/crates/team-agent/src/cli/emit.rs +301 -38
  6. package/crates/team-agent/src/cli/mod.rs +7 -14
  7. package/crates/team-agent/src/cli/status_port.rs +12 -1
  8. package/crates/team-agent/src/cli/tests/base.rs +6 -2
  9. package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
  10. package/crates/team-agent/src/cli/tests/leader_watch.rs +5 -3
  11. package/crates/team-agent/src/cli/tests/main_preserved.rs +28 -2
  12. package/crates/team-agent/src/cli/tests/peer_allow.rs +19 -0
  13. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +72 -0
  14. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -3
  15. package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
  16. package/crates/team-agent/src/cli/types.rs +35 -2
  17. package/crates/team-agent/src/coordinator/runtime_detectors.rs +7 -2
  18. package/crates/team-agent/src/coordinator/tick.rs +86 -16
  19. package/crates/team-agent/src/diagnose/comms.rs +10 -2
  20. package/crates/team-agent/src/leader/lease.rs +98 -19
  21. package/crates/team-agent/src/leader/rediscover/tests.rs +21 -7
  22. package/crates/team-agent/src/leader/rediscover.rs +16 -7
  23. package/crates/team-agent/src/leader/start.rs +25 -12
  24. package/crates/team-agent/src/leader/tests/byte_findings.rs +11 -2
  25. package/crates/team-agent/src/leader/tests/lease_claim.rs +248 -7
  26. package/crates/team-agent/src/lifecycle/launch.rs +116 -100
  27. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +5 -12
  28. package/crates/team-agent/src/lifecycle/tests/core.rs +1 -1
  29. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +16 -15
  30. package/crates/team-agent/src/lifecycle/types.rs +1 -1
  31. package/crates/team-agent/src/messaging/leader_receiver.rs +22 -8
  32. package/crates/team-agent/src/messaging/send.rs +21 -4
  33. package/crates/team-agent/src/provider/adapter.rs +22 -0
  34. package/crates/team-agent/src/provider/session/capture.rs +228 -0
  35. package/crates/team-agent/src/state/identity.rs +42 -12
  36. package/crates/team-agent/src/state/identity_keys.rs +134 -0
  37. package/crates/team-agent/src/state/mod.rs +8 -0
  38. package/crates/team-agent/src/state/owner_gate.rs +11 -1
  39. package/crates/team-agent/src/state/ownership.rs +556 -0
  40. package/crates/team-agent/src/state/paths.rs +358 -0
  41. package/crates/team-agent/src/state/persist.rs +43 -5
  42. package/crates/team-agent/src/state/projection.rs +13 -5
  43. package/crates/team-agent/src/tmux_backend.rs +12 -0
  44. package/package.json +4 -4
  45. package/skills/team-agent/SKILL.md +3 -3
@@ -35,18 +35,45 @@ use super::*;
35
35
  let receiver = r.receiver.as_ref().expect("claimed → receiver");
36
36
  assert_eq!(receiver.pane_id, caller);
37
37
  assert_eq!(receiver.discovery, Some(Discovery::ClaimLeader));
38
- // dual-state: BOTH workspace state.json + team/<session> snapshot carry %5/epoch 1.
38
+ // Stage 3d (identity-boundary unified plan, architect direction
39
+ // 2026-06-23): canonical owner now lives at teams.<team_key>; the
40
+ // top-level dual-write was dropped. Read through the ownership
41
+ // repository which preserves the legacy precedence for callers
42
+ // that still consult top-level on migration states.
39
43
  let ws_path = crate::state::persist::runtime_state_path(&ws);
40
44
  let ws_state: serde_json::Value =
41
45
  serde_json::from_str(&std::fs::read_to_string(&ws_path).unwrap()).unwrap();
42
- assert_eq!(ws_state["team_owner"]["pane_id"], serde_json::json!("%5"));
43
- assert_eq!(ws_state["team_owner"]["owner_epoch"], serde_json::json!(1));
46
+ // The canonical team_key is what `team_state_key(state)` derives
47
+ // (here: state's session_name "team-agent-x" since no explicit
48
+ // team_key/team_dir/spec_path was seeded). The test's `team_id`
49
+ // parameter is the LOOKUP key but `claim_lease_no_incident` uses
50
+ // the state-derived key for canonical writes. Stage 5 will unify
51
+ // these via TeamRuntimePaths.
52
+ let canonical_key = crate::state::projection::team_state_key(&ws_state);
53
+ let ws_owner =
54
+ crate::state::ownership::read_owner_value(&ws_state, &canonical_key)
55
+ .expect("Stage 3d: vacant acquire writes canonical owner");
56
+ assert_eq!(ws_owner["pane_id"], serde_json::json!("%5"));
57
+ assert_eq!(ws_owner["owner_epoch"], serde_json::json!(1));
58
+ // team_id parameter is currently unused by the canonical write
59
+ // (architect Stage 5 will close this gap); silence the warning.
60
+ let _ = &team_id;
44
61
  let snap_path = crate::model::paths::runtime_dir(&ws)
45
62
  .join("teams").join("team-agent-x").join("state.json");
46
63
  let snap: serde_json::Value =
47
64
  serde_json::from_str(&std::fs::read_to_string(&snap_path).unwrap()).unwrap();
48
- assert_eq!(snap["team_owner"]["pane_id"], serde_json::json!("%5"), "dual-state snapshot owner");
49
- assert_eq!(snap["team_owner"]["owner_epoch"], serde_json::json!(1));
65
+ // Snapshot is per-session; team_state_key on the snapshot derives
66
+ // from its session_name. Read top-level first (legacy snapshot
67
+ // layout), fall back to derived team key.
68
+ let snap_owner = snap
69
+ .get("team_owner")
70
+ .or_else(|| {
71
+ let key = crate::state::projection::team_state_key(&snap);
72
+ snap.get("teams").and_then(|t| t.get(key)).and_then(|e| e.get("team_owner"))
73
+ })
74
+ .expect("dual-state snapshot owner");
75
+ assert_eq!(snap_owner["pane_id"], serde_json::json!("%5"), "dual-state snapshot owner");
76
+ assert_eq!(snap_owner["owner_epoch"], serde_json::json!(1));
50
77
  }
51
78
 
52
79
  // RED — DEAD-OWNER RECOVER: bound pane %1 absent from live set, caller %5
@@ -173,8 +200,11 @@ use super::*;
173
200
  &std::fs::read_to_string(crate::state::persist::runtime_state_path(&ws)).unwrap(),
174
201
  )
175
202
  .unwrap();
203
+ // Stage 3d: canonical receiver at teams.<team_key>.leader_receiver.
204
+ let team_key = crate::state::projection::team_state_key(&persisted);
205
+ let receiver = &persisted["teams"][&team_key]["leader_receiver"];
176
206
  assert_eq!(
177
- persisted["leader_receiver"]["tmux_socket"],
207
+ receiver["tmux_socket"],
178
208
  serde_json::json!(leader_socket),
179
209
  "state.json must carry enough endpoint information for later delivery to use tmux -S"
180
210
  );
@@ -290,8 +320,11 @@ use super::*;
290
320
  &std::fs::read_to_string(crate::state::persist::runtime_state_path(&ws)).unwrap(),
291
321
  )
292
322
  .unwrap();
323
+ // Stage 3d: canonical receiver at teams.<team_key>.leader_receiver.
324
+ let team_key = crate::state::projection::team_state_key(&persisted);
325
+ let receiver = &persisted["teams"][&team_key]["leader_receiver"];
293
326
  assert_eq!(
294
- persisted["leader_receiver"]["tmux_socket"],
327
+ receiver["tmux_socket"],
295
328
  serde_json::json!(current_socket),
296
329
  "state must not preserve a short endpoint after explicit claim; state={persisted}"
297
330
  );
@@ -408,3 +441,211 @@ use super::*;
408
441
  // _lease_epoch:400, _emit_lease_refusal:469, audit :712-729). These REDs lock the EXACT
409
442
  // golden shape per divergence; driven via claim_lease_no_incident(+seeded_liveness), OS-safe.
410
443
  // ═════════════════════════════════════════════════════════════════════════
444
+
445
+ /// **S3-OWNER-001 regression** (architect Stage 3 owner persist fix,
446
+ /// .team/artifacts/stage3-owner-persist-fix.md). Pre-fix on commit
447
+ /// `5d980c7`:
448
+ /// 1. `claim-leader --team alpha` returns `ok=true/status=claimed`.
449
+ /// 2. In-memory state has `state.teams.alpha.team_owner` set by
450
+ /// `write_binding_to_state` → `state::ownership::write_owner`.
451
+ /// 3. `save_claim_team_scoped_state` calls `compact_team_state(state)` to
452
+ /// build the per-team snapshot. `compact_team_state` strips the
453
+ /// `teams` key entirely — the just-written nested ownership record
454
+ /// is DROPPED on the way to disk.
455
+ /// 4. After Stage 3d removed the top-level dual-write, no copy survives.
456
+ /// 5. Reloading the workspace state.json shows `persisted_owner_locations=[]`.
457
+ ///
458
+ /// Fix: `save_claim_team_scoped_state` now uses
459
+ /// `compact_team_state_preserving_ownership(state, target_key)` which
460
+ /// re-injects `team_owner / leader_receiver / owner_epoch` from
461
+ /// `state.teams[target_key]` into the compacted entry. Top-level fields
462
+ /// remain absent (Stage 3d canonical shape preserved).
463
+ #[test]
464
+ #[serial_test::serial(env)]
465
+ fn claim_team_scoped_persists_canonical_owner_under_teams_target_key() {
466
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
467
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
468
+ let ws = p2_temp_ws("claim_team_persist");
469
+ let target = "alpha";
470
+ let team_id = TeamKey::new(target);
471
+ let caller = PaneId::new("%5");
472
+ // Seed runtime state with a teams entry but no owner fields anywhere
473
+ // (the S3-OWNER-001 starting condition). team_dir is set so
474
+ // team_state_key resolves to the target team_key, matching the
475
+ // claim path's projected-state shape.
476
+ let mut state = serde_json::json!({
477
+ "session_name": "team-agent-x",
478
+ "team_dir": format!("/tmp/{}", target),
479
+ "teams": {target: {}},
480
+ });
481
+ let event_log = crate::event_log::EventLog::new(&ws);
482
+ let live = seeded_liveness(&["%5"]);
483
+ let r = claim_lease_no_incident(
484
+ &ws, &mut state, Some(target), &team_id, &caller, false, &event_log, &live,
485
+ )
486
+ .unwrap();
487
+ assert!(r.ok, "S3-OWNER-001: claim must succeed; got {r:?}");
488
+ assert_eq!(r.status, LeaseStatus::Claimed);
489
+
490
+ // Reload state from disk — this is where the pre-fix loss surfaced.
491
+ let persisted_path = crate::state::persist::runtime_state_path(&ws);
492
+ let persisted: serde_json::Value = serde_json::from_str(
493
+ &std::fs::read_to_string(&persisted_path).unwrap(),
494
+ )
495
+ .unwrap();
496
+
497
+ // Canonical: teams.<target_key>.{team_owner, leader_receiver, owner_epoch}.
498
+ let entry = &persisted["teams"][target];
499
+ assert_eq!(
500
+ entry["team_owner"]["pane_id"],
501
+ serde_json::json!("%5"),
502
+ "S3-OWNER-001 fix: teams.{target}.team_owner.pane_id persisted; \
503
+ persisted={persisted}"
504
+ );
505
+ assert_eq!(
506
+ entry["leader_receiver"]["pane_id"],
507
+ serde_json::json!("%5"),
508
+ "S3-OWNER-001 fix: teams.{target}.leader_receiver.pane_id persisted; \
509
+ persisted={persisted}"
510
+ );
511
+ assert_eq!(
512
+ entry["owner_epoch"], serde_json::json!(1),
513
+ "S3-OWNER-001 fix: teams.{target}.owner_epoch=1 persisted; \
514
+ persisted={persisted}"
515
+ );
516
+
517
+ // Stage 3d invariant: top-level owner fields stay ABSENT (no
518
+ // recurrence of the legacy dual-write).
519
+ assert!(
520
+ persisted.get("team_owner").is_none(),
521
+ "Stage 3d: top-level team_owner must remain absent; persisted={persisted}"
522
+ );
523
+ assert!(
524
+ persisted.get("leader_receiver").is_none(),
525
+ "Stage 3d: top-level leader_receiver must remain absent; persisted={persisted}"
526
+ );
527
+ assert!(
528
+ persisted.get("owner_epoch").is_none(),
529
+ "Stage 3d: top-level owner_epoch must remain absent; persisted={persisted}"
530
+ );
531
+ }
532
+
533
+ /// **S3-OWNER-002 regression** (architect Stage 3 top-level cleanup,
534
+ /// .team/artifacts/stage3-toplevel-cleanup-fix.md). Even with the
535
+ /// nested-ownership-preserving compact (aae818f), a SECOND claim on a
536
+ /// state whose root still carries legacy top-level `team_owner` /
537
+ /// `leader_receiver` / `owner_epoch` would leave the stale root copy
538
+ /// on disk while the canonical `teams.<key>` advanced. Evidence:
539
+ /// `state-after-claim.json` showed `team_owner=%2/epoch=1` at root and
540
+ /// `teams.<key>.team_owner=%3/epoch=2` — exact dual-source recurrence.
541
+ ///
542
+ /// Fix: `save_claim_team_scoped_state` strips legacy root ownership
543
+ /// fields before save via `state::ownership::strip_top_level_ownership`,
544
+ /// and the legacy "copy top-level owner from incoming state into
545
+ /// merged" loop is removed. After every successful named-team owner
546
+ /// mutation, persisted root is canonical-only.
547
+ #[test]
548
+ #[serial_test::serial(env)]
549
+ fn second_claim_strips_legacy_top_level_ownership_from_persisted_root() {
550
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
551
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
552
+ let ws = p2_temp_ws("claim_team_second_strip");
553
+ let target = "alpha";
554
+ let team_id = TeamKey::new(target);
555
+ let caller = PaneId::new("%3");
556
+ // Seed a state whose root carries stale legacy top-level owner
557
+ // (%2/epoch=1) AND a teams.<target_key> entry that already has
558
+ // canonical ownership at the previous epoch. This is the
559
+ // post-S3-OWNER-001-fix shape on disk after one prior claim.
560
+ let mut state = serde_json::json!({
561
+ "session_name": "team-agent-x",
562
+ "team_dir": format!("/tmp/{}", target),
563
+ "active_team_key": target,
564
+ "team_owner": {
565
+ "pane_id": "%2",
566
+ "provider": "codex",
567
+ "machine_fingerprint": "fp",
568
+ "leader_session_uuid": "OLDUUID",
569
+ "owner_epoch": 1,
570
+ "claimed_at": "t",
571
+ "claimed_via": "claim-leader",
572
+ },
573
+ "leader_receiver": {
574
+ "pane_id": "%2",
575
+ "owner_epoch": 1,
576
+ "leader_session_uuid": "OLDUUID",
577
+ },
578
+ "owner_epoch": 1,
579
+ "teams": {
580
+ target: {
581
+ "team_owner": {
582
+ "pane_id": "%2",
583
+ "provider": "codex",
584
+ "machine_fingerprint": "fp",
585
+ "leader_session_uuid": "OLDUUID",
586
+ "owner_epoch": 1,
587
+ "claimed_at": "t",
588
+ "claimed_via": "claim-leader",
589
+ },
590
+ "leader_receiver": {
591
+ "pane_id": "%2",
592
+ "owner_epoch": 1,
593
+ "leader_session_uuid": "OLDUUID",
594
+ },
595
+ "owner_epoch": 1,
596
+ }
597
+ },
598
+ });
599
+ // Persist the seeded shape so `save_claim_team_scoped_state` loads
600
+ // it back as `existing` on save.
601
+ crate::state::persist::save_runtime_state(&ws, &state).unwrap();
602
+ let event_log = crate::event_log::EventLog::new(&ws);
603
+ // Both panes live so the dead-owner reclaim branch doesn't apply;
604
+ // explicit takeover via confirm=true forces the claim past the
605
+ // live-owner gate.
606
+ let live = seeded_liveness(&["%2", "%3"]);
607
+ let r = claim_lease_no_incident(
608
+ &ws, &mut state, Some(target), &team_id, &caller, true, &event_log, &live,
609
+ )
610
+ .unwrap();
611
+ assert!(r.ok, "S3-OWNER-002: explicit takeover must succeed; got {r:?}");
612
+ assert_eq!(r.status, LeaseStatus::Claimed);
613
+
614
+ let persisted_path = crate::state::persist::runtime_state_path(&ws);
615
+ let persisted: serde_json::Value = serde_json::from_str(
616
+ &std::fs::read_to_string(&persisted_path).unwrap(),
617
+ )
618
+ .unwrap();
619
+
620
+ // Canonical advanced.
621
+ let entry = &persisted["teams"][target];
622
+ assert_eq!(
623
+ entry["team_owner"]["pane_id"],
624
+ serde_json::json!("%3"),
625
+ "S3-OWNER-002 fix: canonical owner advanced to new pane; persisted={persisted}"
626
+ );
627
+ assert_eq!(
628
+ entry["owner_epoch"],
629
+ serde_json::json!(2),
630
+ "S3-OWNER-002 fix: canonical owner_epoch advanced to 2; persisted={persisted}"
631
+ );
632
+ assert_eq!(
633
+ entry["leader_receiver"]["pane_id"],
634
+ serde_json::json!("%3"),
635
+ "S3-OWNER-002 fix: canonical receiver advanced; persisted={persisted}"
636
+ );
637
+
638
+ // Top-level stale legacy owner stripped.
639
+ assert!(
640
+ persisted.get("team_owner").is_none(),
641
+ "S3-OWNER-002 fix: stale top-level team_owner must be stripped; persisted={persisted}"
642
+ );
643
+ assert!(
644
+ persisted.get("leader_receiver").is_none(),
645
+ "S3-OWNER-002 fix: stale top-level leader_receiver must be stripped; persisted={persisted}"
646
+ );
647
+ assert!(
648
+ persisted.get("owner_epoch").is_none(),
649
+ "S3-OWNER-002 fix: stale top-level owner_epoch must be stripped; persisted={persisted}"
650
+ );
651
+ }