diff --git a/skills/continuous-learning-v2/agents/observer-loop.sh b/skills/continuous-learning-v2/agents/observer-loop.sh index 698cd8b68..f75365920 100755 --- a/skills/continuous-learning-v2/agents/observer-loop.sh +++ b/skills/continuous-learning-v2/agents/observer-loop.sh @@ -248,13 +248,16 @@ PROMPT # 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). + # stdin is explicitly closed with > "$LOG_FILE" 2>&1 & + -p "$prompt_content" < /dev/null >> "$LOG_FILE" 2>&1 & claude_pid=$! ( diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index 2830fbed3..82724226a 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -3175,6 +3175,40 @@ async function runTests() { passed++; else failed++; + if ( + test('observer scripts only call homunculus resolvers the shared lib defines (#2452)', () => { + const skillRoot = path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2'); + const libSource = fs.readFileSync(path.join(skillRoot, 'scripts', 'lib', 'homunculus-dir.sh'), 'utf8'); + const definedResolvers = new Set([...libSource.matchAll(/^([A-Za-z_][A-Za-z0-9_]*_resolve_homunculus_dir)\(\)/gm)].map((m) => m[1])); + assert.ok(definedResolvers.size > 0, 'homunculus-dir.sh should define a homunculus resolver function'); + + const callers = [ + ['agents', 'start-observer.sh'], + ['hooks', 'observe.sh'], + ['scripts', 'detect-project.sh'], + ['scripts', 'migrate-homunculus.sh'] + ]; + for (const rel of callers) { + const callerSource = fs.readFileSync(path.join(skillRoot, ...rel), 'utf8'); + for (const match of callerSource.matchAll(/([A-Za-z_][A-Za-z0-9_]*_resolve_homunculus_dir)\b/g)) { + assert.ok(definedResolvers.has(match[1]), `${rel.join('/')} calls ${match[1]}, which homunculus-dir.sh does not define (stale name breaks daemon boot under set -e)`); + } + } + }) + ) + passed++; + else failed++; + + if ( + test('observer-loop closes stdin on the backgrounded claude analysis call (#2452)', () => { + const observerLoopSource = fs.readFileSync(path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'agents', 'observer-loop.sh'), 'utf8'); + + assert.ok(observerLoopSource.includes('-p "$prompt_content" < /dev/null'), 'observer-loop should close stdin on the backgrounded claude call so Git Bash children do not hang on inherited stdin and exit 1'); + }) + ) + passed++; + else failed++; + if (SKIP_BASH) { console.log(' ⊘ detect-project exports the resolved Python command (skipped on Windows)'); passed++;