@jstn-sdk/rcs 0.1.0 → 0.1.6

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 (1739) hide show
  1. package/README.md +154 -101
  2. package/dist/agents/definitions.d.ts.map +1 -1
  3. package/dist/agents/definitions.js +0 -101
  4. package/dist/agents/definitions.js.map +1 -1
  5. package/dist/blueprint/runtime.d.ts +52 -0
  6. package/dist/blueprint/runtime.d.ts.map +1 -0
  7. package/dist/{ralplan → blueprint}/runtime.js +19 -19
  8. package/dist/blueprint/runtime.js.map +1 -0
  9. package/dist/catalog/reader.d.ts.map +1 -1
  10. package/dist/catalog/reader.js +8 -2
  11. package/dist/catalog/reader.js.map +1 -1
  12. package/dist/catalog/schema.js +1 -1
  13. package/dist/catalog/schema.js.map +1 -1
  14. package/dist/cli/forge.d.ts +17 -0
  15. package/dist/cli/{ralph.d.ts.map → forge.d.ts.map} +1 -1
  16. package/dist/cli/{ralph.js → forge.js} +82 -82
  17. package/dist/cli/{ralph.js.map → forge.js.map} +1 -1
  18. package/dist/cli/index.d.ts +1 -1
  19. package/dist/cli/index.d.ts.map +1 -1
  20. package/dist/cli/index.js +20 -15
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/cli/setup.d.ts.map +1 -1
  23. package/dist/cli/setup.js +2 -3
  24. package/dist/cli/setup.js.map +1 -1
  25. package/dist/cli/star-prompt.js +2 -2
  26. package/dist/cli/star-prompt.js.map +1 -1
  27. package/dist/cli/state.js +1 -1
  28. package/dist/cli/team.d.ts.map +1 -1
  29. package/dist/cli/team.js +3 -2
  30. package/dist/cli/team.js.map +1 -1
  31. package/dist/cli/tmux-hook.d.ts.map +1 -1
  32. package/dist/cli/tmux-hook.js +9 -1
  33. package/dist/cli/tmux-hook.js.map +1 -1
  34. package/dist/config/generator.d.ts +1 -1
  35. package/dist/config/generator.d.ts.map +1 -1
  36. package/dist/config/generator.js +1 -1
  37. package/dist/config/generator.js.map +1 -1
  38. package/dist/forge/contract.d.ts +17 -0
  39. package/dist/{ralph → forge}/contract.d.ts.map +1 -1
  40. package/dist/{ralph → forge}/contract.js +16 -16
  41. package/dist/{ralph → forge}/contract.js.map +1 -1
  42. package/dist/{ralph → forge}/persistence.d.ts +5 -5
  43. package/dist/{ralph → forge}/persistence.d.ts.map +1 -1
  44. package/dist/{ralph → forge}/persistence.js +7 -6
  45. package/dist/forge/persistence.js.map +1 -0
  46. package/dist/hooks/agents-overlay.d.ts +1 -1
  47. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  48. package/dist/hooks/agents-overlay.js +37 -31
  49. package/dist/hooks/agents-overlay.js.map +1 -1
  50. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  51. package/dist/hooks/extensibility/dispatcher.js +82 -14
  52. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  53. package/dist/hooks/keyword-detector.d.ts +8 -8
  54. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  55. package/dist/hooks/keyword-detector.js +94 -64
  56. package/dist/hooks/keyword-detector.js.map +1 -1
  57. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  58. package/dist/hooks/keyword-registry.js +9 -11
  59. package/dist/hooks/keyword-registry.js.map +1 -1
  60. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  61. package/dist/hooks/prompt-guidance-contract.js +10 -21
  62. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  63. package/dist/hooks/task-size-detector.js +2 -2
  64. package/dist/hooks/task-size-detector.js.map +1 -1
  65. package/dist/hooks/triage-state.d.ts +1 -1
  66. package/dist/hooks/triage-state.js +1 -1
  67. package/dist/hud/colors.d.ts +2 -2
  68. package/dist/hud/colors.js +2 -2
  69. package/dist/hud/render.js +21 -21
  70. package/dist/hud/render.js.map +1 -1
  71. package/dist/hud/state.d.ts +3 -3
  72. package/dist/hud/state.d.ts.map +1 -1
  73. package/dist/hud/state.js +18 -15
  74. package/dist/hud/state.js.map +1 -1
  75. package/dist/hud/types.d.ts +6 -6
  76. package/dist/hud/types.d.ts.map +1 -1
  77. package/dist/mcp/bootstrap.d.ts.map +1 -1
  78. package/dist/mcp/bootstrap.js +36 -2
  79. package/dist/mcp/bootstrap.js.map +1 -1
  80. package/dist/mcp/state-paths.d.ts +1 -0
  81. package/dist/mcp/state-paths.d.ts.map +1 -1
  82. package/dist/mcp/state-paths.js +4 -1
  83. package/dist/mcp/state-paths.js.map +1 -1
  84. package/dist/mcp/state-server.d.ts +4 -4
  85. package/dist/mcp/state-server.js +2 -2
  86. package/dist/mcp/state-server.js.map +1 -1
  87. package/dist/modes/base.d.ts +2 -2
  88. package/dist/modes/base.d.ts.map +1 -1
  89. package/dist/modes/base.js +29 -26
  90. package/dist/modes/base.js.map +1 -1
  91. package/dist/notifications/reply-listener.d.ts.map +1 -1
  92. package/dist/notifications/reply-listener.js +7 -1
  93. package/dist/notifications/reply-listener.js.map +1 -1
  94. package/dist/notifications/tmux.d.ts.map +1 -1
  95. package/dist/notifications/tmux.js +39 -6
  96. package/dist/notifications/tmux.js.map +1 -1
  97. package/dist/pipeline/index.d.ts +7 -6
  98. package/dist/pipeline/index.d.ts.map +1 -1
  99. package/dist/pipeline/index.js +5 -4
  100. package/dist/pipeline/index.js.map +1 -1
  101. package/dist/pipeline/orchestrator.d.ts +5 -5
  102. package/dist/pipeline/orchestrator.js +25 -25
  103. package/dist/pipeline/orchestrator.js.map +1 -1
  104. package/dist/pipeline/stages/blueprint.d.ts +25 -0
  105. package/dist/pipeline/stages/blueprint.d.ts.map +1 -0
  106. package/dist/pipeline/stages/{ralplan.js → blueprint.js} +16 -16
  107. package/dist/pipeline/stages/blueprint.js.map +1 -0
  108. package/dist/pipeline/stages/code-review.d.ts +2 -2
  109. package/dist/pipeline/stages/code-review.js +6 -6
  110. package/dist/pipeline/stages/code-review.js.map +1 -1
  111. package/dist/pipeline/stages/forge-verify.d.ts +50 -0
  112. package/dist/pipeline/stages/forge-verify.d.ts.map +1 -0
  113. package/dist/pipeline/stages/{ralph-verify.js → forge-verify.js} +21 -24
  114. package/dist/pipeline/stages/forge-verify.js.map +1 -0
  115. package/dist/pipeline/stages/team-exec.d.ts +1 -1
  116. package/dist/pipeline/stages/team-exec.js +19 -19
  117. package/dist/pipeline/stages/team-exec.js.map +1 -1
  118. package/dist/pipeline/types.d.ts +12 -12
  119. package/dist/pipeline/types.d.ts.map +1 -1
  120. package/dist/pipeline/types.js +1 -1
  121. package/dist/planning/artifacts.d.ts +3 -4
  122. package/dist/planning/artifacts.d.ts.map +1 -1
  123. package/dist/planning/artifacts.js +2 -3
  124. package/dist/planning/artifacts.js.map +1 -1
  125. package/dist/question/policy.js +1 -1
  126. package/dist/runtime/bridge.d.ts.map +1 -1
  127. package/dist/runtime/bridge.js +70 -13
  128. package/dist/runtime/bridge.js.map +1 -1
  129. package/dist/scripts/codex-native-hook.js +30 -30
  130. package/dist/scripts/codex-native-hook.js.map +1 -1
  131. package/dist/scripts/eval/eval-cross-server-party-flow.d.ts +3 -0
  132. package/dist/scripts/eval/eval-cross-server-party-flow.d.ts.map +1 -0
  133. package/dist/scripts/eval/eval-cross-server-party-flow.js +12 -0
  134. package/dist/scripts/eval/eval-cross-server-party-flow.js.map +1 -0
  135. package/dist/scripts/eval/eval-gui-onboarding-clarity.d.ts +3 -0
  136. package/dist/scripts/eval/eval-gui-onboarding-clarity.d.ts.map +1 -0
  137. package/dist/scripts/eval/eval-gui-onboarding-clarity.js +17 -0
  138. package/dist/scripts/eval/eval-gui-onboarding-clarity.js.map +1 -0
  139. package/dist/scripts/eval/eval-liveops-reward-loop-balance.d.ts +3 -0
  140. package/dist/scripts/eval/eval-liveops-reward-loop-balance.d.ts.map +1 -0
  141. package/dist/scripts/eval/eval-liveops-reward-loop-balance.js +12 -0
  142. package/dist/scripts/eval/eval-liveops-reward-loop-balance.js.map +1 -0
  143. package/dist/scripts/eval/eval-profile-datastore-recovery.d.ts +3 -0
  144. package/dist/scripts/eval/eval-profile-datastore-recovery.d.ts.map +1 -0
  145. package/dist/scripts/eval/eval-profile-datastore-recovery.js +17 -0
  146. package/dist/scripts/eval/eval-profile-datastore-recovery.js.map +1 -0
  147. package/dist/scripts/eval/eval-remote-contract-hardening.d.ts +3 -0
  148. package/dist/scripts/eval/eval-remote-contract-hardening.d.ts.map +1 -0
  149. package/dist/scripts/eval/eval-remote-contract-hardening.js +17 -0
  150. package/dist/scripts/eval/eval-remote-contract-hardening.js.map +1 -0
  151. package/dist/scripts/notify-fallback-watcher.js +140 -139
  152. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  153. package/dist/scripts/notify-hook/forge-session-resume.d.ts +23 -0
  154. package/dist/scripts/notify-hook/{ralph-session-resume.d.ts.map → forge-session-resume.d.ts.map} +1 -1
  155. package/dist/scripts/notify-hook/{ralph-session-resume.js → forge-session-resume.js} +37 -36
  156. package/dist/scripts/notify-hook/{ralph-session-resume.js.map → forge-session-resume.js.map} +1 -1
  157. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  158. package/dist/scripts/notify-hook/team-dispatch.js +34 -4
  159. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  160. package/dist/scripts/notify-hook/visual-verdict.js +3 -3
  161. package/dist/scripts/notify-hook.js +9 -9
  162. package/dist/scripts/run-test-files.js +1 -1
  163. package/dist/scripts/run-test-files.js.map +1 -1
  164. package/dist/scripts/surface-taxonomy.d.ts +23 -0
  165. package/dist/scripts/surface-taxonomy.d.ts.map +1 -0
  166. package/dist/scripts/surface-taxonomy.js +271 -0
  167. package/dist/scripts/surface-taxonomy.js.map +1 -0
  168. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  169. package/dist/scripts/sync-plugin-mirror.js +5 -4
  170. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  171. package/dist/scripts/tmux-hook-engine.d.ts +1 -1
  172. package/dist/scripts/tmux-hook-engine.d.ts.map +1 -1
  173. package/dist/scripts/tmux-hook-engine.js +29 -20
  174. package/dist/scripts/tmux-hook-engine.js.map +1 -1
  175. package/dist/state/operations.d.ts +1 -1
  176. package/dist/state/operations.d.ts.map +1 -1
  177. package/dist/state/operations.js +18 -18
  178. package/dist/state/operations.js.map +1 -1
  179. package/dist/state/skill-active.d.ts +13 -1
  180. package/dist/state/skill-active.d.ts.map +1 -1
  181. package/dist/state/skill-active.js +38 -17
  182. package/dist/state/skill-active.js.map +1 -1
  183. package/dist/state/workflow-transition.d.ts +6 -5
  184. package/dist/state/workflow-transition.d.ts.map +1 -1
  185. package/dist/state/workflow-transition.js +27 -15
  186. package/dist/state/workflow-transition.js.map +1 -1
  187. package/dist/team/contracts.d.ts +1 -1
  188. package/dist/team/contracts.js +2 -2
  189. package/dist/team/followup-planner.d.ts +2 -2
  190. package/dist/team/followup-planner.d.ts.map +1 -1
  191. package/dist/team/followup-planner.js +16 -14
  192. package/dist/team/followup-planner.js.map +1 -1
  193. package/dist/team/idle-nudge.d.ts.map +1 -1
  194. package/dist/team/idle-nudge.js +3 -2
  195. package/dist/team/idle-nudge.js.map +1 -1
  196. package/dist/team/leader-activity.js +1 -1
  197. package/dist/team/model-contract.d.ts.map +1 -1
  198. package/dist/team/model-contract.js +4 -1
  199. package/dist/team/model-contract.js.map +1 -1
  200. package/dist/team/orchestrator.js +4 -4
  201. package/dist/team/orchestrator.js.map +1 -1
  202. package/dist/team/role-router.js +3 -3
  203. package/dist/team/role-router.js.map +1 -1
  204. package/dist/team/state/dispatch.d.ts.map +1 -1
  205. package/dist/team/state/dispatch.js +4 -1
  206. package/dist/team/state/dispatch.js.map +1 -1
  207. package/dist/team/tmux-session.d.ts +4 -0
  208. package/dist/team/tmux-session.d.ts.map +1 -1
  209. package/dist/team/tmux-session.js +42 -9
  210. package/dist/team/tmux-session.js.map +1 -1
  211. package/dist/team/worktree.d.ts +1 -1
  212. package/dist/utils/platform-command.d.ts.map +1 -1
  213. package/dist/utils/platform-command.js +9 -0
  214. package/dist/utils/platform-command.js.map +1 -1
  215. package/dist/verification/verifier.d.ts +1 -1
  216. package/dist/verification/verifier.js +2 -2
  217. package/docs/STATE_MODEL.md +24 -24
  218. package/docs/agents.html +8 -16
  219. package/docs/archive/README.md +15 -0
  220. package/docs/{prompt-migration-changelog.md → archive/prompt-migration-changelog.md} +0 -11
  221. package/docs/{release-body-0.9.0.md → archive/release-body-0.9.0.md} +6 -24
  222. package/docs/{release-body-0.9.1.md → archive/release-body-0.9.1.md} +3 -3
  223. package/docs/codex-native-hooks.md +4 -4
  224. package/docs/contracts/forge-cancel-contract.md +20 -0
  225. package/docs/contracts/forge-state-contract.md +52 -0
  226. package/docs/contracts/multi-state-transition-contract.md +5 -5
  227. package/docs/contracts/multi-state-transition-review.md +3 -3
  228. package/docs/contracts/repo-aware-team-dag-decomposition.md +1 -1
  229. package/docs/contracts/rust-runtime-thin-adapter-contract.md +1 -1
  230. package/docs/contracts/team-startup-dispatch-latency.md +1 -1
  231. package/docs/getting-started.html +11 -1
  232. package/docs/guidance-schema.md +6 -3
  233. package/docs/index.html +55 -4
  234. package/docs/integrations.html +4 -3
  235. package/docs/issues/team-forge-followup-team.md +38 -0
  236. package/docs/openclaw-integration.md +2 -2
  237. package/docs/prompt-guidance-contract.md +11 -11
  238. package/docs/prs/{dev-deprecate-team-ralph.md → dev-deprecate-team-forge.md} +27 -27
  239. package/docs/prs/{dev-fix-ralph-live-pane-invariant.md → dev-fix-forge-live-pane-invariant.md} +7 -7
  240. package/docs/prs/{dev-team-ralph-workflow-positioning.md → dev-team-forge-workflow-positioning.md} +7 -7
  241. package/docs/qa/forge-persistence-gate.md +20 -0
  242. package/docs/qa/rust-runtime-thin-adapter-gate.md +31 -40
  243. package/docs/readme/README.de.md +14 -0
  244. package/docs/readme/README.el.md +14 -0
  245. package/docs/readme/README.es.md +14 -0
  246. package/docs/readme/README.fr.md +14 -0
  247. package/docs/readme/README.it.md +14 -0
  248. package/docs/readme/README.ja.md +14 -0
  249. package/docs/readme/README.ko.md +14 -0
  250. package/docs/readme/README.pl.md +14 -0
  251. package/docs/readme/README.pt.md +14 -0
  252. package/docs/readme/README.ru.md +14 -0
  253. package/docs/readme/README.tr.md +14 -0
  254. package/docs/readme/README.uk.md +14 -0
  255. package/docs/readme/README.vi.md +14 -0
  256. package/docs/readme/README.zh-TW.md +14 -0
  257. package/docs/readme/README.zh.md +14 -0
  258. package/docs/readme/rcs-cover.svg +75 -0
  259. package/docs/reference/canonical-vocabulary.md +106 -0
  260. package/docs/reference/forge-parity-matrix.md +26 -0
  261. package/docs/reference/forge-upstream-baseline.md +32 -0
  262. package/docs/reference/rcs-config-schema-routing.md +5 -5
  263. package/docs/reference/roblox-pre-action-protocol.md +4 -0
  264. package/docs/reference/roblox-taxonomy-migration-plan.md +46 -0
  265. package/docs/reference/roblox-workspace-standard.md +83 -0
  266. package/docs/reference/robloxstudio-mcp-compatibility.md +117 -0
  267. package/docs/reference/semantic-design-system.md +110 -0
  268. package/docs/reference/surface-map.md +131 -0
  269. package/docs/reference/team-allocation-rebalance-policy.md +1 -1
  270. package/docs/release-notes-v0.1.0.md +1 -1
  271. package/docs/release-notes-v0.1.1.md +49 -0
  272. package/docs/release-notes-v0.1.6.md +27 -0
  273. package/docs/reports/open-prs-dev-readiness-2026-04-09.md +2 -2
  274. package/docs/shared/agent-tiers.md +3 -3
  275. package/docs/skills.html +10 -12
  276. package/docs/troubleshooting.md +1 -1
  277. package/package.json +20 -13
  278. package/plugins/roblox-ai-os-creator-skills/.codex-plugin/plugin.json +1 -1
  279. package/plugins/roblox-ai-os-creator-skills/docs/reference/roblox-pre-action-protocol.md +4 -0
  280. package/plugins/roblox-ai-os-creator-skills/skills/ai-slop-cleaner/SKILL.md +14 -7
  281. package/plugins/roblox-ai-os-creator-skills/skills/analyze/SKILL.md +9 -2
  282. package/plugins/roblox-ai-os-creator-skills/skills/ask-claude/SKILL.md +7 -0
  283. package/plugins/roblox-ai-os-creator-skills/skills/ask-gemini/SKILL.md +7 -0
  284. package/plugins/roblox-ai-os-creator-skills/skills/autoforge/SKILL.md +7 -0
  285. package/plugins/roblox-ai-os-creator-skills/skills/autopilot/SKILL.md +48 -41
  286. package/plugins/roblox-ai-os-creator-skills/skills/autoresearch/SKILL.md +8 -1
  287. package/plugins/roblox-ai-os-creator-skills/skills/blueprint/SKILL.md +227 -9
  288. package/plugins/roblox-ai-os-creator-skills/skills/blueprint-loop/SKILL.md +7 -0
  289. package/plugins/roblox-ai-os-creator-skills/skills/blueprint-psych/SKILL.md +7 -0
  290. package/plugins/roblox-ai-os-creator-skills/skills/blueprint-retention/SKILL.md +7 -0
  291. package/plugins/roblox-ai-os-creator-skills/skills/blueprint-social/SKILL.md +7 -0
  292. package/plugins/roblox-ai-os-creator-skills/skills/brief/SKILL.md +7 -0
  293. package/plugins/roblox-ai-os-creator-skills/skills/brief-audience/SKILL.md +7 -0
  294. package/plugins/roblox-ai-os-creator-skills/skills/brief-motivation/SKILL.md +7 -0
  295. package/plugins/roblox-ai-os-creator-skills/skills/cancel/SKILL.md +59 -52
  296. package/plugins/roblox-ai-os-creator-skills/skills/code-review/SKILL.md +30 -24
  297. package/plugins/roblox-ai-os-creator-skills/skills/configure-notifications/SKILL.md +7 -0
  298. package/plugins/roblox-ai-os-creator-skills/skills/crew/SKILL.md +7 -0
  299. package/plugins/roblox-ai-os-creator-skills/skills/deep-interview/SKILL.md +25 -18
  300. package/plugins/roblox-ai-os-creator-skills/skills/doctor/SKILL.md +7 -0
  301. package/plugins/roblox-ai-os-creator-skills/skills/forge/SKILL.md +174 -11
  302. package/plugins/roblox-ai-os-creator-skills/skills/forge-community/SKILL.md +7 -0
  303. package/plugins/roblox-ai-os-creator-skills/skills/forge-daily-loop/SKILL.md +7 -0
  304. package/plugins/roblox-ai-os-creator-skills/skills/forge-event-loop/SKILL.md +7 -0
  305. package/plugins/roblox-ai-os-creator-skills/skills/forge-fomo/SKILL.md +7 -0
  306. package/plugins/roblox-ai-os-creator-skills/skills/forge-mastery/SKILL.md +7 -0
  307. package/plugins/roblox-ai-os-creator-skills/skills/forge-progression/SKILL.md +7 -0
  308. package/plugins/roblox-ai-os-creator-skills/skills/forge-reward-loop/SKILL.md +7 -0
  309. package/plugins/roblox-ai-os-creator-skills/skills/forge-status/SKILL.md +7 -0
  310. package/plugins/roblox-ai-os-creator-skills/skills/help/SKILL.md +8 -1
  311. package/plugins/roblox-ai-os-creator-skills/skills/hud/SKILL.md +16 -9
  312. package/plugins/roblox-ai-os-creator-skills/skills/note/SKILL.md +8 -1
  313. package/plugins/roblox-ai-os-creator-skills/skills/pipeline/SKILL.md +18 -11
  314. package/plugins/roblox-ai-os-creator-skills/skills/plan/SKILL.md +36 -29
  315. package/plugins/roblox-ai-os-creator-skills/skills/rcs-setup/SKILL.md +8 -1
  316. package/plugins/roblox-ai-os-creator-skills/skills/security-review/SKILL.md +120 -236
  317. package/plugins/roblox-ai-os-creator-skills/skills/skill/SKILL.md +20 -13
  318. package/plugins/roblox-ai-os-creator-skills/skills/team/SKILL.md +17 -11
  319. package/plugins/roblox-ai-os-creator-skills/skills/trace/SKILL.md +7 -0
  320. package/plugins/roblox-ai-os-creator-skills/skills/ultraqa/SKILL.md +10 -3
  321. package/plugins/roblox-ai-os-creator-skills/skills/ultrawork/SKILL.md +19 -12
  322. package/plugins/roblox-ai-os-creator-skills/skills/{visual-ralph → visual-forge}/SKILL.md +36 -27
  323. package/plugins/roblox-ai-os-creator-skills/skills/visual-verdict/SKILL.md +9 -2
  324. package/plugins/roblox-ai-os-creator-skills/skills/wiki/SKILL.md +10 -3
  325. package/plugins/roblox-ai-os-creator-skills/skills/worker/SKILL.md +16 -7
  326. package/plugins/roblox-ai-os-creator-skills/templates/roblox/pre-action-plan.md +1 -0
  327. package/prompts/analyst.md +7 -0
  328. package/prompts/architect.md +11 -4
  329. package/prompts/build-fixer.md +7 -0
  330. package/prompts/code-reviewer.md +9 -2
  331. package/prompts/code-simplifier.md +4 -0
  332. package/prompts/critic.md +13 -6
  333. package/prompts/debugger.md +8 -1
  334. package/prompts/dependency-expert.md +8 -1
  335. package/prompts/designer.md +20 -10
  336. package/prompts/executor.md +7 -0
  337. package/prompts/explore-harness.md +7 -0
  338. package/prompts/explore.md +7 -0
  339. package/prompts/git-master.md +8 -1
  340. package/prompts/planner.md +10 -3
  341. package/prompts/researcher.md +7 -0
  342. package/prompts/security-reviewer.md +76 -92
  343. package/prompts/sisyphus-lite.md +7 -0
  344. package/prompts/team-executor.md +7 -0
  345. package/prompts/team-orchestrator.md +9 -2
  346. package/prompts/test-engineer.md +11 -3
  347. package/prompts/verifier.md +7 -0
  348. package/prompts/vision.md +9 -2
  349. package/prompts/writer.md +11 -4
  350. package/skills/.agents/skills/roblox-animations/SKILL.md +220 -0
  351. package/skills/.agents/skills/roblox-datastores/SKILL.md +219 -0
  352. package/skills/.agents/skills/roblox-gui/SKILL.md +192 -0
  353. package/skills/.agents/skills/roblox-monetization/SKILL.md +208 -0
  354. package/skills/.agents/skills/roblox-performance/SKILL.md +230 -0
  355. package/skills/.agents/skills/roblox-remote-events/SKILL.md +199 -0
  356. package/skills/.agents/skills/roblox-security/SKILL.md +236 -0
  357. package/skills/ai-slop-cleaner/SKILL.md +14 -7
  358. package/skills/analyze/SKILL.md +9 -2
  359. package/skills/ask-claude/SKILL.md +7 -0
  360. package/skills/ask-gemini/SKILL.md +7 -0
  361. package/skills/autoforge/SKILL.md +7 -0
  362. package/skills/autopilot/SKILL.md +48 -41
  363. package/skills/autoresearch/SKILL.md +8 -1
  364. package/skills/blueprint/SKILL.md +227 -9
  365. package/skills/blueprint-loop/SKILL.md +7 -0
  366. package/skills/blueprint-psych/SKILL.md +7 -0
  367. package/skills/blueprint-retention/SKILL.md +7 -0
  368. package/skills/blueprint-social/SKILL.md +7 -0
  369. package/skills/brief/SKILL.md +7 -0
  370. package/skills/brief-audience/SKILL.md +7 -0
  371. package/skills/brief-motivation/SKILL.md +7 -0
  372. package/skills/build-fix/SKILL.md +9 -2
  373. package/skills/cancel/SKILL.md +59 -52
  374. package/skills/code-review/SKILL.md +30 -24
  375. package/skills/configure-notifications/SKILL.md +7 -0
  376. package/skills/crew/SKILL.md +7 -0
  377. package/skills/deep-interview/SKILL.md +25 -18
  378. package/skills/deepsearch/SKILL.md +7 -0
  379. package/skills/doctor/SKILL.md +7 -0
  380. package/skills/ecomode/SKILL.md +9 -2
  381. package/skills/forge/SKILL.md +174 -11
  382. package/skills/forge-community/SKILL.md +7 -0
  383. package/skills/forge-daily-loop/SKILL.md +7 -0
  384. package/skills/forge-event-loop/SKILL.md +7 -0
  385. package/skills/forge-fomo/SKILL.md +7 -0
  386. package/skills/{ralph-init → forge-init}/SKILL.md +20 -13
  387. package/skills/forge-mastery/SKILL.md +7 -0
  388. package/skills/forge-progression/SKILL.md +7 -0
  389. package/skills/forge-reward-loop/SKILL.md +7 -0
  390. package/skills/forge-status/SKILL.md +7 -0
  391. package/skills/git-master/SKILL.md +7 -0
  392. package/skills/help/SKILL.md +8 -1
  393. package/skills/hud/SKILL.md +16 -9
  394. package/skills/note/SKILL.md +8 -1
  395. package/skills/pipeline/SKILL.md +18 -11
  396. package/skills/plan/SKILL.md +36 -29
  397. package/skills/rcs-setup/SKILL.md +8 -1
  398. package/skills/review/SKILL.md +7 -0
  399. package/skills/security-review/SKILL.md +120 -236
  400. package/skills/skill/SKILL.md +20 -13
  401. package/skills/skills-lock.json +47 -0
  402. package/skills/swarm/SKILL.md +8 -1
  403. package/skills/tdd/SKILL.md +7 -0
  404. package/skills/team/SKILL.md +17 -11
  405. package/skills/trace/SKILL.md +7 -0
  406. package/skills/ultraqa/SKILL.md +10 -3
  407. package/skills/ultrawork/SKILL.md +19 -12
  408. package/skills/{visual-ralph → visual-forge}/SKILL.md +36 -27
  409. package/skills/visual-verdict/SKILL.md +9 -2
  410. package/skills/web-clone/SKILL.md +14 -7
  411. package/skills/wiki/SKILL.md +10 -3
  412. package/skills/worker/SKILL.md +16 -7
  413. package/src/scripts/__tests__/codex-native-hook.test.ts +386 -319
  414. package/src/scripts/__tests__/run-test-files.test.ts +6 -4
  415. package/src/scripts/__tests__/verify-native-agents.test.ts +16 -16
  416. package/src/scripts/codex-native-hook.ts +34 -34
  417. package/src/scripts/eval/eval-cross-server-party-flow.ts +14 -0
  418. package/src/scripts/eval/eval-gui-onboarding-clarity.ts +20 -0
  419. package/src/scripts/eval/eval-liveops-reward-loop-balance.ts +14 -0
  420. package/src/scripts/eval/eval-profile-datastore-recovery.ts +20 -0
  421. package/src/scripts/eval/eval-remote-contract-hardening.ts +20 -0
  422. package/src/scripts/notify-fallback-watcher.ts +147 -146
  423. package/src/scripts/notify-hook/__tests__/team-worker-posttooluse.test.ts +24 -10
  424. package/src/scripts/notify-hook/{ralph-session-resume.ts → forge-session-resume.ts} +45 -43
  425. package/src/scripts/notify-hook/team-dispatch.ts +31 -4
  426. package/src/scripts/notify-hook/visual-verdict.ts +3 -3
  427. package/src/scripts/notify-hook.ts +10 -10
  428. package/src/scripts/run-test-files.ts +1 -1
  429. package/src/scripts/surface-taxonomy.ts +316 -0
  430. package/src/scripts/sync-plugin-mirror.ts +5 -4
  431. package/src/scripts/tmux-hook-engine.ts +31 -19
  432. package/templates/AGENTS.md +24 -15
  433. package/templates/catalog-manifest.json +5 -88
  434. package/templates/roblox/pre-action-plan.md +1 -0
  435. package/templates/roblox/robloxstudio-mcp.codex.json +18 -0
  436. package/templates/roblox/robloxstudio-mcp.windows.json +22 -0
  437. package/dist/adapt/__tests__/foundation.test.d.ts +0 -2
  438. package/dist/adapt/__tests__/foundation.test.d.ts.map +0 -1
  439. package/dist/adapt/__tests__/foundation.test.js +0 -171
  440. package/dist/adapt/__tests__/foundation.test.js.map +0 -1
  441. package/dist/adapt/__tests__/hermes.test.d.ts +0 -2
  442. package/dist/adapt/__tests__/hermes.test.d.ts.map +0 -1
  443. package/dist/adapt/__tests__/hermes.test.js +0 -137
  444. package/dist/adapt/__tests__/hermes.test.js.map +0 -1
  445. package/dist/agents/__tests__/definitions.test.d.ts +0 -2
  446. package/dist/agents/__tests__/definitions.test.d.ts.map +0 -1
  447. package/dist/agents/__tests__/definitions.test.js +0 -62
  448. package/dist/agents/__tests__/definitions.test.js.map +0 -1
  449. package/dist/agents/__tests__/native-config.test.d.ts +0 -2
  450. package/dist/agents/__tests__/native-config.test.d.ts.map +0 -1
  451. package/dist/agents/__tests__/native-config.test.js +0 -278
  452. package/dist/agents/__tests__/native-config.test.js.map +0 -1
  453. package/dist/autoresearch/__tests__/contracts.test.d.ts +0 -2
  454. package/dist/autoresearch/__tests__/contracts.test.d.ts.map +0 -1
  455. package/dist/autoresearch/__tests__/contracts.test.js +0 -127
  456. package/dist/autoresearch/__tests__/contracts.test.js.map +0 -1
  457. package/dist/autoresearch/__tests__/runtime-parity-extra.test.d.ts +0 -2
  458. package/dist/autoresearch/__tests__/runtime-parity-extra.test.d.ts.map +0 -1
  459. package/dist/autoresearch/__tests__/runtime-parity-extra.test.js +0 -356
  460. package/dist/autoresearch/__tests__/runtime-parity-extra.test.js.map +0 -1
  461. package/dist/autoresearch/__tests__/runtime.test.d.ts +0 -2
  462. package/dist/autoresearch/__tests__/runtime.test.d.ts.map +0 -1
  463. package/dist/autoresearch/__tests__/runtime.test.js +0 -218
  464. package/dist/autoresearch/__tests__/runtime.test.js.map +0 -1
  465. package/dist/autoresearch/__tests__/skill-validation.test.d.ts +0 -2
  466. package/dist/autoresearch/__tests__/skill-validation.test.d.ts.map +0 -1
  467. package/dist/autoresearch/__tests__/skill-validation.test.js +0 -91
  468. package/dist/autoresearch/__tests__/skill-validation.test.js.map +0 -1
  469. package/dist/catalog/__tests__/generator.test.d.ts +0 -2
  470. package/dist/catalog/__tests__/generator.test.d.ts.map +0 -1
  471. package/dist/catalog/__tests__/generator.test.js +0 -49
  472. package/dist/catalog/__tests__/generator.test.js.map +0 -1
  473. package/dist/catalog/__tests__/plugin-bundle-ssot.test.d.ts +0 -2
  474. package/dist/catalog/__tests__/plugin-bundle-ssot.test.d.ts.map +0 -1
  475. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +0 -83
  476. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +0 -1
  477. package/dist/catalog/__tests__/schema.test.d.ts +0 -2
  478. package/dist/catalog/__tests__/schema.test.d.ts.map +0 -1
  479. package/dist/catalog/__tests__/schema.test.js +0 -91
  480. package/dist/catalog/__tests__/schema.test.js.map +0 -1
  481. package/dist/cli/__tests__/adapt-help.test.d.ts +0 -2
  482. package/dist/cli/__tests__/adapt-help.test.d.ts.map +0 -1
  483. package/dist/cli/__tests__/adapt-help.test.js +0 -37
  484. package/dist/cli/__tests__/adapt-help.test.js.map +0 -1
  485. package/dist/cli/__tests__/adapt.test.d.ts +0 -2
  486. package/dist/cli/__tests__/adapt.test.d.ts.map +0 -1
  487. package/dist/cli/__tests__/adapt.test.js +0 -62
  488. package/dist/cli/__tests__/adapt.test.js.map +0 -1
  489. package/dist/cli/__tests__/agents-init.test.d.ts +0 -2
  490. package/dist/cli/__tests__/agents-init.test.d.ts.map +0 -1
  491. package/dist/cli/__tests__/agents-init.test.js +0 -184
  492. package/dist/cli/__tests__/agents-init.test.js.map +0 -1
  493. package/dist/cli/__tests__/agents.test.d.ts +0 -2
  494. package/dist/cli/__tests__/agents.test.d.ts.map +0 -1
  495. package/dist/cli/__tests__/agents.test.js +0 -137
  496. package/dist/cli/__tests__/agents.test.js.map +0 -1
  497. package/dist/cli/__tests__/ask.test.d.ts +0 -2
  498. package/dist/cli/__tests__/ask.test.d.ts.map +0 -1
  499. package/dist/cli/__tests__/ask.test.js +0 -265
  500. package/dist/cli/__tests__/ask.test.js.map +0 -1
  501. package/dist/cli/__tests__/autoresearch-guided.test.d.ts +0 -2
  502. package/dist/cli/__tests__/autoresearch-guided.test.d.ts.map +0 -1
  503. package/dist/cli/__tests__/autoresearch-guided.test.js +0 -365
  504. package/dist/cli/__tests__/autoresearch-guided.test.js.map +0 -1
  505. package/dist/cli/__tests__/autoresearch.test.d.ts +0 -2
  506. package/dist/cli/__tests__/autoresearch.test.d.ts.map +0 -1
  507. package/dist/cli/__tests__/autoresearch.test.js +0 -203
  508. package/dist/cli/__tests__/autoresearch.test.js.map +0 -1
  509. package/dist/cli/__tests__/catalog-contract.test.d.ts +0 -2
  510. package/dist/cli/__tests__/catalog-contract.test.d.ts.map +0 -1
  511. package/dist/cli/__tests__/catalog-contract.test.js +0 -18
  512. package/dist/cli/__tests__/catalog-contract.test.js.map +0 -1
  513. package/dist/cli/__tests__/cleanup.test.d.ts +0 -2
  514. package/dist/cli/__tests__/cleanup.test.d.ts.map +0 -1
  515. package/dist/cli/__tests__/cleanup.test.js +0 -419
  516. package/dist/cli/__tests__/cleanup.test.js.map +0 -1
  517. package/dist/cli/__tests__/codex-plugin-layout.test.d.ts +0 -2
  518. package/dist/cli/__tests__/codex-plugin-layout.test.d.ts.map +0 -1
  519. package/dist/cli/__tests__/codex-plugin-layout.test.js +0 -210
  520. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +0 -1
  521. package/dist/cli/__tests__/doctor-context-window-warning.test.d.ts +0 -2
  522. package/dist/cli/__tests__/doctor-context-window-warning.test.d.ts.map +0 -1
  523. package/dist/cli/__tests__/doctor-context-window-warning.test.js +0 -122
  524. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +0 -1
  525. package/dist/cli/__tests__/doctor-invalid-config.test.d.ts +0 -2
  526. package/dist/cli/__tests__/doctor-invalid-config.test.d.ts.map +0 -1
  527. package/dist/cli/__tests__/doctor-invalid-config.test.js +0 -52
  528. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +0 -1
  529. package/dist/cli/__tests__/doctor-team.test.d.ts +0 -2
  530. package/dist/cli/__tests__/doctor-team.test.d.ts.map +0 -1
  531. package/dist/cli/__tests__/doctor-team.test.js +0 -299
  532. package/dist/cli/__tests__/doctor-team.test.js.map +0 -1
  533. package/dist/cli/__tests__/doctor-warning-copy.test.d.ts +0 -2
  534. package/dist/cli/__tests__/doctor-warning-copy.test.d.ts.map +0 -1
  535. package/dist/cli/__tests__/doctor-warning-copy.test.js +0 -438
  536. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +0 -1
  537. package/dist/cli/__tests__/error-handling-warnings.test.d.ts +0 -2
  538. package/dist/cli/__tests__/error-handling-warnings.test.d.ts.map +0 -1
  539. package/dist/cli/__tests__/error-handling-warnings.test.js +0 -52
  540. package/dist/cli/__tests__/error-handling-warnings.test.js.map +0 -1
  541. package/dist/cli/__tests__/exec.test.d.ts +0 -2
  542. package/dist/cli/__tests__/exec.test.d.ts.map +0 -1
  543. package/dist/cli/__tests__/exec.test.js +0 -213
  544. package/dist/cli/__tests__/exec.test.js.map +0 -1
  545. package/dist/cli/__tests__/explore-windows-diagnostics.test.d.ts +0 -2
  546. package/dist/cli/__tests__/explore-windows-diagnostics.test.d.ts.map +0 -1
  547. package/dist/cli/__tests__/explore-windows-diagnostics.test.js +0 -17
  548. package/dist/cli/__tests__/explore-windows-diagnostics.test.js.map +0 -1
  549. package/dist/cli/__tests__/explore.test.d.ts +0 -2
  550. package/dist/cli/__tests__/explore.test.d.ts.map +0 -1
  551. package/dist/cli/__tests__/explore.test.js +0 -1090
  552. package/dist/cli/__tests__/explore.test.js.map +0 -1
  553. package/dist/cli/__tests__/hooks.test.d.ts +0 -2
  554. package/dist/cli/__tests__/hooks.test.d.ts.map +0 -1
  555. package/dist/cli/__tests__/hooks.test.js +0 -55
  556. package/dist/cli/__tests__/hooks.test.js.map +0 -1
  557. package/dist/cli/__tests__/index.test.d.ts +0 -2
  558. package/dist/cli/__tests__/index.test.d.ts.map +0 -1
  559. package/dist/cli/__tests__/index.test.js +0 -2259
  560. package/dist/cli/__tests__/index.test.js.map +0 -1
  561. package/dist/cli/__tests__/launch-fallback.test.d.ts +0 -2
  562. package/dist/cli/__tests__/launch-fallback.test.d.ts.map +0 -1
  563. package/dist/cli/__tests__/launch-fallback.test.js +0 -661
  564. package/dist/cli/__tests__/launch-fallback.test.js.map +0 -1
  565. package/dist/cli/__tests__/lifecycle-notifications.test.d.ts +0 -2
  566. package/dist/cli/__tests__/lifecycle-notifications.test.d.ts.map +0 -1
  567. package/dist/cli/__tests__/lifecycle-notifications.test.js +0 -48
  568. package/dist/cli/__tests__/lifecycle-notifications.test.js.map +0 -1
  569. package/dist/cli/__tests__/list.test.d.ts +0 -2
  570. package/dist/cli/__tests__/list.test.d.ts.map +0 -1
  571. package/dist/cli/__tests__/list.test.js +0 -38
  572. package/dist/cli/__tests__/list.test.js.map +0 -1
  573. package/dist/cli/__tests__/mcp-parity.test.d.ts +0 -2
  574. package/dist/cli/__tests__/mcp-parity.test.d.ts.map +0 -1
  575. package/dist/cli/__tests__/mcp-parity.test.js +0 -228
  576. package/dist/cli/__tests__/mcp-parity.test.js.map +0 -1
  577. package/dist/cli/__tests__/mcp-serve.test.d.ts +0 -2
  578. package/dist/cli/__tests__/mcp-serve.test.d.ts.map +0 -1
  579. package/dist/cli/__tests__/mcp-serve.test.js +0 -64
  580. package/dist/cli/__tests__/mcp-serve.test.js.map +0 -1
  581. package/dist/cli/__tests__/native-assets.test.d.ts +0 -2
  582. package/dist/cli/__tests__/native-assets.test.d.ts.map +0 -1
  583. package/dist/cli/__tests__/native-assets.test.js +0 -308
  584. package/dist/cli/__tests__/native-assets.test.js.map +0 -1
  585. package/dist/cli/__tests__/native-hook-dispatch-contract.test.d.ts +0 -2
  586. package/dist/cli/__tests__/native-hook-dispatch-contract.test.d.ts.map +0 -1
  587. package/dist/cli/__tests__/native-hook-dispatch-contract.test.js +0 -11
  588. package/dist/cli/__tests__/native-hook-dispatch-contract.test.js.map +0 -1
  589. package/dist/cli/__tests__/nested-help-routing.test.d.ts +0 -2
  590. package/dist/cli/__tests__/nested-help-routing.test.d.ts.map +0 -1
  591. package/dist/cli/__tests__/nested-help-routing.test.js +0 -96
  592. package/dist/cli/__tests__/nested-help-routing.test.js.map +0 -1
  593. package/dist/cli/__tests__/package-bin-contract.test.d.ts +0 -2
  594. package/dist/cli/__tests__/package-bin-contract.test.d.ts.map +0 -1
  595. package/dist/cli/__tests__/package-bin-contract.test.js +0 -177
  596. package/dist/cli/__tests__/package-bin-contract.test.js.map +0 -1
  597. package/dist/cli/__tests__/packaged-explore-harness-lock.d.ts +0 -3
  598. package/dist/cli/__tests__/packaged-explore-harness-lock.d.ts.map +0 -1
  599. package/dist/cli/__tests__/packaged-explore-harness-lock.js +0 -67
  600. package/dist/cli/__tests__/packaged-explore-harness-lock.js.map +0 -1
  601. package/dist/cli/__tests__/packaged-script-resolution.test.d.ts +0 -2
  602. package/dist/cli/__tests__/packaged-script-resolution.test.d.ts.map +0 -1
  603. package/dist/cli/__tests__/packaged-script-resolution.test.js +0 -19
  604. package/dist/cli/__tests__/packaged-script-resolution.test.js.map +0 -1
  605. package/dist/cli/__tests__/prompt-skill-sanitization.test.d.ts +0 -2
  606. package/dist/cli/__tests__/prompt-skill-sanitization.test.d.ts.map +0 -1
  607. package/dist/cli/__tests__/prompt-skill-sanitization.test.js +0 -48
  608. package/dist/cli/__tests__/prompt-skill-sanitization.test.js.map +0 -1
  609. package/dist/cli/__tests__/question.test.d.ts +0 -2
  610. package/dist/cli/__tests__/question.test.d.ts.map +0 -1
  611. package/dist/cli/__tests__/question.test.js +0 -633
  612. package/dist/cli/__tests__/question.test.js.map +0 -1
  613. package/dist/cli/__tests__/ralph-deslop-contract.test.d.ts +0 -2
  614. package/dist/cli/__tests__/ralph-deslop-contract.test.d.ts.map +0 -1
  615. package/dist/cli/__tests__/ralph-deslop-contract.test.js +0 -28
  616. package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +0 -1
  617. package/dist/cli/__tests__/ralph-prd-deep-interview.test.d.ts +0 -2
  618. package/dist/cli/__tests__/ralph-prd-deep-interview.test.d.ts.map +0 -1
  619. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js +0 -24
  620. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +0 -1
  621. package/dist/cli/__tests__/ralph-prd-smoke.test.d.ts +0 -2
  622. package/dist/cli/__tests__/ralph-prd-smoke.test.d.ts.map +0 -1
  623. package/dist/cli/__tests__/ralph-prd-smoke.test.js +0 -167
  624. package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +0 -1
  625. package/dist/cli/__tests__/ralph.test.d.ts +0 -2
  626. package/dist/cli/__tests__/ralph.test.d.ts.map +0 -1
  627. package/dist/cli/__tests__/ralph.test.js +0 -256
  628. package/dist/cli/__tests__/ralph.test.js.map +0 -1
  629. package/dist/cli/__tests__/resume.test.d.ts +0 -2
  630. package/dist/cli/__tests__/resume.test.d.ts.map +0 -1
  631. package/dist/cli/__tests__/resume.test.js +0 -84
  632. package/dist/cli/__tests__/resume.test.js.map +0 -1
  633. package/dist/cli/__tests__/session-scoped-runtime.test.d.ts +0 -2
  634. package/dist/cli/__tests__/session-scoped-runtime.test.d.ts.map +0 -1
  635. package/dist/cli/__tests__/session-scoped-runtime.test.js +0 -146
  636. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +0 -1
  637. package/dist/cli/__tests__/session-search-help.test.d.ts +0 -2
  638. package/dist/cli/__tests__/session-search-help.test.d.ts.map +0 -1
  639. package/dist/cli/__tests__/session-search-help.test.js +0 -76
  640. package/dist/cli/__tests__/session-search-help.test.js.map +0 -1
  641. package/dist/cli/__tests__/session-search.test.d.ts +0 -2
  642. package/dist/cli/__tests__/session-search.test.d.ts.map +0 -1
  643. package/dist/cli/__tests__/session-search.test.js +0 -77
  644. package/dist/cli/__tests__/session-search.test.js.map +0 -1
  645. package/dist/cli/__tests__/setup-agents-overwrite.test.d.ts +0 -2
  646. package/dist/cli/__tests__/setup-agents-overwrite.test.d.ts.map +0 -1
  647. package/dist/cli/__tests__/setup-agents-overwrite.test.js +0 -457
  648. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +0 -1
  649. package/dist/cli/__tests__/setup-gh-star.test.d.ts +0 -2
  650. package/dist/cli/__tests__/setup-gh-star.test.d.ts.map +0 -1
  651. package/dist/cli/__tests__/setup-gh-star.test.js +0 -67
  652. package/dist/cli/__tests__/setup-gh-star.test.js.map +0 -1
  653. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.d.ts +0 -2
  654. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.d.ts.map +0 -1
  655. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +0 -189
  656. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +0 -1
  657. package/dist/cli/__tests__/setup-install-mode.test.d.ts +0 -2
  658. package/dist/cli/__tests__/setup-install-mode.test.d.ts.map +0 -1
  659. package/dist/cli/__tests__/setup-install-mode.test.js +0 -873
  660. package/dist/cli/__tests__/setup-install-mode.test.js.map +0 -1
  661. package/dist/cli/__tests__/setup-prompts-overwrite.test.d.ts +0 -2
  662. package/dist/cli/__tests__/setup-prompts-overwrite.test.d.ts.map +0 -1
  663. package/dist/cli/__tests__/setup-prompts-overwrite.test.js +0 -191
  664. package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +0 -1
  665. package/dist/cli/__tests__/setup-refresh.test.d.ts +0 -2
  666. package/dist/cli/__tests__/setup-refresh.test.d.ts.map +0 -1
  667. package/dist/cli/__tests__/setup-refresh.test.js +0 -591
  668. package/dist/cli/__tests__/setup-refresh.test.js.map +0 -1
  669. package/dist/cli/__tests__/setup-scope.test.d.ts +0 -2
  670. package/dist/cli/__tests__/setup-scope.test.d.ts.map +0 -1
  671. package/dist/cli/__tests__/setup-scope.test.js +0 -340
  672. package/dist/cli/__tests__/setup-scope.test.js.map +0 -1
  673. package/dist/cli/__tests__/setup-skill-validation.test.d.ts +0 -2
  674. package/dist/cli/__tests__/setup-skill-validation.test.d.ts.map +0 -1
  675. package/dist/cli/__tests__/setup-skill-validation.test.js +0 -44
  676. package/dist/cli/__tests__/setup-skill-validation.test.js.map +0 -1
  677. package/dist/cli/__tests__/setup-skills-overwrite.test.d.ts +0 -2
  678. package/dist/cli/__tests__/setup-skills-overwrite.test.d.ts.map +0 -1
  679. package/dist/cli/__tests__/setup-skills-overwrite.test.js +0 -295
  680. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +0 -1
  681. package/dist/cli/__tests__/sidecar.test.d.ts +0 -2
  682. package/dist/cli/__tests__/sidecar.test.d.ts.map +0 -1
  683. package/dist/cli/__tests__/sidecar.test.js +0 -24
  684. package/dist/cli/__tests__/sidecar.test.js.map +0 -1
  685. package/dist/cli/__tests__/sparkshell-cli.test.d.ts +0 -2
  686. package/dist/cli/__tests__/sparkshell-cli.test.d.ts.map +0 -1
  687. package/dist/cli/__tests__/sparkshell-cli.test.js +0 -400
  688. package/dist/cli/__tests__/sparkshell-cli.test.js.map +0 -1
  689. package/dist/cli/__tests__/sparkshell-packaging.test.d.ts +0 -2
  690. package/dist/cli/__tests__/sparkshell-packaging.test.d.ts.map +0 -1
  691. package/dist/cli/__tests__/sparkshell-packaging.test.js +0 -74
  692. package/dist/cli/__tests__/sparkshell-packaging.test.js.map +0 -1
  693. package/dist/cli/__tests__/star-prompt.test.d.ts +0 -2
  694. package/dist/cli/__tests__/star-prompt.test.d.ts.map +0 -1
  695. package/dist/cli/__tests__/star-prompt.test.js +0 -172
  696. package/dist/cli/__tests__/star-prompt.test.js.map +0 -1
  697. package/dist/cli/__tests__/state.test.d.ts +0 -2
  698. package/dist/cli/__tests__/state.test.d.ts.map +0 -1
  699. package/dist/cli/__tests__/state.test.js +0 -46
  700. package/dist/cli/__tests__/state.test.js.map +0 -1
  701. package/dist/cli/__tests__/team-decompose.test.d.ts +0 -2
  702. package/dist/cli/__tests__/team-decompose.test.d.ts.map +0 -1
  703. package/dist/cli/__tests__/team-decompose.test.js +0 -133
  704. package/dist/cli/__tests__/team-decompose.test.js.map +0 -1
  705. package/dist/cli/__tests__/team.test.d.ts +0 -2
  706. package/dist/cli/__tests__/team.test.d.ts.map +0 -1
  707. package/dist/cli/__tests__/team.test.js +0 -1820
  708. package/dist/cli/__tests__/team.test.js.map +0 -1
  709. package/dist/cli/__tests__/uninstall.test.d.ts +0 -2
  710. package/dist/cli/__tests__/uninstall.test.d.ts.map +0 -1
  711. package/dist/cli/__tests__/uninstall.test.js +0 -766
  712. package/dist/cli/__tests__/uninstall.test.js.map +0 -1
  713. package/dist/cli/__tests__/update.test.d.ts +0 -2
  714. package/dist/cli/__tests__/update.test.d.ts.map +0 -1
  715. package/dist/cli/__tests__/update.test.js +0 -589
  716. package/dist/cli/__tests__/update.test.js.map +0 -1
  717. package/dist/cli/__tests__/version-sync-contract.test.d.ts +0 -2
  718. package/dist/cli/__tests__/version-sync-contract.test.d.ts.map +0 -1
  719. package/dist/cli/__tests__/version-sync-contract.test.js +0 -41
  720. package/dist/cli/__tests__/version-sync-contract.test.js.map +0 -1
  721. package/dist/cli/__tests__/version.test.d.ts +0 -2
  722. package/dist/cli/__tests__/version.test.d.ts.map +0 -1
  723. package/dist/cli/__tests__/version.test.js +0 -21
  724. package/dist/cli/__tests__/version.test.js.map +0 -1
  725. package/dist/cli/__tests__/windows-popup-loop-contract.test.d.ts +0 -2
  726. package/dist/cli/__tests__/windows-popup-loop-contract.test.d.ts.map +0 -1
  727. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +0 -31
  728. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +0 -1
  729. package/dist/cli/ralph.d.ts +0 -17
  730. package/dist/compat/__tests__/doctor-contract.test.d.ts +0 -2
  731. package/dist/compat/__tests__/doctor-contract.test.d.ts.map +0 -1
  732. package/dist/compat/__tests__/doctor-contract.test.js +0 -108
  733. package/dist/compat/__tests__/doctor-contract.test.js.map +0 -1
  734. package/dist/compat/__tests__/rust-runtime-compat.test.d.ts +0 -2
  735. package/dist/compat/__tests__/rust-runtime-compat.test.d.ts.map +0 -1
  736. package/dist/compat/__tests__/rust-runtime-compat.test.js +0 -218
  737. package/dist/compat/__tests__/rust-runtime-compat.test.js.map +0 -1
  738. package/dist/config/__tests__/codex-hooks.test.d.ts +0 -2
  739. package/dist/config/__tests__/codex-hooks.test.d.ts.map +0 -1
  740. package/dist/config/__tests__/codex-hooks.test.js +0 -77
  741. package/dist/config/__tests__/codex-hooks.test.js.map +0 -1
  742. package/dist/config/__tests__/generator-idempotent.test.d.ts +0 -2
  743. package/dist/config/__tests__/generator-idempotent.test.d.ts.map +0 -1
  744. package/dist/config/__tests__/generator-idempotent.test.js +0 -882
  745. package/dist/config/__tests__/generator-idempotent.test.js.map +0 -1
  746. package/dist/config/__tests__/generator-notify.test.d.ts +0 -2
  747. package/dist/config/__tests__/generator-notify.test.d.ts.map +0 -1
  748. package/dist/config/__tests__/generator-notify.test.js +0 -343
  749. package/dist/config/__tests__/generator-notify.test.js.map +0 -1
  750. package/dist/config/__tests__/generator-status-line-presets.test.d.ts +0 -2
  751. package/dist/config/__tests__/generator-status-line-presets.test.d.ts.map +0 -1
  752. package/dist/config/__tests__/generator-status-line-presets.test.js +0 -203
  753. package/dist/config/__tests__/generator-status-line-presets.test.js.map +0 -1
  754. package/dist/config/__tests__/mcp-registry.test.d.ts +0 -2
  755. package/dist/config/__tests__/mcp-registry.test.d.ts.map +0 -1
  756. package/dist/config/__tests__/mcp-registry.test.js +0 -190
  757. package/dist/config/__tests__/mcp-registry.test.js.map +0 -1
  758. package/dist/config/__tests__/models.test.d.ts +0 -2
  759. package/dist/config/__tests__/models.test.d.ts.map +0 -1
  760. package/dist/config/__tests__/models.test.js +0 -224
  761. package/dist/config/__tests__/models.test.js.map +0 -1
  762. package/dist/config/__tests__/wiki-config-contract.test.d.ts +0 -2
  763. package/dist/config/__tests__/wiki-config-contract.test.d.ts.map +0 -1
  764. package/dist/config/__tests__/wiki-config-contract.test.js +0 -19
  765. package/dist/config/__tests__/wiki-config-contract.test.js.map +0 -1
  766. package/dist/document-refresh/__tests__/enforcer.test.d.ts +0 -2
  767. package/dist/document-refresh/__tests__/enforcer.test.d.ts.map +0 -1
  768. package/dist/document-refresh/__tests__/enforcer.test.js +0 -128
  769. package/dist/document-refresh/__tests__/enforcer.test.js.map +0 -1
  770. package/dist/hooks/__tests__/agents-overlay.test.d.ts +0 -8
  771. package/dist/hooks/__tests__/agents-overlay.test.d.ts.map +0 -1
  772. package/dist/hooks/__tests__/agents-overlay.test.js +0 -644
  773. package/dist/hooks/__tests__/agents-overlay.test.js.map +0 -1
  774. package/dist/hooks/__tests__/analyze-routing-contract.test.d.ts +0 -2
  775. package/dist/hooks/__tests__/analyze-routing-contract.test.d.ts.map +0 -1
  776. package/dist/hooks/__tests__/analyze-routing-contract.test.js +0 -45
  777. package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +0 -1
  778. package/dist/hooks/__tests__/analyze-skill-contract.test.d.ts +0 -2
  779. package/dist/hooks/__tests__/analyze-skill-contract.test.d.ts.map +0 -1
  780. package/dist/hooks/__tests__/analyze-skill-contract.test.js +0 -48
  781. package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +0 -1
  782. package/dist/hooks/__tests__/anti-slop-workflow.test.d.ts +0 -2
  783. package/dist/hooks/__tests__/anti-slop-workflow.test.d.ts.map +0 -1
  784. package/dist/hooks/__tests__/anti-slop-workflow.test.js +0 -146
  785. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +0 -1
  786. package/dist/hooks/__tests__/autopilot-skill-contract.test.d.ts +0 -2
  787. package/dist/hooks/__tests__/autopilot-skill-contract.test.d.ts.map +0 -1
  788. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +0 -37
  789. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +0 -1
  790. package/dist/hooks/__tests__/clawhip-event-contract.test.d.ts +0 -2
  791. package/dist/hooks/__tests__/clawhip-event-contract.test.d.ts.map +0 -1
  792. package/dist/hooks/__tests__/clawhip-event-contract.test.js +0 -37
  793. package/dist/hooks/__tests__/clawhip-event-contract.test.js.map +0 -1
  794. package/dist/hooks/__tests__/code-review-skill-contract.test.d.ts +0 -2
  795. package/dist/hooks/__tests__/code-review-skill-contract.test.d.ts.map +0 -1
  796. package/dist/hooks/__tests__/code-review-skill-contract.test.js +0 -56
  797. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +0 -1
  798. package/dist/hooks/__tests__/codebase-map.test.d.ts +0 -8
  799. package/dist/hooks/__tests__/codebase-map.test.d.ts.map +0 -1
  800. package/dist/hooks/__tests__/codebase-map.test.js +0 -218
  801. package/dist/hooks/__tests__/codebase-map.test.js.map +0 -1
  802. package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts +0 -18
  803. package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts.map +0 -1
  804. package/dist/hooks/__tests__/consensus-execution-handoff.test.js +0 -234
  805. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +0 -1
  806. package/dist/hooks/__tests__/debugger-log-recency-contract.test.d.ts +0 -2
  807. package/dist/hooks/__tests__/debugger-log-recency-contract.test.d.ts.map +0 -1
  808. package/dist/hooks/__tests__/debugger-log-recency-contract.test.js +0 -20
  809. package/dist/hooks/__tests__/debugger-log-recency-contract.test.js.map +0 -1
  810. package/dist/hooks/__tests__/deep-interview-contract.test.d.ts +0 -2
  811. package/dist/hooks/__tests__/deep-interview-contract.test.d.ts.map +0 -1
  812. package/dist/hooks/__tests__/deep-interview-contract.test.js +0 -213
  813. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +0 -1
  814. package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.d.ts +0 -2
  815. package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.d.ts.map +0 -1
  816. package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.js +0 -43
  817. package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.js.map +0 -1
  818. package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.d.ts +0 -2
  819. package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.d.ts.map +0 -1
  820. package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.js +0 -38
  821. package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.js.map +0 -1
  822. package/dist/hooks/__tests__/explore-routing.test.d.ts +0 -2
  823. package/dist/hooks/__tests__/explore-routing.test.d.ts.map +0 -1
  824. package/dist/hooks/__tests__/explore-routing.test.js +0 -43
  825. package/dist/hooks/__tests__/explore-routing.test.js.map +0 -1
  826. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.d.ts +0 -2
  827. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.d.ts.map +0 -1
  828. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js +0 -69
  829. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js.map +0 -1
  830. package/dist/hooks/__tests__/keyword-detector.test.d.ts +0 -2
  831. package/dist/hooks/__tests__/keyword-detector.test.d.ts.map +0 -1
  832. package/dist/hooks/__tests__/keyword-detector.test.js +0 -1716
  833. package/dist/hooks/__tests__/keyword-detector.test.js.map +0 -1
  834. package/dist/hooks/__tests__/notify-fallback-watcher.test.d.ts +0 -2
  835. package/dist/hooks/__tests__/notify-fallback-watcher.test.d.ts.map +0 -1
  836. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +0 -3898
  837. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +0 -1
  838. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.d.ts +0 -2
  839. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.d.ts.map +0 -1
  840. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +0 -786
  841. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +0 -1
  842. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.d.ts +0 -2
  843. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.d.ts.map +0 -1
  844. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +0 -2397
  845. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +0 -1
  846. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.d.ts +0 -2
  847. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.d.ts.map +0 -1
  848. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +0 -160
  849. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +0 -1
  850. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.d.ts +0 -2
  851. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.d.ts.map +0 -1
  852. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +0 -1178
  853. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +0 -1
  854. package/dist/hooks/__tests__/notify-hook-modules.test.d.ts +0 -9
  855. package/dist/hooks/__tests__/notify-hook-modules.test.d.ts.map +0 -1
  856. package/dist/hooks/__tests__/notify-hook-modules.test.js +0 -529
  857. package/dist/hooks/__tests__/notify-hook-modules.test.js.map +0 -1
  858. package/dist/hooks/__tests__/notify-hook-native-dispatch-contract.test.d.ts +0 -2
  859. package/dist/hooks/__tests__/notify-hook-native-dispatch-contract.test.d.ts.map +0 -1
  860. package/dist/hooks/__tests__/notify-hook-native-dispatch-contract.test.js +0 -14
  861. package/dist/hooks/__tests__/notify-hook-native-dispatch-contract.test.js.map +0 -1
  862. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.d.ts +0 -2
  863. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.d.ts.map +0 -1
  864. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +0 -682
  865. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +0 -1
  866. package/dist/hooks/__tests__/notify-hook-regression-205.test.d.ts +0 -9
  867. package/dist/hooks/__tests__/notify-hook-regression-205.test.d.ts.map +0 -1
  868. package/dist/hooks/__tests__/notify-hook-regression-205.test.js +0 -255
  869. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +0 -1
  870. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.d.ts +0 -2
  871. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.d.ts.map +0 -1
  872. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +0 -162
  873. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +0 -1
  874. package/dist/hooks/__tests__/notify-hook-session-scope.test.d.ts +0 -2
  875. package/dist/hooks/__tests__/notify-hook-session-scope.test.d.ts.map +0 -1
  876. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +0 -301
  877. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +0 -1
  878. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.d.ts +0 -2
  879. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.d.ts.map +0 -1
  880. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +0 -1510
  881. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +0 -1
  882. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.d.ts +0 -2
  883. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.d.ts.map +0 -1
  884. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +0 -2879
  885. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +0 -1
  886. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.d.ts +0 -2
  887. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.d.ts.map +0 -1
  888. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +0 -228
  889. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +0 -1
  890. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.d.ts +0 -2
  891. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.d.ts.map +0 -1
  892. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +0 -35
  893. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +0 -1
  894. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.d.ts +0 -2
  895. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.d.ts.map +0 -1
  896. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +0 -1589
  897. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +0 -1
  898. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.d.ts +0 -10
  899. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.d.ts.map +0 -1
  900. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +0 -0
  901. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +0 -1
  902. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.d.ts +0 -11
  903. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.d.ts.map +0 -1
  904. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js +0 -266
  905. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js.map +0 -1
  906. package/dist/hooks/__tests__/notify-hook-worker-idle.test.d.ts +0 -2
  907. package/dist/hooks/__tests__/notify-hook-worker-idle.test.d.ts.map +0 -1
  908. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +0 -895
  909. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +0 -1
  910. package/dist/hooks/__tests__/openclaw-setup-contract.test.d.ts +0 -2
  911. package/dist/hooks/__tests__/openclaw-setup-contract.test.d.ts.map +0 -1
  912. package/dist/hooks/__tests__/openclaw-setup-contract.test.js +0 -61
  913. package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +0 -1
  914. package/dist/hooks/__tests__/pre-context-gate-skills.test.d.ts +0 -2
  915. package/dist/hooks/__tests__/pre-context-gate-skills.test.d.ts.map +0 -1
  916. package/dist/hooks/__tests__/pre-context-gate-skills.test.js +0 -40
  917. package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +0 -1
  918. package/dist/hooks/__tests__/prompt-guidance-catalog.test.d.ts +0 -2
  919. package/dist/hooks/__tests__/prompt-guidance-catalog.test.d.ts.map +0 -1
  920. package/dist/hooks/__tests__/prompt-guidance-catalog.test.js +0 -11
  921. package/dist/hooks/__tests__/prompt-guidance-catalog.test.js.map +0 -1
  922. package/dist/hooks/__tests__/prompt-guidance-contract.test.d.ts +0 -2
  923. package/dist/hooks/__tests__/prompt-guidance-contract.test.d.ts.map +0 -1
  924. package/dist/hooks/__tests__/prompt-guidance-contract.test.js +0 -38
  925. package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +0 -1
  926. package/dist/hooks/__tests__/prompt-guidance-fragments.test.d.ts +0 -2
  927. package/dist/hooks/__tests__/prompt-guidance-fragments.test.d.ts.map +0 -1
  928. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js +0 -48
  929. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +0 -1
  930. package/dist/hooks/__tests__/prompt-guidance-scenarios.test.d.ts +0 -2
  931. package/dist/hooks/__tests__/prompt-guidance-scenarios.test.d.ts.map +0 -1
  932. package/dist/hooks/__tests__/prompt-guidance-scenarios.test.js +0 -11
  933. package/dist/hooks/__tests__/prompt-guidance-scenarios.test.js.map +0 -1
  934. package/dist/hooks/__tests__/prompt-guidance-test-helpers.d.ts +0 -5
  935. package/dist/hooks/__tests__/prompt-guidance-test-helpers.d.ts.map +0 -1
  936. package/dist/hooks/__tests__/prompt-guidance-test-helpers.js +0 -34
  937. package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +0 -1
  938. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.d.ts +0 -2
  939. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.d.ts.map +0 -1
  940. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +0 -65
  941. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +0 -1
  942. package/dist/hooks/__tests__/prompt-orchestration-boundary.test.d.ts +0 -2
  943. package/dist/hooks/__tests__/prompt-orchestration-boundary.test.d.ts.map +0 -1
  944. package/dist/hooks/__tests__/prompt-orchestration-boundary.test.js +0 -38
  945. package/dist/hooks/__tests__/prompt-orchestration-boundary.test.js.map +0 -1
  946. package/dist/hooks/__tests__/prompt-refactor-contract.test.d.ts +0 -2
  947. package/dist/hooks/__tests__/prompt-refactor-contract.test.d.ts.map +0 -1
  948. package/dist/hooks/__tests__/prompt-refactor-contract.test.js +0 -22
  949. package/dist/hooks/__tests__/prompt-refactor-contract.test.js.map +0 -1
  950. package/dist/hooks/__tests__/prompt-team-routing.test.d.ts +0 -2
  951. package/dist/hooks/__tests__/prompt-team-routing.test.d.ts.map +0 -1
  952. package/dist/hooks/__tests__/prompt-team-routing.test.js +0 -49
  953. package/dist/hooks/__tests__/prompt-team-routing.test.js.map +0 -1
  954. package/dist/hooks/__tests__/session.test.d.ts +0 -2
  955. package/dist/hooks/__tests__/session.test.d.ts.map +0 -1
  956. package/dist/hooks/__tests__/session.test.js +0 -322
  957. package/dist/hooks/__tests__/session.test.js.map +0 -1
  958. package/dist/hooks/__tests__/skill-guidance-contract.test.d.ts +0 -2
  959. package/dist/hooks/__tests__/skill-guidance-contract.test.d.ts.map +0 -1
  960. package/dist/hooks/__tests__/skill-guidance-contract.test.js +0 -29
  961. package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +0 -1
  962. package/dist/hooks/__tests__/task-size-detector.test.d.ts +0 -2
  963. package/dist/hooks/__tests__/task-size-detector.test.d.ts.map +0 -1
  964. package/dist/hooks/__tests__/task-size-detector.test.js +0 -330
  965. package/dist/hooks/__tests__/task-size-detector.test.js.map +0 -1
  966. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.d.ts +0 -2
  967. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.d.ts.map +0 -1
  968. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js +0 -28
  969. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +0 -1
  970. package/dist/hooks/__tests__/tmux-hook-engine-types-sync.test.d.ts +0 -2
  971. package/dist/hooks/__tests__/tmux-hook-engine-types-sync.test.d.ts.map +0 -1
  972. package/dist/hooks/__tests__/tmux-hook-engine-types-sync.test.js +0 -24
  973. package/dist/hooks/__tests__/tmux-hook-engine-types-sync.test.js.map +0 -1
  974. package/dist/hooks/__tests__/tmux-hook-engine.test.d.ts +0 -2
  975. package/dist/hooks/__tests__/tmux-hook-engine.test.d.ts.map +0 -1
  976. package/dist/hooks/__tests__/tmux-hook-engine.test.js +0 -403
  977. package/dist/hooks/__tests__/tmux-hook-engine.test.js.map +0 -1
  978. package/dist/hooks/__tests__/triage-config.test.d.ts +0 -2
  979. package/dist/hooks/__tests__/triage-config.test.d.ts.map +0 -1
  980. package/dist/hooks/__tests__/triage-config.test.js +0 -211
  981. package/dist/hooks/__tests__/triage-config.test.js.map +0 -1
  982. package/dist/hooks/__tests__/triage-heuristic.test.d.ts +0 -2
  983. package/dist/hooks/__tests__/triage-heuristic.test.d.ts.map +0 -1
  984. package/dist/hooks/__tests__/triage-heuristic.test.js +0 -285
  985. package/dist/hooks/__tests__/triage-heuristic.test.js.map +0 -1
  986. package/dist/hooks/__tests__/triage-state.test.d.ts +0 -2
  987. package/dist/hooks/__tests__/triage-state.test.d.ts.map +0 -1
  988. package/dist/hooks/__tests__/triage-state.test.js +0 -426
  989. package/dist/hooks/__tests__/triage-state.test.js.map +0 -1
  990. package/dist/hooks/__tests__/visual-ralph-skill.test.d.ts +0 -2
  991. package/dist/hooks/__tests__/visual-ralph-skill.test.d.ts.map +0 -1
  992. package/dist/hooks/__tests__/visual-ralph-skill.test.js +0 -44
  993. package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +0 -1
  994. package/dist/hooks/__tests__/visual-verdict-loop.test.d.ts +0 -2
  995. package/dist/hooks/__tests__/visual-verdict-loop.test.d.ts.map +0 -1
  996. package/dist/hooks/__tests__/visual-verdict-loop.test.js +0 -35
  997. package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +0 -1
  998. package/dist/hooks/__tests__/wiki-docs-contract.test.d.ts +0 -2
  999. package/dist/hooks/__tests__/wiki-docs-contract.test.d.ts.map +0 -1
  1000. package/dist/hooks/__tests__/wiki-docs-contract.test.js +0 -34
  1001. package/dist/hooks/__tests__/wiki-docs-contract.test.js.map +0 -1
  1002. package/dist/hooks/code-simplifier/__tests__/index.test.d.ts +0 -2
  1003. package/dist/hooks/code-simplifier/__tests__/index.test.d.ts.map +0 -1
  1004. package/dist/hooks/code-simplifier/__tests__/index.test.js +0 -187
  1005. package/dist/hooks/code-simplifier/__tests__/index.test.js.map +0 -1
  1006. package/dist/hooks/extensibility/__tests__/dispatcher.test.d.ts +0 -2
  1007. package/dist/hooks/extensibility/__tests__/dispatcher.test.d.ts.map +0 -1
  1008. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +0 -242
  1009. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +0 -1
  1010. package/dist/hooks/extensibility/__tests__/events.test.d.ts +0 -2
  1011. package/dist/hooks/extensibility/__tests__/events.test.d.ts.map +0 -1
  1012. package/dist/hooks/extensibility/__tests__/events.test.js +0 -125
  1013. package/dist/hooks/extensibility/__tests__/events.test.js.map +0 -1
  1014. package/dist/hooks/extensibility/__tests__/example-hook-plugins.test.d.ts +0 -2
  1015. package/dist/hooks/extensibility/__tests__/example-hook-plugins.test.d.ts.map +0 -1
  1016. package/dist/hooks/extensibility/__tests__/example-hook-plugins.test.js +0 -153
  1017. package/dist/hooks/extensibility/__tests__/example-hook-plugins.test.js.map +0 -1
  1018. package/dist/hooks/extensibility/__tests__/loader.test.d.ts +0 -2
  1019. package/dist/hooks/extensibility/__tests__/loader.test.d.ts.map +0 -1
  1020. package/dist/hooks/extensibility/__tests__/loader.test.js +0 -254
  1021. package/dist/hooks/extensibility/__tests__/loader.test.js.map +0 -1
  1022. package/dist/hooks/extensibility/__tests__/logging.test.d.ts +0 -2
  1023. package/dist/hooks/extensibility/__tests__/logging.test.d.ts.map +0 -1
  1024. package/dist/hooks/extensibility/__tests__/logging.test.js +0 -74
  1025. package/dist/hooks/extensibility/__tests__/logging.test.js.map +0 -1
  1026. package/dist/hooks/extensibility/__tests__/plugin-runner.test.d.ts +0 -2
  1027. package/dist/hooks/extensibility/__tests__/plugin-runner.test.d.ts.map +0 -1
  1028. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js +0 -202
  1029. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js.map +0 -1
  1030. package/dist/hooks/extensibility/__tests__/runtime.test.d.ts +0 -2
  1031. package/dist/hooks/extensibility/__tests__/runtime.test.d.ts.map +0 -1
  1032. package/dist/hooks/extensibility/__tests__/runtime.test.js +0 -198
  1033. package/dist/hooks/extensibility/__tests__/runtime.test.js.map +0 -1
  1034. package/dist/hooks/extensibility/__tests__/sdk-public-surface.test.d.ts +0 -2
  1035. package/dist/hooks/extensibility/__tests__/sdk-public-surface.test.d.ts.map +0 -1
  1036. package/dist/hooks/extensibility/__tests__/sdk-public-surface.test.js +0 -32
  1037. package/dist/hooks/extensibility/__tests__/sdk-public-surface.test.js.map +0 -1
  1038. package/dist/hooks/extensibility/__tests__/sdk.test.d.ts +0 -2
  1039. package/dist/hooks/extensibility/__tests__/sdk.test.d.ts.map +0 -1
  1040. package/dist/hooks/extensibility/__tests__/sdk.test.js +0 -479
  1041. package/dist/hooks/extensibility/__tests__/sdk.test.js.map +0 -1
  1042. package/dist/hud/__tests__/authority.test.d.ts +0 -2
  1043. package/dist/hud/__tests__/authority.test.d.ts.map +0 -1
  1044. package/dist/hud/__tests__/authority.test.js +0 -56
  1045. package/dist/hud/__tests__/authority.test.js.map +0 -1
  1046. package/dist/hud/__tests__/colors.test.d.ts +0 -2
  1047. package/dist/hud/__tests__/colors.test.d.ts.map +0 -1
  1048. package/dist/hud/__tests__/colors.test.js +0 -92
  1049. package/dist/hud/__tests__/colors.test.js.map +0 -1
  1050. package/dist/hud/__tests__/hud-tmux-injection.test.d.ts +0 -10
  1051. package/dist/hud/__tests__/hud-tmux-injection.test.d.ts.map +0 -1
  1052. package/dist/hud/__tests__/hud-tmux-injection.test.js +0 -150
  1053. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +0 -1
  1054. package/dist/hud/__tests__/index.test.d.ts +0 -2
  1055. package/dist/hud/__tests__/index.test.d.ts.map +0 -1
  1056. package/dist/hud/__tests__/index.test.js +0 -180
  1057. package/dist/hud/__tests__/index.test.js.map +0 -1
  1058. package/dist/hud/__tests__/reconcile.test.d.ts +0 -2
  1059. package/dist/hud/__tests__/reconcile.test.d.ts.map +0 -1
  1060. package/dist/hud/__tests__/reconcile.test.js +0 -125
  1061. package/dist/hud/__tests__/reconcile.test.js.map +0 -1
  1062. package/dist/hud/__tests__/render.test.d.ts +0 -2
  1063. package/dist/hud/__tests__/render.test.d.ts.map +0 -1
  1064. package/dist/hud/__tests__/render.test.js +0 -573
  1065. package/dist/hud/__tests__/render.test.js.map +0 -1
  1066. package/dist/hud/__tests__/state.test.d.ts +0 -2
  1067. package/dist/hud/__tests__/state.test.d.ts.map +0 -1
  1068. package/dist/hud/__tests__/state.test.js +0 -618
  1069. package/dist/hud/__tests__/state.test.js.map +0 -1
  1070. package/dist/hud/__tests__/types.test.d.ts +0 -2
  1071. package/dist/hud/__tests__/types.test.d.ts.map +0 -1
  1072. package/dist/hud/__tests__/types.test.js +0 -79
  1073. package/dist/hud/__tests__/types.test.js.map +0 -1
  1074. package/dist/hud/__tests__/watch.test.d.ts +0 -2
  1075. package/dist/hud/__tests__/watch.test.d.ts.map +0 -1
  1076. package/dist/hud/__tests__/watch.test.js +0 -63
  1077. package/dist/hud/__tests__/watch.test.js.map +0 -1
  1078. package/dist/mcp/__tests__/bootstrap.test.d.ts +0 -2
  1079. package/dist/mcp/__tests__/bootstrap.test.d.ts.map +0 -1
  1080. package/dist/mcp/__tests__/bootstrap.test.js +0 -207
  1081. package/dist/mcp/__tests__/bootstrap.test.js.map +0 -1
  1082. package/dist/mcp/__tests__/code-intel-server.test.d.ts +0 -2
  1083. package/dist/mcp/__tests__/code-intel-server.test.d.ts.map +0 -1
  1084. package/dist/mcp/__tests__/code-intel-server.test.js +0 -70
  1085. package/dist/mcp/__tests__/code-intel-server.test.js.map +0 -1
  1086. package/dist/mcp/__tests__/memory-server.test.d.ts +0 -2
  1087. package/dist/mcp/__tests__/memory-server.test.d.ts.map +0 -1
  1088. package/dist/mcp/__tests__/memory-server.test.js +0 -36
  1089. package/dist/mcp/__tests__/memory-server.test.js.map +0 -1
  1090. package/dist/mcp/__tests__/memory-validation.test.d.ts +0 -2
  1091. package/dist/mcp/__tests__/memory-validation.test.d.ts.map +0 -1
  1092. package/dist/mcp/__tests__/memory-validation.test.js +0 -29
  1093. package/dist/mcp/__tests__/memory-validation.test.js.map +0 -1
  1094. package/dist/mcp/__tests__/path-traversal.test.d.ts +0 -2
  1095. package/dist/mcp/__tests__/path-traversal.test.d.ts.map +0 -1
  1096. package/dist/mcp/__tests__/path-traversal.test.js +0 -83
  1097. package/dist/mcp/__tests__/path-traversal.test.js.map +0 -1
  1098. package/dist/mcp/__tests__/server-lifecycle.test.d.ts +0 -2
  1099. package/dist/mcp/__tests__/server-lifecycle.test.d.ts.map +0 -1
  1100. package/dist/mcp/__tests__/server-lifecycle.test.js +0 -260
  1101. package/dist/mcp/__tests__/server-lifecycle.test.js.map +0 -1
  1102. package/dist/mcp/__tests__/state-paths.test.d.ts +0 -2
  1103. package/dist/mcp/__tests__/state-paths.test.d.ts.map +0 -1
  1104. package/dist/mcp/__tests__/state-paths.test.js +0 -209
  1105. package/dist/mcp/__tests__/state-paths.test.js.map +0 -1
  1106. package/dist/mcp/__tests__/state-server-ralph-phase.test.d.ts +0 -2
  1107. package/dist/mcp/__tests__/state-server-ralph-phase.test.d.ts.map +0 -1
  1108. package/dist/mcp/__tests__/state-server-ralph-phase.test.js +0 -109
  1109. package/dist/mcp/__tests__/state-server-ralph-phase.test.js.map +0 -1
  1110. package/dist/mcp/__tests__/state-server-schema.test.d.ts +0 -2
  1111. package/dist/mcp/__tests__/state-server-schema.test.d.ts.map +0 -1
  1112. package/dist/mcp/__tests__/state-server-schema.test.js +0 -29
  1113. package/dist/mcp/__tests__/state-server-schema.test.js.map +0 -1
  1114. package/dist/mcp/__tests__/state-server-team-tools.test.d.ts +0 -2
  1115. package/dist/mcp/__tests__/state-server-team-tools.test.d.ts.map +0 -1
  1116. package/dist/mcp/__tests__/state-server-team-tools.test.js +0 -35
  1117. package/dist/mcp/__tests__/state-server-team-tools.test.js.map +0 -1
  1118. package/dist/mcp/__tests__/state-server.test.d.ts +0 -2
  1119. package/dist/mcp/__tests__/state-server.test.d.ts.map +0 -1
  1120. package/dist/mcp/__tests__/state-server.test.js +0 -965
  1121. package/dist/mcp/__tests__/state-server.test.js.map +0 -1
  1122. package/dist/mcp/__tests__/trace-server.test.d.ts +0 -2
  1123. package/dist/mcp/__tests__/trace-server.test.d.ts.map +0 -1
  1124. package/dist/mcp/__tests__/trace-server.test.js +0 -119
  1125. package/dist/mcp/__tests__/trace-server.test.js.map +0 -1
  1126. package/dist/mcp/__tests__/wiki-server.test.d.ts +0 -2
  1127. package/dist/mcp/__tests__/wiki-server.test.d.ts.map +0 -1
  1128. package/dist/mcp/__tests__/wiki-server.test.js +0 -30
  1129. package/dist/mcp/__tests__/wiki-server.test.js.map +0 -1
  1130. package/dist/modes/__tests__/base-autoresearch-contract.test.d.ts +0 -2
  1131. package/dist/modes/__tests__/base-autoresearch-contract.test.d.ts.map +0 -1
  1132. package/dist/modes/__tests__/base-autoresearch-contract.test.js +0 -123
  1133. package/dist/modes/__tests__/base-autoresearch-contract.test.js.map +0 -1
  1134. package/dist/modes/__tests__/base-multi-state-compat.test.d.ts +0 -2
  1135. package/dist/modes/__tests__/base-multi-state-compat.test.d.ts.map +0 -1
  1136. package/dist/modes/__tests__/base-multi-state-compat.test.js +0 -38
  1137. package/dist/modes/__tests__/base-multi-state-compat.test.js.map +0 -1
  1138. package/dist/modes/__tests__/base-ralph-contract.test.d.ts +0 -2
  1139. package/dist/modes/__tests__/base-ralph-contract.test.d.ts.map +0 -1
  1140. package/dist/modes/__tests__/base-ralph-contract.test.js +0 -64
  1141. package/dist/modes/__tests__/base-ralph-contract.test.js.map +0 -1
  1142. package/dist/modes/__tests__/base-session-scope.test.d.ts +0 -2
  1143. package/dist/modes/__tests__/base-session-scope.test.d.ts.map +0 -1
  1144. package/dist/modes/__tests__/base-session-scope.test.js +0 -98
  1145. package/dist/modes/__tests__/base-session-scope.test.js.map +0 -1
  1146. package/dist/modes/__tests__/base-tmux-pane.test.d.ts +0 -2
  1147. package/dist/modes/__tests__/base-tmux-pane.test.d.ts.map +0 -1
  1148. package/dist/modes/__tests__/base-tmux-pane.test.js +0 -39
  1149. package/dist/modes/__tests__/base-tmux-pane.test.js.map +0 -1
  1150. package/dist/notifications/__tests__/config.test.d.ts +0 -2
  1151. package/dist/notifications/__tests__/config.test.d.ts.map +0 -1
  1152. package/dist/notifications/__tests__/config.test.js +0 -269
  1153. package/dist/notifications/__tests__/config.test.js.map +0 -1
  1154. package/dist/notifications/__tests__/custom-alias-enablement.test.d.ts +0 -2
  1155. package/dist/notifications/__tests__/custom-alias-enablement.test.d.ts.map +0 -1
  1156. package/dist/notifications/__tests__/custom-alias-enablement.test.js +0 -84
  1157. package/dist/notifications/__tests__/custom-alias-enablement.test.js.map +0 -1
  1158. package/dist/notifications/__tests__/dispatch-cooldown.test.d.ts +0 -5
  1159. package/dist/notifications/__tests__/dispatch-cooldown.test.d.ts.map +0 -1
  1160. package/dist/notifications/__tests__/dispatch-cooldown.test.js +0 -100
  1161. package/dist/notifications/__tests__/dispatch-cooldown.test.js.map +0 -1
  1162. package/dist/notifications/__tests__/dispatcher.test.d.ts +0 -2
  1163. package/dist/notifications/__tests__/dispatcher.test.d.ts.map +0 -1
  1164. package/dist/notifications/__tests__/dispatcher.test.js +0 -202
  1165. package/dist/notifications/__tests__/dispatcher.test.js.map +0 -1
  1166. package/dist/notifications/__tests__/formatter.test.d.ts +0 -2
  1167. package/dist/notifications/__tests__/formatter.test.d.ts.map +0 -1
  1168. package/dist/notifications/__tests__/formatter.test.js +0 -270
  1169. package/dist/notifications/__tests__/formatter.test.js.map +0 -1
  1170. package/dist/notifications/__tests__/hook-config.test.d.ts +0 -5
  1171. package/dist/notifications/__tests__/hook-config.test.d.ts.map +0 -1
  1172. package/dist/notifications/__tests__/hook-config.test.js +0 -139
  1173. package/dist/notifications/__tests__/hook-config.test.js.map +0 -1
  1174. package/dist/notifications/__tests__/idle-cooldown.test.d.ts +0 -5
  1175. package/dist/notifications/__tests__/idle-cooldown.test.d.ts.map +0 -1
  1176. package/dist/notifications/__tests__/idle-cooldown.test.js +0 -209
  1177. package/dist/notifications/__tests__/idle-cooldown.test.js.map +0 -1
  1178. package/dist/notifications/__tests__/index.test.d.ts +0 -2
  1179. package/dist/notifications/__tests__/index.test.d.ts.map +0 -1
  1180. package/dist/notifications/__tests__/index.test.js +0 -188
  1181. package/dist/notifications/__tests__/index.test.js.map +0 -1
  1182. package/dist/notifications/__tests__/lifecycle-dedupe.test.d.ts +0 -2
  1183. package/dist/notifications/__tests__/lifecycle-dedupe.test.d.ts.map +0 -1
  1184. package/dist/notifications/__tests__/lifecycle-dedupe.test.js +0 -86
  1185. package/dist/notifications/__tests__/lifecycle-dedupe.test.js.map +0 -1
  1186. package/dist/notifications/__tests__/notifier.test.d.ts +0 -2
  1187. package/dist/notifications/__tests__/notifier.test.d.ts.map +0 -1
  1188. package/dist/notifications/__tests__/notifier.test.js +0 -239
  1189. package/dist/notifications/__tests__/notifier.test.js.map +0 -1
  1190. package/dist/notifications/__tests__/profiles.test.d.ts +0 -2
  1191. package/dist/notifications/__tests__/profiles.test.d.ts.map +0 -1
  1192. package/dist/notifications/__tests__/profiles.test.js +0 -404
  1193. package/dist/notifications/__tests__/profiles.test.js.map +0 -1
  1194. package/dist/notifications/__tests__/reply-config.test.d.ts +0 -2
  1195. package/dist/notifications/__tests__/reply-config.test.d.ts.map +0 -1
  1196. package/dist/notifications/__tests__/reply-config.test.js +0 -79
  1197. package/dist/notifications/__tests__/reply-config.test.js.map +0 -1
  1198. package/dist/notifications/__tests__/reply-listener.test.d.ts +0 -2
  1199. package/dist/notifications/__tests__/reply-listener.test.d.ts.map +0 -1
  1200. package/dist/notifications/__tests__/reply-listener.test.js +0 -723
  1201. package/dist/notifications/__tests__/reply-listener.test.js.map +0 -1
  1202. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.d.ts +0 -2
  1203. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.d.ts.map +0 -1
  1204. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js +0 -93
  1205. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js.map +0 -1
  1206. package/dist/notifications/__tests__/session-registry.test.d.ts +0 -2
  1207. package/dist/notifications/__tests__/session-registry.test.d.ts.map +0 -1
  1208. package/dist/notifications/__tests__/session-registry.test.js +0 -234
  1209. package/dist/notifications/__tests__/session-registry.test.js.map +0 -1
  1210. package/dist/notifications/__tests__/session-status.test.d.ts +0 -2
  1211. package/dist/notifications/__tests__/session-status.test.d.ts.map +0 -1
  1212. package/dist/notifications/__tests__/session-status.test.js +0 -249
  1213. package/dist/notifications/__tests__/session-status.test.js.map +0 -1
  1214. package/dist/notifications/__tests__/temp-mode.test.d.ts +0 -2
  1215. package/dist/notifications/__tests__/temp-mode.test.d.ts.map +0 -1
  1216. package/dist/notifications/__tests__/temp-mode.test.js +0 -172
  1217. package/dist/notifications/__tests__/temp-mode.test.js.map +0 -1
  1218. package/dist/notifications/__tests__/template-engine.test.d.ts +0 -5
  1219. package/dist/notifications/__tests__/template-engine.test.d.ts.map +0 -1
  1220. package/dist/notifications/__tests__/template-engine.test.js +0 -158
  1221. package/dist/notifications/__tests__/template-engine.test.js.map +0 -1
  1222. package/dist/notifications/__tests__/tmux-detector.test.d.ts +0 -2
  1223. package/dist/notifications/__tests__/tmux-detector.test.d.ts.map +0 -1
  1224. package/dist/notifications/__tests__/tmux-detector.test.js +0 -208
  1225. package/dist/notifications/__tests__/tmux-detector.test.js.map +0 -1
  1226. package/dist/notifications/__tests__/tmux.test.d.ts +0 -2
  1227. package/dist/notifications/__tests__/tmux.test.d.ts.map +0 -1
  1228. package/dist/notifications/__tests__/tmux.test.js +0 -285
  1229. package/dist/notifications/__tests__/tmux.test.js.map +0 -1
  1230. package/dist/notifications/__tests__/verbosity.test.d.ts +0 -2
  1231. package/dist/notifications/__tests__/verbosity.test.d.ts.map +0 -1
  1232. package/dist/notifications/__tests__/verbosity.test.js +0 -237
  1233. package/dist/notifications/__tests__/verbosity.test.js.map +0 -1
  1234. package/dist/openclaw/__tests__/config.test.d.ts +0 -6
  1235. package/dist/openclaw/__tests__/config.test.d.ts.map +0 -1
  1236. package/dist/openclaw/__tests__/config.test.js +0 -344
  1237. package/dist/openclaw/__tests__/config.test.js.map +0 -1
  1238. package/dist/openclaw/__tests__/dispatcher.test.d.ts +0 -5
  1239. package/dist/openclaw/__tests__/dispatcher.test.d.ts.map +0 -1
  1240. package/dist/openclaw/__tests__/dispatcher.test.js +0 -169
  1241. package/dist/openclaw/__tests__/dispatcher.test.js.map +0 -1
  1242. package/dist/openclaw/__tests__/index.test.d.ts +0 -6
  1243. package/dist/openclaw/__tests__/index.test.d.ts.map +0 -1
  1244. package/dist/openclaw/__tests__/index.test.js +0 -382
  1245. package/dist/openclaw/__tests__/index.test.js.map +0 -1
  1246. package/dist/pipeline/__tests__/orchestrator.test.d.ts +0 -2
  1247. package/dist/pipeline/__tests__/orchestrator.test.d.ts.map +0 -1
  1248. package/dist/pipeline/__tests__/orchestrator.test.js +0 -505
  1249. package/dist/pipeline/__tests__/orchestrator.test.js.map +0 -1
  1250. package/dist/pipeline/__tests__/stages.test.d.ts +0 -2
  1251. package/dist/pipeline/__tests__/stages.test.d.ts.map +0 -1
  1252. package/dist/pipeline/__tests__/stages.test.js +0 -754
  1253. package/dist/pipeline/__tests__/stages.test.js.map +0 -1
  1254. package/dist/pipeline/stages/ralph-verify.d.ts +0 -53
  1255. package/dist/pipeline/stages/ralph-verify.d.ts.map +0 -1
  1256. package/dist/pipeline/stages/ralph-verify.js.map +0 -1
  1257. package/dist/pipeline/stages/ralplan.d.ts +0 -25
  1258. package/dist/pipeline/stages/ralplan.d.ts.map +0 -1
  1259. package/dist/pipeline/stages/ralplan.js.map +0 -1
  1260. package/dist/planning/__tests__/artifacts.test.d.ts +0 -2
  1261. package/dist/planning/__tests__/artifacts.test.d.ts.map +0 -1
  1262. package/dist/planning/__tests__/artifacts.test.js +0 -544
  1263. package/dist/planning/__tests__/artifacts.test.js.map +0 -1
  1264. package/dist/question/__tests__/client.test.d.ts +0 -2
  1265. package/dist/question/__tests__/client.test.d.ts.map +0 -1
  1266. package/dist/question/__tests__/client.test.js +0 -90
  1267. package/dist/question/__tests__/client.test.js.map +0 -1
  1268. package/dist/question/__tests__/deep-interview.test.d.ts +0 -2
  1269. package/dist/question/__tests__/deep-interview.test.d.ts.map +0 -1
  1270. package/dist/question/__tests__/deep-interview.test.js +0 -209
  1271. package/dist/question/__tests__/deep-interview.test.js.map +0 -1
  1272. package/dist/question/__tests__/policy.test.d.ts +0 -2
  1273. package/dist/question/__tests__/policy.test.d.ts.map +0 -1
  1274. package/dist/question/__tests__/policy.test.js +0 -107
  1275. package/dist/question/__tests__/policy.test.js.map +0 -1
  1276. package/dist/question/__tests__/renderer.test.d.ts +0 -2
  1277. package/dist/question/__tests__/renderer.test.d.ts.map +0 -1
  1278. package/dist/question/__tests__/renderer.test.js +0 -707
  1279. package/dist/question/__tests__/renderer.test.js.map +0 -1
  1280. package/dist/question/__tests__/state.test.d.ts +0 -2
  1281. package/dist/question/__tests__/state.test.d.ts.map +0 -1
  1282. package/dist/question/__tests__/state.test.js +0 -102
  1283. package/dist/question/__tests__/state.test.js.map +0 -1
  1284. package/dist/question/__tests__/types.test.d.ts +0 -2
  1285. package/dist/question/__tests__/types.test.d.ts.map +0 -1
  1286. package/dist/question/__tests__/types.test.js +0 -65
  1287. package/dist/question/__tests__/types.test.js.map +0 -1
  1288. package/dist/question/__tests__/ui.test.d.ts +0 -2
  1289. package/dist/question/__tests__/ui.test.d.ts.map +0 -1
  1290. package/dist/question/__tests__/ui.test.js +0 -446
  1291. package/dist/question/__tests__/ui.test.js.map +0 -1
  1292. package/dist/ralph/__tests__/persistence.test.d.ts +0 -2
  1293. package/dist/ralph/__tests__/persistence.test.d.ts.map +0 -1
  1294. package/dist/ralph/__tests__/persistence.test.js +0 -116
  1295. package/dist/ralph/__tests__/persistence.test.js.map +0 -1
  1296. package/dist/ralph/contract.d.ts +0 -17
  1297. package/dist/ralph/persistence.js.map +0 -1
  1298. package/dist/ralplan/__tests__/runtime.test.d.ts +0 -2
  1299. package/dist/ralplan/__tests__/runtime.test.d.ts.map +0 -1
  1300. package/dist/ralplan/__tests__/runtime.test.js +0 -165
  1301. package/dist/ralplan/__tests__/runtime.test.js.map +0 -1
  1302. package/dist/ralplan/runtime.d.ts +0 -52
  1303. package/dist/ralplan/runtime.d.ts.map +0 -1
  1304. package/dist/ralplan/runtime.js.map +0 -1
  1305. package/dist/runtime/__tests__/bridge.test.d.ts +0 -2
  1306. package/dist/runtime/__tests__/bridge.test.d.ts.map +0 -1
  1307. package/dist/runtime/__tests__/bridge.test.js +0 -194
  1308. package/dist/runtime/__tests__/bridge.test.js.map +0 -1
  1309. package/dist/runtime/__tests__/run-loop.test.d.ts +0 -2
  1310. package/dist/runtime/__tests__/run-loop.test.d.ts.map +0 -1
  1311. package/dist/runtime/__tests__/run-loop.test.js +0 -35
  1312. package/dist/runtime/__tests__/run-loop.test.js.map +0 -1
  1313. package/dist/runtime/__tests__/run-outcome.test.d.ts +0 -2
  1314. package/dist/runtime/__tests__/run-outcome.test.d.ts.map +0 -1
  1315. package/dist/runtime/__tests__/run-outcome.test.js +0 -102
  1316. package/dist/runtime/__tests__/run-outcome.test.js.map +0 -1
  1317. package/dist/runtime/__tests__/run-state.test.d.ts +0 -2
  1318. package/dist/runtime/__tests__/run-state.test.d.ts.map +0 -1
  1319. package/dist/runtime/__tests__/run-state.test.js +0 -37
  1320. package/dist/runtime/__tests__/run-state.test.js.map +0 -1
  1321. package/dist/scripts/__tests__/codex-native-hook.test.d.ts +0 -2
  1322. package/dist/scripts/__tests__/codex-native-hook.test.d.ts.map +0 -1
  1323. package/dist/scripts/__tests__/codex-native-hook.test.js +0 -6788
  1324. package/dist/scripts/__tests__/codex-native-hook.test.js.map +0 -1
  1325. package/dist/scripts/__tests__/generate-release-body.test.d.ts +0 -2
  1326. package/dist/scripts/__tests__/generate-release-body.test.d.ts.map +0 -1
  1327. package/dist/scripts/__tests__/generate-release-body.test.js +0 -233
  1328. package/dist/scripts/__tests__/generate-release-body.test.js.map +0 -1
  1329. package/dist/scripts/__tests__/hook-derived-watcher.test.d.ts +0 -2
  1330. package/dist/scripts/__tests__/hook-derived-watcher.test.d.ts.map +0 -1
  1331. package/dist/scripts/__tests__/hook-derived-watcher.test.js +0 -195
  1332. package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +0 -1
  1333. package/dist/scripts/__tests__/postinstall.test.d.ts +0 -2
  1334. package/dist/scripts/__tests__/postinstall.test.d.ts.map +0 -1
  1335. package/dist/scripts/__tests__/postinstall.test.js +0 -92
  1336. package/dist/scripts/__tests__/postinstall.test.js.map +0 -1
  1337. package/dist/scripts/__tests__/prompt-inventory.test.d.ts +0 -2
  1338. package/dist/scripts/__tests__/prompt-inventory.test.d.ts.map +0 -1
  1339. package/dist/scripts/__tests__/prompt-inventory.test.js +0 -56
  1340. package/dist/scripts/__tests__/prompt-inventory.test.js.map +0 -1
  1341. package/dist/scripts/__tests__/run-test-files.test.d.ts +0 -2
  1342. package/dist/scripts/__tests__/run-test-files.test.d.ts.map +0 -1
  1343. package/dist/scripts/__tests__/run-test-files.test.js +0 -62
  1344. package/dist/scripts/__tests__/run-test-files.test.js.map +0 -1
  1345. package/dist/scripts/__tests__/smoke-packed-install.test.d.ts +0 -2
  1346. package/dist/scripts/__tests__/smoke-packed-install.test.d.ts.map +0 -1
  1347. package/dist/scripts/__tests__/smoke-packed-install.test.js +0 -135
  1348. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +0 -1
  1349. package/dist/scripts/__tests__/test-reply-listener-live.test.d.ts +0 -2
  1350. package/dist/scripts/__tests__/test-reply-listener-live.test.d.ts.map +0 -1
  1351. package/dist/scripts/__tests__/test-reply-listener-live.test.js +0 -82
  1352. package/dist/scripts/__tests__/test-reply-listener-live.test.js.map +0 -1
  1353. package/dist/scripts/__tests__/verify-native-agents.test.d.ts +0 -2
  1354. package/dist/scripts/__tests__/verify-native-agents.test.d.ts.map +0 -1
  1355. package/dist/scripts/__tests__/verify-native-agents.test.js +0 -166
  1356. package/dist/scripts/__tests__/verify-native-agents.test.js.map +0 -1
  1357. package/dist/scripts/eval/eval-candidate-handoff.d.ts +0 -2
  1358. package/dist/scripts/eval/eval-candidate-handoff.d.ts.map +0 -1
  1359. package/dist/scripts/eval/eval-candidate-handoff.js +0 -11
  1360. package/dist/scripts/eval/eval-candidate-handoff.js.map +0 -1
  1361. package/dist/scripts/eval/eval-cli-discoverability.d.ts +0 -3
  1362. package/dist/scripts/eval/eval-cli-discoverability.d.ts.map +0 -1
  1363. package/dist/scripts/eval/eval-cli-discoverability.js +0 -37
  1364. package/dist/scripts/eval/eval-cli-discoverability.js.map +0 -1
  1365. package/dist/scripts/eval/eval-fresh-run-tagging.d.ts +0 -2
  1366. package/dist/scripts/eval/eval-fresh-run-tagging.d.ts.map +0 -1
  1367. package/dist/scripts/eval/eval-fresh-run-tagging.js +0 -11
  1368. package/dist/scripts/eval/eval-fresh-run-tagging.js.map +0 -1
  1369. package/dist/scripts/eval/eval-help-consistency.d.ts +0 -2
  1370. package/dist/scripts/eval/eval-help-consistency.d.ts.map +0 -1
  1371. package/dist/scripts/eval/eval-help-consistency.js +0 -12
  1372. package/dist/scripts/eval/eval-help-consistency.js.map +0 -1
  1373. package/dist/scripts/eval/eval-in-action-cat-shellout-demo.d.ts +0 -2
  1374. package/dist/scripts/eval/eval-in-action-cat-shellout-demo.d.ts.map +0 -1
  1375. package/dist/scripts/eval/eval-in-action-cat-shellout-demo.js +0 -31
  1376. package/dist/scripts/eval/eval-in-action-cat-shellout-demo.js.map +0 -1
  1377. package/dist/scripts/eval/eval-parity-smoke.d.ts +0 -2
  1378. package/dist/scripts/eval/eval-parity-smoke.d.ts.map +0 -1
  1379. package/dist/scripts/eval/eval-parity-smoke.js +0 -23
  1380. package/dist/scripts/eval/eval-parity-smoke.js.map +0 -1
  1381. package/dist/scripts/eval/eval-parity-sweep.d.ts +0 -2
  1382. package/dist/scripts/eval/eval-parity-sweep.d.ts.map +0 -1
  1383. package/dist/scripts/eval/eval-parity-sweep.js +0 -29
  1384. package/dist/scripts/eval/eval-parity-sweep.js.map +0 -1
  1385. package/dist/scripts/eval/eval-resume-dirty-guard.d.ts +0 -2
  1386. package/dist/scripts/eval/eval-resume-dirty-guard.d.ts.map +0 -1
  1387. package/dist/scripts/eval/eval-resume-dirty-guard.js +0 -11
  1388. package/dist/scripts/eval/eval-resume-dirty-guard.js.map +0 -1
  1389. package/dist/scripts/eval/eval-security-path-traversal.d.ts +0 -3
  1390. package/dist/scripts/eval/eval-security-path-traversal.d.ts.map +0 -1
  1391. package/dist/scripts/eval/eval-security-path-traversal.js +0 -35
  1392. package/dist/scripts/eval/eval-security-path-traversal.js.map +0 -1
  1393. package/dist/scripts/notify-hook/__tests__/operational-events.test.d.ts +0 -2
  1394. package/dist/scripts/notify-hook/__tests__/operational-events.test.d.ts.map +0 -1
  1395. package/dist/scripts/notify-hook/__tests__/operational-events.test.js +0 -24
  1396. package/dist/scripts/notify-hook/__tests__/operational-events.test.js.map +0 -1
  1397. package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.d.ts +0 -2
  1398. package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.d.ts.map +0 -1
  1399. package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.js +0 -153
  1400. package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.js.map +0 -1
  1401. package/dist/scripts/notify-hook/ralph-session-resume.d.ts +0 -22
  1402. package/dist/session-history/__tests__/search.test.d.ts +0 -2
  1403. package/dist/session-history/__tests__/search.test.d.ts.map +0 -1
  1404. package/dist/session-history/__tests__/search.test.js +0 -150
  1405. package/dist/session-history/__tests__/search.test.js.map +0 -1
  1406. package/dist/sidecar/__tests__/boundary.test.d.ts +0 -2
  1407. package/dist/sidecar/__tests__/boundary.test.d.ts.map +0 -1
  1408. package/dist/sidecar/__tests__/boundary.test.js +0 -48
  1409. package/dist/sidecar/__tests__/boundary.test.js.map +0 -1
  1410. package/dist/sidecar/__tests__/collector.test.d.ts +0 -2
  1411. package/dist/sidecar/__tests__/collector.test.d.ts.map +0 -1
  1412. package/dist/sidecar/__tests__/collector.test.js +0 -162
  1413. package/dist/sidecar/__tests__/collector.test.js.map +0 -1
  1414. package/dist/sidecar/__tests__/render.test.d.ts +0 -2
  1415. package/dist/sidecar/__tests__/render.test.d.ts.map +0 -1
  1416. package/dist/sidecar/__tests__/render.test.js +0 -67
  1417. package/dist/sidecar/__tests__/render.test.js.map +0 -1
  1418. package/dist/sidecar/__tests__/tmux.test.d.ts +0 -2
  1419. package/dist/sidecar/__tests__/tmux.test.d.ts.map +0 -1
  1420. package/dist/sidecar/__tests__/tmux.test.js +0 -30
  1421. package/dist/sidecar/__tests__/tmux.test.js.map +0 -1
  1422. package/dist/sidecar/__tests__/watch.test.d.ts +0 -2
  1423. package/dist/sidecar/__tests__/watch.test.d.ts.map +0 -1
  1424. package/dist/sidecar/__tests__/watch.test.js +0 -42
  1425. package/dist/sidecar/__tests__/watch.test.js.map +0 -1
  1426. package/dist/state/__tests__/mode-state-context.test.d.ts +0 -2
  1427. package/dist/state/__tests__/mode-state-context.test.d.ts.map +0 -1
  1428. package/dist/state/__tests__/mode-state-context.test.js +0 -35
  1429. package/dist/state/__tests__/mode-state-context.test.js.map +0 -1
  1430. package/dist/state/__tests__/operations-ralph-phase.test.d.ts +0 -2
  1431. package/dist/state/__tests__/operations-ralph-phase.test.d.ts.map +0 -1
  1432. package/dist/state/__tests__/operations-ralph-phase.test.js +0 -103
  1433. package/dist/state/__tests__/operations-ralph-phase.test.js.map +0 -1
  1434. package/dist/state/__tests__/operations.test.d.ts +0 -2
  1435. package/dist/state/__tests__/operations.test.d.ts.map +0 -1
  1436. package/dist/state/__tests__/operations.test.js +0 -439
  1437. package/dist/state/__tests__/operations.test.js.map +0 -1
  1438. package/dist/state/__tests__/path-traversal.test.d.ts +0 -2
  1439. package/dist/state/__tests__/path-traversal.test.d.ts.map +0 -1
  1440. package/dist/state/__tests__/path-traversal.test.js +0 -49
  1441. package/dist/state/__tests__/path-traversal.test.js.map +0 -1
  1442. package/dist/state/__tests__/skill-active.test.d.ts +0 -2
  1443. package/dist/state/__tests__/skill-active.test.d.ts.map +0 -1
  1444. package/dist/state/__tests__/skill-active.test.js +0 -160
  1445. package/dist/state/__tests__/skill-active.test.js.map +0 -1
  1446. package/dist/state/__tests__/workflow-transition.test.d.ts +0 -2
  1447. package/dist/state/__tests__/workflow-transition.test.d.ts.map +0 -1
  1448. package/dist/state/__tests__/workflow-transition.test.js +0 -77
  1449. package/dist/state/__tests__/workflow-transition.test.js.map +0 -1
  1450. package/dist/subagents/__tests__/tracker.test.d.ts +0 -2
  1451. package/dist/subagents/__tests__/tracker.test.d.ts.map +0 -1
  1452. package/dist/subagents/__tests__/tracker.test.js +0 -47
  1453. package/dist/subagents/__tests__/tracker.test.js.map +0 -1
  1454. package/dist/team/__tests__/allocation-policy.test.d.ts +0 -2
  1455. package/dist/team/__tests__/allocation-policy.test.d.ts.map +0 -1
  1456. package/dist/team/__tests__/allocation-policy.test.js +0 -111
  1457. package/dist/team/__tests__/allocation-policy.test.js.map +0 -1
  1458. package/dist/team/__tests__/api-interop.test.d.ts +0 -2
  1459. package/dist/team/__tests__/api-interop.test.d.ts.map +0 -1
  1460. package/dist/team/__tests__/api-interop.test.js +0 -2262
  1461. package/dist/team/__tests__/api-interop.test.js.map +0 -1
  1462. package/dist/team/__tests__/commit-hygiene.test.d.ts +0 -2
  1463. package/dist/team/__tests__/commit-hygiene.test.d.ts.map +0 -1
  1464. package/dist/team/__tests__/commit-hygiene.test.js +0 -93
  1465. package/dist/team/__tests__/commit-hygiene.test.js.map +0 -1
  1466. package/dist/team/__tests__/cross-rebase-smoke.test.d.ts +0 -2
  1467. package/dist/team/__tests__/cross-rebase-smoke.test.d.ts.map +0 -1
  1468. package/dist/team/__tests__/cross-rebase-smoke.test.js +0 -161
  1469. package/dist/team/__tests__/cross-rebase-smoke.test.js.map +0 -1
  1470. package/dist/team/__tests__/current-task-baseline.test.d.ts +0 -2
  1471. package/dist/team/__tests__/current-task-baseline.test.d.ts.map +0 -1
  1472. package/dist/team/__tests__/current-task-baseline.test.js +0 -87
  1473. package/dist/team/__tests__/current-task-baseline.test.js.map +0 -1
  1474. package/dist/team/__tests__/delegation-policy.test.d.ts +0 -2
  1475. package/dist/team/__tests__/delegation-policy.test.d.ts.map +0 -1
  1476. package/dist/team/__tests__/delegation-policy.test.js +0 -69
  1477. package/dist/team/__tests__/delegation-policy.test.js.map +0 -1
  1478. package/dist/team/__tests__/delivery-e2e-smoke.test.d.ts +0 -2
  1479. package/dist/team/__tests__/delivery-e2e-smoke.test.d.ts.map +0 -1
  1480. package/dist/team/__tests__/delivery-e2e-smoke.test.js +0 -679
  1481. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +0 -1
  1482. package/dist/team/__tests__/events.test.d.ts +0 -2
  1483. package/dist/team/__tests__/events.test.d.ts.map +0 -1
  1484. package/dist/team/__tests__/events.test.js +0 -313
  1485. package/dist/team/__tests__/events.test.js.map +0 -1
  1486. package/dist/team/__tests__/followup-planner.test.d.ts +0 -2
  1487. package/dist/team/__tests__/followup-planner.test.d.ts.map +0 -1
  1488. package/dist/team/__tests__/followup-planner.test.js +0 -84
  1489. package/dist/team/__tests__/followup-planner.test.js.map +0 -1
  1490. package/dist/team/__tests__/hardening-e2e.test.d.ts +0 -2
  1491. package/dist/team/__tests__/hardening-e2e.test.d.ts.map +0 -1
  1492. package/dist/team/__tests__/hardening-e2e.test.js +0 -98
  1493. package/dist/team/__tests__/hardening-e2e.test.js.map +0 -1
  1494. package/dist/team/__tests__/hook-primary-e2e-contract.test.d.ts +0 -2
  1495. package/dist/team/__tests__/hook-primary-e2e-contract.test.d.ts.map +0 -1
  1496. package/dist/team/__tests__/hook-primary-e2e-contract.test.js +0 -78
  1497. package/dist/team/__tests__/hook-primary-e2e-contract.test.js.map +0 -1
  1498. package/dist/team/__tests__/idle-nudge.test.d.ts +0 -2
  1499. package/dist/team/__tests__/idle-nudge.test.d.ts.map +0 -1
  1500. package/dist/team/__tests__/idle-nudge.test.js +0 -230
  1501. package/dist/team/__tests__/idle-nudge.test.js.map +0 -1
  1502. package/dist/team/__tests__/leader-activity.test.d.ts +0 -2
  1503. package/dist/team/__tests__/leader-activity.test.d.ts.map +0 -1
  1504. package/dist/team/__tests__/leader-activity.test.js +0 -261
  1505. package/dist/team/__tests__/leader-activity.test.js.map +0 -1
  1506. package/dist/team/__tests__/mcp-comm.test.d.ts +0 -2
  1507. package/dist/team/__tests__/mcp-comm.test.d.ts.map +0 -1
  1508. package/dist/team/__tests__/mcp-comm.test.js +0 -289
  1509. package/dist/team/__tests__/mcp-comm.test.js.map +0 -1
  1510. package/dist/team/__tests__/model-contract.test.d.ts +0 -2
  1511. package/dist/team/__tests__/model-contract.test.d.ts.map +0 -1
  1512. package/dist/team/__tests__/model-contract.test.js +0 -171
  1513. package/dist/team/__tests__/model-contract.test.js.map +0 -1
  1514. package/dist/team/__tests__/orchestrator.test.d.ts +0 -2
  1515. package/dist/team/__tests__/orchestrator.test.d.ts.map +0 -1
  1516. package/dist/team/__tests__/orchestrator.test.js +0 -111
  1517. package/dist/team/__tests__/orchestrator.test.js.map +0 -1
  1518. package/dist/team/__tests__/phase-controller.test.d.ts +0 -2
  1519. package/dist/team/__tests__/phase-controller.test.d.ts.map +0 -1
  1520. package/dist/team/__tests__/phase-controller.test.js +0 -50
  1521. package/dist/team/__tests__/phase-controller.test.js.map +0 -1
  1522. package/dist/team/__tests__/rebalance-policy.test.d.ts +0 -2
  1523. package/dist/team/__tests__/rebalance-policy.test.d.ts.map +0 -1
  1524. package/dist/team/__tests__/rebalance-policy.test.js +0 -168
  1525. package/dist/team/__tests__/rebalance-policy.test.js.map +0 -1
  1526. package/dist/team/__tests__/repo-aware-decomposition.test.d.ts +0 -2
  1527. package/dist/team/__tests__/repo-aware-decomposition.test.d.ts.map +0 -1
  1528. package/dist/team/__tests__/repo-aware-decomposition.test.js +0 -136
  1529. package/dist/team/__tests__/repo-aware-decomposition.test.js.map +0 -1
  1530. package/dist/team/__tests__/role-router.test.d.ts +0 -2
  1531. package/dist/team/__tests__/role-router.test.d.ts.map +0 -1
  1532. package/dist/team/__tests__/role-router.test.js +0 -263
  1533. package/dist/team/__tests__/role-router.test.js.map +0 -1
  1534. package/dist/team/__tests__/runtime-cli.test.d.ts +0 -2
  1535. package/dist/team/__tests__/runtime-cli.test.d.ts.map +0 -1
  1536. package/dist/team/__tests__/runtime-cli.test.js +0 -304
  1537. package/dist/team/__tests__/runtime-cli.test.js.map +0 -1
  1538. package/dist/team/__tests__/runtime.test.d.ts +0 -2
  1539. package/dist/team/__tests__/runtime.test.d.ts.map +0 -1
  1540. package/dist/team/__tests__/runtime.test.js +0 -5734
  1541. package/dist/team/__tests__/runtime.test.js.map +0 -1
  1542. package/dist/team/__tests__/scaling.test.d.ts +0 -2
  1543. package/dist/team/__tests__/scaling.test.d.ts.map +0 -1
  1544. package/dist/team/__tests__/scaling.test.js +0 -1005
  1545. package/dist/team/__tests__/scaling.test.js.map +0 -1
  1546. package/dist/team/__tests__/shutdown-fallback.test.d.ts +0 -2
  1547. package/dist/team/__tests__/shutdown-fallback.test.d.ts.map +0 -1
  1548. package/dist/team/__tests__/shutdown-fallback.test.js +0 -125
  1549. package/dist/team/__tests__/shutdown-fallback.test.js.map +0 -1
  1550. package/dist/team/__tests__/state-root.test.d.ts +0 -2
  1551. package/dist/team/__tests__/state-root.test.d.ts.map +0 -1
  1552. package/dist/team/__tests__/state-root.test.js +0 -195
  1553. package/dist/team/__tests__/state-root.test.js.map +0 -1
  1554. package/dist/team/__tests__/state.test.d.ts +0 -2
  1555. package/dist/team/__tests__/state.test.d.ts.map +0 -1
  1556. package/dist/team/__tests__/state.test.js +0 -1859
  1557. package/dist/team/__tests__/state.test.js.map +0 -1
  1558. package/dist/team/__tests__/team-identity.test.d.ts +0 -2
  1559. package/dist/team/__tests__/team-identity.test.d.ts.map +0 -1
  1560. package/dist/team/__tests__/team-identity.test.js +0 -166
  1561. package/dist/team/__tests__/team-identity.test.js.map +0 -1
  1562. package/dist/team/__tests__/team-ops-contract.test.d.ts +0 -2
  1563. package/dist/team/__tests__/team-ops-contract.test.d.ts.map +0 -1
  1564. package/dist/team/__tests__/team-ops-contract.test.js +0 -96
  1565. package/dist/team/__tests__/team-ops-contract.test.js.map +0 -1
  1566. package/dist/team/__tests__/tmux-claude-workers-demo.test.d.ts +0 -2
  1567. package/dist/team/__tests__/tmux-claude-workers-demo.test.d.ts.map +0 -1
  1568. package/dist/team/__tests__/tmux-claude-workers-demo.test.js +0 -191
  1569. package/dist/team/__tests__/tmux-claude-workers-demo.test.js.map +0 -1
  1570. package/dist/team/__tests__/tmux-session.test.d.ts +0 -2
  1571. package/dist/team/__tests__/tmux-session.test.d.ts.map +0 -1
  1572. package/dist/team/__tests__/tmux-session.test.js +0 -3785
  1573. package/dist/team/__tests__/tmux-session.test.js.map +0 -1
  1574. package/dist/team/__tests__/tmux-test-fixture.d.ts +0 -20
  1575. package/dist/team/__tests__/tmux-test-fixture.d.ts.map +0 -1
  1576. package/dist/team/__tests__/tmux-test-fixture.js +0 -152
  1577. package/dist/team/__tests__/tmux-test-fixture.js.map +0 -1
  1578. package/dist/team/__tests__/tmux-test-fixture.test.d.ts +0 -2
  1579. package/dist/team/__tests__/tmux-test-fixture.test.d.ts.map +0 -1
  1580. package/dist/team/__tests__/tmux-test-fixture.test.js +0 -113
  1581. package/dist/team/__tests__/tmux-test-fixture.test.js.map +0 -1
  1582. package/dist/team/__tests__/worker-bootstrap.test.d.ts +0 -2
  1583. package/dist/team/__tests__/worker-bootstrap.test.d.ts.map +0 -1
  1584. package/dist/team/__tests__/worker-bootstrap.test.js +0 -685
  1585. package/dist/team/__tests__/worker-bootstrap.test.js.map +0 -1
  1586. package/dist/team/__tests__/worker-runtime-identity.test.d.ts +0 -2
  1587. package/dist/team/__tests__/worker-runtime-identity.test.d.ts.map +0 -1
  1588. package/dist/team/__tests__/worker-runtime-identity.test.js +0 -250
  1589. package/dist/team/__tests__/worker-runtime-identity.test.js.map +0 -1
  1590. package/dist/team/__tests__/worktree.test.d.ts +0 -2
  1591. package/dist/team/__tests__/worktree.test.d.ts.map +0 -1
  1592. package/dist/team/__tests__/worktree.test.js +0 -317
  1593. package/dist/team/__tests__/worktree.test.js.map +0 -1
  1594. package/dist/utils/__tests__/agents-md.test.d.ts +0 -2
  1595. package/dist/utils/__tests__/agents-md.test.d.ts.map +0 -1
  1596. package/dist/utils/__tests__/agents-md.test.js +0 -52
  1597. package/dist/utils/__tests__/agents-md.test.js.map +0 -1
  1598. package/dist/utils/__tests__/agents-model-table.test.d.ts +0 -2
  1599. package/dist/utils/__tests__/agents-model-table.test.d.ts.map +0 -1
  1600. package/dist/utils/__tests__/agents-model-table.test.js +0 -104
  1601. package/dist/utils/__tests__/agents-model-table.test.js.map +0 -1
  1602. package/dist/utils/__tests__/dep-versions.test.d.ts +0 -2
  1603. package/dist/utils/__tests__/dep-versions.test.d.ts.map +0 -1
  1604. package/dist/utils/__tests__/dep-versions.test.js +0 -46
  1605. package/dist/utils/__tests__/dep-versions.test.js.map +0 -1
  1606. package/dist/utils/__tests__/package.test.d.ts +0 -2
  1607. package/dist/utils/__tests__/package.test.d.ts.map +0 -1
  1608. package/dist/utils/__tests__/package.test.js +0 -21
  1609. package/dist/utils/__tests__/package.test.js.map +0 -1
  1610. package/dist/utils/__tests__/paths.test.d.ts +0 -2
  1611. package/dist/utils/__tests__/paths.test.d.ts.map +0 -1
  1612. package/dist/utils/__tests__/paths.test.js +0 -541
  1613. package/dist/utils/__tests__/paths.test.js.map +0 -1
  1614. package/dist/utils/__tests__/platform-command.test.d.ts +0 -2
  1615. package/dist/utils/__tests__/platform-command.test.d.ts.map +0 -1
  1616. package/dist/utils/__tests__/platform-command.test.js +0 -410
  1617. package/dist/utils/__tests__/platform-command.test.js.map +0 -1
  1618. package/dist/utils/__tests__/repo-deps.test.d.ts +0 -2
  1619. package/dist/utils/__tests__/repo-deps.test.d.ts.map +0 -1
  1620. package/dist/utils/__tests__/repo-deps.test.js +0 -71
  1621. package/dist/utils/__tests__/repo-deps.test.js.map +0 -1
  1622. package/dist/verification/__tests__/ci-rust-gates.test.d.ts +0 -2
  1623. package/dist/verification/__tests__/ci-rust-gates.test.d.ts.map +0 -1
  1624. package/dist/verification/__tests__/ci-rust-gates.test.js +0 -89
  1625. package/dist/verification/__tests__/ci-rust-gates.test.js.map +0 -1
  1626. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.d.ts +0 -2
  1627. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.d.ts.map +0 -1
  1628. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +0 -54
  1629. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +0 -1
  1630. package/dist/verification/__tests__/explore-harness-release-workflow.test.d.ts +0 -2
  1631. package/dist/verification/__tests__/explore-harness-release-workflow.test.d.ts.map +0 -1
  1632. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +0 -73
  1633. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +0 -1
  1634. package/dist/verification/__tests__/native-release-manifest.test.d.ts +0 -2
  1635. package/dist/verification/__tests__/native-release-manifest.test.d.ts.map +0 -1
  1636. package/dist/verification/__tests__/native-release-manifest.test.js +0 -80
  1637. package/dist/verification/__tests__/native-release-manifest.test.js.map +0 -1
  1638. package/dist/verification/__tests__/pr-check-workflow.test.d.ts +0 -2
  1639. package/dist/verification/__tests__/pr-check-workflow.test.d.ts.map +0 -1
  1640. package/dist/verification/__tests__/pr-check-workflow.test.js +0 -27
  1641. package/dist/verification/__tests__/pr-check-workflow.test.js.map +0 -1
  1642. package/dist/verification/__tests__/ralph-persistence-gate.test.d.ts +0 -2
  1643. package/dist/verification/__tests__/ralph-persistence-gate.test.d.ts.map +0 -1
  1644. package/dist/verification/__tests__/ralph-persistence-gate.test.js +0 -55
  1645. package/dist/verification/__tests__/ralph-persistence-gate.test.js.map +0 -1
  1646. package/dist/verification/__tests__/rust-runtime-thin-adapter-gate.test.d.ts +0 -2
  1647. package/dist/verification/__tests__/rust-runtime-thin-adapter-gate.test.d.ts.map +0 -1
  1648. package/dist/verification/__tests__/rust-runtime-thin-adapter-gate.test.js +0 -32
  1649. package/dist/verification/__tests__/rust-runtime-thin-adapter-gate.test.js.map +0 -1
  1650. package/dist/verification/__tests__/verifier.test.d.ts +0 -2
  1651. package/dist/verification/__tests__/verifier.test.d.ts.map +0 -1
  1652. package/dist/verification/__tests__/verifier.test.js +0 -113
  1653. package/dist/verification/__tests__/verifier.test.js.map +0 -1
  1654. package/dist/visual/__tests__/verdict.test.d.ts +0 -2
  1655. package/dist/visual/__tests__/verdict.test.d.ts.map +0 -1
  1656. package/dist/visual/__tests__/verdict.test.js +0 -81
  1657. package/dist/visual/__tests__/verdict.test.js.map +0 -1
  1658. package/dist/wiki/__tests__/cjk-tokenize.test.d.ts +0 -12
  1659. package/dist/wiki/__tests__/cjk-tokenize.test.d.ts.map +0 -1
  1660. package/dist/wiki/__tests__/cjk-tokenize.test.js +0 -139
  1661. package/dist/wiki/__tests__/cjk-tokenize.test.js.map +0 -1
  1662. package/dist/wiki/__tests__/crlf-parse.test.d.ts +0 -2
  1663. package/dist/wiki/__tests__/crlf-parse.test.d.ts.map +0 -1
  1664. package/dist/wiki/__tests__/crlf-parse.test.js +0 -24
  1665. package/dist/wiki/__tests__/crlf-parse.test.js.map +0 -1
  1666. package/dist/wiki/__tests__/escape-newline.test.d.ts +0 -2
  1667. package/dist/wiki/__tests__/escape-newline.test.d.ts.map +0 -1
  1668. package/dist/wiki/__tests__/escape-newline.test.js +0 -45
  1669. package/dist/wiki/__tests__/escape-newline.test.js.map +0 -1
  1670. package/dist/wiki/__tests__/ingest.test.d.ts +0 -5
  1671. package/dist/wiki/__tests__/ingest.test.d.ts.map +0 -1
  1672. package/dist/wiki/__tests__/ingest.test.js +0 -181
  1673. package/dist/wiki/__tests__/ingest.test.js.map +0 -1
  1674. package/dist/wiki/__tests__/lint.test.d.ts +0 -5
  1675. package/dist/wiki/__tests__/lint.test.d.ts.map +0 -1
  1676. package/dist/wiki/__tests__/lint.test.js +0 -163
  1677. package/dist/wiki/__tests__/lint.test.js.map +0 -1
  1678. package/dist/wiki/__tests__/query.test.d.ts +0 -5
  1679. package/dist/wiki/__tests__/query.test.d.ts.map +0 -1
  1680. package/dist/wiki/__tests__/query.test.js +0 -141
  1681. package/dist/wiki/__tests__/query.test.js.map +0 -1
  1682. package/dist/wiki/__tests__/reserved-file-guard.test.d.ts +0 -2
  1683. package/dist/wiki/__tests__/reserved-file-guard.test.d.ts.map +0 -1
  1684. package/dist/wiki/__tests__/reserved-file-guard.test.js +0 -44
  1685. package/dist/wiki/__tests__/reserved-file-guard.test.js.map +0 -1
  1686. package/dist/wiki/__tests__/session-hooks.test.d.ts +0 -5
  1687. package/dist/wiki/__tests__/session-hooks.test.d.ts.map +0 -1
  1688. package/dist/wiki/__tests__/session-hooks.test.js +0 -36
  1689. package/dist/wiki/__tests__/session-hooks.test.js.map +0 -1
  1690. package/dist/wiki/__tests__/slug-nonascii.test.d.ts +0 -2
  1691. package/dist/wiki/__tests__/slug-nonascii.test.d.ts.map +0 -1
  1692. package/dist/wiki/__tests__/slug-nonascii.test.js +0 -30
  1693. package/dist/wiki/__tests__/slug-nonascii.test.js.map +0 -1
  1694. package/dist/wiki/__tests__/storage.test.d.ts +0 -5
  1695. package/dist/wiki/__tests__/storage.test.d.ts.map +0 -1
  1696. package/dist/wiki/__tests__/storage.test.js +0 -278
  1697. package/dist/wiki/__tests__/storage.test.js.map +0 -1
  1698. package/dist/wiki/__tests__/test-helpers.d.ts +0 -31
  1699. package/dist/wiki/__tests__/test-helpers.d.ts.map +0 -1
  1700. package/dist/wiki/__tests__/test-helpers.js +0 -108
  1701. package/dist/wiki/__tests__/test-helpers.js.map +0 -1
  1702. package/docs/contracts/ralph-cancel-contract.md +0 -23
  1703. package/docs/contracts/ralph-state-contract.md +0 -95
  1704. package/docs/issues/team-ralph-followup-team.md +0 -38
  1705. package/docs/qa/ralph-persistence-gate.md +0 -59
  1706. package/docs/reference/ralph-parity-matrix.md +0 -25
  1707. package/docs/reference/ralph-upstream-baseline.md +0 -34
  1708. package/plugins/roblox-ai-os-creator-skills/skills/ralph/SKILL.md +0 -269
  1709. package/plugins/roblox-ai-os-creator-skills/skills/ralplan/SKILL.md +0 -162
  1710. package/prompts/api-reviewer.md +0 -113
  1711. package/prompts/information-architect.md +0 -226
  1712. package/prompts/performance-reviewer.md +0 -109
  1713. package/prompts/product-analyst.md +0 -304
  1714. package/prompts/product-manager.md +0 -245
  1715. package/prompts/qa-tester.md +0 -124
  1716. package/prompts/quality-reviewer.md +0 -123
  1717. package/prompts/quality-strategist.md +0 -274
  1718. package/prompts/style-reviewer.md +0 -102
  1719. package/prompts/ux-researcher.md +0 -327
  1720. package/skills/frontend-ui-ux/SKILL.md +0 -34
  1721. package/skills/ralph/SKILL.md +0 -269
  1722. package/skills/ralplan/SKILL.md +0 -162
  1723. package/src/scripts/eval/eval-adaptive-sort-optimization.py +0 -24
  1724. package/src/scripts/eval/eval-candidate-handoff.ts +0 -8
  1725. package/src/scripts/eval/eval-cli-discoverability.ts +0 -40
  1726. package/src/scripts/eval/eval-fresh-run-tagging.ts +0 -8
  1727. package/src/scripts/eval/eval-help-consistency.ts +0 -11
  1728. package/src/scripts/eval/eval-in-action-cat-shellout-demo.ts +0 -31
  1729. package/src/scripts/eval/eval-ml-kaggle-model-optimization.py +0 -29
  1730. package/src/scripts/eval/eval-noisy-bayesopt-highdim.py +0 -44
  1731. package/src/scripts/eval/eval-noisy-latent-subspace-discovery.py +0 -44
  1732. package/src/scripts/eval/eval-parity-smoke.ts +0 -20
  1733. package/src/scripts/eval/eval-parity-sweep.ts +0 -26
  1734. package/src/scripts/eval/eval-resume-dirty-guard.ts +0 -8
  1735. package/src/scripts/eval/eval-security-path-traversal.ts +0 -38
  1736. package/src/scripts/run-autoresearch-showcase.sh +0 -75
  1737. /package/docs/{migration-mainline-post-v0.4.4.md → archive/migration-mainline-post-v0.4.4.md} +0 -0
  1738. /package/docs/{qa-plan-0.4.2.md → archive/qa-plan-0.4.2.md} +0 -0
  1739. /package/docs/{qa-report-0.4.2.md → archive/qa-report-0.4.2.md} +0 -0
@@ -1,3898 +0,0 @@
1
- import { describe, it } from 'node:test';
2
- import { existsSync } from 'node:fs';
3
- import { once } from 'node:events';
4
- import assert from 'node:assert/strict';
5
- import { appendFile, chmod, mkdtemp, mkdir, readFile, rename, rm, symlink, writeFile } from 'node:fs/promises';
6
- import { tmpdir } from 'node:os';
7
- import { join, delimiter as pathDelimiter } from 'node:path';
8
- import { fileURLToPath } from 'node:url';
9
- import { spawn, spawnSync } from 'node:child_process';
10
- import { randomUUID } from 'node:crypto';
11
- import { initTeamState, enqueueDispatchRequest, readDispatchRequest } from '../../team/state.js';
12
- import { buildTmuxSessionName, buildWindowsMsysBackgroundHelperBootstrapScript } from '../../cli/index.js';
13
- import { writeSessionStart } from '../session.js';
14
- const DEFAULT_AUTO_NUDGE_RESPONSE = 'continue with the current task only if it is already authorized';
15
- function distScript(name) {
16
- return fileURLToPath(new URL(`../../../dist/scripts/${name}`, import.meta.url));
17
- }
18
- function prependPath(dir) {
19
- const tail = process.env.PATH || process.env.Path || '';
20
- return tail ? `${dir}${pathDelimiter}${tail}` : dir;
21
- }
22
- async function writeFakeTmuxExecutable(fakeBinDir, scriptBody) {
23
- const normalized = scriptBody.replace(/\r\n/g, '\n');
24
- if (process.platform === 'win32') {
25
- const innerPath = join(fakeBinDir, 'tmux-stub.sh');
26
- await writeFile(innerPath, normalized, 'utf-8');
27
- await chmod(innerPath, 0o755).catch(() => { });
28
- const bashCandidates = [
29
- join(process.env.ProgramFiles || '', 'Git', 'bin', 'bash.exe'),
30
- join(process.env['ProgramFiles(x86)'] || '', 'Git', 'bin', 'bash.exe'),
31
- ];
32
- let bashPath = '';
33
- for (const candidate of bashCandidates) {
34
- if (candidate && existsSync(candidate)) {
35
- bashPath = candidate;
36
- break;
37
- }
38
- }
39
- if (!bashPath) {
40
- throw new Error('notify-fallback-watcher tests on win32 require Git for Windows (bash.exe)');
41
- }
42
- // Batch `tmux.cmd` forwards with `%*` and re-parses `%42`-style tmux pane ids; PowerShell -File keeps argv literal.
43
- const tmuxPs1 = join(fakeBinDir, 'tmux.ps1');
44
- const bashForPs1 = bashPath.replace(/'/g, "''");
45
- const ps1Body = [
46
- 'param(',
47
- ' [Parameter(Mandatory = $false, ValueFromRemainingArguments = $true)]',
48
- ' [string[]] $Remaining',
49
- ')',
50
- "$ErrorActionPreference = 'Stop'",
51
- `$bash = '${bashForPs1}'`,
52
- "$stub = Join-Path $PSScriptRoot 'tmux-stub.sh'",
53
- '& $bash $stub @Remaining',
54
- 'exit $LASTEXITCODE',
55
- '',
56
- ].join('\r\n');
57
- await writeFile(tmuxPs1, ps1Body, 'utf-8');
58
- return;
59
- }
60
- const shimPath = join(fakeBinDir, 'tmux');
61
- await writeFile(shimPath, normalized, 'utf-8');
62
- await chmod(shimPath, 0o755);
63
- }
64
- async function appendLine(path, line) {
65
- const prev = await readFile(path, 'utf-8');
66
- const content = prev + `${JSON.stringify(line)}\n`;
67
- await writeFile(path, content);
68
- }
69
- function todaySessionDir(baseHome) {
70
- const now = new Date();
71
- return join(baseHome, '.codex', 'sessions', String(now.getUTCFullYear()), String(now.getUTCMonth() + 1).padStart(2, '0'), String(now.getUTCDate()).padStart(2, '0'));
72
- }
73
- async function readLines(path) {
74
- const content = await readFile(path, 'utf-8').catch(() => '');
75
- return content.split('\n').map(s => s.trim()).filter(Boolean);
76
- }
77
- async function readJsonLines(path) {
78
- const content = await readFile(path, 'utf-8').catch(() => '');
79
- return content
80
- .split('\n')
81
- .map((line) => line.trim())
82
- .filter(Boolean)
83
- .map((line) => JSON.parse(line));
84
- }
85
- async function writeCanonicalWatcherTeamFixture(wd, { teamName = 'dispatch-team', sessionId = 'sess-current', ownerSessionId = sessionId, coarseState = 'missing', terminal = false, } = {}) {
86
- const stateDir = join(wd, '.rcs', 'state');
87
- const teamDir = join(stateDir, 'team', teamName);
88
- const nowIso = new Date().toISOString();
89
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
90
- await mkdir(join(teamDir, 'workers'), { recursive: true });
91
- await writeFile(join(stateDir, 'session.json'), JSON.stringify({ session_id: sessionId }, null, 2));
92
- if (coarseState !== 'missing') {
93
- await writeFile(join(stateDir, 'team-state.json'), JSON.stringify({
94
- active: coarseState === 'active',
95
- team_name: teamName,
96
- current_phase: terminal ? 'complete' : 'team-exec',
97
- ...(terminal ? { completed_at: nowIso } : {}),
98
- }, null, 2));
99
- }
100
- await writeFile(join(stateDir, 'hud-state.json'), JSON.stringify({
101
- last_turn_at: new Date(Date.now() - 300_000).toISOString(),
102
- turn_count: 3,
103
- }, null, 2));
104
- const manifest = {
105
- schema_version: 2,
106
- name: teamName,
107
- task: 'canonical watcher fallback repro',
108
- leader: {
109
- session_id: ownerSessionId,
110
- worker_id: 'leader-fixed',
111
- role: 'coordinator',
112
- },
113
- policy: {
114
- worker_launch_mode: 'interactive',
115
- display_mode: 'split_pane',
116
- dispatch_mode: 'hook_preferred_with_fallback',
117
- dispatch_ack_timeout_ms: 2000,
118
- },
119
- governance: {
120
- delegation_only: false,
121
- plan_approval_required: false,
122
- nested_teams_allowed: false,
123
- one_team_per_leader_session: true,
124
- cleanup_requires_all_workers_inactive: true,
125
- },
126
- lifecycle_profile: 'default',
127
- permissions_snapshot: {
128
- approval_mode: 'never',
129
- sandbox_mode: 'danger-full-access',
130
- network_access: true,
131
- },
132
- tmux_session: `${teamName}:0`,
133
- leader_pane_id: '%42',
134
- hud_pane_id: null,
135
- resize_hook_name: null,
136
- resize_hook_target: null,
137
- worker_count: 1,
138
- next_task_id: 1,
139
- workers: [
140
- { name: 'worker-1', index: 1, pane_id: '%42', role: 'executor' },
141
- ],
142
- created_at: nowIso,
143
- };
144
- await writeFile(join(teamDir, 'manifest.v2.json'), JSON.stringify(manifest, null, 2));
145
- await writeFile(join(teamDir, 'config.json'), JSON.stringify({
146
- name: teamName,
147
- tmux_session: `${teamName}:0`,
148
- leader_pane_id: '%42',
149
- workers: [
150
- { name: 'worker-1', pane_id: '%42' },
151
- ],
152
- }, null, 2));
153
- await writeFile(join(teamDir, 'phase.json'), JSON.stringify({
154
- current_phase: terminal ? 'complete' : 'team-exec',
155
- updated_at: nowIso,
156
- transitions: terminal ? [{ from: 'team-exec', to: 'complete', at: nowIso }] : [],
157
- }, null, 2));
158
- }
159
- async function sleep(ms) {
160
- await new Promise(resolve => setTimeout(resolve, ms));
161
- }
162
- async function waitFor(predicate, timeoutMs = 3000, stepMs = 50) {
163
- const deadline = Date.now() + timeoutMs;
164
- while (Date.now() < deadline) {
165
- if (await predicate())
166
- return;
167
- await sleep(stepMs);
168
- }
169
- throw new Error(`waitFor timed out after ${timeoutMs}ms`);
170
- }
171
- function isPidAlive(pid) {
172
- if (!pid || !Number.isFinite(pid) || pid <= 0)
173
- return false;
174
- try {
175
- process.kill(pid, 0);
176
- return true;
177
- }
178
- catch {
179
- return false;
180
- }
181
- }
182
- async function waitForExit(child, timeoutMs = 4000) {
183
- if (child.exitCode !== null || child.signalCode !== null)
184
- return;
185
- await Promise.race([
186
- once(child, 'exit'),
187
- sleep(timeoutMs).then(() => {
188
- throw new Error(`process ${child.pid ?? 'unknown'} did not exit within ${timeoutMs}ms`);
189
- }),
190
- ]);
191
- }
192
- function defaultAutoNudgePattern(targetPane) {
193
- return new RegExp(`send-keys -t ${targetPane} -l ${DEFAULT_AUTO_NUDGE_RESPONSE.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')} \\[RCS_TMUX_INJECT\\]`);
194
- }
195
- function bashPathForStub(filePath) {
196
- return filePath.replace(/\\/g, '/');
197
- }
198
- function buildFakeTmux(tmuxLogPath, options = {}) {
199
- const tmuxLogPathBash = bashPathForStub(tmuxLogPath);
200
- return `#!/usr/bin/env bash
201
- set -eu
202
- echo "$@" >> "${tmuxLogPathBash}"
203
- cmd="$1"
204
- shift || true
205
- if [[ "$cmd" == "capture-pane" ]]; then
206
- if [[ -n "\${RCS_TEST_CAPTURE_SEQUENCE_FILE:-}" && -f "\${RCS_TEST_CAPTURE_SEQUENCE_FILE}" ]]; then
207
- counterFile="\${RCS_TEST_CAPTURE_COUNTER_FILE:-\${RCS_TEST_CAPTURE_SEQUENCE_FILE}.idx}"
208
- idx=0
209
- if [[ -f "$counterFile" ]]; then idx="$(cat "$counterFile")"; fi
210
- lineNo=$((idx + 1))
211
- line="$(sed -n "\${lineNo}p" "\${RCS_TEST_CAPTURE_SEQUENCE_FILE}" || true)"
212
- if [[ -z "$line" ]]; then
213
- line="$(tail -n 1 "\${RCS_TEST_CAPTURE_SEQUENCE_FILE}" || true)"
214
- fi
215
- printf "%s\\n" "$line"
216
- echo "$lineNo" > "$counterFile"
217
- exit 0
218
- fi
219
- if [[ -n "\${RCS_TEST_CAPTURE_FILE:-}" && -f "\${RCS_TEST_CAPTURE_FILE}" ]]; then
220
- cat "\${RCS_TEST_CAPTURE_FILE}"
221
- fi
222
- exit 0
223
- fi
224
- if [[ "$cmd" == "display-message" ]]; then
225
- target=""
226
- fmt=""
227
- while [[ "$#" -gt 0 ]]; do
228
- case "$1" in
229
- -t)
230
- shift
231
- target="$1"
232
- ;;
233
- *)
234
- fmt="$1"
235
- ;;
236
- esac
237
- shift || true
238
- done
239
- if [[ "$fmt" == "#{pane_in_mode}" ]]; then
240
- echo "0"
241
- exit 0
242
- fi
243
- if [[ "$fmt" == "#{pane_id}" ]]; then
244
- # Real tmux resolves session:window targets to a %pane id; mirror that for team worker targets.
245
- if [[ -n "$target" && "$target" != %* && "$target" == *.* ]]; then
246
- echo "%42"
247
- exit 0
248
- fi
249
- echo "\${target:-%42}"
250
- exit 0
251
- fi
252
- if [[ "$fmt" == "#{pane_current_path}" ]]; then
253
- dirname "${tmuxLogPathBash}"
254
- exit 0
255
- fi
256
- if [[ "$fmt" == "#{pane_current_command}" ]]; then
257
- echo "codex"
258
- exit 0
259
- fi
260
- if [[ "$fmt" == "#S" ]]; then
261
- echo "\${RCS_TEST_TMUX_SESSION_NAME:-session-test}"
262
- exit 0
263
- fi
264
- exit 0
265
- fi
266
- if [[ "$cmd" == "send-keys" ]]; then
267
- sendKeysArgs="$*"
268
- sendKeysArgs="\${sendKeysArgs//$'\\r'/}"
269
- if [[ "${options.failSendKeys === true ? '1' : '0'}" == "1" ]]; then
270
- echo "send failed" >&2
271
- exit 1
272
- fi
273
- if [[ -n "${options.failSendKeysMatch || ''}" && "$sendKeysArgs" == *"${options.failSendKeysMatch || ''}"* ]]; then
274
- echo "send failed" >&2
275
- exit 1
276
- fi
277
- exit 0
278
- fi
279
- if [[ "$cmd" == "list-panes" ]]; then
280
- target=""
281
- while [[ "$#" -gt 0 ]]; do
282
- case "$1" in
283
- -t)
284
- shift
285
- target="$1"
286
- ;;
287
- esac
288
- shift || true
289
- done
290
- if [[ -n "$target" ]]; then
291
- printf "%%42\tcodex\tcodex\n"
292
- exit 0
293
- fi
294
- echo "%42 1"
295
- exit 0
296
- fi
297
- exit 0
298
- `;
299
- }
300
- function buildManagedRalphTmux(tmuxLogPath, options) {
301
- const { cwd, managedSessionName, anchorPane, livePane, codexPanes, missingAnchor = false } = options;
302
- const tmuxLogPathBash = bashPathForStub(tmuxLogPath);
303
- const cwdBash = bashPathForStub(cwd);
304
- const panes = (codexPanes && codexPanes.length > 0)
305
- ? codexPanes
306
- : [{ paneId: livePane, active: true, currentCommand: 'codex', startCommand: 'codex' }];
307
- const listPaneOutput = panes
308
- .map((pane) => {
309
- const paneId = pane.paneId;
310
- const active = pane.active ? '1' : '0';
311
- const currentCommand = pane.currentCommand || 'codex';
312
- const startCommand = pane.startCommand || 'codex';
313
- return `${paneId}\t${active}\t${currentCommand}\t${startCommand}`;
314
- })
315
- .join('\n');
316
- const paneCommandBranches = panes
317
- .map((pane) => {
318
- const currentCommand = (pane.currentCommand || 'codex').replace(/"/g, '\\"');
319
- const startCommand = (pane.startCommand || 'codex').replace(/"/g, '\\"');
320
- return ` if [[ "$format" == "#{pane_current_command}" && "$target" == "${pane.paneId}" ]]; then
321
- echo "${currentCommand}"
322
- exit 0
323
- fi
324
- if [[ "$format" == "#{pane_start_command}" && "$target" == "${pane.paneId}" ]]; then
325
- echo "${startCommand}"
326
- exit 0
327
- fi`;
328
- })
329
- .join('\n');
330
- return `#!/usr/bin/env bash
331
- set -eu
332
- echo "$@" >> "${tmuxLogPathBash}"
333
- cmd="$1"
334
- shift || true
335
- if [[ "$cmd" == "display-message" ]]; then
336
- target=""
337
- format=""
338
- while [[ "$#" -gt 0 ]]; do
339
- case "$1" in
340
- -p) shift ;;
341
- -t) target="$2"; shift 2 ;;
342
- *) format="$1"; shift ;;
343
- esac
344
- done
345
- if [[ "$target" == "${anchorPane}" && "${missingAnchor ? '1' : '0'}" == "1" ]]; then
346
- echo "pane missing" >&2
347
- exit 1
348
- fi
349
- if [[ "$format" == "#{pane_in_mode}" ]]; then
350
- echo "0"
351
- exit 0
352
- fi
353
- if [[ "$format" == "#{pane_id}" ]]; then
354
- echo "$target"
355
- exit 0
356
- fi
357
- if [[ "$format" == "#{pane_current_path}" ]]; then
358
- echo "${cwdBash}"
359
- exit 0
360
- fi
361
- ${paneCommandBranches}
362
- if [[ "$format" == "#S" ]]; then
363
- if [[ "$target" == "${anchorPane}" || "$target" == "${livePane}" ]]; then
364
- echo "${managedSessionName}"
365
- exit 0
366
- fi
367
- echo "unknown target" >&2
368
- exit 1
369
- fi
370
- exit 0
371
- fi
372
- if [[ "$cmd" == "list-panes" ]]; then
373
- target=""
374
- while [[ "$#" -gt 0 ]]; do
375
- case "$1" in
376
- -s)
377
- shift
378
- ;;
379
- -F)
380
- shift 2
381
- ;;
382
- -t)
383
- shift
384
- target="$1"
385
- shift
386
- ;;
387
- *)
388
- shift
389
- ;;
390
- esac
391
- done
392
- if [[ "$target" == "${managedSessionName}" ]]; then
393
- printf '%s\n' "${listPaneOutput}"
394
- exit 0
395
- fi
396
- echo "can't find session" >&2
397
- exit 1
398
- fi
399
- if [[ "$cmd" == "capture-pane" ]]; then
400
- exit 0
401
- fi
402
- if [[ "$cmd" == "send-keys" ]]; then
403
- exit 0
404
- fi
405
- exit 0
406
- `;
407
- }
408
- function buildCleanNotifyEnv(overrides = {}) {
409
- return {
410
- ...process.env,
411
- RCS_TEAM_WORKER: '',
412
- RCS_TEAM_STATE_ROOT: '',
413
- RCS_TEAM_LEADER_CWD: '',
414
- RCS_MODEL_INSTRUCTIONS_FILE: '',
415
- TMUX: '',
416
- TMUX_PANE: '',
417
- ...overrides,
418
- };
419
- }
420
- function windowsLocalAppTempDir() {
421
- if (process.platform !== 'win32')
422
- return null;
423
- const base = process.env.USERPROFILE || process.env.HOME;
424
- if (!base)
425
- return null;
426
- const candidate = join(base, 'AppData', 'Local', 'Temp');
427
- return existsSync(candidate) ? candidate : null;
428
- }
429
- /** mkdtemp roots: Node caches `os.tmpdir()`, so mutating TMP in describe does not fix Cursor/adjacent install dirs on Windows. */
430
- function safeTestTmpdir() {
431
- const local = windowsLocalAppTempDir();
432
- if (local)
433
- return local;
434
- return tmpdir();
435
- }
436
- describe('notify-fallback watcher', () => {
437
- it('uses offset-bounded rollout reads instead of re-reading whole tracked files', async () => {
438
- const source = await readFile(distScript('notify-fallback-watcher.js'), 'utf-8');
439
- assert.match(source, /async function readFileDelta/);
440
- assert.match(source, /while \(totalBytesRead < length\)/);
441
- assert.match(source, /nextOffset: offset \+ totalBytesRead/);
442
- assert.match(source, /new StringDecoder\('utf8'\)/);
443
- assert.match(source, /decoder\.write\(bytes\)/);
444
- assert.match(source, /const fileStat = await stat\(path\)\.catch\(\(\) => null\);\s*if \(!fileStat\)\s*continue;/);
445
- assert.match(source, /if \(currentSize < meta\.offset\) \{\s*meta\.offset = 0;\s*meta\.partial = '';/);
446
- assert.doesNotMatch(source, /const content = await readFile\(path, 'utf-8'\)[\s\S]*const delta = content\.slice\(meta\.offset\)/);
447
- assert.doesNotMatch(source, /stat\(path\)\.catch\(\(\) => \(\{ size: 0 \}\)\)/);
448
- });
449
- it('one-shot mode forwards only recent task_complete events', async () => {
450
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-once-'));
451
- const tempHome = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-home-'));
452
- const sid = `test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
453
- const sessionDir = todaySessionDir(tempHome);
454
- const rolloutPath = join(sessionDir, `rollout-test-fallback-once-${sid}.jsonl`);
455
- try {
456
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
457
- await mkdir(join(wd, '.rcs', 'state'), { recursive: true });
458
- await mkdir(sessionDir, { recursive: true });
459
- const staleIso = new Date(Date.now() - 60_000).toISOString();
460
- const freshIso = new Date(Date.now() + 2_000).toISOString();
461
- const threadId = `thread-${sid}`;
462
- const staleTurn = `turn-stale-${sid}`;
463
- const freshTurn = `turn-fresh-${sid}`;
464
- const lines = [
465
- {
466
- timestamp: freshIso,
467
- type: 'session_meta',
468
- payload: { id: threadId, cwd: wd },
469
- },
470
- {
471
- timestamp: staleIso,
472
- type: 'event_msg',
473
- payload: {
474
- type: 'task_complete',
475
- turn_id: staleTurn,
476
- last_agent_message: 'stale message',
477
- },
478
- },
479
- {
480
- timestamp: freshIso,
481
- type: 'event_msg',
482
- payload: {
483
- type: 'task_complete',
484
- turn_id: freshTurn,
485
- last_agent_message: 'fresh message',
486
- },
487
- },
488
- ];
489
- await writeFile(rolloutPath, `${lines.map(v => JSON.stringify(v)).join('\n')}\n`);
490
- const watcherScript = distScript('notify-fallback-watcher.js');
491
- const notifyHook = distScript('notify-hook.js');
492
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env: buildCleanNotifyEnv({ HOME: tempHome }) });
493
- assert.equal(result.status, 0, result.stderr || result.stdout);
494
- const turnLog = join(wd, '.rcs', 'logs', `turns-${new Date().toISOString().split('T')[0]}.jsonl`);
495
- const turnLines = await readLines(turnLog);
496
- assert.equal(turnLines.length, 1);
497
- assert.match(turnLines[0], new RegExp(freshTurn));
498
- assert.doesNotMatch(turnLines[0], new RegExp(staleTurn));
499
- const fallbackLog = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
500
- const fallbackEntries = await readJsonLines(fallbackLog);
501
- assert.deepEqual(fallbackEntries.map((entry) => entry.type), ['fallback_notify']);
502
- }
503
- finally {
504
- await rm(wd, { recursive: true, force: true });
505
- await rm(tempHome, { recursive: true, force: true });
506
- await rm(rolloutPath, { force: true });
507
- }
508
- });
509
- it('rotates notify-fallback logs when the size cap is exceeded', async () => {
510
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-once-rotate-'));
511
- const tempHome = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-home-'));
512
- const sid = randomUUID();
513
- const sessionDir = todaySessionDir(tempHome);
514
- const rolloutPath = join(sessionDir, `rollout-test-fallback-rotate-${sid}.jsonl`);
515
- try {
516
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
517
- await mkdir(join(wd, '.rcs', 'state'), { recursive: true });
518
- await mkdir(sessionDir, { recursive: true });
519
- const threadId = `thread-${sid}`;
520
- const turnIds = ['first', 'second', 'third'].map((label) => `turn-${label}-${sid}`);
521
- const nowIso = new Date(Date.now() + 2_000).toISOString();
522
- const lines = [
523
- {
524
- timestamp: nowIso,
525
- type: 'session_meta',
526
- payload: { id: threadId, cwd: wd },
527
- },
528
- ...turnIds.map((turnId) => ({
529
- timestamp: nowIso,
530
- type: 'event_msg',
531
- payload: {
532
- type: 'task_complete',
533
- turn_id: turnId,
534
- last_agent_message: `message for ${turnId}`,
535
- },
536
- })),
537
- ];
538
- await writeFile(rolloutPath, `${lines.map(v => JSON.stringify(v)).join('\n')}\n`);
539
- const watcherScript = distScript('notify-fallback-watcher.js');
540
- const notifyHook = distScript('notify-hook.js');
541
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50', '--log-max-bytes', '1'], { encoding: 'utf-8', env: buildCleanNotifyEnv({ HOME: tempHome }) });
542
- assert.equal(result.status, 0, result.stderr || result.stdout);
543
- const fallbackLog = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
544
- const rotatedLog = `${fallbackLog}.1`;
545
- const currentEntries = await readJsonLines(fallbackLog);
546
- const rotatedEntries = await readJsonLines(rotatedLog);
547
- assert.equal(currentEntries.length, 1);
548
- assert.equal(rotatedEntries.length, 1);
549
- assert.equal(currentEntries[0]?.turn_id, turnIds[2]);
550
- assert.equal(rotatedEntries[0]?.turn_id, turnIds[1]);
551
- assert.deepEqual(currentEntries.map((entry) => entry.type), ['fallback_notify']);
552
- assert.deepEqual(rotatedEntries.map((entry) => entry.type), ['fallback_notify']);
553
- }
554
- finally {
555
- await rm(wd, { recursive: true, force: true });
556
- await rm(tempHome, { recursive: true, force: true });
557
- await rm(rolloutPath, { force: true });
558
- }
559
- });
560
- it('streaming mode buffers partial JSON lines until the newline arrives', async () => {
561
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-stream-partial-'));
562
- const tempHome = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-home-'));
563
- const sid = randomUUID();
564
- const sessionDir = todaySessionDir(tempHome);
565
- const rolloutPath = join(sessionDir, `rollout-test-fallback-stream-partial-${sid}.jsonl`);
566
- try {
567
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
568
- await mkdir(join(wd, '.rcs', 'state'), { recursive: true });
569
- await mkdir(sessionDir, { recursive: true });
570
- const nowIso = new Date().toISOString();
571
- const threadId = `thread-${sid}`;
572
- const partialTurn = `turn-partial-${sid}`;
573
- await writeFile(rolloutPath, `${JSON.stringify({
574
- timestamp: nowIso,
575
- type: 'session_meta',
576
- payload: { id: threadId, cwd: wd },
577
- })}
578
- `);
579
- const watcherScript = distScript('notify-fallback-watcher.js');
580
- const notifyHook = distScript('notify-hook.js');
581
- const watcherStatePath = join(wd, '.rcs', 'state', 'notify-fallback-state.json');
582
- const turnLog = join(wd, '.rcs', 'logs', `turns-${new Date().toISOString().split('T')[0]}.jsonl`);
583
- const child = spawn(process.execPath, [watcherScript, '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '75'], { cwd: wd, stdio: 'ignore', env: buildCleanNotifyEnv({ HOME: tempHome }) });
584
- await waitFor(async () => {
585
- try {
586
- const state = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
587
- return state.tracked_files === 1;
588
- }
589
- catch {
590
- return false;
591
- }
592
- });
593
- const partialPrefix = JSON.stringify({
594
- timestamp: new Date(Date.now() + 500).toISOString(),
595
- type: 'event_msg',
596
- payload: {
597
- type: 'task_complete',
598
- turn_id: partialTurn,
599
- last_agent_message: 'partial message',
600
- },
601
- });
602
- const splitAt = Math.floor(partialPrefix.length / 2);
603
- await writeFile(rolloutPath, `${await readFile(rolloutPath, 'utf-8')}${partialPrefix.slice(0, splitAt)}`);
604
- await sleep(250);
605
- const beforeLines = await readLines(turnLog);
606
- assert.equal(beforeLines.length, 0, 'partial line should not be emitted before newline completes it');
607
- await writeFile(rolloutPath, `${await readFile(rolloutPath, 'utf-8')}${partialPrefix.slice(splitAt)}\n`);
608
- await waitFor(async () => {
609
- const turnLines = await readLines(turnLog);
610
- return turnLines.length === 1 && new RegExp(partialTurn).test(turnLines[0] ?? '');
611
- }, 4000, 75);
612
- child.kill('SIGTERM');
613
- await once(child, 'exit');
614
- const turnLines = await readLines(turnLog);
615
- assert.equal(turnLines.length, 1);
616
- assert.match(turnLines[0], new RegExp(partialTurn));
617
- }
618
- finally {
619
- await rm(wd, { recursive: true, force: true });
620
- await rm(tempHome, { recursive: true, force: true });
621
- await rm(rolloutPath, { force: true });
622
- }
623
- });
624
- it('streaming mode preserves multibyte text split across polling reads', async () => {
625
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-stream-utf8-'));
626
- const tempHome = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-home-'));
627
- const sid = randomUUID();
628
- const sessionDir = todaySessionDir(tempHome);
629
- const rolloutPath = join(sessionDir, `rollout-test-fallback-stream-utf8-${sid}.jsonl`);
630
- try {
631
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
632
- await mkdir(join(wd, '.rcs', 'state'), { recursive: true });
633
- await mkdir(sessionDir, { recursive: true });
634
- const nowIso = new Date().toISOString();
635
- const threadId = `thread-${sid}`;
636
- const utf8Turn = `turn-utf8-${sid}`;
637
- const emojiMessage = 'split emoji 🧪 preserved';
638
- await writeFile(rolloutPath, `${JSON.stringify({
639
- timestamp: nowIso,
640
- type: 'session_meta',
641
- payload: { id: threadId, cwd: wd },
642
- })}\n`);
643
- const watcherScript = distScript('notify-fallback-watcher.js');
644
- const notifyHook = distScript('notify-hook.js');
645
- const watcherStatePath = join(wd, '.rcs', 'state', 'notify-fallback-state.json');
646
- const turnLog = join(wd, '.rcs', 'logs', `turns-${new Date().toISOString().split('T')[0]}.jsonl`);
647
- const child = spawn(process.execPath, [watcherScript, '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '75'], { cwd: wd, stdio: 'ignore', env: buildCleanNotifyEnv({ HOME: tempHome }) });
648
- await waitFor(async () => {
649
- try {
650
- const state = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
651
- return state.tracked_files === 1;
652
- }
653
- catch {
654
- return false;
655
- }
656
- });
657
- const eventLine = `${JSON.stringify({
658
- timestamp: new Date(Date.now() + 500).toISOString(),
659
- type: 'event_msg',
660
- payload: {
661
- type: 'task_complete',
662
- turn_id: utf8Turn,
663
- last_agent_message: emojiMessage,
664
- },
665
- })}\n`;
666
- const bytes = Buffer.from(eventLine, 'utf8');
667
- const emojiOffset = bytes.indexOf(Buffer.from('🧪', 'utf8'));
668
- assert.ok(emojiOffset > 0, 'expected test payload to contain emoji bytes');
669
- await appendFile(rolloutPath, bytes.subarray(0, emojiOffset + 1));
670
- await sleep(250);
671
- assert.equal((await readLines(turnLog)).length, 0, 'incomplete UTF-8 and JSON line should not emit');
672
- const hiddenRolloutPath = `${rolloutPath}.missing`;
673
- await rename(rolloutPath, hiddenRolloutPath);
674
- await sleep(250);
675
- assert.equal((await readLines(turnLog)).length, 0, 'transient missing file should preserve buffered bytes');
676
- await rename(hiddenRolloutPath, rolloutPath);
677
- await appendFile(rolloutPath, bytes.subarray(emojiOffset + 1));
678
- await waitFor(async () => {
679
- const turnLines = await readLines(turnLog);
680
- return turnLines.length === 1 && turnLines[0].includes(utf8Turn) && turnLines[0].includes(emojiMessage);
681
- }, 4000, 75);
682
- child.kill('SIGTERM');
683
- await once(child, 'exit');
684
- const turnLines = await readLines(turnLog);
685
- assert.equal(turnLines.length, 1);
686
- assert.match(turnLines[0], new RegExp(utf8Turn));
687
- assert.match(turnLines[0], /split emoji 🧪 preserved/);
688
- }
689
- finally {
690
- await rm(wd, { recursive: true, force: true });
691
- await rm(tempHome, { recursive: true, force: true });
692
- await rm(rolloutPath, { force: true });
693
- }
694
- });
695
- it('streaming mode tails from EOF and does not replay backlog', async () => {
696
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-stream-'));
697
- const tempHome = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-home-'));
698
- const sid = randomUUID();
699
- const sessionDir = todaySessionDir(tempHome);
700
- const rolloutPath = join(sessionDir, `rollout-test-fallback-stream-${sid}.jsonl`);
701
- try {
702
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
703
- await mkdir(join(wd, '.rcs', 'state'), { recursive: true });
704
- await mkdir(sessionDir, { recursive: true });
705
- const nowIso = new Date().toISOString();
706
- const threadId = `thread-${sid}`;
707
- const oldTurn = `turn-old-${sid}`;
708
- const newTurn = `turn-new-${sid}`;
709
- await writeFile(rolloutPath, `${JSON.stringify({
710
- timestamp: nowIso,
711
- type: 'session_meta',
712
- payload: { id: threadId, cwd: wd },
713
- })}\n${JSON.stringify({
714
- timestamp: nowIso,
715
- type: 'event_msg',
716
- payload: {
717
- type: 'task_complete',
718
- turn_id: oldTurn,
719
- last_agent_message: 'old message',
720
- },
721
- })}\n`);
722
- const watcherScript = distScript('notify-fallback-watcher.js');
723
- const notifyHook = distScript('notify-hook.js');
724
- const watcherStatePath = join(wd, '.rcs', 'state', 'notify-fallback-state.json');
725
- const turnLog = join(wd, '.rcs', 'logs', `turns-${new Date().toISOString().split('T')[0]}.jsonl`);
726
- const child = spawn(process.execPath, [watcherScript, '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '75'], {
727
- cwd: wd,
728
- stdio: 'ignore',
729
- env: buildCleanNotifyEnv({ HOME: tempHome }),
730
- });
731
- await waitFor(async () => {
732
- try {
733
- const state = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
734
- return state.tracked_files === 1;
735
- }
736
- catch {
737
- return false;
738
- }
739
- });
740
- await appendLine(rolloutPath, {
741
- timestamp: new Date(Date.now() + 500).toISOString(),
742
- type: 'event_msg',
743
- payload: {
744
- type: 'task_complete',
745
- turn_id: newTurn,
746
- last_agent_message: 'new message',
747
- },
748
- });
749
- await waitFor(async () => {
750
- const turnLines = await readLines(turnLog);
751
- return turnLines.length === 1 && new RegExp(newTurn).test(turnLines[0] ?? '');
752
- }, 4000, 75);
753
- child.kill('SIGTERM');
754
- await once(child, 'exit');
755
- const turnLines = await readLines(turnLog);
756
- assert.equal(turnLines.length, 1);
757
- assert.match(turnLines[0], new RegExp(newTurn));
758
- assert.doesNotMatch(turnLines[0], new RegExp(oldTurn));
759
- }
760
- finally {
761
- await rm(wd, { recursive: true, force: true });
762
- await rm(tempHome, { recursive: true, force: true });
763
- await rm(rolloutPath, { force: true });
764
- }
765
- });
766
- it('records explicit leader-only dispatch drain state and log visibility in one-shot mode', async () => {
767
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-dispatch-state-'));
768
- try {
769
- await initTeamState('dispatch-team', 'task', 'executor', 1, wd);
770
- await enqueueDispatchRequest('dispatch-team', {
771
- kind: 'inbox',
772
- to_worker: 'worker-1',
773
- worker_index: 1,
774
- trigger_message: 'dispatch ping',
775
- }, wd);
776
- const watcherScript = distScript('notify-fallback-watcher.js');
777
- const notifyHook = distScript('notify-hook.js');
778
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50', '--dispatch-max-per-tick', '1'], { encoding: 'utf-8', env: buildCleanNotifyEnv() });
779
- assert.equal(result.status, 0, result.stderr || result.stdout);
780
- const watcherStatePath = join(wd, '.rcs', 'state', 'notify-fallback-state.json');
781
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
782
- assert.equal(watcherState.dispatch_drain?.enabled, true);
783
- assert.equal(watcherState.dispatch_drain?.leader_only, true);
784
- assert.equal(watcherState.dispatch_drain?.max_per_tick, 1);
785
- assert.equal(watcherState.dispatch_drain?.run_count, 1);
786
- assert.equal(watcherState.dispatch_drain?.last_result?.processed, 1);
787
- const logPath = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
788
- const logEntries = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
789
- const drainEvent = logEntries.find((entry) => entry.type === 'dispatch_drain_tick');
790
- assert.ok(drainEvent, 'expected dispatch_drain_tick log event');
791
- assert.equal(drainEvent.leader_only, true);
792
- assert.equal(drainEvent.processed, 1);
793
- }
794
- finally {
795
- await rm(wd, { recursive: true, force: true });
796
- }
797
- });
798
- it('suppresses idle no-op lifecycle and control-plane logs during authority-only one-shot ticks', async () => {
799
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-authority-noop-'));
800
- try {
801
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
802
- await mkdir(join(wd, '.rcs', 'state'), { recursive: true });
803
- const watcherScript = distScript('notify-fallback-watcher.js');
804
- const notifyHook = distScript('notify-hook.js');
805
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--authority-only', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env: buildCleanNotifyEnv() });
806
- assert.equal(result.status, 0, result.stderr || result.stdout);
807
- const watcherStatePath = join(wd, '.rcs', 'state', 'notify-fallback-state.json');
808
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
809
- assert.equal(watcherState.authority_only, true);
810
- assert.equal(watcherState.dispatch_drain?.run_count, 1);
811
- assert.equal(watcherState.dispatch_drain?.last_result?.processed ?? 0, 0);
812
- assert.equal(watcherState.leader_nudge?.run_count, 1);
813
- assert.equal(watcherState.leader_nudge?.precomputed_leader_stale, false);
814
- assert.equal(watcherState.fallback_auto_nudge?.last_reason, 'hud_state_missing');
815
- const logPath = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
816
- const logContent = await readFile(logPath, 'utf-8').catch(() => '');
817
- assert.equal(logContent.trim(), '');
818
- }
819
- finally {
820
- await rm(wd, { recursive: true, force: true });
821
- }
822
- });
823
- it('suppresses authority-only control-plane ticks when only skill-active-state carries the deep-interview input lock', async () => {
824
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-authority-skill-lock-'));
825
- try {
826
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
827
- await mkdir(join(wd, '.rcs', 'state'), { recursive: true });
828
- await writeFile(join(wd, '.rcs', 'state', 'skill-active-state.json'), JSON.stringify({
829
- version: 1,
830
- active: true,
831
- skill: 'deep-interview',
832
- keyword: 'deep interview',
833
- phase: 'planning',
834
- activated_at: '2026-02-25T00:00:00.000Z',
835
- updated_at: '2026-02-25T00:00:00.000Z',
836
- source: 'keyword-detector',
837
- input_lock: {
838
- active: true,
839
- scope: 'deep-interview-auto-approval',
840
- acquired_at: '2026-02-25T00:00:00.000Z',
841
- blocked_inputs: ['yes', 'y', 'proceed', 'continue', 'ok', 'sure', 'go ahead', 'next i should'],
842
- message: 'Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.',
843
- },
844
- }, null, 2));
845
- const watcherScript = distScript('notify-fallback-watcher.js');
846
- const notifyHook = distScript('notify-hook.js');
847
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--authority-only', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env: buildCleanNotifyEnv() });
848
- assert.equal(result.status, 0, result.stderr || result.stdout);
849
- const watcherStatePath = join(wd, '.rcs', 'state', 'notify-fallback-state.json');
850
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
851
- assert.equal(watcherState.authority_only, true);
852
- assert.equal(watcherState.dispatch_drain?.run_count, 1);
853
- assert.equal(watcherState.leader_nudge?.run_count, 0);
854
- assert.equal(watcherState.fallback_auto_nudge?.last_reason, 'init');
855
- const logPath = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
856
- const logContent = await readFile(logPath, 'utf-8').catch(() => '');
857
- assert.equal(logContent.trim(), '');
858
- }
859
- finally {
860
- await rm(wd, { recursive: true, force: true });
861
- }
862
- });
863
- it('backs off authority-only nudge ticks when the primary watcher is healthy', async () => {
864
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-authority-backed-off-'));
865
- const fakeBinDir = join(wd, 'fake-bin');
866
- const tmuxLogPath = join(wd, 'tmux.log');
867
- const codexHome = join(wd, 'codex-home');
868
- try {
869
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
870
- await mkdir(join(wd, '.rcs', 'state'), { recursive: true });
871
- await mkdir(fakeBinDir, { recursive: true });
872
- await mkdir(codexHome, { recursive: true });
873
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
874
- await writeFile(join(codexHome, '.rcs-config.json'), JSON.stringify({
875
- autoNudge: { enabled: true, delaySec: 0, ttlMs: 30_000 },
876
- }, null, 2));
877
- await writeSessionStart(wd, 'sess-managed-fallback');
878
- await mkdir(join(wd, '.rcs', 'state', 'sessions', 'sess-managed-fallback'), { recursive: true });
879
- await writeFile(join(wd, '.rcs', 'state', 'sessions', 'sess-managed-fallback', 'hud-state.json'), JSON.stringify({
880
- last_turn_at: new Date(Date.now() - 6_000).toISOString(),
881
- turn_count: 7,
882
- last_agent_output: 'Keep going and finish the cleanup from here.',
883
- }, null, 2));
884
- await writeFile(join(wd, '.rcs', 'state', 'notify-fallback.pid'), JSON.stringify({
885
- pid: process.pid,
886
- cwd: wd,
887
- started_at: new Date().toISOString(),
888
- }, null, 2));
889
- await writeFile(join(wd, '.rcs', 'state', 'notify-fallback-state.json'), JSON.stringify({
890
- pid: process.pid,
891
- cwd: wd,
892
- authority_only: false,
893
- poll_ms: 250,
894
- dispatch_drain: { last_tick_at: new Date().toISOString() },
895
- }, null, 2));
896
- const watcherScript = distScript('notify-fallback-watcher.js');
897
- const notifyHook = distScript('notify-hook.js');
898
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--authority-only', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
899
- encoding: 'utf-8',
900
- env: buildCleanNotifyEnv({
901
- PATH: prependPath(fakeBinDir),
902
- CODEX_HOME: codexHome,
903
- RCS_SESSION_ID: 'sess-managed-fallback',
904
- TMUX: '1',
905
- TMUX_PANE: '%42',
906
- RCS_NOTIFY_FALLBACK_AUTO_NUDGE_STALL_MS: '5000',
907
- }),
908
- });
909
- assert.equal(result.status, 0, result.stderr || result.stdout);
910
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
911
- assert.doesNotMatch(tmuxLog, defaultAutoNudgePattern('%42'));
912
- const watcherState = JSON.parse(await readFile(join(wd, '.rcs', 'state', 'notify-fallback-state.json'), 'utf-8'));
913
- assert.equal(watcherState.pid, process.pid, 'authority backoff should preserve the primary watcher state owner');
914
- assert.equal(watcherState.authority_only, false, 'authority backoff should not overwrite primary watcher ownership');
915
- assert.equal(watcherState.authority_backoff?.active, true);
916
- assert.equal(watcherState.authority_backoff?.reason, 'primary_watcher_healthy');
917
- assert.equal(watcherState.authority_backoff?.primary_pid, process.pid);
918
- assert.match(watcherState.dispatch_drain?.last_tick_at ?? '', /^\d{4}-\d{2}-\d{2}T/);
919
- const logPath = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
920
- const logContent = await readFile(logPath, 'utf-8').catch(() => '');
921
- assert.equal(logContent.trim(), '');
922
- }
923
- finally {
924
- await rm(wd, { recursive: true, force: true });
925
- }
926
- });
927
- it('treats symlinked cwd aliases as the same primary watcher during authority handoff', async () => {
928
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-cwd-alias-'));
929
- const aliasWd = `${wd}-alias`;
930
- const fakeBinDir = join(wd, 'fake-bin');
931
- const tmuxLogPath = join(wd, 'tmux.log');
932
- try {
933
- await symlink(wd, aliasWd, process.platform === 'win32' ? 'junction' : 'dir');
934
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
935
- await mkdir(fakeBinDir, { recursive: true });
936
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
937
- await writeSessionStart(wd, 'sess-cwd-alias');
938
- await writeFile(join(wd, '.rcs', 'state', 'notify-fallback.pid'), JSON.stringify({
939
- pid: process.pid,
940
- cwd: wd,
941
- started_at: new Date().toISOString(),
942
- }, null, 2));
943
- await writeFile(join(wd, '.rcs', 'state', 'notify-fallback-state.json'), JSON.stringify({
944
- pid: process.pid,
945
- cwd: wd,
946
- authority_only: false,
947
- poll_ms: 250,
948
- dispatch_drain: { last_tick_at: new Date().toISOString() },
949
- }, null, 2));
950
- const watcherScript = distScript('notify-fallback-watcher.js');
951
- const notifyHook = distScript('notify-hook.js');
952
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--authority-only', '--cwd', aliasWd, '--notify-script', notifyHook, '--poll-ms', '50'], {
953
- encoding: 'utf-8',
954
- env: buildCleanNotifyEnv({
955
- PATH: prependPath(fakeBinDir),
956
- RCS_SESSION_ID: 'sess-cwd-alias',
957
- TMUX: '1',
958
- TMUX_PANE: '%42',
959
- }),
960
- });
961
- assert.equal(result.status, 0, result.stderr || result.stdout);
962
- const watcherState = JSON.parse(await readFile(join(wd, '.rcs', 'state', 'notify-fallback-state.json'), 'utf-8'));
963
- assert.equal(watcherState.authority_backoff?.active, true);
964
- assert.equal(watcherState.authority_backoff?.reason, 'primary_watcher_healthy');
965
- assert.equal(watcherState.authority_backoff?.primary_pid, process.pid);
966
- }
967
- finally {
968
- await rm(aliasWd, { recursive: true, force: true });
969
- await rm(wd, { recursive: true, force: true });
970
- }
971
- });
972
- it('disables fallback watcher nudges when deep-interview state is active', async () => {
973
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-deep-interview-suppressed-'));
974
- const fakeBinDir = join(wd, 'fake-bin');
975
- const tmuxLogPath = join(wd, 'tmux.log');
976
- try {
977
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
978
- await mkdir(join(wd, '.rcs', 'state', 'team', 'dispatch-team'), { recursive: true });
979
- await mkdir(fakeBinDir, { recursive: true });
980
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
981
- await writeFile(join(wd, '.rcs', 'state', 'deep-interview-state.json'), JSON.stringify({
982
- active: true,
983
- mode: 'deep-interview',
984
- current_phase: 'deep-interview',
985
- }, null, 2));
986
- await writeFile(join(wd, '.rcs', 'state', 'ralph-state.json'), JSON.stringify({
987
- active: true,
988
- current_phase: 'executing',
989
- tmux_pane_id: '%42',
990
- }, null, 2));
991
- await writeFile(join(wd, '.rcs', 'state', 'team-state.json'), JSON.stringify({
992
- active: true,
993
- team_name: 'dispatch-team',
994
- current_phase: 'team-exec',
995
- }, null, 2));
996
- await writeFile(join(wd, '.rcs', 'state', 'hud-state.json'), JSON.stringify({
997
- last_turn_at: new Date(Date.now() - 300_000).toISOString(),
998
- turn_count: 3,
999
- last_agent_output: 'Would you like me to continue?',
1000
- }, null, 2));
1001
- await writeFile(join(wd, '.rcs', 'state', 'team', 'dispatch-team', 'config.json'), JSON.stringify({
1002
- name: 'dispatch-team',
1003
- tmux_session: 'rcs-team-dispatch-team',
1004
- leader_pane_id: '%42',
1005
- }, null, 2));
1006
- const watcherScript = distScript('notify-fallback-watcher.js');
1007
- const notifyHook = distScript('notify-hook.js');
1008
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook], {
1009
- encoding: 'utf-8',
1010
- env: buildCleanNotifyEnv({ PATH: prependPath(fakeBinDir) }),
1011
- });
1012
- assert.equal(result.status, 0, result.stderr || result.stdout);
1013
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
1014
- assert.doesNotMatch(tmuxLog, /Ralph loop active continue/);
1015
- assert.doesNotMatch(tmuxLog, /Team dispatch-team:/);
1016
- assert.doesNotMatch(tmuxLog, new RegExp(`${DEFAULT_AUTO_NUDGE_RESPONSE.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')} \\[RCS_TMUX_INJECT\\]`));
1017
- }
1018
- finally {
1019
- await rm(wd, { recursive: true, force: true });
1020
- }
1021
- });
1022
- it('disables fallback watcher nudges when only skill-active-state carries the deep-interview input lock', async () => {
1023
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-deep-interview-skill-lock-'));
1024
- const fakeBinDir = join(wd, 'fake-bin');
1025
- const tmuxLogPath = join(wd, 'tmux.log');
1026
- try {
1027
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
1028
- await mkdir(join(wd, '.rcs', 'state', 'team', 'dispatch-team'), { recursive: true });
1029
- await mkdir(fakeBinDir, { recursive: true });
1030
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
1031
- await writeFile(join(wd, '.rcs', 'state', 'skill-active-state.json'), JSON.stringify({
1032
- version: 1,
1033
- active: true,
1034
- skill: 'deep-interview',
1035
- keyword: 'deep interview',
1036
- phase: 'planning',
1037
- activated_at: '2026-02-25T00:00:00.000Z',
1038
- updated_at: '2026-02-25T00:00:00.000Z',
1039
- source: 'keyword-detector',
1040
- input_lock: {
1041
- active: true,
1042
- scope: 'deep-interview-auto-approval',
1043
- acquired_at: '2026-02-25T00:00:00.000Z',
1044
- blocked_inputs: ['yes', 'y', 'proceed', 'continue', 'ok', 'sure', 'go ahead', 'next i should'],
1045
- message: 'Deep interview is active; auto-approval shortcuts are blocked until the interview finishes.',
1046
- },
1047
- }, null, 2));
1048
- await writeFile(join(wd, '.rcs', 'state', 'ralph-state.json'), JSON.stringify({
1049
- active: true,
1050
- current_phase: 'executing',
1051
- tmux_pane_id: '%42',
1052
- }, null, 2));
1053
- await writeFile(join(wd, '.rcs', 'state', 'team-state.json'), JSON.stringify({
1054
- active: true,
1055
- team_name: 'dispatch-team',
1056
- current_phase: 'team-exec',
1057
- }, null, 2));
1058
- await writeFile(join(wd, '.rcs', 'state', 'hud-state.json'), JSON.stringify({
1059
- last_turn_at: new Date(Date.now() - 300_000).toISOString(),
1060
- turn_count: 3,
1061
- last_agent_output: 'Would you like me to continue?',
1062
- }, null, 2));
1063
- await writeFile(join(wd, '.rcs', 'state', 'team', 'dispatch-team', 'config.json'), JSON.stringify({
1064
- name: 'dispatch-team',
1065
- tmux_session: 'rcs-team-dispatch-team',
1066
- leader_pane_id: '%42',
1067
- }, null, 2));
1068
- const watcherScript = distScript('notify-fallback-watcher.js');
1069
- const notifyHook = distScript('notify-hook.js');
1070
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook], {
1071
- encoding: 'utf-8',
1072
- env: buildCleanNotifyEnv({ PATH: prependPath(fakeBinDir) }),
1073
- });
1074
- assert.equal(result.status, 0, result.stderr || result.stdout);
1075
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
1076
- assert.doesNotMatch(tmuxLog, /Ralph loop active continue/);
1077
- assert.doesNotMatch(tmuxLog, /Team dispatch-team:/);
1078
- assert.doesNotMatch(tmuxLog, new RegExp(`${DEFAULT_AUTO_NUDGE_RESPONSE.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')} \\[RCS_TMUX_INJECT\\]`));
1079
- }
1080
- finally {
1081
- await rm(wd, { recursive: true, force: true });
1082
- }
1083
- });
1084
- it('runs leader nudge checks from the fallback watcher so stale alerts do not wait for a leader turn', async () => {
1085
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-leader-nudge-'));
1086
- const fakeBinDir = join(wd, 'fake-bin');
1087
- const tmuxLogPath = join(wd, 'tmux.log');
1088
- try {
1089
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
1090
- await mkdir(join(wd, '.rcs', 'state', 'team', 'dispatch-team'), { recursive: true });
1091
- await mkdir(fakeBinDir, { recursive: true });
1092
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
1093
- await writeFile(join(wd, '.rcs', 'state', 'team-state.json'), JSON.stringify({
1094
- active: true,
1095
- team_name: 'dispatch-team',
1096
- current_phase: 'team-exec',
1097
- }, null, 2));
1098
- await writeFile(join(wd, '.rcs', 'state', 'hud-state.json'), JSON.stringify({
1099
- last_turn_at: new Date(Date.now() - 300_000).toISOString(),
1100
- turn_count: 3,
1101
- }, null, 2));
1102
- await writeFile(join(wd, '.rcs', 'state', 'team', 'dispatch-team', 'config.json'), JSON.stringify({
1103
- name: 'dispatch-team',
1104
- tmux_session: 'rcs-team-dispatch-team',
1105
- leader_pane_id: '%42',
1106
- }, null, 2));
1107
- const watcherScript = distScript('notify-fallback-watcher.js');
1108
- const notifyHook = distScript('notify-hook.js');
1109
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook], {
1110
- encoding: 'utf-8',
1111
- env: buildCleanNotifyEnv({
1112
- PATH: prependPath(fakeBinDir),
1113
- RCS_SESSION_ID: 'sess-canonical-inactive',
1114
- }),
1115
- });
1116
- assert.equal(result.status, 0, result.stderr || result.stdout);
1117
- const tmuxLog = await readFile(tmuxLogPath, 'utf8');
1118
- assert.match(tmuxLog, /send-keys -t %42 -l Team dispatch-team: leader stale, \d+ worker pane\(s\) still active\./);
1119
- const watcherStatePath = join(wd, '.rcs', 'state', 'notify-fallback-state.json');
1120
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
1121
- assert.equal(watcherState.poll_ms, 250);
1122
- assert.equal(watcherState.leader_nudge?.enabled, true);
1123
- assert.equal(watcherState.leader_nudge?.leader_only, true);
1124
- assert.equal(watcherState.leader_nudge?.run_count, 1);
1125
- assert.equal(watcherState.leader_nudge?.precomputed_leader_stale, true);
1126
- const logPath = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
1127
- const logEntries = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
1128
- const nudgeEvent = logEntries.find((entry) => entry.type === 'leader_nudge_tick');
1129
- assert.ok(nudgeEvent, 'expected leader_nudge_tick log event');
1130
- assert.equal(nudgeEvent.leader_only, true);
1131
- assert.equal(nudgeEvent.precomputed_leader_stale, true);
1132
- const deliveryLogPath = join(wd, '.rcs', 'logs', `team-delivery-${new Date().toISOString().slice(0, 10)}.jsonl`);
1133
- const deliveryEntries = await readJsonLines(deliveryLogPath);
1134
- assert.ok(deliveryEntries.some((entry) => entry.event === 'nudge_triggered'
1135
- && entry.source === 'notify_fallback_watcher'
1136
- && entry.transport === 'send-keys'
1137
- && entry.result === 'sent'));
1138
- }
1139
- finally {
1140
- await rm(wd, { recursive: true, force: true });
1141
- }
1142
- });
1143
- it('runs leader nudge checks from canonical fallback when coarse team-state is inactive', async () => {
1144
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-leader-nudge-canonical-inactive-'));
1145
- const fakeBinDir = join(wd, 'fake-bin');
1146
- const tmuxLogPath = join(wd, 'tmux.log');
1147
- try {
1148
- await mkdir(fakeBinDir, { recursive: true });
1149
- await writeCanonicalWatcherTeamFixture(wd, {
1150
- teamName: 'dispatch-team',
1151
- sessionId: 'sess-canonical-inactive',
1152
- ownerSessionId: 'sess-canonical-inactive',
1153
- coarseState: 'inactive',
1154
- });
1155
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
1156
- const watcherScript = distScript('notify-fallback-watcher.js');
1157
- const notifyHook = distScript('notify-hook.js');
1158
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook], {
1159
- encoding: 'utf-8',
1160
- env: buildCleanNotifyEnv({
1161
- PATH: prependPath(fakeBinDir),
1162
- RCS_SESSION_ID: 'sess-canonical-inactive',
1163
- }),
1164
- });
1165
- assert.equal(result.status, 0, result.stderr || result.stdout);
1166
- const tmuxLog = await readFile(tmuxLogPath, 'utf8');
1167
- assert.match(tmuxLog, /send-keys -t %42 -l Team dispatch-team: leader stale, \d+ worker pane\(s\) still active\./);
1168
- }
1169
- finally {
1170
- await rm(wd, { recursive: true, force: true });
1171
- }
1172
- });
1173
- it('ignores invalid session_id before watcher session path joins', async () => {
1174
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-invalid-session-id-'));
1175
- const fakeBinDir = join(wd, 'fake-bin');
1176
- const tmuxLogPath = join(wd, 'tmux.log');
1177
- try {
1178
- await mkdir(fakeBinDir, { recursive: true });
1179
- await writeCanonicalWatcherTeamFixture(wd, {
1180
- teamName: 'dispatch-team',
1181
- sessionId: 'safe-session',
1182
- ownerSessionId: 'safe-session',
1183
- coarseState: 'inactive',
1184
- });
1185
- await writeFile(join(wd, '.rcs', 'state', 'session.json'), JSON.stringify({
1186
- session_id: '../escape',
1187
- cwd: wd,
1188
- pid: process.pid,
1189
- started_at: new Date().toISOString(),
1190
- }, null, 2));
1191
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
1192
- const watcherScript = distScript('notify-fallback-watcher.js');
1193
- const notifyHook = distScript('notify-hook.js');
1194
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook], {
1195
- encoding: 'utf-8',
1196
- env: buildCleanNotifyEnv({
1197
- PATH: prependPath(fakeBinDir),
1198
- }),
1199
- });
1200
- assert.equal(result.status, 0, result.stderr || result.stdout);
1201
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
1202
- assert.equal(tmuxLog, '', 'invalid session_id should not reach session-scoped or canonical follow-up joins');
1203
- }
1204
- finally {
1205
- await rm(wd, { recursive: true, force: true });
1206
- }
1207
- });
1208
- it('skips fallback watcher leader nudges when the leader is not stale even if mailbox messages exist', async () => {
1209
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-leader-nudge-fresh-'));
1210
- const fakeBinDir = join(wd, 'fake-bin');
1211
- const tmuxLogPath = join(wd, 'tmux.log');
1212
- try {
1213
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
1214
- await mkdir(join(wd, '.rcs', 'state', 'team', 'dispatch-team', 'mailbox'), { recursive: true });
1215
- await mkdir(fakeBinDir, { recursive: true });
1216
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
1217
- await writeFile(join(wd, '.rcs', 'state', 'team-state.json'), JSON.stringify({
1218
- active: true,
1219
- team_name: 'dispatch-team',
1220
- current_phase: 'team-exec',
1221
- }, null, 2));
1222
- await writeFile(join(wd, '.rcs', 'state', 'hud-state.json'), JSON.stringify({
1223
- last_turn_at: new Date().toISOString(),
1224
- turn_count: 3,
1225
- }, null, 2));
1226
- await writeFile(join(wd, '.rcs', 'state', 'team', 'dispatch-team', 'config.json'), JSON.stringify({
1227
- name: 'dispatch-team',
1228
- tmux_session: 'rcs-team-dispatch-team',
1229
- leader_pane_id: '%42',
1230
- }, null, 2));
1231
- await writeFile(join(wd, '.rcs', 'state', 'team', 'dispatch-team', 'mailbox', 'leader-fixed.json'), JSON.stringify({
1232
- worker: 'leader-fixed',
1233
- messages: [
1234
- {
1235
- message_id: 'msg-1',
1236
- from_worker: 'worker-1',
1237
- to_worker: 'leader-fixed',
1238
- body: 'fresh mailbox message',
1239
- created_at: new Date().toISOString(),
1240
- },
1241
- ],
1242
- }, null, 2));
1243
- const watcherScript = distScript('notify-fallback-watcher.js');
1244
- const notifyHook = distScript('notify-hook.js');
1245
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook], {
1246
- encoding: 'utf-8',
1247
- env: buildCleanNotifyEnv({
1248
- PATH: prependPath(fakeBinDir),
1249
- RCS_SESSION_ID: 'sess-canonical-inactive',
1250
- }),
1251
- });
1252
- assert.equal(result.status, 0, result.stderr || result.stdout);
1253
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
1254
- assert.doesNotMatch(tmuxLog, /send-keys -t %42 -l Team dispatch-team:/);
1255
- const watcherStatePath = join(wd, '.rcs', 'state', 'notify-fallback-state.json');
1256
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
1257
- assert.equal(watcherState.leader_nudge?.enabled, true);
1258
- assert.equal(watcherState.leader_nudge?.leader_only, true);
1259
- assert.equal(watcherState.leader_nudge?.run_count, 1);
1260
- assert.equal(watcherState.leader_nudge?.precomputed_leader_stale, false);
1261
- const logPath = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
1262
- const logEntries = await readJsonLines(logPath);
1263
- const nudgeEvent = logEntries.find((entry) => entry.type === 'leader_nudge_tick');
1264
- assert.equal(nudgeEvent, undefined);
1265
- }
1266
- finally {
1267
- await rm(wd, { recursive: true, force: true });
1268
- }
1269
- });
1270
- it('runs stalled-worker leader nudges from the fallback watcher even when the leader is not stale', async () => {
1271
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-worker-stall-nudge-'));
1272
- const fakeBinDir = join(wd, 'fake-bin');
1273
- const tmuxLogPath = join(wd, 'tmux.log');
1274
- const tmuxLogForBash = bashPathForStub(tmuxLogPath);
1275
- try {
1276
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
1277
- await mkdir(join(wd, '.rcs', 'state', 'team', 'dispatch-team', 'workers', 'worker-1'), { recursive: true });
1278
- await mkdir(join(wd, '.rcs', 'state', 'team', 'dispatch-team', 'tasks'), { recursive: true });
1279
- await mkdir(fakeBinDir, { recursive: true });
1280
- const tmuxScript = `#!/usr/bin/env bash
1281
- set -eu
1282
- echo "$@" >> "${tmuxLogForBash}"
1283
- cmd="$1"
1284
- shift || true
1285
- if [[ "$cmd" == "display-message" ]]; then
1286
- target=""
1287
- fmt=""
1288
- while [[ "$#" -gt 0 ]]; do
1289
- case "$1" in
1290
- -t)
1291
- shift
1292
- target="$1"
1293
- ;;
1294
- *)
1295
- fmt="$1"
1296
- ;;
1297
- esac
1298
- shift || true
1299
- done
1300
- if [[ "$fmt" == "#{pane_in_mode}" ]]; then
1301
- echo "0"
1302
- exit 0
1303
- fi
1304
- if [[ "$fmt" == "#{pane_id}" ]]; then
1305
- # Real tmux resolves session:window targets to a %pane id; mirror that for team worker targets.
1306
- if [[ -n "$target" && "$target" != %* && "$target" == *.* ]]; then
1307
- echo "%42"
1308
- exit 0
1309
- fi
1310
- echo "\${target:-%42}"
1311
- exit 0
1312
- fi
1313
- if [[ "$fmt" == "#{pane_current_path}" ]]; then
1314
- dirname "${tmuxLogForBash}"
1315
- exit 0
1316
- fi
1317
- if [[ "$fmt" == "#{pane_current_command}" ]]; then
1318
- echo "codex"
1319
- exit 0
1320
- fi
1321
- if [[ "$fmt" == "#S" ]]; then
1322
- echo "rcs-team-dispatch-team"
1323
- exit 0
1324
- fi
1325
- exit 0
1326
- fi
1327
- if [[ "$cmd" == "send-keys" ]]; then
1328
- exit 0
1329
- fi
1330
- if [[ "$cmd" == "list-panes" ]]; then
1331
- target=""
1332
- while [[ "$#" -gt 0 ]]; do
1333
- case "$1" in
1334
- -t)
1335
- shift
1336
- target="$1"
1337
- ;;
1338
- esac
1339
- shift || true
1340
- done
1341
- if [[ -n "$target" ]]; then
1342
- printf "%%42 12345\n%%10 12346\n%%11 12347\n"
1343
- exit 0
1344
- fi
1345
- echo "%42 1"
1346
- exit 0
1347
- fi
1348
- exit 0
1349
- `;
1350
- await writeFakeTmuxExecutable(fakeBinDir, tmuxScript);
1351
- const now = Date.now();
1352
- await writeFile(join(wd, '.rcs', 'state', 'team-state.json'), JSON.stringify({
1353
- active: true,
1354
- team_name: 'dispatch-team',
1355
- current_phase: 'team-exec',
1356
- }, null, 2));
1357
- await writeFile(join(wd, '.rcs', 'state', 'hud-state.json'), JSON.stringify({
1358
- last_turn_at: new Date().toISOString(),
1359
- turn_count: 3,
1360
- }, null, 2));
1361
- await writeFile(join(wd, '.rcs', 'state', 'team', 'dispatch-team', 'config.json'), JSON.stringify({
1362
- name: 'dispatch-team',
1363
- tmux_session: 'rcs-team-dispatch-team',
1364
- leader_pane_id: '%42',
1365
- workers: [
1366
- { name: 'worker-1', index: 1, pane_id: '%10' },
1367
- { name: 'worker-2', index: 2, pane_id: '%11' },
1368
- ],
1369
- }, null, 2));
1370
- await writeFile(join(wd, '.rcs', 'state', 'team', 'dispatch-team', 'tasks', 'task-1.json'), JSON.stringify({
1371
- id: '1',
1372
- subject: 'Pending work',
1373
- description: 'Needs attention',
1374
- status: 'pending',
1375
- created_at: new Date().toISOString(),
1376
- }, null, 2));
1377
- await writeFile(join(wd, '.rcs', 'state', 'team', 'dispatch-team', 'workers', 'worker-1', 'status.json'), JSON.stringify({
1378
- state: 'working',
1379
- current_task_id: '1',
1380
- updated_at: new Date(now - 180_000).toISOString(),
1381
- }, null, 2));
1382
- await writeFile(join(wd, '.rcs', 'state', 'team', 'dispatch-team', 'workers', 'worker-1', 'heartbeat.json'), JSON.stringify({
1383
- alive: true,
1384
- pid: 101,
1385
- turn_count: 2,
1386
- last_turn_at: new Date(now - 180_000).toISOString(),
1387
- }, null, 2));
1388
- await writeFile(join(wd, '.rcs', 'state', 'team-leader-nudge.json'), JSON.stringify({
1389
- last_nudged_by_team: {
1390
- 'dispatch-team': {
1391
- at: new Date(now - 5_000).toISOString(),
1392
- last_message_id: '',
1393
- reason: 'new_mailbox_message',
1394
- },
1395
- },
1396
- progress_by_team: {
1397
- 'dispatch-team': {
1398
- signature: JSON.stringify({
1399
- tasks: [{ id: '1', owner: '', status: 'pending' }],
1400
- workers: [
1401
- {
1402
- worker: 'worker-1',
1403
- state: 'working',
1404
- current_task_id: '1',
1405
- status_missing: false,
1406
- turn_count: 2,
1407
- heartbeat_missing: false,
1408
- },
1409
- {
1410
- worker: 'worker-2',
1411
- state: 'unknown',
1412
- current_task_id: '',
1413
- status_missing: true,
1414
- turn_count: null,
1415
- heartbeat_missing: true,
1416
- },
1417
- ],
1418
- }),
1419
- last_progress_at: new Date(now - 180_000).toISOString(),
1420
- },
1421
- },
1422
- }, null, 2));
1423
- const watcherScript = distScript('notify-fallback-watcher.js');
1424
- const notifyHook = distScript('notify-hook.js');
1425
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook], {
1426
- encoding: 'utf-8',
1427
- env: buildCleanNotifyEnv({
1428
- PATH: prependPath(fakeBinDir),
1429
- RCS_TEAM_PROGRESS_STALL_MS: '60000',
1430
- RCS_TEAM_LEADER_NUDGE_MS: '30000',
1431
- RCS_TEAM_LEADER_STALE_MS: '60000',
1432
- }),
1433
- });
1434
- assert.equal(result.status, 0, result.stderr || result.stdout);
1435
- const tmuxLog = await readFile(tmuxLogPath, 'utf8');
1436
- assert.match(tmuxLog, /send-keys -t %42 -l Team dispatch-team: worker panes stalled, no progress 3m\./);
1437
- assert.doesNotMatch(tmuxLog, /leader stale/);
1438
- const watcherStatePath = join(wd, '.rcs', 'state', 'notify-fallback-state.json');
1439
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
1440
- assert.equal(watcherState.leader_nudge?.enabled, true);
1441
- assert.equal(watcherState.leader_nudge?.leader_only, true);
1442
- assert.equal(watcherState.leader_nudge?.run_count, 1);
1443
- assert.equal(watcherState.leader_nudge?.precomputed_leader_stale, false);
1444
- }
1445
- finally {
1446
- await rm(wd, { recursive: true, force: true });
1447
- }
1448
- });
1449
- it('auto-nudges stalled session output even when no active mode state exists', async () => {
1450
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-auto-nudge-stalled-'));
1451
- const fakeBinDir = join(wd, 'fake-bin');
1452
- const tmuxLogPath = join(wd, 'tmux.log');
1453
- const codexHome = join(wd, 'codex-home');
1454
- try {
1455
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
1456
- await mkdir(join(wd, '.rcs', 'state'), { recursive: true });
1457
- await mkdir(fakeBinDir, { recursive: true });
1458
- await mkdir(codexHome, { recursive: true });
1459
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
1460
- await writeFile(join(codexHome, '.rcs-config.json'), JSON.stringify({
1461
- autoNudge: { enabled: true, delaySec: 0, ttlMs: 30_000 },
1462
- }, null, 2));
1463
- await writeSessionStart(wd, 'sess-managed-fallback');
1464
- await mkdir(join(wd, '.rcs', 'state', 'sessions', 'sess-managed-fallback'), { recursive: true });
1465
- await writeFile(join(wd, '.rcs', 'state', 'sessions', 'sess-managed-fallback', 'hud-state.json'), JSON.stringify({
1466
- last_turn_at: new Date(Date.now() - 6_000).toISOString(),
1467
- turn_count: 7,
1468
- last_agent_output: 'Keep going and finish the cleanup from here.',
1469
- }, null, 2));
1470
- const watcherScript = distScript('notify-fallback-watcher.js');
1471
- const notifyHook = distScript('notify-hook.js');
1472
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
1473
- encoding: 'utf-8',
1474
- env: buildCleanNotifyEnv({
1475
- PATH: prependPath(fakeBinDir),
1476
- CODEX_HOME: codexHome,
1477
- RCS_SESSION_ID: 'sess-managed-fallback',
1478
- RCS_TEST_TMUX_SESSION_NAME: 'rcs-fallback-auto-nudge-stalled-managed',
1479
- TMUX: '1',
1480
- TMUX_PANE: '%42',
1481
- RCS_NOTIFY_FALLBACK_AUTO_NUDGE_STALL_MS: '5000',
1482
- }),
1483
- });
1484
- assert.equal(result.status, 0, result.stderr || result.stdout);
1485
- const tmuxLog = await readFile(tmuxLogPath, 'utf8');
1486
- assert.match(tmuxLog, defaultAutoNudgePattern('%42'));
1487
- const watcherStatePath = join(wd, '.rcs', 'state', 'notify-fallback-state.json');
1488
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
1489
- assert.equal(watcherState.fallback_auto_nudge?.last_reason, 'sent');
1490
- assert.equal(watcherState.fallback_auto_nudge?.last_turn_count, 7);
1491
- assert.match(watcherState.fallback_auto_nudge?.last_nudged_at ?? '', /^\d{4}-\d{2}-\d{2}T/);
1492
- }
1493
- finally {
1494
- await rm(wd, { recursive: true, force: true });
1495
- }
1496
- });
1497
- it('respects `.rcs/tmux-hook.json` enabled:false for fallback auto-nudge', async () => {
1498
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-auto-nudge-disabled-'));
1499
- const fakeBinDir = join(wd, 'fake-bin');
1500
- const tmuxLogPath = join(wd, 'tmux.log');
1501
- const codexHome = join(wd, 'codex-home');
1502
- try {
1503
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
1504
- await mkdir(join(wd, '.rcs', 'state'), { recursive: true });
1505
- await mkdir(fakeBinDir, { recursive: true });
1506
- await mkdir(codexHome, { recursive: true });
1507
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
1508
- await writeFile(join(codexHome, '.rcs-config.json'), JSON.stringify({
1509
- autoNudge: { enabled: true, delaySec: 0, ttlMs: 30_000 },
1510
- }, null, 2));
1511
- await writeFile(join(wd, '.rcs', 'tmux-hook.json'), JSON.stringify({
1512
- enabled: false,
1513
- target: { type: 'pane', value: '%42' },
1514
- }, null, 2));
1515
- await writeSessionStart(wd, 'sess-managed-fallback');
1516
- await mkdir(join(wd, '.rcs', 'state', 'sessions', 'sess-managed-fallback'), { recursive: true });
1517
- await writeFile(join(wd, '.rcs', 'state', 'sessions', 'sess-managed-fallback', 'hud-state.json'), JSON.stringify({
1518
- last_turn_at: new Date(Date.now() - 6_000).toISOString(),
1519
- turn_count: 7,
1520
- last_agent_output: 'Keep going and finish the cleanup from here.',
1521
- }, null, 2));
1522
- const watcherScript = distScript('notify-fallback-watcher.js');
1523
- const notifyHook = distScript('notify-hook.js');
1524
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
1525
- encoding: 'utf-8',
1526
- env: buildCleanNotifyEnv({
1527
- PATH: prependPath(fakeBinDir),
1528
- CODEX_HOME: codexHome,
1529
- RCS_SESSION_ID: 'sess-managed-fallback',
1530
- TMUX: '1',
1531
- TMUX_PANE: '%42',
1532
- RCS_NOTIFY_FALLBACK_AUTO_NUDGE_STALL_MS: '5000',
1533
- }),
1534
- });
1535
- assert.equal(result.status, 0, result.stderr || result.stdout);
1536
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
1537
- assert.doesNotMatch(tmuxLog, defaultAutoNudgePattern('%42'));
1538
- }
1539
- finally {
1540
- await rm(wd, { recursive: true, force: true });
1541
- }
1542
- });
1543
- it('suppresses fallback unmanaged-session auto-nudge skip logs while idle', async () => {
1544
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-auto-nudge-unmanaged-'));
1545
- const fakeBinDir = join(wd, 'fake-bin');
1546
- const tmuxLogPath = join(wd, 'tmux.log');
1547
- const codexHome = join(wd, 'codex-home');
1548
- try {
1549
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
1550
- await mkdir(join(wd, '.rcs', 'state'), { recursive: true });
1551
- await mkdir(fakeBinDir, { recursive: true });
1552
- await mkdir(codexHome, { recursive: true });
1553
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
1554
- await writeFile(join(codexHome, '.rcs-config.json'), JSON.stringify({
1555
- autoNudge: { enabled: true, delaySec: 0, ttlMs: 30_000 },
1556
- }, null, 2));
1557
- await writeFile(join(wd, '.rcs', 'state', 'hud-state.json'), JSON.stringify({
1558
- last_turn_at: new Date(Date.now() - 6_000).toISOString(),
1559
- turn_count: 9,
1560
- last_agent_output: 'Keep going and finish the cleanup from here.',
1561
- }, null, 2));
1562
- const watcherScript = distScript('notify-fallback-watcher.js');
1563
- const notifyHook = distScript('notify-hook.js');
1564
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
1565
- encoding: 'utf-8',
1566
- env: buildCleanNotifyEnv({
1567
- PATH: prependPath(fakeBinDir),
1568
- CODEX_HOME: codexHome,
1569
- TMUX: '1',
1570
- TMUX_PANE: '%42',
1571
- RCS_NOTIFY_FALLBACK_AUTO_NUDGE_STALL_MS: '5000',
1572
- }),
1573
- });
1574
- assert.equal(result.status, 0, result.stderr || result.stdout);
1575
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
1576
- assert.doesNotMatch(tmuxLog, defaultAutoNudgePattern('%42'));
1577
- const watcherStatePath = join(wd, '.rcs', 'state', 'notify-fallback-state.json');
1578
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
1579
- assert.equal(watcherState.fallback_auto_nudge?.last_reason, 'eligible_but_not_sent');
1580
- const tmuxHookLogPath = join(wd, '.rcs', 'logs', `tmux-hook-${new Date().toISOString().split('T')[0]}.jsonl`);
1581
- const tmuxHookLog = await readFile(tmuxHookLogPath, 'utf-8').catch(() => '');
1582
- assert.equal(tmuxHookLog.trim(), '');
1583
- }
1584
- finally {
1585
- await rm(wd, { recursive: true, force: true });
1586
- }
1587
- });
1588
- it('does not auto-nudge stalled-like output when the latest turn is still fresh', async () => {
1589
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-auto-nudge-fresh-'));
1590
- const fakeBinDir = join(wd, 'fake-bin');
1591
- const tmuxLogPath = join(wd, 'tmux.log');
1592
- const codexHome = join(wd, 'codex-home');
1593
- try {
1594
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
1595
- await mkdir(join(wd, '.rcs', 'state'), { recursive: true });
1596
- await mkdir(fakeBinDir, { recursive: true });
1597
- await mkdir(codexHome, { recursive: true });
1598
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
1599
- await writeFile(join(codexHome, '.rcs-config.json'), JSON.stringify({
1600
- autoNudge: { enabled: true, delaySec: 0 },
1601
- }, null, 2));
1602
- await writeFile(join(wd, '.rcs', 'state', 'hud-state.json'), JSON.stringify({
1603
- last_turn_at: new Date(Date.now() - 1_000).toISOString(),
1604
- turn_count: 8,
1605
- last_agent_output: 'Keep going and finish the cleanup from here.',
1606
- }, null, 2));
1607
- const watcherScript = distScript('notify-fallback-watcher.js');
1608
- const notifyHook = distScript('notify-hook.js');
1609
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
1610
- encoding: 'utf-8',
1611
- env: buildCleanNotifyEnv({
1612
- PATH: prependPath(fakeBinDir),
1613
- CODEX_HOME: codexHome,
1614
- TMUX: '1',
1615
- TMUX_PANE: '%42',
1616
- RCS_NOTIFY_FALLBACK_AUTO_NUDGE_STALL_MS: '5000',
1617
- }),
1618
- });
1619
- assert.equal(result.status, 0, result.stderr || result.stdout);
1620
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
1621
- assert.doesNotMatch(tmuxLog, defaultAutoNudgePattern('%42'));
1622
- const watcherStatePath = join(wd, '.rcs', 'state', 'notify-fallback-state.json');
1623
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
1624
- assert.equal(watcherState.fallback_auto_nudge?.last_reason, 'recent_turn_activity');
1625
- assert.equal(watcherState.fallback_auto_nudge?.last_turn_count, 8);
1626
- }
1627
- finally {
1628
- await rm(wd, { recursive: true, force: true });
1629
- }
1630
- });
1631
- it('does not fallback auto-nudge a stalled hud snapshot that notify-hook already nudged', async () => {
1632
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-auto-nudge-dedup-'));
1633
- const fakeBinDir = join(wd, 'fake-bin');
1634
- const tmuxLogPath = join(wd, 'tmux.log');
1635
- const codexHome = join(wd, 'codex-home');
1636
- const lastTurnAt = new Date(Date.now() - 6_000).toISOString();
1637
- const lastMessage = 'Keep going and finish the cleanup from here.';
1638
- try {
1639
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
1640
- await mkdir(join(wd, '.rcs', 'state'), { recursive: true });
1641
- await mkdir(fakeBinDir, { recursive: true });
1642
- await mkdir(codexHome, { recursive: true });
1643
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
1644
- await writeFile(join(codexHome, '.rcs-config.json'), JSON.stringify({
1645
- autoNudge: { enabled: true, delaySec: 0 },
1646
- }, null, 2));
1647
- await writeFile(join(wd, '.rcs', 'state', 'hud-state.json'), JSON.stringify({
1648
- last_turn_at: lastTurnAt,
1649
- turn_count: 7,
1650
- last_agent_output: lastMessage,
1651
- }, null, 2));
1652
- await writeFile(join(wd, '.rcs', 'state', 'auto-nudge-state.json'), JSON.stringify({
1653
- nudgeCount: 1,
1654
- lastNudgeAt: new Date().toISOString(),
1655
- lastSignature: `hud:7|${lastTurnAt}|stall:proceed_intent`,
1656
- lastSemanticSignature: 'stall:proceed_intent',
1657
- }, null, 2));
1658
- const watcherScript = distScript('notify-fallback-watcher.js');
1659
- const notifyHook = distScript('notify-hook.js');
1660
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
1661
- encoding: 'utf-8',
1662
- env: buildCleanNotifyEnv({
1663
- PATH: prependPath(fakeBinDir),
1664
- CODEX_HOME: codexHome,
1665
- TMUX: '1',
1666
- TMUX_PANE: '%42',
1667
- RCS_NOTIFY_FALLBACK_AUTO_NUDGE_STALL_MS: '5000',
1668
- }),
1669
- });
1670
- assert.equal(result.status, 0, result.stderr || result.stdout);
1671
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
1672
- assert.doesNotMatch(tmuxLog, defaultAutoNudgePattern('%42'));
1673
- const watcherStatePath = join(wd, '.rcs', 'state', 'notify-fallback-state.json');
1674
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
1675
- assert.equal(watcherState.fallback_auto_nudge?.last_reason, 'already_nudged_for_signature');
1676
- assert.equal(watcherState.fallback_auto_nudge?.last_turn_count, 7);
1677
- }
1678
- finally {
1679
- await rm(wd, { recursive: true, force: true });
1680
- }
1681
- });
1682
- it('does not fallback auto-nudge the same stalled hud turn again after TTL expiry', async () => {
1683
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-auto-nudge-exact-dedup-'));
1684
- const fakeBinDir = join(wd, 'fake-bin');
1685
- const tmuxLogPath = join(wd, 'tmux.log');
1686
- const codexHome = join(wd, 'codex-home');
1687
- const lastTurnAt = '2026-03-01T00:00:00.000Z';
1688
- const lastMessage = 'Keep going and finish the cleanup from here.';
1689
- try {
1690
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
1691
- await mkdir(join(wd, '.rcs', 'state'), { recursive: true });
1692
- await mkdir(fakeBinDir, { recursive: true });
1693
- await mkdir(codexHome, { recursive: true });
1694
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
1695
- await writeFile(join(codexHome, '.rcs-config.json'), JSON.stringify({
1696
- autoNudge: { enabled: true, delaySec: 0, ttlMs: 5000 },
1697
- }, null, 2));
1698
- await writeFile(join(wd, '.rcs', 'state', 'hud-state.json'), JSON.stringify({
1699
- last_turn_at: lastTurnAt,
1700
- turn_count: 7,
1701
- last_agent_output: lastMessage,
1702
- }, null, 2));
1703
- await writeFile(join(wd, '.rcs', 'state', 'auto-nudge-state.json'), JSON.stringify({
1704
- nudgeCount: 1,
1705
- lastNudgeAt: '2026-03-01T00:00:10.000Z',
1706
- lastSignature: `hud:7|${lastTurnAt}|stall:proceed_intent`,
1707
- lastSemanticSignature: 'stall:proceed_intent',
1708
- }, null, 2));
1709
- const watcherScript = distScript('notify-fallback-watcher.js');
1710
- const notifyHook = distScript('notify-hook.js');
1711
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
1712
- encoding: 'utf-8',
1713
- env: buildCleanNotifyEnv({
1714
- PATH: prependPath(fakeBinDir),
1715
- CODEX_HOME: codexHome,
1716
- TMUX: '1',
1717
- TMUX_PANE: '%42',
1718
- RCS_NOTIFY_FALLBACK_AUTO_NUDGE_STALL_MS: '5000',
1719
- }),
1720
- });
1721
- assert.equal(result.status, 0, result.stderr || result.stdout);
1722
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
1723
- assert.doesNotMatch(tmuxLog, defaultAutoNudgePattern('%42'));
1724
- const watcherStatePath = join(wd, '.rcs', 'state', 'notify-fallback-state.json');
1725
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
1726
- assert.equal(watcherState.fallback_auto_nudge?.last_reason, 'already_nudged_for_signature');
1727
- assert.equal(watcherState.fallback_auto_nudge?.last_turn_count, 7);
1728
- }
1729
- finally {
1730
- await rm(wd, { recursive: true, force: true });
1731
- }
1732
- });
1733
- it('runs bounded non-turn team dispatch drain tick in leader context', async () => {
1734
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-dispatch-'));
1735
- const previousRuntimeBridge = process.env.RCS_RUNTIME_BRIDGE;
1736
- try {
1737
- process.env.RCS_RUNTIME_BRIDGE = '0';
1738
- await initTeamState('dispatch-team', 'task', 'executor', 1, wd);
1739
- const queued = await enqueueDispatchRequest('dispatch-team', {
1740
- kind: 'inbox',
1741
- to_worker: 'worker-1',
1742
- worker_index: 1,
1743
- trigger_message: 'dispatch ping',
1744
- }, wd);
1745
- const watcherScript = distScript('notify-fallback-watcher.js');
1746
- const notifyHook = distScript('notify-hook.js');
1747
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50', '--dispatch-max-per-tick', '1'], { encoding: 'utf-8', env: buildCleanNotifyEnv() });
1748
- assert.equal(result.status, 0, result.stderr || result.stdout);
1749
- const request = await readDispatchRequest('dispatch-team', queued.request.request_id, wd);
1750
- assert.ok(request);
1751
- assert.notEqual(request?.status, 'pending');
1752
- }
1753
- finally {
1754
- if (typeof previousRuntimeBridge === 'string')
1755
- process.env.RCS_RUNTIME_BRIDGE = previousRuntimeBridge;
1756
- else
1757
- delete process.env.RCS_RUNTIME_BRIDGE;
1758
- await rm(wd, { recursive: true, force: true });
1759
- }
1760
- });
1761
- it('skips dispatch drain in worker context (leader-only guard)', async () => {
1762
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-dispatch-worker-'));
1763
- const previousRuntimeBridge = process.env.RCS_RUNTIME_BRIDGE;
1764
- try {
1765
- process.env.RCS_RUNTIME_BRIDGE = '0';
1766
- await initTeamState('dispatch-team', 'task', 'executor', 1, wd);
1767
- const queued = await enqueueDispatchRequest('dispatch-team', {
1768
- kind: 'inbox',
1769
- to_worker: 'worker-1',
1770
- worker_index: 1,
1771
- trigger_message: 'dispatch ping',
1772
- }, wd);
1773
- const watcherScript = distScript('notify-fallback-watcher.js');
1774
- const notifyHook = distScript('notify-hook.js');
1775
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50', '--dispatch-max-per-tick', '1'], { encoding: 'utf-8', env: buildCleanNotifyEnv({ RCS_TEAM_WORKER: 'dispatch-team/worker-1', RCS_TEAM_STATE_ROOT: join(wd, '.rcs', 'state') }) });
1776
- assert.equal(result.status, 0, result.stderr || result.stdout);
1777
- const request = await readDispatchRequest('dispatch-team', queued.request.request_id, wd);
1778
- assert.equal(request?.status, 'pending');
1779
- const watcherStatePath = join(wd, '.rcs', 'state', 'notify-fallback-state.json');
1780
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
1781
- assert.equal(watcherState.dispatch_drain?.leader_only, false);
1782
- assert.equal(watcherState.dispatch_drain?.last_result?.reason, 'worker_context');
1783
- assert.equal(watcherState.dispatch_drain?.last_result?.processed, 0);
1784
- const logPath = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
1785
- const logEntries = await readJsonLines(logPath);
1786
- const drainEvent = logEntries.find((entry) => entry.type === 'dispatch_drain_tick');
1787
- assert.equal(drainEvent, undefined);
1788
- }
1789
- finally {
1790
- if (typeof previousRuntimeBridge === 'string')
1791
- process.env.RCS_RUNTIME_BRIDGE = previousRuntimeBridge;
1792
- else
1793
- delete process.env.RCS_RUNTIME_BRIDGE;
1794
- await rm(wd, { recursive: true, force: true });
1795
- }
1796
- });
1797
- it('watcher retry does not retype when pre-capture still contains trigger', async () => {
1798
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-dispatch-cm-'));
1799
- const fakeBinDir = join(wd, 'fake-bin');
1800
- const tmuxLogPath = join(wd, 'tmux.log');
1801
- const captureFile = join(wd, 'capture.txt');
1802
- const previousRuntimeBridge = process.env.RCS_RUNTIME_BRIDGE;
1803
- try {
1804
- process.env.RCS_RUNTIME_BRIDGE = '0';
1805
- await mkdir(fakeBinDir, { recursive: true });
1806
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
1807
- await writeFile(captureFile, '... ping ...');
1808
- await initTeamState('dispatch-team', 'task', 'executor', 1, wd);
1809
- const queued = await enqueueDispatchRequest('dispatch-team', {
1810
- kind: 'inbox',
1811
- to_worker: 'worker-1',
1812
- worker_index: 1,
1813
- pane_id: '%42',
1814
- trigger_message: 'ping',
1815
- }, wd);
1816
- const watcherScript = distScript('notify-fallback-watcher.js');
1817
- const notifyHook = distScript('notify-hook.js');
1818
- const env = {
1819
- ...buildCleanNotifyEnv(),
1820
- PATH: prependPath(fakeBinDir),
1821
- RCS_TEST_CAPTURE_FILE: captureFile,
1822
- };
1823
- const first = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50', '--dispatch-max-per-tick', '1'], { encoding: 'utf-8', env });
1824
- assert.equal(first.status, 0, first.stderr || first.stdout);
1825
- const second = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50', '--dispatch-max-per-tick', '1'], { encoding: 'utf-8', env });
1826
- assert.equal(second.status, 0, second.stderr || second.stdout);
1827
- const tmuxLog = await readFile(tmuxLogPath, 'utf8');
1828
- const typeMatches = tmuxLog.match(/send-keys -t %42 -l ping/g) || [];
1829
- assert.equal(typeMatches.length, 1, 'fresh attempt should type once; retries with draft should be submit-only');
1830
- const cmMatches = tmuxLog.match(/send-keys -t %42 C-m/g) || [];
1831
- assert.ok(cmMatches.length > 0, 'submit should use C-m');
1832
- assert.ok(!/send-keys[^\n]*-l[^\n]*C-m/.test(tmuxLog), 'must keep -l payload and C-m submits isolated');
1833
- const request = await readDispatchRequest('dispatch-team', queued.request.request_id, wd);
1834
- assert.equal(request?.status, 'pending');
1835
- assert.equal(request?.attempt_count, 2);
1836
- assert.equal(request?.last_reason, 'tmux_send_keys_unconfirmed');
1837
- }
1838
- finally {
1839
- if (typeof previousRuntimeBridge === 'string')
1840
- process.env.RCS_RUNTIME_BRIDGE = previousRuntimeBridge;
1841
- else
1842
- delete process.env.RCS_RUNTIME_BRIDGE;
1843
- await rm(wd, { recursive: true, force: true });
1844
- }
1845
- });
1846
- it('sends bounded periodic Ralph continue steer while Ralph state stays active', async () => {
1847
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-ralph-active-'));
1848
- const fakeBinDir = join(wd, 'fake-bin');
1849
- const stateDir = join(wd, '.rcs', 'state');
1850
- const tmuxLogPath = join(wd, 'tmux.log');
1851
- const statePath = join(stateDir, 'notify-fallback-state.json');
1852
- const sharedTimestampPath = join(stateDir, 'ralph-last-steer-at');
1853
- try {
1854
- await mkdir(stateDir, { recursive: true });
1855
- await mkdir(fakeBinDir, { recursive: true });
1856
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
1857
- await writeFile(join(stateDir, 'ralph-state.json'), JSON.stringify({
1858
- active: true,
1859
- current_phase: 'executing',
1860
- tmux_pane_id: '%42',
1861
- }, null, 2));
1862
- await writeFile(join(stateDir, 'hud-state.json'), JSON.stringify({
1863
- last_progress_at: new Date(Date.now() - 61_000).toISOString(),
1864
- }, null, 2));
1865
- await writeFile(statePath, JSON.stringify({
1866
- ralph_continue_steer: {
1867
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
1868
- },
1869
- }, null, 2));
1870
- const watcherScript = distScript('notify-fallback-watcher.js');
1871
- const notifyHook = distScript('notify-hook.js');
1872
- const env = {
1873
- ...buildCleanNotifyEnv(),
1874
- PATH: prependPath(fakeBinDir),
1875
- };
1876
- const first = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env });
1877
- assert.equal(first.status, 0, first.stderr || first.stdout);
1878
- const persistedAfterFirst = JSON.parse(await readFile(statePath, 'utf-8'));
1879
- assert.match(persistedAfterFirst.ralph_continue_steer?.last_sent_at ?? '', /^\d{4}-\d{2}-\d{2}T/, 'successful steer should persist a round-trippable ISO last_sent_at');
1880
- assert.equal(persistedAfterFirst.ralph_continue_steer?.cooldown_anchor_at, persistedAfterFirst.ralph_continue_steer?.last_sent_at, 'successful steer should advance the fallback cooldown anchor to the real send time');
1881
- const second = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env });
1882
- assert.equal(second.status, 0, second.stderr || second.stdout);
1883
- const boundedLog = await readFile(tmuxLogPath, 'utf8');
1884
- let sends = boundedLog.match(/send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/g) || [];
1885
- assert.equal(sends.length, 1, 'cadence should suppress a second Ralph steer inside 60s');
1886
- const watcherState = JSON.parse(await readFile(statePath, 'utf-8'));
1887
- const agedIso = new Date(Date.now() - 61_000).toISOString();
1888
- watcherState.ralph_continue_steer.last_sent_at = agedIso;
1889
- watcherState.ralph_continue_steer.shared_last_sent_at = agedIso;
1890
- await writeFile(statePath, JSON.stringify(watcherState, null, 2));
1891
- await writeFile(sharedTimestampPath, `${agedIso}\n`);
1892
- const third = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env });
1893
- assert.equal(third.status, 0, third.stderr || third.stdout);
1894
- const finalLog = await readFile(tmuxLogPath, 'utf8');
1895
- sends = finalLog.match(/send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/g) || [];
1896
- assert.equal(sends.length, 2, 'Ralph steer should fire again once the 60s cadence elapses');
1897
- }
1898
- finally {
1899
- await rm(wd, { recursive: true, force: true });
1900
- }
1901
- });
1902
- it('suppresses Ralph continue steer when hud progress is still fresh after cooldown', async () => {
1903
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-ralph-progress-fresh-'));
1904
- const fakeBinDir = join(wd, 'fake-bin');
1905
- const stateDir = join(wd, '.rcs', 'state');
1906
- const tmuxLogPath = join(wd, 'tmux.log');
1907
- const statePath = join(stateDir, 'notify-fallback-state.json');
1908
- try {
1909
- await mkdir(stateDir, { recursive: true });
1910
- await mkdir(fakeBinDir, { recursive: true });
1911
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
1912
- await writeFile(join(stateDir, 'ralph-state.json'), JSON.stringify({
1913
- active: true,
1914
- current_phase: 'executing',
1915
- tmux_pane_id: '%42',
1916
- }, null, 2));
1917
- await writeFile(join(stateDir, 'hud-state.json'), JSON.stringify({
1918
- last_progress_at: new Date(Date.now() - 5_000).toISOString(),
1919
- }, null, 2));
1920
- await writeFile(statePath, JSON.stringify({
1921
- ralph_continue_steer: {
1922
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
1923
- },
1924
- }, null, 2));
1925
- const watcherScript = distScript('notify-fallback-watcher.js');
1926
- const notifyHook = distScript('notify-hook.js');
1927
- const env = {
1928
- ...buildCleanNotifyEnv(),
1929
- PATH: prependPath(fakeBinDir),
1930
- };
1931
- const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env });
1932
- assert.equal(run.status, 0, run.stderr || run.stdout);
1933
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
1934
- const sends = tmuxLog.match(/send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/g) || [];
1935
- assert.equal(sends.length, 0, 'fresh progress should suppress continue steer even after cooldown elapses');
1936
- const watcherState = JSON.parse(await readFile(statePath, 'utf-8'));
1937
- assert.equal(watcherState.ralph_continue_steer?.last_reason, 'progress_fresh');
1938
- }
1939
- finally {
1940
- await rm(wd, { recursive: true, force: true });
1941
- }
1942
- });
1943
- it('still sends Ralph continue steer when hud progress is stale after cooldown', async () => {
1944
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-ralph-progress-stale-'));
1945
- const fakeBinDir = join(wd, 'fake-bin');
1946
- const stateDir = join(wd, '.rcs', 'state');
1947
- const tmuxLogPath = join(wd, 'tmux.log');
1948
- const statePath = join(stateDir, 'notify-fallback-state.json');
1949
- try {
1950
- await mkdir(stateDir, { recursive: true });
1951
- await mkdir(fakeBinDir, { recursive: true });
1952
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
1953
- await writeFile(join(stateDir, 'ralph-state.json'), JSON.stringify({
1954
- active: true,
1955
- current_phase: 'executing',
1956
- tmux_pane_id: '%42',
1957
- }, null, 2));
1958
- await writeFile(join(stateDir, 'hud-state.json'), JSON.stringify({
1959
- last_progress_at: new Date(Date.now() - 61_000).toISOString(),
1960
- }, null, 2));
1961
- await writeFile(statePath, JSON.stringify({
1962
- ralph_continue_steer: {
1963
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
1964
- },
1965
- }, null, 2));
1966
- const watcherScript = distScript('notify-fallback-watcher.js');
1967
- const notifyHook = distScript('notify-hook.js');
1968
- const env = {
1969
- ...buildCleanNotifyEnv(),
1970
- PATH: prependPath(fakeBinDir),
1971
- };
1972
- const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env });
1973
- assert.equal(run.status, 0, run.stderr || run.stdout);
1974
- const tmuxLog = await readFile(tmuxLogPath, 'utf8');
1975
- const sends = tmuxLog.match(/send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/g) || [];
1976
- assert.equal(sends.length, 1, 'stale progress should still allow continue steer once cooldown elapses');
1977
- const watcherState = JSON.parse(await readFile(statePath, 'utf-8'));
1978
- assert.equal(watcherState.ralph_continue_steer?.last_reason, 'sent');
1979
- }
1980
- finally {
1981
- await rm(wd, { recursive: true, force: true });
1982
- }
1983
- });
1984
- it('suppresses Ralph continue steer when session-scoped Ralph is stuck in stale starting phase', async () => {
1985
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-ralph-starting-stale-'));
1986
- const fakeBinDir = join(wd, 'fake-bin');
1987
- const tmuxLogPath = join(wd, 'tmux.log');
1988
- const stateDir = join(wd, '.rcs', 'state');
1989
- const sessionId = 'sess-starting-stale';
1990
- const sessionStateDir = join(stateDir, 'sessions', sessionId);
1991
- const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
1992
- try {
1993
- await mkdir(sessionStateDir, { recursive: true });
1994
- await mkdir(fakeBinDir, { recursive: true });
1995
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
1996
- await writeSessionStart(wd, sessionId);
1997
- await writeFile(join(sessionStateDir, 'ralph-state.json'), JSON.stringify({
1998
- active: true,
1999
- current_phase: 'starting',
2000
- started_at: new Date(Date.now() - 180_000).toISOString(),
2001
- tmux_pane_id: '%42',
2002
- }, null, 2));
2003
- await writeFile(join(sessionStateDir, 'hud-state.json'), JSON.stringify({
2004
- last_progress_at: new Date(Date.now() - 180_000).toISOString(),
2005
- }, null, 2));
2006
- await writeFile(watcherStatePath, JSON.stringify({
2007
- ralph_continue_steer: {
2008
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2009
- },
2010
- }, null, 2));
2011
- const watcherScript = distScript('notify-fallback-watcher.js');
2012
- const notifyHook = distScript('notify-hook.js');
2013
- const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
2014
- encoding: 'utf-8',
2015
- env: buildCleanNotifyEnv({
2016
- PATH: prependPath(fakeBinDir),
2017
- }),
2018
- });
2019
- assert.equal(run.status, 0, run.stderr || run.stdout);
2020
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
2021
- const sends = tmuxLog.match(/send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/g) || [];
2022
- assert.equal(sends.length, 0, 'stale starting phase should suppress continue steer');
2023
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
2024
- assert.equal(watcherState.ralph_continue_steer?.last_reason, 'starting_stale');
2025
- }
2026
- finally {
2027
- await rm(wd, { recursive: true, force: true });
2028
- }
2029
- });
2030
- it('suppresses Ralph continue steer while tracked native subagents are still active', async () => {
2031
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-ralph-subagents-active-'));
2032
- const fakeBinDir = join(wd, 'fake-bin');
2033
- const stateDir = join(wd, '.rcs', 'state');
2034
- const tmuxLogPath = join(wd, 'tmux.log');
2035
- const statePath = join(stateDir, 'notify-fallback-state.json');
2036
- const fixtureSessionId = 'sess-current';
2037
- const codexSessionId = 'codex-session-1';
2038
- try {
2039
- await mkdir(join(stateDir, 'sessions', fixtureSessionId), { recursive: true });
2040
- await mkdir(fakeBinDir, { recursive: true });
2041
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
2042
- await writeSessionStart(wd, fixtureSessionId);
2043
- await writeFile(join(stateDir, 'sessions', fixtureSessionId, 'ralph-state.json'), JSON.stringify({
2044
- active: true,
2045
- current_phase: 'executing',
2046
- tmux_pane_id: '%42',
2047
- owner_rcs_session_id: fixtureSessionId,
2048
- owner_codex_session_id: codexSessionId,
2049
- }, null, 2));
2050
- await writeFile(join(stateDir, 'sessions', fixtureSessionId, 'hud-state.json'), JSON.stringify({
2051
- last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2052
- }, null, 2));
2053
- await writeFile(statePath, JSON.stringify({
2054
- ralph_continue_steer: {
2055
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2056
- },
2057
- }, null, 2));
2058
- await writeFile(join(stateDir, 'subagent-tracking.json'), JSON.stringify({
2059
- schemaVersion: 1,
2060
- sessions: {
2061
- [codexSessionId]: {
2062
- session_id: codexSessionId,
2063
- leader_thread_id: 'leader-thread',
2064
- updated_at: new Date(Date.now() - 15_000).toISOString(),
2065
- threads: {
2066
- 'leader-thread': {
2067
- thread_id: 'leader-thread',
2068
- kind: 'leader',
2069
- first_seen_at: new Date(Date.now() - 30_000).toISOString(),
2070
- last_seen_at: new Date(Date.now() - 15_000).toISOString(),
2071
- turn_count: 1,
2072
- mode: 'ralph',
2073
- },
2074
- 'sub-thread-1': {
2075
- thread_id: 'sub-thread-1',
2076
- kind: 'subagent',
2077
- first_seen_at: new Date(Date.now() - 30_000).toISOString(),
2078
- last_seen_at: new Date(Date.now() - 15_000).toISOString(),
2079
- turn_count: 1,
2080
- mode: 'ralph',
2081
- },
2082
- },
2083
- },
2084
- },
2085
- }, null, 2));
2086
- const watcherScript = distScript('notify-fallback-watcher.js');
2087
- const notifyHook = distScript('notify-hook.js');
2088
- const env = {
2089
- ...buildCleanNotifyEnv(),
2090
- PATH: prependPath(fakeBinDir),
2091
- };
2092
- const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env });
2093
- assert.equal(run.status, 0, run.stderr || run.stdout);
2094
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
2095
- const sends = tmuxLog.match(/send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/g) || [];
2096
- assert.equal(sends.length, 0, 'active native subagents should block fallback continue steer');
2097
- const watcherState = JSON.parse(await readFile(statePath, 'utf-8'));
2098
- assert.equal(watcherState.ralph_continue_steer?.last_reason, 'subagents_active');
2099
- assert.equal(watcherState.ralph_continue_steer?.subagent_session_id, codexSessionId);
2100
- assert.deepEqual(watcherState.ralph_continue_steer?.active_subagent_thread_ids, ['sub-thread-1']);
2101
- }
2102
- finally {
2103
- await rm(wd, { recursive: true, force: true });
2104
- }
2105
- });
2106
- it('fails closed when Ralph hud progress is missing or invalid', async () => {
2107
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-ralph-progress-guard-'));
2108
- const fakeBinDir = join(wd, 'fake-bin');
2109
- const stateDir = join(wd, '.rcs', 'state');
2110
- const tmuxLogPath = join(wd, 'tmux.log');
2111
- const statePath = join(stateDir, 'notify-fallback-state.json');
2112
- try {
2113
- await mkdir(stateDir, { recursive: true });
2114
- await mkdir(fakeBinDir, { recursive: true });
2115
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
2116
- await writeFile(join(stateDir, 'ralph-state.json'), JSON.stringify({
2117
- active: true,
2118
- current_phase: 'executing',
2119
- tmux_pane_id: '%42',
2120
- }, null, 2));
2121
- await writeFile(statePath, JSON.stringify({
2122
- ralph_continue_steer: {
2123
- pane_id: '%7',
2124
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2125
- },
2126
- }, null, 2));
2127
- const watcherScript = distScript('notify-fallback-watcher.js');
2128
- const notifyHook = distScript('notify-hook.js');
2129
- const env = {
2130
- ...buildCleanNotifyEnv(),
2131
- PATH: prependPath(fakeBinDir),
2132
- };
2133
- const missingRun = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env });
2134
- assert.equal(missingRun.status, 0, missingRun.stderr || missingRun.stdout);
2135
- let watcherState = JSON.parse(await readFile(statePath, 'utf-8'));
2136
- assert.equal(watcherState.ralph_continue_steer?.last_reason, 'progress_missing');
2137
- assert.equal(watcherState.ralph_continue_steer?.pane_id, '%42');
2138
- await writeFile(join(stateDir, 'hud-state.json'), JSON.stringify({
2139
- last_progress_at: 'not-a-date',
2140
- }, null, 2));
2141
- const invalidRun = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env });
2142
- assert.equal(invalidRun.status, 0, invalidRun.stderr || invalidRun.stdout);
2143
- watcherState = JSON.parse(await readFile(statePath, 'utf-8'));
2144
- assert.equal(watcherState.ralph_continue_steer?.last_reason, 'progress_invalid');
2145
- assert.equal(watcherState.ralph_continue_steer?.pane_id, '%42');
2146
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
2147
- const sends = tmuxLog.match(/send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/g) || [];
2148
- assert.equal(sends.length, 0, 'missing or invalid progress should fail closed without sending steer');
2149
- }
2150
- finally {
2151
- await rm(wd, { recursive: true, force: true });
2152
- }
2153
- });
2154
- it('fails closed when active Ralph state has no bound tmux pane', async () => {
2155
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-ralph-pane-missing-'));
2156
- const fakeBinDir = join(wd, 'fake-bin');
2157
- const stateDir = join(wd, '.rcs', 'state');
2158
- const tmuxLogPath = join(wd, 'tmux.log');
2159
- const statePath = join(stateDir, 'notify-fallback-state.json');
2160
- try {
2161
- await mkdir(stateDir, { recursive: true });
2162
- await mkdir(fakeBinDir, { recursive: true });
2163
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
2164
- await writeFile(join(stateDir, 'ralph-state.json'), JSON.stringify({
2165
- active: true,
2166
- current_phase: 'executing',
2167
- }, null, 2));
2168
- await writeFile(join(stateDir, 'hud-state.json'), JSON.stringify({
2169
- last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2170
- }, null, 2));
2171
- await writeFile(statePath, JSON.stringify({
2172
- ralph_continue_steer: {
2173
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2174
- },
2175
- }, null, 2));
2176
- const watcherScript = distScript('notify-fallback-watcher.js');
2177
- const notifyHook = distScript('notify-hook.js');
2178
- const env = {
2179
- ...buildCleanNotifyEnv(),
2180
- PATH: prependPath(fakeBinDir),
2181
- };
2182
- const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env });
2183
- assert.equal(run.status, 0, run.stderr || run.stdout);
2184
- const watcherState = JSON.parse(await readFile(statePath, 'utf-8'));
2185
- assert.equal(watcherState.ralph_continue_steer?.last_reason, 'pane_missing');
2186
- assert.equal(watcherState.ralph_continue_steer?.pane_id, '');
2187
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
2188
- assert.equal(/send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/.test(tmuxLog), false);
2189
- assert.equal(/display-message -p -t %42 #{pane_id}/.test(tmuxLog), false, 'watcher should not guess a pane when tmux_pane_id is missing');
2190
- }
2191
- finally {
2192
- await rm(wd, { recursive: true, force: true });
2193
- }
2194
- });
2195
- it('rebinds a stale-but-present session-scoped Ralph shell pane to the live pane before continue steer', async () => {
2196
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-ralph-rebind-stale-anchor-'));
2197
- const fakeBinDir = join(wd, 'fake-bin');
2198
- const stateDir = join(wd, '.rcs', 'state');
2199
- const tmuxLogPath = join(wd, 'tmux.log');
2200
- const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
2201
- const sessionId = 'sess-ralph-rebind';
2202
- const sessionStateDir = join(stateDir, 'sessions', sessionId);
2203
- const ralphStatePath = join(sessionStateDir, 'ralph-state.json');
2204
- const anchorPane = '%99';
2205
- const livePane = '%42';
2206
- try {
2207
- await mkdir(sessionStateDir, { recursive: true });
2208
- await mkdir(fakeBinDir, { recursive: true });
2209
- const managedSessionName = buildTmuxSessionName(wd, sessionId);
2210
- await writeSessionStart(wd, sessionId, { tmuxSessionName: managedSessionName });
2211
- const sessionJson = JSON.parse(await readFile(join(wd, '.rcs', 'state', 'session.json'), 'utf-8'));
2212
- assert.equal(sessionJson.tmux_session_name, managedSessionName);
2213
- await writeFakeTmuxExecutable(fakeBinDir, buildManagedRalphTmux(tmuxLogPath, {
2214
- cwd: wd,
2215
- managedSessionName,
2216
- anchorPane,
2217
- livePane,
2218
- codexPanes: [
2219
- { paneId: anchorPane, active: false, currentCommand: 'sh', startCommand: 'bash' },
2220
- { paneId: '%41', active: false, currentCommand: 'codex', startCommand: 'codex' },
2221
- { paneId: livePane, active: true, currentCommand: 'codex', startCommand: 'codex' },
2222
- ],
2223
- }));
2224
- await writeFile(ralphStatePath, JSON.stringify({
2225
- active: true,
2226
- current_phase: 'executing',
2227
- tmux_pane_id: anchorPane,
2228
- }, null, 2));
2229
- await writeFile(join(sessionStateDir, 'hud-state.json'), JSON.stringify({
2230
- last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2231
- }, null, 2));
2232
- await writeFile(watcherStatePath, JSON.stringify({
2233
- ralph_continue_steer: {
2234
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2235
- },
2236
- }, null, 2));
2237
- const watcherScript = distScript('notify-fallback-watcher.js');
2238
- const notifyHook = distScript('notify-hook.js');
2239
- const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
2240
- encoding: 'utf-8',
2241
- env: buildCleanNotifyEnv({
2242
- PATH: prependPath(fakeBinDir),
2243
- RCS_TEST_RELAX_TMUX_TIMEOUT: '1',
2244
- }),
2245
- });
2246
- assert.equal(run.status, 0, run.stderr || run.stdout);
2247
- const watcherProbe = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
2248
- const tmuxDebugLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
2249
- assert.equal(watcherProbe.ralph_continue_steer?.last_reason, 'sent', `${JSON.stringify(watcherProbe.ralph_continue_steer ?? {})}\nTMUX_LOG:\n${tmuxDebugLog}`);
2250
- const persistedRalph = JSON.parse(await readFile(ralphStatePath, 'utf-8'));
2251
- assert.equal(persistedRalph.tmux_pane_id, livePane);
2252
- assert.match(persistedRalph.tmux_pane_set_at ?? '', /^\d{4}-\d{2}-\d{2}T/);
2253
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
2254
- assert.equal(watcherState.ralph_continue_steer?.last_reason, 'sent');
2255
- assert.equal(watcherState.ralph_continue_steer?.pane_id, livePane);
2256
- const tmuxLog = await readFile(tmuxLogPath, 'utf8');
2257
- assert.match(tmuxLog, /send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/);
2258
- assert.doesNotMatch(tmuxLog, /send-keys -t %99 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/);
2259
- }
2260
- finally {
2261
- await rm(wd, { recursive: true, force: true });
2262
- }
2263
- });
2264
- it('preserves newer Ralph state fields when a pane rebound happens after the state file advances', async () => {
2265
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-ralph-rebind-state-merge-'));
2266
- const fakeBinDir = join(wd, 'fake-bin');
2267
- const stateDir = join(wd, '.rcs', 'state');
2268
- const tmuxLogPath = join(wd, 'tmux.log');
2269
- const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
2270
- const sessionId = 'sess-ralph-rebind-merge';
2271
- const sessionStateDir = join(stateDir, 'sessions', sessionId);
2272
- const ralphStatePath = join(sessionStateDir, 'ralph-state.json');
2273
- const anchorPane = '%99';
2274
- const livePane = '%42';
2275
- try {
2276
- await mkdir(sessionStateDir, { recursive: true });
2277
- await mkdir(fakeBinDir, { recursive: true });
2278
- await writeSessionStart(wd, sessionId);
2279
- const managedSessionName = buildTmuxSessionName(wd, sessionId);
2280
- const fakeTmux = `#!/usr/bin/env bash
2281
- set -eu
2282
- echo "$@" >> "${tmuxLogPath}"
2283
- cmd="$1"
2284
- shift || true
2285
- if [[ "$cmd" == "display-message" ]]; then
2286
- target=""
2287
- format=""
2288
- while [[ "$#" -gt 0 ]]; do
2289
- case "$1" in
2290
- -p) shift ;;
2291
- -t) target="$2"; shift 2 ;;
2292
- *) format="$1"; shift ;;
2293
- esac
2294
- done
2295
- if [[ "$format" == "#{pane_in_mode}" ]]; then
2296
- echo "0"
2297
- exit 0
2298
- fi
2299
- if [[ "$format" == "#{pane_id}" ]]; then
2300
- echo "$target"
2301
- exit 0
2302
- fi
2303
- if [[ "$format" == "#{pane_current_path}" ]]; then
2304
- echo "${wd}"
2305
- exit 0
2306
- fi
2307
- if [[ "$format" == "#{pane_current_command}" && "$target" == "${anchorPane}" ]]; then
2308
- echo "sh"
2309
- exit 0
2310
- fi
2311
- if [[ "$format" == "#{pane_start_command}" && "$target" == "${anchorPane}" ]]; then
2312
- echo "bash"
2313
- exit 0
2314
- fi
2315
- if [[ "$format" == "#S" && "$target" == "${anchorPane}" ]]; then
2316
- echo "${managedSessionName}"
2317
- exit 0
2318
- fi
2319
- exit 0
2320
- fi
2321
- if [[ "$cmd" == "list-panes" ]]; then
2322
- target=""
2323
- while [[ "$#" -gt 0 ]]; do
2324
- case "$1" in
2325
- -F) shift 2 ;;
2326
- -t) shift; target="$1" ;;
2327
- esac
2328
- shift || true
2329
- done
2330
- if [[ "$target" == "${managedSessionName}" ]]; then
2331
- cat > "${ralphStatePath}" <<'JSON'
2332
- {
2333
- "active": true,
2334
- "current_phase": "reviewing",
2335
- "iteration": 11,
2336
- "owner_codex_session_id": "codex-updated-owner",
2337
- "tmux_pane_id": "%99"
2338
- }
2339
- JSON
2340
- printf "%%99\t0\tsh\tbash\n%%42\t1\tcodex\tcodex\n"
2341
- exit 0
2342
- fi
2343
- echo "can't find session" >&2
2344
- exit 1
2345
- fi
2346
- if [[ "$cmd" == "capture-pane" ]]; then
2347
- exit 0
2348
- fi
2349
- if [[ "$cmd" == "send-keys" ]]; then
2350
- exit 0
2351
- fi
2352
- exit 0
2353
- `;
2354
- await writeFakeTmuxExecutable(fakeBinDir, fakeTmux);
2355
- await writeFile(ralphStatePath, JSON.stringify({
2356
- active: true,
2357
- current_phase: 'executing',
2358
- iteration: 1,
2359
- owner_codex_session_id: 'codex-stale-owner',
2360
- tmux_pane_id: anchorPane,
2361
- }, null, 2));
2362
- await writeFile(join(sessionStateDir, 'hud-state.json'), JSON.stringify({
2363
- last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2364
- }, null, 2));
2365
- await writeFile(watcherStatePath, JSON.stringify({
2366
- ralph_continue_steer: {
2367
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2368
- },
2369
- }, null, 2));
2370
- const watcherScript = distScript('notify-fallback-watcher.js');
2371
- const notifyHook = distScript('notify-hook.js');
2372
- const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
2373
- encoding: 'utf-8',
2374
- env: buildCleanNotifyEnv({
2375
- PATH: prependPath(fakeBinDir),
2376
- }),
2377
- });
2378
- assert.equal(run.status, 0, run.stderr || run.stdout);
2379
- const persistedRalph = JSON.parse(await readFile(ralphStatePath, 'utf-8'));
2380
- assert.equal(persistedRalph.tmux_pane_id, livePane);
2381
- assert.equal(persistedRalph.current_phase, 'reviewing');
2382
- assert.equal(persistedRalph.iteration, 11);
2383
- assert.equal(persistedRalph.owner_codex_session_id, 'codex-updated-owner');
2384
- assert.match(persistedRalph.tmux_pane_set_at ?? '', /^\d{4}-\d{2}-\d{2}T/);
2385
- }
2386
- finally {
2387
- await rm(wd, { recursive: true, force: true });
2388
- }
2389
- });
2390
- it('keeps the verified Ralph anchor pane when another codex pane is focused in the same managed session', async () => {
2391
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-ralph-keep-anchor-pane-'));
2392
- const fakeBinDir = join(wd, 'fake-bin');
2393
- const stateDir = join(wd, '.rcs', 'state');
2394
- const tmuxLogPath = join(wd, 'tmux.log');
2395
- const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
2396
- const sessionId = 'sess-ralph-keep-anchor';
2397
- const sessionStateDir = join(stateDir, 'sessions', sessionId);
2398
- const ralphStatePath = join(sessionStateDir, 'ralph-state.json');
2399
- const anchorPane = '%99';
2400
- const livePane = '%42';
2401
- try {
2402
- await mkdir(sessionStateDir, { recursive: true });
2403
- await mkdir(fakeBinDir, { recursive: true });
2404
- const managedSessionName = buildTmuxSessionName(wd, sessionId);
2405
- await writeSessionStart(wd, sessionId, { tmuxSessionName: managedSessionName });
2406
- await writeFakeTmuxExecutable(fakeBinDir, buildManagedRalphTmux(tmuxLogPath, {
2407
- cwd: wd,
2408
- managedSessionName,
2409
- anchorPane,
2410
- livePane,
2411
- codexPanes: [
2412
- { paneId: anchorPane, active: false, currentCommand: 'codex', startCommand: 'codex' },
2413
- { paneId: livePane, active: true, currentCommand: 'codex', startCommand: 'codex' },
2414
- ],
2415
- }));
2416
- await writeFile(ralphStatePath, JSON.stringify({
2417
- active: true,
2418
- current_phase: 'executing',
2419
- tmux_pane_id: anchorPane,
2420
- }, null, 2));
2421
- await writeFile(join(sessionStateDir, 'hud-state.json'), JSON.stringify({
2422
- last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2423
- }, null, 2));
2424
- await writeFile(watcherStatePath, JSON.stringify({
2425
- ralph_continue_steer: {
2426
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2427
- },
2428
- }, null, 2));
2429
- const watcherScript = distScript('notify-fallback-watcher.js');
2430
- const notifyHook = distScript('notify-hook.js');
2431
- const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
2432
- encoding: 'utf-8',
2433
- env: buildCleanNotifyEnv({
2434
- PATH: prependPath(fakeBinDir),
2435
- }),
2436
- });
2437
- assert.equal(run.status, 0, run.stderr || run.stdout);
2438
- const persistedRalph = JSON.parse(await readFile(ralphStatePath, 'utf-8'));
2439
- assert.equal(persistedRalph.tmux_pane_id, anchorPane);
2440
- assert.equal(typeof persistedRalph.tmux_pane_set_at, 'undefined');
2441
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
2442
- assert.equal(watcherState.ralph_continue_steer?.last_reason, 'sent');
2443
- assert.equal(watcherState.ralph_continue_steer?.pane_id, anchorPane);
2444
- const tmuxLog = await readFile(tmuxLogPath, 'utf8');
2445
- assert.match(tmuxLog, /send-keys -t %99 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/);
2446
- assert.doesNotMatch(tmuxLog, /send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/);
2447
- }
2448
- finally {
2449
- await rm(wd, { recursive: true, force: true });
2450
- }
2451
- });
2452
- it('rebinds a shell-degraded codex anchor to the live pane before continue steer', async () => {
2453
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-ralph-rebind-degraded-codex-anchor-'));
2454
- const fakeBinDir = join(wd, 'fake-bin');
2455
- const stateDir = join(wd, '.rcs', 'state');
2456
- const tmuxLogPath = join(wd, 'tmux.log');
2457
- const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
2458
- const sessionId = 'sess-ralph-degraded-codex-anchor';
2459
- const sessionStateDir = join(stateDir, 'sessions', sessionId);
2460
- const ralphStatePath = join(sessionStateDir, 'ralph-state.json');
2461
- const anchorPane = '%99';
2462
- const livePane = '%42';
2463
- try {
2464
- await mkdir(sessionStateDir, { recursive: true });
2465
- await mkdir(fakeBinDir, { recursive: true });
2466
- const managedSessionName = buildTmuxSessionName(wd, sessionId);
2467
- await writeSessionStart(wd, sessionId, { tmuxSessionName: managedSessionName });
2468
- await writeFakeTmuxExecutable(fakeBinDir, buildManagedRalphTmux(tmuxLogPath, {
2469
- cwd: wd,
2470
- managedSessionName,
2471
- anchorPane,
2472
- livePane,
2473
- codexPanes: [
2474
- { paneId: anchorPane, active: true, currentCommand: 'bash', startCommand: 'codex --model gpt-5' },
2475
- { paneId: livePane, active: false, currentCommand: 'codex', startCommand: 'codex' },
2476
- ],
2477
- }));
2478
- await writeFile(ralphStatePath, JSON.stringify({
2479
- active: true,
2480
- current_phase: 'executing',
2481
- tmux_pane_id: anchorPane,
2482
- }, null, 2));
2483
- await writeFile(join(sessionStateDir, 'hud-state.json'), JSON.stringify({
2484
- last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2485
- }, null, 2));
2486
- await writeFile(watcherStatePath, JSON.stringify({
2487
- ralph_continue_steer: {
2488
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2489
- },
2490
- }, null, 2));
2491
- const watcherScript = distScript('notify-fallback-watcher.js');
2492
- const notifyHook = distScript('notify-hook.js');
2493
- const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
2494
- encoding: 'utf-8',
2495
- env: buildCleanNotifyEnv({
2496
- PATH: prependPath(fakeBinDir),
2497
- }),
2498
- });
2499
- assert.equal(run.status, 0, run.stderr || run.stdout);
2500
- const persistedRalph = JSON.parse(await readFile(ralphStatePath, 'utf-8'));
2501
- assert.equal(persistedRalph.tmux_pane_id, livePane);
2502
- assert.match(persistedRalph.tmux_pane_set_at ?? '', /^\d{4}-\d{2}-\d{2}T/);
2503
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
2504
- assert.equal(watcherState.ralph_continue_steer?.last_reason, 'sent');
2505
- assert.equal(watcherState.ralph_continue_steer?.pane_id, livePane);
2506
- const tmuxLog = await readFile(tmuxLogPath, 'utf8');
2507
- assert.match(tmuxLog, /send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/);
2508
- assert.doesNotMatch(tmuxLog, /send-keys -t %99 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/);
2509
- }
2510
- finally {
2511
- await rm(wd, { recursive: true, force: true });
2512
- }
2513
- });
2514
- it('falls back to the current managed session pane when the stored Ralph pane anchor is dead', async () => {
2515
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-ralph-rebind-dead-anchor-'));
2516
- const fakeBinDir = join(wd, 'fake-bin');
2517
- const stateDir = join(wd, '.rcs', 'state');
2518
- const tmuxLogPath = join(wd, 'tmux.log');
2519
- const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
2520
- const sessionId = 'sess-ralph-dead-anchor';
2521
- const sessionStateDir = join(stateDir, 'sessions', sessionId);
2522
- const ralphStatePath = join(sessionStateDir, 'ralph-state.json');
2523
- const anchorPane = '%99';
2524
- const livePane = '%42';
2525
- try {
2526
- await mkdir(sessionStateDir, { recursive: true });
2527
- await mkdir(fakeBinDir, { recursive: true });
2528
- await writeSessionStart(wd, sessionId);
2529
- const managedSessionName = buildTmuxSessionName(wd, sessionId);
2530
- await writeFakeTmuxExecutable(fakeBinDir, buildManagedRalphTmux(tmuxLogPath, {
2531
- cwd: wd,
2532
- managedSessionName,
2533
- anchorPane,
2534
- livePane,
2535
- codexPanes: [
2536
- { paneId: '%41', active: false, currentCommand: 'codex', startCommand: 'codex' },
2537
- { paneId: livePane, active: true, currentCommand: 'codex', startCommand: 'codex' },
2538
- ],
2539
- missingAnchor: true,
2540
- }));
2541
- await writeFile(ralphStatePath, JSON.stringify({
2542
- active: true,
2543
- current_phase: 'executing',
2544
- tmux_pane_id: anchorPane,
2545
- }, null, 2));
2546
- await writeFile(join(sessionStateDir, 'hud-state.json'), JSON.stringify({
2547
- last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2548
- }, null, 2));
2549
- await writeFile(watcherStatePath, JSON.stringify({
2550
- ralph_continue_steer: {
2551
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2552
- },
2553
- }, null, 2));
2554
- const watcherScript = distScript('notify-fallback-watcher.js');
2555
- const notifyHook = distScript('notify-hook.js');
2556
- const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
2557
- encoding: 'utf-8',
2558
- env: buildCleanNotifyEnv({
2559
- PATH: prependPath(fakeBinDir),
2560
- }),
2561
- });
2562
- assert.equal(run.status, 0, run.stderr || run.stdout);
2563
- const persistedRalph = JSON.parse(await readFile(ralphStatePath, 'utf-8'));
2564
- assert.equal(persistedRalph.tmux_pane_id, livePane);
2565
- assert.match(persistedRalph.tmux_pane_set_at ?? '', /^\d{4}-\d{2}-\d{2}T/);
2566
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
2567
- assert.equal(watcherState.ralph_continue_steer?.last_reason, 'sent');
2568
- assert.equal(watcherState.ralph_continue_steer?.pane_id, livePane);
2569
- const tmuxLog = await readFile(tmuxLogPath, 'utf8');
2570
- assert.match(tmuxLog, /display-message -p -t %99 #S/);
2571
- assert.match(tmuxLog, /list-panes -s -t .*sess-ralph-dead-anchor/);
2572
- assert.match(tmuxLog, /send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/);
2573
- assert.doesNotMatch(tmuxLog, /send-keys -t %99 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/);
2574
- }
2575
- finally {
2576
- await rm(wd, { recursive: true, force: true });
2577
- }
2578
- });
2579
- it('sends the first Ralph continue steer immediately when persisted steer state is empty', async () => {
2580
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-ralph-startup-cooldown-'));
2581
- const fakeBinDir = join(wd, 'fake-bin');
2582
- const stateDir = join(wd, '.rcs', 'state');
2583
- const tmuxLogPath = join(wd, 'tmux.log');
2584
- const statePath = join(stateDir, 'notify-fallback-state.json');
2585
- try {
2586
- await mkdir(stateDir, { recursive: true });
2587
- await mkdir(fakeBinDir, { recursive: true });
2588
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
2589
- await writeFile(join(stateDir, 'ralph-state.json'), JSON.stringify({
2590
- active: true,
2591
- current_phase: 'executing',
2592
- tmux_pane_id: '%42',
2593
- }, null, 2));
2594
- await writeFile(join(stateDir, 'hud-state.json'), JSON.stringify({
2595
- last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2596
- }, null, 2));
2597
- await writeFile(statePath, JSON.stringify({
2598
- ralph_continue_steer: {
2599
- last_sent_at: '',
2600
- },
2601
- }, null, 2));
2602
- const watcherScript = distScript('notify-fallback-watcher.js');
2603
- const notifyHook = distScript('notify-hook.js');
2604
- const env = {
2605
- ...buildCleanNotifyEnv(),
2606
- PATH: prependPath(fakeBinDir),
2607
- };
2608
- const first = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env });
2609
- assert.equal(first.status, 0, first.stderr || first.stdout);
2610
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
2611
- const sends = tmuxLog.match(/send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/g) || [];
2612
- assert.equal(sends.length, 1, 'empty startup state should send the first Ralph steer immediately once progress is stale');
2613
- const watcherState = JSON.parse(await readFile(statePath, 'utf-8'));
2614
- assert.equal(watcherState.ralph_continue_steer?.last_reason, 'sent');
2615
- assert.match(watcherState.ralph_continue_steer?.last_sent_at ?? '', /^\d{4}-\d{2}-\d{2}T/, 'first steer should persist a real send timestamp for active-state signaling');
2616
- assert.equal(watcherState.ralph_continue_steer?.cooldown_anchor_at, watcherState.ralph_continue_steer?.last_sent_at, 'first steer should anchor subsequent cooldowns to the real send time');
2617
- }
2618
- finally {
2619
- await rm(wd, { recursive: true, force: true });
2620
- }
2621
- });
2622
- it('falls back to an aged persisted cooldown anchor when last_sent_at is invalid', async () => {
2623
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-ralph-invalid-last-sent-'));
2624
- const fakeBinDir = join(wd, 'fake-bin');
2625
- const stateDir = join(wd, '.rcs', 'state');
2626
- const tmuxLogPath = join(wd, 'tmux.log');
2627
- const statePath = join(stateDir, 'notify-fallback-state.json');
2628
- try {
2629
- await mkdir(stateDir, { recursive: true });
2630
- await mkdir(fakeBinDir, { recursive: true });
2631
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
2632
- await writeFile(join(stateDir, 'ralph-state.json'), JSON.stringify({
2633
- active: true,
2634
- current_phase: 'executing',
2635
- tmux_pane_id: '%42',
2636
- }, null, 2));
2637
- await writeFile(join(stateDir, 'hud-state.json'), JSON.stringify({
2638
- last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2639
- }, null, 2));
2640
- await writeFile(statePath, JSON.stringify({
2641
- ralph_continue_steer: {
2642
- last_sent_at: 'not-a-date',
2643
- cooldown_anchor_at: new Date(Date.now() - 61_000).toISOString(),
2644
- },
2645
- }, null, 2));
2646
- const watcherScript = distScript('notify-fallback-watcher.js');
2647
- const notifyHook = distScript('notify-hook.js');
2648
- const env = {
2649
- ...buildCleanNotifyEnv(),
2650
- PATH: prependPath(fakeBinDir),
2651
- };
2652
- const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env });
2653
- assert.equal(run.status, 0, run.stderr || run.stdout);
2654
- const tmuxLog = await readFile(tmuxLogPath, 'utf8');
2655
- const sends = tmuxLog.match(/send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/g) || [];
2656
- assert.equal(sends.length, 1, 'invalid last_sent_at should fall back to the persisted cooldown anchor once 60s have elapsed');
2657
- const watcherState = JSON.parse(await readFile(statePath, 'utf-8'));
2658
- assert.match(watcherState.ralph_continue_steer?.last_sent_at ?? '', /^\d{4}-\d{2}-\d{2}T/, 'successful fallback send should replace the invalid last_sent_at with a valid ISO timestamp');
2659
- assert.equal(watcherState.ralph_continue_steer?.cooldown_anchor_at, watcherState.ralph_continue_steer?.last_sent_at, 'fallback send should also refresh the persisted cooldown anchor');
2660
- }
2661
- finally {
2662
- await rm(wd, { recursive: true, force: true });
2663
- }
2664
- });
2665
- it('treats blocked_on_user as terminal so Ralph continue steer stays off', async () => {
2666
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-ralph-blocked-on-user-'));
2667
- const fakeBinDir = join(wd, 'fake-bin');
2668
- const tmuxLogPath = join(wd, 'tmux.log');
2669
- const stateDir = join(wd, '.rcs', 'state');
2670
- const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
2671
- try {
2672
- await mkdir(stateDir, { recursive: true });
2673
- await mkdir(fakeBinDir, { recursive: true });
2674
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
2675
- await writeFile(join(stateDir, 'ralph-state.json'), JSON.stringify({
2676
- active: false,
2677
- current_phase: 'blocked_on_user',
2678
- completed_at: new Date().toISOString(),
2679
- tmux_pane_id: '%42',
2680
- }, null, 2));
2681
- await writeFile(join(stateDir, 'hud-state.json'), JSON.stringify({
2682
- last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2683
- }, null, 2));
2684
- await writeFile(watcherStatePath, JSON.stringify({
2685
- ralph_continue_steer: {
2686
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2687
- },
2688
- }, null, 2));
2689
- const watcherScript = distScript('notify-fallback-watcher.js');
2690
- const notifyHook = distScript('notify-hook.js');
2691
- const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
2692
- encoding: 'utf-8',
2693
- env: buildCleanNotifyEnv({
2694
- PATH: prependPath(fakeBinDir),
2695
- }),
2696
- });
2697
- assert.equal(run.status, 0, run.stderr || run.stdout);
2698
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
2699
- const sends = tmuxLog.match(/send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/g) || [];
2700
- assert.equal(sends.length, 0, 'blocked_on_user should suppress Ralph continue steer');
2701
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
2702
- assert.equal(watcherState.ralph_continue_steer?.active, false);
2703
- assert.equal(watcherState.ralph_continue_steer?.last_reason, 'terminal');
2704
- }
2705
- finally {
2706
- await rm(wd, { recursive: true, force: true });
2707
- }
2708
- });
2709
- it('stops Ralph continue steer immediately once Ralph state is terminal or cleared', async () => {
2710
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-ralph-terminal-'));
2711
- const fakeBinDir = join(wd, 'fake-bin');
2712
- const tmuxLogPath = join(wd, 'tmux.log');
2713
- const stateDir = join(wd, '.rcs', 'state');
2714
- const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
2715
- const ralphStatePath = join(stateDir, 'ralph-state.json');
2716
- try {
2717
- await mkdir(stateDir, { recursive: true });
2718
- await mkdir(fakeBinDir, { recursive: true });
2719
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
2720
- await writeFile(ralphStatePath, JSON.stringify({
2721
- active: true,
2722
- current_phase: 'executing',
2723
- tmux_pane_id: '%42',
2724
- }, null, 2));
2725
- await writeFile(join(stateDir, 'hud-state.json'), JSON.stringify({
2726
- last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2727
- }, null, 2));
2728
- await writeFile(watcherStatePath, JSON.stringify({
2729
- ralph_continue_steer: {
2730
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2731
- },
2732
- }, null, 2));
2733
- const watcherScript = distScript('notify-fallback-watcher.js');
2734
- const notifyHook = distScript('notify-hook.js');
2735
- const env = {
2736
- ...buildCleanNotifyEnv(),
2737
- PATH: prependPath(fakeBinDir),
2738
- };
2739
- const first = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env });
2740
- assert.equal(first.status, 0, first.stderr || first.stdout);
2741
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
2742
- watcherState.ralph_continue_steer.last_sent_at = new Date(Date.now() - 61_000).toISOString();
2743
- await writeFile(watcherStatePath, JSON.stringify(watcherState, null, 2));
2744
- await writeFile(join(stateDir, 'hud-state.json'), JSON.stringify({
2745
- last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2746
- }, null, 2));
2747
- await writeFile(ralphStatePath, JSON.stringify({
2748
- active: false,
2749
- current_phase: 'complete',
2750
- completed_at: new Date().toISOString(),
2751
- tmux_pane_id: '%42',
2752
- }, null, 2));
2753
- const terminalRun = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env });
2754
- assert.equal(terminalRun.status, 0, terminalRun.stderr || terminalRun.stdout);
2755
- await rm(ralphStatePath, { force: true });
2756
- const clearedRun = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { encoding: 'utf-8', env });
2757
- assert.equal(clearedRun.status, 0, clearedRun.stderr || clearedRun.stdout);
2758
- const tmuxLog = await readFile(tmuxLogPath, 'utf8');
2759
- const sends = tmuxLog.match(/send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/g) || [];
2760
- assert.equal(sends.length, 1, 'terminal/cleared Ralph state must stop additional periodic steer sends');
2761
- }
2762
- finally {
2763
- await rm(wd, { recursive: true, force: true });
2764
- }
2765
- });
2766
- it('treats a long-running starting phase as terminal so Ralph steer stops', async () => {
2767
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-starting-phase-stale-'));
2768
- const fakeBinDir = join(wd, 'fake-bin');
2769
- const tmuxLogPath = join(wd, 'tmux.log');
2770
- const stateDir = join(wd, '.rcs', 'state');
2771
- const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
2772
- const staleStartedAt = new Date(Date.now() - 3 * 60_000).toISOString();
2773
- try {
2774
- await mkdir(stateDir, { recursive: true });
2775
- await mkdir(fakeBinDir, { recursive: true });
2776
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
2777
- await writeFile(join(stateDir, 'ralph-state.json'), JSON.stringify({
2778
- active: true,
2779
- current_phase: 'starting',
2780
- started_at: staleStartedAt,
2781
- tmux_pane_id: '%42',
2782
- }, null, 2));
2783
- await writeFile(join(stateDir, 'hud-state.json'), JSON.stringify({
2784
- last_progress_at: new Date(Date.now() - 5 * 60_000).toISOString(),
2785
- }, null, 2));
2786
- await writeFile(watcherStatePath, JSON.stringify({
2787
- ralph_continue_steer: {
2788
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2789
- },
2790
- }, null, 2));
2791
- const watcherScript = distScript('notify-fallback-watcher.js');
2792
- const notifyHook = distScript('notify-hook.js');
2793
- const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
2794
- encoding: 'utf-8',
2795
- env: buildCleanNotifyEnv({
2796
- PATH: prependPath(fakeBinDir),
2797
- }),
2798
- });
2799
- assert.equal(run.status, 0, run.stderr || run.stdout);
2800
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
2801
- const sends = tmuxLog.match(/send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/g) || [];
2802
- assert.equal(sends.length, 0, 'stale starting phase should block Ralph continue steer');
2803
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
2804
- assert.equal(watcherState.ralph_continue_steer?.active, false);
2805
- assert.equal(watcherState.ralph_continue_steer?.last_reason, 'terminal');
2806
- }
2807
- finally {
2808
- await rm(wd, { recursive: true, force: true });
2809
- }
2810
- });
2811
- it('treats an explicit blocked_on_user run_outcome as terminal for Ralph continue steer', async () => {
2812
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-ralph-blocked-on-user-'));
2813
- const fakeBinDir = join(wd, 'fake-bin');
2814
- const tmuxLogPath = join(wd, 'tmux.log');
2815
- const stateDir = join(wd, '.rcs', 'state');
2816
- const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
2817
- try {
2818
- await mkdir(stateDir, { recursive: true });
2819
- await mkdir(fakeBinDir, { recursive: true });
2820
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
2821
- await writeFile(join(stateDir, 'ralph-state.json'), JSON.stringify({
2822
- active: true,
2823
- current_phase: 'executing',
2824
- run_outcome: 'blocked_on_user',
2825
- tmux_pane_id: '%42',
2826
- }, null, 2));
2827
- await writeFile(join(stateDir, 'hud-state.json'), JSON.stringify({
2828
- last_progress_at: new Date(Date.now() - 5 * 60_000).toISOString(),
2829
- }, null, 2));
2830
- await writeFile(watcherStatePath, JSON.stringify({
2831
- ralph_continue_steer: {
2832
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2833
- },
2834
- }, null, 2));
2835
- const watcherScript = distScript('notify-fallback-watcher.js');
2836
- const notifyHook = distScript('notify-hook.js');
2837
- const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
2838
- encoding: 'utf-8',
2839
- env: buildCleanNotifyEnv({
2840
- PATH: prependPath(fakeBinDir),
2841
- }),
2842
- });
2843
- assert.equal(run.status, 0, run.stderr || run.stdout);
2844
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
2845
- const sends = tmuxLog.match(/send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/g) || [];
2846
- assert.equal(sends.length, 0, 'blocked_on_user should suppress Ralph continue steer');
2847
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
2848
- assert.equal(watcherState.ralph_continue_steer?.active, false);
2849
- assert.equal(watcherState.ralph_continue_steer?.last_reason, 'terminal');
2850
- }
2851
- finally {
2852
- await rm(wd, { recursive: true, force: true });
2853
- }
2854
- });
2855
- it('globally debounces Ralph continue steer across concurrent watcher instances', async () => {
2856
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-ralph-global-debounce-'));
2857
- const fakeBinDir = join(wd, 'fake-bin');
2858
- const tmuxLogPath = join(wd, 'tmux.log');
2859
- const stateDir = join(wd, '.rcs', 'state');
2860
- const sharedTimestampPath = join(stateDir, 'ralph-last-steer-at');
2861
- try {
2862
- await mkdir(stateDir, { recursive: true });
2863
- await mkdir(fakeBinDir, { recursive: true });
2864
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
2865
- await writeFile(join(stateDir, 'ralph-state.json'), JSON.stringify({
2866
- active: true,
2867
- current_phase: 'executing',
2868
- tmux_pane_id: '%42',
2869
- }, null, 2));
2870
- await writeFile(join(stateDir, 'hud-state.json'), JSON.stringify({
2871
- last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2872
- }, null, 2));
2873
- await writeFile(join(stateDir, 'notify-fallback-state.json'), JSON.stringify({
2874
- ralph_continue_steer: {
2875
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2876
- },
2877
- }, null, 2));
2878
- const watcherScript = distScript('notify-fallback-watcher.js');
2879
- const notifyHook = distScript('notify-hook.js');
2880
- const env = {
2881
- ...buildCleanNotifyEnv(),
2882
- PATH: prependPath(fakeBinDir),
2883
- };
2884
- const first = spawn(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { cwd: wd, stdio: 'pipe', env });
2885
- const second = spawn(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], { cwd: wd, stdio: 'pipe', env });
2886
- await Promise.all([waitForExit(first, 4000), waitForExit(second, 4000)]);
2887
- assert.equal(first.exitCode, 0);
2888
- assert.equal(second.exitCode, 0);
2889
- const tmuxLog = await readFile(tmuxLogPath, 'utf8');
2890
- const sends = tmuxLog.match(/send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/g) || [];
2891
- assert.equal(sends.length, 1, 'shared timestamp + lock should allow only one concurrent Ralph steer send');
2892
- const sharedTimestamp = (await readFile(sharedTimestampPath, 'utf-8')).trim();
2893
- assert.match(sharedTimestamp, /^\d{4}-\d{2}-\d{2}T/, 'concurrent send winner should persist a shared cooldown timestamp');
2894
- const watcherState = JSON.parse(await readFile(join(stateDir, 'notify-fallback-state.json'), 'utf-8'));
2895
- assert.equal(watcherState.ralph_continue_steer?.shared_timestamp_path, sharedTimestampPath);
2896
- assert.equal(watcherState.ralph_continue_steer?.shared_last_sent_at, sharedTimestamp);
2897
- assert.match(watcherState.ralph_continue_steer?.last_reason ?? '', /^(sent|global_cooldown|global_lock_busy)$/, 'final watcher state should reflect either the winner or a globally throttled loser');
2898
- }
2899
- finally {
2900
- await rm(wd, { recursive: true, force: true });
2901
- }
2902
- });
2903
- it('keeps team control-plane pumping when Ralph continue steer fails', async () => {
2904
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-control-plane-split-'));
2905
- const fakeBinDir = join(wd, 'fake-bin');
2906
- const tmuxLogPath = join(wd, 'tmux.log');
2907
- const previousRuntimeBridge = process.env.RCS_RUNTIME_BRIDGE;
2908
- try {
2909
- process.env.RCS_RUNTIME_BRIDGE = '0';
2910
- await mkdir(join(wd, '.rcs', 'state'), { recursive: true });
2911
- await mkdir(fakeBinDir, { recursive: true });
2912
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath, {
2913
- failSendKeysMatch: 'Ralph loop active continue',
2914
- }));
2915
- await initTeamState('dispatch-team', 'task', 'executor', 1, wd);
2916
- const queued = await enqueueDispatchRequest('dispatch-team', {
2917
- kind: 'inbox',
2918
- to_worker: 'worker-1',
2919
- worker_index: 1,
2920
- trigger_message: 'dispatch ping',
2921
- }, wd);
2922
- await writeFile(join(wd, '.rcs', 'state', 'ralph-state.json'), JSON.stringify({
2923
- active: true,
2924
- current_phase: 'executing',
2925
- tmux_pane_id: '%42',
2926
- }, null, 2));
2927
- await writeFile(join(wd, '.rcs', 'state', 'hud-state.json'), JSON.stringify({
2928
- last_progress_at: new Date(Date.now() - 61_000).toISOString(),
2929
- }, null, 2));
2930
- await writeFile(join(wd, '.rcs', 'state', 'notify-fallback-state.json'), JSON.stringify({
2931
- ralph_continue_steer: {
2932
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
2933
- },
2934
- }, null, 2));
2935
- const watcherScript = distScript('notify-fallback-watcher.js');
2936
- const notifyHook = distScript('notify-hook.js');
2937
- const result = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50', '--dispatch-max-per-tick', '1'], {
2938
- encoding: 'utf-8',
2939
- env: buildCleanNotifyEnv({
2940
- PATH: prependPath(fakeBinDir),
2941
- RCS_RUNTIME_BRIDGE: '0',
2942
- }),
2943
- });
2944
- assert.equal(result.status, 0, result.stderr || result.stdout);
2945
- const request = await readDispatchRequest('dispatch-team', queued.request.request_id, wd);
2946
- assert.ok(request);
2947
- const watcherStatePath = join(wd, '.rcs', 'state', 'notify-fallback-state.json');
2948
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
2949
- assert.equal(watcherState.dispatch_drain?.run_count, 1);
2950
- assert.equal(watcherState.ralph_continue_steer?.last_reason, 'send_failed');
2951
- assert.match(watcherState.ralph_continue_steer?.last_error ?? '', /send failed/i);
2952
- const tmuxLog = await readFile(tmuxLogPath, 'utf8');
2953
- assert.match(tmuxLog, /send-keys -t .* -l dispatch ping/);
2954
- const logPath = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
2955
- const logEntries = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
2956
- const drainEvent = logEntries.find((entry) => entry.type === 'dispatch_drain_tick');
2957
- assert.ok(drainEvent, 'expected dispatch_drain_tick log event');
2958
- const ralphFailureEvent = logEntries.find((entry) => (entry.type === 'ralph_continue_steer' && entry.reason === 'send_failed'));
2959
- assert.ok(ralphFailureEvent, 'expected Ralph failure to be logged without aborting team control-plane pumping');
2960
- }
2961
- finally {
2962
- if (typeof previousRuntimeBridge === 'string')
2963
- process.env.RCS_RUNTIME_BRIDGE = previousRuntimeBridge;
2964
- else
2965
- delete process.env.RCS_RUNTIME_BRIDGE;
2966
- await rm(wd, { recursive: true, force: true });
2967
- }
2968
- });
2969
- it('retypes on every retry when trigger is not in narrow input area', async () => {
2970
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-dispatch-cm-fallback-'));
2971
- const fakeBinDir = join(wd, 'fake-bin');
2972
- const tmuxLogPath = join(wd, 'tmux.log');
2973
- const captureSeqFile = join(wd, 'capture-seq.txt');
2974
- const captureCounterFile = join(wd, 'capture-seq.idx');
2975
- const previousRuntimeBridge = process.env.RCS_RUNTIME_BRIDGE;
2976
- try {
2977
- process.env.RCS_RUNTIME_BRIDGE = '0';
2978
- await mkdir(fakeBinDir, { recursive: true });
2979
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
2980
- // Shared preflight now adds one 80-line capture per tick before the
2981
- // narrow retry check. Pre-capture on retries still returns "ready"
2982
- // (no trigger) so the request is retyped on every retry.
2983
- await writeFile(captureSeqFile, [
2984
- // Run 1 (attempt 0): 1 shared preflight + 3 verify rounds × 2 captures = 7
2985
- 'ready', 'ping', 'ping', 'ping', 'ping', 'ping', 'ping',
2986
- // Run 2 (attempt 1): 1 shared preflight + 1 pre-capture + 3 verify rounds × 2 captures = 8
2987
- 'ready', 'ready', 'ping', 'ping', 'ping', 'ping', 'ping', 'ping',
2988
- // Run 3 (attempt 2): 1 shared preflight + 1 pre-capture + 3 verify rounds × 2 captures = 8
2989
- 'ready', 'ready', 'ping', 'ping', 'ping', 'ping', 'ping', 'ping',
2990
- ].join('\n'));
2991
- await initTeamState('dispatch-team', 'task', 'executor', 1, wd);
2992
- const queued = await enqueueDispatchRequest('dispatch-team', {
2993
- kind: 'inbox',
2994
- to_worker: 'worker-1',
2995
- worker_index: 1,
2996
- pane_id: '%42',
2997
- trigger_message: 'ping',
2998
- }, wd);
2999
- const watcherScript = distScript('notify-fallback-watcher.js');
3000
- const notifyHook = distScript('notify-hook.js');
3001
- const env = {
3002
- ...buildCleanNotifyEnv(),
3003
- PATH: prependPath(fakeBinDir),
3004
- RCS_TEST_CAPTURE_SEQUENCE_FILE: captureSeqFile,
3005
- RCS_TEST_CAPTURE_COUNTER_FILE: captureCounterFile,
3006
- };
3007
- for (let i = 0; i < 3; i += 1) {
3008
- const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50', '--dispatch-max-per-tick', '1'], { encoding: 'utf-8', env });
3009
- assert.equal(run.status, 0, run.stderr || run.stdout);
3010
- }
3011
- const tmuxLog = await readFile(tmuxLogPath, 'utf8');
3012
- const typeMatches = tmuxLog.match(/send-keys -t %42 -l ping/g) || [];
3013
- assert.equal(typeMatches.length, 3, 'should retype on every retry when trigger not in narrow capture (fresh + 2 retries)');
3014
- const request = await readDispatchRequest('dispatch-team', queued.request.request_id, wd);
3015
- assert.equal(request?.status, 'failed');
3016
- assert.equal(request?.last_reason, 'unconfirmed_after_max_retries');
3017
- }
3018
- finally {
3019
- if (typeof previousRuntimeBridge === 'string')
3020
- process.env.RCS_RUNTIME_BRIDGE = previousRuntimeBridge;
3021
- else
3022
- delete process.env.RCS_RUNTIME_BRIDGE;
3023
- await rm(wd, { recursive: true, force: true });
3024
- }
3025
- });
3026
- it('exits when the tracked parent pid is gone', async () => {
3027
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-parent-exit-'));
3028
- const tempHome = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-parent-home-'));
3029
- const watcherScript = distScript('notify-fallback-watcher.js');
3030
- const notifyHook = distScript('notify-hook.js');
3031
- const logPath = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
3032
- let child;
3033
- try {
3034
- const shortLivedParent = spawn(process.execPath, ['-e', 'setTimeout(() => process.exit(0), 10)'], {
3035
- stdio: 'ignore',
3036
- });
3037
- assert.ok(shortLivedParent.pid, 'expected short-lived parent pid');
3038
- const parentPid = shortLivedParent.pid;
3039
- await once(shortLivedParent, 'exit');
3040
- child = spawn(process.execPath, [
3041
- watcherScript,
3042
- '--cwd',
3043
- wd,
3044
- '--notify-script',
3045
- notifyHook,
3046
- '--poll-ms',
3047
- '50',
3048
- '--parent-pid',
3049
- String(parentPid),
3050
- '--max-lifetime-ms',
3051
- '5000',
3052
- ], {
3053
- cwd: wd,
3054
- stdio: 'ignore',
3055
- env: buildCleanNotifyEnv({ HOME: tempHome }),
3056
- });
3057
- await waitForExit(child, 4000);
3058
- assert.equal(child.exitCode, 0);
3059
- const logEntries = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
3060
- assert.ok(logEntries.some((entry) => (entry.type === 'watcher_stop' && entry.reason === 'parent_gone')));
3061
- }
3062
- finally {
3063
- if (child && isPidAlive(child.pid)) {
3064
- child.kill('SIGTERM');
3065
- await waitForExit(child, 4000).catch(() => { });
3066
- }
3067
- await rm(wd, { recursive: true, force: true });
3068
- await rm(tempHome, { recursive: true, force: true });
3069
- }
3070
- });
3071
- it('ignores stale session-scoped Ralph state when the current session identity is stale', async () => {
3072
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-stale-session-ralph-'));
3073
- const fakeBinDir = join(wd, 'fake-bin');
3074
- const tmuxLogPath = join(wd, 'tmux.log');
3075
- const stateDir = join(wd, '.rcs', 'state');
3076
- const sessionId = 'sess-stale';
3077
- const sessionStateDir = join(stateDir, 'sessions', sessionId);
3078
- const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
3079
- try {
3080
- await mkdir(sessionStateDir, { recursive: true });
3081
- await mkdir(fakeBinDir, { recursive: true });
3082
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
3083
- await writeFile(join(stateDir, 'session.json'), JSON.stringify({
3084
- session_id: sessionId,
3085
- started_at: '2026-01-01T00:00:00.000Z',
3086
- cwd: wd,
3087
- pid: Number.MAX_SAFE_INTEGER,
3088
- }, null, 2));
3089
- await writeFile(join(sessionStateDir, 'ralph-state.json'), JSON.stringify({
3090
- active: true,
3091
- current_phase: 'executing',
3092
- tmux_pane_id: '%42',
3093
- }, null, 2));
3094
- await writeFile(watcherStatePath, JSON.stringify({
3095
- ralph_continue_steer: {
3096
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
3097
- },
3098
- }, null, 2));
3099
- const watcherScript = distScript('notify-fallback-watcher.js');
3100
- const notifyHook = distScript('notify-hook.js');
3101
- const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
3102
- encoding: 'utf-8',
3103
- env: buildCleanNotifyEnv({
3104
- PATH: prependPath(fakeBinDir),
3105
- }),
3106
- });
3107
- assert.equal(run.status, 0, run.stderr || run.stdout);
3108
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
3109
- const sends = tmuxLog.match(/send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/g) || [];
3110
- assert.equal(sends.length, 0, 'stale current-session identity must block Ralph continue injection');
3111
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
3112
- assert.equal(watcherState.ralph_continue_steer?.active, false);
3113
- assert.equal(watcherState.ralph_continue_steer?.last_reason, 'stale_current_session');
3114
- }
3115
- finally {
3116
- await rm(wd, { recursive: true, force: true });
3117
- }
3118
- });
3119
- it('ignores stale root Ralph state when the current session has not started Ralph', async () => {
3120
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-stale-root-ralph-'));
3121
- const fakeBinDir = join(wd, 'fake-bin');
3122
- const tmuxLogPath = join(wd, 'tmux.log');
3123
- const stateDir = join(wd, '.rcs', 'state');
3124
- const sessionId = 'sess-fresh';
3125
- const watcherStatePath = join(stateDir, 'notify-fallback-state.json');
3126
- try {
3127
- await mkdir(join(stateDir, 'sessions', sessionId), { recursive: true });
3128
- await mkdir(fakeBinDir, { recursive: true });
3129
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
3130
- await writeSessionStart(wd, sessionId);
3131
- await writeFile(join(stateDir, 'ralph-state.json'), JSON.stringify({
3132
- active: true,
3133
- current_phase: 'executing',
3134
- tmux_pane_id: '%42',
3135
- }, null, 2));
3136
- await writeFile(watcherStatePath, JSON.stringify({
3137
- ralph_continue_steer: {
3138
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
3139
- },
3140
- }, null, 2));
3141
- const watcherScript = distScript('notify-fallback-watcher.js');
3142
- const notifyHook = distScript('notify-hook.js');
3143
- const run = spawnSync(process.execPath, [watcherScript, '--once', '--cwd', wd, '--notify-script', notifyHook, '--poll-ms', '50'], {
3144
- encoding: 'utf-8',
3145
- env: buildCleanNotifyEnv({
3146
- PATH: prependPath(fakeBinDir),
3147
- }),
3148
- });
3149
- assert.equal(run.status, 0, run.stderr || run.stdout);
3150
- const tmuxLog = await readFile(tmuxLogPath, 'utf8').catch(() => '');
3151
- const sends = tmuxLog.match(/send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/g) || [];
3152
- assert.equal(sends.length, 0, 'fresh sessions must ignore stale root Ralph state');
3153
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
3154
- assert.equal(watcherState.ralph_continue_steer?.active, false);
3155
- assert.equal(watcherState.ralph_continue_steer?.last_reason, 'blocked_by_current_session');
3156
- }
3157
- finally {
3158
- await rm(wd, { recursive: true, force: true });
3159
- }
3160
- });
3161
- it('keeps ticking for active session-scoped Ralph after parent loss, then stops once Ralph is terminal', async () => {
3162
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-parent-ralph-active-'));
3163
- const tempHome = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-parent-ralph-home-'));
3164
- const fakeBinDir = join(wd, 'fake-bin');
3165
- const tmuxLogPath = join(wd, 'tmux.log');
3166
- const stateDir = join(wd, '.rcs', 'state');
3167
- const sessionId = 'sess-active-ralph';
3168
- const sessionStateDir = join(stateDir, 'sessions', sessionId);
3169
- const ralphStatePath = join(sessionStateDir, 'ralph-state.json');
3170
- const watcherScript = distScript('notify-fallback-watcher.js');
3171
- const notifyHook = distScript('notify-hook.js');
3172
- const logPath = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
3173
- let child;
3174
- try {
3175
- await mkdir(sessionStateDir, { recursive: true });
3176
- await mkdir(fakeBinDir, { recursive: true });
3177
- await writeSessionStart(wd, sessionId);
3178
- await writeFile(ralphStatePath, JSON.stringify({
3179
- active: true,
3180
- current_phase: 'executing',
3181
- tmux_pane_id: '%42',
3182
- }, null, 2));
3183
- await writeFile(join(sessionStateDir, 'hud-state.json'), JSON.stringify({
3184
- last_progress_at: new Date(Date.now() - 61_000).toISOString(),
3185
- }, null, 2));
3186
- await writeFile(join(stateDir, 'notify-fallback-state.json'), JSON.stringify({
3187
- ralph_continue_steer: {
3188
- last_sent_at: new Date(Date.now() - 61_000).toISOString(),
3189
- },
3190
- }, null, 2));
3191
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
3192
- const shortLivedParent = spawn(process.execPath, ['-e', 'setTimeout(() => process.exit(0), 10)'], {
3193
- stdio: 'ignore',
3194
- });
3195
- assert.ok(shortLivedParent.pid, 'expected short-lived parent pid');
3196
- const parentPid = shortLivedParent.pid;
3197
- await once(shortLivedParent, 'exit');
3198
- child = spawn(process.execPath, [
3199
- watcherScript,
3200
- '--cwd',
3201
- wd,
3202
- '--notify-script',
3203
- notifyHook,
3204
- '--poll-ms',
3205
- '50',
3206
- '--parent-pid',
3207
- String(parentPid),
3208
- '--max-lifetime-ms',
3209
- '5000',
3210
- ], {
3211
- cwd: wd,
3212
- stdio: 'ignore',
3213
- env: buildCleanNotifyEnv({ HOME: tempHome, PATH: prependPath(fakeBinDir) }),
3214
- });
3215
- await waitFor(async () => {
3216
- const tmuxLog = await readFile(tmuxLogPath, 'utf-8').catch(() => '');
3217
- return /send-keys -t %42 -l Ralph loop active continue \[RCS_TMUX_INJECT\]/.test(tmuxLog);
3218
- }, 4000, 50);
3219
- assert.ok(isPidAlive(child.pid), 'expected watcher to stay alive while Ralph remains active');
3220
- await writeFile(ralphStatePath, JSON.stringify({
3221
- active: false,
3222
- current_phase: 'complete',
3223
- completed_at: new Date().toISOString(),
3224
- tmux_pane_id: '%42',
3225
- }, null, 2));
3226
- await waitForExit(child, 4000);
3227
- assert.equal(child.exitCode, 0);
3228
- const logEntries = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
3229
- assert.ok(logEntries.some((entry) => (entry.type === 'watcher_parent_guard' && entry.reason === 'parent_gone_deferred_for_active_ralph')));
3230
- assert.ok(logEntries.some((entry) => (entry.type === 'watcher_stop' && entry.reason === 'parent_gone')));
3231
- }
3232
- finally {
3233
- if (child && isPidAlive(child.pid)) {
3234
- child.kill('SIGTERM');
3235
- await waitForExit(child, 4000).catch(() => { });
3236
- }
3237
- await rm(wd, { recursive: true, force: true });
3238
- await rm(tempHome, { recursive: true, force: true });
3239
- }
3240
- });
3241
- it('stays alive after parent exit while an active team still has live worker panes', async () => {
3242
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-parent-gone-team-'));
3243
- const tempHome = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-parent-gone-team-home-'));
3244
- const fakeBinDir = join(wd, 'fake-bin');
3245
- const tmuxLogPath = join(wd, 'tmux.log');
3246
- const teamStatePath = join(wd, '.rcs', 'state', 'team-state.json');
3247
- let child;
3248
- try {
3249
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
3250
- await mkdir(join(wd, '.rcs', 'state', 'team', 'dispatch-team'), { recursive: true });
3251
- await mkdir(fakeBinDir, { recursive: true });
3252
- await writeFile(teamStatePath, JSON.stringify({
3253
- active: true,
3254
- team_name: 'dispatch-team',
3255
- current_phase: 'team-exec',
3256
- }, null, 2));
3257
- await writeFile(join(wd, '.rcs', 'state', 'team', 'dispatch-team', 'config.json'), JSON.stringify({
3258
- name: 'dispatch-team',
3259
- tmux_session: 'dispatch-team:0',
3260
- leader_pane_id: '%99',
3261
- workers: [
3262
- { name: 'worker-1', pane_id: '%42' },
3263
- ],
3264
- }, null, 2));
3265
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
3266
- const watcherScript = distScript('notify-fallback-watcher.js');
3267
- const notifyHook = distScript('notify-hook.js');
3268
- const logPath = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
3269
- const shortLivedParent = spawn(process.execPath, ['-e', 'setTimeout(() => process.exit(0), 10)'], {
3270
- stdio: 'ignore',
3271
- });
3272
- assert.ok(shortLivedParent.pid, 'expected short-lived parent pid');
3273
- const parentPid = shortLivedParent.pid;
3274
- await once(shortLivedParent, 'exit');
3275
- child = spawn(process.execPath, [
3276
- watcherScript,
3277
- '--cwd',
3278
- wd,
3279
- '--notify-script',
3280
- notifyHook,
3281
- '--poll-ms',
3282
- '50',
3283
- '--parent-pid',
3284
- String(parentPid),
3285
- '--max-lifetime-ms',
3286
- '5000',
3287
- ], {
3288
- cwd: wd,
3289
- stdio: 'ignore',
3290
- env: buildCleanNotifyEnv({ HOME: tempHome, PATH: prependPath(fakeBinDir) }),
3291
- });
3292
- await waitFor(async () => isPidAlive(child?.pid), 4000, 50);
3293
- await waitFor(async () => {
3294
- const logEntries = (await readFile(logPath, 'utf-8').catch(() => ''))
3295
- .trim()
3296
- .split('\n')
3297
- .filter(Boolean)
3298
- .map((line) => JSON.parse(line));
3299
- return logEntries.some((entry) => (entry.type === 'watcher_parent_guard' && entry.reason === 'parent_gone_deferred_for_active_team'));
3300
- }, 4000, 50);
3301
- assert.ok(isPidAlive(child.pid), 'expected watcher to stay alive while team worker panes remain active');
3302
- await writeFile(teamStatePath, JSON.stringify({
3303
- active: false,
3304
- team_name: 'dispatch-team',
3305
- current_phase: 'complete',
3306
- completed_at: new Date().toISOString(),
3307
- }, null, 2));
3308
- await waitForExit(child, 4000);
3309
- assert.equal(child.exitCode, 0);
3310
- const logEntries = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
3311
- assert.ok(logEntries.some((entry) => (entry.type === 'watcher_parent_guard' && entry.reason === 'parent_gone_deferred_for_active_team')));
3312
- assert.ok(logEntries.some((entry) => (entry.type === 'watcher_stop' && entry.reason === 'parent_gone')));
3313
- }
3314
- finally {
3315
- if (child && isPidAlive(child.pid)) {
3316
- child.kill('SIGTERM');
3317
- await waitForExit(child, 4000).catch(() => { });
3318
- }
3319
- await rm(wd, { recursive: true, force: true });
3320
- await rm(tempHome, { recursive: true, force: true });
3321
- }
3322
- });
3323
- it('stays alive after parent exit when coarse team-state is missing but canonical team is active for the current session', async () => {
3324
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-parent-gone-canonical-team-'));
3325
- const tempHome = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-parent-gone-canonical-home-'));
3326
- const fakeBinDir = join(wd, 'fake-bin');
3327
- const tmuxLogPath = join(wd, 'tmux.log');
3328
- let child;
3329
- try {
3330
- await mkdir(fakeBinDir, { recursive: true });
3331
- await writeCanonicalWatcherTeamFixture(wd, {
3332
- teamName: 'dispatch-team',
3333
- sessionId: 'sess-parent-canonical',
3334
- ownerSessionId: 'sess-parent-canonical',
3335
- });
3336
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
3337
- const watcherScript = distScript('notify-fallback-watcher.js');
3338
- const notifyHook = distScript('notify-hook.js');
3339
- const logPath = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
3340
- const shortLivedParent = spawn(process.execPath, ['-e', 'setTimeout(() => process.exit(0), 10)'], {
3341
- stdio: 'ignore',
3342
- });
3343
- assert.ok(shortLivedParent.pid, 'expected short-lived parent pid');
3344
- const parentPid = shortLivedParent.pid;
3345
- await once(shortLivedParent, 'exit');
3346
- child = spawn(process.execPath, [
3347
- watcherScript,
3348
- '--cwd',
3349
- wd,
3350
- '--notify-script',
3351
- notifyHook,
3352
- '--poll-ms',
3353
- '50',
3354
- '--parent-pid',
3355
- String(parentPid),
3356
- '--max-lifetime-ms',
3357
- '5000',
3358
- ], {
3359
- cwd: wd,
3360
- stdio: 'ignore',
3361
- env: buildCleanNotifyEnv({ HOME: tempHome, PATH: prependPath(fakeBinDir) }),
3362
- });
3363
- await waitFor(async () => isPidAlive(child?.pid), 4000, 50);
3364
- await waitFor(async () => {
3365
- const logEntries = (await readFile(logPath, 'utf-8').catch(() => ''))
3366
- .trim()
3367
- .split('\n')
3368
- .filter(Boolean)
3369
- .map((line) => JSON.parse(line));
3370
- return logEntries.some((entry) => (entry.type === 'watcher_parent_guard' && entry.reason === 'parent_gone_deferred_for_active_team'));
3371
- }, 4000, 50);
3372
- assert.ok(isPidAlive(child.pid), 'expected watcher to stay alive while canonical team panes remain active');
3373
- }
3374
- finally {
3375
- if (child && isPidAlive(child.pid)) {
3376
- child.kill('SIGTERM');
3377
- await waitForExit(child, 4000).catch(() => { });
3378
- }
3379
- await rm(wd, { recursive: true, force: true });
3380
- await rm(tempHome, { recursive: true, force: true });
3381
- }
3382
- });
3383
- it('does not defer parent-loss shutdown when canonical owner session is blank and coarse team-state is missing', async () => {
3384
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-parent-gone-ownerless-team-'));
3385
- const tempHome = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-parent-gone-ownerless-home-'));
3386
- const fakeBinDir = join(wd, 'fake-bin');
3387
- const tmuxLogPath = join(wd, 'tmux.log');
3388
- let child;
3389
- try {
3390
- await mkdir(fakeBinDir, { recursive: true });
3391
- await writeCanonicalWatcherTeamFixture(wd, {
3392
- teamName: 'dispatch-team',
3393
- sessionId: 'sess-parent-ownerless',
3394
- ownerSessionId: '',
3395
- });
3396
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
3397
- const watcherScript = distScript('notify-fallback-watcher.js');
3398
- const notifyHook = distScript('notify-hook.js');
3399
- const logPath = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
3400
- const shortLivedParent = spawn(process.execPath, ['-e', 'setTimeout(() => process.exit(0), 10)'], {
3401
- stdio: 'ignore',
3402
- });
3403
- assert.ok(shortLivedParent.pid, 'expected short-lived parent pid');
3404
- const parentPid = shortLivedParent.pid;
3405
- await once(shortLivedParent, 'exit');
3406
- child = spawn(process.execPath, [
3407
- watcherScript,
3408
- '--cwd',
3409
- wd,
3410
- '--notify-script',
3411
- notifyHook,
3412
- '--poll-ms',
3413
- '50',
3414
- '--parent-pid',
3415
- String(parentPid),
3416
- '--max-lifetime-ms',
3417
- '5000',
3418
- ], {
3419
- cwd: wd,
3420
- stdio: 'ignore',
3421
- env: buildCleanNotifyEnv({ HOME: tempHome, PATH: prependPath(fakeBinDir) }),
3422
- });
3423
- await waitForExit(child, 4000);
3424
- assert.equal(child.exitCode, 0);
3425
- const logEntries = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
3426
- assert.ok(logEntries.some((entry) => (entry.type === 'watcher_stop' && entry.reason === 'parent_gone')));
3427
- assert.ok(!logEntries.some((entry) => (entry.type === 'watcher_parent_guard' && entry.reason === 'parent_gone_deferred_for_active_team')), 'ownerless canonical team must not defer parent-loss shutdown');
3428
- }
3429
- finally {
3430
- if (child && isPidAlive(child.pid)) {
3431
- child.kill('SIGTERM');
3432
- await waitForExit(child, 4000).catch(() => { });
3433
- }
3434
- await rm(wd, { recursive: true, force: true });
3435
- await rm(tempHome, { recursive: true, force: true });
3436
- }
3437
- });
3438
- it('rejects invalid session_id before resolving session-scoped team paths', async () => {
3439
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-invalid-session-team-path-'));
3440
- const tempHome = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-invalid-session-team-home-'));
3441
- const fakeBinDir = join(wd, 'fake-bin');
3442
- const tmuxLogPath = join(wd, 'tmux.log');
3443
- const stateDir = join(wd, '.rcs', 'state');
3444
- const maliciousTeamDir = join(stateDir, 'team', 'dispatch-team');
3445
- const sessionPath = join(stateDir, 'session.json');
3446
- let child;
3447
- try {
3448
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
3449
- await mkdir(maliciousTeamDir, { recursive: true });
3450
- await mkdir(fakeBinDir, { recursive: true });
3451
- await writeSessionStart(wd, 'sess-valid');
3452
- const validSession = JSON.parse(await readFile(sessionPath, 'utf-8'));
3453
- await writeFile(sessionPath, JSON.stringify({
3454
- ...validSession,
3455
- session_id: '../team/dispatch-team',
3456
- }, null, 2));
3457
- await writeFile(join(maliciousTeamDir, 'team-state.json'), JSON.stringify({
3458
- active: true,
3459
- team_name: 'dispatch-team',
3460
- current_phase: 'team-exec',
3461
- }, null, 2));
3462
- await writeFile(join(maliciousTeamDir, 'config.json'), JSON.stringify({
3463
- name: 'dispatch-team',
3464
- tmux_session: 'dispatch-team:0',
3465
- leader_pane_id: '%99',
3466
- workers: [
3467
- { name: 'worker-1', pane_id: '%42' },
3468
- ],
3469
- }, null, 2));
3470
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
3471
- const watcherScript = distScript('notify-fallback-watcher.js');
3472
- const notifyHook = distScript('notify-hook.js');
3473
- const logPath = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
3474
- const shortLivedParent = spawn(process.execPath, ['-e', 'setTimeout(() => process.exit(0), 10)'], {
3475
- stdio: 'ignore',
3476
- });
3477
- assert.ok(shortLivedParent.pid, 'expected short-lived parent pid');
3478
- const parentPid = shortLivedParent.pid;
3479
- await once(shortLivedParent, 'exit');
3480
- child = spawn(process.execPath, [
3481
- watcherScript,
3482
- '--cwd',
3483
- wd,
3484
- '--notify-script',
3485
- notifyHook,
3486
- '--poll-ms',
3487
- '50',
3488
- '--parent-pid',
3489
- String(parentPid),
3490
- '--max-lifetime-ms',
3491
- '5000',
3492
- ], {
3493
- cwd: wd,
3494
- stdio: 'ignore',
3495
- env: buildCleanNotifyEnv({ HOME: tempHome, PATH: prependPath(fakeBinDir) }),
3496
- });
3497
- await waitForExit(child, 4000);
3498
- assert.equal(child.exitCode, 0);
3499
- const logEntries = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
3500
- assert.ok(logEntries.some((entry) => (entry.type === 'watcher_stop' && entry.reason === 'parent_gone')));
3501
- assert.ok(!logEntries.some((entry) => (entry.type === 'watcher_parent_guard' && entry.reason === 'parent_gone_deferred_for_active_team')), 'invalid session_id must not be used for session-scoped team path resolution');
3502
- }
3503
- finally {
3504
- if (child && isPidAlive(child.pid)) {
3505
- child.kill('SIGTERM');
3506
- await waitForExit(child, 4000).catch(() => { });
3507
- }
3508
- await rm(wd, { recursive: true, force: true });
3509
- await rm(tempHome, { recursive: true, force: true });
3510
- }
3511
- });
3512
- it('rejects invalid team_name before resolving watcher team paths', async () => {
3513
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-invalid-team-path-'));
3514
- const tempHome = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-invalid-team-home-'));
3515
- const fakeBinDir = join(wd, 'fake-bin');
3516
- const tmuxLogPath = join(wd, 'tmux.log');
3517
- const stateDir = join(wd, '.rcs', 'state');
3518
- const validTeamDir = join(stateDir, 'team', 'dispatch-team');
3519
- let child;
3520
- try {
3521
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
3522
- await mkdir(validTeamDir, { recursive: true });
3523
- await mkdir(fakeBinDir, { recursive: true });
3524
- await writeFile(join(stateDir, 'team-state.json'), JSON.stringify({
3525
- active: true,
3526
- team_name: '../team/dispatch-team',
3527
- current_phase: 'team-exec',
3528
- }, null, 2));
3529
- await writeFile(join(validTeamDir, 'config.json'), JSON.stringify({
3530
- name: 'dispatch-team',
3531
- tmux_session: 'dispatch-team:0',
3532
- leader_pane_id: '%99',
3533
- workers: [
3534
- { name: 'worker-1', pane_id: '%42' },
3535
- ],
3536
- }, null, 2));
3537
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
3538
- const watcherScript = distScript('notify-fallback-watcher.js');
3539
- const notifyHook = distScript('notify-hook.js');
3540
- const logPath = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
3541
- const shortLivedParent = spawn(process.execPath, ['-e', 'setTimeout(() => process.exit(0), 10)'], {
3542
- stdio: 'ignore',
3543
- });
3544
- assert.ok(shortLivedParent.pid, 'expected short-lived parent pid');
3545
- const parentPid = shortLivedParent.pid;
3546
- await once(shortLivedParent, 'exit');
3547
- child = spawn(process.execPath, [
3548
- watcherScript,
3549
- '--cwd',
3550
- wd,
3551
- '--notify-script',
3552
- notifyHook,
3553
- '--poll-ms',
3554
- '50',
3555
- '--parent-pid',
3556
- String(parentPid),
3557
- '--max-lifetime-ms',
3558
- '5000',
3559
- ], {
3560
- cwd: wd,
3561
- stdio: 'ignore',
3562
- env: buildCleanNotifyEnv({ HOME: tempHome, PATH: prependPath(fakeBinDir) }),
3563
- });
3564
- await waitForExit(child, 4000);
3565
- assert.equal(child.exitCode, 0);
3566
- const logEntries = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
3567
- assert.ok(logEntries.some((entry) => (entry.type === 'watcher_stop' && entry.reason === 'parent_gone')));
3568
- assert.ok(!logEntries.some((entry) => (entry.type === 'watcher_parent_guard' && entry.reason === 'parent_gone_deferred_for_active_team')), 'invalid team_name must not be used for watcher team path resolution');
3569
- }
3570
- finally {
3571
- if (child && isPidAlive(child.pid)) {
3572
- child.kill('SIGTERM');
3573
- await waitForExit(child, 4000).catch(() => { });
3574
- }
3575
- await rm(wd, { recursive: true, force: true });
3576
- await rm(tempHome, { recursive: true, force: true });
3577
- }
3578
- });
3579
- it('does not defer parent-loss shutdown for a team that is already terminal in phase.json', async () => {
3580
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-parent-gone-terminal-team-'));
3581
- const tempHome = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-parent-gone-terminal-team-home-'));
3582
- const fakeBinDir = join(wd, 'fake-bin');
3583
- const tmuxLogPath = join(wd, 'tmux.log');
3584
- let child;
3585
- try {
3586
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
3587
- await mkdir(join(wd, '.rcs', 'state', 'team', 'dispatch-team'), { recursive: true });
3588
- await mkdir(fakeBinDir, { recursive: true });
3589
- await writeFile(join(wd, '.rcs', 'state', 'team-state.json'), JSON.stringify({
3590
- active: true,
3591
- team_name: 'dispatch-team',
3592
- current_phase: 'team-exec',
3593
- }, null, 2));
3594
- await writeFile(join(wd, '.rcs', 'state', 'team', 'dispatch-team', 'phase.json'), JSON.stringify({
3595
- current_phase: 'complete',
3596
- }, null, 2));
3597
- await writeFile(join(wd, '.rcs', 'state', 'team', 'dispatch-team', 'config.json'), JSON.stringify({
3598
- name: 'dispatch-team',
3599
- tmux_session: 'dispatch-team:0',
3600
- leader_pane_id: '%99',
3601
- workers: [
3602
- { name: 'worker-1', pane_id: '%42' },
3603
- ],
3604
- }, null, 2));
3605
- await writeFakeTmuxExecutable(fakeBinDir, buildFakeTmux(tmuxLogPath));
3606
- const watcherScript = distScript('notify-fallback-watcher.js');
3607
- const notifyHook = distScript('notify-hook.js');
3608
- const logPath = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
3609
- const shortLivedParent = spawn(process.execPath, ['-e', 'setTimeout(() => process.exit(0), 10)'], {
3610
- stdio: 'ignore',
3611
- });
3612
- assert.ok(shortLivedParent.pid, 'expected short-lived parent pid');
3613
- const parentPid = shortLivedParent.pid;
3614
- await once(shortLivedParent, 'exit');
3615
- child = spawn(process.execPath, [
3616
- watcherScript,
3617
- '--cwd',
3618
- wd,
3619
- '--notify-script',
3620
- notifyHook,
3621
- '--poll-ms',
3622
- '50',
3623
- '--parent-pid',
3624
- String(parentPid),
3625
- '--max-lifetime-ms',
3626
- '5000',
3627
- ], {
3628
- cwd: wd,
3629
- stdio: 'ignore',
3630
- env: buildCleanNotifyEnv({ HOME: tempHome, PATH: prependPath(fakeBinDir) }),
3631
- });
3632
- await waitForExit(child, 4000);
3633
- assert.equal(child.exitCode, 0);
3634
- const logEntries = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
3635
- assert.equal(logEntries.some((entry) => (entry.type === 'watcher_parent_guard' && entry.reason === 'parent_gone_deferred_for_active_team')), false);
3636
- assert.ok(logEntries.some((entry) => (entry.type === 'watcher_stop' && entry.reason === 'parent_gone')));
3637
- }
3638
- finally {
3639
- if (child && isPidAlive(child.pid)) {
3640
- child.kill('SIGTERM');
3641
- await waitForExit(child, 4000).catch(() => { });
3642
- }
3643
- await rm(wd, { recursive: true, force: true });
3644
- await rm(tempHome, { recursive: true, force: true });
3645
- }
3646
- });
3647
- it('replaces a stale watcher from the per-cwd pid file', async () => {
3648
- const replacementTimeoutMs = 20000; // c8-instrumented Node20 full runs can delay watcher handoff well beyond 8s.
3649
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-stale-pid-'));
3650
- const tempHome = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-stale-home-'));
3651
- const watcherScript = distScript('notify-fallback-watcher.js');
3652
- const notifyHook = distScript('notify-hook.js');
3653
- const pidPath = join(wd, '.rcs', 'state', 'notify-fallback.pid');
3654
- let first;
3655
- let second;
3656
- try {
3657
- first = spawn(process.execPath, [
3658
- watcherScript,
3659
- '--cwd',
3660
- wd,
3661
- '--notify-script',
3662
- notifyHook,
3663
- '--poll-ms',
3664
- '50',
3665
- '--parent-pid',
3666
- String(process.pid),
3667
- '--max-lifetime-ms',
3668
- '5000',
3669
- ], {
3670
- cwd: wd,
3671
- stdio: 'ignore',
3672
- env: buildCleanNotifyEnv({ HOME: tempHome }),
3673
- });
3674
- assert.ok(first.pid, 'expected first watcher pid');
3675
- await waitFor(async () => {
3676
- try {
3677
- const pidFile = JSON.parse(await readFile(pidPath, 'utf-8'));
3678
- assert.match(pidFile.owner_token ?? '', /^\d+-\d+-/, 'pid file should include an ownership token');
3679
- return pidFile.pid === first?.pid;
3680
- }
3681
- catch {
3682
- return false;
3683
- }
3684
- }, replacementTimeoutMs, 50);
3685
- second = spawn(process.execPath, [
3686
- watcherScript,
3687
- '--cwd',
3688
- wd,
3689
- '--notify-script',
3690
- notifyHook,
3691
- '--poll-ms',
3692
- '50',
3693
- '--parent-pid',
3694
- String(process.pid),
3695
- '--max-lifetime-ms',
3696
- '5000',
3697
- ], {
3698
- cwd: wd,
3699
- stdio: 'ignore',
3700
- env: buildCleanNotifyEnv({ HOME: tempHome }),
3701
- });
3702
- assert.ok(second.pid, 'expected second watcher pid');
3703
- await waitForExit(first, replacementTimeoutMs);
3704
- assert.equal(first.exitCode, 0);
3705
- await waitFor(async () => {
3706
- try {
3707
- const pidFile = JSON.parse(await readFile(pidPath, 'utf-8'));
3708
- assert.match(pidFile.owner_token ?? '', /^\d+-\d+-/, 'replacement pid file should keep ownership metadata');
3709
- return pidFile.pid === second?.pid;
3710
- }
3711
- catch {
3712
- return false;
3713
- }
3714
- }, replacementTimeoutMs, 50);
3715
- assert.ok(isPidAlive(second.pid), 'expected replacement watcher to remain alive');
3716
- }
3717
- finally {
3718
- if (second && isPidAlive(second.pid)) {
3719
- second.kill('SIGTERM');
3720
- await waitForExit(second, replacementTimeoutMs).catch(() => { });
3721
- }
3722
- if (first && isPidAlive(first.pid)) {
3723
- first.kill('SIGTERM');
3724
- await waitForExit(first, replacementTimeoutMs).catch(() => { });
3725
- }
3726
- await rm(wd, { recursive: true, force: true });
3727
- await rm(tempHome, { recursive: true, force: true });
3728
- }
3729
- });
3730
- it('backs off idle polling and resets to the base cadence after fresh rollout activity', async () => {
3731
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-idle-backoff-'));
3732
- const tempHome = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-idle-backoff-home-'));
3733
- const sid = randomUUID();
3734
- const sessionDir = todaySessionDir(tempHome);
3735
- const rolloutPath = join(sessionDir, `rollout-test-fallback-idle-backoff-${sid}.jsonl`);
3736
- const watcherScript = distScript('notify-fallback-watcher.js');
3737
- const notifyHook = distScript('notify-hook.js');
3738
- const watcherStatePath = join(wd, '.rcs', 'state', 'notify-fallback-state.json');
3739
- const turnLogPath = join(wd, '.rcs', 'logs', `turns-${new Date().toISOString().split('T')[0]}.jsonl`);
3740
- let child;
3741
- try {
3742
- await mkdir(join(wd, '.rcs', 'logs'), { recursive: true });
3743
- await mkdir(join(wd, '.rcs', 'state'), { recursive: true });
3744
- await mkdir(sessionDir, { recursive: true });
3745
- await writeFile(rolloutPath, `${JSON.stringify({
3746
- timestamp: new Date().toISOString(),
3747
- type: 'session_meta',
3748
- payload: { id: `thread-${sid}`, cwd: wd },
3749
- })}
3750
- `);
3751
- child = spawn(process.execPath, [
3752
- watcherScript,
3753
- '--cwd',
3754
- wd,
3755
- '--notify-script',
3756
- notifyHook,
3757
- '--poll-ms',
3758
- '50',
3759
- '--idle-max-poll-ms',
3760
- '200',
3761
- '--parent-pid',
3762
- String(process.pid),
3763
- '--max-lifetime-ms',
3764
- '5000',
3765
- ], {
3766
- cwd: wd,
3767
- stdio: 'ignore',
3768
- env: buildCleanNotifyEnv({ HOME: tempHome, RCS_NOTIFY_FALLBACK_IDLE_MAX_POLL_MS: '200' }),
3769
- });
3770
- await waitFor(async () => {
3771
- try {
3772
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
3773
- return watcherState.adaptive_poll?.current_ms === 200 && watcherState.adaptive_poll?.idle_streak >= 2;
3774
- }
3775
- catch {
3776
- return false;
3777
- }
3778
- }, 4000, 50);
3779
- const freshTurnId = `turn-fresh-${sid}`;
3780
- await appendLine(rolloutPath, {
3781
- timestamp: new Date().toISOString(),
3782
- type: 'event_msg',
3783
- payload: {
3784
- type: 'task_complete',
3785
- turn_id: freshTurnId,
3786
- last_agent_message: 'fresh message after idle backoff',
3787
- },
3788
- });
3789
- await waitFor(async () => {
3790
- const turnLines = await readLines(turnLogPath);
3791
- if (!turnLines.some((line) => line.includes(freshTurnId)))
3792
- return false;
3793
- const watcherState = JSON.parse(await readFile(watcherStatePath, 'utf-8'));
3794
- return watcherState.adaptive_poll?.current_ms === 50
3795
- && watcherState.adaptive_poll?.idle_streak === 0
3796
- && watcherState.adaptive_poll?.last_activity_reason === 'rollout_event';
3797
- }, 4000, 50);
3798
- }
3799
- finally {
3800
- if (child && isPidAlive(child.pid)) {
3801
- child.kill('SIGTERM');
3802
- await waitForExit(child, 4000).catch(() => { });
3803
- }
3804
- await rm(wd, { recursive: true, force: true });
3805
- await rm(tempHome, { recursive: true, force: true });
3806
- await rm(rolloutPath, { force: true });
3807
- }
3808
- });
3809
- it('exits after the configured max lifetime', async () => {
3810
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-max-life-'));
3811
- const tempHome = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-max-home-'));
3812
- const watcherScript = distScript('notify-fallback-watcher.js');
3813
- const notifyHook = distScript('notify-hook.js');
3814
- const logPath = join(wd, '.rcs', 'logs', `notify-fallback-${new Date().toISOString().split('T')[0]}.jsonl`);
3815
- let child;
3816
- try {
3817
- child = spawn(process.execPath, [
3818
- watcherScript,
3819
- '--cwd',
3820
- wd,
3821
- '--notify-script',
3822
- notifyHook,
3823
- '--poll-ms',
3824
- '50',
3825
- '--parent-pid',
3826
- String(process.pid),
3827
- '--max-lifetime-ms',
3828
- '200',
3829
- ], {
3830
- cwd: wd,
3831
- stdio: 'ignore',
3832
- env: buildCleanNotifyEnv({ HOME: tempHome }),
3833
- });
3834
- await waitForExit(child, 4000);
3835
- assert.equal(child.exitCode, 0);
3836
- const logEntries = (await readFile(logPath, 'utf-8')).trim().split('\n').filter(Boolean).map((line) => JSON.parse(line));
3837
- assert.ok(logEntries.some((entry) => (entry.type === 'watcher_stop' && entry.reason === 'max_lifetime_exceeded')));
3838
- }
3839
- finally {
3840
- if (child && isPidAlive(child.pid)) {
3841
- child.kill('SIGTERM');
3842
- await waitForExit(child, 4000).catch(() => { });
3843
- }
3844
- await rm(wd, { recursive: true, force: true });
3845
- await rm(tempHome, { recursive: true, force: true });
3846
- }
3847
- });
3848
- it('keeps the detached helper alive after the hidden bootstrap exits', async () => {
3849
- const wd = await mkdtemp(join(safeTestTmpdir(), 'rcs-fallback-bootstrap-survival-'));
3850
- const readyPath = join(wd, 'helper-ready.json');
3851
- const helperScriptPath = join(wd, 'helper-survival.js');
3852
- try {
3853
- await writeFile(helperScriptPath, `
3854
- const fs = require('node:fs');
3855
- const readyPath = process.argv[2];
3856
- fs.writeFileSync(readyPath, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }));
3857
- setInterval(() => {}, 1000);
3858
- `);
3859
- const bootstrap = spawnSync(process.execPath, [
3860
- '-e',
3861
- buildWindowsMsysBackgroundHelperBootstrapScript([helperScriptPath, readyPath], wd),
3862
- ], {
3863
- cwd: wd,
3864
- encoding: 'utf-8',
3865
- stdio: ['ignore', 'pipe', 'pipe'],
3866
- windowsHide: true,
3867
- });
3868
- assert.equal(bootstrap.status, 0, bootstrap.stderr || bootstrap.stdout);
3869
- const helperPid = Number.parseInt((bootstrap.stdout || '').trim(), 10);
3870
- assert.ok(Number.isFinite(helperPid) && helperPid > 0, 'expected detached helper pid from bootstrap');
3871
- await waitFor(async () => {
3872
- try {
3873
- const ready = JSON.parse(await readFile(readyPath, 'utf-8'));
3874
- return ready.pid === helperPid;
3875
- }
3876
- catch {
3877
- return false;
3878
- }
3879
- }, 4000, 50);
3880
- assert.ok(isPidAlive(helperPid), 'expected detached helper to survive after bootstrap exit');
3881
- process.kill(helperPid, 'SIGTERM');
3882
- await waitFor(async () => !isPidAlive(helperPid), 4000, 50);
3883
- await sleep(300);
3884
- }
3885
- finally {
3886
- for (let attempt = 0; attempt < 6; attempt += 1) {
3887
- try {
3888
- await rm(wd, { recursive: true, force: true });
3889
- break;
3890
- }
3891
- catch {
3892
- await sleep(150 * (attempt + 1));
3893
- }
3894
- }
3895
- }
3896
- });
3897
- });
3898
- //# sourceMappingURL=notify-fallback-watcher.test.js.map