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
6 changes: 5 additions & 1 deletion .claude/hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ documented per event in the official Claude Code docs.

## Hook inventory

All 11 hooks (10 `.py` + `end-of-turn.sh`) are classified against the
All 15 hooks (14 `.py` + `end-of-turn.sh`) are classified against the
`MAP_INVOKED_BY` recursion-guard contract. **REQUIRE_GUARD** hooks early-exit
when MAP spawns a nested subprocess; **FORBID_GUARD** hooks must always fire
and may not carry the guard. Full contract and per-hook rationale:
Expand All @@ -107,6 +107,10 @@ classification is enforced by `scripts/lint-hooks.py` (in `make lint` /
| `pre-compact-save-transcript.py` | `PreCompact` | No | REQUIRE_GUARD | Save full conversation transcript |
| `detect-clarification-triggers.py` | `UserPromptSubmit` | No | REQUIRE_GUARD | Detect "ask if unclear" + async/durability language |
| `end-of-turn.sh` | `Stop` | No | REQUIRE_GUARD | Auto-fix lint/format silently |
| `map-memory-capture.py` | `Stop` | No | REQUIRE_GUARD | Append per-turn scratch WAL record (cross-session memory) |
| `map-memory-endmark.py` | `SessionEnd` | No | REQUIRE_GUARD | Best-effort 'ended' marker for the session WAL |
| `map-memory-finalize.py` | `SessionStart` | No | REQUIRE_GUARD | Finalize prior dirty session scratches into digests (claude -p) |
| `map-memory-recall.py` | `SessionStart` + `UserPromptSubmit` | No | REQUIRE_GUARD | Inject ranked recalled session memory (additionalContext) |

> The Codex twin `.codex/hooks/workflow-gate.py` is FORBID_GUARD like its
> Claude counterpart; this inventory covers `.claude/hooks/` only.
Expand Down
39 changes: 39 additions & 0 deletions .claude/hooks/map-memory-capture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Append per-turn scratch WAL record (cross-session memory). (REQUIRE_GUARD: MAP_INVOKED_BY)."""
import json
import os
import sys
from pathlib import Path

PROJECT_DIR = Path(os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd()))


def _silent() -> None:
sys.stdout.write("{}")
sys.exit(0)


def main() -> None:
if os.environ.get("MAP_INVOKED_BY"): # FIRST statement — recursion guard
sys.exit(0)
try:
input_data = json.load(sys.stdin)
except (json.JSONDecodeError, ValueError):
_silent()
return
# src/ first (dogfood), falls back to installed mapify_cli; no-op if absent.
sys.path.insert(0, str(PROJECT_DIR / "src"))
try:
from mapify_cli.memory.capture import append_turn
except ImportError:
_silent()
return
try:
append_turn(input_data, PROJECT_DIR)
except Exception: # noqa: BLE001 — hooks must never block
pass
_silent()


if __name__ == "__main__":
main()
39 changes: 39 additions & 0 deletions .claude/hooks/map-memory-endmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Best-effort 'ended' marker for the session WAL. (REQUIRE_GUARD: MAP_INVOKED_BY)."""
import json
import os
import sys
from pathlib import Path

PROJECT_DIR = Path(os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd()))


def _silent() -> None:
sys.stdout.write("{}")
sys.exit(0)


def main() -> None:
if os.environ.get("MAP_INVOKED_BY"): # FIRST statement — recursion guard
sys.exit(0)
try:
input_data = json.load(sys.stdin)
except (json.JSONDecodeError, ValueError):
_silent()
return
# src/ first (dogfood), falls back to installed mapify_cli; no-op if absent.
sys.path.insert(0, str(PROJECT_DIR / "src"))
try:
from mapify_cli.memory.capture import on_session_end
except ImportError:
_silent()
return
try:
on_session_end(input_data, PROJECT_DIR)
except Exception: # noqa: BLE001 — hooks must never block
pass
_silent()


if __name__ == "__main__":
main()
49 changes: 49 additions & 0 deletions .claude/hooks/map-memory-finalize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Finalize prior dirty session scratches into digests (claude -p). (REQUIRE_GUARD: MAP_INVOKED_BY)."""
import json
import os
import sys
from pathlib import Path

PROJECT_DIR = Path(os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd()))


def _silent() -> None:
sys.stdout.write("{}")
sys.exit(0)


def main() -> None:
if os.environ.get("MAP_INVOKED_BY"): # FIRST statement — recursion guard
sys.exit(0)
try:
input_data = json.load(sys.stdin)
except (json.JSONDecodeError, ValueError):
_silent()
return
# src/ first (dogfood), falls back to installed mapify_cli; no-op if absent.
sys.path.insert(0, str(PROJECT_DIR / "src"))
try:
from mapify_cli.memory.capture import resolve_session_id
from mapify_cli.memory.finalize import finalize_dirty
except ImportError:
_silent()
return
# claude -p subprocess timeout. MUST stay below the SessionStart hook
# timeout in settings.json (60s) so subprocess.TimeoutExpired fires and runs
# its tmp cleanup before the harness SIGKILLs the whole hook at its own
# deadline (equal timeouts let the harness win the race and orphan the tmp).
try:
timeout = int(os.environ.get("MAP_MEMORY_FINALIZE_TIMEOUT", "50"))
except (ValueError, TypeError):
timeout = 50
try:
incoming = resolve_session_id(input_data, PROJECT_DIR)
finalize_dirty(incoming, PROJECT_DIR, timeout)
except Exception: # noqa: BLE001 — hooks must never block
pass
_silent()


if __name__ == "__main__":
main()
47 changes: 47 additions & 0 deletions .claude/hooks/map-memory-recall.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Inject ranked recalled session memory (additionalContext). (REQUIRE_GUARD: MAP_INVOKED_BY)."""
import json
import os
import sys
from pathlib import Path

PROJECT_DIR = Path(os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd()))


def _silent() -> None:
sys.stdout.write("{}")
sys.exit(0)


def main() -> None:
if os.environ.get("MAP_INVOKED_BY"): # FIRST statement — recursion guard
sys.exit(0)
try:
input_data = json.load(sys.stdin)
except (json.JSONDecodeError, ValueError):
_silent()
return
# src/ first (dogfood), falls back to installed mapify_cli; no-op if absent.
sys.path.insert(0, str(PROJECT_DIR / "src"))
try:
from mapify_cli.memory.recall import build_recall
from mapify_cli.memory.capture import _resolve_branch
except ImportError:
_silent()
return
try:
prompt = str(input_data.get("prompt", ""))
branch = _resolve_branch(PROJECT_DIR)
event = input_data.get("hook_event_name") or "SessionStart"
ctx = build_recall(prompt, branch, PROJECT_DIR)
except Exception: # noqa: BLE001 — hooks must never block
_silent()
return
if ctx:
print(json.dumps({"hookSpecificOutput": {"hookEventName": event, "additionalContext": ctx}}))
else:
_silent()


if __name__ == "__main__":
main()
4 changes: 4 additions & 0 deletions .claude/references/hook-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ the top-level session. They early-exit when the flag is set.
| `ralph-context-pruner.py` | `PreCompact` | No | Restore-point/pruning belongs to the top-level transcript |
| `pre-compact-save-transcript.py` | `PreCompact` | No | Saving the parent transcript; a nested run has its own short-lived transcript |
| `end-of-turn.sh` | `Stop` | No | Auto-format could edit files outside a nested Actor's `affected_files`; lint surfacing is the orchestrator's job |
| `map-memory-capture.py` | `Stop` | No | Memory capture is a top-level-session concern; a nested run (MAP_INVOKED_BY set) must not write to the parent's session WAL |
| `map-memory-endmark.py` | `SessionEnd` | No | End-marker belongs to the top-level session WAL; a nested run must not write an ended marker into the parent's scratch |
| `map-memory-finalize.py` | `SessionStart` | No | Digest finalization is a top-level-session concern; a nested run must not finalize the parent's session scratch |
| `map-memory-recall.py` | `SessionStart` + `UserPromptSubmit` | No | Recall injection targets the top-level session; a nested run must not recall from or inject into the parent's context |

> **Intentional consequence:** suppressing `end-of-turn.sh` and
> `ralph-iteration-logger.py` in nested runs means a nested Actor's lint
Expand Down
57 changes: 57 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,41 @@
"description": "Tells Claude where to find the pre-compaction transcript and workflow state"
}
]
},
{
"description": "MAP Memory Finalize - finalize prior dirty session scratches (runs before recall)",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/map-memory-finalize.py",
"timeout": 60,
"description": "Finalize prior dirty session scratches (runs before recall)"
}
]
},
{
"description": "MAP Memory Recall - inject ranked recalled session memory",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/map-memory-recall.py",
"timeout": 10,
"description": "Inject ranked recalled session memory"
}
]
}
],
"SessionEnd": [
{
"description": "MAP Memory Endmark - best-effort 'ended' marker",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/map-memory-endmark.py",
"timeout": 5,
"description": "Best-effort 'ended' marker"
}
]
}
],
"PreToolUse": [
Expand Down Expand Up @@ -168,6 +203,17 @@
"description": "Records main-session input/output/cache tokens (dedup by msg_id) into the branch token accounting artifacts"
}
]
},
{
"description": "MAP Memory Capture - per-turn scratch WAL",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/map-memory-capture.py",
"timeout": 5,
"description": "Append one LLM-free scratch turn record"
}
]
}
],
"UserPromptSubmit": [
Expand All @@ -192,6 +238,17 @@
"description": "Reads transcript token usage; if compression_policy=auto/aggressive and threshold crossed, injects additionalContext suggesting /compact"
}
]
},
{
"description": "MAP Memory Recall",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/map-memory-recall.py",
"timeout": 10,
"description": "Inject ranked recalled session memory"
}
]
}
]
}
Expand Down
Loading
Loading