From df96abe74cc4a78868247fb4e7499adf95e327eb Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Mon, 6 Apr 2026 14:05:38 -0700 Subject: [PATCH] fix: harden windows observer prompt handling --- .../agents/observer-loop.sh | 21 ++++++++++++++----- tests/hooks/hooks.test.js | 3 +++ tests/hooks/observer-memory.test.js | 10 ++++++++- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/skills/continuous-learning-v2/agents/observer-loop.sh b/skills/continuous-learning-v2/agents/observer-loop.sh index 9a5a6b70..bedc7f67 100755 --- a/skills/continuous-learning-v2/agents/observer-loop.sh +++ b/skills/continuous-learning-v2/agents/observer-loop.sh @@ -169,6 +169,18 @@ Rules: - Examples of project patterns: use React functional components, follow Django REST framework conventions PROMPT + # Read the prompt into memory before the Claude subprocess is spawned. + # On Windows/MSYS2, the mktemp path can differ from the shell's later path + # resolution, so relying on cat "$prompt_file" inside the claude invocation + # can fail even though the file was created successfully. + prompt_content="$(cat "$prompt_file" 2>/dev/null || true)" + rm -f "$prompt_file" + if [ -z "$prompt_content" ]; then + echo "[$(date)] Failed to load observer prompt content, skipping analysis" >> "$LOG_FILE" + rm -f "$analysis_file" + return + fi + timeout_seconds="${ECC_OBSERVER_TIMEOUT_SECONDS:-120}" max_turns="${ECC_OBSERVER_MAX_TURNS:-20}" exit_code=0 @@ -185,17 +197,16 @@ PROMPT # Ensure CWD is PROJECT_DIR so the relative analysis_relpath resolves correctly # on all platforms, not just when the observer happens to be launched from the project root. - cd "$PROJECT_DIR" || { echo "[$(date)] Failed to cd to PROJECT_DIR ($PROJECT_DIR), skipping analysis" >> "$LOG_FILE"; rm -f "$prompt_file" "$analysis_file"; return; } + cd "$PROJECT_DIR" || { echo "[$(date)] Failed to cd to PROJECT_DIR ($PROJECT_DIR), skipping analysis" >> "$LOG_FILE"; rm -f "$analysis_file"; return; } # Prevent observe.sh from recording this automated Haiku session as observations. # Pass prompt via -p flag instead of stdin redirect for Windows compatibility (#842). + # prompt_content is already loaded in-memory so this no longer depends on the + # mktemp absolute path continuing to resolve after cwd changes (#1296). ECC_SKIP_OBSERVE=1 ECC_HOOK_PROFILE=minimal claude --model haiku --max-turns "$max_turns" --print \ --allowedTools "Read,Write" \ - -p "$(cat "$prompt_file")" >> "$LOG_FILE" 2>&1 & + -p "$prompt_content" >> "$LOG_FILE" 2>&1 & claude_pid=$! - # prompt_file content was already expanded by the shell; remove early to avoid - # leaving stale temp files during the (potentially long) analysis window. - rm -f "$prompt_file" ( sleep "$timeout_seconds" diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index 20c52487..4055f87d 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -2433,6 +2433,9 @@ async function runTests() { assert.ok(!observerLoopSource.includes('--max-turns 3'), 'observer-loop should not hardcode a 3-turn limit'); assert.ok(observerLoopSource.includes('ECC_SKIP_OBSERVE=1'), 'observer-loop should suppress observe.sh for automated sessions'); assert.ok(observerLoopSource.includes('ECC_HOOK_PROFILE=minimal'), 'observer-loop should run automated analysis with the minimal hook profile'); + assert.ok(observerLoopSource.includes('prompt_content="$(cat "$prompt_file" 2>/dev/null || true)"'), 'observer-loop should read prompt_file into memory before claude is spawned'); + assert.ok(observerLoopSource.includes('-p "$prompt_content"'), 'observer-loop should pass in-memory prompt content to claude'); + assert.ok(!observerLoopSource.includes('-p "$(cat "$prompt_file")"'), 'observer-loop should not re-read prompt_file at invocation time'); }) ) passed++; diff --git a/tests/hooks/observer-memory.test.js b/tests/hooks/observer-memory.test.js index 36b0da98..892d025b 100644 --- a/tests/hooks/observer-memory.test.js +++ b/tests/hooks/observer-memory.test.js @@ -164,7 +164,8 @@ test('default max analysis lines is 500', () => { test('analysis temp file is created and cleaned up', () => { const content = fs.readFileSync(observerLoopPath, 'utf8'); assert.ok(content.includes('ecc-observer-analysis'), 'Should create a temp analysis file'); - assert.ok(content.includes('rm -f "$prompt_file" "$analysis_file"'), 'Should clean up both prompt and analysis temp files'); + assert.ok(content.includes('rm -f "$prompt_file"'), 'Should clean up the prompt temp file after loading it'); + assert.ok(content.includes('rm -f "$analysis_file"'), 'Should clean up the analysis temp file'); }); test('observer-loop uses project-local temp directory for analysis artifacts', () => { @@ -174,6 +175,13 @@ test('observer-loop uses project-local temp directory for analysis artifacts', ( assert.ok(content.includes('mktemp "${observer_tmp_dir}/ecc-observer-prompt.'), 'Prompt temp file should use the project temp dir'); }); +test('observer-loop loads prompt content before invoking claude', () => { + const content = fs.readFileSync(observerLoopPath, 'utf8'); + assert.ok(content.includes('prompt_content="$(cat "$prompt_file" 2>/dev/null || true)"'), 'Prompt should be read into memory before the claude invocation'); + assert.ok(content.includes('-p "$prompt_content"'), 'Claude should receive the in-memory prompt content'); + assert.ok(!content.includes('-p "$(cat "$prompt_file")"'), 'Claude should not depend on re-reading the prompt file during invocation'); +}); + test('observer-loop prompt requires direct instinct writes without asking permission', () => { const content = fs.readFileSync(observerLoopPath, 'utf8'); const heredocStart = content.indexOf('cat > "$prompt_file" <