mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
Merge pull request #427 from affaan-m/codex/orchestration-harness-skills
fix: harden observe loop prevention
This commit is contained in:
@@ -82,32 +82,13 @@ if [ -n "$STDIN_CWD" ] && [ -d "$STDIN_CWD" ]; then
|
||||
export CLAUDE_PROJECT_DIR="$STDIN_CWD"
|
||||
fi
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Project detection
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# Source shared project detection helper
|
||||
# This sets: PROJECT_ID, PROJECT_NAME, PROJECT_ROOT, PROJECT_DIR
|
||||
source "${SKILL_ROOT}/scripts/detect-project.sh"
|
||||
PYTHON_CMD="${CLV2_PYTHON_CMD:-$PYTHON_CMD}"
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Configuration
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
CONFIG_DIR="${HOME}/.claude/homunculus"
|
||||
OBSERVATIONS_FILE="${PROJECT_DIR}/observations.jsonl"
|
||||
MAX_FILE_SIZE_MB=10
|
||||
|
||||
SENTINEL_FILE="${CLV2_OBSERVER_SENTINEL_FILE:-${PROJECT_ROOT:-$PROJECT_DIR}/.observer.lock}"
|
||||
|
||||
write_guard_sentinel() {
|
||||
printf '%s\n' 'observer paused: confirmation or permission prompt detected; rerun start-observer.sh --reset after reviewing observer.log' > "$SENTINEL_FILE"
|
||||
}
|
||||
|
||||
# Skip if disabled globally
|
||||
if [ -f "$CONFIG_DIR/disabled" ]; then
|
||||
exit 0
|
||||
@@ -119,6 +100,8 @@ fi
|
||||
# - ECC observing its own Haiku observer sessions (self-loop)
|
||||
# - ECC observing other tools' automated sessions (e.g. claude-mem)
|
||||
# - All-night Haiku usage with no human activity
|
||||
# Run these before project detection so skipped sessions cannot mutate
|
||||
# project-scoped observer state.
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
# Env-var checks first (cheapest — no subprocess spawning):
|
||||
@@ -161,6 +144,26 @@ if [ -n "$STDIN_CWD" ]; then
|
||||
done
|
||||
fi
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Project detection
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
SKILL_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# Source shared project detection helper
|
||||
# This sets: PROJECT_ID, PROJECT_NAME, PROJECT_ROOT, PROJECT_DIR
|
||||
source "${SKILL_ROOT}/scripts/detect-project.sh"
|
||||
PYTHON_CMD="${CLV2_PYTHON_CMD:-$PYTHON_CMD}"
|
||||
|
||||
OBSERVATIONS_FILE="${PROJECT_DIR}/observations.jsonl"
|
||||
|
||||
SENTINEL_FILE="${CLV2_OBSERVER_SENTINEL_FILE:-${PROJECT_ROOT:-$PROJECT_DIR}/.observer.lock}"
|
||||
|
||||
write_guard_sentinel() {
|
||||
printf '%s\n' 'observer paused: confirmation or permission prompt detected; rerun start-observer.sh --reset after reviewing observer.log' > "$SENTINEL_FILE"
|
||||
}
|
||||
|
||||
# Skip if a previous run already aborted due to confirmation/permission prompt.
|
||||
# This is the circuit-breaker — stops retrying after a non-interactive failure.
|
||||
if [ -f "$SENTINEL_FILE" ]; then
|
||||
|
||||
@@ -136,6 +136,27 @@ function pathsReferToSameLocation(leftPath, rightPath) {
|
||||
}
|
||||
}
|
||||
|
||||
function createObservePayload(projectDir, overrides = {}) {
|
||||
return JSON.stringify({
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'echo hello' },
|
||||
tool_response: 'ok',
|
||||
session_id: 'session-123',
|
||||
cwd: projectDir,
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
|
||||
function listObservationFiles(homeDir) {
|
||||
const projectsDir = path.join(homeDir, '.claude', 'homunculus', 'projects');
|
||||
if (!fs.existsSync(projectsDir)) return [];
|
||||
|
||||
return fs
|
||||
.readdirSync(projectsDir)
|
||||
.map(projectId => path.join(projectsDir, projectId, 'observations.jsonl'))
|
||||
.filter(observationsPath => fs.existsSync(observationsPath));
|
||||
}
|
||||
|
||||
function createCommandShim(binDir, baseName, logFile) {
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
|
||||
@@ -2414,6 +2435,143 @@ async function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('observe.sh skips non-cli entrypoints without writing observations', async () => {
|
||||
const homeDir = createTestDir();
|
||||
const projectDir = createTestDir();
|
||||
const observePath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'hooks', 'observe.sh');
|
||||
|
||||
try {
|
||||
const result = await runShellScript(observePath, ['post'], createObservePayload(projectDir, { session_id: 'session-non-cli' }), {
|
||||
HOME: homeDir,
|
||||
CLAUDE_PROJECT_DIR: projectDir,
|
||||
CLAUDE_CODE_ENTRYPOINT: 'sdk'
|
||||
}, projectDir);
|
||||
|
||||
assert.strictEqual(result.code, 0, `observe.sh should exit successfully for non-cli entrypoints, stderr: ${result.stderr}`);
|
||||
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'homunculus', 'projects')), 'non-cli entrypoints should exit before project detection runs');
|
||||
assert.deepStrictEqual(listObservationFiles(homeDir), [], 'non-cli entrypoints should not write observations');
|
||||
} finally {
|
||||
cleanupTestDir(homeDir);
|
||||
cleanupTestDir(projectDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('observe.sh skips ECC_SKIP_OBSERVE sessions without writing observations', async () => {
|
||||
const homeDir = createTestDir();
|
||||
const projectDir = createTestDir();
|
||||
const observePath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'hooks', 'observe.sh');
|
||||
|
||||
try {
|
||||
const result = await runShellScript(observePath, ['post'], createObservePayload(projectDir, { session_id: 'session-skip-env' }), {
|
||||
HOME: homeDir,
|
||||
CLAUDE_PROJECT_DIR: projectDir,
|
||||
ECC_SKIP_OBSERVE: '1'
|
||||
}, projectDir);
|
||||
|
||||
assert.strictEqual(result.code, 0, `observe.sh should exit successfully when ECC_SKIP_OBSERVE=1, stderr: ${result.stderr}`);
|
||||
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'homunculus', 'projects')), 'ECC_SKIP_OBSERVE should exit before project detection runs');
|
||||
assert.deepStrictEqual(listObservationFiles(homeDir), [], 'ECC_SKIP_OBSERVE should suppress observation writes');
|
||||
} finally {
|
||||
cleanupTestDir(homeDir);
|
||||
cleanupTestDir(projectDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('observe.sh skips subagent payloads with agent_id without writing observations', async () => {
|
||||
const homeDir = createTestDir();
|
||||
const projectDir = createTestDir();
|
||||
const observePath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'hooks', 'observe.sh');
|
||||
|
||||
try {
|
||||
const result = await runShellScript(
|
||||
observePath,
|
||||
['post'],
|
||||
createObservePayload(projectDir, { session_id: 'session-agent', agent_id: 'agent-123' }),
|
||||
{
|
||||
HOME: homeDir,
|
||||
CLAUDE_PROJECT_DIR: projectDir
|
||||
},
|
||||
projectDir
|
||||
);
|
||||
|
||||
assert.strictEqual(result.code, 0, `observe.sh should exit successfully for subagent sessions, stderr: ${result.stderr}`);
|
||||
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'homunculus', 'projects')), 'subagent sessions should exit before project detection runs');
|
||||
assert.deepStrictEqual(listObservationFiles(homeDir), [], 'subagent sessions should not write observations');
|
||||
} finally {
|
||||
cleanupTestDir(homeDir);
|
||||
cleanupTestDir(projectDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('observe.sh skips default observer-session paths without writing observations', async () => {
|
||||
const homeDir = createTestDir();
|
||||
const projectRoot = createTestDir();
|
||||
const projectDir = path.join(projectRoot, 'observer-sessions-worker');
|
||||
const observePath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'hooks', 'observe.sh');
|
||||
fs.mkdirSync(projectDir, { recursive: true });
|
||||
|
||||
try {
|
||||
const result = await runShellScript(observePath, ['post'], createObservePayload(projectDir, { session_id: 'session-default-skip-path' }), {
|
||||
HOME: homeDir,
|
||||
CLAUDE_PROJECT_DIR: projectDir
|
||||
}, projectDir);
|
||||
|
||||
assert.strictEqual(result.code, 0, `observe.sh should exit successfully for default skip paths, stderr: ${result.stderr}`);
|
||||
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'homunculus', 'projects')), 'default skip paths should exit before project detection runs');
|
||||
assert.deepStrictEqual(listObservationFiles(homeDir), [], 'default skip paths should suppress observation writes');
|
||||
} finally {
|
||||
cleanupTestDir(homeDir);
|
||||
cleanupTestDir(projectRoot);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('observe.sh trims custom skip-path patterns before matching', async () => {
|
||||
const homeDir = createTestDir();
|
||||
const projectRoot = createTestDir();
|
||||
const projectDir = path.join(projectRoot, 'custom-observer-session');
|
||||
const observePath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'hooks', 'observe.sh');
|
||||
fs.mkdirSync(projectDir, { recursive: true });
|
||||
|
||||
try {
|
||||
const result = await runShellScript(observePath, ['post'], createObservePayload(projectDir, { session_id: 'session-custom-skip-path' }), {
|
||||
HOME: homeDir,
|
||||
CLAUDE_PROJECT_DIR: projectDir,
|
||||
ECC_OBSERVE_SKIP_PATHS: ' custom-observer-session , , '
|
||||
}, projectDir);
|
||||
|
||||
assert.strictEqual(result.code, 0, `observe.sh should exit successfully for custom skip paths, stderr: ${result.stderr}`);
|
||||
assert.ok(!fs.existsSync(path.join(homeDir, '.claude', 'homunculus', 'projects')), 'custom skip paths should exit before project detection runs');
|
||||
assert.deepStrictEqual(listObservationFiles(homeDir), [], 'trimmed custom skip paths should suppress observation writes');
|
||||
} finally {
|
||||
cleanupTestDir(homeDir);
|
||||
cleanupTestDir(projectRoot);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('observe.sh ignores empty skip-path entries so normal paths still record observations', async () => {
|
||||
const homeDir = createTestDir();
|
||||
const projectDir = createTestDir();
|
||||
const observePath = path.join(__dirname, '..', '..', 'skills', 'continuous-learning-v2', 'hooks', 'observe.sh');
|
||||
|
||||
try {
|
||||
const result = await runShellScript(observePath, ['post'], createObservePayload(projectDir, { session_id: 'session-empty-skip-paths' }), {
|
||||
HOME: homeDir,
|
||||
CLAUDE_PROJECT_DIR: projectDir,
|
||||
ECC_OBSERVE_SKIP_PATHS: ' , , '
|
||||
}, projectDir);
|
||||
|
||||
assert.strictEqual(result.code, 0, `observe.sh should exit successfully when skip-path entries are empty, stderr: ${result.stderr}`);
|
||||
const observationFiles = listObservationFiles(homeDir);
|
||||
assert.strictEqual(observationFiles.length, 1, 'empty skip-path entries should not suppress normal observations');
|
||||
|
||||
const observations = fs.readFileSync(observationFiles[0], 'utf8').trim().split('\n').filter(Boolean);
|
||||
assert.ok(observations.length > 0, 'normal sessions should still append observations when skip-path entries are empty');
|
||||
} finally {
|
||||
cleanupTestDir(homeDir);
|
||||
cleanupTestDir(projectDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (await asyncTest('matches .tsx extension for type checking', async () => {
|
||||
const testDir = createTestDir();
|
||||
const testFile = path.join(testDir, 'component.tsx');
|
||||
|
||||
Reference in New Issue
Block a user