mirror of
https://github.com/lukaszraczylo/claude-adam.git
synced 2026-10-13 04:59:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ccb07a412 | ||
|
|
f418afa275 | ||
|
|
e3fe691b45 |
@@ -13,7 +13,7 @@ Watches the friction in your coding sessions, clusters the signals via an LLM an
|
|||||||
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://github.com/lukaszraczylo/claude-adam/releases)
|
[](https://github.com/lukaszraczylo/claude-adam/releases)
|
||||||
[](./adam/tests/run-tests.sh)
|
[](./adam/tests/run-tests.sh)
|
||||||
[](https://nodejs.org)
|
[](https://nodejs.org)
|
||||||
[]()
|
[]()
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ The installer copies files into `~/.claude/`, offers to merge ADAM's hook entrie
|
|||||||
Then:
|
Then:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
bash ~/.claude/adam/tests/run-tests.sh # expect: 145 passed, 0 failed
|
bash ~/.claude/adam/tests/run-tests.sh # expect: 150 passed, 0 failed
|
||||||
# … start a fresh Claude Code session …
|
# … start a fresh Claude Code session …
|
||||||
/reflect # walks the proposal queue
|
/reflect # walks the proposal queue
|
||||||
/reflect --explain # also shows the analyst's clustering trace
|
/reflect --explain # also shows the analyst's clustering trace
|
||||||
@@ -235,7 +235,7 @@ Or pass `--explain` to `/reflect` to render the full trace inline.
|
|||||||
```
|
```
|
||||||
~/.claude/
|
~/.claude/
|
||||||
├── hooks/
|
├── hooks/
|
||||||
│ ├── adam-observe.mjs # signal collector (UserPromptSubmit / PreToolUse / PostToolUse)
|
│ ├── adam-observe.mjs # signal collector (UserPromptSubmit / PreToolUse / PostToolUse / PostToolUseFailure)
|
||||||
│ └── adam-nudge.mjs # SessionStart reminder + pending-upgrade warning
|
│ └── adam-nudge.mjs # SessionStart reminder + pending-upgrade warning
|
||||||
├── agents/adam.md # analyst subagent (system prompt + rubric)
|
├── agents/adam.md # analyst subagent (system prompt + rubric)
|
||||||
├── skills/adam-self-improvement/
|
├── skills/adam-self-improvement/
|
||||||
@@ -270,11 +270,12 @@ Or pass `--explain` to `/reflect` to render the full trace inline.
|
|||||||
│ ├── adam-apply-nudge.mjs # nudge proposal apply (message + cue)
|
│ ├── adam-apply-nudge.mjs # nudge proposal apply (message + cue)
|
||||||
│ ├── adam-upgrade.mjs # .adam-new file UX (list/diff/accept)
|
│ ├── adam-upgrade.mjs # .adam-new file UX (list/diff/accept)
|
||||||
│ └── adam-archive.mjs # post-apply journal cleanup
|
│ └── adam-archive.mjs # post-apply journal cleanup
|
||||||
└── tests/run-tests.sh # 145 isolated tests of the tree it sits in; never touches live state
|
└── tests/run-tests.sh # 150 isolated tests of the tree it sits in; never touches live state
|
||||||
```
|
```
|
||||||
|
|
||||||
## What's new
|
## What's new
|
||||||
|
|
||||||
|
- **v0.7.1** — ADAM now sees real tool failures. Claude Code sends failed tool calls to the `PostToolUseFailure` hook event, which ADAM never registered, so every `tool_error_loop` came from `ERROR_RE` matching successful output. `adam-observe.mjs` now handles `PostToolUseFailure` through the same detectors as `PostToolUse` (interrupts excluded), and `settings.json.example` registers it; re-run `install.sh` or add the entry by hand. Successful Read, Edit and Write responses are no longer fingerprinted: source files containing words such as `error` produced 151 false `tool_error_loop` entries across 9 sessions. Found by ADAM's own `/reflect`. 150 tests (up from 145).
|
||||||
- **v0.7.0** — query-conditioned nudges and a lighter `/reflect` skill. A nudge can carry `cue.cwd`: `adam-nudge.mjs` replays it at SessionStart only in that codebase, and `adam-observe.mjs` now also surfaces it once mid-session on a matching `UserPromptSubmit` (opt out with `ADAM_NO_MIDSESSION_NUDGE=1`). New `adam-apply-nudge.mjs` builds the nudge message and cue from proposal frontmatter, so the analyst no longer hand-writes JSON. New `adam-skill-utility.mjs` ranks skills by good:bad outcome co-occurrence (Wilson lower bound, lift against baseline) and feeds a utility-lift rubric penalty; the analyst re-diagnoses unclear or top-severity clusters once per run. `adam-batch.mjs` gains an optional `--max-batches` / `--top-fraction` cap (off by default) and reports `capped`. The per-type apply steps move from `SKILL.md` to `apply-by-type.md`, so `/reflect` loads less context until it applies a proposal. The test harness now tests the tree it sits in, not `~/.claude`, so a repo checkout no longer tests an older install. 145 tests (up from 140).
|
- **v0.7.0** — query-conditioned nudges and a lighter `/reflect` skill. A nudge can carry `cue.cwd`: `adam-nudge.mjs` replays it at SessionStart only in that codebase, and `adam-observe.mjs` now also surfaces it once mid-session on a matching `UserPromptSubmit` (opt out with `ADAM_NO_MIDSESSION_NUDGE=1`). New `adam-apply-nudge.mjs` builds the nudge message and cue from proposal frontmatter, so the analyst no longer hand-writes JSON. New `adam-skill-utility.mjs` ranks skills by good:bad outcome co-occurrence (Wilson lower bound, lift against baseline) and feeds a utility-lift rubric penalty; the analyst re-diagnoses unclear or top-severity clusters once per run. `adam-batch.mjs` gains an optional `--max-batches` / `--top-fraction` cap (off by default) and reports `capped`. The per-type apply steps move from `SKILL.md` to `apply-by-type.md`, so `/reflect` loads less context until it applies a proposal. The test harness now tests the tree it sits in, not `~/.claude`, so a repo checkout no longer tests an older install. 145 tests (up from 140).
|
||||||
- **v0.6.4** — rollback now keeps its promise. `adam-rollback.mjs`'s docstring always claimed it "removes the ab-tracking entry (so it doesn't re-trigger)," but `executeRollback()` never did — so a rolled-back proposal kept flagging as `regressed` on every subsequent `/reflect`, triggering endless `not_found` rollback attempts. It now deletes the matching `ab-tracking.jsonl` row by `proposal_id` (preserving unrelated rows). Surfaced by running ADAM's own loop twice. 140 tests (up from 138).
|
- **v0.6.4** — rollback now keeps its promise. `adam-rollback.mjs`'s docstring always claimed it "removes the ab-tracking entry (so it doesn't re-trigger)," but `executeRollback()` never did — so a rolled-back proposal kept flagging as `regressed` on every subsequent `/reflect`, triggering endless `not_found` rollback attempts. It now deletes the matching `ab-tracking.jsonl` row by `proposal_id` (preserving unrelated rows). Surfaced by running ADAM's own loop twice. 140 tests (up from 138).
|
||||||
- **v0.6.3** — release-update notifier. `install.sh` now writes a `~/.claude/adam/.version` marker; `adam-nudge.mjs` (SessionStart) compares it against the latest GitHub release at most once/day (cached, 1.5 s network cap, best-effort — never blocks) and prints a **notify-only** one-line update prompt. Deliberately not auto-applied: re-running the installer resets ADAM's own `/reflect`-applied skill edits, so you choose when to update. Opt out with `ADAM_NO_UPDATE_CHECK=1`. See "Staying up to date". 138 tests (up from 134).
|
- **v0.6.3** — release-update notifier. `install.sh` now writes a `~/.claude/adam/.version` marker; `adam-nudge.mjs` (SessionStart) compares it against the latest GitHub release at most once/day (cached, 1.5 s network cap, best-effort — never blocks) and prints a **notify-only** one-line update prompt. Deliberately not auto-applied: re-running the installer resets ADAM's own `/reflect`-applied skill edits, so you choose when to update. Opt out with `ADAM_NO_UPDATE_CHECK=1`. See "Staying up to date". 138 tests (up from 134).
|
||||||
|
|||||||
@@ -187,6 +187,25 @@ for i in 1 2 3; do
|
|||||||
done
|
done
|
||||||
assert_grep "$ROOT/journal.jsonl" '"type":"tool_error_loop"' "3x same error logs tool_error_loop"
|
assert_grep "$ROOT/journal.jsonl" '"type":"tool_error_loop"' "3x same error logs tool_error_loop"
|
||||||
|
|
||||||
|
# --- Test 9b: PostToolUseFailure feeds the same detectors ---
|
||||||
|
echo "Test 9b: 3x PostToolUseFailure with the same error logs tool_error_loop"
|
||||||
|
reset_state
|
||||||
|
for i in 1 2 3; do
|
||||||
|
echo '{"hook_event_name":"PostToolUseFailure","tool_name":"Bash","tool_input":{"command":"npm test"},"error":"Exit code 1\nError: Cannot find module express","is_interrupt":false,"session_id":"s9b","cwd":"/tmp/x"}' \
|
||||||
|
| HOOK_RUN >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
assert_grep "$ROOT/journal.jsonl" '"type":"tool_error_loop"' "3x PostToolUseFailure logs tool_error_loop"
|
||||||
|
|
||||||
|
# --- Test 9c: interrupted tool calls are not errors ---
|
||||||
|
echo "Test 9c: 8x PostToolUseFailure with is_interrupt:true counts as tool use but logs no tool_error_loop"
|
||||||
|
reset_state
|
||||||
|
for i in 1 2 3 4 5 6 7 8; do
|
||||||
|
echo '{"hook_event_name":"PostToolUseFailure","tool_name":"Bash","tool_input":{"command":"sleep 99"},"error":"Error: aborted","is_interrupt":true,"session_id":"s9c","cwd":"/tmp/x"}' \
|
||||||
|
| HOOK_RUN >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
assert_grep "$ROOT/journal.jsonl" '"type":"dead_end"' "interrupted calls still count as tool events"
|
||||||
|
assert_no_grep "$ROOT/journal.jsonl" '"type":"tool_error_loop"' "interrupts do not log tool_error_loop"
|
||||||
|
|
||||||
# --- Test 10: dead_end on long autonomous run ---
|
# --- Test 10: dead_end on long autonomous run ---
|
||||||
echo "Test 10: dead_end after 8 tools without UserPromptSubmit"
|
echo "Test 10: dead_end after 8 tools without UserPromptSubmit"
|
||||||
reset_state
|
reset_state
|
||||||
@@ -631,6 +650,27 @@ else
|
|||||||
echo " FAIL: hex/epoch fingerprint mismatch (a=$fp_a b=$fp_b)"; FAIL=$((FAIL+1))
|
echo " FAIL: hex/epoch fingerprint mismatch (a=$fp_a b=$fp_b)"; FAIL=$((FAIL+1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- Test 36b: fingerprint — file-tool success objects are not errors ---
|
||||||
|
echo "Test 36b: Read/Edit/Write success objects with error words return null"
|
||||||
|
fp_r=$(node -e "import('$HOOK').then(m => console.log(m.errorFingerprint({type:'text',file:{filePath:'/x/cache.go',content:'return fmt.Errorf(\"failed: not found\")'}})))")
|
||||||
|
fp_e=$(node -e "import('$HOOK').then(m => console.log(m.errorFingerprint({filePath:'/x/a.ts',oldString:'throw new Error(1)',newString:'x',originalFile:'undefined error'})))")
|
||||||
|
fp_w=$(node -e "import('$HOOK').then(m => console.log(m.errorFingerprint({type:'create',filePath:'/x/b.ts',content:'const errorRateTier = \"failed\"'})))")
|
||||||
|
if [ "$fp_r" = "null" ] && [ "$fp_e" = "null" ] && [ "$fp_w" = "null" ]; then
|
||||||
|
echo " PASS: file-tool success shapes not fingerprinted"; PASS=$((PASS+1))
|
||||||
|
else
|
||||||
|
echo " FAIL: success shape fingerprinted (r=$fp_r e=$fp_e w=$fp_w)"; FAIL=$((FAIL+1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Test 36c: fingerprint — real error shapes still fingerprinted ---
|
||||||
|
echo "Test 36c: is_error:true with filePath and plain error strings still fingerprint"
|
||||||
|
fp_x=$(node -e "import('$HOOK').then(m => console.log(m.errorFingerprint({is_error:true,filePath:'/x/a.ts',content:'String to replace not found in file.'})))")
|
||||||
|
fp_s=$(node -e "import('$HOOK').then(m => console.log(m.errorFingerprint('Error: result (70,310 characters) exceeds maximum allowed tokens')))")
|
||||||
|
if [ "$fp_x" != "null" ] && [ "$fp_s" != "null" ]; then
|
||||||
|
echo " PASS: error shapes keep fingerprints ($fp_x, $fp_s)"; PASS=$((PASS+1))
|
||||||
|
else
|
||||||
|
echo " FAIL: error shape lost fingerprint (x=$fp_x s=$fp_s)"; FAIL=$((FAIL+1))
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Test 37: correction corpus — strong tokens fire ---
|
# --- Test 37: correction corpus — strong tokens fire ---
|
||||||
echo "Test 37: strong-correction tokens each emit correction signal"
|
echo "Test 37: strong-correction tokens each emit correction signal"
|
||||||
strong_ok=1
|
strong_ok=1
|
||||||
|
|||||||
+16
-5
@@ -249,6 +249,12 @@ function snapshotContext(state) {
|
|||||||
|
|
||||||
function errorFingerprint(toolResponse) {
|
function errorFingerprint(toolResponse) {
|
||||||
if (!toolResponse) return null;
|
if (!toolResponse) return null;
|
||||||
|
// Read/Edit/Write success objects carry `file`/`filePath` and no `is_error`.
|
||||||
|
// PostToolUse never receives failed calls (CC routes them to
|
||||||
|
// PostToolUseFailure), so ERROR_RE here would only match source-file text.
|
||||||
|
if (typeof toolResponse === "object" && !Array.isArray(toolResponse) &&
|
||||||
|
toolResponse.is_error !== true &&
|
||||||
|
("file" in toolResponse || "filePath" in toolResponse)) return null;
|
||||||
let text = "";
|
let text = "";
|
||||||
if (typeof toolResponse === "string") text = toolResponse;
|
if (typeof toolResponse === "string") text = toolResponse;
|
||||||
else if (toolResponse.content !== undefined) {
|
else if (toolResponse.content !== undefined) {
|
||||||
@@ -459,12 +465,17 @@ function main() {
|
|||||||
state.subagent_dispatch_emitted[name] = true;
|
state.subagent_dispatch_emitted[name] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (event === "PostToolUse") {
|
} else if (event === "PostToolUse" || event === "PostToolUseFailure") {
|
||||||
const tool = input.tool_name || "unknown";
|
const tool = input.tool_name || "unknown";
|
||||||
const argsHash = djb2(JSON.stringify(input.tool_input || {}));
|
const argsHash = djb2(JSON.stringify(input.tool_input || {}));
|
||||||
const file = (input.tool_input && (input.tool_input.file_path || input.tool_input.path)) || null;
|
const file = (input.tool_input && (input.tool_input.file_path || input.tool_input.path)) || null;
|
||||||
|
|
||||||
const toolResponse = input.tool_response;
|
// Claude Code sends failed tool calls to PostToolUseFailure with the error
|
||||||
|
// text in `error`. Shape it like an is_error response so every PostToolUse
|
||||||
|
// detector sees real failures. An interrupt is an abort, not a tool error.
|
||||||
|
const toolResponse = event === "PostToolUseFailure"
|
||||||
|
? { is_error: input.is_interrupt !== true, content: String(input.error || "") }
|
||||||
|
: input.tool_response;
|
||||||
const respExcerpt = (() => {
|
const respExcerpt = (() => {
|
||||||
if (!toolResponse) return null;
|
if (!toolResponse) return null;
|
||||||
const text = typeof toolResponse === "string" ? toolResponse
|
const text = typeof toolResponse === "string" ? toolResponse
|
||||||
@@ -535,11 +546,11 @@ function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.tool_response && typeof input.tool_response === "object") {
|
if (toolResponse && typeof toolResponse === "object") {
|
||||||
bumpUsage("payload:tool_response_seen");
|
bumpUsage("payload:tool_response_seen");
|
||||||
}
|
}
|
||||||
|
|
||||||
const fp = errorFingerprint(input.tool_response);
|
const fp = errorFingerprint(toolResponse);
|
||||||
if (fp) {
|
if (fp) {
|
||||||
bumpUsage("payload:tool_response_error_seen");
|
bumpUsage("payload:tool_response_error_seen");
|
||||||
if (state.recentRecoveries.length) {
|
if (state.recentRecoveries.length) {
|
||||||
@@ -584,7 +595,7 @@ function main() {
|
|||||||
if (tool === "Bash") {
|
if (tool === "Bash") {
|
||||||
const cmd = (input.tool_input && input.tool_input.command) || "";
|
const cmd = (input.tool_input && input.tool_input.command) || "";
|
||||||
const isBuildCmd = BUILD_RE.test(cmd);
|
const isBuildCmd = BUILD_RE.test(cmd);
|
||||||
const hasError = (input.tool_response && input.tool_response.is_error === true) || fp !== null;
|
const hasError = (toolResponse && toolResponse.is_error === true) || fp !== null;
|
||||||
if (isBuildCmd && hasError) {
|
if (isBuildCmd && hasError) {
|
||||||
state.build_failure_count += 1;
|
state.build_failure_count += 1;
|
||||||
if (state.build_failure_count >= BUILD_LOOP_THRESHOLD && !state.build_loop_emitted) {
|
if (state.build_failure_count >= BUILD_LOOP_THRESHOLD && !state.build_loop_emitted) {
|
||||||
|
|||||||
@@ -34,6 +34,17 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"PostToolUseFailure": [
|
||||||
|
{
|
||||||
|
"matcher": "",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node \"$HOME/.claude/hooks/adam-observe.mjs\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"SessionStart": [
|
"SessionStart": [
|
||||||
{
|
{
|
||||||
"hooks": [
|
"hooks": [
|
||||||
|
|||||||
Reference in New Issue
Block a user