@team-agent/installer 0.1.4 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.1.4",
3
+ "version": "0.1.7",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -35,7 +35,15 @@ from team_agent.providers import ResumeUnavailable, get_adapter, shell_command_f
35
35
  from team_agent.routing import route_task
36
36
  from team_agent.simple_yaml import dumps
37
37
  from team_agent.spec import load_spec, validate_result_envelope, workspace_from_spec
38
- from team_agent.state import load_runtime_state, runtime_state_path, save_runtime_state, write_team_state
38
+ from team_agent.state import (
39
+ SESSION_CAPTURE_FIELDS,
40
+ SESSION_STATE_FIELDS,
41
+ load_runtime_state,
42
+ normalize_agent_session_state,
43
+ runtime_state_path,
44
+ save_runtime_state,
45
+ write_team_state,
46
+ )
39
47
  from team_agent.task_graph import ready_tasks, update_task_status
40
48
  from team_agent.task_graph import TASK_STATUSES
41
49
 
@@ -520,10 +528,7 @@ def _load_snapshot_state(path: Path) -> dict[str, Any] | None:
520
528
  state = json.loads(path.read_text(encoding="utf-8"))
521
529
  except (OSError, json.JSONDecodeError):
522
530
  return None
523
- for agent_state in state.get("agents", {}).values():
524
- if isinstance(agent_state, dict):
525
- for field in ["session_id", "rollout_path", "captured_at", "captured_via", "attribution_confidence", "spawn_cwd"]:
526
- agent_state.setdefault(field, None)
531
+ normalize_agent_session_state(state)
527
532
  return state
528
533
 
529
534
 
@@ -1570,8 +1575,7 @@ def _capture_agent_session(
1570
1575
  result = adapter.capture_session_id(agent_id, spawn_context, timeout_s=timeout_s)
1571
1576
  if not isinstance(result, dict) or not result.get("session_id"):
1572
1577
  return None
1573
- for key in ["session_id", "rollout_path", "captured_at", "captured_via", "attribution_confidence", "spawn_cwd"]:
1574
- agent_state[key] = result.get(key)
1578
+ _copy_session_metadata(agent_state, result)
1575
1579
  agent_state.pop("_pending_session_id", None)
1576
1580
  event_log.write(
1577
1581
  "session.captured",
@@ -1585,6 +1589,16 @@ def _capture_agent_session(
1585
1589
  return result
1586
1590
 
1587
1591
 
1592
+ def _copy_session_metadata(target: dict[str, Any], source: dict[str, Any]) -> None:
1593
+ for key in SESSION_STATE_FIELDS:
1594
+ target[key] = source.get(key)
1595
+
1596
+
1597
+ def _clear_session_capture_fields(target: dict[str, Any]) -> None:
1598
+ for key in SESSION_CAPTURE_FIELDS:
1599
+ target[key] = None
1600
+
1601
+
1588
1602
  def _attach_profile_resume_root(workspace: Path, command_agent: dict[str, Any], previous: dict[str, Any]) -> dict[str, Any]:
1589
1603
  profile_launch = command_agent.get("_provider_profile") or prepare_agent_profile_launch(workspace, command_agent)
1590
1604
  if not profile_launch:
@@ -1631,8 +1645,7 @@ def _prepare_resume_state(
1631
1645
  if not repaired:
1632
1646
  repaired = adapter.recover_session_id(agent_id, prepared, workspace, exclude_session_ids or set())
1633
1647
  if repaired:
1634
- for key in ["session_id", "rollout_path", "captured_at", "captured_via", "attribution_confidence", "spawn_cwd"]:
1635
- prepared[key] = repaired.get(key)
1648
+ _copy_session_metadata(prepared, repaired)
1636
1649
  event_log.write(
1637
1650
  "resume.session_repaired",
1638
1651
  agent_id=agent_id,
@@ -1657,8 +1670,7 @@ def _prepare_resume_state(
1657
1670
  f"Cannot resume agent {agent_id}: stored session {session_id} is not available. "
1658
1671
  "Use --allow-fresh only if losing that worker context is acceptable."
1659
1672
  )
1660
- for key in ["session_id", "rollout_path", "captured_at", "captured_via", "attribution_confidence"]:
1661
- prepared[key] = None
1673
+ _clear_session_capture_fields(prepared)
1662
1674
  event_log.write(
1663
1675
  "resume.session_unavailable",
1664
1676
  agent_id=agent_id,
@@ -1972,8 +1984,7 @@ def restart(workspace: Path, allow_fresh: bool = False, team: str | None = None)
1972
1984
  if profile_launch.get("claude_projects_root"):
1973
1985
  agent_state["claude_projects_root"] = profile_launch["claude_projects_root"]
1974
1986
  if restart_mode == "fresh":
1975
- for key in ["session_id", "rollout_path", "captured_at", "captured_via", "attribution_confidence"]:
1976
- agent_state[key] = None
1987
+ _clear_session_capture_fields(agent_state)
1977
1988
  if command_agent.get("_session_id"):
1978
1989
  agent_state["_pending_session_id"] = command_agent["_session_id"]
1979
1990
  _capture_agent_session(
@@ -2231,8 +2242,7 @@ def _start_agent_unlocked(workspace: Path, agent_id: str, force: bool, open_disp
2231
2242
  if profile_launch.get("claude_projects_root"):
2232
2243
  agent_state["claude_projects_root"] = profile_launch["claude_projects_root"]
2233
2244
  if start_mode == "fresh":
2234
- for key in ["session_id", "rollout_path", "captured_at", "captured_via", "attribution_confidence"]:
2235
- agent_state[key] = None
2245
+ _clear_session_capture_fields(agent_state)
2236
2246
  if command_agent.get("_session_id"):
2237
2247
  agent_state["_pending_session_id"] = command_agent["_session_id"]
2238
2248
  _capture_agent_session(workspace, agent_id, agent_state, event_log, timeout_s=1.5, exclude_session_ids=known_session_ids)
@@ -10,12 +10,15 @@ from team_agent.paths import runtime_dir
10
10
  from team_agent.simple_yaml import dumps
11
11
 
12
12
 
13
- SESSION_STATE_FIELDS = [
13
+ SESSION_CAPTURE_FIELDS = [
14
14
  "session_id",
15
15
  "rollout_path",
16
16
  "captured_at",
17
17
  "captured_via",
18
18
  "attribution_confidence",
19
+ ]
20
+ SESSION_STATE_FIELDS = [
21
+ *SESSION_CAPTURE_FIELDS,
19
22
  "spawn_cwd",
20
23
  ]
21
24
 
@@ -24,15 +27,22 @@ def runtime_state_path(workspace: Path) -> Path:
24
27
  return runtime_dir(workspace) / "state.json"
25
28
 
26
29
 
30
+ def normalize_agent_session_state(state: dict[str, Any]) -> None:
31
+ agents = state.get("agents", {})
32
+ if not isinstance(agents, dict):
33
+ return
34
+ for agent_state in agents.values():
35
+ if isinstance(agent_state, dict):
36
+ for field in SESSION_STATE_FIELDS:
37
+ agent_state.setdefault(field, None)
38
+
39
+
27
40
  def load_runtime_state(workspace: Path) -> dict[str, Any]:
28
41
  path = runtime_state_path(workspace)
29
42
  if not path.exists():
30
43
  return {"agents": {}, "tasks": [], "session_name": None}
31
44
  state = json.loads(path.read_text(encoding="utf-8"))
32
- for agent_state in state.get("agents", {}).values():
33
- if isinstance(agent_state, dict):
34
- for field in SESSION_STATE_FIELDS:
35
- agent_state.setdefault(field, None)
45
+ normalize_agent_session_state(state)
36
46
  return state
37
47
 
38
48
 
@@ -701,6 +701,7 @@ class RuntimeTests(unittest.TestCase):
701
701
  spec_path.write_text(dumps(spec), encoding="utf-8")
702
702
  with (
703
703
  patch("team_agent.runtime.shutil_which", return_value="/usr/bin/tmux"),
704
+ patch("team_agent.providers.shutil.which", return_value="/usr/bin/codex"),
704
705
  patch("team_agent.runtime._tmux_session_exists", return_value=False),
705
706
  patch("team_agent.runtime._tmux_current_client_pane_info", return_value=None),
706
707
  patch("team_agent.runtime._tmux_list_panes", return_value=[]),
@@ -709,6 +710,21 @@ class RuntimeTests(unittest.TestCase):
709
710
  runtime.launch(spec_path, auto_approve=True)
710
711
  self.assertIn("could not locate a tmux-managed leader pane", str(ctx.exception))
711
712
 
713
+ def test_launch_blocks_missing_provider_command(self) -> None:
714
+ with tempfile.TemporaryDirectory(prefix="team-agent-provider-missing-") as tmp:
715
+ workspace = Path(tmp)
716
+ spec = _fake_spec(workspace)
717
+ spec["agents"][0]["provider"] = "codex"
718
+ spec_path = workspace / "team.spec.yaml"
719
+ spec_path.write_text(dumps(spec), encoding="utf-8")
720
+ with (
721
+ patch("team_agent.runtime.shutil_which", return_value="/usr/bin/tmux"),
722
+ patch("team_agent.providers.shutil.which", return_value=None),
723
+ self.assertRaises(TeamAgentRuntimeError) as ctx,
724
+ ):
725
+ runtime.launch(spec_path, auto_approve=True)
726
+ self.assertIn("Provider codex command 'codex' not found", str(ctx.exception))
727
+
712
728
  def test_worker_to_leader_direct_injection_uses_standard_payload_and_does_not_route_task(self) -> None:
713
729
  if not shutil.which("tmux"):
714
730
  self.skipTest("tmux not installed")
@@ -1310,7 +1326,8 @@ Implement bounded tasks and report result_envelope_v1.
1310
1326
  )
1311
1327
  compiled = compile_team(team, workspace / "team.spec.yaml")["spec"]
1312
1328
  self.assertIsNone(compiled["agents"][0]["model"])
1313
- preflight = runtime.preflight(team)
1329
+ with patch("team_agent.runtime.shutil_which", return_value="/usr/bin/tmux"):
1330
+ preflight = runtime.preflight(team)
1314
1331
  self.assertTrue(preflight["ok"])
1315
1332
  profiles = next(check for check in preflight["checks"] if check["name"] == "profiles")
1316
1333
  implementer = next(item for item in profiles["checks"] if item["agent_id"] == "implementer")
@@ -3126,7 +3143,11 @@ Handle fake tasks.
3126
3143
  cwd = os.getcwd()
3127
3144
  os.chdir(workspace)
3128
3145
  try:
3129
- with self.assertRaises(runtime.RuntimeError) as ctx:
3146
+ with (
3147
+ patch("team_agent.runtime.shutil_which", return_value="/usr/bin/tmux"),
3148
+ patch("team_agent.runtime._model_checks_for_agents", return_value=[]),
3149
+ self.assertRaises(runtime.RuntimeError) as ctx,
3150
+ ):
3130
3151
  runtime.quick_start(team)
3131
3152
  finally:
3132
3153
  os.chdir(cwd)
@@ -3137,7 +3158,10 @@ Handle fake tasks.
3137
3158
  workspace = Path(tmp)
3138
3159
  state = {
3139
3160
  "session_name": "team-old",
3140
- "agents": {"fake_impl": {"status": "running", "provider": "fake", "window": "fake_impl"}},
3161
+ "agents": {
3162
+ "fake_impl": {"status": "running", "provider": "fake", "window": "fake_impl"},
3163
+ "legacy_bad": "untouched",
3164
+ },
3141
3165
  "tasks": [],
3142
3166
  }
3143
3167
  save_runtime_state(workspace, state)
@@ -3146,6 +3170,98 @@ Handle fake tasks.
3146
3170
  for key in ["session_id", "rollout_path", "captured_at", "captured_via", "attribution_confidence", "spawn_cwd"]:
3147
3171
  self.assertIn(key, agent)
3148
3172
  self.assertIsNone(agent[key])
3173
+ self.assertEqual(loaded["agents"]["legacy_bad"], "untouched")
3174
+
3175
+ def test_runtime_state_missing_file_default_is_literal(self) -> None:
3176
+ with tempfile.TemporaryDirectory(prefix="team-agent-missing-state-") as tmp:
3177
+ self.assertEqual(load_runtime_state(Path(tmp)), {"agents": {}, "tasks": [], "session_name": None})
3178
+
3179
+ def test_snapshot_state_session_fields_are_backward_compatible(self) -> None:
3180
+ with tempfile.TemporaryDirectory(prefix="team-agent-old-snapshot-") as tmp:
3181
+ state_path = Path(tmp) / "state.json"
3182
+ state_path.write_text(
3183
+ json.dumps(
3184
+ {
3185
+ "session_name": "team-old",
3186
+ "agents": {
3187
+ "fake_impl": {"status": "stopped", "provider": "fake", "window": "fake_impl"},
3188
+ "legacy_bad": None,
3189
+ },
3190
+ "tasks": [],
3191
+ }
3192
+ ),
3193
+ encoding="utf-8",
3194
+ )
3195
+ loaded = runtime._load_snapshot_state(state_path)
3196
+ self.assertIsNotNone(loaded)
3197
+ agent = loaded["agents"]["fake_impl"]
3198
+ for key in ["session_id", "rollout_path", "captured_at", "captured_via", "attribution_confidence", "spawn_cwd"]:
3199
+ self.assertIn(key, agent)
3200
+ self.assertIsNone(agent[key])
3201
+ self.assertIsNone(loaded["agents"]["legacy_bad"])
3202
+
3203
+ def test_snapshot_state_malformed_json_returns_none(self) -> None:
3204
+ with tempfile.TemporaryDirectory(prefix="team-agent-bad-snapshot-") as tmp:
3205
+ state_path = Path(tmp) / "state.json"
3206
+ state_path.write_text("{bad json", encoding="utf-8")
3207
+ self.assertIsNone(runtime._load_snapshot_state(state_path))
3208
+
3209
+ def test_session_state_normalization_ignores_non_dict_agents_container(self) -> None:
3210
+ with tempfile.TemporaryDirectory(prefix="team-agent-odd-agents-") as tmp:
3211
+ workspace = Path(tmp)
3212
+ state_path = runtime.runtime_state_path(workspace)
3213
+ state_path.parent.mkdir(parents=True)
3214
+ state_path.write_text(json.dumps({"session_name": "team-old", "agents": "legacy-bad", "tasks": []}), encoding="utf-8")
3215
+
3216
+ loaded = load_runtime_state(workspace)
3217
+ self.assertEqual(loaded["agents"], "legacy-bad")
3218
+
3219
+ snapshot = runtime._load_snapshot_state(state_path)
3220
+ self.assertIsNotNone(snapshot)
3221
+ self.assertEqual(snapshot["agents"], "legacy-bad")
3222
+
3223
+ def test_copy_session_metadata_copies_known_fields_only(self) -> None:
3224
+ full_source = {
3225
+ "session_id": "session-123",
3226
+ "rollout_path": "rollout.jsonl",
3227
+ "captured_at": "2026-05-16T00:00:00+00:00",
3228
+ "captured_via": "fs_watch",
3229
+ "attribution_confidence": "high",
3230
+ "spawn_cwd": "/tmp/workspace",
3231
+ "extra": "ignored",
3232
+ }
3233
+ target = {"preexisting": "kept"}
3234
+
3235
+ runtime._copy_session_metadata(target, full_source)
3236
+
3237
+ for key in runtime.SESSION_STATE_FIELDS:
3238
+ self.assertEqual(target[key], full_source[key])
3239
+ self.assertEqual(target["preexisting"], "kept")
3240
+ self.assertNotIn("extra", target)
3241
+
3242
+ missing_source = {"session_id": "session-456"}
3243
+ runtime._copy_session_metadata(target, missing_source)
3244
+
3245
+ self.assertEqual(target["session_id"], "session-456")
3246
+ for key in runtime.SESSION_STATE_FIELDS:
3247
+ if key != "session_id":
3248
+ self.assertIsNone(target[key])
3249
+
3250
+ def test_clear_session_capture_fields_preserves_spawn_cwd(self) -> None:
3251
+ target = {
3252
+ "session_id": "session-123",
3253
+ "rollout_path": "rollout.jsonl",
3254
+ "captured_at": "2026-05-16T00:00:00+00:00",
3255
+ "captured_via": "fs_watch",
3256
+ "attribution_confidence": "high",
3257
+ "spawn_cwd": "/tmp/workspace",
3258
+ }
3259
+
3260
+ runtime._clear_session_capture_fields(target)
3261
+
3262
+ for key in runtime.SESSION_CAPTURE_FIELDS:
3263
+ self.assertIsNone(target[key])
3264
+ self.assertEqual(target["spawn_cwd"], "/tmp/workspace")
3149
3265
 
3150
3266
  def test_claude_adapter_predetermines_session_id_and_resume_command(self) -> None:
3151
3267
  adapter = get_adapter("claude_code")
@@ -3249,6 +3365,65 @@ Handle fake tasks.
3249
3365
  self.assertTrue(any(e["event"] == "resume.session_unverified" for e in events))
3250
3366
  self.assertTrue(any(e["event"] == "resume.session_repaired" for e in events))
3251
3367
 
3368
+ def test_prepare_resume_state_copies_adapter_repaired_session_metadata(self) -> None:
3369
+ with tempfile.TemporaryDirectory(prefix="team-agent-adapter-repair-") as tmp:
3370
+ workspace = Path(tmp)
3371
+ event_log = runtime.EventLog(workspace)
3372
+ adapter = Mock()
3373
+ adapter.session_is_resumable.return_value = False
3374
+ adapter.recover_session_id.return_value = {
3375
+ "session_id": "repaired-session",
3376
+ "rollout_path": "repaired.jsonl",
3377
+ "captured_at": "2026-05-16T00:00:00+00:00",
3378
+ "captured_via": "fs_repair",
3379
+ "attribution_confidence": "high",
3380
+ "spawn_cwd": str(workspace / "repaired-cwd"),
3381
+ }
3382
+ previous = {
3383
+ "status": "stopped",
3384
+ "provider": "claude_code",
3385
+ "spawn_cwd": str(workspace / "old-cwd"),
3386
+ }
3387
+
3388
+ repaired = runtime._prepare_resume_state(workspace, "analyst", previous, adapter, event_log)
3389
+
3390
+ self.assertEqual(repaired["session_id"], "repaired-session")
3391
+ self.assertEqual(repaired["rollout_path"], "repaired.jsonl")
3392
+ self.assertEqual(repaired["captured_at"], "2026-05-16T00:00:00+00:00")
3393
+ self.assertEqual(repaired["captured_via"], "fs_repair")
3394
+ self.assertEqual(repaired["attribution_confidence"], "high")
3395
+ self.assertEqual(repaired["spawn_cwd"], str(workspace / "repaired-cwd"))
3396
+
3397
+ def test_prepare_resume_state_allow_fresh_clears_capture_fields_and_preserves_spawn_cwd(self) -> None:
3398
+ with tempfile.TemporaryDirectory(prefix="team-agent-resume-allow-fresh-") as tmp:
3399
+ workspace = Path(tmp)
3400
+ adapter = Mock()
3401
+ adapter.session_is_resumable.return_value = False
3402
+ adapter.recover_session_id.return_value = None
3403
+ previous = {
3404
+ "status": "stopped",
3405
+ "provider": "claude_code",
3406
+ "session_id": "stale-session",
3407
+ "rollout_path": "stale.jsonl",
3408
+ "captured_at": "2026-05-16T00:00:00+00:00",
3409
+ "captured_via": "fs_watch",
3410
+ "attribution_confidence": "high",
3411
+ "spawn_cwd": str(workspace / "old-cwd"),
3412
+ }
3413
+
3414
+ prepared = runtime._prepare_resume_state(
3415
+ workspace,
3416
+ "analyst",
3417
+ previous,
3418
+ adapter,
3419
+ runtime.EventLog(workspace),
3420
+ allow_fresh_on_resume_failure=True,
3421
+ )
3422
+
3423
+ for key in runtime.SESSION_CAPTURE_FIELDS:
3424
+ self.assertIsNone(prepared[key])
3425
+ self.assertEqual(prepared["spawn_cwd"], str(workspace / "old-cwd"))
3426
+
3252
3427
  def test_prepare_resume_state_repairs_claude_session_from_pending_isolated_id(self) -> None:
3253
3428
  adapter = get_adapter("claude_code")
3254
3429
  with tempfile.TemporaryDirectory(prefix="team-agent-claude-pending-repair-") as tmp:
@@ -3318,12 +3493,19 @@ Handle fake tasks.
3318
3493
  "spawn_cwd": str(workspace),
3319
3494
  "spawned_at": "2026-05-16T00:00:00+00:00",
3320
3495
  "claude_projects_root": str(projects_root),
3496
+ "_pending_session_id": "pending-session",
3321
3497
  }
3322
3498
  with patch("team_agent.runtime.get_adapter", return_value=adapter):
3323
3499
  result = runtime._capture_agent_session(workspace, "analyst", agent_state, runtime.EventLog(workspace), timeout_s=0)
3324
3500
 
3325
3501
  self.assertEqual(result["session_id"], "session-123")
3326
3502
  self.assertEqual(seen["claude_projects_root"], str(projects_root))
3503
+ for key in runtime.SESSION_STATE_FIELDS:
3504
+ self.assertEqual(agent_state[key], result[key])
3505
+ self.assertNotIn("_pending_session_id", agent_state)
3506
+ event = _events(workspace)[-1]
3507
+ self.assertEqual(event["event"], "session.captured")
3508
+ self.assertEqual(event["session_id"], "session-123")
3327
3509
 
3328
3510
  def test_startup_verify_rejects_window_that_disappears_immediately(self) -> None:
3329
3511
  with tempfile.TemporaryDirectory(prefix="team-agent-startup-stability-") as tmp:
@@ -3551,16 +3733,7 @@ Handle fake tasks.
3551
3733
  save_runtime_state(workspace, state)
3552
3734
 
3553
3735
  started_windows: set[str] = set()
3554
-
3555
- def fake_run_cmd(args: list[str], timeout: int = 20):
3556
- proc = Mock(returncode=1 if args[:2] == ["tmux", "has-session"] else 0, stdout="", stderr="")
3557
- if args[:3] == ["tmux", "new-session", "-d"]:
3558
- started_windows.add(args[6])
3559
- elif args[:2] == ["tmux", "new-window"]:
3560
- started_windows.add(args[5])
3561
- elif args[:3] == ["tmux", "list-windows", "-t"]:
3562
- proc.stdout = "\n".join(sorted(started_windows))
3563
- return proc
3736
+ fake_run_cmd = _make_fake_tmux_window_run_cmd(started_windows)
3564
3737
 
3565
3738
  with patch("team_agent.runtime.run_cmd", side_effect=fake_run_cmd), patch(
3566
3739
  "team_agent.runtime.start_coordinator", return_value={"ok": True, "pid": 321, "status": "started"}
@@ -3578,6 +3751,10 @@ Handle fake tasks.
3578
3751
  ):
3579
3752
  fresh = runtime.restart(workspace)
3580
3753
  self.assertEqual(fresh["agents"][0]["restart_mode"], "fresh")
3754
+ fresh_agent = load_runtime_state(workspace)["agents"]["fake_impl"]
3755
+ for key in runtime.SESSION_CAPTURE_FIELDS:
3756
+ self.assertIsNone(fresh_agent[key])
3757
+ self.assertEqual(fresh_agent["spawn_cwd"], str(workspace))
3581
3758
  self.assertTrue(any(e["event"] == "restart.fresh_spawn" for e in _events(workspace)))
3582
3759
 
3583
3760
  def test_restart_requires_team_selector_when_multiple_snapshots_exist(self) -> None:
@@ -3616,16 +3793,7 @@ Handle fake tasks.
3616
3793
  self.assertIn("team-beta", str(ctx.exception))
3617
3794
 
3618
3795
  started_windows: set[str] = set()
3619
-
3620
- def fake_run_cmd(args: list[str], timeout: int = 20):
3621
- proc = Mock(returncode=1 if args[:2] == ["tmux", "has-session"] else 0, stdout="", stderr="")
3622
- if args[:3] == ["tmux", "new-session", "-d"]:
3623
- started_windows.add(args[6])
3624
- elif args[:2] == ["tmux", "new-window"]:
3625
- started_windows.add(args[5])
3626
- elif args[:3] == ["tmux", "list-windows", "-t"]:
3627
- proc.stdout = "\n".join(sorted(started_windows))
3628
- return proc
3796
+ fake_run_cmd = _make_fake_tmux_window_run_cmd(started_windows)
3629
3797
 
3630
3798
  with patch("team_agent.runtime.run_cmd", side_effect=fake_run_cmd), patch(
3631
3799
  "team_agent.runtime.start_coordinator", return_value={"ok": True, "pid": 321, "status": "started"}
@@ -3663,16 +3831,7 @@ Handle fake tasks.
3663
3831
  )
3664
3832
  started_windows: set[str] = set()
3665
3833
  display_snapshots: list[set[str]] = []
3666
-
3667
- def fake_run_cmd(args: list[str], timeout: int = 20):
3668
- proc = Mock(returncode=1 if args[:2] == ["tmux", "has-session"] else 0, stdout="", stderr="")
3669
- if args[:3] == ["tmux", "new-session", "-d"]:
3670
- started_windows.add(args[6])
3671
- elif args[:2] == ["tmux", "new-window"]:
3672
- started_windows.add(args[5])
3673
- elif args[:3] == ["tmux", "list-windows", "-t"]:
3674
- proc.stdout = "\n".join(sorted(started_windows))
3675
- return proc
3834
+ fake_run_cmd = _make_fake_tmux_window_run_cmd(started_windows)
3676
3835
 
3677
3836
  def fake_open_display(workspace_arg, session_name, window_name, agent, event_log):
3678
3837
  display_snapshots.append(set(started_windows))
@@ -3892,6 +4051,10 @@ Handle fake tasks.
3892
4051
  self.assertTrue(result["ok"])
3893
4052
  self.assertEqual(result["start_mode"], "fresh")
3894
4053
  self.assertEqual(start_modes, ["resumed", "fresh"])
4054
+ agent_state = load_runtime_state(workspace)["agents"]["fake_impl"]
4055
+ for key in runtime.SESSION_CAPTURE_FIELDS:
4056
+ self.assertIsNone(agent_state[key])
4057
+ self.assertEqual(agent_state["spawn_cwd"], str(workspace))
3895
4058
  events = _events(workspace)
3896
4059
  self.assertTrue(any(e["event"] == "start_agent.window_missing_after_start" for e in events))
3897
4060
  self.assertTrue(any(e["event"] == "start_agent.resume_window_missing_fallback_fresh" for e in events))
@@ -5300,6 +5463,20 @@ def _fake_spec(workspace: Path) -> dict:
5300
5463
  return spec
5301
5464
 
5302
5465
 
5466
+ def _make_fake_tmux_window_run_cmd(started_windows: set[str]):
5467
+ def fake_run_cmd(args: list[str], timeout: int = 20):
5468
+ proc = Mock(returncode=1 if args[:2] == ["tmux", "has-session"] else 0, stdout="", stderr="")
5469
+ if args[:3] == ["tmux", "new-session", "-d"]:
5470
+ started_windows.add(args[6])
5471
+ elif args[:2] == ["tmux", "new-window"]:
5472
+ started_windows.add(args[5])
5473
+ elif args[:3] == ["tmux", "list-windows", "-t"]:
5474
+ proc.stdout = "\n".join(sorted(started_windows))
5475
+ return proc
5476
+
5477
+ return fake_run_cmd
5478
+
5479
+
5303
5480
  def _write_doc_team(workspace: Path) -> Path:
5304
5481
  team = workspace / ".team" / "current"
5305
5482
  (team / "agents").mkdir(parents=True, exist_ok=True)