@jstn-sdk/rcs 0.1.0 → 0.1.1

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 (1736) hide show
  1. package/README.md +142 -102
  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.js +15 -15
  20. package/dist/cli/setup.d.ts.map +1 -1
  21. package/dist/cli/setup.js +2 -3
  22. package/dist/cli/setup.js.map +1 -1
  23. package/dist/cli/star-prompt.js +2 -2
  24. package/dist/cli/star-prompt.js.map +1 -1
  25. package/dist/cli/state.js +1 -1
  26. package/dist/cli/team.d.ts.map +1 -1
  27. package/dist/cli/team.js +3 -2
  28. package/dist/cli/team.js.map +1 -1
  29. package/dist/cli/tmux-hook.d.ts.map +1 -1
  30. package/dist/cli/tmux-hook.js +9 -1
  31. package/dist/cli/tmux-hook.js.map +1 -1
  32. package/dist/config/generator.d.ts +1 -1
  33. package/dist/config/generator.d.ts.map +1 -1
  34. package/dist/config/generator.js +1 -1
  35. package/dist/config/generator.js.map +1 -1
  36. package/dist/forge/contract.d.ts +17 -0
  37. package/dist/{ralph → forge}/contract.d.ts.map +1 -1
  38. package/dist/{ralph → forge}/contract.js +16 -16
  39. package/dist/{ralph → forge}/contract.js.map +1 -1
  40. package/dist/{ralph → forge}/persistence.d.ts +5 -5
  41. package/dist/{ralph → forge}/persistence.d.ts.map +1 -1
  42. package/dist/{ralph → forge}/persistence.js +7 -6
  43. package/dist/forge/persistence.js.map +1 -0
  44. package/dist/hooks/agents-overlay.d.ts +1 -1
  45. package/dist/hooks/agents-overlay.d.ts.map +1 -1
  46. package/dist/hooks/agents-overlay.js +37 -31
  47. package/dist/hooks/agents-overlay.js.map +1 -1
  48. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  49. package/dist/hooks/extensibility/dispatcher.js +82 -14
  50. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  51. package/dist/hooks/keyword-detector.d.ts +8 -8
  52. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  53. package/dist/hooks/keyword-detector.js +94 -64
  54. package/dist/hooks/keyword-detector.js.map +1 -1
  55. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  56. package/dist/hooks/keyword-registry.js +9 -11
  57. package/dist/hooks/keyword-registry.js.map +1 -1
  58. package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
  59. package/dist/hooks/prompt-guidance-contract.js +10 -21
  60. package/dist/hooks/prompt-guidance-contract.js.map +1 -1
  61. package/dist/hooks/task-size-detector.js +2 -2
  62. package/dist/hooks/task-size-detector.js.map +1 -1
  63. package/dist/hooks/triage-state.d.ts +1 -1
  64. package/dist/hooks/triage-state.js +1 -1
  65. package/dist/hud/colors.d.ts +2 -2
  66. package/dist/hud/colors.js +2 -2
  67. package/dist/hud/render.js +21 -21
  68. package/dist/hud/render.js.map +1 -1
  69. package/dist/hud/state.d.ts +3 -3
  70. package/dist/hud/state.d.ts.map +1 -1
  71. package/dist/hud/state.js +18 -15
  72. package/dist/hud/state.js.map +1 -1
  73. package/dist/hud/types.d.ts +6 -6
  74. package/dist/hud/types.d.ts.map +1 -1
  75. package/dist/mcp/bootstrap.d.ts.map +1 -1
  76. package/dist/mcp/bootstrap.js +36 -2
  77. package/dist/mcp/bootstrap.js.map +1 -1
  78. package/dist/mcp/state-paths.d.ts +1 -0
  79. package/dist/mcp/state-paths.d.ts.map +1 -1
  80. package/dist/mcp/state-paths.js +4 -1
  81. package/dist/mcp/state-paths.js.map +1 -1
  82. package/dist/mcp/state-server.d.ts +4 -4
  83. package/dist/mcp/state-server.js +2 -2
  84. package/dist/mcp/state-server.js.map +1 -1
  85. package/dist/modes/base.d.ts +2 -2
  86. package/dist/modes/base.d.ts.map +1 -1
  87. package/dist/modes/base.js +29 -26
  88. package/dist/modes/base.js.map +1 -1
  89. package/dist/notifications/reply-listener.d.ts.map +1 -1
  90. package/dist/notifications/reply-listener.js +7 -1
  91. package/dist/notifications/reply-listener.js.map +1 -1
  92. package/dist/notifications/tmux.d.ts.map +1 -1
  93. package/dist/notifications/tmux.js +39 -6
  94. package/dist/notifications/tmux.js.map +1 -1
  95. package/dist/pipeline/index.d.ts +7 -6
  96. package/dist/pipeline/index.d.ts.map +1 -1
  97. package/dist/pipeline/index.js +5 -4
  98. package/dist/pipeline/index.js.map +1 -1
  99. package/dist/pipeline/orchestrator.d.ts +5 -5
  100. package/dist/pipeline/orchestrator.js +25 -25
  101. package/dist/pipeline/orchestrator.js.map +1 -1
  102. package/dist/pipeline/stages/blueprint.d.ts +25 -0
  103. package/dist/pipeline/stages/blueprint.d.ts.map +1 -0
  104. package/dist/pipeline/stages/{ralplan.js → blueprint.js} +16 -16
  105. package/dist/pipeline/stages/blueprint.js.map +1 -0
  106. package/dist/pipeline/stages/code-review.d.ts +2 -2
  107. package/dist/pipeline/stages/code-review.js +6 -6
  108. package/dist/pipeline/stages/code-review.js.map +1 -1
  109. package/dist/pipeline/stages/forge-verify.d.ts +50 -0
  110. package/dist/pipeline/stages/forge-verify.d.ts.map +1 -0
  111. package/dist/pipeline/stages/{ralph-verify.js → forge-verify.js} +21 -24
  112. package/dist/pipeline/stages/forge-verify.js.map +1 -0
  113. package/dist/pipeline/stages/team-exec.d.ts +1 -1
  114. package/dist/pipeline/stages/team-exec.js +19 -19
  115. package/dist/pipeline/stages/team-exec.js.map +1 -1
  116. package/dist/pipeline/types.d.ts +12 -12
  117. package/dist/pipeline/types.d.ts.map +1 -1
  118. package/dist/pipeline/types.js +1 -1
  119. package/dist/planning/artifacts.d.ts +3 -4
  120. package/dist/planning/artifacts.d.ts.map +1 -1
  121. package/dist/planning/artifacts.js +2 -3
  122. package/dist/planning/artifacts.js.map +1 -1
  123. package/dist/question/policy.js +1 -1
  124. package/dist/runtime/bridge.d.ts.map +1 -1
  125. package/dist/runtime/bridge.js +70 -13
  126. package/dist/runtime/bridge.js.map +1 -1
  127. package/dist/scripts/codex-native-hook.js +30 -30
  128. package/dist/scripts/codex-native-hook.js.map +1 -1
  129. package/dist/scripts/eval/eval-cross-server-party-flow.d.ts +3 -0
  130. package/dist/scripts/eval/eval-cross-server-party-flow.d.ts.map +1 -0
  131. package/dist/scripts/eval/eval-cross-server-party-flow.js +12 -0
  132. package/dist/scripts/eval/eval-cross-server-party-flow.js.map +1 -0
  133. package/dist/scripts/eval/eval-gui-onboarding-clarity.d.ts +3 -0
  134. package/dist/scripts/eval/eval-gui-onboarding-clarity.d.ts.map +1 -0
  135. package/dist/scripts/eval/eval-gui-onboarding-clarity.js +17 -0
  136. package/dist/scripts/eval/eval-gui-onboarding-clarity.js.map +1 -0
  137. package/dist/scripts/eval/eval-liveops-reward-loop-balance.d.ts +3 -0
  138. package/dist/scripts/eval/eval-liveops-reward-loop-balance.d.ts.map +1 -0
  139. package/dist/scripts/eval/eval-liveops-reward-loop-balance.js +12 -0
  140. package/dist/scripts/eval/eval-liveops-reward-loop-balance.js.map +1 -0
  141. package/dist/scripts/eval/eval-profile-datastore-recovery.d.ts +3 -0
  142. package/dist/scripts/eval/eval-profile-datastore-recovery.d.ts.map +1 -0
  143. package/dist/scripts/eval/eval-profile-datastore-recovery.js +17 -0
  144. package/dist/scripts/eval/eval-profile-datastore-recovery.js.map +1 -0
  145. package/dist/scripts/eval/eval-remote-contract-hardening.d.ts +3 -0
  146. package/dist/scripts/eval/eval-remote-contract-hardening.d.ts.map +1 -0
  147. package/dist/scripts/eval/eval-remote-contract-hardening.js +17 -0
  148. package/dist/scripts/eval/eval-remote-contract-hardening.js.map +1 -0
  149. package/dist/scripts/notify-fallback-watcher.js +140 -139
  150. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  151. package/dist/scripts/notify-hook/forge-session-resume.d.ts +23 -0
  152. package/dist/scripts/notify-hook/{ralph-session-resume.d.ts.map → forge-session-resume.d.ts.map} +1 -1
  153. package/dist/scripts/notify-hook/{ralph-session-resume.js → forge-session-resume.js} +37 -36
  154. package/dist/scripts/notify-hook/{ralph-session-resume.js.map → forge-session-resume.js.map} +1 -1
  155. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  156. package/dist/scripts/notify-hook/team-dispatch.js +34 -4
  157. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  158. package/dist/scripts/notify-hook/visual-verdict.js +3 -3
  159. package/dist/scripts/notify-hook.js +9 -9
  160. package/dist/scripts/run-test-files.js +1 -1
  161. package/dist/scripts/run-test-files.js.map +1 -1
  162. package/dist/scripts/surface-taxonomy.d.ts +23 -0
  163. package/dist/scripts/surface-taxonomy.d.ts.map +1 -0
  164. package/dist/scripts/surface-taxonomy.js +271 -0
  165. package/dist/scripts/surface-taxonomy.js.map +1 -0
  166. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  167. package/dist/scripts/sync-plugin-mirror.js +5 -4
  168. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  169. package/dist/scripts/tmux-hook-engine.d.ts +1 -1
  170. package/dist/scripts/tmux-hook-engine.d.ts.map +1 -1
  171. package/dist/scripts/tmux-hook-engine.js +29 -20
  172. package/dist/scripts/tmux-hook-engine.js.map +1 -1
  173. package/dist/state/operations.d.ts +1 -1
  174. package/dist/state/operations.d.ts.map +1 -1
  175. package/dist/state/operations.js +18 -18
  176. package/dist/state/operations.js.map +1 -1
  177. package/dist/state/skill-active.d.ts +13 -1
  178. package/dist/state/skill-active.d.ts.map +1 -1
  179. package/dist/state/skill-active.js +38 -17
  180. package/dist/state/skill-active.js.map +1 -1
  181. package/dist/state/workflow-transition.d.ts +6 -5
  182. package/dist/state/workflow-transition.d.ts.map +1 -1
  183. package/dist/state/workflow-transition.js +27 -15
  184. package/dist/state/workflow-transition.js.map +1 -1
  185. package/dist/team/contracts.d.ts +1 -1
  186. package/dist/team/contracts.js +2 -2
  187. package/dist/team/followup-planner.d.ts +2 -2
  188. package/dist/team/followup-planner.d.ts.map +1 -1
  189. package/dist/team/followup-planner.js +16 -14
  190. package/dist/team/followup-planner.js.map +1 -1
  191. package/dist/team/idle-nudge.d.ts.map +1 -1
  192. package/dist/team/idle-nudge.js +3 -2
  193. package/dist/team/idle-nudge.js.map +1 -1
  194. package/dist/team/leader-activity.js +1 -1
  195. package/dist/team/model-contract.d.ts.map +1 -1
  196. package/dist/team/model-contract.js +4 -1
  197. package/dist/team/model-contract.js.map +1 -1
  198. package/dist/team/orchestrator.js +4 -4
  199. package/dist/team/orchestrator.js.map +1 -1
  200. package/dist/team/role-router.js +3 -3
  201. package/dist/team/role-router.js.map +1 -1
  202. package/dist/team/state/dispatch.d.ts.map +1 -1
  203. package/dist/team/state/dispatch.js +4 -1
  204. package/dist/team/state/dispatch.js.map +1 -1
  205. package/dist/team/tmux-session.d.ts +4 -0
  206. package/dist/team/tmux-session.d.ts.map +1 -1
  207. package/dist/team/tmux-session.js +42 -9
  208. package/dist/team/tmux-session.js.map +1 -1
  209. package/dist/team/worktree.d.ts +1 -1
  210. package/dist/utils/platform-command.d.ts.map +1 -1
  211. package/dist/utils/platform-command.js +9 -0
  212. package/dist/utils/platform-command.js.map +1 -1
  213. package/dist/verification/verifier.d.ts +1 -1
  214. package/dist/verification/verifier.js +2 -2
  215. package/docs/STATE_MODEL.md +24 -24
  216. package/docs/agents.html +8 -16
  217. package/docs/archive/README.md +15 -0
  218. package/docs/{prompt-migration-changelog.md → archive/prompt-migration-changelog.md} +0 -11
  219. package/docs/{release-body-0.9.0.md → archive/release-body-0.9.0.md} +6 -24
  220. package/docs/{release-body-0.9.1.md → archive/release-body-0.9.1.md} +3 -3
  221. package/docs/codex-native-hooks.md +4 -4
  222. package/docs/contracts/forge-cancel-contract.md +20 -0
  223. package/docs/contracts/forge-state-contract.md +52 -0
  224. package/docs/contracts/multi-state-transition-contract.md +5 -5
  225. package/docs/contracts/multi-state-transition-review.md +3 -3
  226. package/docs/contracts/repo-aware-team-dag-decomposition.md +1 -1
  227. package/docs/contracts/rust-runtime-thin-adapter-contract.md +1 -1
  228. package/docs/contracts/team-startup-dispatch-latency.md +1 -1
  229. package/docs/getting-started.html +6 -1
  230. package/docs/guidance-schema.md +6 -3
  231. package/docs/index.html +55 -4
  232. package/docs/integrations.html +4 -3
  233. package/docs/issues/team-forge-followup-team.md +38 -0
  234. package/docs/openclaw-integration.md +2 -2
  235. package/docs/prompt-guidance-contract.md +11 -11
  236. package/docs/prs/{dev-deprecate-team-ralph.md → dev-deprecate-team-forge.md} +27 -27
  237. package/docs/prs/{dev-fix-ralph-live-pane-invariant.md → dev-fix-forge-live-pane-invariant.md} +7 -7
  238. package/docs/prs/{dev-team-ralph-workflow-positioning.md → dev-team-forge-workflow-positioning.md} +7 -7
  239. package/docs/qa/forge-persistence-gate.md +20 -0
  240. package/docs/qa/rust-runtime-thin-adapter-gate.md +31 -40
  241. package/docs/readme/README.de.md +13 -0
  242. package/docs/readme/README.el.md +13 -0
  243. package/docs/readme/README.es.md +13 -0
  244. package/docs/readme/README.fr.md +13 -0
  245. package/docs/readme/README.it.md +13 -0
  246. package/docs/readme/README.ja.md +13 -0
  247. package/docs/readme/README.ko.md +13 -0
  248. package/docs/readme/README.pl.md +13 -0
  249. package/docs/readme/README.pt.md +13 -0
  250. package/docs/readme/README.ru.md +13 -0
  251. package/docs/readme/README.tr.md +13 -0
  252. package/docs/readme/README.uk.md +13 -0
  253. package/docs/readme/README.vi.md +13 -0
  254. package/docs/readme/README.zh-TW.md +13 -0
  255. package/docs/readme/README.zh.md +13 -0
  256. package/docs/readme/rcs-cover.svg +75 -0
  257. package/docs/reference/canonical-vocabulary.md +106 -0
  258. package/docs/reference/forge-parity-matrix.md +26 -0
  259. package/docs/reference/forge-upstream-baseline.md +32 -0
  260. package/docs/reference/rcs-config-schema-routing.md +5 -5
  261. package/docs/reference/roblox-pre-action-protocol.md +4 -0
  262. package/docs/reference/roblox-taxonomy-migration-plan.md +46 -0
  263. package/docs/reference/roblox-workspace-standard.md +83 -0
  264. package/docs/reference/robloxstudio-mcp-compatibility.md +117 -0
  265. package/docs/reference/semantic-design-system.md +110 -0
  266. package/docs/reference/surface-map.md +131 -0
  267. package/docs/reference/team-allocation-rebalance-policy.md +1 -1
  268. package/docs/release-notes-v0.1.0.md +1 -1
  269. package/docs/release-notes-v0.1.1.md +49 -0
  270. package/docs/reports/open-prs-dev-readiness-2026-04-09.md +2 -2
  271. package/docs/shared/agent-tiers.md +3 -3
  272. package/docs/skills.html +10 -12
  273. package/docs/troubleshooting.md +1 -1
  274. package/package.json +20 -13
  275. package/plugins/roblox-ai-os-creator-skills/.codex-plugin/plugin.json +1 -1
  276. package/plugins/roblox-ai-os-creator-skills/docs/reference/roblox-pre-action-protocol.md +4 -0
  277. package/plugins/roblox-ai-os-creator-skills/skills/ai-slop-cleaner/SKILL.md +14 -7
  278. package/plugins/roblox-ai-os-creator-skills/skills/analyze/SKILL.md +9 -2
  279. package/plugins/roblox-ai-os-creator-skills/skills/ask-claude/SKILL.md +7 -0
  280. package/plugins/roblox-ai-os-creator-skills/skills/ask-gemini/SKILL.md +7 -0
  281. package/plugins/roblox-ai-os-creator-skills/skills/autoforge/SKILL.md +7 -0
  282. package/plugins/roblox-ai-os-creator-skills/skills/autopilot/SKILL.md +48 -41
  283. package/plugins/roblox-ai-os-creator-skills/skills/autoresearch/SKILL.md +8 -1
  284. package/plugins/roblox-ai-os-creator-skills/skills/blueprint/SKILL.md +227 -9
  285. package/plugins/roblox-ai-os-creator-skills/skills/blueprint-loop/SKILL.md +7 -0
  286. package/plugins/roblox-ai-os-creator-skills/skills/blueprint-psych/SKILL.md +7 -0
  287. package/plugins/roblox-ai-os-creator-skills/skills/blueprint-retention/SKILL.md +7 -0
  288. package/plugins/roblox-ai-os-creator-skills/skills/blueprint-social/SKILL.md +7 -0
  289. package/plugins/roblox-ai-os-creator-skills/skills/brief/SKILL.md +7 -0
  290. package/plugins/roblox-ai-os-creator-skills/skills/brief-audience/SKILL.md +7 -0
  291. package/plugins/roblox-ai-os-creator-skills/skills/brief-motivation/SKILL.md +7 -0
  292. package/plugins/roblox-ai-os-creator-skills/skills/cancel/SKILL.md +59 -52
  293. package/plugins/roblox-ai-os-creator-skills/skills/code-review/SKILL.md +30 -24
  294. package/plugins/roblox-ai-os-creator-skills/skills/configure-notifications/SKILL.md +7 -0
  295. package/plugins/roblox-ai-os-creator-skills/skills/crew/SKILL.md +7 -0
  296. package/plugins/roblox-ai-os-creator-skills/skills/deep-interview/SKILL.md +25 -18
  297. package/plugins/roblox-ai-os-creator-skills/skills/doctor/SKILL.md +7 -0
  298. package/plugins/roblox-ai-os-creator-skills/skills/forge/SKILL.md +174 -11
  299. package/plugins/roblox-ai-os-creator-skills/skills/forge-community/SKILL.md +7 -0
  300. package/plugins/roblox-ai-os-creator-skills/skills/forge-daily-loop/SKILL.md +7 -0
  301. package/plugins/roblox-ai-os-creator-skills/skills/forge-event-loop/SKILL.md +7 -0
  302. package/plugins/roblox-ai-os-creator-skills/skills/forge-fomo/SKILL.md +7 -0
  303. package/plugins/roblox-ai-os-creator-skills/skills/forge-mastery/SKILL.md +7 -0
  304. package/plugins/roblox-ai-os-creator-skills/skills/forge-progression/SKILL.md +7 -0
  305. package/plugins/roblox-ai-os-creator-skills/skills/forge-reward-loop/SKILL.md +7 -0
  306. package/plugins/roblox-ai-os-creator-skills/skills/forge-status/SKILL.md +7 -0
  307. package/plugins/roblox-ai-os-creator-skills/skills/help/SKILL.md +8 -1
  308. package/plugins/roblox-ai-os-creator-skills/skills/hud/SKILL.md +16 -9
  309. package/plugins/roblox-ai-os-creator-skills/skills/note/SKILL.md +8 -1
  310. package/plugins/roblox-ai-os-creator-skills/skills/pipeline/SKILL.md +18 -11
  311. package/plugins/roblox-ai-os-creator-skills/skills/plan/SKILL.md +36 -29
  312. package/plugins/roblox-ai-os-creator-skills/skills/rcs-setup/SKILL.md +8 -1
  313. package/plugins/roblox-ai-os-creator-skills/skills/security-review/SKILL.md +120 -236
  314. package/plugins/roblox-ai-os-creator-skills/skills/skill/SKILL.md +20 -13
  315. package/plugins/roblox-ai-os-creator-skills/skills/team/SKILL.md +17 -11
  316. package/plugins/roblox-ai-os-creator-skills/skills/trace/SKILL.md +7 -0
  317. package/plugins/roblox-ai-os-creator-skills/skills/ultraqa/SKILL.md +10 -3
  318. package/plugins/roblox-ai-os-creator-skills/skills/ultrawork/SKILL.md +19 -12
  319. package/plugins/roblox-ai-os-creator-skills/skills/{visual-ralph → visual-forge}/SKILL.md +36 -27
  320. package/plugins/roblox-ai-os-creator-skills/skills/visual-verdict/SKILL.md +9 -2
  321. package/plugins/roblox-ai-os-creator-skills/skills/wiki/SKILL.md +10 -3
  322. package/plugins/roblox-ai-os-creator-skills/skills/worker/SKILL.md +16 -7
  323. package/plugins/roblox-ai-os-creator-skills/templates/roblox/pre-action-plan.md +1 -0
  324. package/prompts/analyst.md +7 -0
  325. package/prompts/architect.md +11 -4
  326. package/prompts/build-fixer.md +7 -0
  327. package/prompts/code-reviewer.md +9 -2
  328. package/prompts/code-simplifier.md +4 -0
  329. package/prompts/critic.md +13 -6
  330. package/prompts/debugger.md +8 -1
  331. package/prompts/dependency-expert.md +8 -1
  332. package/prompts/designer.md +20 -10
  333. package/prompts/executor.md +7 -0
  334. package/prompts/explore-harness.md +7 -0
  335. package/prompts/explore.md +7 -0
  336. package/prompts/git-master.md +8 -1
  337. package/prompts/planner.md +10 -3
  338. package/prompts/researcher.md +7 -0
  339. package/prompts/security-reviewer.md +76 -92
  340. package/prompts/sisyphus-lite.md +7 -0
  341. package/prompts/team-executor.md +7 -0
  342. package/prompts/team-orchestrator.md +9 -2
  343. package/prompts/test-engineer.md +11 -3
  344. package/prompts/verifier.md +7 -0
  345. package/prompts/vision.md +9 -2
  346. package/prompts/writer.md +11 -4
  347. package/skills/.agents/skills/roblox-animations/SKILL.md +220 -0
  348. package/skills/.agents/skills/roblox-datastores/SKILL.md +219 -0
  349. package/skills/.agents/skills/roblox-gui/SKILL.md +192 -0
  350. package/skills/.agents/skills/roblox-monetization/SKILL.md +208 -0
  351. package/skills/.agents/skills/roblox-performance/SKILL.md +230 -0
  352. package/skills/.agents/skills/roblox-remote-events/SKILL.md +199 -0
  353. package/skills/.agents/skills/roblox-security/SKILL.md +236 -0
  354. package/skills/ai-slop-cleaner/SKILL.md +14 -7
  355. package/skills/analyze/SKILL.md +9 -2
  356. package/skills/ask-claude/SKILL.md +7 -0
  357. package/skills/ask-gemini/SKILL.md +7 -0
  358. package/skills/autoforge/SKILL.md +7 -0
  359. package/skills/autopilot/SKILL.md +48 -41
  360. package/skills/autoresearch/SKILL.md +8 -1
  361. package/skills/blueprint/SKILL.md +227 -9
  362. package/skills/blueprint-loop/SKILL.md +7 -0
  363. package/skills/blueprint-psych/SKILL.md +7 -0
  364. package/skills/blueprint-retention/SKILL.md +7 -0
  365. package/skills/blueprint-social/SKILL.md +7 -0
  366. package/skills/brief/SKILL.md +7 -0
  367. package/skills/brief-audience/SKILL.md +7 -0
  368. package/skills/brief-motivation/SKILL.md +7 -0
  369. package/skills/build-fix/SKILL.md +9 -2
  370. package/skills/cancel/SKILL.md +59 -52
  371. package/skills/code-review/SKILL.md +30 -24
  372. package/skills/configure-notifications/SKILL.md +7 -0
  373. package/skills/crew/SKILL.md +7 -0
  374. package/skills/deep-interview/SKILL.md +25 -18
  375. package/skills/deepsearch/SKILL.md +7 -0
  376. package/skills/doctor/SKILL.md +7 -0
  377. package/skills/ecomode/SKILL.md +9 -2
  378. package/skills/forge/SKILL.md +174 -11
  379. package/skills/forge-community/SKILL.md +7 -0
  380. package/skills/forge-daily-loop/SKILL.md +7 -0
  381. package/skills/forge-event-loop/SKILL.md +7 -0
  382. package/skills/forge-fomo/SKILL.md +7 -0
  383. package/skills/{ralph-init → forge-init}/SKILL.md +20 -13
  384. package/skills/forge-mastery/SKILL.md +7 -0
  385. package/skills/forge-progression/SKILL.md +7 -0
  386. package/skills/forge-reward-loop/SKILL.md +7 -0
  387. package/skills/forge-status/SKILL.md +7 -0
  388. package/skills/git-master/SKILL.md +7 -0
  389. package/skills/help/SKILL.md +8 -1
  390. package/skills/hud/SKILL.md +16 -9
  391. package/skills/note/SKILL.md +8 -1
  392. package/skills/pipeline/SKILL.md +18 -11
  393. package/skills/plan/SKILL.md +36 -29
  394. package/skills/rcs-setup/SKILL.md +8 -1
  395. package/skills/review/SKILL.md +7 -0
  396. package/skills/security-review/SKILL.md +120 -236
  397. package/skills/skill/SKILL.md +20 -13
  398. package/skills/skills-lock.json +47 -0
  399. package/skills/swarm/SKILL.md +8 -1
  400. package/skills/tdd/SKILL.md +7 -0
  401. package/skills/team/SKILL.md +17 -11
  402. package/skills/trace/SKILL.md +7 -0
  403. package/skills/ultraqa/SKILL.md +10 -3
  404. package/skills/ultrawork/SKILL.md +19 -12
  405. package/skills/{visual-ralph → visual-forge}/SKILL.md +36 -27
  406. package/skills/visual-verdict/SKILL.md +9 -2
  407. package/skills/web-clone/SKILL.md +14 -7
  408. package/skills/wiki/SKILL.md +10 -3
  409. package/skills/worker/SKILL.md +16 -7
  410. package/src/scripts/__tests__/codex-native-hook.test.ts +386 -319
  411. package/src/scripts/__tests__/run-test-files.test.ts +6 -4
  412. package/src/scripts/__tests__/verify-native-agents.test.ts +16 -16
  413. package/src/scripts/codex-native-hook.ts +34 -34
  414. package/src/scripts/eval/eval-cross-server-party-flow.ts +14 -0
  415. package/src/scripts/eval/eval-gui-onboarding-clarity.ts +20 -0
  416. package/src/scripts/eval/eval-liveops-reward-loop-balance.ts +14 -0
  417. package/src/scripts/eval/eval-profile-datastore-recovery.ts +20 -0
  418. package/src/scripts/eval/eval-remote-contract-hardening.ts +20 -0
  419. package/src/scripts/notify-fallback-watcher.ts +147 -146
  420. package/src/scripts/notify-hook/__tests__/team-worker-posttooluse.test.ts +24 -10
  421. package/src/scripts/notify-hook/{ralph-session-resume.ts → forge-session-resume.ts} +45 -43
  422. package/src/scripts/notify-hook/team-dispatch.ts +31 -4
  423. package/src/scripts/notify-hook/visual-verdict.ts +3 -3
  424. package/src/scripts/notify-hook.ts +10 -10
  425. package/src/scripts/run-test-files.ts +1 -1
  426. package/src/scripts/surface-taxonomy.ts +316 -0
  427. package/src/scripts/sync-plugin-mirror.ts +5 -4
  428. package/src/scripts/tmux-hook-engine.ts +31 -19
  429. package/templates/AGENTS.md +24 -15
  430. package/templates/catalog-manifest.json +5 -88
  431. package/templates/roblox/pre-action-plan.md +1 -0
  432. package/templates/roblox/robloxstudio-mcp.codex.json +18 -0
  433. package/templates/roblox/robloxstudio-mcp.windows.json +22 -0
  434. package/dist/adapt/__tests__/foundation.test.d.ts +0 -2
  435. package/dist/adapt/__tests__/foundation.test.d.ts.map +0 -1
  436. package/dist/adapt/__tests__/foundation.test.js +0 -171
  437. package/dist/adapt/__tests__/foundation.test.js.map +0 -1
  438. package/dist/adapt/__tests__/hermes.test.d.ts +0 -2
  439. package/dist/adapt/__tests__/hermes.test.d.ts.map +0 -1
  440. package/dist/adapt/__tests__/hermes.test.js +0 -137
  441. package/dist/adapt/__tests__/hermes.test.js.map +0 -1
  442. package/dist/agents/__tests__/definitions.test.d.ts +0 -2
  443. package/dist/agents/__tests__/definitions.test.d.ts.map +0 -1
  444. package/dist/agents/__tests__/definitions.test.js +0 -62
  445. package/dist/agents/__tests__/definitions.test.js.map +0 -1
  446. package/dist/agents/__tests__/native-config.test.d.ts +0 -2
  447. package/dist/agents/__tests__/native-config.test.d.ts.map +0 -1
  448. package/dist/agents/__tests__/native-config.test.js +0 -278
  449. package/dist/agents/__tests__/native-config.test.js.map +0 -1
  450. package/dist/autoresearch/__tests__/contracts.test.d.ts +0 -2
  451. package/dist/autoresearch/__tests__/contracts.test.d.ts.map +0 -1
  452. package/dist/autoresearch/__tests__/contracts.test.js +0 -127
  453. package/dist/autoresearch/__tests__/contracts.test.js.map +0 -1
  454. package/dist/autoresearch/__tests__/runtime-parity-extra.test.d.ts +0 -2
  455. package/dist/autoresearch/__tests__/runtime-parity-extra.test.d.ts.map +0 -1
  456. package/dist/autoresearch/__tests__/runtime-parity-extra.test.js +0 -356
  457. package/dist/autoresearch/__tests__/runtime-parity-extra.test.js.map +0 -1
  458. package/dist/autoresearch/__tests__/runtime.test.d.ts +0 -2
  459. package/dist/autoresearch/__tests__/runtime.test.d.ts.map +0 -1
  460. package/dist/autoresearch/__tests__/runtime.test.js +0 -218
  461. package/dist/autoresearch/__tests__/runtime.test.js.map +0 -1
  462. package/dist/autoresearch/__tests__/skill-validation.test.d.ts +0 -2
  463. package/dist/autoresearch/__tests__/skill-validation.test.d.ts.map +0 -1
  464. package/dist/autoresearch/__tests__/skill-validation.test.js +0 -91
  465. package/dist/autoresearch/__tests__/skill-validation.test.js.map +0 -1
  466. package/dist/catalog/__tests__/generator.test.d.ts +0 -2
  467. package/dist/catalog/__tests__/generator.test.d.ts.map +0 -1
  468. package/dist/catalog/__tests__/generator.test.js +0 -49
  469. package/dist/catalog/__tests__/generator.test.js.map +0 -1
  470. package/dist/catalog/__tests__/plugin-bundle-ssot.test.d.ts +0 -2
  471. package/dist/catalog/__tests__/plugin-bundle-ssot.test.d.ts.map +0 -1
  472. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +0 -83
  473. package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +0 -1
  474. package/dist/catalog/__tests__/schema.test.d.ts +0 -2
  475. package/dist/catalog/__tests__/schema.test.d.ts.map +0 -1
  476. package/dist/catalog/__tests__/schema.test.js +0 -91
  477. package/dist/catalog/__tests__/schema.test.js.map +0 -1
  478. package/dist/cli/__tests__/adapt-help.test.d.ts +0 -2
  479. package/dist/cli/__tests__/adapt-help.test.d.ts.map +0 -1
  480. package/dist/cli/__tests__/adapt-help.test.js +0 -37
  481. package/dist/cli/__tests__/adapt-help.test.js.map +0 -1
  482. package/dist/cli/__tests__/adapt.test.d.ts +0 -2
  483. package/dist/cli/__tests__/adapt.test.d.ts.map +0 -1
  484. package/dist/cli/__tests__/adapt.test.js +0 -62
  485. package/dist/cli/__tests__/adapt.test.js.map +0 -1
  486. package/dist/cli/__tests__/agents-init.test.d.ts +0 -2
  487. package/dist/cli/__tests__/agents-init.test.d.ts.map +0 -1
  488. package/dist/cli/__tests__/agents-init.test.js +0 -184
  489. package/dist/cli/__tests__/agents-init.test.js.map +0 -1
  490. package/dist/cli/__tests__/agents.test.d.ts +0 -2
  491. package/dist/cli/__tests__/agents.test.d.ts.map +0 -1
  492. package/dist/cli/__tests__/agents.test.js +0 -137
  493. package/dist/cli/__tests__/agents.test.js.map +0 -1
  494. package/dist/cli/__tests__/ask.test.d.ts +0 -2
  495. package/dist/cli/__tests__/ask.test.d.ts.map +0 -1
  496. package/dist/cli/__tests__/ask.test.js +0 -265
  497. package/dist/cli/__tests__/ask.test.js.map +0 -1
  498. package/dist/cli/__tests__/autoresearch-guided.test.d.ts +0 -2
  499. package/dist/cli/__tests__/autoresearch-guided.test.d.ts.map +0 -1
  500. package/dist/cli/__tests__/autoresearch-guided.test.js +0 -365
  501. package/dist/cli/__tests__/autoresearch-guided.test.js.map +0 -1
  502. package/dist/cli/__tests__/autoresearch.test.d.ts +0 -2
  503. package/dist/cli/__tests__/autoresearch.test.d.ts.map +0 -1
  504. package/dist/cli/__tests__/autoresearch.test.js +0 -203
  505. package/dist/cli/__tests__/autoresearch.test.js.map +0 -1
  506. package/dist/cli/__tests__/catalog-contract.test.d.ts +0 -2
  507. package/dist/cli/__tests__/catalog-contract.test.d.ts.map +0 -1
  508. package/dist/cli/__tests__/catalog-contract.test.js +0 -18
  509. package/dist/cli/__tests__/catalog-contract.test.js.map +0 -1
  510. package/dist/cli/__tests__/cleanup.test.d.ts +0 -2
  511. package/dist/cli/__tests__/cleanup.test.d.ts.map +0 -1
  512. package/dist/cli/__tests__/cleanup.test.js +0 -419
  513. package/dist/cli/__tests__/cleanup.test.js.map +0 -1
  514. package/dist/cli/__tests__/codex-plugin-layout.test.d.ts +0 -2
  515. package/dist/cli/__tests__/codex-plugin-layout.test.d.ts.map +0 -1
  516. package/dist/cli/__tests__/codex-plugin-layout.test.js +0 -210
  517. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +0 -1
  518. package/dist/cli/__tests__/doctor-context-window-warning.test.d.ts +0 -2
  519. package/dist/cli/__tests__/doctor-context-window-warning.test.d.ts.map +0 -1
  520. package/dist/cli/__tests__/doctor-context-window-warning.test.js +0 -122
  521. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +0 -1
  522. package/dist/cli/__tests__/doctor-invalid-config.test.d.ts +0 -2
  523. package/dist/cli/__tests__/doctor-invalid-config.test.d.ts.map +0 -1
  524. package/dist/cli/__tests__/doctor-invalid-config.test.js +0 -52
  525. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +0 -1
  526. package/dist/cli/__tests__/doctor-team.test.d.ts +0 -2
  527. package/dist/cli/__tests__/doctor-team.test.d.ts.map +0 -1
  528. package/dist/cli/__tests__/doctor-team.test.js +0 -299
  529. package/dist/cli/__tests__/doctor-team.test.js.map +0 -1
  530. package/dist/cli/__tests__/doctor-warning-copy.test.d.ts +0 -2
  531. package/dist/cli/__tests__/doctor-warning-copy.test.d.ts.map +0 -1
  532. package/dist/cli/__tests__/doctor-warning-copy.test.js +0 -438
  533. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +0 -1
  534. package/dist/cli/__tests__/error-handling-warnings.test.d.ts +0 -2
  535. package/dist/cli/__tests__/error-handling-warnings.test.d.ts.map +0 -1
  536. package/dist/cli/__tests__/error-handling-warnings.test.js +0 -52
  537. package/dist/cli/__tests__/error-handling-warnings.test.js.map +0 -1
  538. package/dist/cli/__tests__/exec.test.d.ts +0 -2
  539. package/dist/cli/__tests__/exec.test.d.ts.map +0 -1
  540. package/dist/cli/__tests__/exec.test.js +0 -213
  541. package/dist/cli/__tests__/exec.test.js.map +0 -1
  542. package/dist/cli/__tests__/explore-windows-diagnostics.test.d.ts +0 -2
  543. package/dist/cli/__tests__/explore-windows-diagnostics.test.d.ts.map +0 -1
  544. package/dist/cli/__tests__/explore-windows-diagnostics.test.js +0 -17
  545. package/dist/cli/__tests__/explore-windows-diagnostics.test.js.map +0 -1
  546. package/dist/cli/__tests__/explore.test.d.ts +0 -2
  547. package/dist/cli/__tests__/explore.test.d.ts.map +0 -1
  548. package/dist/cli/__tests__/explore.test.js +0 -1090
  549. package/dist/cli/__tests__/explore.test.js.map +0 -1
  550. package/dist/cli/__tests__/hooks.test.d.ts +0 -2
  551. package/dist/cli/__tests__/hooks.test.d.ts.map +0 -1
  552. package/dist/cli/__tests__/hooks.test.js +0 -55
  553. package/dist/cli/__tests__/hooks.test.js.map +0 -1
  554. package/dist/cli/__tests__/index.test.d.ts +0 -2
  555. package/dist/cli/__tests__/index.test.d.ts.map +0 -1
  556. package/dist/cli/__tests__/index.test.js +0 -2259
  557. package/dist/cli/__tests__/index.test.js.map +0 -1
  558. package/dist/cli/__tests__/launch-fallback.test.d.ts +0 -2
  559. package/dist/cli/__tests__/launch-fallback.test.d.ts.map +0 -1
  560. package/dist/cli/__tests__/launch-fallback.test.js +0 -661
  561. package/dist/cli/__tests__/launch-fallback.test.js.map +0 -1
  562. package/dist/cli/__tests__/lifecycle-notifications.test.d.ts +0 -2
  563. package/dist/cli/__tests__/lifecycle-notifications.test.d.ts.map +0 -1
  564. package/dist/cli/__tests__/lifecycle-notifications.test.js +0 -48
  565. package/dist/cli/__tests__/lifecycle-notifications.test.js.map +0 -1
  566. package/dist/cli/__tests__/list.test.d.ts +0 -2
  567. package/dist/cli/__tests__/list.test.d.ts.map +0 -1
  568. package/dist/cli/__tests__/list.test.js +0 -38
  569. package/dist/cli/__tests__/list.test.js.map +0 -1
  570. package/dist/cli/__tests__/mcp-parity.test.d.ts +0 -2
  571. package/dist/cli/__tests__/mcp-parity.test.d.ts.map +0 -1
  572. package/dist/cli/__tests__/mcp-parity.test.js +0 -228
  573. package/dist/cli/__tests__/mcp-parity.test.js.map +0 -1
  574. package/dist/cli/__tests__/mcp-serve.test.d.ts +0 -2
  575. package/dist/cli/__tests__/mcp-serve.test.d.ts.map +0 -1
  576. package/dist/cli/__tests__/mcp-serve.test.js +0 -64
  577. package/dist/cli/__tests__/mcp-serve.test.js.map +0 -1
  578. package/dist/cli/__tests__/native-assets.test.d.ts +0 -2
  579. package/dist/cli/__tests__/native-assets.test.d.ts.map +0 -1
  580. package/dist/cli/__tests__/native-assets.test.js +0 -308
  581. package/dist/cli/__tests__/native-assets.test.js.map +0 -1
  582. package/dist/cli/__tests__/native-hook-dispatch-contract.test.d.ts +0 -2
  583. package/dist/cli/__tests__/native-hook-dispatch-contract.test.d.ts.map +0 -1
  584. package/dist/cli/__tests__/native-hook-dispatch-contract.test.js +0 -11
  585. package/dist/cli/__tests__/native-hook-dispatch-contract.test.js.map +0 -1
  586. package/dist/cli/__tests__/nested-help-routing.test.d.ts +0 -2
  587. package/dist/cli/__tests__/nested-help-routing.test.d.ts.map +0 -1
  588. package/dist/cli/__tests__/nested-help-routing.test.js +0 -96
  589. package/dist/cli/__tests__/nested-help-routing.test.js.map +0 -1
  590. package/dist/cli/__tests__/package-bin-contract.test.d.ts +0 -2
  591. package/dist/cli/__tests__/package-bin-contract.test.d.ts.map +0 -1
  592. package/dist/cli/__tests__/package-bin-contract.test.js +0 -177
  593. package/dist/cli/__tests__/package-bin-contract.test.js.map +0 -1
  594. package/dist/cli/__tests__/packaged-explore-harness-lock.d.ts +0 -3
  595. package/dist/cli/__tests__/packaged-explore-harness-lock.d.ts.map +0 -1
  596. package/dist/cli/__tests__/packaged-explore-harness-lock.js +0 -67
  597. package/dist/cli/__tests__/packaged-explore-harness-lock.js.map +0 -1
  598. package/dist/cli/__tests__/packaged-script-resolution.test.d.ts +0 -2
  599. package/dist/cli/__tests__/packaged-script-resolution.test.d.ts.map +0 -1
  600. package/dist/cli/__tests__/packaged-script-resolution.test.js +0 -19
  601. package/dist/cli/__tests__/packaged-script-resolution.test.js.map +0 -1
  602. package/dist/cli/__tests__/prompt-skill-sanitization.test.d.ts +0 -2
  603. package/dist/cli/__tests__/prompt-skill-sanitization.test.d.ts.map +0 -1
  604. package/dist/cli/__tests__/prompt-skill-sanitization.test.js +0 -48
  605. package/dist/cli/__tests__/prompt-skill-sanitization.test.js.map +0 -1
  606. package/dist/cli/__tests__/question.test.d.ts +0 -2
  607. package/dist/cli/__tests__/question.test.d.ts.map +0 -1
  608. package/dist/cli/__tests__/question.test.js +0 -633
  609. package/dist/cli/__tests__/question.test.js.map +0 -1
  610. package/dist/cli/__tests__/ralph-deslop-contract.test.d.ts +0 -2
  611. package/dist/cli/__tests__/ralph-deslop-contract.test.d.ts.map +0 -1
  612. package/dist/cli/__tests__/ralph-deslop-contract.test.js +0 -28
  613. package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +0 -1
  614. package/dist/cli/__tests__/ralph-prd-deep-interview.test.d.ts +0 -2
  615. package/dist/cli/__tests__/ralph-prd-deep-interview.test.d.ts.map +0 -1
  616. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js +0 -24
  617. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +0 -1
  618. package/dist/cli/__tests__/ralph-prd-smoke.test.d.ts +0 -2
  619. package/dist/cli/__tests__/ralph-prd-smoke.test.d.ts.map +0 -1
  620. package/dist/cli/__tests__/ralph-prd-smoke.test.js +0 -167
  621. package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +0 -1
  622. package/dist/cli/__tests__/ralph.test.d.ts +0 -2
  623. package/dist/cli/__tests__/ralph.test.d.ts.map +0 -1
  624. package/dist/cli/__tests__/ralph.test.js +0 -256
  625. package/dist/cli/__tests__/ralph.test.js.map +0 -1
  626. package/dist/cli/__tests__/resume.test.d.ts +0 -2
  627. package/dist/cli/__tests__/resume.test.d.ts.map +0 -1
  628. package/dist/cli/__tests__/resume.test.js +0 -84
  629. package/dist/cli/__tests__/resume.test.js.map +0 -1
  630. package/dist/cli/__tests__/session-scoped-runtime.test.d.ts +0 -2
  631. package/dist/cli/__tests__/session-scoped-runtime.test.d.ts.map +0 -1
  632. package/dist/cli/__tests__/session-scoped-runtime.test.js +0 -146
  633. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +0 -1
  634. package/dist/cli/__tests__/session-search-help.test.d.ts +0 -2
  635. package/dist/cli/__tests__/session-search-help.test.d.ts.map +0 -1
  636. package/dist/cli/__tests__/session-search-help.test.js +0 -76
  637. package/dist/cli/__tests__/session-search-help.test.js.map +0 -1
  638. package/dist/cli/__tests__/session-search.test.d.ts +0 -2
  639. package/dist/cli/__tests__/session-search.test.d.ts.map +0 -1
  640. package/dist/cli/__tests__/session-search.test.js +0 -77
  641. package/dist/cli/__tests__/session-search.test.js.map +0 -1
  642. package/dist/cli/__tests__/setup-agents-overwrite.test.d.ts +0 -2
  643. package/dist/cli/__tests__/setup-agents-overwrite.test.d.ts.map +0 -1
  644. package/dist/cli/__tests__/setup-agents-overwrite.test.js +0 -457
  645. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +0 -1
  646. package/dist/cli/__tests__/setup-gh-star.test.d.ts +0 -2
  647. package/dist/cli/__tests__/setup-gh-star.test.d.ts.map +0 -1
  648. package/dist/cli/__tests__/setup-gh-star.test.js +0 -67
  649. package/dist/cli/__tests__/setup-gh-star.test.js.map +0 -1
  650. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.d.ts +0 -2
  651. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.d.ts.map +0 -1
  652. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +0 -189
  653. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +0 -1
  654. package/dist/cli/__tests__/setup-install-mode.test.d.ts +0 -2
  655. package/dist/cli/__tests__/setup-install-mode.test.d.ts.map +0 -1
  656. package/dist/cli/__tests__/setup-install-mode.test.js +0 -873
  657. package/dist/cli/__tests__/setup-install-mode.test.js.map +0 -1
  658. package/dist/cli/__tests__/setup-prompts-overwrite.test.d.ts +0 -2
  659. package/dist/cli/__tests__/setup-prompts-overwrite.test.d.ts.map +0 -1
  660. package/dist/cli/__tests__/setup-prompts-overwrite.test.js +0 -191
  661. package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +0 -1
  662. package/dist/cli/__tests__/setup-refresh.test.d.ts +0 -2
  663. package/dist/cli/__tests__/setup-refresh.test.d.ts.map +0 -1
  664. package/dist/cli/__tests__/setup-refresh.test.js +0 -591
  665. package/dist/cli/__tests__/setup-refresh.test.js.map +0 -1
  666. package/dist/cli/__tests__/setup-scope.test.d.ts +0 -2
  667. package/dist/cli/__tests__/setup-scope.test.d.ts.map +0 -1
  668. package/dist/cli/__tests__/setup-scope.test.js +0 -340
  669. package/dist/cli/__tests__/setup-scope.test.js.map +0 -1
  670. package/dist/cli/__tests__/setup-skill-validation.test.d.ts +0 -2
  671. package/dist/cli/__tests__/setup-skill-validation.test.d.ts.map +0 -1
  672. package/dist/cli/__tests__/setup-skill-validation.test.js +0 -44
  673. package/dist/cli/__tests__/setup-skill-validation.test.js.map +0 -1
  674. package/dist/cli/__tests__/setup-skills-overwrite.test.d.ts +0 -2
  675. package/dist/cli/__tests__/setup-skills-overwrite.test.d.ts.map +0 -1
  676. package/dist/cli/__tests__/setup-skills-overwrite.test.js +0 -295
  677. package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +0 -1
  678. package/dist/cli/__tests__/sidecar.test.d.ts +0 -2
  679. package/dist/cli/__tests__/sidecar.test.d.ts.map +0 -1
  680. package/dist/cli/__tests__/sidecar.test.js +0 -24
  681. package/dist/cli/__tests__/sidecar.test.js.map +0 -1
  682. package/dist/cli/__tests__/sparkshell-cli.test.d.ts +0 -2
  683. package/dist/cli/__tests__/sparkshell-cli.test.d.ts.map +0 -1
  684. package/dist/cli/__tests__/sparkshell-cli.test.js +0 -400
  685. package/dist/cli/__tests__/sparkshell-cli.test.js.map +0 -1
  686. package/dist/cli/__tests__/sparkshell-packaging.test.d.ts +0 -2
  687. package/dist/cli/__tests__/sparkshell-packaging.test.d.ts.map +0 -1
  688. package/dist/cli/__tests__/sparkshell-packaging.test.js +0 -74
  689. package/dist/cli/__tests__/sparkshell-packaging.test.js.map +0 -1
  690. package/dist/cli/__tests__/star-prompt.test.d.ts +0 -2
  691. package/dist/cli/__tests__/star-prompt.test.d.ts.map +0 -1
  692. package/dist/cli/__tests__/star-prompt.test.js +0 -172
  693. package/dist/cli/__tests__/star-prompt.test.js.map +0 -1
  694. package/dist/cli/__tests__/state.test.d.ts +0 -2
  695. package/dist/cli/__tests__/state.test.d.ts.map +0 -1
  696. package/dist/cli/__tests__/state.test.js +0 -46
  697. package/dist/cli/__tests__/state.test.js.map +0 -1
  698. package/dist/cli/__tests__/team-decompose.test.d.ts +0 -2
  699. package/dist/cli/__tests__/team-decompose.test.d.ts.map +0 -1
  700. package/dist/cli/__tests__/team-decompose.test.js +0 -133
  701. package/dist/cli/__tests__/team-decompose.test.js.map +0 -1
  702. package/dist/cli/__tests__/team.test.d.ts +0 -2
  703. package/dist/cli/__tests__/team.test.d.ts.map +0 -1
  704. package/dist/cli/__tests__/team.test.js +0 -1820
  705. package/dist/cli/__tests__/team.test.js.map +0 -1
  706. package/dist/cli/__tests__/uninstall.test.d.ts +0 -2
  707. package/dist/cli/__tests__/uninstall.test.d.ts.map +0 -1
  708. package/dist/cli/__tests__/uninstall.test.js +0 -766
  709. package/dist/cli/__tests__/uninstall.test.js.map +0 -1
  710. package/dist/cli/__tests__/update.test.d.ts +0 -2
  711. package/dist/cli/__tests__/update.test.d.ts.map +0 -1
  712. package/dist/cli/__tests__/update.test.js +0 -589
  713. package/dist/cli/__tests__/update.test.js.map +0 -1
  714. package/dist/cli/__tests__/version-sync-contract.test.d.ts +0 -2
  715. package/dist/cli/__tests__/version-sync-contract.test.d.ts.map +0 -1
  716. package/dist/cli/__tests__/version-sync-contract.test.js +0 -41
  717. package/dist/cli/__tests__/version-sync-contract.test.js.map +0 -1
  718. package/dist/cli/__tests__/version.test.d.ts +0 -2
  719. package/dist/cli/__tests__/version.test.d.ts.map +0 -1
  720. package/dist/cli/__tests__/version.test.js +0 -21
  721. package/dist/cli/__tests__/version.test.js.map +0 -1
  722. package/dist/cli/__tests__/windows-popup-loop-contract.test.d.ts +0 -2
  723. package/dist/cli/__tests__/windows-popup-loop-contract.test.d.ts.map +0 -1
  724. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +0 -31
  725. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +0 -1
  726. package/dist/cli/ralph.d.ts +0 -17
  727. package/dist/compat/__tests__/doctor-contract.test.d.ts +0 -2
  728. package/dist/compat/__tests__/doctor-contract.test.d.ts.map +0 -1
  729. package/dist/compat/__tests__/doctor-contract.test.js +0 -108
  730. package/dist/compat/__tests__/doctor-contract.test.js.map +0 -1
  731. package/dist/compat/__tests__/rust-runtime-compat.test.d.ts +0 -2
  732. package/dist/compat/__tests__/rust-runtime-compat.test.d.ts.map +0 -1
  733. package/dist/compat/__tests__/rust-runtime-compat.test.js +0 -218
  734. package/dist/compat/__tests__/rust-runtime-compat.test.js.map +0 -1
  735. package/dist/config/__tests__/codex-hooks.test.d.ts +0 -2
  736. package/dist/config/__tests__/codex-hooks.test.d.ts.map +0 -1
  737. package/dist/config/__tests__/codex-hooks.test.js +0 -77
  738. package/dist/config/__tests__/codex-hooks.test.js.map +0 -1
  739. package/dist/config/__tests__/generator-idempotent.test.d.ts +0 -2
  740. package/dist/config/__tests__/generator-idempotent.test.d.ts.map +0 -1
  741. package/dist/config/__tests__/generator-idempotent.test.js +0 -882
  742. package/dist/config/__tests__/generator-idempotent.test.js.map +0 -1
  743. package/dist/config/__tests__/generator-notify.test.d.ts +0 -2
  744. package/dist/config/__tests__/generator-notify.test.d.ts.map +0 -1
  745. package/dist/config/__tests__/generator-notify.test.js +0 -343
  746. package/dist/config/__tests__/generator-notify.test.js.map +0 -1
  747. package/dist/config/__tests__/generator-status-line-presets.test.d.ts +0 -2
  748. package/dist/config/__tests__/generator-status-line-presets.test.d.ts.map +0 -1
  749. package/dist/config/__tests__/generator-status-line-presets.test.js +0 -203
  750. package/dist/config/__tests__/generator-status-line-presets.test.js.map +0 -1
  751. package/dist/config/__tests__/mcp-registry.test.d.ts +0 -2
  752. package/dist/config/__tests__/mcp-registry.test.d.ts.map +0 -1
  753. package/dist/config/__tests__/mcp-registry.test.js +0 -190
  754. package/dist/config/__tests__/mcp-registry.test.js.map +0 -1
  755. package/dist/config/__tests__/models.test.d.ts +0 -2
  756. package/dist/config/__tests__/models.test.d.ts.map +0 -1
  757. package/dist/config/__tests__/models.test.js +0 -224
  758. package/dist/config/__tests__/models.test.js.map +0 -1
  759. package/dist/config/__tests__/wiki-config-contract.test.d.ts +0 -2
  760. package/dist/config/__tests__/wiki-config-contract.test.d.ts.map +0 -1
  761. package/dist/config/__tests__/wiki-config-contract.test.js +0 -19
  762. package/dist/config/__tests__/wiki-config-contract.test.js.map +0 -1
  763. package/dist/document-refresh/__tests__/enforcer.test.d.ts +0 -2
  764. package/dist/document-refresh/__tests__/enforcer.test.d.ts.map +0 -1
  765. package/dist/document-refresh/__tests__/enforcer.test.js +0 -128
  766. package/dist/document-refresh/__tests__/enforcer.test.js.map +0 -1
  767. package/dist/hooks/__tests__/agents-overlay.test.d.ts +0 -8
  768. package/dist/hooks/__tests__/agents-overlay.test.d.ts.map +0 -1
  769. package/dist/hooks/__tests__/agents-overlay.test.js +0 -644
  770. package/dist/hooks/__tests__/agents-overlay.test.js.map +0 -1
  771. package/dist/hooks/__tests__/analyze-routing-contract.test.d.ts +0 -2
  772. package/dist/hooks/__tests__/analyze-routing-contract.test.d.ts.map +0 -1
  773. package/dist/hooks/__tests__/analyze-routing-contract.test.js +0 -45
  774. package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +0 -1
  775. package/dist/hooks/__tests__/analyze-skill-contract.test.d.ts +0 -2
  776. package/dist/hooks/__tests__/analyze-skill-contract.test.d.ts.map +0 -1
  777. package/dist/hooks/__tests__/analyze-skill-contract.test.js +0 -48
  778. package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +0 -1
  779. package/dist/hooks/__tests__/anti-slop-workflow.test.d.ts +0 -2
  780. package/dist/hooks/__tests__/anti-slop-workflow.test.d.ts.map +0 -1
  781. package/dist/hooks/__tests__/anti-slop-workflow.test.js +0 -146
  782. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +0 -1
  783. package/dist/hooks/__tests__/autopilot-skill-contract.test.d.ts +0 -2
  784. package/dist/hooks/__tests__/autopilot-skill-contract.test.d.ts.map +0 -1
  785. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +0 -37
  786. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +0 -1
  787. package/dist/hooks/__tests__/clawhip-event-contract.test.d.ts +0 -2
  788. package/dist/hooks/__tests__/clawhip-event-contract.test.d.ts.map +0 -1
  789. package/dist/hooks/__tests__/clawhip-event-contract.test.js +0 -37
  790. package/dist/hooks/__tests__/clawhip-event-contract.test.js.map +0 -1
  791. package/dist/hooks/__tests__/code-review-skill-contract.test.d.ts +0 -2
  792. package/dist/hooks/__tests__/code-review-skill-contract.test.d.ts.map +0 -1
  793. package/dist/hooks/__tests__/code-review-skill-contract.test.js +0 -56
  794. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +0 -1
  795. package/dist/hooks/__tests__/codebase-map.test.d.ts +0 -8
  796. package/dist/hooks/__tests__/codebase-map.test.d.ts.map +0 -1
  797. package/dist/hooks/__tests__/codebase-map.test.js +0 -218
  798. package/dist/hooks/__tests__/codebase-map.test.js.map +0 -1
  799. package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts +0 -18
  800. package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts.map +0 -1
  801. package/dist/hooks/__tests__/consensus-execution-handoff.test.js +0 -234
  802. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +0 -1
  803. package/dist/hooks/__tests__/debugger-log-recency-contract.test.d.ts +0 -2
  804. package/dist/hooks/__tests__/debugger-log-recency-contract.test.d.ts.map +0 -1
  805. package/dist/hooks/__tests__/debugger-log-recency-contract.test.js +0 -20
  806. package/dist/hooks/__tests__/debugger-log-recency-contract.test.js.map +0 -1
  807. package/dist/hooks/__tests__/deep-interview-contract.test.d.ts +0 -2
  808. package/dist/hooks/__tests__/deep-interview-contract.test.d.ts.map +0 -1
  809. package/dist/hooks/__tests__/deep-interview-contract.test.js +0 -213
  810. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +0 -1
  811. package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.d.ts +0 -2
  812. package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.d.ts.map +0 -1
  813. package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.js +0 -43
  814. package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.js.map +0 -1
  815. package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.d.ts +0 -2
  816. package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.d.ts.map +0 -1
  817. package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.js +0 -38
  818. package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.js.map +0 -1
  819. package/dist/hooks/__tests__/explore-routing.test.d.ts +0 -2
  820. package/dist/hooks/__tests__/explore-routing.test.d.ts.map +0 -1
  821. package/dist/hooks/__tests__/explore-routing.test.js +0 -43
  822. package/dist/hooks/__tests__/explore-routing.test.js.map +0 -1
  823. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.d.ts +0 -2
  824. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.d.ts.map +0 -1
  825. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js +0 -69
  826. package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js.map +0 -1
  827. package/dist/hooks/__tests__/keyword-detector.test.d.ts +0 -2
  828. package/dist/hooks/__tests__/keyword-detector.test.d.ts.map +0 -1
  829. package/dist/hooks/__tests__/keyword-detector.test.js +0 -1716
  830. package/dist/hooks/__tests__/keyword-detector.test.js.map +0 -1
  831. package/dist/hooks/__tests__/notify-fallback-watcher.test.d.ts +0 -2
  832. package/dist/hooks/__tests__/notify-fallback-watcher.test.d.ts.map +0 -1
  833. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +0 -3898
  834. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +0 -1
  835. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.d.ts +0 -2
  836. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.d.ts.map +0 -1
  837. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +0 -786
  838. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +0 -1
  839. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.d.ts +0 -2
  840. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.d.ts.map +0 -1
  841. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +0 -2397
  842. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +0 -1
  843. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.d.ts +0 -2
  844. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.d.ts.map +0 -1
  845. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +0 -160
  846. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +0 -1
  847. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.d.ts +0 -2
  848. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.d.ts.map +0 -1
  849. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +0 -1178
  850. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +0 -1
  851. package/dist/hooks/__tests__/notify-hook-modules.test.d.ts +0 -9
  852. package/dist/hooks/__tests__/notify-hook-modules.test.d.ts.map +0 -1
  853. package/dist/hooks/__tests__/notify-hook-modules.test.js +0 -529
  854. package/dist/hooks/__tests__/notify-hook-modules.test.js.map +0 -1
  855. package/dist/hooks/__tests__/notify-hook-native-dispatch-contract.test.d.ts +0 -2
  856. package/dist/hooks/__tests__/notify-hook-native-dispatch-contract.test.d.ts.map +0 -1
  857. package/dist/hooks/__tests__/notify-hook-native-dispatch-contract.test.js +0 -14
  858. package/dist/hooks/__tests__/notify-hook-native-dispatch-contract.test.js.map +0 -1
  859. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.d.ts +0 -2
  860. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.d.ts.map +0 -1
  861. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +0 -682
  862. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +0 -1
  863. package/dist/hooks/__tests__/notify-hook-regression-205.test.d.ts +0 -9
  864. package/dist/hooks/__tests__/notify-hook-regression-205.test.d.ts.map +0 -1
  865. package/dist/hooks/__tests__/notify-hook-regression-205.test.js +0 -255
  866. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +0 -1
  867. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.d.ts +0 -2
  868. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.d.ts.map +0 -1
  869. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +0 -162
  870. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +0 -1
  871. package/dist/hooks/__tests__/notify-hook-session-scope.test.d.ts +0 -2
  872. package/dist/hooks/__tests__/notify-hook-session-scope.test.d.ts.map +0 -1
  873. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +0 -301
  874. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +0 -1
  875. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.d.ts +0 -2
  876. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.d.ts.map +0 -1
  877. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +0 -1510
  878. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +0 -1
  879. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.d.ts +0 -2
  880. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.d.ts.map +0 -1
  881. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +0 -2879
  882. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +0 -1
  883. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.d.ts +0 -2
  884. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.d.ts.map +0 -1
  885. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +0 -228
  886. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +0 -1
  887. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.d.ts +0 -2
  888. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.d.ts.map +0 -1
  889. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +0 -35
  890. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +0 -1
  891. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.d.ts +0 -2
  892. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.d.ts.map +0 -1
  893. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +0 -1589
  894. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +0 -1
  895. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.d.ts +0 -10
  896. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.d.ts.map +0 -1
  897. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +0 -0
  898. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +0 -1
  899. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.d.ts +0 -11
  900. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.d.ts.map +0 -1
  901. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js +0 -266
  902. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js.map +0 -1
  903. package/dist/hooks/__tests__/notify-hook-worker-idle.test.d.ts +0 -2
  904. package/dist/hooks/__tests__/notify-hook-worker-idle.test.d.ts.map +0 -1
  905. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +0 -895
  906. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +0 -1
  907. package/dist/hooks/__tests__/openclaw-setup-contract.test.d.ts +0 -2
  908. package/dist/hooks/__tests__/openclaw-setup-contract.test.d.ts.map +0 -1
  909. package/dist/hooks/__tests__/openclaw-setup-contract.test.js +0 -61
  910. package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +0 -1
  911. package/dist/hooks/__tests__/pre-context-gate-skills.test.d.ts +0 -2
  912. package/dist/hooks/__tests__/pre-context-gate-skills.test.d.ts.map +0 -1
  913. package/dist/hooks/__tests__/pre-context-gate-skills.test.js +0 -40
  914. package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +0 -1
  915. package/dist/hooks/__tests__/prompt-guidance-catalog.test.d.ts +0 -2
  916. package/dist/hooks/__tests__/prompt-guidance-catalog.test.d.ts.map +0 -1
  917. package/dist/hooks/__tests__/prompt-guidance-catalog.test.js +0 -11
  918. package/dist/hooks/__tests__/prompt-guidance-catalog.test.js.map +0 -1
  919. package/dist/hooks/__tests__/prompt-guidance-contract.test.d.ts +0 -2
  920. package/dist/hooks/__tests__/prompt-guidance-contract.test.d.ts.map +0 -1
  921. package/dist/hooks/__tests__/prompt-guidance-contract.test.js +0 -38
  922. package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +0 -1
  923. package/dist/hooks/__tests__/prompt-guidance-fragments.test.d.ts +0 -2
  924. package/dist/hooks/__tests__/prompt-guidance-fragments.test.d.ts.map +0 -1
  925. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js +0 -48
  926. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +0 -1
  927. package/dist/hooks/__tests__/prompt-guidance-scenarios.test.d.ts +0 -2
  928. package/dist/hooks/__tests__/prompt-guidance-scenarios.test.d.ts.map +0 -1
  929. package/dist/hooks/__tests__/prompt-guidance-scenarios.test.js +0 -11
  930. package/dist/hooks/__tests__/prompt-guidance-scenarios.test.js.map +0 -1
  931. package/dist/hooks/__tests__/prompt-guidance-test-helpers.d.ts +0 -5
  932. package/dist/hooks/__tests__/prompt-guidance-test-helpers.d.ts.map +0 -1
  933. package/dist/hooks/__tests__/prompt-guidance-test-helpers.js +0 -34
  934. package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +0 -1
  935. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.d.ts +0 -2
  936. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.d.ts.map +0 -1
  937. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +0 -65
  938. package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +0 -1
  939. package/dist/hooks/__tests__/prompt-orchestration-boundary.test.d.ts +0 -2
  940. package/dist/hooks/__tests__/prompt-orchestration-boundary.test.d.ts.map +0 -1
  941. package/dist/hooks/__tests__/prompt-orchestration-boundary.test.js +0 -38
  942. package/dist/hooks/__tests__/prompt-orchestration-boundary.test.js.map +0 -1
  943. package/dist/hooks/__tests__/prompt-refactor-contract.test.d.ts +0 -2
  944. package/dist/hooks/__tests__/prompt-refactor-contract.test.d.ts.map +0 -1
  945. package/dist/hooks/__tests__/prompt-refactor-contract.test.js +0 -22
  946. package/dist/hooks/__tests__/prompt-refactor-contract.test.js.map +0 -1
  947. package/dist/hooks/__tests__/prompt-team-routing.test.d.ts +0 -2
  948. package/dist/hooks/__tests__/prompt-team-routing.test.d.ts.map +0 -1
  949. package/dist/hooks/__tests__/prompt-team-routing.test.js +0 -49
  950. package/dist/hooks/__tests__/prompt-team-routing.test.js.map +0 -1
  951. package/dist/hooks/__tests__/session.test.d.ts +0 -2
  952. package/dist/hooks/__tests__/session.test.d.ts.map +0 -1
  953. package/dist/hooks/__tests__/session.test.js +0 -322
  954. package/dist/hooks/__tests__/session.test.js.map +0 -1
  955. package/dist/hooks/__tests__/skill-guidance-contract.test.d.ts +0 -2
  956. package/dist/hooks/__tests__/skill-guidance-contract.test.d.ts.map +0 -1
  957. package/dist/hooks/__tests__/skill-guidance-contract.test.js +0 -29
  958. package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +0 -1
  959. package/dist/hooks/__tests__/task-size-detector.test.d.ts +0 -2
  960. package/dist/hooks/__tests__/task-size-detector.test.d.ts.map +0 -1
  961. package/dist/hooks/__tests__/task-size-detector.test.js +0 -330
  962. package/dist/hooks/__tests__/task-size-detector.test.js.map +0 -1
  963. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.d.ts +0 -2
  964. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.d.ts.map +0 -1
  965. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js +0 -28
  966. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +0 -1
  967. package/dist/hooks/__tests__/tmux-hook-engine-types-sync.test.d.ts +0 -2
  968. package/dist/hooks/__tests__/tmux-hook-engine-types-sync.test.d.ts.map +0 -1
  969. package/dist/hooks/__tests__/tmux-hook-engine-types-sync.test.js +0 -24
  970. package/dist/hooks/__tests__/tmux-hook-engine-types-sync.test.js.map +0 -1
  971. package/dist/hooks/__tests__/tmux-hook-engine.test.d.ts +0 -2
  972. package/dist/hooks/__tests__/tmux-hook-engine.test.d.ts.map +0 -1
  973. package/dist/hooks/__tests__/tmux-hook-engine.test.js +0 -403
  974. package/dist/hooks/__tests__/tmux-hook-engine.test.js.map +0 -1
  975. package/dist/hooks/__tests__/triage-config.test.d.ts +0 -2
  976. package/dist/hooks/__tests__/triage-config.test.d.ts.map +0 -1
  977. package/dist/hooks/__tests__/triage-config.test.js +0 -211
  978. package/dist/hooks/__tests__/triage-config.test.js.map +0 -1
  979. package/dist/hooks/__tests__/triage-heuristic.test.d.ts +0 -2
  980. package/dist/hooks/__tests__/triage-heuristic.test.d.ts.map +0 -1
  981. package/dist/hooks/__tests__/triage-heuristic.test.js +0 -285
  982. package/dist/hooks/__tests__/triage-heuristic.test.js.map +0 -1
  983. package/dist/hooks/__tests__/triage-state.test.d.ts +0 -2
  984. package/dist/hooks/__tests__/triage-state.test.d.ts.map +0 -1
  985. package/dist/hooks/__tests__/triage-state.test.js +0 -426
  986. package/dist/hooks/__tests__/triage-state.test.js.map +0 -1
  987. package/dist/hooks/__tests__/visual-ralph-skill.test.d.ts +0 -2
  988. package/dist/hooks/__tests__/visual-ralph-skill.test.d.ts.map +0 -1
  989. package/dist/hooks/__tests__/visual-ralph-skill.test.js +0 -44
  990. package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +0 -1
  991. package/dist/hooks/__tests__/visual-verdict-loop.test.d.ts +0 -2
  992. package/dist/hooks/__tests__/visual-verdict-loop.test.d.ts.map +0 -1
  993. package/dist/hooks/__tests__/visual-verdict-loop.test.js +0 -35
  994. package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +0 -1
  995. package/dist/hooks/__tests__/wiki-docs-contract.test.d.ts +0 -2
  996. package/dist/hooks/__tests__/wiki-docs-contract.test.d.ts.map +0 -1
  997. package/dist/hooks/__tests__/wiki-docs-contract.test.js +0 -34
  998. package/dist/hooks/__tests__/wiki-docs-contract.test.js.map +0 -1
  999. package/dist/hooks/code-simplifier/__tests__/index.test.d.ts +0 -2
  1000. package/dist/hooks/code-simplifier/__tests__/index.test.d.ts.map +0 -1
  1001. package/dist/hooks/code-simplifier/__tests__/index.test.js +0 -187
  1002. package/dist/hooks/code-simplifier/__tests__/index.test.js.map +0 -1
  1003. package/dist/hooks/extensibility/__tests__/dispatcher.test.d.ts +0 -2
  1004. package/dist/hooks/extensibility/__tests__/dispatcher.test.d.ts.map +0 -1
  1005. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +0 -242
  1006. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +0 -1
  1007. package/dist/hooks/extensibility/__tests__/events.test.d.ts +0 -2
  1008. package/dist/hooks/extensibility/__tests__/events.test.d.ts.map +0 -1
  1009. package/dist/hooks/extensibility/__tests__/events.test.js +0 -125
  1010. package/dist/hooks/extensibility/__tests__/events.test.js.map +0 -1
  1011. package/dist/hooks/extensibility/__tests__/example-hook-plugins.test.d.ts +0 -2
  1012. package/dist/hooks/extensibility/__tests__/example-hook-plugins.test.d.ts.map +0 -1
  1013. package/dist/hooks/extensibility/__tests__/example-hook-plugins.test.js +0 -153
  1014. package/dist/hooks/extensibility/__tests__/example-hook-plugins.test.js.map +0 -1
  1015. package/dist/hooks/extensibility/__tests__/loader.test.d.ts +0 -2
  1016. package/dist/hooks/extensibility/__tests__/loader.test.d.ts.map +0 -1
  1017. package/dist/hooks/extensibility/__tests__/loader.test.js +0 -254
  1018. package/dist/hooks/extensibility/__tests__/loader.test.js.map +0 -1
  1019. package/dist/hooks/extensibility/__tests__/logging.test.d.ts +0 -2
  1020. package/dist/hooks/extensibility/__tests__/logging.test.d.ts.map +0 -1
  1021. package/dist/hooks/extensibility/__tests__/logging.test.js +0 -74
  1022. package/dist/hooks/extensibility/__tests__/logging.test.js.map +0 -1
  1023. package/dist/hooks/extensibility/__tests__/plugin-runner.test.d.ts +0 -2
  1024. package/dist/hooks/extensibility/__tests__/plugin-runner.test.d.ts.map +0 -1
  1025. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js +0 -202
  1026. package/dist/hooks/extensibility/__tests__/plugin-runner.test.js.map +0 -1
  1027. package/dist/hooks/extensibility/__tests__/runtime.test.d.ts +0 -2
  1028. package/dist/hooks/extensibility/__tests__/runtime.test.d.ts.map +0 -1
  1029. package/dist/hooks/extensibility/__tests__/runtime.test.js +0 -198
  1030. package/dist/hooks/extensibility/__tests__/runtime.test.js.map +0 -1
  1031. package/dist/hooks/extensibility/__tests__/sdk-public-surface.test.d.ts +0 -2
  1032. package/dist/hooks/extensibility/__tests__/sdk-public-surface.test.d.ts.map +0 -1
  1033. package/dist/hooks/extensibility/__tests__/sdk-public-surface.test.js +0 -32
  1034. package/dist/hooks/extensibility/__tests__/sdk-public-surface.test.js.map +0 -1
  1035. package/dist/hooks/extensibility/__tests__/sdk.test.d.ts +0 -2
  1036. package/dist/hooks/extensibility/__tests__/sdk.test.d.ts.map +0 -1
  1037. package/dist/hooks/extensibility/__tests__/sdk.test.js +0 -479
  1038. package/dist/hooks/extensibility/__tests__/sdk.test.js.map +0 -1
  1039. package/dist/hud/__tests__/authority.test.d.ts +0 -2
  1040. package/dist/hud/__tests__/authority.test.d.ts.map +0 -1
  1041. package/dist/hud/__tests__/authority.test.js +0 -56
  1042. package/dist/hud/__tests__/authority.test.js.map +0 -1
  1043. package/dist/hud/__tests__/colors.test.d.ts +0 -2
  1044. package/dist/hud/__tests__/colors.test.d.ts.map +0 -1
  1045. package/dist/hud/__tests__/colors.test.js +0 -92
  1046. package/dist/hud/__tests__/colors.test.js.map +0 -1
  1047. package/dist/hud/__tests__/hud-tmux-injection.test.d.ts +0 -10
  1048. package/dist/hud/__tests__/hud-tmux-injection.test.d.ts.map +0 -1
  1049. package/dist/hud/__tests__/hud-tmux-injection.test.js +0 -150
  1050. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +0 -1
  1051. package/dist/hud/__tests__/index.test.d.ts +0 -2
  1052. package/dist/hud/__tests__/index.test.d.ts.map +0 -1
  1053. package/dist/hud/__tests__/index.test.js +0 -180
  1054. package/dist/hud/__tests__/index.test.js.map +0 -1
  1055. package/dist/hud/__tests__/reconcile.test.d.ts +0 -2
  1056. package/dist/hud/__tests__/reconcile.test.d.ts.map +0 -1
  1057. package/dist/hud/__tests__/reconcile.test.js +0 -125
  1058. package/dist/hud/__tests__/reconcile.test.js.map +0 -1
  1059. package/dist/hud/__tests__/render.test.d.ts +0 -2
  1060. package/dist/hud/__tests__/render.test.d.ts.map +0 -1
  1061. package/dist/hud/__tests__/render.test.js +0 -573
  1062. package/dist/hud/__tests__/render.test.js.map +0 -1
  1063. package/dist/hud/__tests__/state.test.d.ts +0 -2
  1064. package/dist/hud/__tests__/state.test.d.ts.map +0 -1
  1065. package/dist/hud/__tests__/state.test.js +0 -618
  1066. package/dist/hud/__tests__/state.test.js.map +0 -1
  1067. package/dist/hud/__tests__/types.test.d.ts +0 -2
  1068. package/dist/hud/__tests__/types.test.d.ts.map +0 -1
  1069. package/dist/hud/__tests__/types.test.js +0 -79
  1070. package/dist/hud/__tests__/types.test.js.map +0 -1
  1071. package/dist/hud/__tests__/watch.test.d.ts +0 -2
  1072. package/dist/hud/__tests__/watch.test.d.ts.map +0 -1
  1073. package/dist/hud/__tests__/watch.test.js +0 -63
  1074. package/dist/hud/__tests__/watch.test.js.map +0 -1
  1075. package/dist/mcp/__tests__/bootstrap.test.d.ts +0 -2
  1076. package/dist/mcp/__tests__/bootstrap.test.d.ts.map +0 -1
  1077. package/dist/mcp/__tests__/bootstrap.test.js +0 -207
  1078. package/dist/mcp/__tests__/bootstrap.test.js.map +0 -1
  1079. package/dist/mcp/__tests__/code-intel-server.test.d.ts +0 -2
  1080. package/dist/mcp/__tests__/code-intel-server.test.d.ts.map +0 -1
  1081. package/dist/mcp/__tests__/code-intel-server.test.js +0 -70
  1082. package/dist/mcp/__tests__/code-intel-server.test.js.map +0 -1
  1083. package/dist/mcp/__tests__/memory-server.test.d.ts +0 -2
  1084. package/dist/mcp/__tests__/memory-server.test.d.ts.map +0 -1
  1085. package/dist/mcp/__tests__/memory-server.test.js +0 -36
  1086. package/dist/mcp/__tests__/memory-server.test.js.map +0 -1
  1087. package/dist/mcp/__tests__/memory-validation.test.d.ts +0 -2
  1088. package/dist/mcp/__tests__/memory-validation.test.d.ts.map +0 -1
  1089. package/dist/mcp/__tests__/memory-validation.test.js +0 -29
  1090. package/dist/mcp/__tests__/memory-validation.test.js.map +0 -1
  1091. package/dist/mcp/__tests__/path-traversal.test.d.ts +0 -2
  1092. package/dist/mcp/__tests__/path-traversal.test.d.ts.map +0 -1
  1093. package/dist/mcp/__tests__/path-traversal.test.js +0 -83
  1094. package/dist/mcp/__tests__/path-traversal.test.js.map +0 -1
  1095. package/dist/mcp/__tests__/server-lifecycle.test.d.ts +0 -2
  1096. package/dist/mcp/__tests__/server-lifecycle.test.d.ts.map +0 -1
  1097. package/dist/mcp/__tests__/server-lifecycle.test.js +0 -260
  1098. package/dist/mcp/__tests__/server-lifecycle.test.js.map +0 -1
  1099. package/dist/mcp/__tests__/state-paths.test.d.ts +0 -2
  1100. package/dist/mcp/__tests__/state-paths.test.d.ts.map +0 -1
  1101. package/dist/mcp/__tests__/state-paths.test.js +0 -209
  1102. package/dist/mcp/__tests__/state-paths.test.js.map +0 -1
  1103. package/dist/mcp/__tests__/state-server-ralph-phase.test.d.ts +0 -2
  1104. package/dist/mcp/__tests__/state-server-ralph-phase.test.d.ts.map +0 -1
  1105. package/dist/mcp/__tests__/state-server-ralph-phase.test.js +0 -109
  1106. package/dist/mcp/__tests__/state-server-ralph-phase.test.js.map +0 -1
  1107. package/dist/mcp/__tests__/state-server-schema.test.d.ts +0 -2
  1108. package/dist/mcp/__tests__/state-server-schema.test.d.ts.map +0 -1
  1109. package/dist/mcp/__tests__/state-server-schema.test.js +0 -29
  1110. package/dist/mcp/__tests__/state-server-schema.test.js.map +0 -1
  1111. package/dist/mcp/__tests__/state-server-team-tools.test.d.ts +0 -2
  1112. package/dist/mcp/__tests__/state-server-team-tools.test.d.ts.map +0 -1
  1113. package/dist/mcp/__tests__/state-server-team-tools.test.js +0 -35
  1114. package/dist/mcp/__tests__/state-server-team-tools.test.js.map +0 -1
  1115. package/dist/mcp/__tests__/state-server.test.d.ts +0 -2
  1116. package/dist/mcp/__tests__/state-server.test.d.ts.map +0 -1
  1117. package/dist/mcp/__tests__/state-server.test.js +0 -965
  1118. package/dist/mcp/__tests__/state-server.test.js.map +0 -1
  1119. package/dist/mcp/__tests__/trace-server.test.d.ts +0 -2
  1120. package/dist/mcp/__tests__/trace-server.test.d.ts.map +0 -1
  1121. package/dist/mcp/__tests__/trace-server.test.js +0 -119
  1122. package/dist/mcp/__tests__/trace-server.test.js.map +0 -1
  1123. package/dist/mcp/__tests__/wiki-server.test.d.ts +0 -2
  1124. package/dist/mcp/__tests__/wiki-server.test.d.ts.map +0 -1
  1125. package/dist/mcp/__tests__/wiki-server.test.js +0 -30
  1126. package/dist/mcp/__tests__/wiki-server.test.js.map +0 -1
  1127. package/dist/modes/__tests__/base-autoresearch-contract.test.d.ts +0 -2
  1128. package/dist/modes/__tests__/base-autoresearch-contract.test.d.ts.map +0 -1
  1129. package/dist/modes/__tests__/base-autoresearch-contract.test.js +0 -123
  1130. package/dist/modes/__tests__/base-autoresearch-contract.test.js.map +0 -1
  1131. package/dist/modes/__tests__/base-multi-state-compat.test.d.ts +0 -2
  1132. package/dist/modes/__tests__/base-multi-state-compat.test.d.ts.map +0 -1
  1133. package/dist/modes/__tests__/base-multi-state-compat.test.js +0 -38
  1134. package/dist/modes/__tests__/base-multi-state-compat.test.js.map +0 -1
  1135. package/dist/modes/__tests__/base-ralph-contract.test.d.ts +0 -2
  1136. package/dist/modes/__tests__/base-ralph-contract.test.d.ts.map +0 -1
  1137. package/dist/modes/__tests__/base-ralph-contract.test.js +0 -64
  1138. package/dist/modes/__tests__/base-ralph-contract.test.js.map +0 -1
  1139. package/dist/modes/__tests__/base-session-scope.test.d.ts +0 -2
  1140. package/dist/modes/__tests__/base-session-scope.test.d.ts.map +0 -1
  1141. package/dist/modes/__tests__/base-session-scope.test.js +0 -98
  1142. package/dist/modes/__tests__/base-session-scope.test.js.map +0 -1
  1143. package/dist/modes/__tests__/base-tmux-pane.test.d.ts +0 -2
  1144. package/dist/modes/__tests__/base-tmux-pane.test.d.ts.map +0 -1
  1145. package/dist/modes/__tests__/base-tmux-pane.test.js +0 -39
  1146. package/dist/modes/__tests__/base-tmux-pane.test.js.map +0 -1
  1147. package/dist/notifications/__tests__/config.test.d.ts +0 -2
  1148. package/dist/notifications/__tests__/config.test.d.ts.map +0 -1
  1149. package/dist/notifications/__tests__/config.test.js +0 -269
  1150. package/dist/notifications/__tests__/config.test.js.map +0 -1
  1151. package/dist/notifications/__tests__/custom-alias-enablement.test.d.ts +0 -2
  1152. package/dist/notifications/__tests__/custom-alias-enablement.test.d.ts.map +0 -1
  1153. package/dist/notifications/__tests__/custom-alias-enablement.test.js +0 -84
  1154. package/dist/notifications/__tests__/custom-alias-enablement.test.js.map +0 -1
  1155. package/dist/notifications/__tests__/dispatch-cooldown.test.d.ts +0 -5
  1156. package/dist/notifications/__tests__/dispatch-cooldown.test.d.ts.map +0 -1
  1157. package/dist/notifications/__tests__/dispatch-cooldown.test.js +0 -100
  1158. package/dist/notifications/__tests__/dispatch-cooldown.test.js.map +0 -1
  1159. package/dist/notifications/__tests__/dispatcher.test.d.ts +0 -2
  1160. package/dist/notifications/__tests__/dispatcher.test.d.ts.map +0 -1
  1161. package/dist/notifications/__tests__/dispatcher.test.js +0 -202
  1162. package/dist/notifications/__tests__/dispatcher.test.js.map +0 -1
  1163. package/dist/notifications/__tests__/formatter.test.d.ts +0 -2
  1164. package/dist/notifications/__tests__/formatter.test.d.ts.map +0 -1
  1165. package/dist/notifications/__tests__/formatter.test.js +0 -270
  1166. package/dist/notifications/__tests__/formatter.test.js.map +0 -1
  1167. package/dist/notifications/__tests__/hook-config.test.d.ts +0 -5
  1168. package/dist/notifications/__tests__/hook-config.test.d.ts.map +0 -1
  1169. package/dist/notifications/__tests__/hook-config.test.js +0 -139
  1170. package/dist/notifications/__tests__/hook-config.test.js.map +0 -1
  1171. package/dist/notifications/__tests__/idle-cooldown.test.d.ts +0 -5
  1172. package/dist/notifications/__tests__/idle-cooldown.test.d.ts.map +0 -1
  1173. package/dist/notifications/__tests__/idle-cooldown.test.js +0 -209
  1174. package/dist/notifications/__tests__/idle-cooldown.test.js.map +0 -1
  1175. package/dist/notifications/__tests__/index.test.d.ts +0 -2
  1176. package/dist/notifications/__tests__/index.test.d.ts.map +0 -1
  1177. package/dist/notifications/__tests__/index.test.js +0 -188
  1178. package/dist/notifications/__tests__/index.test.js.map +0 -1
  1179. package/dist/notifications/__tests__/lifecycle-dedupe.test.d.ts +0 -2
  1180. package/dist/notifications/__tests__/lifecycle-dedupe.test.d.ts.map +0 -1
  1181. package/dist/notifications/__tests__/lifecycle-dedupe.test.js +0 -86
  1182. package/dist/notifications/__tests__/lifecycle-dedupe.test.js.map +0 -1
  1183. package/dist/notifications/__tests__/notifier.test.d.ts +0 -2
  1184. package/dist/notifications/__tests__/notifier.test.d.ts.map +0 -1
  1185. package/dist/notifications/__tests__/notifier.test.js +0 -239
  1186. package/dist/notifications/__tests__/notifier.test.js.map +0 -1
  1187. package/dist/notifications/__tests__/profiles.test.d.ts +0 -2
  1188. package/dist/notifications/__tests__/profiles.test.d.ts.map +0 -1
  1189. package/dist/notifications/__tests__/profiles.test.js +0 -404
  1190. package/dist/notifications/__tests__/profiles.test.js.map +0 -1
  1191. package/dist/notifications/__tests__/reply-config.test.d.ts +0 -2
  1192. package/dist/notifications/__tests__/reply-config.test.d.ts.map +0 -1
  1193. package/dist/notifications/__tests__/reply-config.test.js +0 -79
  1194. package/dist/notifications/__tests__/reply-config.test.js.map +0 -1
  1195. package/dist/notifications/__tests__/reply-listener.test.d.ts +0 -2
  1196. package/dist/notifications/__tests__/reply-listener.test.d.ts.map +0 -1
  1197. package/dist/notifications/__tests__/reply-listener.test.js +0 -723
  1198. package/dist/notifications/__tests__/reply-listener.test.js.map +0 -1
  1199. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.d.ts +0 -2
  1200. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.d.ts.map +0 -1
  1201. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js +0 -93
  1202. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js.map +0 -1
  1203. package/dist/notifications/__tests__/session-registry.test.d.ts +0 -2
  1204. package/dist/notifications/__tests__/session-registry.test.d.ts.map +0 -1
  1205. package/dist/notifications/__tests__/session-registry.test.js +0 -234
  1206. package/dist/notifications/__tests__/session-registry.test.js.map +0 -1
  1207. package/dist/notifications/__tests__/session-status.test.d.ts +0 -2
  1208. package/dist/notifications/__tests__/session-status.test.d.ts.map +0 -1
  1209. package/dist/notifications/__tests__/session-status.test.js +0 -249
  1210. package/dist/notifications/__tests__/session-status.test.js.map +0 -1
  1211. package/dist/notifications/__tests__/temp-mode.test.d.ts +0 -2
  1212. package/dist/notifications/__tests__/temp-mode.test.d.ts.map +0 -1
  1213. package/dist/notifications/__tests__/temp-mode.test.js +0 -172
  1214. package/dist/notifications/__tests__/temp-mode.test.js.map +0 -1
  1215. package/dist/notifications/__tests__/template-engine.test.d.ts +0 -5
  1216. package/dist/notifications/__tests__/template-engine.test.d.ts.map +0 -1
  1217. package/dist/notifications/__tests__/template-engine.test.js +0 -158
  1218. package/dist/notifications/__tests__/template-engine.test.js.map +0 -1
  1219. package/dist/notifications/__tests__/tmux-detector.test.d.ts +0 -2
  1220. package/dist/notifications/__tests__/tmux-detector.test.d.ts.map +0 -1
  1221. package/dist/notifications/__tests__/tmux-detector.test.js +0 -208
  1222. package/dist/notifications/__tests__/tmux-detector.test.js.map +0 -1
  1223. package/dist/notifications/__tests__/tmux.test.d.ts +0 -2
  1224. package/dist/notifications/__tests__/tmux.test.d.ts.map +0 -1
  1225. package/dist/notifications/__tests__/tmux.test.js +0 -285
  1226. package/dist/notifications/__tests__/tmux.test.js.map +0 -1
  1227. package/dist/notifications/__tests__/verbosity.test.d.ts +0 -2
  1228. package/dist/notifications/__tests__/verbosity.test.d.ts.map +0 -1
  1229. package/dist/notifications/__tests__/verbosity.test.js +0 -237
  1230. package/dist/notifications/__tests__/verbosity.test.js.map +0 -1
  1231. package/dist/openclaw/__tests__/config.test.d.ts +0 -6
  1232. package/dist/openclaw/__tests__/config.test.d.ts.map +0 -1
  1233. package/dist/openclaw/__tests__/config.test.js +0 -344
  1234. package/dist/openclaw/__tests__/config.test.js.map +0 -1
  1235. package/dist/openclaw/__tests__/dispatcher.test.d.ts +0 -5
  1236. package/dist/openclaw/__tests__/dispatcher.test.d.ts.map +0 -1
  1237. package/dist/openclaw/__tests__/dispatcher.test.js +0 -169
  1238. package/dist/openclaw/__tests__/dispatcher.test.js.map +0 -1
  1239. package/dist/openclaw/__tests__/index.test.d.ts +0 -6
  1240. package/dist/openclaw/__tests__/index.test.d.ts.map +0 -1
  1241. package/dist/openclaw/__tests__/index.test.js +0 -382
  1242. package/dist/openclaw/__tests__/index.test.js.map +0 -1
  1243. package/dist/pipeline/__tests__/orchestrator.test.d.ts +0 -2
  1244. package/dist/pipeline/__tests__/orchestrator.test.d.ts.map +0 -1
  1245. package/dist/pipeline/__tests__/orchestrator.test.js +0 -505
  1246. package/dist/pipeline/__tests__/orchestrator.test.js.map +0 -1
  1247. package/dist/pipeline/__tests__/stages.test.d.ts +0 -2
  1248. package/dist/pipeline/__tests__/stages.test.d.ts.map +0 -1
  1249. package/dist/pipeline/__tests__/stages.test.js +0 -754
  1250. package/dist/pipeline/__tests__/stages.test.js.map +0 -1
  1251. package/dist/pipeline/stages/ralph-verify.d.ts +0 -53
  1252. package/dist/pipeline/stages/ralph-verify.d.ts.map +0 -1
  1253. package/dist/pipeline/stages/ralph-verify.js.map +0 -1
  1254. package/dist/pipeline/stages/ralplan.d.ts +0 -25
  1255. package/dist/pipeline/stages/ralplan.d.ts.map +0 -1
  1256. package/dist/pipeline/stages/ralplan.js.map +0 -1
  1257. package/dist/planning/__tests__/artifacts.test.d.ts +0 -2
  1258. package/dist/planning/__tests__/artifacts.test.d.ts.map +0 -1
  1259. package/dist/planning/__tests__/artifacts.test.js +0 -544
  1260. package/dist/planning/__tests__/artifacts.test.js.map +0 -1
  1261. package/dist/question/__tests__/client.test.d.ts +0 -2
  1262. package/dist/question/__tests__/client.test.d.ts.map +0 -1
  1263. package/dist/question/__tests__/client.test.js +0 -90
  1264. package/dist/question/__tests__/client.test.js.map +0 -1
  1265. package/dist/question/__tests__/deep-interview.test.d.ts +0 -2
  1266. package/dist/question/__tests__/deep-interview.test.d.ts.map +0 -1
  1267. package/dist/question/__tests__/deep-interview.test.js +0 -209
  1268. package/dist/question/__tests__/deep-interview.test.js.map +0 -1
  1269. package/dist/question/__tests__/policy.test.d.ts +0 -2
  1270. package/dist/question/__tests__/policy.test.d.ts.map +0 -1
  1271. package/dist/question/__tests__/policy.test.js +0 -107
  1272. package/dist/question/__tests__/policy.test.js.map +0 -1
  1273. package/dist/question/__tests__/renderer.test.d.ts +0 -2
  1274. package/dist/question/__tests__/renderer.test.d.ts.map +0 -1
  1275. package/dist/question/__tests__/renderer.test.js +0 -707
  1276. package/dist/question/__tests__/renderer.test.js.map +0 -1
  1277. package/dist/question/__tests__/state.test.d.ts +0 -2
  1278. package/dist/question/__tests__/state.test.d.ts.map +0 -1
  1279. package/dist/question/__tests__/state.test.js +0 -102
  1280. package/dist/question/__tests__/state.test.js.map +0 -1
  1281. package/dist/question/__tests__/types.test.d.ts +0 -2
  1282. package/dist/question/__tests__/types.test.d.ts.map +0 -1
  1283. package/dist/question/__tests__/types.test.js +0 -65
  1284. package/dist/question/__tests__/types.test.js.map +0 -1
  1285. package/dist/question/__tests__/ui.test.d.ts +0 -2
  1286. package/dist/question/__tests__/ui.test.d.ts.map +0 -1
  1287. package/dist/question/__tests__/ui.test.js +0 -446
  1288. package/dist/question/__tests__/ui.test.js.map +0 -1
  1289. package/dist/ralph/__tests__/persistence.test.d.ts +0 -2
  1290. package/dist/ralph/__tests__/persistence.test.d.ts.map +0 -1
  1291. package/dist/ralph/__tests__/persistence.test.js +0 -116
  1292. package/dist/ralph/__tests__/persistence.test.js.map +0 -1
  1293. package/dist/ralph/contract.d.ts +0 -17
  1294. package/dist/ralph/persistence.js.map +0 -1
  1295. package/dist/ralplan/__tests__/runtime.test.d.ts +0 -2
  1296. package/dist/ralplan/__tests__/runtime.test.d.ts.map +0 -1
  1297. package/dist/ralplan/__tests__/runtime.test.js +0 -165
  1298. package/dist/ralplan/__tests__/runtime.test.js.map +0 -1
  1299. package/dist/ralplan/runtime.d.ts +0 -52
  1300. package/dist/ralplan/runtime.d.ts.map +0 -1
  1301. package/dist/ralplan/runtime.js.map +0 -1
  1302. package/dist/runtime/__tests__/bridge.test.d.ts +0 -2
  1303. package/dist/runtime/__tests__/bridge.test.d.ts.map +0 -1
  1304. package/dist/runtime/__tests__/bridge.test.js +0 -194
  1305. package/dist/runtime/__tests__/bridge.test.js.map +0 -1
  1306. package/dist/runtime/__tests__/run-loop.test.d.ts +0 -2
  1307. package/dist/runtime/__tests__/run-loop.test.d.ts.map +0 -1
  1308. package/dist/runtime/__tests__/run-loop.test.js +0 -35
  1309. package/dist/runtime/__tests__/run-loop.test.js.map +0 -1
  1310. package/dist/runtime/__tests__/run-outcome.test.d.ts +0 -2
  1311. package/dist/runtime/__tests__/run-outcome.test.d.ts.map +0 -1
  1312. package/dist/runtime/__tests__/run-outcome.test.js +0 -102
  1313. package/dist/runtime/__tests__/run-outcome.test.js.map +0 -1
  1314. package/dist/runtime/__tests__/run-state.test.d.ts +0 -2
  1315. package/dist/runtime/__tests__/run-state.test.d.ts.map +0 -1
  1316. package/dist/runtime/__tests__/run-state.test.js +0 -37
  1317. package/dist/runtime/__tests__/run-state.test.js.map +0 -1
  1318. package/dist/scripts/__tests__/codex-native-hook.test.d.ts +0 -2
  1319. package/dist/scripts/__tests__/codex-native-hook.test.d.ts.map +0 -1
  1320. package/dist/scripts/__tests__/codex-native-hook.test.js +0 -6788
  1321. package/dist/scripts/__tests__/codex-native-hook.test.js.map +0 -1
  1322. package/dist/scripts/__tests__/generate-release-body.test.d.ts +0 -2
  1323. package/dist/scripts/__tests__/generate-release-body.test.d.ts.map +0 -1
  1324. package/dist/scripts/__tests__/generate-release-body.test.js +0 -233
  1325. package/dist/scripts/__tests__/generate-release-body.test.js.map +0 -1
  1326. package/dist/scripts/__tests__/hook-derived-watcher.test.d.ts +0 -2
  1327. package/dist/scripts/__tests__/hook-derived-watcher.test.d.ts.map +0 -1
  1328. package/dist/scripts/__tests__/hook-derived-watcher.test.js +0 -195
  1329. package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +0 -1
  1330. package/dist/scripts/__tests__/postinstall.test.d.ts +0 -2
  1331. package/dist/scripts/__tests__/postinstall.test.d.ts.map +0 -1
  1332. package/dist/scripts/__tests__/postinstall.test.js +0 -92
  1333. package/dist/scripts/__tests__/postinstall.test.js.map +0 -1
  1334. package/dist/scripts/__tests__/prompt-inventory.test.d.ts +0 -2
  1335. package/dist/scripts/__tests__/prompt-inventory.test.d.ts.map +0 -1
  1336. package/dist/scripts/__tests__/prompt-inventory.test.js +0 -56
  1337. package/dist/scripts/__tests__/prompt-inventory.test.js.map +0 -1
  1338. package/dist/scripts/__tests__/run-test-files.test.d.ts +0 -2
  1339. package/dist/scripts/__tests__/run-test-files.test.d.ts.map +0 -1
  1340. package/dist/scripts/__tests__/run-test-files.test.js +0 -62
  1341. package/dist/scripts/__tests__/run-test-files.test.js.map +0 -1
  1342. package/dist/scripts/__tests__/smoke-packed-install.test.d.ts +0 -2
  1343. package/dist/scripts/__tests__/smoke-packed-install.test.d.ts.map +0 -1
  1344. package/dist/scripts/__tests__/smoke-packed-install.test.js +0 -135
  1345. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +0 -1
  1346. package/dist/scripts/__tests__/test-reply-listener-live.test.d.ts +0 -2
  1347. package/dist/scripts/__tests__/test-reply-listener-live.test.d.ts.map +0 -1
  1348. package/dist/scripts/__tests__/test-reply-listener-live.test.js +0 -82
  1349. package/dist/scripts/__tests__/test-reply-listener-live.test.js.map +0 -1
  1350. package/dist/scripts/__tests__/verify-native-agents.test.d.ts +0 -2
  1351. package/dist/scripts/__tests__/verify-native-agents.test.d.ts.map +0 -1
  1352. package/dist/scripts/__tests__/verify-native-agents.test.js +0 -166
  1353. package/dist/scripts/__tests__/verify-native-agents.test.js.map +0 -1
  1354. package/dist/scripts/eval/eval-candidate-handoff.d.ts +0 -2
  1355. package/dist/scripts/eval/eval-candidate-handoff.d.ts.map +0 -1
  1356. package/dist/scripts/eval/eval-candidate-handoff.js +0 -11
  1357. package/dist/scripts/eval/eval-candidate-handoff.js.map +0 -1
  1358. package/dist/scripts/eval/eval-cli-discoverability.d.ts +0 -3
  1359. package/dist/scripts/eval/eval-cli-discoverability.d.ts.map +0 -1
  1360. package/dist/scripts/eval/eval-cli-discoverability.js +0 -37
  1361. package/dist/scripts/eval/eval-cli-discoverability.js.map +0 -1
  1362. package/dist/scripts/eval/eval-fresh-run-tagging.d.ts +0 -2
  1363. package/dist/scripts/eval/eval-fresh-run-tagging.d.ts.map +0 -1
  1364. package/dist/scripts/eval/eval-fresh-run-tagging.js +0 -11
  1365. package/dist/scripts/eval/eval-fresh-run-tagging.js.map +0 -1
  1366. package/dist/scripts/eval/eval-help-consistency.d.ts +0 -2
  1367. package/dist/scripts/eval/eval-help-consistency.d.ts.map +0 -1
  1368. package/dist/scripts/eval/eval-help-consistency.js +0 -12
  1369. package/dist/scripts/eval/eval-help-consistency.js.map +0 -1
  1370. package/dist/scripts/eval/eval-in-action-cat-shellout-demo.d.ts +0 -2
  1371. package/dist/scripts/eval/eval-in-action-cat-shellout-demo.d.ts.map +0 -1
  1372. package/dist/scripts/eval/eval-in-action-cat-shellout-demo.js +0 -31
  1373. package/dist/scripts/eval/eval-in-action-cat-shellout-demo.js.map +0 -1
  1374. package/dist/scripts/eval/eval-parity-smoke.d.ts +0 -2
  1375. package/dist/scripts/eval/eval-parity-smoke.d.ts.map +0 -1
  1376. package/dist/scripts/eval/eval-parity-smoke.js +0 -23
  1377. package/dist/scripts/eval/eval-parity-smoke.js.map +0 -1
  1378. package/dist/scripts/eval/eval-parity-sweep.d.ts +0 -2
  1379. package/dist/scripts/eval/eval-parity-sweep.d.ts.map +0 -1
  1380. package/dist/scripts/eval/eval-parity-sweep.js +0 -29
  1381. package/dist/scripts/eval/eval-parity-sweep.js.map +0 -1
  1382. package/dist/scripts/eval/eval-resume-dirty-guard.d.ts +0 -2
  1383. package/dist/scripts/eval/eval-resume-dirty-guard.d.ts.map +0 -1
  1384. package/dist/scripts/eval/eval-resume-dirty-guard.js +0 -11
  1385. package/dist/scripts/eval/eval-resume-dirty-guard.js.map +0 -1
  1386. package/dist/scripts/eval/eval-security-path-traversal.d.ts +0 -3
  1387. package/dist/scripts/eval/eval-security-path-traversal.d.ts.map +0 -1
  1388. package/dist/scripts/eval/eval-security-path-traversal.js +0 -35
  1389. package/dist/scripts/eval/eval-security-path-traversal.js.map +0 -1
  1390. package/dist/scripts/notify-hook/__tests__/operational-events.test.d.ts +0 -2
  1391. package/dist/scripts/notify-hook/__tests__/operational-events.test.d.ts.map +0 -1
  1392. package/dist/scripts/notify-hook/__tests__/operational-events.test.js +0 -24
  1393. package/dist/scripts/notify-hook/__tests__/operational-events.test.js.map +0 -1
  1394. package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.d.ts +0 -2
  1395. package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.d.ts.map +0 -1
  1396. package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.js +0 -153
  1397. package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.js.map +0 -1
  1398. package/dist/scripts/notify-hook/ralph-session-resume.d.ts +0 -22
  1399. package/dist/session-history/__tests__/search.test.d.ts +0 -2
  1400. package/dist/session-history/__tests__/search.test.d.ts.map +0 -1
  1401. package/dist/session-history/__tests__/search.test.js +0 -150
  1402. package/dist/session-history/__tests__/search.test.js.map +0 -1
  1403. package/dist/sidecar/__tests__/boundary.test.d.ts +0 -2
  1404. package/dist/sidecar/__tests__/boundary.test.d.ts.map +0 -1
  1405. package/dist/sidecar/__tests__/boundary.test.js +0 -48
  1406. package/dist/sidecar/__tests__/boundary.test.js.map +0 -1
  1407. package/dist/sidecar/__tests__/collector.test.d.ts +0 -2
  1408. package/dist/sidecar/__tests__/collector.test.d.ts.map +0 -1
  1409. package/dist/sidecar/__tests__/collector.test.js +0 -162
  1410. package/dist/sidecar/__tests__/collector.test.js.map +0 -1
  1411. package/dist/sidecar/__tests__/render.test.d.ts +0 -2
  1412. package/dist/sidecar/__tests__/render.test.d.ts.map +0 -1
  1413. package/dist/sidecar/__tests__/render.test.js +0 -67
  1414. package/dist/sidecar/__tests__/render.test.js.map +0 -1
  1415. package/dist/sidecar/__tests__/tmux.test.d.ts +0 -2
  1416. package/dist/sidecar/__tests__/tmux.test.d.ts.map +0 -1
  1417. package/dist/sidecar/__tests__/tmux.test.js +0 -30
  1418. package/dist/sidecar/__tests__/tmux.test.js.map +0 -1
  1419. package/dist/sidecar/__tests__/watch.test.d.ts +0 -2
  1420. package/dist/sidecar/__tests__/watch.test.d.ts.map +0 -1
  1421. package/dist/sidecar/__tests__/watch.test.js +0 -42
  1422. package/dist/sidecar/__tests__/watch.test.js.map +0 -1
  1423. package/dist/state/__tests__/mode-state-context.test.d.ts +0 -2
  1424. package/dist/state/__tests__/mode-state-context.test.d.ts.map +0 -1
  1425. package/dist/state/__tests__/mode-state-context.test.js +0 -35
  1426. package/dist/state/__tests__/mode-state-context.test.js.map +0 -1
  1427. package/dist/state/__tests__/operations-ralph-phase.test.d.ts +0 -2
  1428. package/dist/state/__tests__/operations-ralph-phase.test.d.ts.map +0 -1
  1429. package/dist/state/__tests__/operations-ralph-phase.test.js +0 -103
  1430. package/dist/state/__tests__/operations-ralph-phase.test.js.map +0 -1
  1431. package/dist/state/__tests__/operations.test.d.ts +0 -2
  1432. package/dist/state/__tests__/operations.test.d.ts.map +0 -1
  1433. package/dist/state/__tests__/operations.test.js +0 -439
  1434. package/dist/state/__tests__/operations.test.js.map +0 -1
  1435. package/dist/state/__tests__/path-traversal.test.d.ts +0 -2
  1436. package/dist/state/__tests__/path-traversal.test.d.ts.map +0 -1
  1437. package/dist/state/__tests__/path-traversal.test.js +0 -49
  1438. package/dist/state/__tests__/path-traversal.test.js.map +0 -1
  1439. package/dist/state/__tests__/skill-active.test.d.ts +0 -2
  1440. package/dist/state/__tests__/skill-active.test.d.ts.map +0 -1
  1441. package/dist/state/__tests__/skill-active.test.js +0 -160
  1442. package/dist/state/__tests__/skill-active.test.js.map +0 -1
  1443. package/dist/state/__tests__/workflow-transition.test.d.ts +0 -2
  1444. package/dist/state/__tests__/workflow-transition.test.d.ts.map +0 -1
  1445. package/dist/state/__tests__/workflow-transition.test.js +0 -77
  1446. package/dist/state/__tests__/workflow-transition.test.js.map +0 -1
  1447. package/dist/subagents/__tests__/tracker.test.d.ts +0 -2
  1448. package/dist/subagents/__tests__/tracker.test.d.ts.map +0 -1
  1449. package/dist/subagents/__tests__/tracker.test.js +0 -47
  1450. package/dist/subagents/__tests__/tracker.test.js.map +0 -1
  1451. package/dist/team/__tests__/allocation-policy.test.d.ts +0 -2
  1452. package/dist/team/__tests__/allocation-policy.test.d.ts.map +0 -1
  1453. package/dist/team/__tests__/allocation-policy.test.js +0 -111
  1454. package/dist/team/__tests__/allocation-policy.test.js.map +0 -1
  1455. package/dist/team/__tests__/api-interop.test.d.ts +0 -2
  1456. package/dist/team/__tests__/api-interop.test.d.ts.map +0 -1
  1457. package/dist/team/__tests__/api-interop.test.js +0 -2262
  1458. package/dist/team/__tests__/api-interop.test.js.map +0 -1
  1459. package/dist/team/__tests__/commit-hygiene.test.d.ts +0 -2
  1460. package/dist/team/__tests__/commit-hygiene.test.d.ts.map +0 -1
  1461. package/dist/team/__tests__/commit-hygiene.test.js +0 -93
  1462. package/dist/team/__tests__/commit-hygiene.test.js.map +0 -1
  1463. package/dist/team/__tests__/cross-rebase-smoke.test.d.ts +0 -2
  1464. package/dist/team/__tests__/cross-rebase-smoke.test.d.ts.map +0 -1
  1465. package/dist/team/__tests__/cross-rebase-smoke.test.js +0 -161
  1466. package/dist/team/__tests__/cross-rebase-smoke.test.js.map +0 -1
  1467. package/dist/team/__tests__/current-task-baseline.test.d.ts +0 -2
  1468. package/dist/team/__tests__/current-task-baseline.test.d.ts.map +0 -1
  1469. package/dist/team/__tests__/current-task-baseline.test.js +0 -87
  1470. package/dist/team/__tests__/current-task-baseline.test.js.map +0 -1
  1471. package/dist/team/__tests__/delegation-policy.test.d.ts +0 -2
  1472. package/dist/team/__tests__/delegation-policy.test.d.ts.map +0 -1
  1473. package/dist/team/__tests__/delegation-policy.test.js +0 -69
  1474. package/dist/team/__tests__/delegation-policy.test.js.map +0 -1
  1475. package/dist/team/__tests__/delivery-e2e-smoke.test.d.ts +0 -2
  1476. package/dist/team/__tests__/delivery-e2e-smoke.test.d.ts.map +0 -1
  1477. package/dist/team/__tests__/delivery-e2e-smoke.test.js +0 -679
  1478. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +0 -1
  1479. package/dist/team/__tests__/events.test.d.ts +0 -2
  1480. package/dist/team/__tests__/events.test.d.ts.map +0 -1
  1481. package/dist/team/__tests__/events.test.js +0 -313
  1482. package/dist/team/__tests__/events.test.js.map +0 -1
  1483. package/dist/team/__tests__/followup-planner.test.d.ts +0 -2
  1484. package/dist/team/__tests__/followup-planner.test.d.ts.map +0 -1
  1485. package/dist/team/__tests__/followup-planner.test.js +0 -84
  1486. package/dist/team/__tests__/followup-planner.test.js.map +0 -1
  1487. package/dist/team/__tests__/hardening-e2e.test.d.ts +0 -2
  1488. package/dist/team/__tests__/hardening-e2e.test.d.ts.map +0 -1
  1489. package/dist/team/__tests__/hardening-e2e.test.js +0 -98
  1490. package/dist/team/__tests__/hardening-e2e.test.js.map +0 -1
  1491. package/dist/team/__tests__/hook-primary-e2e-contract.test.d.ts +0 -2
  1492. package/dist/team/__tests__/hook-primary-e2e-contract.test.d.ts.map +0 -1
  1493. package/dist/team/__tests__/hook-primary-e2e-contract.test.js +0 -78
  1494. package/dist/team/__tests__/hook-primary-e2e-contract.test.js.map +0 -1
  1495. package/dist/team/__tests__/idle-nudge.test.d.ts +0 -2
  1496. package/dist/team/__tests__/idle-nudge.test.d.ts.map +0 -1
  1497. package/dist/team/__tests__/idle-nudge.test.js +0 -230
  1498. package/dist/team/__tests__/idle-nudge.test.js.map +0 -1
  1499. package/dist/team/__tests__/leader-activity.test.d.ts +0 -2
  1500. package/dist/team/__tests__/leader-activity.test.d.ts.map +0 -1
  1501. package/dist/team/__tests__/leader-activity.test.js +0 -261
  1502. package/dist/team/__tests__/leader-activity.test.js.map +0 -1
  1503. package/dist/team/__tests__/mcp-comm.test.d.ts +0 -2
  1504. package/dist/team/__tests__/mcp-comm.test.d.ts.map +0 -1
  1505. package/dist/team/__tests__/mcp-comm.test.js +0 -289
  1506. package/dist/team/__tests__/mcp-comm.test.js.map +0 -1
  1507. package/dist/team/__tests__/model-contract.test.d.ts +0 -2
  1508. package/dist/team/__tests__/model-contract.test.d.ts.map +0 -1
  1509. package/dist/team/__tests__/model-contract.test.js +0 -171
  1510. package/dist/team/__tests__/model-contract.test.js.map +0 -1
  1511. package/dist/team/__tests__/orchestrator.test.d.ts +0 -2
  1512. package/dist/team/__tests__/orchestrator.test.d.ts.map +0 -1
  1513. package/dist/team/__tests__/orchestrator.test.js +0 -111
  1514. package/dist/team/__tests__/orchestrator.test.js.map +0 -1
  1515. package/dist/team/__tests__/phase-controller.test.d.ts +0 -2
  1516. package/dist/team/__tests__/phase-controller.test.d.ts.map +0 -1
  1517. package/dist/team/__tests__/phase-controller.test.js +0 -50
  1518. package/dist/team/__tests__/phase-controller.test.js.map +0 -1
  1519. package/dist/team/__tests__/rebalance-policy.test.d.ts +0 -2
  1520. package/dist/team/__tests__/rebalance-policy.test.d.ts.map +0 -1
  1521. package/dist/team/__tests__/rebalance-policy.test.js +0 -168
  1522. package/dist/team/__tests__/rebalance-policy.test.js.map +0 -1
  1523. package/dist/team/__tests__/repo-aware-decomposition.test.d.ts +0 -2
  1524. package/dist/team/__tests__/repo-aware-decomposition.test.d.ts.map +0 -1
  1525. package/dist/team/__tests__/repo-aware-decomposition.test.js +0 -136
  1526. package/dist/team/__tests__/repo-aware-decomposition.test.js.map +0 -1
  1527. package/dist/team/__tests__/role-router.test.d.ts +0 -2
  1528. package/dist/team/__tests__/role-router.test.d.ts.map +0 -1
  1529. package/dist/team/__tests__/role-router.test.js +0 -263
  1530. package/dist/team/__tests__/role-router.test.js.map +0 -1
  1531. package/dist/team/__tests__/runtime-cli.test.d.ts +0 -2
  1532. package/dist/team/__tests__/runtime-cli.test.d.ts.map +0 -1
  1533. package/dist/team/__tests__/runtime-cli.test.js +0 -304
  1534. package/dist/team/__tests__/runtime-cli.test.js.map +0 -1
  1535. package/dist/team/__tests__/runtime.test.d.ts +0 -2
  1536. package/dist/team/__tests__/runtime.test.d.ts.map +0 -1
  1537. package/dist/team/__tests__/runtime.test.js +0 -5734
  1538. package/dist/team/__tests__/runtime.test.js.map +0 -1
  1539. package/dist/team/__tests__/scaling.test.d.ts +0 -2
  1540. package/dist/team/__tests__/scaling.test.d.ts.map +0 -1
  1541. package/dist/team/__tests__/scaling.test.js +0 -1005
  1542. package/dist/team/__tests__/scaling.test.js.map +0 -1
  1543. package/dist/team/__tests__/shutdown-fallback.test.d.ts +0 -2
  1544. package/dist/team/__tests__/shutdown-fallback.test.d.ts.map +0 -1
  1545. package/dist/team/__tests__/shutdown-fallback.test.js +0 -125
  1546. package/dist/team/__tests__/shutdown-fallback.test.js.map +0 -1
  1547. package/dist/team/__tests__/state-root.test.d.ts +0 -2
  1548. package/dist/team/__tests__/state-root.test.d.ts.map +0 -1
  1549. package/dist/team/__tests__/state-root.test.js +0 -195
  1550. package/dist/team/__tests__/state-root.test.js.map +0 -1
  1551. package/dist/team/__tests__/state.test.d.ts +0 -2
  1552. package/dist/team/__tests__/state.test.d.ts.map +0 -1
  1553. package/dist/team/__tests__/state.test.js +0 -1859
  1554. package/dist/team/__tests__/state.test.js.map +0 -1
  1555. package/dist/team/__tests__/team-identity.test.d.ts +0 -2
  1556. package/dist/team/__tests__/team-identity.test.d.ts.map +0 -1
  1557. package/dist/team/__tests__/team-identity.test.js +0 -166
  1558. package/dist/team/__tests__/team-identity.test.js.map +0 -1
  1559. package/dist/team/__tests__/team-ops-contract.test.d.ts +0 -2
  1560. package/dist/team/__tests__/team-ops-contract.test.d.ts.map +0 -1
  1561. package/dist/team/__tests__/team-ops-contract.test.js +0 -96
  1562. package/dist/team/__tests__/team-ops-contract.test.js.map +0 -1
  1563. package/dist/team/__tests__/tmux-claude-workers-demo.test.d.ts +0 -2
  1564. package/dist/team/__tests__/tmux-claude-workers-demo.test.d.ts.map +0 -1
  1565. package/dist/team/__tests__/tmux-claude-workers-demo.test.js +0 -191
  1566. package/dist/team/__tests__/tmux-claude-workers-demo.test.js.map +0 -1
  1567. package/dist/team/__tests__/tmux-session.test.d.ts +0 -2
  1568. package/dist/team/__tests__/tmux-session.test.d.ts.map +0 -1
  1569. package/dist/team/__tests__/tmux-session.test.js +0 -3785
  1570. package/dist/team/__tests__/tmux-session.test.js.map +0 -1
  1571. package/dist/team/__tests__/tmux-test-fixture.d.ts +0 -20
  1572. package/dist/team/__tests__/tmux-test-fixture.d.ts.map +0 -1
  1573. package/dist/team/__tests__/tmux-test-fixture.js +0 -152
  1574. package/dist/team/__tests__/tmux-test-fixture.js.map +0 -1
  1575. package/dist/team/__tests__/tmux-test-fixture.test.d.ts +0 -2
  1576. package/dist/team/__tests__/tmux-test-fixture.test.d.ts.map +0 -1
  1577. package/dist/team/__tests__/tmux-test-fixture.test.js +0 -113
  1578. package/dist/team/__tests__/tmux-test-fixture.test.js.map +0 -1
  1579. package/dist/team/__tests__/worker-bootstrap.test.d.ts +0 -2
  1580. package/dist/team/__tests__/worker-bootstrap.test.d.ts.map +0 -1
  1581. package/dist/team/__tests__/worker-bootstrap.test.js +0 -685
  1582. package/dist/team/__tests__/worker-bootstrap.test.js.map +0 -1
  1583. package/dist/team/__tests__/worker-runtime-identity.test.d.ts +0 -2
  1584. package/dist/team/__tests__/worker-runtime-identity.test.d.ts.map +0 -1
  1585. package/dist/team/__tests__/worker-runtime-identity.test.js +0 -250
  1586. package/dist/team/__tests__/worker-runtime-identity.test.js.map +0 -1
  1587. package/dist/team/__tests__/worktree.test.d.ts +0 -2
  1588. package/dist/team/__tests__/worktree.test.d.ts.map +0 -1
  1589. package/dist/team/__tests__/worktree.test.js +0 -317
  1590. package/dist/team/__tests__/worktree.test.js.map +0 -1
  1591. package/dist/utils/__tests__/agents-md.test.d.ts +0 -2
  1592. package/dist/utils/__tests__/agents-md.test.d.ts.map +0 -1
  1593. package/dist/utils/__tests__/agents-md.test.js +0 -52
  1594. package/dist/utils/__tests__/agents-md.test.js.map +0 -1
  1595. package/dist/utils/__tests__/agents-model-table.test.d.ts +0 -2
  1596. package/dist/utils/__tests__/agents-model-table.test.d.ts.map +0 -1
  1597. package/dist/utils/__tests__/agents-model-table.test.js +0 -104
  1598. package/dist/utils/__tests__/agents-model-table.test.js.map +0 -1
  1599. package/dist/utils/__tests__/dep-versions.test.d.ts +0 -2
  1600. package/dist/utils/__tests__/dep-versions.test.d.ts.map +0 -1
  1601. package/dist/utils/__tests__/dep-versions.test.js +0 -46
  1602. package/dist/utils/__tests__/dep-versions.test.js.map +0 -1
  1603. package/dist/utils/__tests__/package.test.d.ts +0 -2
  1604. package/dist/utils/__tests__/package.test.d.ts.map +0 -1
  1605. package/dist/utils/__tests__/package.test.js +0 -21
  1606. package/dist/utils/__tests__/package.test.js.map +0 -1
  1607. package/dist/utils/__tests__/paths.test.d.ts +0 -2
  1608. package/dist/utils/__tests__/paths.test.d.ts.map +0 -1
  1609. package/dist/utils/__tests__/paths.test.js +0 -541
  1610. package/dist/utils/__tests__/paths.test.js.map +0 -1
  1611. package/dist/utils/__tests__/platform-command.test.d.ts +0 -2
  1612. package/dist/utils/__tests__/platform-command.test.d.ts.map +0 -1
  1613. package/dist/utils/__tests__/platform-command.test.js +0 -410
  1614. package/dist/utils/__tests__/platform-command.test.js.map +0 -1
  1615. package/dist/utils/__tests__/repo-deps.test.d.ts +0 -2
  1616. package/dist/utils/__tests__/repo-deps.test.d.ts.map +0 -1
  1617. package/dist/utils/__tests__/repo-deps.test.js +0 -71
  1618. package/dist/utils/__tests__/repo-deps.test.js.map +0 -1
  1619. package/dist/verification/__tests__/ci-rust-gates.test.d.ts +0 -2
  1620. package/dist/verification/__tests__/ci-rust-gates.test.d.ts.map +0 -1
  1621. package/dist/verification/__tests__/ci-rust-gates.test.js +0 -89
  1622. package/dist/verification/__tests__/ci-rust-gates.test.js.map +0 -1
  1623. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.d.ts +0 -2
  1624. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.d.ts.map +0 -1
  1625. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +0 -54
  1626. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +0 -1
  1627. package/dist/verification/__tests__/explore-harness-release-workflow.test.d.ts +0 -2
  1628. package/dist/verification/__tests__/explore-harness-release-workflow.test.d.ts.map +0 -1
  1629. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +0 -73
  1630. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +0 -1
  1631. package/dist/verification/__tests__/native-release-manifest.test.d.ts +0 -2
  1632. package/dist/verification/__tests__/native-release-manifest.test.d.ts.map +0 -1
  1633. package/dist/verification/__tests__/native-release-manifest.test.js +0 -80
  1634. package/dist/verification/__tests__/native-release-manifest.test.js.map +0 -1
  1635. package/dist/verification/__tests__/pr-check-workflow.test.d.ts +0 -2
  1636. package/dist/verification/__tests__/pr-check-workflow.test.d.ts.map +0 -1
  1637. package/dist/verification/__tests__/pr-check-workflow.test.js +0 -27
  1638. package/dist/verification/__tests__/pr-check-workflow.test.js.map +0 -1
  1639. package/dist/verification/__tests__/ralph-persistence-gate.test.d.ts +0 -2
  1640. package/dist/verification/__tests__/ralph-persistence-gate.test.d.ts.map +0 -1
  1641. package/dist/verification/__tests__/ralph-persistence-gate.test.js +0 -55
  1642. package/dist/verification/__tests__/ralph-persistence-gate.test.js.map +0 -1
  1643. package/dist/verification/__tests__/rust-runtime-thin-adapter-gate.test.d.ts +0 -2
  1644. package/dist/verification/__tests__/rust-runtime-thin-adapter-gate.test.d.ts.map +0 -1
  1645. package/dist/verification/__tests__/rust-runtime-thin-adapter-gate.test.js +0 -32
  1646. package/dist/verification/__tests__/rust-runtime-thin-adapter-gate.test.js.map +0 -1
  1647. package/dist/verification/__tests__/verifier.test.d.ts +0 -2
  1648. package/dist/verification/__tests__/verifier.test.d.ts.map +0 -1
  1649. package/dist/verification/__tests__/verifier.test.js +0 -113
  1650. package/dist/verification/__tests__/verifier.test.js.map +0 -1
  1651. package/dist/visual/__tests__/verdict.test.d.ts +0 -2
  1652. package/dist/visual/__tests__/verdict.test.d.ts.map +0 -1
  1653. package/dist/visual/__tests__/verdict.test.js +0 -81
  1654. package/dist/visual/__tests__/verdict.test.js.map +0 -1
  1655. package/dist/wiki/__tests__/cjk-tokenize.test.d.ts +0 -12
  1656. package/dist/wiki/__tests__/cjk-tokenize.test.d.ts.map +0 -1
  1657. package/dist/wiki/__tests__/cjk-tokenize.test.js +0 -139
  1658. package/dist/wiki/__tests__/cjk-tokenize.test.js.map +0 -1
  1659. package/dist/wiki/__tests__/crlf-parse.test.d.ts +0 -2
  1660. package/dist/wiki/__tests__/crlf-parse.test.d.ts.map +0 -1
  1661. package/dist/wiki/__tests__/crlf-parse.test.js +0 -24
  1662. package/dist/wiki/__tests__/crlf-parse.test.js.map +0 -1
  1663. package/dist/wiki/__tests__/escape-newline.test.d.ts +0 -2
  1664. package/dist/wiki/__tests__/escape-newline.test.d.ts.map +0 -1
  1665. package/dist/wiki/__tests__/escape-newline.test.js +0 -45
  1666. package/dist/wiki/__tests__/escape-newline.test.js.map +0 -1
  1667. package/dist/wiki/__tests__/ingest.test.d.ts +0 -5
  1668. package/dist/wiki/__tests__/ingest.test.d.ts.map +0 -1
  1669. package/dist/wiki/__tests__/ingest.test.js +0 -181
  1670. package/dist/wiki/__tests__/ingest.test.js.map +0 -1
  1671. package/dist/wiki/__tests__/lint.test.d.ts +0 -5
  1672. package/dist/wiki/__tests__/lint.test.d.ts.map +0 -1
  1673. package/dist/wiki/__tests__/lint.test.js +0 -163
  1674. package/dist/wiki/__tests__/lint.test.js.map +0 -1
  1675. package/dist/wiki/__tests__/query.test.d.ts +0 -5
  1676. package/dist/wiki/__tests__/query.test.d.ts.map +0 -1
  1677. package/dist/wiki/__tests__/query.test.js +0 -141
  1678. package/dist/wiki/__tests__/query.test.js.map +0 -1
  1679. package/dist/wiki/__tests__/reserved-file-guard.test.d.ts +0 -2
  1680. package/dist/wiki/__tests__/reserved-file-guard.test.d.ts.map +0 -1
  1681. package/dist/wiki/__tests__/reserved-file-guard.test.js +0 -44
  1682. package/dist/wiki/__tests__/reserved-file-guard.test.js.map +0 -1
  1683. package/dist/wiki/__tests__/session-hooks.test.d.ts +0 -5
  1684. package/dist/wiki/__tests__/session-hooks.test.d.ts.map +0 -1
  1685. package/dist/wiki/__tests__/session-hooks.test.js +0 -36
  1686. package/dist/wiki/__tests__/session-hooks.test.js.map +0 -1
  1687. package/dist/wiki/__tests__/slug-nonascii.test.d.ts +0 -2
  1688. package/dist/wiki/__tests__/slug-nonascii.test.d.ts.map +0 -1
  1689. package/dist/wiki/__tests__/slug-nonascii.test.js +0 -30
  1690. package/dist/wiki/__tests__/slug-nonascii.test.js.map +0 -1
  1691. package/dist/wiki/__tests__/storage.test.d.ts +0 -5
  1692. package/dist/wiki/__tests__/storage.test.d.ts.map +0 -1
  1693. package/dist/wiki/__tests__/storage.test.js +0 -278
  1694. package/dist/wiki/__tests__/storage.test.js.map +0 -1
  1695. package/dist/wiki/__tests__/test-helpers.d.ts +0 -31
  1696. package/dist/wiki/__tests__/test-helpers.d.ts.map +0 -1
  1697. package/dist/wiki/__tests__/test-helpers.js +0 -108
  1698. package/dist/wiki/__tests__/test-helpers.js.map +0 -1
  1699. package/docs/contracts/ralph-cancel-contract.md +0 -23
  1700. package/docs/contracts/ralph-state-contract.md +0 -95
  1701. package/docs/issues/team-ralph-followup-team.md +0 -38
  1702. package/docs/qa/ralph-persistence-gate.md +0 -59
  1703. package/docs/reference/ralph-parity-matrix.md +0 -25
  1704. package/docs/reference/ralph-upstream-baseline.md +0 -34
  1705. package/plugins/roblox-ai-os-creator-skills/skills/ralph/SKILL.md +0 -269
  1706. package/plugins/roblox-ai-os-creator-skills/skills/ralplan/SKILL.md +0 -162
  1707. package/prompts/api-reviewer.md +0 -113
  1708. package/prompts/information-architect.md +0 -226
  1709. package/prompts/performance-reviewer.md +0 -109
  1710. package/prompts/product-analyst.md +0 -304
  1711. package/prompts/product-manager.md +0 -245
  1712. package/prompts/qa-tester.md +0 -124
  1713. package/prompts/quality-reviewer.md +0 -123
  1714. package/prompts/quality-strategist.md +0 -274
  1715. package/prompts/style-reviewer.md +0 -102
  1716. package/prompts/ux-researcher.md +0 -327
  1717. package/skills/frontend-ui-ux/SKILL.md +0 -34
  1718. package/skills/ralph/SKILL.md +0 -269
  1719. package/skills/ralplan/SKILL.md +0 -162
  1720. package/src/scripts/eval/eval-adaptive-sort-optimization.py +0 -24
  1721. package/src/scripts/eval/eval-candidate-handoff.ts +0 -8
  1722. package/src/scripts/eval/eval-cli-discoverability.ts +0 -40
  1723. package/src/scripts/eval/eval-fresh-run-tagging.ts +0 -8
  1724. package/src/scripts/eval/eval-help-consistency.ts +0 -11
  1725. package/src/scripts/eval/eval-in-action-cat-shellout-demo.ts +0 -31
  1726. package/src/scripts/eval/eval-ml-kaggle-model-optimization.py +0 -29
  1727. package/src/scripts/eval/eval-noisy-bayesopt-highdim.py +0 -44
  1728. package/src/scripts/eval/eval-noisy-latent-subspace-discovery.py +0 -44
  1729. package/src/scripts/eval/eval-parity-smoke.ts +0 -20
  1730. package/src/scripts/eval/eval-parity-sweep.ts +0 -26
  1731. package/src/scripts/eval/eval-resume-dirty-guard.ts +0 -8
  1732. package/src/scripts/eval/eval-security-path-traversal.ts +0 -38
  1733. package/src/scripts/run-autoresearch-showcase.sh +0 -75
  1734. /package/docs/{migration-mainline-post-v0.4.4.md → archive/migration-mainline-post-v0.4.4.md} +0 -0
  1735. /package/docs/{qa-plan-0.4.2.md → archive/qa-plan-0.4.2.md} +0 -0
  1736. /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