Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions scripts/claude-mem-heal.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -207,16 +207,39 @@ function Repair-HooksJson {
}
$content = Get-Content $Target -Raw -ErrorAction SilentlyContinue
if (-not $content) { return }
$broken = 'break; }; done'
$fixed = '}; done | head -n1'
if (-not $content.Contains($broken)) {

$broken017 = 'break; }; done'
$fixed017 = '}; done | head -n1'
# BUG-018: ALL hooks that terminate with `hook claude-code <X>"` lack
# Claude Code's {"continue":true,"suppressOutput":true} directive. The
# bun-runner empty-stdin diagnostic (upstream claude-mem#2188) goes to
# stdout and Claude Code blocks the operation. User empirically hit
# UserPromptSubmit first and Stop minutes later -- regex captures all
# 5 (session-init / context / observation / file-context / summarize).
# Setup hook's version-check.js terminator is left untouched -- not on
# the user hot path (fires only on plugin install/update).
$pattern018 = 'hook claude-code ([a-z][a-z-]*)"'
$replacement018 = 'hook claude-code $1 2>/dev/null; echo ''{\"continue\":true,\"suppressOutput\":true}''"'

$has017 = $content.Contains($broken017)
$has018 = $content -match $pattern018
if (-not $has017 -and -not $has018) {
Write-HealVerbose "hooks.json already healthy: $Target"
return
}
$count = ([regex]::Matches($content, [regex]::Escape($broken))).Count
$patched = $content.Replace($broken, $fixed)

$patched = $content
if ($has017) {
$count017 = ([regex]::Matches($patched, [regex]::Escape($broken017))).Count
$patched = $patched.Replace($broken017, $fixed017)
Write-HealLog "patched hooks.json (BUG-017, $count017 hook(s) -> head -n1 race-free form): $Target"
}
if ($has018) {
$count018 = ([regex]::Matches($patched, $pattern018)).Count
$patched = $patched -replace $pattern018, $replacement018
Write-HealLog "patched hooks.json ($count018 hook(s) -> BUG-018 continue directive): $Target"
}
Set-Content -Path $Target -Value $patched -Encoding UTF8 -NoNewline
Write-HealLog "patched hooks.json (BUG-017, $count hook(s) -> head -n1 race-free form): $Target"
}

function Repair-PluginDir {
Expand Down
34 changes: 30 additions & 4 deletions scripts/claude-mem-heal.sh
Original file line number Diff line number Diff line change
Expand Up @@ -148,17 +148,43 @@ heal_zod() {
# breaks early, unconsumed producer writes EPIPE on Git Bash Windows.
# Minimal substitution: `break; }; done` -> `}; done | head -n1` keeps the
# loop running to completion, then head takes the first printed match.
#
# BUG-018 (2026-05-21): after BUG-017 closed the EPIPE race, the
# UserPromptSubmit hook STILL blocked because its command terminates with
# `node ... hook claude-code session-init` and does NOT emit Claude Code's
# `{"continue":true,"suppressOutput":true}` directive. bun-runner.js's
# empty-stdin diagnostic (upstream claude-mem#2188) goes to stdout, Claude
# Code reads it as non-continue, blocks the prompt. Heal appends the
# directive in the same pass.
heal_hooks_json() {
target="$1"
[ -f "$target" ] || { verbose "no hooks.json at $target"; return 0; }
if ! grep -qF 'break; }; done' "$target" 2>/dev/null; then
# Detect either BUG-017 signature (break; }; done) or BUG-018 signature
# (UserPromptSubmit terminator without the continue directive).
has_017=$(grep -cF 'break; }; done' "$target" 2>/dev/null || echo 0)
has_018=$(grep -cE 'session-init"$|session-init"[^}]*$' "$target" 2>/dev/null | head -1)
has_018=${has_018:-0}
# Simpler: also check explicitly for the un-patched session-init terminator.
if ! grep -qF 'break; }; done' "$target" && \
! grep -qF 'session-init"' "$target"; then
verbose "hooks.json already healthy: $target"
return 0
fi
# Use a temp file -- sed -i with portable escaping for the pipe delim.
# Use `#` as sed delimiter -- `|` appears literally in the replacement
# (`done | head -n1`) and would confuse `s|...|...|g`.
#
# BUG-018 substitution is GENERIC across all 5 `hook claude-code <X>"`
# terminators (UserPromptSubmit/session-init, SessionStart/context,
# PostToolUse/observation, PreToolUse/file-context, Stop/summarize).
# The user empirically hit UserPromptSubmit first (BUG-018 narrow scope)
# then Stop minutes later (originally deferred as BUG-018b -- now folded
# in via regex capture). Setup hook (`version-check.js`) is left untouched:
# it only fires on plugin install/update, not user hot path.
tmp="$target.tmp.$$"
sed 's|break; }; done|}; done | head -n1|g' "$target" > "$tmp" && mv "$tmp" "$target"
log "patched hooks.json (BUG-017, head -n1 race-free form): $target"
sed -e 's#break; }; done#}; done | head -n1#g' \
-e 's#hook claude-code \([a-z][a-z-]*\)"#hook claude-code \1 2>/dev/null; echo '\''{\\"continue\\":true,\\"suppressOutput\\":true}'\''"#g' \
"$target" > "$tmp" && mv "$tmp" "$target"
log "patched hooks.json (BUG-017 head-n1 + BUG-018 continue-directive on all 5 hooks): $target"
}

heal_dir() {
Expand Down
46 changes: 46 additions & 0 deletions specs/BUG-018-userpromptsubmit-continue-directive/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
id: "BUG-018-userpromptsubmit-continue-directive"
type: spec
status: implementing
created: "2026-05-21"
tags: [spec, proposal, claude-mem, heal, cross-os-parity, hook-protocol]
template_version: "1.0"
---

# BUG-018-userpromptsubmit-continue-directive

## Why

After BUG-017 (PR #84) closed the EPIPE race for claude-mem hooks, the user immediately hit a SECOND blocker. Error message changed (no more `printf: write error`), proving BUG-017 landed, but `UserPromptSubmit operation blocked by hook ... No stderr output`. Minutes later, after a narrow BUG-018 manual patch (only session-init), the Stop hook also failed 9 consecutive times forcing Claude Code's `CLAUDE_CODE_STOP_HOOK_BLOCK_CAP` to override.

Root cause: claude-mem ships 6 hooks. 5 terminate with `node ... hook claude-code <event>` and lack Claude Code's `{"continue":true,"suppressOutput":true}` directive. bun-runner.js writes diagnostic stdout when stdin is empty (upstream claude-mem#2188); Claude Code reads non-JSON-directive stdout as "do not continue" -> blocks. SessionStart-start is the only hook already emitting the directive.

## What

Extend `heal_hooks_json` / `Repair-HooksJson` with a regex-based substitution that finds any `hook claude-code <event>"` terminator and appends ` 2>/dev/null; echo '{"continue":true,"suppressOutput":true}'` before the closing JSON quote. Same heal pass as BUG-017 (one sed/Replace per pattern). All 5 affected hooks get the directive in one pass.

Idempotent: subsequent runs see no broken terminator left -> silent skip.

## Out of scope

- **Setup hook (`node version-check.js`).** Different terminator pattern; fires only on plugin install/update, not user hot path. Future BUG-018b if it surfaces.
- **AST-level JSON manipulation.** Pure string substitution is sufficient.

## Risks / open questions

- **Risk: regex over-matches in future hooks.json variants.** Mitigation: regex constrained to `[a-z][a-z-]*` for event name -- excludes Setup's `version-check.js` and capitalized identifiers.
- **Risk: `2>/dev/null` silences legitimate stderr.** Mitigation: bun-runner writes to stdout (Claude Code's complaint is "No stderr output"); silencing stderr is harmless.

## Acceptance criteria

- [ ] Both heal scripts contain regex substitution covering all 5 hook terminators.
- [ ] Both heal scripts log `BUG-018` and reference `continue` in the replacement.
- [ ] `tests/setup-linux.bats`: 1 new parity assert.
- [ ] `bash -n` + PowerShell AST + PSScriptAnalyzer clean; ASCII-only.
- [ ] Empirical: user's hooks.json patched on all 5 hooks; prompts complete without loop.

## References

- Vault: `10_projects/dotfiles/11-tasks.md` BUG-018 entry.
- Predecessor: BUG-017 (PR #84) -- EPIPE race fix.
- Upstream: [thedotmack/claude-mem#2607](https://github.com/thedotmack/claude-mem/issues/2607) (cascade race) + claude-mem#2188 (hook protocol mismatch).
24 changes: 24 additions & 0 deletions specs/BUG-018-userpromptsubmit-continue-directive/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
tags: [spec, tasks, claude-mem, heal]
created: "2026-05-21"
---

# Tasks - BUG-018-userpromptsubmit-continue-directive

## Setup
- [x] Branch off main (post BUG-017 merge).
- [x] Spec scaffolded (used -ForceNoVault; vault entry uses BUG-018-claude-mem-userpromptsubmit-continue-directive slug).

## Implementation
- [x] Regex substitution in claude-mem-heal.sh (sed -e).
- [x] Regex replace in claude-mem-heal.ps1 (-replace).
- [x] tests/setup-linux.bats parity assert.

## Lint
- [x] bash -n OK; PowerShell AST clean; PSSA clean; ASCII-only.
- [x] Empirical: 5 hooks patched on user's Windows; loop resolved.

## Closing
- [x] verification.md filled.
- [ ] CI green on PR #85 after spec scaffold.
- [ ] Post-merge: archive, tick vault, append lesson.
46 changes: 46 additions & 0 deletions specs/BUG-018-userpromptsubmit-continue-directive/verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
tags: [spec, verification, claude-mem, heal]
created: "2026-05-21"
---

# Verification - BUG-018-userpromptsubmit-continue-directive

## Evidence

- claude-mem-heal.sh::heal_hooks_json -- sed with regex capture for any `hook claude-code <event>"`.
- claude-mem-heal.ps1::Repair-HooksJson -- PowerShell `-replace` with equivalent regex; reports counts.
- tests/setup-linux.bats BUG-018 parity block.

## Empirical (2026-05-21)

1. BUG-016 merged: .mcp.json fixed.
2. BUG-017 merged: hooks.json cascade race fixed; error changed from `printf: write error` to `No stderr output`.
3. Narrow BUG-018 manual patch (session-init only): UserPromptSubmit works (ping/pong) but Stop fails 9 times.
4. Regex BUG-018 patch (this commit): all 5 hooks patched; loop resolved.

Post-patch grep on user's hooks.json:
- `continue":true` count: 6 (5 patched + 1 pre-existing in SessionStart-start)
- Unpatched `hook claude-code <X>"` terminators: 0

## Lint

- bash -n OK
- PowerShell AST parse clean
- PSScriptAnalyzer clean
- ASCII-only zero non-ASCII

## Decisions

- **Regex capture over per-event substitutions:** maintainable if upstream adds a 6th event.
- **Setup hook left untouched:** different terminator, not on user hot path.
- **`2>/dev/null` to silence stderr:** harmless since bun-runner writes to stdout.

## Lesson candidate

"When a bug class affects N callsites of an upstream system, the heal must patch ALL N in the same PR. Each deferral cost ~30 min of user pain today: BUG-016 (.mcp.json) -> deferred hooks.json (BUG-017) -> deferred continue-directive (BUG-018 narrow) -> all 5 hooks (BUG-018 extended). Be proactive, not reactive."

## Archive checklist
- [ ] status: archived post-merge
- [ ] move to specs/archive/
- [ ] tick vault entry with PR link
- [ ] append lesson to 90-lessons.md
22 changes: 22 additions & 0 deletions tests/setup-linux.bats
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,28 @@ setup() {
grep -qF 'BUG-017' "$DOTFILES_DIR/scripts/claude-mem-heal.ps1"
}

# --- BUG-018: ALL 5 `hook claude-code <X>` hooks missing continue directive ---
# After BUG-017 closed the EPIPE race, every claude-mem hook that terminates
# with `node ... hook claude-code <event>"` blocks Claude Code because the
# stdout output is not a {"continue":true} directive (upstream bun-runner
# stdout vs Claude Code hook protocol mismatch, claude-mem#2188).
# The regex-based substitution catches all 5 in one pass: session-init
# (UserPromptSubmit), context (SessionStart), observation (PostToolUse),
# file-context (PreToolUse), summarize (Stop).

@test "parity: both heal scripts append continue directive to ALL hook claude-code <X> terminators (BUG-018)" {
# The substitution must use a regex capture so all 5 event terminators get
# the same treatment in one pass.
grep -q 'hook claude-code' "$DOTFILES_DIR/scripts/claude-mem-heal.sh"
grep -q 'hook claude-code' "$DOTFILES_DIR/scripts/claude-mem-heal.ps1"
# The replacement must contain the {"continue":true} directive (literal in
# both heal scripts, with JSON-escaped quotes \").
grep -qF 'continue' "$DOTFILES_DIR/scripts/claude-mem-heal.sh"
grep -qF 'continue' "$DOTFILES_DIR/scripts/claude-mem-heal.ps1"
grep -qF 'BUG-018' "$DOTFILES_DIR/scripts/claude-mem-heal.sh"
grep -qF 'BUG-018' "$DOTFILES_DIR/scripts/claude-mem-heal.ps1"
}

# --- BUG-012: legacy marketplace junction/symlink for plugin discovery ---
# Claude Code clones the claude-mem marketplace under the GitHub repo name
# `thedotmack-claude-mem/`, but the plugin's bundled hooks.json hardcodes
Expand Down
Loading