swarph-cli 0.13.4__tar.gz → 0.13.6__tar.gz

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 (114) hide show
  1. {swarph_cli-0.13.4/src/swarph_cli.egg-info → swarph_cli-0.13.6}/PKG-INFO +1 -1
  2. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/pyproject.toml +1 -1
  3. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/__init__.py +1 -1
  4. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/spawn.py +7 -0
  5. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/watchdog.py +59 -9
  6. {swarph_cli-0.13.4 → swarph_cli-0.13.6/src/swarph_cli.egg-info}/PKG-INFO +1 -1
  7. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_spawn_command.py +12 -0
  8. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_watchdog.py +60 -0
  9. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/LICENSE +0 -0
  10. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/README.md +0 -0
  11. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/setup.cfg +0 -0
  12. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/caller.py +0 -0
  13. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/capture/__init__.py +0 -0
  14. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/capture/harden.py +0 -0
  15. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/capture/lineage.py +0 -0
  16. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/capture/liveness.py +0 -0
  17. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/capture/manifest.py +0 -0
  18. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/capture/paths.py +0 -0
  19. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/capture/verify.py +0 -0
  20. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/cell.py +0 -0
  21. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/__init__.py +0 -0
  22. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/add.py +0 -0
  23. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/cell.py +0 -0
  24. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/chat.py +0 -0
  25. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/compress.py +0 -0
  26. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/daemon.py +0 -0
  27. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/hook_output.py +0 -0
  28. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/hooks.py +0 -0
  29. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/import_session.py +0 -0
  30. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/init.py +0 -0
  31. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/install_hook.py +0 -0
  32. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/mcp_server.py +0 -0
  33. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/memory_sync.py +0 -0
  34. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/mesh.py +0 -0
  35. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/onboard.py +0 -0
  36. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/protocol_handler.py +0 -0
  37. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/ratify.py +0 -0
  38. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/commands/security.py +0 -0
  39. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/compress/__init__.py +0 -0
  40. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/compress/levers.py +0 -0
  41. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/compress/marker.py +0 -0
  42. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/compress/verify.py +0 -0
  43. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/main.py +0 -0
  44. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/parsers/__init__.py +0 -0
  45. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/parsers/claude.py +0 -0
  46. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/systemd/swarph-watchdog.default +0 -0
  47. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/systemd/swarph-watchdog.service +0 -0
  48. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli/systemd/swarph-watchdog.timer +0 -0
  49. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli.egg-info/SOURCES.txt +0 -0
  50. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli.egg-info/dependency_links.txt +0 -0
  51. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli.egg-info/entry_points.txt +0 -0
  52. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli.egg-info/requires.txt +0 -0
  53. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/src/swarph_cli.egg-info/top_level.txt +0 -0
  54. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_add_security_gate.py +0 -0
  55. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_artifact_add.py +0 -0
  56. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_artifact_hash.py +0 -0
  57. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_artifact_lib.py +0 -0
  58. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_artifact_mcp.py +0 -0
  59. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_artifact_skill.py +0 -0
  60. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_artifact_tool.py +0 -0
  61. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_artifact_uri.py +0 -0
  62. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_caller_meta_guard.py +0 -0
  63. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_capture_lineage.py +0 -0
  64. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_capture_liveness.py +0 -0
  65. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_capture_manifest.py +0 -0
  66. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_capture_paths.py +0 -0
  67. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_capture_security.py +0 -0
  68. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_cell_command_dispatch.py +0 -0
  69. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_cell_harden.py +0 -0
  70. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_cell_loader.py +0 -0
  71. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_cell_verify.py +0 -0
  72. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_chat_command.py +0 -0
  73. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_claude_parser.py +0 -0
  74. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_claude_tmux_template.py +0 -0
  75. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_compress_caller_convention.py +0 -0
  76. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_compress_command.py +0 -0
  77. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_compress_levers.py +0 -0
  78. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_compress_marker.py +0 -0
  79. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_compress_verify.py +0 -0
  80. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_daemon_command.py +0 -0
  81. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_feature_to_uri.py +0 -0
  82. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_hook_output.py +0 -0
  83. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_hooks_add.py +0 -0
  84. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_hooks_bundle.py +0 -0
  85. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_hooks_init.py +0 -0
  86. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_hooks_lifecycle.py +0 -0
  87. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_hooks_merge.py +0 -0
  88. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_import_command.py +0 -0
  89. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_init_command.py +0 -0
  90. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_install_hook.py +0 -0
  91. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_main.py +0 -0
  92. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_mcp_server.py +0 -0
  93. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_memory_sync.py +0 -0
  94. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_mesh_command.py +0 -0
  95. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_mesh_sidecar.py +0 -0
  96. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_onboard_command.py +0 -0
  97. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_protocol_handler.py +0 -0
  98. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_ratify_command.py +0 -0
  99. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_repl_caller_convention.py +0 -0
  100. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_security.py +0 -0
  101. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_session_path_role_gate.py +0 -0
  102. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_smoke_chat.py +0 -0
  103. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_smoke_one_shot.py +0 -0
  104. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_smoke_phase_5_5.py +0 -0
  105. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_spawn_live_pin.py +0 -0
  106. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_spawn_mitosis_lineage.py +0 -0
  107. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_spawn_tmux_session.py +0 -0
  108. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_spawn_windows_relaunch.py +0 -0
  109. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_version_consistency.py +0 -0
  110. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_watchdog_dm_wake.py +0 -0
  111. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_watchdog_dm_wake_cooldown.py +0 -0
  112. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_watchdog_dm_wake_wiring.py +0 -0
  113. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_watchdog_model_rung.py +0 -0
  114. {swarph_cli-0.13.4 → swarph_cli-0.13.6}/tests/test_watchdog_stale_peers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.13.4
3
+ Version: 0.13.6
4
4
  Summary: The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider `swarph spawn` (claude/codex/antigravity per cell.provider via a ProviderMembrane + subprocess billing-scrub), interactive `swarph init`, `swarph mesh` (send/inbox/register) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.5: foolproof Windows auto-relaunch (genuine-WT detection via process ancestry, not WT_SESSION).
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "swarph-cli"
7
- version = "0.13.4"
7
+ version = "0.13.6"
8
8
  description = "The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider `swarph spawn` (claude/codex/antigravity per cell.provider via a ProviderMembrane + subprocess billing-scrub), interactive `swarph init`, `swarph mesh` (send/inbox/register) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.5: foolproof Windows auto-relaunch (genuine-WT detection via process ancestry, not WT_SESSION)."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -16,6 +16,6 @@ The architecture splits CLI from substrate so:
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
- __version__ = "0.13.4"
19
+ __version__ = "0.13.6"
20
20
 
21
21
  __all__ = ["__version__"]
@@ -418,6 +418,13 @@ def _claude_env() -> dict[str, str]:
418
418
  """
419
419
  env = scrub_env_for_subprocess()
420
420
  env["SWARPH_SPAWN"] = "1"
421
+ # Disable the Claude Code in-session rating survey ("How is Claude doing this
422
+ # session?"). On a headless/automated cell it pops up as a modal that the
423
+ # wake-injector refuses to type into, so it stalls scheduled wakes
424
+ # indefinitely (a survey deferred the weekly newsletter 2+ hours — see
425
+ # feedback_modal_stalls_cell_wake). Suppresses ONLY the survey; does NOT
426
+ # touch telemetry / auto-update / error reporting.
427
+ env["CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY"] = "1"
421
428
  return env
422
429
 
423
430
 
@@ -259,6 +259,31 @@ def _resolve_cursor_path(
259
259
  return Path("/tmp/lab-claude-cursor.json")
260
260
 
261
261
 
262
+ def _resolve_activity_marker_path(
263
+ role: str,
264
+ explicit: Optional[str],
265
+ cell_yaml_value: Optional[str] = None,
266
+ ) -> Path:
267
+ """Resolve the turn-activity marker — the Stop-hook touches it every turn-end.
268
+
269
+ Precedence: ``--activity-marker`` > ``cell.yaml`` extra.activity_marker_path
270
+ > ``$TMPDIR/<role>-claude-active.txt``.
271
+
272
+ Unlike the cursor (often the inbox-DRAIN cursor, touched only on drain and so
273
+ STALE during active non-draining work → false-fire,
274
+ feedback_watchdog_liveness_proxy), this marker tracks real turn activity. The
275
+ watchdog uses the FRESHEST mtime of {cursor, marker} as the effective
276
+ last-activity, so a fresh marker rescues a stale drain-cursor and an absent
277
+ marker harmlessly falls back to the cursor.
278
+ """
279
+ if explicit:
280
+ return Path(explicit).expanduser()
281
+ if cell_yaml_value:
282
+ return Path(cell_yaml_value).expanduser()
283
+ tmpdir = os.environ.get("TMPDIR", "/tmp")
284
+ return Path(tmpdir) / f"{role}-claude-active.txt"
285
+
286
+
262
287
  def _resolve_tmux_session(
263
288
  role: str,
264
289
  explicit: Optional[str],
@@ -283,11 +308,14 @@ def _resolve_tmux_session(
283
308
  return role
284
309
 
285
310
 
286
- def _read_cell_yaml_pins(role: str) -> tuple[Optional[str], Optional[str]]:
287
- """Best-effort read of cell.yaml extra.cursor_path + extra.tmux_session.
311
+ def _read_cell_yaml_pins(
312
+ role: str,
313
+ ) -> tuple[Optional[str], Optional[str], Optional[str]]:
314
+ """Best-effort read of cell.yaml extra.cursor_path + extra.tmux_session +
315
+ extra.activity_marker_path.
288
316
 
289
317
  Tries the cwd-local ``./cell.yaml`` first (matches hook_output discovery),
290
- falls back to ``<cells_dir>/<role>.yaml``. Returns (None, None) on any
318
+ falls back to ``<cells_dir>/<role>.yaml``. Returns (None, None, None) on any
291
319
  failure — F4 is additive non-breaking, malformed cell.yaml falls through
292
320
  to the legacy convention defaults.
293
321
 
@@ -311,19 +339,21 @@ def _read_cell_yaml_pins(role: str) -> tuple[Optional[str], Optional[str]]:
311
339
  if candidate.is_file():
312
340
  cell_path = candidate
313
341
  if cell_path is None:
314
- return None, None
342
+ return None, None, None
315
343
 
316
344
  try:
317
345
  cell = load_cell(cell_path)
318
346
  except (CellError, OSError):
319
- return None, None
347
+ return None, None, None
320
348
 
321
349
  extra = cell.extra or {}
322
350
  cursor_path = extra.get("cursor_path")
323
351
  tmux_session = extra.get("tmux_session")
352
+ activity_marker = extra.get("activity_marker_path")
324
353
  return (
325
354
  str(cursor_path) if cursor_path else None,
326
355
  str(tmux_session) if tmux_session else None,
356
+ str(activity_marker) if activity_marker else None,
327
357
  )
328
358
 
329
359
 
@@ -1158,7 +1188,7 @@ def _run_local_check(args: argparse.Namespace) -> int:
1158
1188
  # `extra.tmux_session` when --cell is provided; explicit CLI args still
1159
1189
  # win. Best-effort: malformed cell.yaml falls through to legacy
1160
1190
  # convention defaults (additive non-breaking).
1161
- cell_cursor, cell_tmux = _read_cell_yaml_pins(role)
1191
+ cell_cursor, cell_tmux, cell_activity = _read_cell_yaml_pins(role)
1162
1192
  cursor = _resolve_cursor_path(role, args.cursor, cell_cursor)
1163
1193
  tmux_session = _resolve_tmux_session(role, args.tmux_session, cell_tmux)
1164
1194
  log_path = _resolve_log_path(args.log)
@@ -1180,10 +1210,22 @@ def _run_local_check(args: argparse.Namespace) -> int:
1180
1210
  "cell_yaml_pinned_tmux": cell_tmux is not None,
1181
1211
  }
1182
1212
 
1183
- # PRIMARY signal: cursor file mtime
1184
- cursor_mtime = _stat_mtime(cursor)
1213
+ # PRIMARY signal: liveness = FRESHEST mtime of {drain-cursor, turn-activity
1214
+ # marker}. The cursor is often the inbox-DRAIN cursor (touched only on drain)
1215
+ # → it goes stale during active non-draining work and false-fires recovery
1216
+ # (feedback_watchdog_liveness_proxy). The Stop-hook marker is touched every
1217
+ # turn-end. A fresh marker rescues a stale cursor; an absent/unreadable marker
1218
+ # harmlessly falls back to the cursor (no change for cells without it).
1219
+ activity_marker = _resolve_activity_marker_path(
1220
+ role, getattr(args, "activity_marker", None), cell_activity
1221
+ )
1222
+ diag["activity_marker"] = str(activity_marker)
1223
+ _live_mtimes = [
1224
+ m for m in (_stat_mtime(cursor), _stat_mtime(activity_marker)) if m is not None
1225
+ ]
1226
+ cursor_mtime = max(_live_mtimes) if _live_mtimes else None
1185
1227
  if cursor_mtime is None:
1186
- diag["error"] = f"cursor file unreadable: {cursor}"
1228
+ diag["error"] = f"cursor file unreadable: {cursor} (and marker {activity_marker})"
1187
1229
  _log_event(log_path, "error", diag, verbose)
1188
1230
  return 3
1189
1231
  cursor_age = _now() - cursor_mtime
@@ -1660,6 +1702,14 @@ def _build_parser() -> argparse.ArgumentParser:
1660
1702
  )
1661
1703
  p.add_argument("--cell", default=os.environ.get("SWARPH_CELL", "lab"))
1662
1704
  p.add_argument("--cursor", default=None)
1705
+ p.add_argument(
1706
+ "--activity-marker", default=None,
1707
+ help="Turn-activity marker path (the Stop-hook touches it every "
1708
+ "turn-end). The freshest mtime of {cursor, marker} is the "
1709
+ "effective liveness, so an actively-working-but-not-draining cell "
1710
+ "isn't false-judged dark. Default $TMPDIR/<role>-claude-active.txt; "
1711
+ "cell.yaml extra.activity_marker_path also honored.",
1712
+ )
1663
1713
  p.add_argument("--threshold", type=int, default=_DEFAULT_THRESHOLD_SEC)
1664
1714
  p.add_argument(
1665
1715
  "--pane-activity-threshold",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swarph-cli
3
- Version: 0.13.4
3
+ Version: 0.13.6
4
4
  Summary: The `swarph` binary — multi-LLM CLI + mesh-gateway integration: multi-provider `swarph spawn` (claude/codex/antigravity per cell.provider via a ProviderMembrane + subprocess billing-scrub), interactive `swarph init`, `swarph mesh` (send/inbox/register) + inbox sidecar, `assisted_memory` (git-backed durable memory), session import, watchdog. v0.9.5: foolproof Windows auto-relaunch (genuine-WT detection via process ancestry, not WT_SESSION).
5
5
  Author: Pierre Samson, Claude Opus
6
6
  License: MIT
@@ -1343,3 +1343,15 @@ def test_run_spawn_grok_assisted_memory_no_double_system_prompt(
1343
1343
  assert "you are grok-researcher." in argv[argv.index("--system-prompt-override") + 1]
1344
1344
  assert "--rules" in argv # CURRENT_TASK injected here, not as a 2nd override
1345
1345
  assert "Active task body" in argv[argv.index("--rules") + 1]
1346
+
1347
+
1348
+ def test_claude_env_disables_feedback_survey(monkeypatch):
1349
+ """The Claude Code session-rating survey ("How is Claude doing this
1350
+ session?") is a modal that stalls a headless cell's scheduled wake — the
1351
+ wake-injector refuses to type into a modal, so a survey deferred the weekly
1352
+ newsletter 2+ hours (feedback_modal_stalls_cell_wake). Spawned claude cells
1353
+ disable it at the source via env."""
1354
+ monkeypatch.setenv("PATH", "/usr/bin:/bin")
1355
+ from swarph_cli.commands.spawn import _claude_env
1356
+ env = _claude_env()
1357
+ assert env.get("CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY") == "1"
@@ -23,6 +23,7 @@ _POSIX_WATCHDOG_SKIP = pytest.mark.skipif(
23
23
 
24
24
  from swarph_cli.commands.watchdog import (
25
25
  _DEFAULT_THRESHOLD_SEC,
26
+ _resolve_activity_marker_path,
26
27
  _resolve_cursor_path,
27
28
  _resolve_log_path,
28
29
  run_watchdog,
@@ -871,3 +872,62 @@ def test_pane_activity_age_takes_most_recent(monkeypatch):
871
872
  lambda cmd, **kw: _R(0, f"|{now - 500}|{now - 10}\n"))
872
873
  age = _wd._pane_activity_age_sec("lab")
873
874
  assert age is not None and age <= 20
875
+
876
+
877
+ # ---------------------------------------------------------------------------
878
+ # Liveness generalization — turn-activity marker as a second signal
879
+ # (feedback_watchdog_liveness_proxy): the drain-cursor goes stale during active
880
+ # non-draining work; the Stop-hook active.txt (touched every turn-end) rescues it.
881
+ # ---------------------------------------------------------------------------
882
+
883
+
884
+ def test_resolve_activity_marker_explicit_wins(isolated_state):
885
+ p = _resolve_activity_marker_path("lab", "/x/y.txt", "/cell/z.txt")
886
+ assert str(p) == "/x/y.txt"
887
+
888
+
889
+ def test_resolve_activity_marker_cell_yaml_beats_default(isolated_state):
890
+ p = _resolve_activity_marker_path("lab", None, "/cell/z.txt")
891
+ assert str(p) == "/cell/z.txt"
892
+
893
+
894
+ def test_resolve_activity_marker_default_is_role_active_in_tmpdir(isolated_state):
895
+ p = _resolve_activity_marker_path("lab", None, None)
896
+ assert p == isolated_state / "lab-claude-active.txt"
897
+
898
+
899
+ def test_stale_cursor_but_fresh_activity_marker_returns_noop(isolated_state, stale_cursor):
900
+ # Drain-cursor is stale (1hr) BUT the Stop-hook turn-activity marker at the
901
+ # DEFAULT path is fresh → freshest-of-both is fresh → healthy noop, NO false A1.
902
+ marker = isolated_state / "lab-claude-active.txt"
903
+ marker.write_text("") # current mtime
904
+ with patch("swarph_cli.commands.watchdog._process_alive", return_value=True), \
905
+ patch("swarph_cli.commands.watchdog._gateway_unread_count", return_value=3), \
906
+ patch("swarph_cli.commands.watchdog._tmux_session_exists", return_value=True), \
907
+ patch("swarph_cli.commands.watchdog._pane_activity_age_sec", return_value=None):
908
+ rc = run_watchdog(argv=[
909
+ "--check", "--cell", "lab",
910
+ "--cursor", str(stale_cursor),
911
+ "--threshold", "60",
912
+ ])
913
+ assert rc == 0 # marker freshness rescues the stale cursor
914
+
915
+
916
+ def test_stale_cursor_and_stale_marker_still_fires_a1(isolated_state, stale_cursor):
917
+ # Both stale → unchanged legacy behavior (A1). Absent/stale marker is harmless.
918
+ import os as _os
919
+ marker = isolated_state / "lab-claude-active.txt"
920
+ marker.write_text("")
921
+ one_hour_ago = time.time() - 3600
922
+ _os.utime(marker, (one_hour_ago, one_hour_ago))
923
+ with patch("swarph_cli.commands.watchdog._process_alive", return_value=True), \
924
+ patch("swarph_cli.commands.watchdog._gateway_unread_count", return_value=3), \
925
+ patch("swarph_cli.commands.watchdog._tmux_session_exists", return_value=True), \
926
+ patch("swarph_cli.commands.watchdog._pane_activity_age_sec", return_value=None), \
927
+ patch("swarph_cli.commands.watchdog._tmux_send_keys", return_value=True):
928
+ rc = run_watchdog(argv=[
929
+ "--check", "--cell", "lab",
930
+ "--cursor", str(stale_cursor),
931
+ "--threshold", "60",
932
+ ])
933
+ assert rc == 1 # both signals dark → A1 (unchanged)
File without changes
File without changes
File without changes