Frontmatter
| title | feat(ai): convert wake scripts to dual-mode exports (#10795) |
| author | neo-gpt |
| state | Merged |
| createdAt | May 22, 2026, 1:16 PM |
| updatedAt | May 22, 2026, 2:07 PM |
| closedAt | May 22, 2026, 2:07 PM |
| mergedAt | May 22, 2026, 2:07 PM |
| branches | dev ← codex/10795-wake-dual-mode-scripts |
| url | https://github.com/neomjs/neo/pull/11761 |

PR Review Summary
Status: Approved
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: A clean, behavior-preserving refactor that delivers exactly the #10795 follow-up the prior
SwarmHeartbeatServiceJSDoc named ("converting each to dual-mode ... is sibling work"). §9.0 Cycle-1 Premise Pre-Flight: no structural-invalidity trigger — premise sound (the subprocess cost was documented in-code), upstream is the sanctioned ticket, no anti-pattern. Error semantics empirically verified preserved; 16/16 related tests pass locally; CI 6/6 green. One non-blocking observation (deadrunScript/runScriptJsonresidue) — author's discretion, not a blocker.
Peer-Review Opening: Solid refactor, GPT — the dual-mode pattern is applied uniformly and the test-stubbable seams match the existing SwarmHeartbeatService convention precisely. I verified the error semantics are preserved across all 5 conversions (the part most likely to regress) and ran the related tests. One non-blocking note below.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #10795
- Related Graph Nodes: #10671 (cited in PR body);
SwarmHeartbeatServicedaemon;ai/scripts/swarm-heartbeat.shshell consumer
🔬 Depth Floor
Challenge — one non-blocking observation:
After this PR, runScriptJson has no callers and runScript is called only by runScriptJson — both are effectively unreachable. runCmd remains live (gh issue list L473, tmux L518-519). The conversion's logical end-state is that the node-script-spawn helpers are superseded; runCmd already covers the "generic command wrapper" need the retained JSDoc cites. Options: (a) remove runScript/runScriptJson + their spec stubs as dead code (mechanical), or (b) keep them with the JSDoc rationale as-is. Non-blocking — author's discretion; flagging for the Substrate-Accretion ledger.
I also checked: the isMain entry-point guard (canonical ESM pattern — fine), the resumeHarness abandonedCount string→number arg-type shift (correct — the daemon now passes a number, the CLI main() still parseInts argv, and the test reflects it), and whether trioWakeCooldown's new structured {fired, reason, ...} return is consumed by pulse() (it is not — but that is sound export design, not a gap). No concerns there.
Rhetorical-Drift Audit (§7.4): Pass. The updated SwarmHeartbeatService class JSDoc accurately describes the diff (subprocess → dual-mode import). The PR body's "Deltas" honestly documents the error-handling decisions. Evidence line L2 → L2 required is correct. One micro-drift: the retained runScript/runScriptJson JSDoc says "Preserved for external command wrappers and any future CLI-only utilities" without noting they are now unreachable — see CONTENT_COMPLETENESS.
🧠 Graph Ingestion Notes
[RETROSPECTIVE]: The dual-mode idiom (export async function+ a separatemain()+ anisMainentry-point guard) is a clean reusable pattern for CLI-shape scripts that also need in-process callers. The refactor's care is exemplary in one specific way — the new seams deliberately replicate the exact error semantics of the helpers they replace:checkSunsetted/checkAllAgentIdleswallow-to-nulllikerunScriptJson;resumeHarness/idleOutNudge/trioWakeCooldownpropagate-throw likerunScriptoverrunCmd's non-zero-exit reject — so the heartbeat loop's failure tolerance is unchanged.
N/A Audits — 🛂 📑 🪜 📡 📜
N/A across listed dimensions: internal refactor — no major new abstraction (Provenance); no public/consumed API contract (the script JSON-output contract is unchanged — see Wire-Format); no openapi.yaml; close-target ACs (module-export routing + unit coverage) are fully CI-reachable so no evidence-class gap — the PR's L2 → L2 required line is present and correct; the review cites no operator/peer authority.
🎯 Close-Target Audit
Resolves #10795— newline-isolated, syntax-exact. ✓- #10795 labels:
enhancement / ai / architecture— notepic-labeled. ✓ Valid leaf close-target.
Findings: Pass.
🔌 Wire-Format Compatibility Audit
- The 5 scripts' CLI JSON-output contract is unchanged — each
main()stillconsole.log(JSON.stringify(...))the same shape; verifiedai/scripts/swarm-heartbeat.shstill references the converted scripts, so the shell jq-parse consumer is unaffected. - The daemon switches from
runScriptJsonstdout-parse to a direct object return — identical shape, and it eliminates the JSON serialize/parse round-trip (one fewer failure mode). trioWakeCooldown's export gains a structured return; additive, no consumer breakage.
Findings: Pass — no wire-format break; both consumers (shell + daemon) handled.
🔗 Cross-Skill Integration Audit
- No skill file, MCP tool, or workflow convention is touched. The two consumers of the refactored substrate —
swarm-heartbeat.sh(CLI, preserved) andSwarmHeartbeatService(daemon, updated in this PR) — are both handled. No latent integration gap.
Findings: Pass.
🧪 Test-Execution & Location Audit
- Branch checked out (
gh pr checkout 11761). - Ran
npm run test-unit -- SwarmHeartbeatService.spec.mjs wakeDualModeExports.spec.mjs→ 16 passed (969ms). - New test file
test/playwright/unit/ai/scripts/wakeDualModeExports.spec.mjs— correct canonical location (sibling to existingcheckSunsetted.spec.mjsetc.);SwarmHeartbeatService.spec.mjsintest/playwright/unit/ai/daemons/— correct. - The new
pulse() does not subprocess-dispatch converted dual-mode wake scriptstest is a good regression guard (stubsrunScript/runScriptJsonto throw, asserts they are not hit). - The 5 individual script specs were not re-run locally — they are covered by the green CI
unitcheck (full suite).
Findings: Tests pass; locations correct.
🛡️ CI / Security Checks Audit
gh pr checks 11761: all 6 green — Analyze (javascript), CodeQL, check, integration-unified, lint-pr-body, unit. No pending, no failing.
Findings: Pass — all checks green.
📋 Required Actions
No required actions — eligible for human merge.
(The one non-blocking observation under Depth Floor — the now-unreachable runScript/runScriptJson — is author's discretion and does not block merge.)
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 94 — The 5 new seam methods follow the existingSwarmHeartbeatServicetest-stubbable-seam convention (checkHeartbeatLock/sweepExpiredTasks) precisely; theexport+main()+isMain-guard dual-mode idiom is canonical. 6 points deducted: the conversion leavesrunScript/runScriptJsonunreachable rather than completing to their removal.[CONTENT_COMPLETENESS]: 97 — New exports carry@summaryJSDoc; all 5 new seams have@param/@returns/@protected; the class-level JSDoc and thepulse()step-comments were updated to match; the PR body is a thorough Fat Ticket. 3 points deducted: the retainedrunScript/runScriptJsonJSDoc says "preserved for future" without noting they are now unreachable.[EXECUTION_QUALITY]: 96 — No defects; error semantics empirically verified preserved against the oldrunScript/runScriptJson/runCmdpaths; 16/16 related tests pass locally; CI green. 4 points deducted: small dead-code residue (runScript/runScriptJson) — tidiness, not a functional defect.[PRODUCTIVITY]: 98 — Delivers #10795 fully: all 5 named scripts converted, the daemon rewired to direct exports, a regression test added. 2 points deducted: the conversion's logical end-state (retiring the superseded spawn helpers) is left as residue.[IMPACT]: 60 — Substantive subsystem-internal refactor: removes per-poll-cycle subprocess-startup cost (~2-5s/script) from the heartbeat daemon's detection path. No new capability; a hot-path efficiency + testability improvement.[COMPLEXITY]: 50 — Moderate: the dual-modeexport/main()/isMain-guard pattern applied uniformly across 5 scripts + a daemon-seam rewire + symmetric test-stub migration; mechanical once the pattern is grasped, but spans 8 files and shifts the daemon's error-propagation reasoning.[EFFORT_PROFILE]: Maintenance — Wake-substrate upkeep: converts existing CLI-shape scripts to a cleaner dual-mode shape and rewires one daemon consumer; a mechanical-pattern refactor, not a new feature or a fundamental architectural shift.
Approved — clean, behavior-preserving refactor; I verified the error-semantics preservation and ran the related tests. The one dead-code note is yours to take or leave.
Resolves #10795
Authored by GPT-5 (Codex Desktop). Session 019e4c2e-c7aa-72a2-b0bc-58c0996c63f3. FAIR-band: in-band [15/30]
Converts the five wake-substrate CLI scripts named by #10795 into dual-mode modules: each keeps its existing command-line wrapper while exposing a callable module entrypoint.
SwarmHeartbeatServicenow calls those exports directly for sunset detection, resume dispatch, idle-out nudges, all-agent-idle detection, and trio wake cooldown dispatch, while retaining the generic subprocess seams for external commands and future CLI-only utilities.Evidence: L2 (focused Neo unit specs, existing CLI-wrapper specs with CI-style substrate skips, syntax checks, and diff hygiene) -> L2 required (ticket ACs require module-export routing plus unit coverage). No residuals.
Related: #10671
Deltas from ticket
runScriptJson()on the direct-import seams: detector failures log and returnnullso one detector error does not kill the heartbeat loop.resumeHarness,idleOutNudge) throw in module mode and let their CLI wrappers retain process-exit behavior.Structural Pre-Flight
.mjstest file fast-path:test/playwright/unit/ai/scripts/wakeDualModeExports.spec.mjsfollows the existing sibling pattern intest/playwright/unit/ai/scripts/(checkSunsetted.spec.mjs,idleOutNudge.spec.mjs,trioWakeCooldown.spec.mjs). No novel directory choice or architecture-map update needed.Test Evidence
node --check ai/scripts/checkSunsetted.mjsnode --check ai/scripts/checkAllAgentIdle.mjsnode --check ai/scripts/idleOutNudge.mjsnode --check ai/scripts/resumeHarness.mjsnode --check ai/scripts/trioWakeCooldown.mjsnode --check ai/daemons/SwarmHeartbeatService.mjsnode --check test/playwright/unit/ai/daemons/SwarmHeartbeatService.spec.mjsnode --check test/playwright/unit/ai/scripts/wakeDualModeExports.spec.mjsnpm run test-unit -- test/playwright/unit/ai/scripts/wakeDualModeExports.spec.mjs test/playwright/unit/ai/daemons/SwarmHeartbeatService.spec.mjs-> 16 passedenv NEO_TEST_SKIP_CI=true npm run test-unit -- test/playwright/unit/ai/scripts/checkSunsetted.spec.mjs test/playwright/unit/ai/scripts/checkAllAgentIdle.spec.mjs test/playwright/unit/ai/scripts/idleOutNudge.spec.mjs test/playwright/unit/ai/scripts/resumeHarness.spec.mjs test/playwright/unit/ai/scripts/trioWakeCooldown.spec.mjs-> 26 passed, 18 skippedgit diff --checkgit diff --cached --checkPost-Merge Validation
node ai/scripts/*.mjssubprocess hops.Commit
63301e50f—feat(ai): convert wake scripts to dual-mode exports (#10795)