@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.
- package/README.md +142 -102
- package/dist/agents/definitions.d.ts.map +1 -1
- package/dist/agents/definitions.js +0 -101
- package/dist/agents/definitions.js.map +1 -1
- package/dist/blueprint/runtime.d.ts +52 -0
- package/dist/blueprint/runtime.d.ts.map +1 -0
- package/dist/{ralplan → blueprint}/runtime.js +19 -19
- package/dist/blueprint/runtime.js.map +1 -0
- package/dist/catalog/reader.d.ts.map +1 -1
- package/dist/catalog/reader.js +8 -2
- package/dist/catalog/reader.js.map +1 -1
- package/dist/catalog/schema.js +1 -1
- package/dist/catalog/schema.js.map +1 -1
- package/dist/cli/forge.d.ts +17 -0
- package/dist/cli/{ralph.d.ts.map → forge.d.ts.map} +1 -1
- package/dist/cli/{ralph.js → forge.js} +82 -82
- package/dist/cli/{ralph.js.map → forge.js.map} +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +15 -15
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +2 -3
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/star-prompt.js +2 -2
- package/dist/cli/star-prompt.js.map +1 -1
- package/dist/cli/state.js +1 -1
- package/dist/cli/team.d.ts.map +1 -1
- package/dist/cli/team.js +3 -2
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/tmux-hook.d.ts.map +1 -1
- package/dist/cli/tmux-hook.js +9 -1
- package/dist/cli/tmux-hook.js.map +1 -1
- package/dist/config/generator.d.ts +1 -1
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +1 -1
- package/dist/config/generator.js.map +1 -1
- package/dist/forge/contract.d.ts +17 -0
- package/dist/{ralph → forge}/contract.d.ts.map +1 -1
- package/dist/{ralph → forge}/contract.js +16 -16
- package/dist/{ralph → forge}/contract.js.map +1 -1
- package/dist/{ralph → forge}/persistence.d.ts +5 -5
- package/dist/{ralph → forge}/persistence.d.ts.map +1 -1
- package/dist/{ralph → forge}/persistence.js +7 -6
- package/dist/forge/persistence.js.map +1 -0
- package/dist/hooks/agents-overlay.d.ts +1 -1
- package/dist/hooks/agents-overlay.d.ts.map +1 -1
- package/dist/hooks/agents-overlay.js +37 -31
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js +82 -14
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +8 -8
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +94 -64
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +9 -11
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
- package/dist/hooks/prompt-guidance-contract.js +10 -21
- package/dist/hooks/prompt-guidance-contract.js.map +1 -1
- package/dist/hooks/task-size-detector.js +2 -2
- package/dist/hooks/task-size-detector.js.map +1 -1
- package/dist/hooks/triage-state.d.ts +1 -1
- package/dist/hooks/triage-state.js +1 -1
- package/dist/hud/colors.d.ts +2 -2
- package/dist/hud/colors.js +2 -2
- package/dist/hud/render.js +21 -21
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/state.d.ts +3 -3
- package/dist/hud/state.d.ts.map +1 -1
- package/dist/hud/state.js +18 -15
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/types.d.ts +6 -6
- package/dist/hud/types.d.ts.map +1 -1
- package/dist/mcp/bootstrap.d.ts.map +1 -1
- package/dist/mcp/bootstrap.js +36 -2
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +1 -0
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +4 -1
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts +4 -4
- package/dist/mcp/state-server.js +2 -2
- package/dist/mcp/state-server.js.map +1 -1
- package/dist/modes/base.d.ts +2 -2
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +29 -26
- package/dist/modes/base.js.map +1 -1
- package/dist/notifications/reply-listener.d.ts.map +1 -1
- package/dist/notifications/reply-listener.js +7 -1
- package/dist/notifications/reply-listener.js.map +1 -1
- package/dist/notifications/tmux.d.ts.map +1 -1
- package/dist/notifications/tmux.js +39 -6
- package/dist/notifications/tmux.js.map +1 -1
- package/dist/pipeline/index.d.ts +7 -6
- package/dist/pipeline/index.d.ts.map +1 -1
- package/dist/pipeline/index.js +5 -4
- package/dist/pipeline/index.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts +5 -5
- package/dist/pipeline/orchestrator.js +25 -25
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/stages/blueprint.d.ts +25 -0
- package/dist/pipeline/stages/blueprint.d.ts.map +1 -0
- package/dist/pipeline/stages/{ralplan.js → blueprint.js} +16 -16
- package/dist/pipeline/stages/blueprint.js.map +1 -0
- package/dist/pipeline/stages/code-review.d.ts +2 -2
- package/dist/pipeline/stages/code-review.js +6 -6
- package/dist/pipeline/stages/code-review.js.map +1 -1
- package/dist/pipeline/stages/forge-verify.d.ts +50 -0
- package/dist/pipeline/stages/forge-verify.d.ts.map +1 -0
- package/dist/pipeline/stages/{ralph-verify.js → forge-verify.js} +21 -24
- package/dist/pipeline/stages/forge-verify.js.map +1 -0
- package/dist/pipeline/stages/team-exec.d.ts +1 -1
- package/dist/pipeline/stages/team-exec.js +19 -19
- package/dist/pipeline/stages/team-exec.js.map +1 -1
- package/dist/pipeline/types.d.ts +12 -12
- package/dist/pipeline/types.d.ts.map +1 -1
- package/dist/pipeline/types.js +1 -1
- package/dist/planning/artifacts.d.ts +3 -4
- package/dist/planning/artifacts.d.ts.map +1 -1
- package/dist/planning/artifacts.js +2 -3
- package/dist/planning/artifacts.js.map +1 -1
- package/dist/question/policy.js +1 -1
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js +70 -13
- package/dist/runtime/bridge.js.map +1 -1
- package/dist/scripts/codex-native-hook.js +30 -30
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/eval/eval-cross-server-party-flow.d.ts +3 -0
- package/dist/scripts/eval/eval-cross-server-party-flow.d.ts.map +1 -0
- package/dist/scripts/eval/eval-cross-server-party-flow.js +12 -0
- package/dist/scripts/eval/eval-cross-server-party-flow.js.map +1 -0
- package/dist/scripts/eval/eval-gui-onboarding-clarity.d.ts +3 -0
- package/dist/scripts/eval/eval-gui-onboarding-clarity.d.ts.map +1 -0
- package/dist/scripts/eval/eval-gui-onboarding-clarity.js +17 -0
- package/dist/scripts/eval/eval-gui-onboarding-clarity.js.map +1 -0
- package/dist/scripts/eval/eval-liveops-reward-loop-balance.d.ts +3 -0
- package/dist/scripts/eval/eval-liveops-reward-loop-balance.d.ts.map +1 -0
- package/dist/scripts/eval/eval-liveops-reward-loop-balance.js +12 -0
- package/dist/scripts/eval/eval-liveops-reward-loop-balance.js.map +1 -0
- package/dist/scripts/eval/eval-profile-datastore-recovery.d.ts +3 -0
- package/dist/scripts/eval/eval-profile-datastore-recovery.d.ts.map +1 -0
- package/dist/scripts/eval/eval-profile-datastore-recovery.js +17 -0
- package/dist/scripts/eval/eval-profile-datastore-recovery.js.map +1 -0
- package/dist/scripts/eval/eval-remote-contract-hardening.d.ts +3 -0
- package/dist/scripts/eval/eval-remote-contract-hardening.d.ts.map +1 -0
- package/dist/scripts/eval/eval-remote-contract-hardening.js +17 -0
- package/dist/scripts/eval/eval-remote-contract-hardening.js.map +1 -0
- package/dist/scripts/notify-fallback-watcher.js +140 -139
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/forge-session-resume.d.ts +23 -0
- package/dist/scripts/notify-hook/{ralph-session-resume.d.ts.map → forge-session-resume.d.ts.map} +1 -1
- package/dist/scripts/notify-hook/{ralph-session-resume.js → forge-session-resume.js} +37 -36
- package/dist/scripts/notify-hook/{ralph-session-resume.js.map → forge-session-resume.js.map} +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +34 -4
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/visual-verdict.js +3 -3
- package/dist/scripts/notify-hook.js +9 -9
- package/dist/scripts/run-test-files.js +1 -1
- package/dist/scripts/run-test-files.js.map +1 -1
- package/dist/scripts/surface-taxonomy.d.ts +23 -0
- package/dist/scripts/surface-taxonomy.d.ts.map +1 -0
- package/dist/scripts/surface-taxonomy.js +271 -0
- package/dist/scripts/surface-taxonomy.js.map +1 -0
- package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +5 -4
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/tmux-hook-engine.d.ts +1 -1
- package/dist/scripts/tmux-hook-engine.d.ts.map +1 -1
- package/dist/scripts/tmux-hook-engine.js +29 -20
- package/dist/scripts/tmux-hook-engine.js.map +1 -1
- package/dist/state/operations.d.ts +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +18 -18
- package/dist/state/operations.js.map +1 -1
- package/dist/state/skill-active.d.ts +13 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +38 -17
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/workflow-transition.d.ts +6 -5
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js +27 -15
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/team/contracts.d.ts +1 -1
- package/dist/team/contracts.js +2 -2
- package/dist/team/followup-planner.d.ts +2 -2
- package/dist/team/followup-planner.d.ts.map +1 -1
- package/dist/team/followup-planner.js +16 -14
- package/dist/team/followup-planner.js.map +1 -1
- package/dist/team/idle-nudge.d.ts.map +1 -1
- package/dist/team/idle-nudge.js +3 -2
- package/dist/team/idle-nudge.js.map +1 -1
- package/dist/team/leader-activity.js +1 -1
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +4 -1
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/orchestrator.js +4 -4
- package/dist/team/orchestrator.js.map +1 -1
- package/dist/team/role-router.js +3 -3
- package/dist/team/role-router.js.map +1 -1
- package/dist/team/state/dispatch.d.ts.map +1 -1
- package/dist/team/state/dispatch.js +4 -1
- package/dist/team/state/dispatch.js.map +1 -1
- package/dist/team/tmux-session.d.ts +4 -0
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +42 -9
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worktree.d.ts +1 -1
- package/dist/utils/platform-command.d.ts.map +1 -1
- package/dist/utils/platform-command.js +9 -0
- package/dist/utils/platform-command.js.map +1 -1
- package/dist/verification/verifier.d.ts +1 -1
- package/dist/verification/verifier.js +2 -2
- package/docs/STATE_MODEL.md +24 -24
- package/docs/agents.html +8 -16
- package/docs/archive/README.md +15 -0
- package/docs/{prompt-migration-changelog.md → archive/prompt-migration-changelog.md} +0 -11
- package/docs/{release-body-0.9.0.md → archive/release-body-0.9.0.md} +6 -24
- package/docs/{release-body-0.9.1.md → archive/release-body-0.9.1.md} +3 -3
- package/docs/codex-native-hooks.md +4 -4
- package/docs/contracts/forge-cancel-contract.md +20 -0
- package/docs/contracts/forge-state-contract.md +52 -0
- package/docs/contracts/multi-state-transition-contract.md +5 -5
- package/docs/contracts/multi-state-transition-review.md +3 -3
- package/docs/contracts/repo-aware-team-dag-decomposition.md +1 -1
- package/docs/contracts/rust-runtime-thin-adapter-contract.md +1 -1
- package/docs/contracts/team-startup-dispatch-latency.md +1 -1
- package/docs/getting-started.html +6 -1
- package/docs/guidance-schema.md +6 -3
- package/docs/index.html +55 -4
- package/docs/integrations.html +4 -3
- package/docs/issues/team-forge-followup-team.md +38 -0
- package/docs/openclaw-integration.md +2 -2
- package/docs/prompt-guidance-contract.md +11 -11
- package/docs/prs/{dev-deprecate-team-ralph.md → dev-deprecate-team-forge.md} +27 -27
- package/docs/prs/{dev-fix-ralph-live-pane-invariant.md → dev-fix-forge-live-pane-invariant.md} +7 -7
- package/docs/prs/{dev-team-ralph-workflow-positioning.md → dev-team-forge-workflow-positioning.md} +7 -7
- package/docs/qa/forge-persistence-gate.md +20 -0
- package/docs/qa/rust-runtime-thin-adapter-gate.md +31 -40
- package/docs/readme/README.de.md +13 -0
- package/docs/readme/README.el.md +13 -0
- package/docs/readme/README.es.md +13 -0
- package/docs/readme/README.fr.md +13 -0
- package/docs/readme/README.it.md +13 -0
- package/docs/readme/README.ja.md +13 -0
- package/docs/readme/README.ko.md +13 -0
- package/docs/readme/README.pl.md +13 -0
- package/docs/readme/README.pt.md +13 -0
- package/docs/readme/README.ru.md +13 -0
- package/docs/readme/README.tr.md +13 -0
- package/docs/readme/README.uk.md +13 -0
- package/docs/readme/README.vi.md +13 -0
- package/docs/readme/README.zh-TW.md +13 -0
- package/docs/readme/README.zh.md +13 -0
- package/docs/readme/rcs-cover.svg +75 -0
- package/docs/reference/canonical-vocabulary.md +106 -0
- package/docs/reference/forge-parity-matrix.md +26 -0
- package/docs/reference/forge-upstream-baseline.md +32 -0
- package/docs/reference/rcs-config-schema-routing.md +5 -5
- package/docs/reference/roblox-pre-action-protocol.md +4 -0
- package/docs/reference/roblox-taxonomy-migration-plan.md +46 -0
- package/docs/reference/roblox-workspace-standard.md +83 -0
- package/docs/reference/robloxstudio-mcp-compatibility.md +117 -0
- package/docs/reference/semantic-design-system.md +110 -0
- package/docs/reference/surface-map.md +131 -0
- package/docs/reference/team-allocation-rebalance-policy.md +1 -1
- package/docs/release-notes-v0.1.0.md +1 -1
- package/docs/release-notes-v0.1.1.md +49 -0
- package/docs/reports/open-prs-dev-readiness-2026-04-09.md +2 -2
- package/docs/shared/agent-tiers.md +3 -3
- package/docs/skills.html +10 -12
- package/docs/troubleshooting.md +1 -1
- package/package.json +20 -13
- package/plugins/roblox-ai-os-creator-skills/.codex-plugin/plugin.json +1 -1
- package/plugins/roblox-ai-os-creator-skills/docs/reference/roblox-pre-action-protocol.md +4 -0
- package/plugins/roblox-ai-os-creator-skills/skills/ai-slop-cleaner/SKILL.md +14 -7
- package/plugins/roblox-ai-os-creator-skills/skills/analyze/SKILL.md +9 -2
- package/plugins/roblox-ai-os-creator-skills/skills/ask-claude/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/ask-gemini/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/autoforge/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/autopilot/SKILL.md +48 -41
- package/plugins/roblox-ai-os-creator-skills/skills/autoresearch/SKILL.md +8 -1
- package/plugins/roblox-ai-os-creator-skills/skills/blueprint/SKILL.md +227 -9
- package/plugins/roblox-ai-os-creator-skills/skills/blueprint-loop/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/blueprint-psych/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/blueprint-retention/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/blueprint-social/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/brief/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/brief-audience/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/brief-motivation/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/cancel/SKILL.md +59 -52
- package/plugins/roblox-ai-os-creator-skills/skills/code-review/SKILL.md +30 -24
- package/plugins/roblox-ai-os-creator-skills/skills/configure-notifications/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/crew/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/deep-interview/SKILL.md +25 -18
- package/plugins/roblox-ai-os-creator-skills/skills/doctor/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/forge/SKILL.md +174 -11
- package/plugins/roblox-ai-os-creator-skills/skills/forge-community/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/forge-daily-loop/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/forge-event-loop/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/forge-fomo/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/forge-mastery/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/forge-progression/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/forge-reward-loop/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/forge-status/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/help/SKILL.md +8 -1
- package/plugins/roblox-ai-os-creator-skills/skills/hud/SKILL.md +16 -9
- package/plugins/roblox-ai-os-creator-skills/skills/note/SKILL.md +8 -1
- package/plugins/roblox-ai-os-creator-skills/skills/pipeline/SKILL.md +18 -11
- package/plugins/roblox-ai-os-creator-skills/skills/plan/SKILL.md +36 -29
- package/plugins/roblox-ai-os-creator-skills/skills/rcs-setup/SKILL.md +8 -1
- package/plugins/roblox-ai-os-creator-skills/skills/security-review/SKILL.md +120 -236
- package/plugins/roblox-ai-os-creator-skills/skills/skill/SKILL.md +20 -13
- package/plugins/roblox-ai-os-creator-skills/skills/team/SKILL.md +17 -11
- package/plugins/roblox-ai-os-creator-skills/skills/trace/SKILL.md +7 -0
- package/plugins/roblox-ai-os-creator-skills/skills/ultraqa/SKILL.md +10 -3
- package/plugins/roblox-ai-os-creator-skills/skills/ultrawork/SKILL.md +19 -12
- package/plugins/roblox-ai-os-creator-skills/skills/{visual-ralph → visual-forge}/SKILL.md +36 -27
- package/plugins/roblox-ai-os-creator-skills/skills/visual-verdict/SKILL.md +9 -2
- package/plugins/roblox-ai-os-creator-skills/skills/wiki/SKILL.md +10 -3
- package/plugins/roblox-ai-os-creator-skills/skills/worker/SKILL.md +16 -7
- package/plugins/roblox-ai-os-creator-skills/templates/roblox/pre-action-plan.md +1 -0
- package/prompts/analyst.md +7 -0
- package/prompts/architect.md +11 -4
- package/prompts/build-fixer.md +7 -0
- package/prompts/code-reviewer.md +9 -2
- package/prompts/code-simplifier.md +4 -0
- package/prompts/critic.md +13 -6
- package/prompts/debugger.md +8 -1
- package/prompts/dependency-expert.md +8 -1
- package/prompts/designer.md +20 -10
- package/prompts/executor.md +7 -0
- package/prompts/explore-harness.md +7 -0
- package/prompts/explore.md +7 -0
- package/prompts/git-master.md +8 -1
- package/prompts/planner.md +10 -3
- package/prompts/researcher.md +7 -0
- package/prompts/security-reviewer.md +76 -92
- package/prompts/sisyphus-lite.md +7 -0
- package/prompts/team-executor.md +7 -0
- package/prompts/team-orchestrator.md +9 -2
- package/prompts/test-engineer.md +11 -3
- package/prompts/verifier.md +7 -0
- package/prompts/vision.md +9 -2
- package/prompts/writer.md +11 -4
- package/skills/.agents/skills/roblox-animations/SKILL.md +220 -0
- package/skills/.agents/skills/roblox-datastores/SKILL.md +219 -0
- package/skills/.agents/skills/roblox-gui/SKILL.md +192 -0
- package/skills/.agents/skills/roblox-monetization/SKILL.md +208 -0
- package/skills/.agents/skills/roblox-performance/SKILL.md +230 -0
- package/skills/.agents/skills/roblox-remote-events/SKILL.md +199 -0
- package/skills/.agents/skills/roblox-security/SKILL.md +236 -0
- package/skills/ai-slop-cleaner/SKILL.md +14 -7
- package/skills/analyze/SKILL.md +9 -2
- package/skills/ask-claude/SKILL.md +7 -0
- package/skills/ask-gemini/SKILL.md +7 -0
- package/skills/autoforge/SKILL.md +7 -0
- package/skills/autopilot/SKILL.md +48 -41
- package/skills/autoresearch/SKILL.md +8 -1
- package/skills/blueprint/SKILL.md +227 -9
- package/skills/blueprint-loop/SKILL.md +7 -0
- package/skills/blueprint-psych/SKILL.md +7 -0
- package/skills/blueprint-retention/SKILL.md +7 -0
- package/skills/blueprint-social/SKILL.md +7 -0
- package/skills/brief/SKILL.md +7 -0
- package/skills/brief-audience/SKILL.md +7 -0
- package/skills/brief-motivation/SKILL.md +7 -0
- package/skills/build-fix/SKILL.md +9 -2
- package/skills/cancel/SKILL.md +59 -52
- package/skills/code-review/SKILL.md +30 -24
- package/skills/configure-notifications/SKILL.md +7 -0
- package/skills/crew/SKILL.md +7 -0
- package/skills/deep-interview/SKILL.md +25 -18
- package/skills/deepsearch/SKILL.md +7 -0
- package/skills/doctor/SKILL.md +7 -0
- package/skills/ecomode/SKILL.md +9 -2
- package/skills/forge/SKILL.md +174 -11
- package/skills/forge-community/SKILL.md +7 -0
- package/skills/forge-daily-loop/SKILL.md +7 -0
- package/skills/forge-event-loop/SKILL.md +7 -0
- package/skills/forge-fomo/SKILL.md +7 -0
- package/skills/{ralph-init → forge-init}/SKILL.md +20 -13
- package/skills/forge-mastery/SKILL.md +7 -0
- package/skills/forge-progression/SKILL.md +7 -0
- package/skills/forge-reward-loop/SKILL.md +7 -0
- package/skills/forge-status/SKILL.md +7 -0
- package/skills/git-master/SKILL.md +7 -0
- package/skills/help/SKILL.md +8 -1
- package/skills/hud/SKILL.md +16 -9
- package/skills/note/SKILL.md +8 -1
- package/skills/pipeline/SKILL.md +18 -11
- package/skills/plan/SKILL.md +36 -29
- package/skills/rcs-setup/SKILL.md +8 -1
- package/skills/review/SKILL.md +7 -0
- package/skills/security-review/SKILL.md +120 -236
- package/skills/skill/SKILL.md +20 -13
- package/skills/skills-lock.json +47 -0
- package/skills/swarm/SKILL.md +8 -1
- package/skills/tdd/SKILL.md +7 -0
- package/skills/team/SKILL.md +17 -11
- package/skills/trace/SKILL.md +7 -0
- package/skills/ultraqa/SKILL.md +10 -3
- package/skills/ultrawork/SKILL.md +19 -12
- package/skills/{visual-ralph → visual-forge}/SKILL.md +36 -27
- package/skills/visual-verdict/SKILL.md +9 -2
- package/skills/web-clone/SKILL.md +14 -7
- package/skills/wiki/SKILL.md +10 -3
- package/skills/worker/SKILL.md +16 -7
- package/src/scripts/__tests__/codex-native-hook.test.ts +386 -319
- package/src/scripts/__tests__/run-test-files.test.ts +6 -4
- package/src/scripts/__tests__/verify-native-agents.test.ts +16 -16
- package/src/scripts/codex-native-hook.ts +34 -34
- package/src/scripts/eval/eval-cross-server-party-flow.ts +14 -0
- package/src/scripts/eval/eval-gui-onboarding-clarity.ts +20 -0
- package/src/scripts/eval/eval-liveops-reward-loop-balance.ts +14 -0
- package/src/scripts/eval/eval-profile-datastore-recovery.ts +20 -0
- package/src/scripts/eval/eval-remote-contract-hardening.ts +20 -0
- package/src/scripts/notify-fallback-watcher.ts +147 -146
- package/src/scripts/notify-hook/__tests__/team-worker-posttooluse.test.ts +24 -10
- package/src/scripts/notify-hook/{ralph-session-resume.ts → forge-session-resume.ts} +45 -43
- package/src/scripts/notify-hook/team-dispatch.ts +31 -4
- package/src/scripts/notify-hook/visual-verdict.ts +3 -3
- package/src/scripts/notify-hook.ts +10 -10
- package/src/scripts/run-test-files.ts +1 -1
- package/src/scripts/surface-taxonomy.ts +316 -0
- package/src/scripts/sync-plugin-mirror.ts +5 -4
- package/src/scripts/tmux-hook-engine.ts +31 -19
- package/templates/AGENTS.md +24 -15
- package/templates/catalog-manifest.json +5 -88
- package/templates/roblox/pre-action-plan.md +1 -0
- package/templates/roblox/robloxstudio-mcp.codex.json +18 -0
- package/templates/roblox/robloxstudio-mcp.windows.json +22 -0
- package/dist/adapt/__tests__/foundation.test.d.ts +0 -2
- package/dist/adapt/__tests__/foundation.test.d.ts.map +0 -1
- package/dist/adapt/__tests__/foundation.test.js +0 -171
- package/dist/adapt/__tests__/foundation.test.js.map +0 -1
- package/dist/adapt/__tests__/hermes.test.d.ts +0 -2
- package/dist/adapt/__tests__/hermes.test.d.ts.map +0 -1
- package/dist/adapt/__tests__/hermes.test.js +0 -137
- package/dist/adapt/__tests__/hermes.test.js.map +0 -1
- package/dist/agents/__tests__/definitions.test.d.ts +0 -2
- package/dist/agents/__tests__/definitions.test.d.ts.map +0 -1
- package/dist/agents/__tests__/definitions.test.js +0 -62
- package/dist/agents/__tests__/definitions.test.js.map +0 -1
- package/dist/agents/__tests__/native-config.test.d.ts +0 -2
- package/dist/agents/__tests__/native-config.test.d.ts.map +0 -1
- package/dist/agents/__tests__/native-config.test.js +0 -278
- package/dist/agents/__tests__/native-config.test.js.map +0 -1
- package/dist/autoresearch/__tests__/contracts.test.d.ts +0 -2
- package/dist/autoresearch/__tests__/contracts.test.d.ts.map +0 -1
- package/dist/autoresearch/__tests__/contracts.test.js +0 -127
- package/dist/autoresearch/__tests__/contracts.test.js.map +0 -1
- package/dist/autoresearch/__tests__/runtime-parity-extra.test.d.ts +0 -2
- package/dist/autoresearch/__tests__/runtime-parity-extra.test.d.ts.map +0 -1
- package/dist/autoresearch/__tests__/runtime-parity-extra.test.js +0 -356
- package/dist/autoresearch/__tests__/runtime-parity-extra.test.js.map +0 -1
- package/dist/autoresearch/__tests__/runtime.test.d.ts +0 -2
- package/dist/autoresearch/__tests__/runtime.test.d.ts.map +0 -1
- package/dist/autoresearch/__tests__/runtime.test.js +0 -218
- package/dist/autoresearch/__tests__/runtime.test.js.map +0 -1
- package/dist/autoresearch/__tests__/skill-validation.test.d.ts +0 -2
- package/dist/autoresearch/__tests__/skill-validation.test.d.ts.map +0 -1
- package/dist/autoresearch/__tests__/skill-validation.test.js +0 -91
- package/dist/autoresearch/__tests__/skill-validation.test.js.map +0 -1
- package/dist/catalog/__tests__/generator.test.d.ts +0 -2
- package/dist/catalog/__tests__/generator.test.d.ts.map +0 -1
- package/dist/catalog/__tests__/generator.test.js +0 -49
- package/dist/catalog/__tests__/generator.test.js.map +0 -1
- package/dist/catalog/__tests__/plugin-bundle-ssot.test.d.ts +0 -2
- package/dist/catalog/__tests__/plugin-bundle-ssot.test.d.ts.map +0 -1
- package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +0 -83
- package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +0 -1
- package/dist/catalog/__tests__/schema.test.d.ts +0 -2
- package/dist/catalog/__tests__/schema.test.d.ts.map +0 -1
- package/dist/catalog/__tests__/schema.test.js +0 -91
- package/dist/catalog/__tests__/schema.test.js.map +0 -1
- package/dist/cli/__tests__/adapt-help.test.d.ts +0 -2
- package/dist/cli/__tests__/adapt-help.test.d.ts.map +0 -1
- package/dist/cli/__tests__/adapt-help.test.js +0 -37
- package/dist/cli/__tests__/adapt-help.test.js.map +0 -1
- package/dist/cli/__tests__/adapt.test.d.ts +0 -2
- package/dist/cli/__tests__/adapt.test.d.ts.map +0 -1
- package/dist/cli/__tests__/adapt.test.js +0 -62
- package/dist/cli/__tests__/adapt.test.js.map +0 -1
- package/dist/cli/__tests__/agents-init.test.d.ts +0 -2
- package/dist/cli/__tests__/agents-init.test.d.ts.map +0 -1
- package/dist/cli/__tests__/agents-init.test.js +0 -184
- package/dist/cli/__tests__/agents-init.test.js.map +0 -1
- package/dist/cli/__tests__/agents.test.d.ts +0 -2
- package/dist/cli/__tests__/agents.test.d.ts.map +0 -1
- package/dist/cli/__tests__/agents.test.js +0 -137
- package/dist/cli/__tests__/agents.test.js.map +0 -1
- package/dist/cli/__tests__/ask.test.d.ts +0 -2
- package/dist/cli/__tests__/ask.test.d.ts.map +0 -1
- package/dist/cli/__tests__/ask.test.js +0 -265
- package/dist/cli/__tests__/ask.test.js.map +0 -1
- package/dist/cli/__tests__/autoresearch-guided.test.d.ts +0 -2
- package/dist/cli/__tests__/autoresearch-guided.test.d.ts.map +0 -1
- package/dist/cli/__tests__/autoresearch-guided.test.js +0 -365
- package/dist/cli/__tests__/autoresearch-guided.test.js.map +0 -1
- package/dist/cli/__tests__/autoresearch.test.d.ts +0 -2
- package/dist/cli/__tests__/autoresearch.test.d.ts.map +0 -1
- package/dist/cli/__tests__/autoresearch.test.js +0 -203
- package/dist/cli/__tests__/autoresearch.test.js.map +0 -1
- package/dist/cli/__tests__/catalog-contract.test.d.ts +0 -2
- package/dist/cli/__tests__/catalog-contract.test.d.ts.map +0 -1
- package/dist/cli/__tests__/catalog-contract.test.js +0 -18
- package/dist/cli/__tests__/catalog-contract.test.js.map +0 -1
- package/dist/cli/__tests__/cleanup.test.d.ts +0 -2
- package/dist/cli/__tests__/cleanup.test.d.ts.map +0 -1
- package/dist/cli/__tests__/cleanup.test.js +0 -419
- package/dist/cli/__tests__/cleanup.test.js.map +0 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.d.ts +0 -2
- package/dist/cli/__tests__/codex-plugin-layout.test.d.ts.map +0 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +0 -210
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +0 -1
- package/dist/cli/__tests__/doctor-context-window-warning.test.d.ts +0 -2
- package/dist/cli/__tests__/doctor-context-window-warning.test.d.ts.map +0 -1
- package/dist/cli/__tests__/doctor-context-window-warning.test.js +0 -122
- package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +0 -1
- package/dist/cli/__tests__/doctor-invalid-config.test.d.ts +0 -2
- package/dist/cli/__tests__/doctor-invalid-config.test.d.ts.map +0 -1
- package/dist/cli/__tests__/doctor-invalid-config.test.js +0 -52
- package/dist/cli/__tests__/doctor-invalid-config.test.js.map +0 -1
- package/dist/cli/__tests__/doctor-team.test.d.ts +0 -2
- package/dist/cli/__tests__/doctor-team.test.d.ts.map +0 -1
- package/dist/cli/__tests__/doctor-team.test.js +0 -299
- package/dist/cli/__tests__/doctor-team.test.js.map +0 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.d.ts +0 -2
- package/dist/cli/__tests__/doctor-warning-copy.test.d.ts.map +0 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +0 -438
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +0 -1
- package/dist/cli/__tests__/error-handling-warnings.test.d.ts +0 -2
- package/dist/cli/__tests__/error-handling-warnings.test.d.ts.map +0 -1
- package/dist/cli/__tests__/error-handling-warnings.test.js +0 -52
- package/dist/cli/__tests__/error-handling-warnings.test.js.map +0 -1
- package/dist/cli/__tests__/exec.test.d.ts +0 -2
- package/dist/cli/__tests__/exec.test.d.ts.map +0 -1
- package/dist/cli/__tests__/exec.test.js +0 -213
- package/dist/cli/__tests__/exec.test.js.map +0 -1
- package/dist/cli/__tests__/explore-windows-diagnostics.test.d.ts +0 -2
- package/dist/cli/__tests__/explore-windows-diagnostics.test.d.ts.map +0 -1
- package/dist/cli/__tests__/explore-windows-diagnostics.test.js +0 -17
- package/dist/cli/__tests__/explore-windows-diagnostics.test.js.map +0 -1
- package/dist/cli/__tests__/explore.test.d.ts +0 -2
- package/dist/cli/__tests__/explore.test.d.ts.map +0 -1
- package/dist/cli/__tests__/explore.test.js +0 -1090
- package/dist/cli/__tests__/explore.test.js.map +0 -1
- package/dist/cli/__tests__/hooks.test.d.ts +0 -2
- package/dist/cli/__tests__/hooks.test.d.ts.map +0 -1
- package/dist/cli/__tests__/hooks.test.js +0 -55
- package/dist/cli/__tests__/hooks.test.js.map +0 -1
- package/dist/cli/__tests__/index.test.d.ts +0 -2
- package/dist/cli/__tests__/index.test.d.ts.map +0 -1
- package/dist/cli/__tests__/index.test.js +0 -2259
- package/dist/cli/__tests__/index.test.js.map +0 -1
- package/dist/cli/__tests__/launch-fallback.test.d.ts +0 -2
- package/dist/cli/__tests__/launch-fallback.test.d.ts.map +0 -1
- package/dist/cli/__tests__/launch-fallback.test.js +0 -661
- package/dist/cli/__tests__/launch-fallback.test.js.map +0 -1
- package/dist/cli/__tests__/lifecycle-notifications.test.d.ts +0 -2
- package/dist/cli/__tests__/lifecycle-notifications.test.d.ts.map +0 -1
- package/dist/cli/__tests__/lifecycle-notifications.test.js +0 -48
- package/dist/cli/__tests__/lifecycle-notifications.test.js.map +0 -1
- package/dist/cli/__tests__/list.test.d.ts +0 -2
- package/dist/cli/__tests__/list.test.d.ts.map +0 -1
- package/dist/cli/__tests__/list.test.js +0 -38
- package/dist/cli/__tests__/list.test.js.map +0 -1
- package/dist/cli/__tests__/mcp-parity.test.d.ts +0 -2
- package/dist/cli/__tests__/mcp-parity.test.d.ts.map +0 -1
- package/dist/cli/__tests__/mcp-parity.test.js +0 -228
- package/dist/cli/__tests__/mcp-parity.test.js.map +0 -1
- package/dist/cli/__tests__/mcp-serve.test.d.ts +0 -2
- package/dist/cli/__tests__/mcp-serve.test.d.ts.map +0 -1
- package/dist/cli/__tests__/mcp-serve.test.js +0 -64
- package/dist/cli/__tests__/mcp-serve.test.js.map +0 -1
- package/dist/cli/__tests__/native-assets.test.d.ts +0 -2
- package/dist/cli/__tests__/native-assets.test.d.ts.map +0 -1
- package/dist/cli/__tests__/native-assets.test.js +0 -308
- package/dist/cli/__tests__/native-assets.test.js.map +0 -1
- package/dist/cli/__tests__/native-hook-dispatch-contract.test.d.ts +0 -2
- package/dist/cli/__tests__/native-hook-dispatch-contract.test.d.ts.map +0 -1
- package/dist/cli/__tests__/native-hook-dispatch-contract.test.js +0 -11
- package/dist/cli/__tests__/native-hook-dispatch-contract.test.js.map +0 -1
- package/dist/cli/__tests__/nested-help-routing.test.d.ts +0 -2
- package/dist/cli/__tests__/nested-help-routing.test.d.ts.map +0 -1
- package/dist/cli/__tests__/nested-help-routing.test.js +0 -96
- package/dist/cli/__tests__/nested-help-routing.test.js.map +0 -1
- package/dist/cli/__tests__/package-bin-contract.test.d.ts +0 -2
- package/dist/cli/__tests__/package-bin-contract.test.d.ts.map +0 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +0 -177
- package/dist/cli/__tests__/package-bin-contract.test.js.map +0 -1
- package/dist/cli/__tests__/packaged-explore-harness-lock.d.ts +0 -3
- package/dist/cli/__tests__/packaged-explore-harness-lock.d.ts.map +0 -1
- package/dist/cli/__tests__/packaged-explore-harness-lock.js +0 -67
- package/dist/cli/__tests__/packaged-explore-harness-lock.js.map +0 -1
- package/dist/cli/__tests__/packaged-script-resolution.test.d.ts +0 -2
- package/dist/cli/__tests__/packaged-script-resolution.test.d.ts.map +0 -1
- package/dist/cli/__tests__/packaged-script-resolution.test.js +0 -19
- package/dist/cli/__tests__/packaged-script-resolution.test.js.map +0 -1
- package/dist/cli/__tests__/prompt-skill-sanitization.test.d.ts +0 -2
- package/dist/cli/__tests__/prompt-skill-sanitization.test.d.ts.map +0 -1
- package/dist/cli/__tests__/prompt-skill-sanitization.test.js +0 -48
- package/dist/cli/__tests__/prompt-skill-sanitization.test.js.map +0 -1
- package/dist/cli/__tests__/question.test.d.ts +0 -2
- package/dist/cli/__tests__/question.test.d.ts.map +0 -1
- package/dist/cli/__tests__/question.test.js +0 -633
- package/dist/cli/__tests__/question.test.js.map +0 -1
- package/dist/cli/__tests__/ralph-deslop-contract.test.d.ts +0 -2
- package/dist/cli/__tests__/ralph-deslop-contract.test.d.ts.map +0 -1
- package/dist/cli/__tests__/ralph-deslop-contract.test.js +0 -28
- package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +0 -1
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.d.ts +0 -2
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.d.ts.map +0 -1
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.js +0 -24
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +0 -1
- package/dist/cli/__tests__/ralph-prd-smoke.test.d.ts +0 -2
- package/dist/cli/__tests__/ralph-prd-smoke.test.d.ts.map +0 -1
- package/dist/cli/__tests__/ralph-prd-smoke.test.js +0 -167
- package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +0 -1
- package/dist/cli/__tests__/ralph.test.d.ts +0 -2
- package/dist/cli/__tests__/ralph.test.d.ts.map +0 -1
- package/dist/cli/__tests__/ralph.test.js +0 -256
- package/dist/cli/__tests__/ralph.test.js.map +0 -1
- package/dist/cli/__tests__/resume.test.d.ts +0 -2
- package/dist/cli/__tests__/resume.test.d.ts.map +0 -1
- package/dist/cli/__tests__/resume.test.js +0 -84
- package/dist/cli/__tests__/resume.test.js.map +0 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.d.ts +0 -2
- package/dist/cli/__tests__/session-scoped-runtime.test.d.ts.map +0 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +0 -146
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +0 -1
- package/dist/cli/__tests__/session-search-help.test.d.ts +0 -2
- package/dist/cli/__tests__/session-search-help.test.d.ts.map +0 -1
- package/dist/cli/__tests__/session-search-help.test.js +0 -76
- package/dist/cli/__tests__/session-search-help.test.js.map +0 -1
- package/dist/cli/__tests__/session-search.test.d.ts +0 -2
- package/dist/cli/__tests__/session-search.test.d.ts.map +0 -1
- package/dist/cli/__tests__/session-search.test.js +0 -77
- package/dist/cli/__tests__/session-search.test.js.map +0 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.d.ts +0 -2
- package/dist/cli/__tests__/setup-agents-overwrite.test.d.ts.map +0 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +0 -457
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +0 -1
- package/dist/cli/__tests__/setup-gh-star.test.d.ts +0 -2
- package/dist/cli/__tests__/setup-gh-star.test.d.ts.map +0 -1
- package/dist/cli/__tests__/setup-gh-star.test.js +0 -67
- package/dist/cli/__tests__/setup-gh-star.test.js.map +0 -1
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.d.ts +0 -2
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.d.ts.map +0 -1
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +0 -189
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +0 -1
- package/dist/cli/__tests__/setup-install-mode.test.d.ts +0 -2
- package/dist/cli/__tests__/setup-install-mode.test.d.ts.map +0 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +0 -873
- package/dist/cli/__tests__/setup-install-mode.test.js.map +0 -1
- package/dist/cli/__tests__/setup-prompts-overwrite.test.d.ts +0 -2
- package/dist/cli/__tests__/setup-prompts-overwrite.test.d.ts.map +0 -1
- package/dist/cli/__tests__/setup-prompts-overwrite.test.js +0 -191
- package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +0 -1
- package/dist/cli/__tests__/setup-refresh.test.d.ts +0 -2
- package/dist/cli/__tests__/setup-refresh.test.d.ts.map +0 -1
- package/dist/cli/__tests__/setup-refresh.test.js +0 -591
- package/dist/cli/__tests__/setup-refresh.test.js.map +0 -1
- package/dist/cli/__tests__/setup-scope.test.d.ts +0 -2
- package/dist/cli/__tests__/setup-scope.test.d.ts.map +0 -1
- package/dist/cli/__tests__/setup-scope.test.js +0 -340
- package/dist/cli/__tests__/setup-scope.test.js.map +0 -1
- package/dist/cli/__tests__/setup-skill-validation.test.d.ts +0 -2
- package/dist/cli/__tests__/setup-skill-validation.test.d.ts.map +0 -1
- package/dist/cli/__tests__/setup-skill-validation.test.js +0 -44
- package/dist/cli/__tests__/setup-skill-validation.test.js.map +0 -1
- package/dist/cli/__tests__/setup-skills-overwrite.test.d.ts +0 -2
- package/dist/cli/__tests__/setup-skills-overwrite.test.d.ts.map +0 -1
- package/dist/cli/__tests__/setup-skills-overwrite.test.js +0 -295
- package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +0 -1
- package/dist/cli/__tests__/sidecar.test.d.ts +0 -2
- package/dist/cli/__tests__/sidecar.test.d.ts.map +0 -1
- package/dist/cli/__tests__/sidecar.test.js +0 -24
- package/dist/cli/__tests__/sidecar.test.js.map +0 -1
- package/dist/cli/__tests__/sparkshell-cli.test.d.ts +0 -2
- package/dist/cli/__tests__/sparkshell-cli.test.d.ts.map +0 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +0 -400
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +0 -1
- package/dist/cli/__tests__/sparkshell-packaging.test.d.ts +0 -2
- package/dist/cli/__tests__/sparkshell-packaging.test.d.ts.map +0 -1
- package/dist/cli/__tests__/sparkshell-packaging.test.js +0 -74
- package/dist/cli/__tests__/sparkshell-packaging.test.js.map +0 -1
- package/dist/cli/__tests__/star-prompt.test.d.ts +0 -2
- package/dist/cli/__tests__/star-prompt.test.d.ts.map +0 -1
- package/dist/cli/__tests__/star-prompt.test.js +0 -172
- package/dist/cli/__tests__/star-prompt.test.js.map +0 -1
- package/dist/cli/__tests__/state.test.d.ts +0 -2
- package/dist/cli/__tests__/state.test.d.ts.map +0 -1
- package/dist/cli/__tests__/state.test.js +0 -46
- package/dist/cli/__tests__/state.test.js.map +0 -1
- package/dist/cli/__tests__/team-decompose.test.d.ts +0 -2
- package/dist/cli/__tests__/team-decompose.test.d.ts.map +0 -1
- package/dist/cli/__tests__/team-decompose.test.js +0 -133
- package/dist/cli/__tests__/team-decompose.test.js.map +0 -1
- package/dist/cli/__tests__/team.test.d.ts +0 -2
- package/dist/cli/__tests__/team.test.d.ts.map +0 -1
- package/dist/cli/__tests__/team.test.js +0 -1820
- package/dist/cli/__tests__/team.test.js.map +0 -1
- package/dist/cli/__tests__/uninstall.test.d.ts +0 -2
- package/dist/cli/__tests__/uninstall.test.d.ts.map +0 -1
- package/dist/cli/__tests__/uninstall.test.js +0 -766
- package/dist/cli/__tests__/uninstall.test.js.map +0 -1
- package/dist/cli/__tests__/update.test.d.ts +0 -2
- package/dist/cli/__tests__/update.test.d.ts.map +0 -1
- package/dist/cli/__tests__/update.test.js +0 -589
- package/dist/cli/__tests__/update.test.js.map +0 -1
- package/dist/cli/__tests__/version-sync-contract.test.d.ts +0 -2
- package/dist/cli/__tests__/version-sync-contract.test.d.ts.map +0 -1
- package/dist/cli/__tests__/version-sync-contract.test.js +0 -41
- package/dist/cli/__tests__/version-sync-contract.test.js.map +0 -1
- package/dist/cli/__tests__/version.test.d.ts +0 -2
- package/dist/cli/__tests__/version.test.d.ts.map +0 -1
- package/dist/cli/__tests__/version.test.js +0 -21
- package/dist/cli/__tests__/version.test.js.map +0 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.d.ts +0 -2
- package/dist/cli/__tests__/windows-popup-loop-contract.test.d.ts.map +0 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +0 -31
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +0 -1
- package/dist/cli/ralph.d.ts +0 -17
- package/dist/compat/__tests__/doctor-contract.test.d.ts +0 -2
- package/dist/compat/__tests__/doctor-contract.test.d.ts.map +0 -1
- package/dist/compat/__tests__/doctor-contract.test.js +0 -108
- package/dist/compat/__tests__/doctor-contract.test.js.map +0 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.d.ts +0 -2
- package/dist/compat/__tests__/rust-runtime-compat.test.d.ts.map +0 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.js +0 -218
- package/dist/compat/__tests__/rust-runtime-compat.test.js.map +0 -1
- package/dist/config/__tests__/codex-hooks.test.d.ts +0 -2
- package/dist/config/__tests__/codex-hooks.test.d.ts.map +0 -1
- package/dist/config/__tests__/codex-hooks.test.js +0 -77
- package/dist/config/__tests__/codex-hooks.test.js.map +0 -1
- package/dist/config/__tests__/generator-idempotent.test.d.ts +0 -2
- package/dist/config/__tests__/generator-idempotent.test.d.ts.map +0 -1
- package/dist/config/__tests__/generator-idempotent.test.js +0 -882
- package/dist/config/__tests__/generator-idempotent.test.js.map +0 -1
- package/dist/config/__tests__/generator-notify.test.d.ts +0 -2
- package/dist/config/__tests__/generator-notify.test.d.ts.map +0 -1
- package/dist/config/__tests__/generator-notify.test.js +0 -343
- package/dist/config/__tests__/generator-notify.test.js.map +0 -1
- package/dist/config/__tests__/generator-status-line-presets.test.d.ts +0 -2
- package/dist/config/__tests__/generator-status-line-presets.test.d.ts.map +0 -1
- package/dist/config/__tests__/generator-status-line-presets.test.js +0 -203
- package/dist/config/__tests__/generator-status-line-presets.test.js.map +0 -1
- package/dist/config/__tests__/mcp-registry.test.d.ts +0 -2
- package/dist/config/__tests__/mcp-registry.test.d.ts.map +0 -1
- package/dist/config/__tests__/mcp-registry.test.js +0 -190
- package/dist/config/__tests__/mcp-registry.test.js.map +0 -1
- package/dist/config/__tests__/models.test.d.ts +0 -2
- package/dist/config/__tests__/models.test.d.ts.map +0 -1
- package/dist/config/__tests__/models.test.js +0 -224
- package/dist/config/__tests__/models.test.js.map +0 -1
- package/dist/config/__tests__/wiki-config-contract.test.d.ts +0 -2
- package/dist/config/__tests__/wiki-config-contract.test.d.ts.map +0 -1
- package/dist/config/__tests__/wiki-config-contract.test.js +0 -19
- package/dist/config/__tests__/wiki-config-contract.test.js.map +0 -1
- package/dist/document-refresh/__tests__/enforcer.test.d.ts +0 -2
- package/dist/document-refresh/__tests__/enforcer.test.d.ts.map +0 -1
- package/dist/document-refresh/__tests__/enforcer.test.js +0 -128
- package/dist/document-refresh/__tests__/enforcer.test.js.map +0 -1
- package/dist/hooks/__tests__/agents-overlay.test.d.ts +0 -8
- package/dist/hooks/__tests__/agents-overlay.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +0 -644
- package/dist/hooks/__tests__/agents-overlay.test.js.map +0 -1
- package/dist/hooks/__tests__/analyze-routing-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/analyze-routing-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/analyze-routing-contract.test.js +0 -45
- package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +0 -1
- package/dist/hooks/__tests__/analyze-skill-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/analyze-skill-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/analyze-skill-contract.test.js +0 -48
- package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +0 -1
- package/dist/hooks/__tests__/anti-slop-workflow.test.d.ts +0 -2
- package/dist/hooks/__tests__/anti-slop-workflow.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/anti-slop-workflow.test.js +0 -146
- package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +0 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/autopilot-skill-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +0 -37
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +0 -1
- package/dist/hooks/__tests__/clawhip-event-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/clawhip-event-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/clawhip-event-contract.test.js +0 -37
- package/dist/hooks/__tests__/clawhip-event-contract.test.js.map +0 -1
- package/dist/hooks/__tests__/code-review-skill-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/code-review-skill-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/code-review-skill-contract.test.js +0 -56
- package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +0 -1
- package/dist/hooks/__tests__/codebase-map.test.d.ts +0 -8
- package/dist/hooks/__tests__/codebase-map.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/codebase-map.test.js +0 -218
- package/dist/hooks/__tests__/codebase-map.test.js.map +0 -1
- package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts +0 -18
- package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js +0 -234
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +0 -1
- package/dist/hooks/__tests__/debugger-log-recency-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/debugger-log-recency-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/debugger-log-recency-contract.test.js +0 -20
- package/dist/hooks/__tests__/debugger-log-recency-contract.test.js.map +0 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/deep-interview-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js +0 -213
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +0 -1
- package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.js +0 -43
- package/dist/hooks/__tests__/explicit-terminal-stop-docs-contract.test.js.map +0 -1
- package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.js +0 -38
- package/dist/hooks/__tests__/explicit-terminal-stop-model-docs-contract.test.js.map +0 -1
- package/dist/hooks/__tests__/explore-routing.test.d.ts +0 -2
- package/dist/hooks/__tests__/explore-routing.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/explore-routing.test.js +0 -43
- package/dist/hooks/__tests__/explore-routing.test.js.map +0 -1
- package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js +0 -69
- package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js.map +0 -1
- package/dist/hooks/__tests__/keyword-detector.test.d.ts +0 -2
- package/dist/hooks/__tests__/keyword-detector.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +0 -1716
- package/dist/hooks/__tests__/keyword-detector.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.d.ts +0 -2
- package/dist/hooks/__tests__/notify-fallback-watcher.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +0 -3898
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.d.ts +0 -2
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +0 -786
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.d.ts +0 -2
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +0 -2397
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.d.ts +0 -2
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +0 -160
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.d.ts +0 -2
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +0 -1178
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-hook-modules.test.d.ts +0 -9
- package/dist/hooks/__tests__/notify-hook-modules.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-hook-modules.test.js +0 -529
- package/dist/hooks/__tests__/notify-hook-modules.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-hook-native-dispatch-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/notify-hook-native-dispatch-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-hook-native-dispatch-contract.test.js +0 -14
- package/dist/hooks/__tests__/notify-hook-native-dispatch-contract.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.d.ts +0 -2
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +0 -682
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-hook-regression-205.test.d.ts +0 -9
- package/dist/hooks/__tests__/notify-hook-regression-205.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js +0 -255
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.d.ts +0 -2
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +0 -162
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-hook-session-scope.test.d.ts +0 -2
- package/dist/hooks/__tests__/notify-hook-session-scope.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js +0 -301
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.d.ts +0 -2
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +0 -1510
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.d.ts +0 -2
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +0 -2879
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.d.ts +0 -2
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +0 -228
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.d.ts +0 -2
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +0 -35
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.d.ts +0 -2
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +0 -1589
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.d.ts +0 -10
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +0 -0
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-hook-visual-verdict.test.d.ts +0 -11
- package/dist/hooks/__tests__/notify-hook-visual-verdict.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js +0 -266
- package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js.map +0 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.d.ts +0 -2
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +0 -895
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +0 -1
- package/dist/hooks/__tests__/openclaw-setup-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/openclaw-setup-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/openclaw-setup-contract.test.js +0 -61
- package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +0 -1
- package/dist/hooks/__tests__/pre-context-gate-skills.test.d.ts +0 -2
- package/dist/hooks/__tests__/pre-context-gate-skills.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/pre-context-gate-skills.test.js +0 -40
- package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +0 -1
- package/dist/hooks/__tests__/prompt-guidance-catalog.test.d.ts +0 -2
- package/dist/hooks/__tests__/prompt-guidance-catalog.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/prompt-guidance-catalog.test.js +0 -11
- package/dist/hooks/__tests__/prompt-guidance-catalog.test.js.map +0 -1
- package/dist/hooks/__tests__/prompt-guidance-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/prompt-guidance-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js +0 -38
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +0 -1
- package/dist/hooks/__tests__/prompt-guidance-fragments.test.d.ts +0 -2
- package/dist/hooks/__tests__/prompt-guidance-fragments.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/prompt-guidance-fragments.test.js +0 -48
- package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +0 -1
- package/dist/hooks/__tests__/prompt-guidance-scenarios.test.d.ts +0 -2
- package/dist/hooks/__tests__/prompt-guidance-scenarios.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/prompt-guidance-scenarios.test.js +0 -11
- package/dist/hooks/__tests__/prompt-guidance-scenarios.test.js.map +0 -1
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.d.ts +0 -5
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.d.ts.map +0 -1
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.js +0 -34
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +0 -1
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.d.ts +0 -2
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +0 -65
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +0 -1
- package/dist/hooks/__tests__/prompt-orchestration-boundary.test.d.ts +0 -2
- package/dist/hooks/__tests__/prompt-orchestration-boundary.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/prompt-orchestration-boundary.test.js +0 -38
- package/dist/hooks/__tests__/prompt-orchestration-boundary.test.js.map +0 -1
- package/dist/hooks/__tests__/prompt-refactor-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/prompt-refactor-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/prompt-refactor-contract.test.js +0 -22
- package/dist/hooks/__tests__/prompt-refactor-contract.test.js.map +0 -1
- package/dist/hooks/__tests__/prompt-team-routing.test.d.ts +0 -2
- package/dist/hooks/__tests__/prompt-team-routing.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/prompt-team-routing.test.js +0 -49
- package/dist/hooks/__tests__/prompt-team-routing.test.js.map +0 -1
- package/dist/hooks/__tests__/session.test.d.ts +0 -2
- package/dist/hooks/__tests__/session.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/session.test.js +0 -322
- package/dist/hooks/__tests__/session.test.js.map +0 -1
- package/dist/hooks/__tests__/skill-guidance-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/skill-guidance-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/skill-guidance-contract.test.js +0 -29
- package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +0 -1
- package/dist/hooks/__tests__/task-size-detector.test.d.ts +0 -2
- package/dist/hooks/__tests__/task-size-detector.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/task-size-detector.test.js +0 -330
- package/dist/hooks/__tests__/task-size-detector.test.js.map +0 -1
- package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js +0 -28
- package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +0 -1
- package/dist/hooks/__tests__/tmux-hook-engine-types-sync.test.d.ts +0 -2
- package/dist/hooks/__tests__/tmux-hook-engine-types-sync.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/tmux-hook-engine-types-sync.test.js +0 -24
- package/dist/hooks/__tests__/tmux-hook-engine-types-sync.test.js.map +0 -1
- package/dist/hooks/__tests__/tmux-hook-engine.test.d.ts +0 -2
- package/dist/hooks/__tests__/tmux-hook-engine.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/tmux-hook-engine.test.js +0 -403
- package/dist/hooks/__tests__/tmux-hook-engine.test.js.map +0 -1
- package/dist/hooks/__tests__/triage-config.test.d.ts +0 -2
- package/dist/hooks/__tests__/triage-config.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/triage-config.test.js +0 -211
- package/dist/hooks/__tests__/triage-config.test.js.map +0 -1
- package/dist/hooks/__tests__/triage-heuristic.test.d.ts +0 -2
- package/dist/hooks/__tests__/triage-heuristic.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/triage-heuristic.test.js +0 -285
- package/dist/hooks/__tests__/triage-heuristic.test.js.map +0 -1
- package/dist/hooks/__tests__/triage-state.test.d.ts +0 -2
- package/dist/hooks/__tests__/triage-state.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/triage-state.test.js +0 -426
- package/dist/hooks/__tests__/triage-state.test.js.map +0 -1
- package/dist/hooks/__tests__/visual-ralph-skill.test.d.ts +0 -2
- package/dist/hooks/__tests__/visual-ralph-skill.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/visual-ralph-skill.test.js +0 -44
- package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +0 -1
- package/dist/hooks/__tests__/visual-verdict-loop.test.d.ts +0 -2
- package/dist/hooks/__tests__/visual-verdict-loop.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/visual-verdict-loop.test.js +0 -35
- package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +0 -1
- package/dist/hooks/__tests__/wiki-docs-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/wiki-docs-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/wiki-docs-contract.test.js +0 -34
- package/dist/hooks/__tests__/wiki-docs-contract.test.js.map +0 -1
- package/dist/hooks/code-simplifier/__tests__/index.test.d.ts +0 -2
- package/dist/hooks/code-simplifier/__tests__/index.test.d.ts.map +0 -1
- package/dist/hooks/code-simplifier/__tests__/index.test.js +0 -187
- package/dist/hooks/code-simplifier/__tests__/index.test.js.map +0 -1
- package/dist/hooks/extensibility/__tests__/dispatcher.test.d.ts +0 -2
- package/dist/hooks/extensibility/__tests__/dispatcher.test.d.ts.map +0 -1
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js +0 -242
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +0 -1
- package/dist/hooks/extensibility/__tests__/events.test.d.ts +0 -2
- package/dist/hooks/extensibility/__tests__/events.test.d.ts.map +0 -1
- package/dist/hooks/extensibility/__tests__/events.test.js +0 -125
- package/dist/hooks/extensibility/__tests__/events.test.js.map +0 -1
- package/dist/hooks/extensibility/__tests__/example-hook-plugins.test.d.ts +0 -2
- package/dist/hooks/extensibility/__tests__/example-hook-plugins.test.d.ts.map +0 -1
- package/dist/hooks/extensibility/__tests__/example-hook-plugins.test.js +0 -153
- package/dist/hooks/extensibility/__tests__/example-hook-plugins.test.js.map +0 -1
- package/dist/hooks/extensibility/__tests__/loader.test.d.ts +0 -2
- package/dist/hooks/extensibility/__tests__/loader.test.d.ts.map +0 -1
- package/dist/hooks/extensibility/__tests__/loader.test.js +0 -254
- package/dist/hooks/extensibility/__tests__/loader.test.js.map +0 -1
- package/dist/hooks/extensibility/__tests__/logging.test.d.ts +0 -2
- package/dist/hooks/extensibility/__tests__/logging.test.d.ts.map +0 -1
- package/dist/hooks/extensibility/__tests__/logging.test.js +0 -74
- package/dist/hooks/extensibility/__tests__/logging.test.js.map +0 -1
- package/dist/hooks/extensibility/__tests__/plugin-runner.test.d.ts +0 -2
- package/dist/hooks/extensibility/__tests__/plugin-runner.test.d.ts.map +0 -1
- package/dist/hooks/extensibility/__tests__/plugin-runner.test.js +0 -202
- package/dist/hooks/extensibility/__tests__/plugin-runner.test.js.map +0 -1
- package/dist/hooks/extensibility/__tests__/runtime.test.d.ts +0 -2
- package/dist/hooks/extensibility/__tests__/runtime.test.d.ts.map +0 -1
- package/dist/hooks/extensibility/__tests__/runtime.test.js +0 -198
- package/dist/hooks/extensibility/__tests__/runtime.test.js.map +0 -1
- package/dist/hooks/extensibility/__tests__/sdk-public-surface.test.d.ts +0 -2
- package/dist/hooks/extensibility/__tests__/sdk-public-surface.test.d.ts.map +0 -1
- package/dist/hooks/extensibility/__tests__/sdk-public-surface.test.js +0 -32
- package/dist/hooks/extensibility/__tests__/sdk-public-surface.test.js.map +0 -1
- package/dist/hooks/extensibility/__tests__/sdk.test.d.ts +0 -2
- package/dist/hooks/extensibility/__tests__/sdk.test.d.ts.map +0 -1
- package/dist/hooks/extensibility/__tests__/sdk.test.js +0 -479
- package/dist/hooks/extensibility/__tests__/sdk.test.js.map +0 -1
- package/dist/hud/__tests__/authority.test.d.ts +0 -2
- package/dist/hud/__tests__/authority.test.d.ts.map +0 -1
- package/dist/hud/__tests__/authority.test.js +0 -56
- package/dist/hud/__tests__/authority.test.js.map +0 -1
- package/dist/hud/__tests__/colors.test.d.ts +0 -2
- package/dist/hud/__tests__/colors.test.d.ts.map +0 -1
- package/dist/hud/__tests__/colors.test.js +0 -92
- package/dist/hud/__tests__/colors.test.js.map +0 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.d.ts +0 -10
- package/dist/hud/__tests__/hud-tmux-injection.test.d.ts.map +0 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.js +0 -150
- package/dist/hud/__tests__/hud-tmux-injection.test.js.map +0 -1
- package/dist/hud/__tests__/index.test.d.ts +0 -2
- package/dist/hud/__tests__/index.test.d.ts.map +0 -1
- package/dist/hud/__tests__/index.test.js +0 -180
- package/dist/hud/__tests__/index.test.js.map +0 -1
- package/dist/hud/__tests__/reconcile.test.d.ts +0 -2
- package/dist/hud/__tests__/reconcile.test.d.ts.map +0 -1
- package/dist/hud/__tests__/reconcile.test.js +0 -125
- package/dist/hud/__tests__/reconcile.test.js.map +0 -1
- package/dist/hud/__tests__/render.test.d.ts +0 -2
- package/dist/hud/__tests__/render.test.d.ts.map +0 -1
- package/dist/hud/__tests__/render.test.js +0 -573
- package/dist/hud/__tests__/render.test.js.map +0 -1
- package/dist/hud/__tests__/state.test.d.ts +0 -2
- package/dist/hud/__tests__/state.test.d.ts.map +0 -1
- package/dist/hud/__tests__/state.test.js +0 -618
- package/dist/hud/__tests__/state.test.js.map +0 -1
- package/dist/hud/__tests__/types.test.d.ts +0 -2
- package/dist/hud/__tests__/types.test.d.ts.map +0 -1
- package/dist/hud/__tests__/types.test.js +0 -79
- package/dist/hud/__tests__/types.test.js.map +0 -1
- package/dist/hud/__tests__/watch.test.d.ts +0 -2
- package/dist/hud/__tests__/watch.test.d.ts.map +0 -1
- package/dist/hud/__tests__/watch.test.js +0 -63
- package/dist/hud/__tests__/watch.test.js.map +0 -1
- package/dist/mcp/__tests__/bootstrap.test.d.ts +0 -2
- package/dist/mcp/__tests__/bootstrap.test.d.ts.map +0 -1
- package/dist/mcp/__tests__/bootstrap.test.js +0 -207
- package/dist/mcp/__tests__/bootstrap.test.js.map +0 -1
- package/dist/mcp/__tests__/code-intel-server.test.d.ts +0 -2
- package/dist/mcp/__tests__/code-intel-server.test.d.ts.map +0 -1
- package/dist/mcp/__tests__/code-intel-server.test.js +0 -70
- package/dist/mcp/__tests__/code-intel-server.test.js.map +0 -1
- package/dist/mcp/__tests__/memory-server.test.d.ts +0 -2
- package/dist/mcp/__tests__/memory-server.test.d.ts.map +0 -1
- package/dist/mcp/__tests__/memory-server.test.js +0 -36
- package/dist/mcp/__tests__/memory-server.test.js.map +0 -1
- package/dist/mcp/__tests__/memory-validation.test.d.ts +0 -2
- package/dist/mcp/__tests__/memory-validation.test.d.ts.map +0 -1
- package/dist/mcp/__tests__/memory-validation.test.js +0 -29
- package/dist/mcp/__tests__/memory-validation.test.js.map +0 -1
- package/dist/mcp/__tests__/path-traversal.test.d.ts +0 -2
- package/dist/mcp/__tests__/path-traversal.test.d.ts.map +0 -1
- package/dist/mcp/__tests__/path-traversal.test.js +0 -83
- package/dist/mcp/__tests__/path-traversal.test.js.map +0 -1
- package/dist/mcp/__tests__/server-lifecycle.test.d.ts +0 -2
- package/dist/mcp/__tests__/server-lifecycle.test.d.ts.map +0 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +0 -260
- package/dist/mcp/__tests__/server-lifecycle.test.js.map +0 -1
- package/dist/mcp/__tests__/state-paths.test.d.ts +0 -2
- package/dist/mcp/__tests__/state-paths.test.d.ts.map +0 -1
- package/dist/mcp/__tests__/state-paths.test.js +0 -209
- package/dist/mcp/__tests__/state-paths.test.js.map +0 -1
- package/dist/mcp/__tests__/state-server-ralph-phase.test.d.ts +0 -2
- package/dist/mcp/__tests__/state-server-ralph-phase.test.d.ts.map +0 -1
- package/dist/mcp/__tests__/state-server-ralph-phase.test.js +0 -109
- package/dist/mcp/__tests__/state-server-ralph-phase.test.js.map +0 -1
- package/dist/mcp/__tests__/state-server-schema.test.d.ts +0 -2
- package/dist/mcp/__tests__/state-server-schema.test.d.ts.map +0 -1
- package/dist/mcp/__tests__/state-server-schema.test.js +0 -29
- package/dist/mcp/__tests__/state-server-schema.test.js.map +0 -1
- package/dist/mcp/__tests__/state-server-team-tools.test.d.ts +0 -2
- package/dist/mcp/__tests__/state-server-team-tools.test.d.ts.map +0 -1
- package/dist/mcp/__tests__/state-server-team-tools.test.js +0 -35
- package/dist/mcp/__tests__/state-server-team-tools.test.js.map +0 -1
- package/dist/mcp/__tests__/state-server.test.d.ts +0 -2
- package/dist/mcp/__tests__/state-server.test.d.ts.map +0 -1
- package/dist/mcp/__tests__/state-server.test.js +0 -965
- package/dist/mcp/__tests__/state-server.test.js.map +0 -1
- package/dist/mcp/__tests__/trace-server.test.d.ts +0 -2
- package/dist/mcp/__tests__/trace-server.test.d.ts.map +0 -1
- package/dist/mcp/__tests__/trace-server.test.js +0 -119
- package/dist/mcp/__tests__/trace-server.test.js.map +0 -1
- package/dist/mcp/__tests__/wiki-server.test.d.ts +0 -2
- package/dist/mcp/__tests__/wiki-server.test.d.ts.map +0 -1
- package/dist/mcp/__tests__/wiki-server.test.js +0 -30
- package/dist/mcp/__tests__/wiki-server.test.js.map +0 -1
- package/dist/modes/__tests__/base-autoresearch-contract.test.d.ts +0 -2
- package/dist/modes/__tests__/base-autoresearch-contract.test.d.ts.map +0 -1
- package/dist/modes/__tests__/base-autoresearch-contract.test.js +0 -123
- package/dist/modes/__tests__/base-autoresearch-contract.test.js.map +0 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.d.ts +0 -2
- package/dist/modes/__tests__/base-multi-state-compat.test.d.ts.map +0 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js +0 -38
- package/dist/modes/__tests__/base-multi-state-compat.test.js.map +0 -1
- package/dist/modes/__tests__/base-ralph-contract.test.d.ts +0 -2
- package/dist/modes/__tests__/base-ralph-contract.test.d.ts.map +0 -1
- package/dist/modes/__tests__/base-ralph-contract.test.js +0 -64
- package/dist/modes/__tests__/base-ralph-contract.test.js.map +0 -1
- package/dist/modes/__tests__/base-session-scope.test.d.ts +0 -2
- package/dist/modes/__tests__/base-session-scope.test.d.ts.map +0 -1
- package/dist/modes/__tests__/base-session-scope.test.js +0 -98
- package/dist/modes/__tests__/base-session-scope.test.js.map +0 -1
- package/dist/modes/__tests__/base-tmux-pane.test.d.ts +0 -2
- package/dist/modes/__tests__/base-tmux-pane.test.d.ts.map +0 -1
- package/dist/modes/__tests__/base-tmux-pane.test.js +0 -39
- package/dist/modes/__tests__/base-tmux-pane.test.js.map +0 -1
- package/dist/notifications/__tests__/config.test.d.ts +0 -2
- package/dist/notifications/__tests__/config.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/config.test.js +0 -269
- package/dist/notifications/__tests__/config.test.js.map +0 -1
- package/dist/notifications/__tests__/custom-alias-enablement.test.d.ts +0 -2
- package/dist/notifications/__tests__/custom-alias-enablement.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/custom-alias-enablement.test.js +0 -84
- package/dist/notifications/__tests__/custom-alias-enablement.test.js.map +0 -1
- package/dist/notifications/__tests__/dispatch-cooldown.test.d.ts +0 -5
- package/dist/notifications/__tests__/dispatch-cooldown.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/dispatch-cooldown.test.js +0 -100
- package/dist/notifications/__tests__/dispatch-cooldown.test.js.map +0 -1
- package/dist/notifications/__tests__/dispatcher.test.d.ts +0 -2
- package/dist/notifications/__tests__/dispatcher.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/dispatcher.test.js +0 -202
- package/dist/notifications/__tests__/dispatcher.test.js.map +0 -1
- package/dist/notifications/__tests__/formatter.test.d.ts +0 -2
- package/dist/notifications/__tests__/formatter.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/formatter.test.js +0 -270
- package/dist/notifications/__tests__/formatter.test.js.map +0 -1
- package/dist/notifications/__tests__/hook-config.test.d.ts +0 -5
- package/dist/notifications/__tests__/hook-config.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/hook-config.test.js +0 -139
- package/dist/notifications/__tests__/hook-config.test.js.map +0 -1
- package/dist/notifications/__tests__/idle-cooldown.test.d.ts +0 -5
- package/dist/notifications/__tests__/idle-cooldown.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/idle-cooldown.test.js +0 -209
- package/dist/notifications/__tests__/idle-cooldown.test.js.map +0 -1
- package/dist/notifications/__tests__/index.test.d.ts +0 -2
- package/dist/notifications/__tests__/index.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/index.test.js +0 -188
- package/dist/notifications/__tests__/index.test.js.map +0 -1
- package/dist/notifications/__tests__/lifecycle-dedupe.test.d.ts +0 -2
- package/dist/notifications/__tests__/lifecycle-dedupe.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/lifecycle-dedupe.test.js +0 -86
- package/dist/notifications/__tests__/lifecycle-dedupe.test.js.map +0 -1
- package/dist/notifications/__tests__/notifier.test.d.ts +0 -2
- package/dist/notifications/__tests__/notifier.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/notifier.test.js +0 -239
- package/dist/notifications/__tests__/notifier.test.js.map +0 -1
- package/dist/notifications/__tests__/profiles.test.d.ts +0 -2
- package/dist/notifications/__tests__/profiles.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/profiles.test.js +0 -404
- package/dist/notifications/__tests__/profiles.test.js.map +0 -1
- package/dist/notifications/__tests__/reply-config.test.d.ts +0 -2
- package/dist/notifications/__tests__/reply-config.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/reply-config.test.js +0 -79
- package/dist/notifications/__tests__/reply-config.test.js.map +0 -1
- package/dist/notifications/__tests__/reply-listener.test.d.ts +0 -2
- package/dist/notifications/__tests__/reply-listener.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/reply-listener.test.js +0 -723
- package/dist/notifications/__tests__/reply-listener.test.js.map +0 -1
- package/dist/notifications/__tests__/session-idle-tail-dedupe.test.d.ts +0 -2
- package/dist/notifications/__tests__/session-idle-tail-dedupe.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js +0 -93
- package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js.map +0 -1
- package/dist/notifications/__tests__/session-registry.test.d.ts +0 -2
- package/dist/notifications/__tests__/session-registry.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/session-registry.test.js +0 -234
- package/dist/notifications/__tests__/session-registry.test.js.map +0 -1
- package/dist/notifications/__tests__/session-status.test.d.ts +0 -2
- package/dist/notifications/__tests__/session-status.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/session-status.test.js +0 -249
- package/dist/notifications/__tests__/session-status.test.js.map +0 -1
- package/dist/notifications/__tests__/temp-mode.test.d.ts +0 -2
- package/dist/notifications/__tests__/temp-mode.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/temp-mode.test.js +0 -172
- package/dist/notifications/__tests__/temp-mode.test.js.map +0 -1
- package/dist/notifications/__tests__/template-engine.test.d.ts +0 -5
- package/dist/notifications/__tests__/template-engine.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/template-engine.test.js +0 -158
- package/dist/notifications/__tests__/template-engine.test.js.map +0 -1
- package/dist/notifications/__tests__/tmux-detector.test.d.ts +0 -2
- package/dist/notifications/__tests__/tmux-detector.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/tmux-detector.test.js +0 -208
- package/dist/notifications/__tests__/tmux-detector.test.js.map +0 -1
- package/dist/notifications/__tests__/tmux.test.d.ts +0 -2
- package/dist/notifications/__tests__/tmux.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/tmux.test.js +0 -285
- package/dist/notifications/__tests__/tmux.test.js.map +0 -1
- package/dist/notifications/__tests__/verbosity.test.d.ts +0 -2
- package/dist/notifications/__tests__/verbosity.test.d.ts.map +0 -1
- package/dist/notifications/__tests__/verbosity.test.js +0 -237
- package/dist/notifications/__tests__/verbosity.test.js.map +0 -1
- package/dist/openclaw/__tests__/config.test.d.ts +0 -6
- package/dist/openclaw/__tests__/config.test.d.ts.map +0 -1
- package/dist/openclaw/__tests__/config.test.js +0 -344
- package/dist/openclaw/__tests__/config.test.js.map +0 -1
- package/dist/openclaw/__tests__/dispatcher.test.d.ts +0 -5
- package/dist/openclaw/__tests__/dispatcher.test.d.ts.map +0 -1
- package/dist/openclaw/__tests__/dispatcher.test.js +0 -169
- package/dist/openclaw/__tests__/dispatcher.test.js.map +0 -1
- package/dist/openclaw/__tests__/index.test.d.ts +0 -6
- package/dist/openclaw/__tests__/index.test.d.ts.map +0 -1
- package/dist/openclaw/__tests__/index.test.js +0 -382
- package/dist/openclaw/__tests__/index.test.js.map +0 -1
- package/dist/pipeline/__tests__/orchestrator.test.d.ts +0 -2
- package/dist/pipeline/__tests__/orchestrator.test.d.ts.map +0 -1
- package/dist/pipeline/__tests__/orchestrator.test.js +0 -505
- package/dist/pipeline/__tests__/orchestrator.test.js.map +0 -1
- package/dist/pipeline/__tests__/stages.test.d.ts +0 -2
- package/dist/pipeline/__tests__/stages.test.d.ts.map +0 -1
- package/dist/pipeline/__tests__/stages.test.js +0 -754
- package/dist/pipeline/__tests__/stages.test.js.map +0 -1
- package/dist/pipeline/stages/ralph-verify.d.ts +0 -53
- package/dist/pipeline/stages/ralph-verify.d.ts.map +0 -1
- package/dist/pipeline/stages/ralph-verify.js.map +0 -1
- package/dist/pipeline/stages/ralplan.d.ts +0 -25
- package/dist/pipeline/stages/ralplan.d.ts.map +0 -1
- package/dist/pipeline/stages/ralplan.js.map +0 -1
- package/dist/planning/__tests__/artifacts.test.d.ts +0 -2
- package/dist/planning/__tests__/artifacts.test.d.ts.map +0 -1
- package/dist/planning/__tests__/artifacts.test.js +0 -544
- package/dist/planning/__tests__/artifacts.test.js.map +0 -1
- package/dist/question/__tests__/client.test.d.ts +0 -2
- package/dist/question/__tests__/client.test.d.ts.map +0 -1
- package/dist/question/__tests__/client.test.js +0 -90
- package/dist/question/__tests__/client.test.js.map +0 -1
- package/dist/question/__tests__/deep-interview.test.d.ts +0 -2
- package/dist/question/__tests__/deep-interview.test.d.ts.map +0 -1
- package/dist/question/__tests__/deep-interview.test.js +0 -209
- package/dist/question/__tests__/deep-interview.test.js.map +0 -1
- package/dist/question/__tests__/policy.test.d.ts +0 -2
- package/dist/question/__tests__/policy.test.d.ts.map +0 -1
- package/dist/question/__tests__/policy.test.js +0 -107
- package/dist/question/__tests__/policy.test.js.map +0 -1
- package/dist/question/__tests__/renderer.test.d.ts +0 -2
- package/dist/question/__tests__/renderer.test.d.ts.map +0 -1
- package/dist/question/__tests__/renderer.test.js +0 -707
- package/dist/question/__tests__/renderer.test.js.map +0 -1
- package/dist/question/__tests__/state.test.d.ts +0 -2
- package/dist/question/__tests__/state.test.d.ts.map +0 -1
- package/dist/question/__tests__/state.test.js +0 -102
- package/dist/question/__tests__/state.test.js.map +0 -1
- package/dist/question/__tests__/types.test.d.ts +0 -2
- package/dist/question/__tests__/types.test.d.ts.map +0 -1
- package/dist/question/__tests__/types.test.js +0 -65
- package/dist/question/__tests__/types.test.js.map +0 -1
- package/dist/question/__tests__/ui.test.d.ts +0 -2
- package/dist/question/__tests__/ui.test.d.ts.map +0 -1
- package/dist/question/__tests__/ui.test.js +0 -446
- package/dist/question/__tests__/ui.test.js.map +0 -1
- package/dist/ralph/__tests__/persistence.test.d.ts +0 -2
- package/dist/ralph/__tests__/persistence.test.d.ts.map +0 -1
- package/dist/ralph/__tests__/persistence.test.js +0 -116
- package/dist/ralph/__tests__/persistence.test.js.map +0 -1
- package/dist/ralph/contract.d.ts +0 -17
- package/dist/ralph/persistence.js.map +0 -1
- package/dist/ralplan/__tests__/runtime.test.d.ts +0 -2
- package/dist/ralplan/__tests__/runtime.test.d.ts.map +0 -1
- package/dist/ralplan/__tests__/runtime.test.js +0 -165
- package/dist/ralplan/__tests__/runtime.test.js.map +0 -1
- package/dist/ralplan/runtime.d.ts +0 -52
- package/dist/ralplan/runtime.d.ts.map +0 -1
- package/dist/ralplan/runtime.js.map +0 -1
- package/dist/runtime/__tests__/bridge.test.d.ts +0 -2
- package/dist/runtime/__tests__/bridge.test.d.ts.map +0 -1
- package/dist/runtime/__tests__/bridge.test.js +0 -194
- package/dist/runtime/__tests__/bridge.test.js.map +0 -1
- package/dist/runtime/__tests__/run-loop.test.d.ts +0 -2
- package/dist/runtime/__tests__/run-loop.test.d.ts.map +0 -1
- package/dist/runtime/__tests__/run-loop.test.js +0 -35
- package/dist/runtime/__tests__/run-loop.test.js.map +0 -1
- package/dist/runtime/__tests__/run-outcome.test.d.ts +0 -2
- package/dist/runtime/__tests__/run-outcome.test.d.ts.map +0 -1
- package/dist/runtime/__tests__/run-outcome.test.js +0 -102
- package/dist/runtime/__tests__/run-outcome.test.js.map +0 -1
- package/dist/runtime/__tests__/run-state.test.d.ts +0 -2
- package/dist/runtime/__tests__/run-state.test.d.ts.map +0 -1
- package/dist/runtime/__tests__/run-state.test.js +0 -37
- package/dist/runtime/__tests__/run-state.test.js.map +0 -1
- package/dist/scripts/__tests__/codex-native-hook.test.d.ts +0 -2
- package/dist/scripts/__tests__/codex-native-hook.test.d.ts.map +0 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +0 -6788
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +0 -1
- package/dist/scripts/__tests__/generate-release-body.test.d.ts +0 -2
- package/dist/scripts/__tests__/generate-release-body.test.d.ts.map +0 -1
- package/dist/scripts/__tests__/generate-release-body.test.js +0 -233
- package/dist/scripts/__tests__/generate-release-body.test.js.map +0 -1
- package/dist/scripts/__tests__/hook-derived-watcher.test.d.ts +0 -2
- package/dist/scripts/__tests__/hook-derived-watcher.test.d.ts.map +0 -1
- package/dist/scripts/__tests__/hook-derived-watcher.test.js +0 -195
- package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +0 -1
- package/dist/scripts/__tests__/postinstall.test.d.ts +0 -2
- package/dist/scripts/__tests__/postinstall.test.d.ts.map +0 -1
- package/dist/scripts/__tests__/postinstall.test.js +0 -92
- package/dist/scripts/__tests__/postinstall.test.js.map +0 -1
- package/dist/scripts/__tests__/prompt-inventory.test.d.ts +0 -2
- package/dist/scripts/__tests__/prompt-inventory.test.d.ts.map +0 -1
- package/dist/scripts/__tests__/prompt-inventory.test.js +0 -56
- package/dist/scripts/__tests__/prompt-inventory.test.js.map +0 -1
- package/dist/scripts/__tests__/run-test-files.test.d.ts +0 -2
- package/dist/scripts/__tests__/run-test-files.test.d.ts.map +0 -1
- package/dist/scripts/__tests__/run-test-files.test.js +0 -62
- package/dist/scripts/__tests__/run-test-files.test.js.map +0 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.d.ts +0 -2
- package/dist/scripts/__tests__/smoke-packed-install.test.d.ts.map +0 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +0 -135
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +0 -1
- package/dist/scripts/__tests__/test-reply-listener-live.test.d.ts +0 -2
- package/dist/scripts/__tests__/test-reply-listener-live.test.d.ts.map +0 -1
- package/dist/scripts/__tests__/test-reply-listener-live.test.js +0 -82
- package/dist/scripts/__tests__/test-reply-listener-live.test.js.map +0 -1
- package/dist/scripts/__tests__/verify-native-agents.test.d.ts +0 -2
- package/dist/scripts/__tests__/verify-native-agents.test.d.ts.map +0 -1
- package/dist/scripts/__tests__/verify-native-agents.test.js +0 -166
- package/dist/scripts/__tests__/verify-native-agents.test.js.map +0 -1
- package/dist/scripts/eval/eval-candidate-handoff.d.ts +0 -2
- package/dist/scripts/eval/eval-candidate-handoff.d.ts.map +0 -1
- package/dist/scripts/eval/eval-candidate-handoff.js +0 -11
- package/dist/scripts/eval/eval-candidate-handoff.js.map +0 -1
- package/dist/scripts/eval/eval-cli-discoverability.d.ts +0 -3
- package/dist/scripts/eval/eval-cli-discoverability.d.ts.map +0 -1
- package/dist/scripts/eval/eval-cli-discoverability.js +0 -37
- package/dist/scripts/eval/eval-cli-discoverability.js.map +0 -1
- package/dist/scripts/eval/eval-fresh-run-tagging.d.ts +0 -2
- package/dist/scripts/eval/eval-fresh-run-tagging.d.ts.map +0 -1
- package/dist/scripts/eval/eval-fresh-run-tagging.js +0 -11
- package/dist/scripts/eval/eval-fresh-run-tagging.js.map +0 -1
- package/dist/scripts/eval/eval-help-consistency.d.ts +0 -2
- package/dist/scripts/eval/eval-help-consistency.d.ts.map +0 -1
- package/dist/scripts/eval/eval-help-consistency.js +0 -12
- package/dist/scripts/eval/eval-help-consistency.js.map +0 -1
- package/dist/scripts/eval/eval-in-action-cat-shellout-demo.d.ts +0 -2
- package/dist/scripts/eval/eval-in-action-cat-shellout-demo.d.ts.map +0 -1
- package/dist/scripts/eval/eval-in-action-cat-shellout-demo.js +0 -31
- package/dist/scripts/eval/eval-in-action-cat-shellout-demo.js.map +0 -1
- package/dist/scripts/eval/eval-parity-smoke.d.ts +0 -2
- package/dist/scripts/eval/eval-parity-smoke.d.ts.map +0 -1
- package/dist/scripts/eval/eval-parity-smoke.js +0 -23
- package/dist/scripts/eval/eval-parity-smoke.js.map +0 -1
- package/dist/scripts/eval/eval-parity-sweep.d.ts +0 -2
- package/dist/scripts/eval/eval-parity-sweep.d.ts.map +0 -1
- package/dist/scripts/eval/eval-parity-sweep.js +0 -29
- package/dist/scripts/eval/eval-parity-sweep.js.map +0 -1
- package/dist/scripts/eval/eval-resume-dirty-guard.d.ts +0 -2
- package/dist/scripts/eval/eval-resume-dirty-guard.d.ts.map +0 -1
- package/dist/scripts/eval/eval-resume-dirty-guard.js +0 -11
- package/dist/scripts/eval/eval-resume-dirty-guard.js.map +0 -1
- package/dist/scripts/eval/eval-security-path-traversal.d.ts +0 -3
- package/dist/scripts/eval/eval-security-path-traversal.d.ts.map +0 -1
- package/dist/scripts/eval/eval-security-path-traversal.js +0 -35
- package/dist/scripts/eval/eval-security-path-traversal.js.map +0 -1
- package/dist/scripts/notify-hook/__tests__/operational-events.test.d.ts +0 -2
- package/dist/scripts/notify-hook/__tests__/operational-events.test.d.ts.map +0 -1
- package/dist/scripts/notify-hook/__tests__/operational-events.test.js +0 -24
- package/dist/scripts/notify-hook/__tests__/operational-events.test.js.map +0 -1
- package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.d.ts +0 -2
- package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.d.ts.map +0 -1
- package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.js +0 -153
- package/dist/scripts/notify-hook/__tests__/team-worker-posttooluse.test.js.map +0 -1
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts +0 -22
- package/dist/session-history/__tests__/search.test.d.ts +0 -2
- package/dist/session-history/__tests__/search.test.d.ts.map +0 -1
- package/dist/session-history/__tests__/search.test.js +0 -150
- package/dist/session-history/__tests__/search.test.js.map +0 -1
- package/dist/sidecar/__tests__/boundary.test.d.ts +0 -2
- package/dist/sidecar/__tests__/boundary.test.d.ts.map +0 -1
- package/dist/sidecar/__tests__/boundary.test.js +0 -48
- package/dist/sidecar/__tests__/boundary.test.js.map +0 -1
- package/dist/sidecar/__tests__/collector.test.d.ts +0 -2
- package/dist/sidecar/__tests__/collector.test.d.ts.map +0 -1
- package/dist/sidecar/__tests__/collector.test.js +0 -162
- package/dist/sidecar/__tests__/collector.test.js.map +0 -1
- package/dist/sidecar/__tests__/render.test.d.ts +0 -2
- package/dist/sidecar/__tests__/render.test.d.ts.map +0 -1
- package/dist/sidecar/__tests__/render.test.js +0 -67
- package/dist/sidecar/__tests__/render.test.js.map +0 -1
- package/dist/sidecar/__tests__/tmux.test.d.ts +0 -2
- package/dist/sidecar/__tests__/tmux.test.d.ts.map +0 -1
- package/dist/sidecar/__tests__/tmux.test.js +0 -30
- package/dist/sidecar/__tests__/tmux.test.js.map +0 -1
- package/dist/sidecar/__tests__/watch.test.d.ts +0 -2
- package/dist/sidecar/__tests__/watch.test.d.ts.map +0 -1
- package/dist/sidecar/__tests__/watch.test.js +0 -42
- package/dist/sidecar/__tests__/watch.test.js.map +0 -1
- package/dist/state/__tests__/mode-state-context.test.d.ts +0 -2
- package/dist/state/__tests__/mode-state-context.test.d.ts.map +0 -1
- package/dist/state/__tests__/mode-state-context.test.js +0 -35
- package/dist/state/__tests__/mode-state-context.test.js.map +0 -1
- package/dist/state/__tests__/operations-ralph-phase.test.d.ts +0 -2
- package/dist/state/__tests__/operations-ralph-phase.test.d.ts.map +0 -1
- package/dist/state/__tests__/operations-ralph-phase.test.js +0 -103
- package/dist/state/__tests__/operations-ralph-phase.test.js.map +0 -1
- package/dist/state/__tests__/operations.test.d.ts +0 -2
- package/dist/state/__tests__/operations.test.d.ts.map +0 -1
- package/dist/state/__tests__/operations.test.js +0 -439
- package/dist/state/__tests__/operations.test.js.map +0 -1
- package/dist/state/__tests__/path-traversal.test.d.ts +0 -2
- package/dist/state/__tests__/path-traversal.test.d.ts.map +0 -1
- package/dist/state/__tests__/path-traversal.test.js +0 -49
- package/dist/state/__tests__/path-traversal.test.js.map +0 -1
- package/dist/state/__tests__/skill-active.test.d.ts +0 -2
- package/dist/state/__tests__/skill-active.test.d.ts.map +0 -1
- package/dist/state/__tests__/skill-active.test.js +0 -160
- package/dist/state/__tests__/skill-active.test.js.map +0 -1
- package/dist/state/__tests__/workflow-transition.test.d.ts +0 -2
- package/dist/state/__tests__/workflow-transition.test.d.ts.map +0 -1
- package/dist/state/__tests__/workflow-transition.test.js +0 -77
- package/dist/state/__tests__/workflow-transition.test.js.map +0 -1
- package/dist/subagents/__tests__/tracker.test.d.ts +0 -2
- package/dist/subagents/__tests__/tracker.test.d.ts.map +0 -1
- package/dist/subagents/__tests__/tracker.test.js +0 -47
- package/dist/subagents/__tests__/tracker.test.js.map +0 -1
- package/dist/team/__tests__/allocation-policy.test.d.ts +0 -2
- package/dist/team/__tests__/allocation-policy.test.d.ts.map +0 -1
- package/dist/team/__tests__/allocation-policy.test.js +0 -111
- package/dist/team/__tests__/allocation-policy.test.js.map +0 -1
- package/dist/team/__tests__/api-interop.test.d.ts +0 -2
- package/dist/team/__tests__/api-interop.test.d.ts.map +0 -1
- package/dist/team/__tests__/api-interop.test.js +0 -2262
- package/dist/team/__tests__/api-interop.test.js.map +0 -1
- package/dist/team/__tests__/commit-hygiene.test.d.ts +0 -2
- package/dist/team/__tests__/commit-hygiene.test.d.ts.map +0 -1
- package/dist/team/__tests__/commit-hygiene.test.js +0 -93
- package/dist/team/__tests__/commit-hygiene.test.js.map +0 -1
- package/dist/team/__tests__/cross-rebase-smoke.test.d.ts +0 -2
- package/dist/team/__tests__/cross-rebase-smoke.test.d.ts.map +0 -1
- package/dist/team/__tests__/cross-rebase-smoke.test.js +0 -161
- package/dist/team/__tests__/cross-rebase-smoke.test.js.map +0 -1
- package/dist/team/__tests__/current-task-baseline.test.d.ts +0 -2
- package/dist/team/__tests__/current-task-baseline.test.d.ts.map +0 -1
- package/dist/team/__tests__/current-task-baseline.test.js +0 -87
- package/dist/team/__tests__/current-task-baseline.test.js.map +0 -1
- package/dist/team/__tests__/delegation-policy.test.d.ts +0 -2
- package/dist/team/__tests__/delegation-policy.test.d.ts.map +0 -1
- package/dist/team/__tests__/delegation-policy.test.js +0 -69
- package/dist/team/__tests__/delegation-policy.test.js.map +0 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.d.ts +0 -2
- package/dist/team/__tests__/delivery-e2e-smoke.test.d.ts.map +0 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +0 -679
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +0 -1
- package/dist/team/__tests__/events.test.d.ts +0 -2
- package/dist/team/__tests__/events.test.d.ts.map +0 -1
- package/dist/team/__tests__/events.test.js +0 -313
- package/dist/team/__tests__/events.test.js.map +0 -1
- package/dist/team/__tests__/followup-planner.test.d.ts +0 -2
- package/dist/team/__tests__/followup-planner.test.d.ts.map +0 -1
- package/dist/team/__tests__/followup-planner.test.js +0 -84
- package/dist/team/__tests__/followup-planner.test.js.map +0 -1
- package/dist/team/__tests__/hardening-e2e.test.d.ts +0 -2
- package/dist/team/__tests__/hardening-e2e.test.d.ts.map +0 -1
- package/dist/team/__tests__/hardening-e2e.test.js +0 -98
- package/dist/team/__tests__/hardening-e2e.test.js.map +0 -1
- package/dist/team/__tests__/hook-primary-e2e-contract.test.d.ts +0 -2
- package/dist/team/__tests__/hook-primary-e2e-contract.test.d.ts.map +0 -1
- package/dist/team/__tests__/hook-primary-e2e-contract.test.js +0 -78
- package/dist/team/__tests__/hook-primary-e2e-contract.test.js.map +0 -1
- package/dist/team/__tests__/idle-nudge.test.d.ts +0 -2
- package/dist/team/__tests__/idle-nudge.test.d.ts.map +0 -1
- package/dist/team/__tests__/idle-nudge.test.js +0 -230
- package/dist/team/__tests__/idle-nudge.test.js.map +0 -1
- package/dist/team/__tests__/leader-activity.test.d.ts +0 -2
- package/dist/team/__tests__/leader-activity.test.d.ts.map +0 -1
- package/dist/team/__tests__/leader-activity.test.js +0 -261
- package/dist/team/__tests__/leader-activity.test.js.map +0 -1
- package/dist/team/__tests__/mcp-comm.test.d.ts +0 -2
- package/dist/team/__tests__/mcp-comm.test.d.ts.map +0 -1
- package/dist/team/__tests__/mcp-comm.test.js +0 -289
- package/dist/team/__tests__/mcp-comm.test.js.map +0 -1
- package/dist/team/__tests__/model-contract.test.d.ts +0 -2
- package/dist/team/__tests__/model-contract.test.d.ts.map +0 -1
- package/dist/team/__tests__/model-contract.test.js +0 -171
- package/dist/team/__tests__/model-contract.test.js.map +0 -1
- package/dist/team/__tests__/orchestrator.test.d.ts +0 -2
- package/dist/team/__tests__/orchestrator.test.d.ts.map +0 -1
- package/dist/team/__tests__/orchestrator.test.js +0 -111
- package/dist/team/__tests__/orchestrator.test.js.map +0 -1
- package/dist/team/__tests__/phase-controller.test.d.ts +0 -2
- package/dist/team/__tests__/phase-controller.test.d.ts.map +0 -1
- package/dist/team/__tests__/phase-controller.test.js +0 -50
- package/dist/team/__tests__/phase-controller.test.js.map +0 -1
- package/dist/team/__tests__/rebalance-policy.test.d.ts +0 -2
- package/dist/team/__tests__/rebalance-policy.test.d.ts.map +0 -1
- package/dist/team/__tests__/rebalance-policy.test.js +0 -168
- package/dist/team/__tests__/rebalance-policy.test.js.map +0 -1
- package/dist/team/__tests__/repo-aware-decomposition.test.d.ts +0 -2
- package/dist/team/__tests__/repo-aware-decomposition.test.d.ts.map +0 -1
- package/dist/team/__tests__/repo-aware-decomposition.test.js +0 -136
- package/dist/team/__tests__/repo-aware-decomposition.test.js.map +0 -1
- package/dist/team/__tests__/role-router.test.d.ts +0 -2
- package/dist/team/__tests__/role-router.test.d.ts.map +0 -1
- package/dist/team/__tests__/role-router.test.js +0 -263
- package/dist/team/__tests__/role-router.test.js.map +0 -1
- package/dist/team/__tests__/runtime-cli.test.d.ts +0 -2
- package/dist/team/__tests__/runtime-cli.test.d.ts.map +0 -1
- package/dist/team/__tests__/runtime-cli.test.js +0 -304
- package/dist/team/__tests__/runtime-cli.test.js.map +0 -1
- package/dist/team/__tests__/runtime.test.d.ts +0 -2
- package/dist/team/__tests__/runtime.test.d.ts.map +0 -1
- package/dist/team/__tests__/runtime.test.js +0 -5734
- package/dist/team/__tests__/runtime.test.js.map +0 -1
- package/dist/team/__tests__/scaling.test.d.ts +0 -2
- package/dist/team/__tests__/scaling.test.d.ts.map +0 -1
- package/dist/team/__tests__/scaling.test.js +0 -1005
- package/dist/team/__tests__/scaling.test.js.map +0 -1
- package/dist/team/__tests__/shutdown-fallback.test.d.ts +0 -2
- package/dist/team/__tests__/shutdown-fallback.test.d.ts.map +0 -1
- package/dist/team/__tests__/shutdown-fallback.test.js +0 -125
- package/dist/team/__tests__/shutdown-fallback.test.js.map +0 -1
- package/dist/team/__tests__/state-root.test.d.ts +0 -2
- package/dist/team/__tests__/state-root.test.d.ts.map +0 -1
- package/dist/team/__tests__/state-root.test.js +0 -195
- package/dist/team/__tests__/state-root.test.js.map +0 -1
- package/dist/team/__tests__/state.test.d.ts +0 -2
- package/dist/team/__tests__/state.test.d.ts.map +0 -1
- package/dist/team/__tests__/state.test.js +0 -1859
- package/dist/team/__tests__/state.test.js.map +0 -1
- package/dist/team/__tests__/team-identity.test.d.ts +0 -2
- package/dist/team/__tests__/team-identity.test.d.ts.map +0 -1
- package/dist/team/__tests__/team-identity.test.js +0 -166
- package/dist/team/__tests__/team-identity.test.js.map +0 -1
- package/dist/team/__tests__/team-ops-contract.test.d.ts +0 -2
- package/dist/team/__tests__/team-ops-contract.test.d.ts.map +0 -1
- package/dist/team/__tests__/team-ops-contract.test.js +0 -96
- package/dist/team/__tests__/team-ops-contract.test.js.map +0 -1
- package/dist/team/__tests__/tmux-claude-workers-demo.test.d.ts +0 -2
- package/dist/team/__tests__/tmux-claude-workers-demo.test.d.ts.map +0 -1
- package/dist/team/__tests__/tmux-claude-workers-demo.test.js +0 -191
- package/dist/team/__tests__/tmux-claude-workers-demo.test.js.map +0 -1
- package/dist/team/__tests__/tmux-session.test.d.ts +0 -2
- package/dist/team/__tests__/tmux-session.test.d.ts.map +0 -1
- package/dist/team/__tests__/tmux-session.test.js +0 -3785
- package/dist/team/__tests__/tmux-session.test.js.map +0 -1
- package/dist/team/__tests__/tmux-test-fixture.d.ts +0 -20
- package/dist/team/__tests__/tmux-test-fixture.d.ts.map +0 -1
- package/dist/team/__tests__/tmux-test-fixture.js +0 -152
- package/dist/team/__tests__/tmux-test-fixture.js.map +0 -1
- package/dist/team/__tests__/tmux-test-fixture.test.d.ts +0 -2
- package/dist/team/__tests__/tmux-test-fixture.test.d.ts.map +0 -1
- package/dist/team/__tests__/tmux-test-fixture.test.js +0 -113
- package/dist/team/__tests__/tmux-test-fixture.test.js.map +0 -1
- package/dist/team/__tests__/worker-bootstrap.test.d.ts +0 -2
- package/dist/team/__tests__/worker-bootstrap.test.d.ts.map +0 -1
- package/dist/team/__tests__/worker-bootstrap.test.js +0 -685
- package/dist/team/__tests__/worker-bootstrap.test.js.map +0 -1
- package/dist/team/__tests__/worker-runtime-identity.test.d.ts +0 -2
- package/dist/team/__tests__/worker-runtime-identity.test.d.ts.map +0 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +0 -250
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +0 -1
- package/dist/team/__tests__/worktree.test.d.ts +0 -2
- package/dist/team/__tests__/worktree.test.d.ts.map +0 -1
- package/dist/team/__tests__/worktree.test.js +0 -317
- package/dist/team/__tests__/worktree.test.js.map +0 -1
- package/dist/utils/__tests__/agents-md.test.d.ts +0 -2
- package/dist/utils/__tests__/agents-md.test.d.ts.map +0 -1
- package/dist/utils/__tests__/agents-md.test.js +0 -52
- package/dist/utils/__tests__/agents-md.test.js.map +0 -1
- package/dist/utils/__tests__/agents-model-table.test.d.ts +0 -2
- package/dist/utils/__tests__/agents-model-table.test.d.ts.map +0 -1
- package/dist/utils/__tests__/agents-model-table.test.js +0 -104
- package/dist/utils/__tests__/agents-model-table.test.js.map +0 -1
- package/dist/utils/__tests__/dep-versions.test.d.ts +0 -2
- package/dist/utils/__tests__/dep-versions.test.d.ts.map +0 -1
- package/dist/utils/__tests__/dep-versions.test.js +0 -46
- package/dist/utils/__tests__/dep-versions.test.js.map +0 -1
- package/dist/utils/__tests__/package.test.d.ts +0 -2
- package/dist/utils/__tests__/package.test.d.ts.map +0 -1
- package/dist/utils/__tests__/package.test.js +0 -21
- package/dist/utils/__tests__/package.test.js.map +0 -1
- package/dist/utils/__tests__/paths.test.d.ts +0 -2
- package/dist/utils/__tests__/paths.test.d.ts.map +0 -1
- package/dist/utils/__tests__/paths.test.js +0 -541
- package/dist/utils/__tests__/paths.test.js.map +0 -1
- package/dist/utils/__tests__/platform-command.test.d.ts +0 -2
- package/dist/utils/__tests__/platform-command.test.d.ts.map +0 -1
- package/dist/utils/__tests__/platform-command.test.js +0 -410
- package/dist/utils/__tests__/platform-command.test.js.map +0 -1
- package/dist/utils/__tests__/repo-deps.test.d.ts +0 -2
- package/dist/utils/__tests__/repo-deps.test.d.ts.map +0 -1
- package/dist/utils/__tests__/repo-deps.test.js +0 -71
- package/dist/utils/__tests__/repo-deps.test.js.map +0 -1
- package/dist/verification/__tests__/ci-rust-gates.test.d.ts +0 -2
- package/dist/verification/__tests__/ci-rust-gates.test.d.ts.map +0 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +0 -89
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +0 -1
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.d.ts +0 -2
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.d.ts.map +0 -1
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +0 -54
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +0 -1
- package/dist/verification/__tests__/explore-harness-release-workflow.test.d.ts +0 -2
- package/dist/verification/__tests__/explore-harness-release-workflow.test.d.ts.map +0 -1
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js +0 -73
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +0 -1
- package/dist/verification/__tests__/native-release-manifest.test.d.ts +0 -2
- package/dist/verification/__tests__/native-release-manifest.test.d.ts.map +0 -1
- package/dist/verification/__tests__/native-release-manifest.test.js +0 -80
- package/dist/verification/__tests__/native-release-manifest.test.js.map +0 -1
- package/dist/verification/__tests__/pr-check-workflow.test.d.ts +0 -2
- package/dist/verification/__tests__/pr-check-workflow.test.d.ts.map +0 -1
- package/dist/verification/__tests__/pr-check-workflow.test.js +0 -27
- package/dist/verification/__tests__/pr-check-workflow.test.js.map +0 -1
- package/dist/verification/__tests__/ralph-persistence-gate.test.d.ts +0 -2
- package/dist/verification/__tests__/ralph-persistence-gate.test.d.ts.map +0 -1
- package/dist/verification/__tests__/ralph-persistence-gate.test.js +0 -55
- package/dist/verification/__tests__/ralph-persistence-gate.test.js.map +0 -1
- package/dist/verification/__tests__/rust-runtime-thin-adapter-gate.test.d.ts +0 -2
- package/dist/verification/__tests__/rust-runtime-thin-adapter-gate.test.d.ts.map +0 -1
- package/dist/verification/__tests__/rust-runtime-thin-adapter-gate.test.js +0 -32
- package/dist/verification/__tests__/rust-runtime-thin-adapter-gate.test.js.map +0 -1
- package/dist/verification/__tests__/verifier.test.d.ts +0 -2
- package/dist/verification/__tests__/verifier.test.d.ts.map +0 -1
- package/dist/verification/__tests__/verifier.test.js +0 -113
- package/dist/verification/__tests__/verifier.test.js.map +0 -1
- package/dist/visual/__tests__/verdict.test.d.ts +0 -2
- package/dist/visual/__tests__/verdict.test.d.ts.map +0 -1
- package/dist/visual/__tests__/verdict.test.js +0 -81
- package/dist/visual/__tests__/verdict.test.js.map +0 -1
- package/dist/wiki/__tests__/cjk-tokenize.test.d.ts +0 -12
- package/dist/wiki/__tests__/cjk-tokenize.test.d.ts.map +0 -1
- package/dist/wiki/__tests__/cjk-tokenize.test.js +0 -139
- package/dist/wiki/__tests__/cjk-tokenize.test.js.map +0 -1
- package/dist/wiki/__tests__/crlf-parse.test.d.ts +0 -2
- package/dist/wiki/__tests__/crlf-parse.test.d.ts.map +0 -1
- package/dist/wiki/__tests__/crlf-parse.test.js +0 -24
- package/dist/wiki/__tests__/crlf-parse.test.js.map +0 -1
- package/dist/wiki/__tests__/escape-newline.test.d.ts +0 -2
- package/dist/wiki/__tests__/escape-newline.test.d.ts.map +0 -1
- package/dist/wiki/__tests__/escape-newline.test.js +0 -45
- package/dist/wiki/__tests__/escape-newline.test.js.map +0 -1
- package/dist/wiki/__tests__/ingest.test.d.ts +0 -5
- package/dist/wiki/__tests__/ingest.test.d.ts.map +0 -1
- package/dist/wiki/__tests__/ingest.test.js +0 -181
- package/dist/wiki/__tests__/ingest.test.js.map +0 -1
- package/dist/wiki/__tests__/lint.test.d.ts +0 -5
- package/dist/wiki/__tests__/lint.test.d.ts.map +0 -1
- package/dist/wiki/__tests__/lint.test.js +0 -163
- package/dist/wiki/__tests__/lint.test.js.map +0 -1
- package/dist/wiki/__tests__/query.test.d.ts +0 -5
- package/dist/wiki/__tests__/query.test.d.ts.map +0 -1
- package/dist/wiki/__tests__/query.test.js +0 -141
- package/dist/wiki/__tests__/query.test.js.map +0 -1
- package/dist/wiki/__tests__/reserved-file-guard.test.d.ts +0 -2
- package/dist/wiki/__tests__/reserved-file-guard.test.d.ts.map +0 -1
- package/dist/wiki/__tests__/reserved-file-guard.test.js +0 -44
- package/dist/wiki/__tests__/reserved-file-guard.test.js.map +0 -1
- package/dist/wiki/__tests__/session-hooks.test.d.ts +0 -5
- package/dist/wiki/__tests__/session-hooks.test.d.ts.map +0 -1
- package/dist/wiki/__tests__/session-hooks.test.js +0 -36
- package/dist/wiki/__tests__/session-hooks.test.js.map +0 -1
- package/dist/wiki/__tests__/slug-nonascii.test.d.ts +0 -2
- package/dist/wiki/__tests__/slug-nonascii.test.d.ts.map +0 -1
- package/dist/wiki/__tests__/slug-nonascii.test.js +0 -30
- package/dist/wiki/__tests__/slug-nonascii.test.js.map +0 -1
- package/dist/wiki/__tests__/storage.test.d.ts +0 -5
- package/dist/wiki/__tests__/storage.test.d.ts.map +0 -1
- package/dist/wiki/__tests__/storage.test.js +0 -278
- package/dist/wiki/__tests__/storage.test.js.map +0 -1
- package/dist/wiki/__tests__/test-helpers.d.ts +0 -31
- package/dist/wiki/__tests__/test-helpers.d.ts.map +0 -1
- package/dist/wiki/__tests__/test-helpers.js +0 -108
- package/dist/wiki/__tests__/test-helpers.js.map +0 -1
- package/docs/contracts/ralph-cancel-contract.md +0 -23
- package/docs/contracts/ralph-state-contract.md +0 -95
- package/docs/issues/team-ralph-followup-team.md +0 -38
- package/docs/qa/ralph-persistence-gate.md +0 -59
- package/docs/reference/ralph-parity-matrix.md +0 -25
- package/docs/reference/ralph-upstream-baseline.md +0 -34
- package/plugins/roblox-ai-os-creator-skills/skills/ralph/SKILL.md +0 -269
- package/plugins/roblox-ai-os-creator-skills/skills/ralplan/SKILL.md +0 -162
- package/prompts/api-reviewer.md +0 -113
- package/prompts/information-architect.md +0 -226
- package/prompts/performance-reviewer.md +0 -109
- package/prompts/product-analyst.md +0 -304
- package/prompts/product-manager.md +0 -245
- package/prompts/qa-tester.md +0 -124
- package/prompts/quality-reviewer.md +0 -123
- package/prompts/quality-strategist.md +0 -274
- package/prompts/style-reviewer.md +0 -102
- package/prompts/ux-researcher.md +0 -327
- package/skills/frontend-ui-ux/SKILL.md +0 -34
- package/skills/ralph/SKILL.md +0 -269
- package/skills/ralplan/SKILL.md +0 -162
- package/src/scripts/eval/eval-adaptive-sort-optimization.py +0 -24
- package/src/scripts/eval/eval-candidate-handoff.ts +0 -8
- package/src/scripts/eval/eval-cli-discoverability.ts +0 -40
- package/src/scripts/eval/eval-fresh-run-tagging.ts +0 -8
- package/src/scripts/eval/eval-help-consistency.ts +0 -11
- package/src/scripts/eval/eval-in-action-cat-shellout-demo.ts +0 -31
- package/src/scripts/eval/eval-ml-kaggle-model-optimization.py +0 -29
- package/src/scripts/eval/eval-noisy-bayesopt-highdim.py +0 -44
- package/src/scripts/eval/eval-noisy-latent-subspace-discovery.py +0 -44
- package/src/scripts/eval/eval-parity-smoke.ts +0 -20
- package/src/scripts/eval/eval-parity-sweep.ts +0 -26
- package/src/scripts/eval/eval-resume-dirty-guard.ts +0 -8
- package/src/scripts/eval/eval-security-path-traversal.ts +0 -38
- package/src/scripts/run-autoresearch-showcase.sh +0 -75
- /package/docs/{migration-mainline-post-v0.4.4.md → archive/migration-mainline-post-v0.4.4.md} +0 -0
- /package/docs/{qa-plan-0.4.2.md → archive/qa-plan-0.4.2.md} +0 -0
- /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
|