From 2dc76bf2039e87314e0c24006ba95d02a25540d6 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sun, 10 May 2026 20:51:12 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20lessons-learned=20loop=20=E2=80=94=20wi?= =?UTF-8?q?n=20signals=20+=20skill=5Fedit=20auto-apply?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds two new hook signal types: - correction_free_streak: 5 consecutive UserPromptSubmits without a correction phrase - clean_recovery: 3 clean PostToolUse events after a struggle signal (tool_error_loop / dead_end / retry_loop) Both carry active_skills/active_agents payloads computed from a 10-event activity ring, so ADAM can attribute wins to whichever skill was active during the streak/recovery. Promotes skill_edit to auto-apply under a strict gate (all required): - conf >= 4 + cross-session evidence (existing rules) - # Why cites a win-signal entry whose active_skills includes target - diff append-only, +lines <= 30 - resulting SKILL.md size <= 2x current size - 7-day cooldown per target (last_auto_edit in applied/ frontmatter) - 30-day blacklist on user rejection (auto_apply_blacklist in rejected/) Skill enforces the gate at apply time as defense in depth: re-stats target, re-checks cooldown and blacklist, verifies append-only, reverts and refuses on byte-cap breach. User-rejected skill_edit proposals automatically write auto_apply_blacklist: true. Win signals participate in the existing v0.2.0 source_entries archive lifecycle, so already-applied evidence does not re-cluster. Test suite: +5 cases (5 new asserts pass), 27 total passing. Spec: ~/.claude/docs/superpowers/specs/2026-05-10-adam-proactive-design.md Plan: ~/.claude/docs/superpowers/plans/2026-05-10-adam-proactive.md --- adam/tests/run-tests.sh | 72 +++++++++++++++++++++++++ agents/adam.md | 31 +++++++++-- hooks/adam-observe.mjs | 75 ++++++++++++++++++++++++--- skills/adam-self-improvement/SKILL.md | 22 ++++++-- 4 files changed, 187 insertions(+), 13 deletions(-) diff --git a/adam/tests/run-tests.sh b/adam/tests/run-tests.sh index 7baa025..c4f4136 100755 --- a/adam/tests/run-tests.sh +++ b/adam/tests/run-tests.sh @@ -279,6 +279,78 @@ else fi rm -rf /tmp/adam-test-18 +# --- Test 19: correction_free_streak fires after 5 clean prompts --- +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 +done +assert_grep "$ROOT/journal.jsonl" '"type":"correction_free_streak"' "5 clean prompts logs correction_free_streak" + +# --- Test 20: correction phrase resets streak counter --- +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 +done +echo '{"hook_event_name":"UserPromptSubmit","prompt":"no, undo that","session_id":"sCB","cwd":"/tmp/x"}' \ + | node "$HOOK" >/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 +if grep -qE '"type":"correction_free_streak"' "$ROOT/journal.jsonl"; then + echo " FAIL: correction_free_streak fired despite intervening correction"; FAIL=$((FAIL+1)) +else + echo " PASS: correction phrase reset the streak counter"; PASS=$((PASS+1)) +fi + +# --- Test 21: clean_recovery fires after struggle + 3 clean tools --- +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 +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 +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" + +# --- Test 22: clean_recovery resets when error breaks the streak --- +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 +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 +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 +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 +if grep -qE '"type":"clean_recovery"' "$ROOT/journal.jsonl"; then + echo " FAIL: clean_recovery fired despite intervening error"; FAIL=$((FAIL+1)) +else + echo " PASS: clean_recovery suppressed by intervening error"; PASS=$((PASS+1)) +fi + +# --- Test 23: active_skills payload populated on win signals --- +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 +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 +done +assert_grep "$ROOT/journal.jsonl" '"active_skills":\["caveman"\]' "active_skills payload includes invoked skill" + echo echo "Results: $PASS passed, $FAIL failed" [ "$FAIL" = "0" ] diff --git a/agents/adam.md b/agents/adam.md index 3f4b027..52ee3f8 100644 --- a/agents/adam.md +++ b/agents/adam.md @@ -43,6 +43,8 @@ The hook emits these `type` values into the journal: | `edit_churn` | same file edited 4× in window | file basename | | `build_loop` | 2 build/test/compile commands fail in session | session | | `subagent_dispatch_pattern` | same subagent dispatched ≥3× cumulatively | subagent_type | +| `correction_free_streak` | 5 clean UserPromptSubmits in a row (no correction phrase) | `active_skills[0]` | +| `clean_recovery` | 3 clean PostToolUse events after a `tool_error_loop`/`dead_end`/`retry_loop` | (`recovered_from`, `active_skills[0]`) | ## Process @@ -65,6 +67,8 @@ The hook emits these `type` values into the journal: - `edit_churn`: cluster by file basename pattern (e.g. `*.test.ts`). - `build_loop`: cluster by `session`. - `subagent_dispatch_pattern`: cluster by `subagent_type`. + - `correction_free_streak`: cluster by `active_skills[0]`. Treat ≥3 streaks across ≥2 sessions naming the same skill as cross-session evidence. + - `clean_recovery`: cluster by (`recovered_from`, `active_skills[0]`). A win cluster qualifies for `skill_edit` only when the named skill exists in `skills_root`. 5. **Multi-axis correlation**: for each session that produced ≥2 distinct struggle types (`tool_error_loop`, `dead_end`, `weak_agent`, `retry_loop`, `edit_churn`, `build_loop`), tag clusters from that session as `multi_axis: true`. This grants +1 confidence at scoring. 6. For each cluster qualifying under the rubric — ≥3 occurrences across ≥2 sessions, OR (for struggle types) ≥1 entry within a single session, OR (for `correction`) ≥3 occurrences across ≥2 cwds: a. If cluster topic matches a rejected idea via the rejected-ideas fuzzy set (≥2 token overlap with rejection's `# Why`), skip with reason `"rejected-similar"`. @@ -172,6 +176,23 @@ Constraints: - Slug (used in `target` path filename) must not collide with any existing memory file. - For `type=feedback` and `type=project`, body MUST contain `**Why:**` and `**How to apply:**` lines (CLAUDE.md memory schema). +## Win-driven `skill_edit` eligibility + +A `skill_edit` proposal sets `auto_apply_eligible: true` ONLY when ALL hold: + +1. `confidence ≥ 4`. +2. `cross_session_evidence == true`. +3. `# Why` cites ≥1 win-signal entry (`clean_recovery` or `correction_free_streak`) whose `active_skills` includes the target skill slug. Record this entry's `ts` in frontmatter field `win_evidence`. +4. Diff is append-only — verify no `-` lines on existing SKILL.md content. +5. Diff `+` lines ≤ 30. +6. Resulting SKILL.md size ≤ 2× current size. Record both byte counts in frontmatter fields `bytes_before`, `bytes_after`. +7. No entry in `applied_dir/` for the same `target` with `last_auto_edit` newer than 7 days ago (cooldown). +8. No entry in `rejected_dir/` for this `target` with `auto_apply_blacklist: true` newer than 30 days ago. + +If any of (3)–(8) fails: still emit the proposal, but `auto_apply_eligible: false` — main thread queues for review. + +Win clusters do NOT override struggle clusters: a single `clean_recovery` cannot turn a `correction` cluster into a `skill_edit`. Struggle paths and win paths are independent. + ## Confidence rubric (deterministic — do NOT vibe) Sum: @@ -189,16 +210,16 @@ Sum: `auto_apply_eligible: true` requires **all** of: - `confidence ≥ 4` - `blast_radius == "low"` -- `type ∈ {memory, skill_new}` +- `type ∈ {memory, skill_new, skill_edit}` — `skill_edit` additionally requires the win-driven gate (see "Win-driven `skill_edit` eligibility") - `cross_session_evidence == true` — the +2 signal-repetition bonus came from the cross-session bullet (≥3× across ≥2 sessions). **Single-session-only struggle proposals always queue, never auto-apply, regardless of total confidence.** Record as frontmatter field `cross_session_evidence: true|false` on every proposal. ## Proposal types | Type | Target | Default blast | Auto-apply? | |---|---|---|---| -| `memory` | `~/.claude/projects//memory/*.md` | low | yes if conf≥4 AND cross_session | +| `memory` | `~/.claude/projects/-Users-nvm/memory/*.md` | low | yes if conf≥4 AND cross_session | | `skill_new` | new dir under `~/.claude/skills/` | low | yes if conf≥4 AND cross_session | -| `skill_edit` | existing skill file | medium | no | +| `skill_edit` | existing skill file | medium | yes if win-evidence + LOC + cooldown gates all pass (see "Win-driven skill_edit eligibility") | | `agent_new` | new file under `~/.claude/agents/` | medium | no | | `agent_edit` | existing agent file | medium | no | | `claude_md_edit` | `~/.claude/CLAUDE.md` | high | no | @@ -243,6 +264,10 @@ source_entries: - "" - "" - "..." +# skill_edit only — required when auto_apply_eligible: true +win_evidence: "" +bytes_before: +bytes_after: --- # Why diff --git a/hooks/adam-observe.mjs b/hooks/adam-observe.mjs index 4611009..aeaeffb 100755 --- a/hooks/adam-observe.mjs +++ b/hooks/adam-observe.mjs @@ -28,6 +28,10 @@ const DEAD_END_THRESHOLD = 8; const EDIT_CHURN_THRESHOLD = 4; const BUILD_LOOP_THRESHOLD = 2; const SUBAGENT_DISPATCH_THRESHOLD = 3; +const CORRECTION_FREE_THRESHOLD = 5; +const CLEAN_RECOVERY_WINDOW = 3; +const STRUGGLE_TYPES = new Set(["tool_error_loop", "dead_end", "retry_loop"]); +const ACTIVE_SKILLS_LOOKBACK = 10; const STATE_MAX_BYTES = 1_000_000; function safeRead(path, fallback) { @@ -77,6 +81,17 @@ function readUsage(name) { return usage[name] || 0; } +function pushActivity(state, kind, name, ts) { + state.activity_ring.push({ kind, name, ts }); + if (state.activity_ring.length > ACTIVE_SKILLS_LOOKBACK) state.activity_ring.shift(); +} + +function activeNames(state, kind) { + const seen = new Set(); + for (const e of state.activity_ring) if (e.kind === kind) seen.add(e.name); + return [...seen]; +} + function errorFingerprint(toolResponse) { if (!toolResponse) return null; let text = ""; @@ -114,6 +129,8 @@ function resetSessionLocal(state) { resetFrictionCounters(state); state.session_subagents = {}; state.subagent_dispatch_emitted = {}; + state.correctionFreeCounter = 0; + state.recoveryWatch = null; } function ensureStateDefaults(state) { @@ -127,6 +144,9 @@ function ensureStateDefaults(state) { if (typeof state.build_loop_emitted !== "boolean") state.build_loop_emitted = false; if (!state.session_subagents || typeof state.session_subagents !== "object") state.session_subagents = {}; if (!state.subagent_dispatch_emitted || typeof state.subagent_dispatch_emitted !== "object") state.subagent_dispatch_emitted = {}; + if (typeof state.correctionFreeCounter !== "number") state.correctionFreeCounter = 0; + if (state.recoveryWatch === undefined) state.recoveryWatch = null; + if (!Array.isArray(state.activity_ring)) state.activity_ring = []; } function main() { @@ -155,6 +175,18 @@ function main() { prev_tool: last.tool || null, prev_file: last.file || null, }); + state.correctionFreeCounter = 0; + } else { + state.correctionFreeCounter += 1; + if (state.correctionFreeCounter >= CORRECTION_FREE_THRESHOLD) { + appendJournal({ + ts, session, cwd, type: "correction_free_streak", + streak: state.correctionFreeCounter, + active_skills: activeNames(state, "skill"), + active_agents: activeNames(state, "agent"), + }); + state.correctionFreeCounter = 0; + } } resetFrictionCounters(state); } else if (event === "PreToolUse") { @@ -162,9 +194,11 @@ function main() { if (tool === "Skill") { const name = (input.tool_input && (input.tool_input.skill || input.tool_input.skill_name)) || "unknown"; bumpUsage(`skill:${name}`); + pushActivity(state, "skill", name, ts); } else if (tool === "Agent") { const name = (input.tool_input && (input.tool_input.subagent_type || input.tool_input.agent)) || "unknown"; bumpUsage(`agent:${name}`); + pushActivity(state, "agent", name, ts); state.session_subagents[name] = (state.session_subagents[name] || 0) + 1; const cumulative = readUsage(`agent:${name}`); const sessionCount = state.session_subagents[name]; @@ -182,6 +216,12 @@ function main() { const argsHash = djb2(JSON.stringify(input.tool_input || {})); const file = (input.tool_input && (input.tool_input.file_path || input.tool_input.path)) || null; + let struggleEmittedThisTurn = null; + const emit = (entry) => { + if (STRUGGLE_TYPES.has(entry.type)) struggleEmittedThisTurn = entry.type; + appendJournal(entry); + }; + const windowEntry = { tool, argsHash, file }; if (tool === "Agent") { const sub = (input.tool_input && (input.tool_input.subagent_type || input.tool_input.agent)) || "unknown"; @@ -192,14 +232,14 @@ function main() { const sameToolArgs = state.tool_window.filter(e => e.tool === tool && e.argsHash === argsHash).length; if (sameToolArgs >= RETRY_THRESHOLD) { - appendJournal({ ts, session, cwd, type: "retry_loop", tool, count: sameToolArgs }); + emit({ ts, session, cwd, type: "retry_loop", tool, count: sameToolArgs }); } if (tool === "Agent") { const subagent = (input.tool_input && (input.tool_input.subagent_type || input.tool_input.agent)) || "unknown"; const recent = state.tool_window.slice(-5).filter(e => e.tool === "Agent" && e.subagent === subagent).length; if (recent >= AGENT_RESPAWN_THRESHOLD) { - appendJournal({ ts, session, cwd, type: "weak_agent", subagent_type: subagent, count: recent }); + emit({ ts, session, cwd, type: "weak_agent", subagent_type: subagent, count: recent }); } } @@ -214,14 +254,14 @@ function main() { if (state.last_errors.length > ERROR_RING_SIZE) state.last_errors.shift(); const sameError = state.last_errors.filter(e => e.fp === fp).length; if (sameError >= ERROR_LOOP_THRESHOLD) { - appendJournal({ ts, session, cwd, type: "tool_error_loop", tool, count: sameError, fp }); + emit({ ts, session, cwd, type: "tool_error_loop", tool, count: sameError, fp }); } } if (file && EDIT_TOOLS.has(tool)) { state.edit_counts[file] = (state.edit_counts[file] || 0) + 1; if (state.edit_counts[file] >= EDIT_CHURN_THRESHOLD && !state.edit_churn_emitted[file]) { - appendJournal({ ts, session, cwd, type: "edit_churn", file, count: state.edit_counts[file] }); + emit({ ts, session, cwd, type: "edit_churn", file, count: state.edit_counts[file] }); state.edit_churn_emitted[file] = true; } const keys = Object.keys(state.edit_counts); @@ -239,7 +279,7 @@ function main() { if (isBuildCmd && hasError) { state.build_failure_count += 1; if (state.build_failure_count >= BUILD_LOOP_THRESHOLD && !state.build_loop_emitted) { - appendJournal({ ts, session, cwd, type: "build_loop", count: state.build_failure_count, command: cmd.slice(0, 80) }); + emit({ ts, session, cwd, type: "build_loop", count: state.build_failure_count, command: cmd.slice(0, 80) }); state.build_loop_emitted = true; } } @@ -247,9 +287,32 @@ function main() { state.tools_since_user += 1; if (state.tools_since_user >= DEAD_END_THRESHOLD && !state.dead_end_emitted) { - appendJournal({ ts, session, cwd, type: "dead_end", count: state.tools_since_user }); + emit({ ts, session, cwd, type: "dead_end", count: state.tools_since_user }); state.dead_end_emitted = true; } + + if (struggleEmittedThisTurn) { + state.recoveryWatch = { recovered_from: struggleEmittedThisTurn, since_ts: ts, clean_count: 0, window_tools: [] }; + } else if (state.recoveryWatch) { + const turnHadError = fp !== null; + if (turnHadError) { + state.recoveryWatch = null; + } else { + state.recoveryWatch.clean_count += 1; + state.recoveryWatch.window_tools.push(tool); + if (state.recoveryWatch.window_tools.length > CLEAN_RECOVERY_WINDOW) state.recoveryWatch.window_tools.shift(); + if (state.recoveryWatch.clean_count >= CLEAN_RECOVERY_WINDOW) { + appendJournal({ + ts, session, cwd, type: "clean_recovery", + recovered_from: state.recoveryWatch.recovered_from, + recovery_window_tools: state.recoveryWatch.window_tools.slice(), + active_skills: activeNames(state, "skill"), + active_agents: activeNames(state, "agent"), + }); + state.recoveryWatch = null; + } + } + } } safeWrite(STATE, state); diff --git a/skills/adam-self-improvement/SKILL.md b/skills/adam-self-improvement/SKILL.md index 8386ce2..beda87f 100644 --- a/skills/adam-self-improvement/SKILL.md +++ b/skills/adam-self-improvement/SKILL.md @@ -44,8 +44,22 @@ For each id in `high_confidence`: - Verify in front of the user: print `id`, `target`, `confidence`, `blast_radius`, `cross_session_evidence`, `auto_apply_eligible`. - Apply the change: - **For `skill_new`**: `mkdir -p ~/.claude/skills//`, then `Write` the proposal's `# Proposed change` body to `~/.claude/skills//SKILL.md`. After write, print: "skill `` written to `~/.claude/skills//SKILL.md` — activates immediately — Claude Code v2.1.0+ auto-hot-reloads user-level skills, no restart needed." - - **For `memory`**: `Write` the proposal's `# Proposed change` body (which MUST include the auto-memory frontmatter — see "Memory drafting protocol" in `agents/adam.md`) to the path in `target` (under `~/.claude/projects//memory/`, where `` is the user's home dir with `/` replaced by `-`, e.g. `-Users-alice` on macOS). Then update `MEMORY.md` index with a one-line pointer. - - **For other types under auto-apply**: apply via Write/Edit per `# Proposed change`. (Note: only `memory` and `skill_new` qualify for auto-apply per the rubric.) + - **For `memory`**: `Write` the proposal's `# Proposed change` body (which MUST include the auto-memory frontmatter — see "Memory drafting protocol" in `agents/adam.md`) to the path in `target`. Then update `MEMORY.md` index with a one-line pointer. + - **For `skill_edit`**: enforce the apply-time gate before writing. + 1. Verify proposal frontmatter has `auto_apply_eligible: true`. If not, abort and queue for review. + 2. Read `target` SKILL.md, capture `current_bytes` from a fresh stat — do NOT trust frontmatter `bytes_before`. + 3. Verify diff in `# Proposed change`: + - Unified-diff format. + - Zero `-` lines on existing SKILL.md content (additions only). + - Total `+` lines ≤ 30. + If any check fails, print one-line refusal reason, leave proposal in `proposals/`, continue. + 4. Cooldown re-check: scan `applied/` frontmatter for `target` matching this and `last_auto_edit` newer than 7 days ago. Refuse if found. + 5. Blacklist re-check: scan `rejected/` frontmatter for `target` matching this and `auto_apply_blacklist: true` newer than 30 days ago. Refuse if found. + 6. Apply via `Edit` tool (append the new section per the diff). Never use `Write` on existing SKILL.md. + 7. Re-stat target. If new size exceeds `2 * current_bytes` (captured in step 2), revert via `Edit` (remove the just-appended section) and refuse — print refusal reason. + 8. Add `last_auto_edit: ` to the proposal frontmatter before moving it. + 9. Tell user: "skill `` extended (added lines) — auto-applied via win-evidence gate." + - **For other types under auto-apply**: apply via Write/Edit per `# Proposed change`. (Note: only `memory`, `skill_new`, and `skill_edit` qualify for auto-apply per the rubric.) - Move proposal to `~/.claude/adam/applied/-.md`. - **Archive consumed journal entries**: `node ~/.claude/adam/scripts/adam-archive.mjs ~/.claude/adam/applied/-.md` — moves entries listed in proposal's `source_entries` from `journal.jsonl` to `journal/actioned-.jsonl` so subsequent `/reflect` runs do not re-cluster them. @@ -66,7 +80,7 @@ c. On **approve**: - For all others: apply via Write/Edit per the proposal's `# Proposed change`. - Move proposal to `~/.claude/adam/applied/-.md`. - Archive: `node ~/.claude/adam/scripts/adam-archive.mjs ~/.claude/adam/applied/-.md`. -d. On **reject**: ask for reason in one line. Append `# Reason\n` to proposal body. Move to `~/.claude/adam/rejected/.md`. Archive: `node ~/.claude/adam/scripts/adam-archive.mjs ~/.claude/adam/rejected/.md`. +d. On **reject**: ask for reason in one line. Append `# Reason\n` to proposal body. If the proposal `type` is `skill_edit`, ALSO add `auto_apply_blacklist: true` to its frontmatter (so future reflects skip auto-apply on this target for 30 days). Move to `~/.claude/adam/rejected/.md`. Archive: `node ~/.claude/adam/scripts/adam-archive.mjs ~/.claude/adam/rejected/.md`. e. On **edit**: ask the user for the change, edit the proposal in place, then loop back to step 3a for that same id. ### 4. Handle failures @@ -96,7 +110,7 @@ Before writing any proposal: - For `claude_md_edit`: confirm 3+ distinct cwds in the `# Why` section. - For `deletion`: confirm both criteria (a) and (b) from the agent's special handling are documented in the proposal. - For `skill_new`: confirm the slug doesn't collide with any existing skill in `~/.claude/skills/`. If it does, refuse and ask user to rename. -- For `skill_edit`: confirm the diff is append-only (no `-` lines that remove existing content) and that target SKILL.md exists. +- For `skill_edit`: confirm the diff is append-only (no `-` lines that remove existing content) and that target SKILL.md exists. When auto-applying, ALSO re-verify the eligibility gate steps in §2 (cooldown, blacklist, byte cap) before any `Edit` call — never trust frontmatter alone. - For `memory`: confirm `# Proposed change` body starts with `---` frontmatter containing required fields `name`, `description`, `type`, `originSessionId`. Refuse if frontmatter missing — agent must redraft per the Memory drafting protocol. - Confirm `source_entries` is present in proposal frontmatter as a non-empty list (used for archive). Warn (do not refuse) if missing — legacy proposals from before v0.2.0 won't have it.