fix(observer): repair daemon boot (stale resolver name) and close analysis stdin (#2452) (#2464)

Two continuous-learning-v2 observer regressions reported in #2452:

- start-observer.sh still called _ecc_resolve_homunculus_dir, but the
  shared lib was renamed to _clv2_resolve_homunculus_dir (with observe.sh
  and detect-project.sh updated, start-observer.sh missed). Under set -e
  every launch dies with exit 127 at line 40 - daemon boot is broken on
  all platforms, not just Windows.
- observer-loop.sh backgrounds the analysis claude call with stdin left
  open; on Git Bash/MSYS2 the child inherits it, waits, warns 'no stdin
  data received', and exits 1 before reading the analysis file. Close
  stdin with </dev/null while keeping the -p prompt flag, preserving the
  Windows-compat decision from #842 instead of reverting to a stdin
  redirect.

Adds two source invariant guards to tests/hooks/hooks.test.js: every
*_resolve_homunculus_dir call site must match a function the shared lib
defines, and the backgrounded claude call must close stdin.

Fixes #2452
This commit is contained in:
Jun
2026-07-22 12:17:19 -04:00
committed by GitHub
parent cd39df154c
commit a1bf029cbf
2 changed files with 38 additions and 1 deletions
@@ -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 </dev/null: on Git Bash/MSYS2 the backgrounded
# child otherwise inherits an open stdin, and claude waits on it, warns
# "no stdin data received", and exits 1 before reading the analysis file (#2452).
# Model is configurable via ECC_OBSERVER_MODEL (defaults to haiku for cost efficiency);
# e.g. ECC_OBSERVER_MODEL=opus for higher-quality instinct extraction. Heavier models are
# slower — consider raising ECC_OBSERVER_TIMEOUT_SECONDS (default 120s) so the watchdog
# doesn't kill the analysis mid-run.
ECC_SKIP_OBSERVE=1 ECC_HOOK_PROFILE=minimal claude --model "${ECC_OBSERVER_MODEL:-haiku}" --max-turns "$max_turns" --print \
--allowedTools "Read,Write" \
-p "$prompt_content" >> "$LOG_FILE" 2>&1 &
-p "$prompt_content" < /dev/null >> "$LOG_FILE" 2>&1 &
claude_pid=$!
(
+34
View File
@@ -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++;