Context
Surfaced as a non-blocking observation during PR #10602 Cycle 2 review (Auto-Wakeup Substrate Phase 1, merged earlier). Filing as scoped follow-up because the auto-wakeup substrate is now live on dev and the cron-invocation safety gap should land before any operator schedules swarm-heartbeat.sh via launchd / cron / wrapper-script invocation.
The Problem
ai/scripts/resumeHarness.mjs line 23 builds the cooldown directory path via process.cwd():
const cooldownDir = path.resolve(process.cwd(), '.neo-ai-data/wake-daemon');
This works correctly when swarm-heartbeat.sh is invoked from the repo root (the canonical operator path). But cron / launchd / generic wrapper-script invocations frequently set cwd to /, the user's $HOME, or the launchd plist's WorkingDirectory setting — none of which are necessarily the repo root.
When cwd ≠ repo root:
- The cooldown file lands at
<wrong-cwd>/.neo-ai-data/wake-daemon/cooldown-<identity>.txt instead of the canonical <repo>/.neo-ai-data/wake-daemon/cooldown-<identity>.txt.
- The next heartbeat cycle's idempotency check looks for the cooldown file at the canonical path, doesn't find it, and fires another resume keystroke.
- The 600s cooldown discipline silently fails; auto-wakeup substrate spams osascript every 5 minutes against the same already-resumed Antigravity session.
This is the AC4 idempotency violation we explicitly closed in Cycle 1 of #10602 — but only along the canonical-cwd path. The cron path bypasses the guard.
The Architectural Reality
Sibling scripts in the same ai/scripts/ directory (e.g., bootstrapWorktree.mjs) use script-relative resolution to avoid this exact failure mode:
const projectRoot = path.resolve(__dirname, '..', '..');
__dirname resolves to the script's actual file location regardless of cwd. The cooldown directory should anchor on __dirname, not process.cwd().
The Fix
Single-line change in ai/scripts/resumeHarness.mjs:
const cooldownDir = path.resolve(process.cwd(), '.neo-ai-data/wake-daemon');
import {fileURLToPath} from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, '..', '..');
const cooldownDir = path.resolve(repoRoot, '.neo-ai-data/wake-daemon');
Same pattern as bootstrapWorktree.mjs lines ~399-401. The existing path import already exists; only fileURLToPath from url needs to be added.
Acceptance Criteria
Out of Scope
- The other PR #10602 follow-up nits (AGENTS_STARTUP §6 auto-wakeup mention, session-sunset workflow cross-reference) — separate concerns, separate tickets if picked up.
- Cross-harness adapter generalization (#10604 covers Phase 2 work).
- Refactoring
swarm-heartbeat.sh invocation pattern.
Avoided Traps
- Trap: defer fix until first empirical breakage. Rejected — cron / launchd invocation is exactly where AC4 idempotency matters most (long-running unattended substrate). Pre-empting the regression preserves the strategic-pivot success window.
- Trap: change cooldown path itself instead of resolution method. Rejected — the canonical path (
<repo>/.neo-ai-data/wake-daemon/) is correct + co-located with bridge.log per the existing convention. Only the resolution method needs fixing.
- Trap: bundle with #10604 HarnessPresence work. Rejected — different scope (cooldown-path is a substrate-internal correctness issue; #10604 is the HarnessPresence registry / appIdentifier enum).
Related
- Empirical anchor: PR #10602 Cycle 2 review (Depth Floor delta-cycle observation).
- Parent epic: #10601 Auto-wakeup substrate.
- Adjacent precedent:
bootstrapWorktree.mjs __dirname-based projectRoot resolution.
Origin Session ID: 86b7a3a0-7b14-4bd1-b707-52c5741aaeeb
Retrieval Hint: "resumeHarness cooldown path process.cwd __dirname cron invocation safety auto-wakeup idempotency"
Context
Surfaced as a non-blocking observation during PR #10602 Cycle 2 review (Auto-Wakeup Substrate Phase 1, merged earlier). Filing as scoped follow-up because the auto-wakeup substrate is now live on
devand the cron-invocation safety gap should land before any operator schedulesswarm-heartbeat.shvia launchd / cron / wrapper-script invocation.The Problem
ai/scripts/resumeHarness.mjsline 23 builds the cooldown directory path viaprocess.cwd():const cooldownDir = path.resolve(process.cwd(), '.neo-ai-data/wake-daemon');This works correctly when
swarm-heartbeat.shis invoked from the repo root (the canonical operator path). But cron / launchd / generic wrapper-script invocations frequently setcwdto/, the user's$HOME, or the launchd plist'sWorkingDirectorysetting — none of which are necessarily the repo root.When
cwd≠ repo root:<wrong-cwd>/.neo-ai-data/wake-daemon/cooldown-<identity>.txtinstead of the canonical<repo>/.neo-ai-data/wake-daemon/cooldown-<identity>.txt.This is the AC4 idempotency violation we explicitly closed in Cycle 1 of #10602 — but only along the canonical-cwd path. The cron path bypasses the guard.
The Architectural Reality
Sibling scripts in the same
ai/scripts/directory (e.g.,bootstrapWorktree.mjs) use script-relative resolution to avoid this exact failure mode:const projectRoot = path.resolve(__dirname, '..', '..'); // ai/scripts/ → ai/ → root__dirnameresolves to the script's actual file location regardless ofcwd. The cooldown directory should anchor on__dirname, notprocess.cwd().The Fix
Single-line change in
ai/scripts/resumeHarness.mjs:// Before: const cooldownDir = path.resolve(process.cwd(), '.neo-ai-data/wake-daemon'); // After: import {fileURLToPath} from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, '..', '..'); const cooldownDir = path.resolve(repoRoot, '.neo-ai-data/wake-daemon');Same pattern as
bootstrapWorktree.mjslines ~399-401. The existingpathimport already exists; onlyfileURLToPathfromurlneeds to be added.Acceptance Criteria
resumeHarness.mjsresolves the cooldown directory via__dirnamerather thanprocess.cwd().node /Users/Shared/github/neomjs/neo/ai/scripts/resumeHarness.mjs @neo-gemini-3-1-pro testfrom/tmp(or any non-repo cwd) writes the cooldown file to the canonical<repo>/.neo-ai-data/wake-daemon/cooldown-neogemini31pro.txtlocation.test/playwright/unit/ai/scripts/resumeHarness.spec.mjs(new file) that verifies the path resolution is__dirname-anchored. Pattern: read the script source, grep forprocess.cwd(), assert it's NOT used for cooldown-path construction.Out of Scope
swarm-heartbeat.shinvocation pattern.Avoided Traps
<repo>/.neo-ai-data/wake-daemon/) is correct + co-located withbridge.logper the existing convention. Only the resolution method needs fixing.Related
bootstrapWorktree.mjs__dirname-based projectRoot resolution.Origin Session ID: 86b7a3a0-7b14-4bd1-b707-52c5741aaeeb Retrieval Hint: "resumeHarness cooldown path process.cwd __dirname cron invocation safety auto-wakeup idempotency"