mirror of
https://github.com/lukaszraczylo/claude-adam.git
synced 2026-07-11 04:01:46 +00:00
v0.3.1: code review pass + DX overhaul
Bug fixes (HIGH):
- adam-observe.mjs: errorFingerprint no longer false-positives when
toolResponse.is_error === false; ERROR_RE only used as fallback when
is_error is undefined.
- adam-observe.mjs: resetSessionLocal now clears tool_window so retry_loop
cannot fire on the first tool of a new session by matching prior session.
- adam-archive.mjs: ts dedup uses Map<ts, count> instead of Set<ts>; two
journal entries sharing a millisecond are no longer both archived when
only one is referenced in source_entries.
- adam-nudge.mjs: only counts proposal filenames matching
/^\d{4}-\d{2}-\d{3}-/ pattern; README/notes in proposals/ no longer bump.
- skills/adam-self-improvement/SKILL.md: contradiction_flag veto now applied
at apply time (carry-over from earlier review).
Test isolation:
- adam/tests/run-tests.sh: ALWAYS runs against an isolated $HOME under
mktemp -d. Previously truncated live ~/.claude/adam/journal.jsonl on
every run — destructive on production state.
Conciseness:
- agents/adam.md: -19 LOC (cuts: vestigial cursor sentence, duplicate
not-do bullets, blast-radius bullet collapse, Inputs paths delegate to
SKILL.md, win-cluster-vs-struggle-cluster commentary already enforced
by cluster-key separation, # Overlap section spec compressed).
- skills/adam-self-improvement/SKILL.md: -4 LOC (framing paragraph, dead
catch-all bullet for non-eligible types).
Auto-prune script DELETED:
- The cumulative-count primitive cannot distinguish "never used" from
"used before tracking began"; mtime gate is meaningless for installed
files. Auto-prune deferred to v0.4 with a per-key lastSeen schema.
Cross-platform:
- macOS (BSD coreutils) and Linux (Alpine, glibc + musl) verified.
- All scripts use portable forms (stat -f || stat -c, mktemp -d -t).
- README documents platform support explicitly.
DX overhaul:
- install.sh: hardened — supports `curl | bash` via auto-clone,
--version=vX.Y.Z pinning, --yes / --dry-run flags, jq-based
settings.json merge with diff prompt and backup, conservative file
copy that detects local mtime drift and writes <file>.adam-new
instead of clobbering, idempotent across re-runs.
- adam-uninstall.sh: NEW. Soft-archives ~/.claude/adam/ to .bak.<ts>/
by default; --purge to delete; --yes for non-interactive; jq-based
settings.json cleanup with diff prompt.
- README.md: curl one-liner install + version-pinned variant at top,
What's New section through v0.3.1, upgrade-safe data files callout,
uninstaller documentation, platform support note, expanded rubric
showing skill_edit gate.
Test count: 27 passed, 0 failed (was 27 — no regression).
This commit is contained in:
@@ -81,15 +81,20 @@ function main() {
|
||||
}
|
||||
|
||||
const lines = readFileSync(JOURNAL, "utf8").split("\n").filter(Boolean);
|
||||
const tsSet = new Set(sourceEntries);
|
||||
// tsCounts: how many entries with this ts the proposal claims as its own.
|
||||
// Same-millisecond duplicates: only consume up to the recorded count.
|
||||
const tsCounts = new Map();
|
||||
for (const ts of sourceEntries) tsCounts.set(ts, (tsCounts.get(ts) || 0) + 1);
|
||||
const matched = [];
|
||||
const remaining = [];
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const e = JSON.parse(line);
|
||||
if (e.ts && tsSet.has(e.ts)) {
|
||||
const remainingCount = e.ts ? (tsCounts.get(e.ts) || 0) : 0;
|
||||
if (remainingCount > 0) {
|
||||
matched.push(line);
|
||||
tsCounts.set(e.ts, remainingCount - 1);
|
||||
} else {
|
||||
remaining.push(line);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env node
|
||||
// Test driver for ~/.claude/hooks/adam-observe.mjs.
|
||||
// Usage: node test-hook.mjs (runs all tests in this file).
|
||||
// Spawns the hook with synthesized stdin in a tmp HOME, asserts journal contents.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, mkdirSync, readFileSync, existsSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const HOOK = join(fileURLToPath(new URL("../../hooks/adam-observe.mjs", import.meta.url)));
|
||||
|
||||
export function newTmpHome() {
|
||||
const home = mkdtempSync(join(tmpdir(), "adam-test-"));
|
||||
mkdirSync(join(home, ".claude/adam"), { recursive: true });
|
||||
return home;
|
||||
}
|
||||
|
||||
export function feed(home, input) {
|
||||
const r = spawnSync("node", [HOOK], {
|
||||
input: JSON.stringify(input),
|
||||
env: { ...process.env, HOME: home },
|
||||
encoding: "utf8",
|
||||
timeout: 5000,
|
||||
});
|
||||
if (r.status !== 0) throw new Error(`hook exit ${r.status}: ${r.stderr}`);
|
||||
return r;
|
||||
}
|
||||
|
||||
export function readJournal(home) {
|
||||
const p = join(home, ".claude/adam/journal.jsonl");
|
||||
if (!existsSync(p)) return [];
|
||||
return readFileSync(p, "utf8")
|
||||
.trim().split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
||||
}
|
||||
|
||||
export function assert(cond, msg) {
|
||||
if (!cond) { console.error(`FAIL: ${msg}`); process.exit(1); }
|
||||
console.log(`ok: ${msg}`);
|
||||
}
|
||||
|
||||
export function cleanup(home) { try { rmSync(home, { recursive: true, force: true }); } catch {} }
|
||||
|
||||
// Tests below this line — added by subsequent tasks.
|
||||
|
||||
function testCorrectionFreeStreak() {
|
||||
const home = newTmpHome();
|
||||
try {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
feed(home, {
|
||||
hook_event_name: "UserPromptSubmit",
|
||||
session_id: "s1",
|
||||
cwd: "/x",
|
||||
prompt: `please continue with the work item ${i}`,
|
||||
});
|
||||
}
|
||||
const j = readJournal(home);
|
||||
const streaks = j.filter(e => e.type === "correction_free_streak");
|
||||
assert(streaks.length === 1, "exactly one correction_free_streak after 5 clean prompts");
|
||||
assert(streaks[0].streak === 5, "streak field is 5");
|
||||
assert(streaks[0].session === "s1", "session id captured");
|
||||
} finally { cleanup(home); }
|
||||
}
|
||||
|
||||
function testStreakResetsOnSessionChange() {
|
||||
const home = newTmpHome();
|
||||
try {
|
||||
// 4 in s1 (counter=4, no streak yet), then 1 in s2 (counter must reset → 1, no streak)
|
||||
for (let i = 0; i < 4; i++) feed(home, { hook_event_name: "UserPromptSubmit", session_id: "s1", cwd: "/x", prompt: "ok" });
|
||||
feed(home, { hook_event_name: "UserPromptSubmit", session_id: "s2", cwd: "/x", prompt: "ok" });
|
||||
const j = readJournal(home);
|
||||
assert(j.filter(e => e.type === "correction_free_streak").length === 0, "no streak when session changes mid-streak");
|
||||
} finally { cleanup(home); }
|
||||
}
|
||||
|
||||
function testCleanRecovery() {
|
||||
const home = newTmpHome();
|
||||
try {
|
||||
// Trigger tool_error_loop: 3 PostToolUse with same error fingerprint.
|
||||
for (let i = 0; i < 3; i++) {
|
||||
feed(home, {
|
||||
hook_event_name: "PostToolUse",
|
||||
session_id: "s1", cwd: "/x",
|
||||
tool_name: "Bash",
|
||||
tool_input: { command: `echo ${i}` },
|
||||
tool_response: { is_error: true, content: "error: command not found" },
|
||||
});
|
||||
}
|
||||
// Then 3 clean PostToolUse events.
|
||||
for (let i = 0; i < 3; i++) {
|
||||
feed(home, {
|
||||
hook_event_name: "PostToolUse",
|
||||
session_id: "s1", cwd: "/x",
|
||||
tool_name: "Read",
|
||||
tool_input: { file_path: `/tmp/ok-${i}` },
|
||||
tool_response: { content: "fine" },
|
||||
});
|
||||
}
|
||||
const j = readJournal(home);
|
||||
const recs = j.filter(e => e.type === "clean_recovery");
|
||||
assert(recs.length === 1, "one clean_recovery emitted after 3 clean tools post-struggle");
|
||||
assert(recs[0].recovered_from === "tool_error_loop", "recovered_from set");
|
||||
} finally { cleanup(home); }
|
||||
}
|
||||
|
||||
function testRecoveryResetsOnError() {
|
||||
const home = newTmpHome();
|
||||
try {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
feed(home, {
|
||||
hook_event_name: "PostToolUse", session_id: "s1", cwd: "/x",
|
||||
tool_name: "Bash",
|
||||
tool_input: { command: `cmd ${i}` },
|
||||
tool_response: { is_error: true, content: "error: failed" },
|
||||
});
|
||||
}
|
||||
feed(home, { hook_event_name: "PostToolUse", session_id: "s1", cwd: "/x",
|
||||
tool_name: "Read", tool_input: { file_path: "/tmp/a" }, tool_response: { content: "ok" } });
|
||||
feed(home, { hook_event_name: "PostToolUse", session_id: "s1", cwd: "/x",
|
||||
tool_name: "Read", tool_input: { file_path: "/tmp/b" }, tool_response: { content: "ok" } });
|
||||
feed(home, { hook_event_name: "PostToolUse", session_id: "s1", cwd: "/x",
|
||||
tool_name: "Bash", tool_input: { command: "x" }, tool_response: { is_error: true, content: "error: again" } });
|
||||
feed(home, { hook_event_name: "PostToolUse", session_id: "s1", cwd: "/x",
|
||||
tool_name: "Read", tool_input: { file_path: "/tmp/c" }, tool_response: { content: "ok" } });
|
||||
const j = readJournal(home);
|
||||
assert(j.filter(e => e.type === "clean_recovery").length === 0, "no clean_recovery when error breaks the streak");
|
||||
} finally { cleanup(home); }
|
||||
}
|
||||
|
||||
function testActiveSkillsPayload() {
|
||||
const home = newTmpHome();
|
||||
try {
|
||||
feed(home, { hook_event_name: "PreToolUse", session_id: "s1", cwd: "/x",
|
||||
tool_name: "Skill", tool_input: { skill: "my-skill" } });
|
||||
for (let i = 0; i < 5; i++) {
|
||||
feed(home, { hook_event_name: "UserPromptSubmit", session_id: "s1", cwd: "/x", prompt: "ok" });
|
||||
}
|
||||
const j = readJournal(home);
|
||||
const s = j.find(e => e.type === "correction_free_streak");
|
||||
assert(s && Array.isArray(s.active_skills) && s.active_skills.includes("my-skill"),
|
||||
"correction_free_streak payload includes active skill");
|
||||
} finally { cleanup(home); }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
testCorrectionFreeStreak();
|
||||
testStreakResetsOnSessionChange();
|
||||
testCleanRecovery();
|
||||
testRecoveryResetsOnError();
|
||||
testActiveSkillsPayload();
|
||||
console.log("all tests passed");
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) main();
|
||||
+53
-40
@@ -1,8 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# Test harness: ALWAYS runs against an isolated $HOME under mktemp.
|
||||
# The hook/nudge/archive scripts being tested are sourced from the real $HOME
|
||||
# but invoked with HOME="$TMP_HOME" so journal/state/usage write to the sandbox.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$HOME/.claude/adam"
|
||||
HOOK="$HOME/.claude/hooks/adam-observe.mjs"
|
||||
REAL_HOME="$HOME"
|
||||
HOOK="$REAL_HOME/.claude/hooks/adam-observe.mjs"
|
||||
NUDGE="$REAL_HOME/.claude/hooks/adam-nudge.mjs"
|
||||
ARCHIVE="$REAL_HOME/.claude/adam/scripts/adam-archive.mjs"
|
||||
|
||||
TMP_HOME="$(mktemp -d -t adam-test.XXXXXX)"
|
||||
trap 'rm -rf "$TMP_HOME"' EXIT INT TERM
|
||||
mkdir -p "$TMP_HOME/.claude/adam/proposals" "$TMP_HOME/.claude/adam/applied" "$TMP_HOME/.claude/adam/rejected" "$TMP_HOME/.claude/adam/journal"
|
||||
|
||||
ROOT="$TMP_HOME/.claude/adam"
|
||||
HOOK_RUN() { HOME="$TMP_HOME" node "$HOOK" "$@"; }
|
||||
NUDGE_RUN() { HOME="$TMP_HOME" node "$NUDGE" "$@"; }
|
||||
ARCHIVE_RUN() { HOME="$TMP_HOME" node "$ARCHIVE" "$@"; }
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
@@ -40,7 +55,7 @@ assert_grep() {
|
||||
echo "Test 1: user correction"
|
||||
reset_state
|
||||
echo '{"hook_event_name":"UserPromptSubmit","prompt":"no, that is wrong","session_id":"s1","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
assert_lines "$ROOT/journal.jsonl" 1 "correction creates journal entry"
|
||||
assert_grep "$ROOT/journal.jsonl" '"type":"correction"' "entry has correct type"
|
||||
|
||||
@@ -49,7 +64,7 @@ echo "Test 2: retry loop"
|
||||
reset_state
|
||||
for i in 1 2 3; do
|
||||
echo '{"hook_event_name":"PostToolUse","tool_name":"Bash","tool_input":{"command":"ls"},"session_id":"s1","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
done
|
||||
assert_grep "$ROOT/journal.jsonl" '"type":"retry_loop"' "3x same Bash logs retry_loop"
|
||||
|
||||
@@ -57,29 +72,29 @@ assert_grep "$ROOT/journal.jsonl" '"type":"retry_loop"' "3x same Bash logs retry
|
||||
echo "Test 3: usage counter"
|
||||
reset_state
|
||||
echo '{"hook_event_name":"PreToolUse","tool_name":"Skill","tool_input":{"skill":"foo"},"session_id":"s1","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
assert_grep "$ROOT/usage.json" '"skill:foo"' "Skill invocation increments usage counter"
|
||||
|
||||
# --- Test 3b: agent prefix in usage counter ---
|
||||
echo "Test 3b: agent prefix"
|
||||
reset_state
|
||||
echo '{"hook_event_name":"PreToolUse","tool_name":"Agent","tool_input":{"subagent_type":"bar"},"session_id":"s1","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
assert_grep "$ROOT/usage.json" '"agent:bar"' "Agent invocation increments prefixed counter"
|
||||
|
||||
# --- Test 4: weak agent ---
|
||||
echo "Test 4: weak agent"
|
||||
reset_state
|
||||
echo '{"hook_event_name":"PostToolUse","tool_name":"Agent","tool_input":{"subagent_type":"x"},"session_id":"s1","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
echo '{"hook_event_name":"PostToolUse","tool_name":"Agent","tool_input":{"subagent_type":"x"},"session_id":"s1","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
assert_grep "$ROOT/journal.jsonl" '"type":"weak_agent"' "2x same agent logs weak_agent"
|
||||
|
||||
# --- Test 5: hook never blocks (exit 0) ---
|
||||
echo "Test 5: hook always exit 0 even on garbage input"
|
||||
reset_state
|
||||
if echo 'not json' | node "$HOOK" >/dev/null 2>&1; then
|
||||
if echo 'not json' | HOOK_RUN >/dev/null 2>&1; then
|
||||
echo " PASS: garbage input exit 0"; PASS=$((PASS+1))
|
||||
else
|
||||
echo " FAIL: garbage input non-zero exit"; FAIL=$((FAIL+1))
|
||||
@@ -91,7 +106,7 @@ reset_state
|
||||
# Seed journal with > 5 MB to trigger rotation on next write
|
||||
head -c 5500000 /dev/urandom | base64 > "$ROOT/journal.jsonl"
|
||||
echo '{"hook_event_name":"UserPromptSubmit","prompt":"no, that is wrong","session_id":"s1","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
rotated=$(ls "$ROOT/journal/" 2>/dev/null | wc -l | tr -d ' ')
|
||||
if [ "$rotated" -ge "1" ]; then
|
||||
echo " PASS: journal rotated ($rotated archive present)"; PASS=$((PASS+1))
|
||||
@@ -103,10 +118,9 @@ rm -f "$ROOT/journal/"*.jsonl 2>/dev/null
|
||||
|
||||
# --- Test 7: nudge prints reminder when ≥3 proposals ---
|
||||
echo "Test 7: SessionStart nudge"
|
||||
NUDGE="$HOME/.claude/hooks/adam-nudge.mjs"
|
||||
rm -f "$ROOT/proposals/"*.md 2>/dev/null
|
||||
touch "$ROOT/proposals/a.md" "$ROOT/proposals/b.md" "$ROOT/proposals/c.md"
|
||||
out=$(echo '{"hook_event_name":"SessionStart"}' | node "$NUDGE" 2>&1 || true)
|
||||
touch "$ROOT/proposals/2026-05-10-001-memory-a.md" "$ROOT/proposals/2026-05-10-002-skill_new-b.md" "$ROOT/proposals/2026-05-10-003-skill_edit-c.md"
|
||||
out=$(echo '{"hook_event_name":"SessionStart"}' | NUDGE_RUN 2>&1 || true)
|
||||
if echo "$out" | grep -q "3 proposals queued"; then
|
||||
echo " PASS: nudge prints reminder"; PASS=$((PASS+1))
|
||||
else
|
||||
@@ -115,7 +129,7 @@ fi
|
||||
rm -f "$ROOT/proposals/"*.md
|
||||
|
||||
echo "Test 8: nudge silent when 0 proposals"
|
||||
out=$(echo '{"hook_event_name":"SessionStart"}' | node "$NUDGE" 2>&1 || true)
|
||||
out=$(echo '{"hook_event_name":"SessionStart"}' | NUDGE_RUN 2>&1 || true)
|
||||
if [ -z "$out" ]; then
|
||||
echo " PASS: nudge silent"; PASS=$((PASS+1))
|
||||
else
|
||||
@@ -127,7 +141,7 @@ echo "Test 9: tool_error_loop on repeated identical error"
|
||||
reset_state
|
||||
for i in 1 2 3; do
|
||||
echo '{"hook_event_name":"PostToolUse","tool_name":"Bash","tool_input":{"command":"foo"},"tool_response":{"is_error":true,"content":"Error: command not found: foo"},"session_id":"s9","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
done
|
||||
assert_grep "$ROOT/journal.jsonl" '"type":"tool_error_loop"' "3x same error logs tool_error_loop"
|
||||
|
||||
@@ -136,7 +150,7 @@ echo "Test 10: dead_end after 8 tools without UserPromptSubmit"
|
||||
reset_state
|
||||
for i in 1 2 3 4 5 6 7 8; do
|
||||
echo "{\"hook_event_name\":\"PostToolUse\",\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"step$i\"},\"session_id\":\"s10\",\"cwd\":\"/tmp/x\"}" \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
done
|
||||
assert_grep "$ROOT/journal.jsonl" '"type":"dead_end"' "8x PostToolUse without prompt logs dead_end"
|
||||
|
||||
@@ -145,13 +159,13 @@ echo "Test 11: dead_end counter resets on UserPromptSubmit"
|
||||
reset_state
|
||||
for i in 1 2 3 4 5 6 7; do
|
||||
echo "{\"hook_event_name\":\"PostToolUse\",\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"step$i\"},\"session_id\":\"s11\",\"cwd\":\"/tmp/x\"}" \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
done
|
||||
echo '{"hook_event_name":"UserPromptSubmit","prompt":"continue","session_id":"s11","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
for i in 8 9 10 11 12; do
|
||||
echo "{\"hook_event_name\":\"PostToolUse\",\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"step$i\"},\"session_id\":\"s11\",\"cwd\":\"/tmp/x\"}" \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
done
|
||||
if grep -qE '"type":"dead_end"' "$ROOT/journal.jsonl"; then
|
||||
echo " FAIL: dead_end fired despite reset"; FAIL=$((FAIL+1))
|
||||
@@ -164,11 +178,11 @@ echo "Test 12: session change resets dead_end counter"
|
||||
reset_state
|
||||
for i in 1 2 3 4 5 6 7; do
|
||||
echo "{\"hook_event_name\":\"PostToolUse\",\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"a$i\"},\"session_id\":\"sA\",\"cwd\":\"/tmp/x\"}" \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
done
|
||||
# Now switch to session sB. First post-tool in new session should NOT trigger dead_end.
|
||||
echo '{"hook_event_name":"PostToolUse","tool_name":"Bash","tool_input":{"command":"b1"},"session_id":"sB","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
if grep -qE '"type":"dead_end"' "$ROOT/journal.jsonl"; then
|
||||
echo " FAIL: dead_end fired across session boundary"; FAIL=$((FAIL+1))
|
||||
else
|
||||
@@ -180,7 +194,7 @@ echo "Test 13: edit_churn fires after 4 edits to same file"
|
||||
reset_state
|
||||
for i in 1 2 3 4; do
|
||||
echo '{"hook_event_name":"PostToolUse","tool_name":"Edit","tool_input":{"file_path":"/tmp/x.py"},"session_id":"sE","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
done
|
||||
assert_grep "$ROOT/journal.jsonl" '"type":"edit_churn"' "4x edits to same file logs edit_churn"
|
||||
|
||||
@@ -189,7 +203,7 @@ echo "Test 14: build_loop fires after 2 failed builds"
|
||||
reset_state
|
||||
for i in 1 2; do
|
||||
echo '{"hook_event_name":"PostToolUse","tool_name":"Bash","tool_input":{"command":"go test ./..."},"tool_response":{"is_error":true,"content":"FAIL: TestFoo"},"session_id":"sB","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
done
|
||||
assert_grep "$ROOT/journal.jsonl" '"type":"build_loop"' "2x failed test logs build_loop"
|
||||
|
||||
@@ -198,7 +212,7 @@ echo "Test 15: subagent_dispatch_pattern fires after 3 same-type dispatches"
|
||||
reset_state
|
||||
for i in 1 2 3; do
|
||||
echo '{"hook_event_name":"PreToolUse","tool_name":"Agent","tool_input":{"subagent_type":"orchestrator"},"session_id":"sD","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
done
|
||||
assert_grep "$ROOT/journal.jsonl" '"type":"subagent_dispatch_pattern"' "3x same subagent logs subagent_dispatch_pattern"
|
||||
|
||||
@@ -207,7 +221,7 @@ echo "Test 16: build_loop ignores non-build commands"
|
||||
reset_state
|
||||
for i in 1 2 3; do
|
||||
echo '{"hook_event_name":"PostToolUse","tool_name":"Bash","tool_input":{"command":"ls /nope"},"tool_response":{"is_error":true,"content":"No such file"},"session_id":"sN","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
done
|
||||
if grep -qE '"type":"build_loop"' "$ROOT/journal.jsonl"; then
|
||||
echo " FAIL: build_loop fired on non-build command"; FAIL=$((FAIL+1))
|
||||
@@ -217,7 +231,6 @@ fi
|
||||
|
||||
# --- Test 17: adam-archive moves matching entries to actioned file ---
|
||||
echo "Test 17: adam-archive moves matching journal entries"
|
||||
ARCHIVE="$HOME/.claude/adam/scripts/adam-archive.mjs"
|
||||
reset_state
|
||||
rm -f "$ROOT/journal/actioned-test-archive-001.jsonl"
|
||||
cat > "$ROOT/journal.jsonl" <<EOF
|
||||
@@ -242,7 +255,7 @@ source_entries:
|
||||
# Why
|
||||
test
|
||||
EOF
|
||||
node "$ARCHIVE" /tmp/adam-test-17/proposal.md >/dev/null 2>&1 || true
|
||||
ARCHIVE_RUN /tmp/adam-test-17/proposal.md >/dev/null 2>&1 || true
|
||||
remaining=$(wc -l < "$ROOT/journal.jsonl" | tr -d ' ')
|
||||
archived=$(wc -l < "$ROOT/journal/actioned-test-archive-001.jsonl" 2>/dev/null | tr -d ' ' || echo 0)
|
||||
if [ "$remaining" = "1" ] && [ "$archived" = "2" ]; then
|
||||
@@ -265,7 +278,7 @@ type: memory
|
||||
# Why
|
||||
no source_entries
|
||||
EOF
|
||||
node "$ARCHIVE" /tmp/adam-test-18/proposal.md >/dev/null 2>&1 || true
|
||||
ARCHIVE_RUN /tmp/adam-test-18/proposal.md >/dev/null 2>&1 || true
|
||||
if [ -f "$ROOT/journal/actioned-test-noop-002.jsonl" ]; then
|
||||
echo " FAIL: archive file created when no source_entries"; FAIL=$((FAIL+1))
|
||||
else
|
||||
@@ -284,7 +297,7 @@ echo "Test 19: correction_free_streak after 5 clean prompts"
|
||||
reset_state
|
||||
for i in 1 2 3 4 5; do
|
||||
echo "{\"hook_event_name\":\"UserPromptSubmit\",\"prompt\":\"please do step $i\",\"session_id\":\"sCF\",\"cwd\":\"/tmp/x\"}" \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
done
|
||||
assert_grep "$ROOT/journal.jsonl" '"type":"correction_free_streak"' "5 clean prompts logs correction_free_streak"
|
||||
|
||||
@@ -293,12 +306,12 @@ echo "Test 20: correction phrase breaks correction_free_streak"
|
||||
reset_state
|
||||
for i in 1 2 3 4; do
|
||||
echo "{\"hook_event_name\":\"UserPromptSubmit\",\"prompt\":\"please do step $i\",\"session_id\":\"sCB\",\"cwd\":\"/tmp/x\"}" \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
done
|
||||
echo '{"hook_event_name":"UserPromptSubmit","prompt":"no, undo that","session_id":"sCB","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
echo '{"hook_event_name":"UserPromptSubmit","prompt":"go on","session_id":"sCB","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
if grep -qE '"type":"correction_free_streak"' "$ROOT/journal.jsonl"; then
|
||||
echo " FAIL: correction_free_streak fired despite intervening correction"; FAIL=$((FAIL+1))
|
||||
else
|
||||
@@ -310,11 +323,11 @@ echo "Test 21: clean_recovery after struggle + 3 clean tools"
|
||||
reset_state
|
||||
for i in 1 2 3; do
|
||||
echo '{"hook_event_name":"PostToolUse","tool_name":"Bash","tool_input":{"command":"foo"},"tool_response":{"is_error":true,"content":"Error: command not found: foo"},"session_id":"sR","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
done
|
||||
for i in 1 2 3; do
|
||||
echo "{\"hook_event_name\":\"PostToolUse\",\"tool_name\":\"Read\",\"tool_input\":{\"file_path\":\"/tmp/ok-$i\"},\"tool_response\":{\"content\":\"ok\"},\"session_id\":\"sR\",\"cwd\":\"/tmp/x\"}" \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
done
|
||||
assert_grep "$ROOT/journal.jsonl" '"type":"clean_recovery"' "3 clean tools after struggle logs clean_recovery"
|
||||
assert_grep "$ROOT/journal.jsonl" '"recovered_from":"tool_error_loop"' "recovered_from set on clean_recovery"
|
||||
@@ -324,16 +337,16 @@ echo "Test 22: clean_recovery suppressed by intervening error"
|
||||
reset_state
|
||||
for i in 1 2 3; do
|
||||
echo '{"hook_event_name":"PostToolUse","tool_name":"Bash","tool_input":{"command":"foo"},"tool_response":{"is_error":true,"content":"Error: command not found: foo"},"session_id":"sRE","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
done
|
||||
for i in 1 2; do
|
||||
echo "{\"hook_event_name\":\"PostToolUse\",\"tool_name\":\"Read\",\"tool_input\":{\"file_path\":\"/tmp/ok-$i\"},\"tool_response\":{\"content\":\"ok\"},\"session_id\":\"sRE\",\"cwd\":\"/tmp/x\"}" \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
done
|
||||
echo '{"hook_event_name":"PostToolUse","tool_name":"Bash","tool_input":{"command":"x"},"tool_response":{"is_error":true,"content":"Error: again"},"session_id":"sRE","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
echo '{"hook_event_name":"PostToolUse","tool_name":"Read","tool_input":{"file_path":"/tmp/ok-3"},"tool_response":{"content":"ok"},"session_id":"sRE","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
if grep -qE '"type":"clean_recovery"' "$ROOT/journal.jsonl"; then
|
||||
echo " FAIL: clean_recovery fired despite intervening error"; FAIL=$((FAIL+1))
|
||||
else
|
||||
@@ -344,10 +357,10 @@ fi
|
||||
echo "Test 23: correction_free_streak payload includes active skill"
|
||||
reset_state
|
||||
echo '{"hook_event_name":"PreToolUse","tool_name":"Skill","tool_input":{"skill":"caveman"},"session_id":"sAS","cwd":"/tmp/x"}' \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
for i in 1 2 3 4 5; do
|
||||
echo "{\"hook_event_name\":\"UserPromptSubmit\",\"prompt\":\"step $i\",\"session_id\":\"sAS\",\"cwd\":\"/tmp/x\"}" \
|
||||
| node "$HOOK" >/dev/null 2>&1 || true
|
||||
| HOOK_RUN >/dev/null 2>&1 || true
|
||||
done
|
||||
assert_grep "$ROOT/journal.jsonl" '"active_skills":\["caveman"\]' "active_skills payload includes invoked skill"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user