LearnNewsExamplesServices
Frontmatter
id10606
titleresumeHarness cooldown path: use __dirname-based resolution, not process.cwd()
stateClosed
labels
buginvalidairegression
assigneesneo-opus-4-7
createdAtMay 2, 2026, 12:12 AM
updatedAtMay 2, 2026, 12:13 AM
githubUrlhttps://github.com/neomjs/neo/issues/10606
authorneo-opus-4-7
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 2, 2026, 12:13 AM

resumeHarness cooldown path: use __dirname-based resolution, not process.cwd()

Closedbuginvalidairegression
neo-opus-4-7
neo-opus-4-7 commented on May 2, 2026, 12:12 AM

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:

  1. 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.
  2. 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.
  3. 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, '..', '..'); // ai/scripts/ → ai/ → root

__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:

// 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.mjs lines ~399-401. The existing path import already exists; only fileURLToPath from url needs to be added.

Acceptance Criteria

  • (AC1) resumeHarness.mjs resolves the cooldown directory via __dirname rather than process.cwd().
  • (AC2) Empirical: invoking node /Users/Shared/github/neomjs/neo/ai/scripts/resumeHarness.mjs @neo-gemini-3-1-pro test from /tmp (or any non-repo cwd) writes the cooldown file to the canonical <repo>/.neo-ai-data/wake-daemon/cooldown-neogemini31pro.txt location.
  • (AC3) Existing cooldown-file behavior under canonical-cwd invocation is unchanged (idempotent re-fire window still 600s).
  • (AC4) Add a structural test to test/playwright/unit/ai/scripts/resumeHarness.spec.mjs (new file) that verifies the path resolution is __dirname-anchored. Pattern: read the script source, grep for process.cwd(), assert it's NOT used for cooldown-path construction.

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"