mapify-cli 3.3.0__tar.gz → 3.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/PKG-INFO +1 -1
  2. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/pyproject.toml +1 -1
  3. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/__init__.py +1 -1
  4. mapify_cli-3.4.0/src/mapify_cli/templates/hooks/post-compact-context.py +110 -0
  5. mapify_cli-3.4.0/src/mapify_cli/templates/hooks/pre-compact-save-transcript.py +182 -0
  6. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/hooks/ralph-context-pruner.py +4 -7
  7. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/settings.json +31 -5
  8. mapify_cli-3.3.0/src/mapify_cli/templates/hooks/block-dangerous.sh +0 -173
  9. mapify_cli-3.3.0/src/mapify_cli/templates/hooks/block-secrets.py +0 -158
  10. mapify_cli-3.3.0/src/mapify_cli/templates/hooks/improve-prompt.py +0 -74
  11. mapify_cli-3.3.0/src/mapify_cli/templates/hooks/post-edit-reminder.py +0 -87
  12. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/.claude/skills/README.md +0 -0
  13. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/.gitignore +0 -0
  14. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/README.md +0 -0
  15. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/dependency_graph.py +0 -0
  16. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/intent_detector.py +0 -0
  17. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/ralph_state.py +0 -0
  18. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/repo_insight.py +0 -0
  19. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/schemas.py +0 -0
  20. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/CLAUDE.md +0 -0
  21. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/agents/actor.md +0 -0
  22. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/agents/debate-arbiter.md +0 -0
  23. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/agents/documentation-reviewer.md +0 -0
  24. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/agents/evaluator.md +0 -0
  25. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/agents/final-verifier.md +0 -0
  26. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/agents/monitor.md +0 -0
  27. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/agents/predictor.md +0 -0
  28. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/agents/reflector.md +0 -0
  29. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/agents/research-agent.md +0 -0
  30. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/agents/synthesizer.md +0 -0
  31. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/agents/task-decomposer.md +0 -0
  32. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/commands/map-check.md +0 -0
  33. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/commands/map-debate.md +0 -0
  34. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/commands/map-debug.md +0 -0
  35. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/commands/map-efficient.md +0 -0
  36. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/commands/map-fast.md +0 -0
  37. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/commands/map-learn.md +0 -0
  38. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/commands/map-plan.md +0 -0
  39. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/commands/map-release.md +0 -0
  40. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/commands/map-resume.md +0 -0
  41. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/commands/map-review.md +0 -0
  42. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/hooks/end-of-turn.sh +0 -0
  43. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/hooks/ralph-iteration-logger.py +0 -0
  44. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/hooks/safety-guardrails.py +0 -0
  45. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/hooks/workflow-context-injector.py +0 -0
  46. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/hooks/workflow-gate.py +0 -0
  47. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/map/scripts/diagnostics.py +0 -0
  48. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/map/scripts/map_orchestrator.py +0 -0
  49. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/map/scripts/map_step_runner.py +0 -0
  50. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/map/static-analysis/analyze.sh +0 -0
  51. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/map/static-analysis/handlers/common.sh +0 -0
  52. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/map/static-analysis/handlers/go.sh +0 -0
  53. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/map/static-analysis/handlers/python.sh +0 -0
  54. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/map/static-analysis/handlers/rust.sh +0 -0
  55. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/map/static-analysis/handlers/typescript.sh +0 -0
  56. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/ralph-loop-config.json +0 -0
  57. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/references/bash-guidelines.md +0 -0
  58. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/references/decomposition-examples.md +0 -0
  59. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/references/escalation-matrix.md +0 -0
  60. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/references/mcp-usage-examples.md +0 -0
  61. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/references/step-state-schema.md +0 -0
  62. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/references/workflow-state-schema.md +0 -0
  63. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/README.md +0 -0
  64. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-cli-reference/SKILL.md +0 -0
  65. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-cli-reference/scripts/check-command.sh +0 -0
  66. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-planning/SKILL.md +0 -0
  67. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-planning/scripts/check-complete.sh +0 -0
  68. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-planning/scripts/get-plan-path.sh +0 -0
  69. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-planning/scripts/init-session.sh +0 -0
  70. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-planning/scripts/show-focus.sh +0 -0
  71. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-planning/templates/findings.md +0 -0
  72. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-planning/templates/iteration_history.md +0 -0
  73. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-planning/templates/progress.md +0 -0
  74. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-planning/templates/task_plan.md +0 -0
  75. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-workflows-guide/SKILL.md +0 -0
  76. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-workflows-guide/resources/agent-architecture.md +0 -0
  77. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-workflows-guide/resources/map-debug-deep-dive.md +0 -0
  78. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-workflows-guide/resources/map-efficient-deep-dive.md +0 -0
  79. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-workflows-guide/resources/map-fast-deep-dive.md +0 -0
  80. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-workflows-guide/resources/map-feature-deep-dive.md +0 -0
  81. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-workflows-guide/resources/map-refactor-deep-dive.md +0 -0
  82. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/map-workflows-guide/scripts/validate-workflow-choice.py +0 -0
  83. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/skills/skill-rules.json +0 -0
  84. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/templates/workflow-rules.json +0 -0
  85. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/tools/__init__.py +0 -0
  86. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/tools/validate_dependencies.py +0 -0
  87. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/verification_recorder.py +0 -0
  88. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/workflow_finalizer.py +0 -0
  89. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/workflow_logger.py +0 -0
  90. {mapify_cli-3.3.0 → mapify_cli-3.4.0}/src/mapify_cli/workflow_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mapify-cli
3
- Version: 3.3.0
3
+ Version: 3.4.0
4
4
  Summary: MAP Framework installer - Modular Agentic Planner for Claude Code
5
5
  Project-URL: Homepage, https://github.com/azalio/map-framework
6
6
  Project-URL: Repository, https://github.com/azalio/map-framework.git
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mapify-cli"
3
- version = "3.3.0"
3
+ version = "3.4.0"
4
4
  description = "MAP Framework installer - Modular Agentic Planner for Claude Code"
5
5
  authors = [{ name = "MAP Framework Contributors" }]
6
6
  readme = "README.md"
@@ -23,7 +23,7 @@ Or install globally:
23
23
  mapify check
24
24
  """
25
25
 
26
- __version__ = "3.3.0"
26
+ __version__ = "3.4.0"
27
27
 
28
28
  import copy
29
29
  import os
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Post-Compact Context Injector - SessionStart Hook (matcher: compact).
4
+
5
+ After context compaction, injects a pointer to the saved transcript
6
+ so Claude knows where to find the full pre-compaction conversation.
7
+
8
+ Also reads restore_point.json if available (from ralph-context-pruner).
9
+
10
+ Exit codes:
11
+ 0 - Always
12
+ """
13
+ import json
14
+ import os
15
+ import re
16
+ import subprocess
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ PROJECT_DIR = Path(os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd()))
21
+ MAP_DIR = PROJECT_DIR / ".map"
22
+
23
+
24
+ def sanitize_branch_name(branch: str) -> str:
25
+ """Sanitize branch name for safe filesystem paths."""
26
+ sanitized = branch.replace("/", "-")
27
+ sanitized = re.sub(r"[^a-zA-Z0-9_.-]", "-", sanitized)
28
+ sanitized = re.sub(r"-+", "-", sanitized).strip("-")
29
+ if ".." in sanitized or sanitized.startswith("."):
30
+ return "default"
31
+ return sanitized or "default"
32
+
33
+
34
+ def get_branch_name() -> str:
35
+ """Get current git branch name."""
36
+ try:
37
+ result = subprocess.run(
38
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
39
+ capture_output=True,
40
+ text=True,
41
+ cwd=PROJECT_DIR,
42
+ timeout=2,
43
+ )
44
+ if result.returncode == 0:
45
+ return sanitize_branch_name(result.stdout.strip())
46
+ except Exception:
47
+ pass
48
+ return "default"
49
+
50
+
51
+ def main() -> None:
52
+ try:
53
+ json.load(sys.stdin)
54
+ except json.JSONDecodeError:
55
+ pass
56
+
57
+ branch = get_branch_name()
58
+ branch_dir = MAP_DIR / branch
59
+
60
+ parts = []
61
+
62
+ # Check for saved transcript pointer
63
+ pointer = branch_dir / "last-transcript.txt"
64
+ if pointer.exists():
65
+ try:
66
+ transcript_path = pointer.read_text(encoding="utf-8").strip()
67
+ if transcript_path:
68
+ parts.append(
69
+ f"The full transcript of the previous conversation "
70
+ f"(before compaction) was saved to {transcript_path}. "
71
+ f"Read that file if you need details from before compaction."
72
+ )
73
+ except (IOError, OSError):
74
+ pass
75
+
76
+ # Check for workflow restore point
77
+ restore = branch_dir / "restore_point.json"
78
+ if restore.exists():
79
+ try:
80
+ data = json.loads(restore.read_text(encoding="utf-8"))
81
+ state = data.get("workflow_state", {})
82
+ workflow = state.get("workflow", "")
83
+ phase = state.get("current_step", {}).get("phase", "") or state.get(
84
+ "current_state", ""
85
+ )
86
+ if workflow or phase:
87
+ parts.append(
88
+ f"MAP workflow state before compaction: "
89
+ f"workflow={workflow}, phase={phase}. "
90
+ f"Full state: .map/{branch}/workflow_state.json"
91
+ )
92
+ except (json.JSONDecodeError, IOError, OSError):
93
+ pass
94
+
95
+ if not parts:
96
+ print("{}")
97
+ sys.exit(0)
98
+
99
+ result = {
100
+ "hookSpecificOutput": {
101
+ "hookEventName": "SessionStart",
102
+ "additionalContext": "\n".join(parts),
103
+ }
104
+ }
105
+ print(json.dumps(result))
106
+ sys.exit(0)
107
+
108
+
109
+ if __name__ == "__main__":
110
+ main()
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Pre-Compact Transcript Saver - PreCompact Hook.
4
+
5
+ Before context compaction, saves the full conversation transcript
6
+ to .map/<branch>/transcript-YYYY-MM-DD-HH-MM-SS.md as readable markdown.
7
+
8
+ This preserves the full context for later review.
9
+
10
+ Exit codes:
11
+ 0 - Always (PreCompact hooks don't block)
12
+ """
13
+ import json
14
+ import os
15
+ import re
16
+ import subprocess
17
+ import sys
18
+ from datetime import datetime
19
+ from pathlib import Path
20
+
21
+
22
+ PROJECT_DIR = Path(os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd()))
23
+ MAP_DIR = PROJECT_DIR / ".map"
24
+
25
+
26
+ def sanitize_branch_name(branch: str) -> str:
27
+ """Sanitize branch name for safe filesystem paths."""
28
+ sanitized = branch.replace("/", "-")
29
+ sanitized = re.sub(r"[^a-zA-Z0-9_.-]", "-", sanitized)
30
+ sanitized = re.sub(r"-+", "-", sanitized).strip("-")
31
+ if ".." in sanitized or sanitized.startswith("."):
32
+ return "default"
33
+ return sanitized or "default"
34
+
35
+
36
+ def get_branch_name() -> str:
37
+ """Get current git branch name."""
38
+ try:
39
+ result = subprocess.run(
40
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
41
+ capture_output=True,
42
+ text=True,
43
+ cwd=PROJECT_DIR,
44
+ timeout=2,
45
+ )
46
+ if result.returncode == 0:
47
+ return sanitize_branch_name(result.stdout.strip())
48
+ except Exception:
49
+ pass
50
+ return "default"
51
+
52
+
53
+ def extract_text_from_content(content):
54
+ """Extract readable text from message content (string or list)."""
55
+ if isinstance(content, str):
56
+ return content
57
+ if not isinstance(content, list):
58
+ return ""
59
+
60
+ parts = []
61
+ for item in content:
62
+ if isinstance(item, str):
63
+ parts.append(item)
64
+ elif isinstance(item, dict):
65
+ item_type = item.get("type", "")
66
+ if item_type == "text":
67
+ parts.append(item.get("text", ""))
68
+ elif item_type == "tool_use":
69
+ name = item.get("name", "unknown")
70
+ tool_input = item.get("input", {})
71
+ input_str = json.dumps(tool_input, ensure_ascii=False)
72
+ # Truncate long tool inputs
73
+ if len(input_str) > 500:
74
+ input_str = input_str[:500] + "..."
75
+ parts.append(f"**Tool:** `{name}`\n```json\n{input_str}\n```")
76
+ elif item_type == "tool_result":
77
+ result_content = item.get("content", "")
78
+ if isinstance(result_content, list):
79
+ for rc in result_content:
80
+ if isinstance(rc, dict) and rc.get("type") == "text":
81
+ text = rc.get("text", "")
82
+ if len(text) > 1000:
83
+ text = text[:1000] + "...[truncated]"
84
+ parts.append(text)
85
+ elif isinstance(result_content, str):
86
+ if len(result_content) > 1000:
87
+ result_content = result_content[:1000] + "...[truncated]"
88
+ parts.append(result_content)
89
+ return "\n".join(parts)
90
+
91
+
92
+ def parse_transcript(transcript_path: Path) -> str:
93
+ """Parse JSONL transcript into readable markdown."""
94
+ lines = []
95
+ try:
96
+ with open(transcript_path, encoding="utf-8") as f:
97
+ for raw_line in f:
98
+ raw_line = raw_line.strip()
99
+ if not raw_line:
100
+ continue
101
+ try:
102
+ entry = json.loads(raw_line)
103
+ except json.JSONDecodeError:
104
+ continue
105
+
106
+ entry_type = entry.get("type", "")
107
+ message = entry.get("message", {})
108
+ role = message.get("role", "")
109
+ content = message.get("content", "")
110
+
111
+ if entry_type == "human" or role == "user":
112
+ text = extract_text_from_content(content)
113
+ if text.strip():
114
+ lines.append(f"## User\n\n{text}\n")
115
+ elif entry_type == "assistant" or role == "assistant":
116
+ text = extract_text_from_content(content)
117
+ if text.strip():
118
+ lines.append(f"## Assistant\n\n{text}\n")
119
+ elif entry_type == "tool_result":
120
+ text = extract_text_from_content(content)
121
+ if text.strip():
122
+ lines.append(
123
+ f"<details><summary>Tool result</summary>\n\n"
124
+ f"```\n{text}\n```\n</details>\n"
125
+ )
126
+ except (IOError, OSError) as e:
127
+ lines.append(f"Error reading transcript: {e}\n")
128
+
129
+ return "\n".join(lines)
130
+
131
+
132
+ def main() -> None:
133
+ try:
134
+ input_data = json.load(sys.stdin)
135
+ except json.JSONDecodeError:
136
+ input_data = {}
137
+
138
+ transcript_path = input_data.get("transcript_path", "")
139
+ session_id = input_data.get("session_id", "unknown")
140
+
141
+ if not transcript_path or not Path(transcript_path).is_file():
142
+ print("{}")
143
+ sys.exit(0)
144
+
145
+ branch = get_branch_name()
146
+ timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
147
+
148
+ branch_dir = MAP_DIR / branch
149
+ branch_dir.mkdir(parents=True, exist_ok=True)
150
+ outfile = branch_dir / f"transcript-{timestamp}.md"
151
+
152
+ header = (
153
+ f"# Conversation snapshot before compact\n\n"
154
+ f"- **Branch:** {branch}\n"
155
+ f"- **Date:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
156
+ f"- **Session:** {session_id}\n\n"
157
+ f"---\n\n"
158
+ )
159
+
160
+ body = parse_transcript(Path(transcript_path))
161
+
162
+ try:
163
+ outfile.write_text(header + body, encoding="utf-8")
164
+ print(f"[pre-compact-save] Saved transcript to {outfile}", file=sys.stderr)
165
+ except (IOError, OSError) as e:
166
+ print(f"[pre-compact-save] Failed to save: {e}", file=sys.stderr)
167
+ print("{}")
168
+ sys.exit(0)
169
+
170
+ # Write a pointer file so the context-pruner (or compact summary) can reference it
171
+ pointer = branch_dir / "last-transcript.txt"
172
+ try:
173
+ pointer.write_text(str(outfile.relative_to(PROJECT_DIR)), encoding="utf-8")
174
+ except (IOError, OSError):
175
+ pass
176
+
177
+ print("{}")
178
+ sys.exit(0)
179
+
180
+
181
+ if __name__ == "__main__":
182
+ main()
@@ -13,7 +13,7 @@ Exit codes:
13
13
  0 - Always (PreCompact hooks don't block)
14
14
 
15
15
  Output:
16
- hookSpecificOutput.additionalContext - Recovery message injected into context
16
+ Side effects only (PreCompact has no decision control per docs)
17
17
  """
18
18
  import json
19
19
  import os
@@ -239,12 +239,9 @@ def main() -> None:
239
239
  file=sys.stderr,
240
240
  )
241
241
 
242
- # Inject recovery message into context
243
- recovery_msg = format_recovery_message(state, branch)
244
- output["hookSpecificOutput"] = {
245
- "hookEventName": "PreCompact",
246
- "additionalContext": recovery_msg,
247
- }
242
+ # Note: PreCompact has no decision control per docs — additionalContext
243
+ # is not supported. Recovery context is injected via SessionStart(compact)
244
+ # hook (post-compact-context.py) which reads restore_point.json.
248
245
 
249
246
  # Prune log files in ALL branch directories
250
247
  actions = []
@@ -35,6 +35,20 @@
35
35
  ]
36
36
  },
37
37
  "hooks": {
38
+ "SessionStart": [
39
+ {
40
+ "matcher": "compact",
41
+ "description": "Post-Compact Context - inject transcript path and workflow state after compaction",
42
+ "hooks": [
43
+ {
44
+ "type": "command",
45
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-compact-context.py",
46
+ "timeout": 5,
47
+ "description": "Tells Claude where to find the pre-compaction transcript and workflow state"
48
+ }
49
+ ]
50
+ }
51
+ ],
38
52
  "PreToolUse": [
39
53
  {
40
54
  "matcher": "Edit|Write|Read|MultiEdit|Bash",
@@ -75,14 +89,14 @@
75
89
  ],
76
90
  "PostToolUse": [
77
91
  {
78
- "matcher": "Edit|Write|MultiEdit",
79
- "description": "Post-Edit Reminder - lightweight test reminder after code changes",
92
+ "matcher": "Edit|Write|MultiEdit|Bash",
93
+ "description": "Iteration Logger - logs tool calls, detects thrashing patterns",
80
94
  "hooks": [
81
95
  {
82
96
  "type": "command",
83
- "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-edit-reminder.py",
84
- "timeout": 3,
85
- "description": "Reminds to run tests after Edit/Write (only when MAP workflow active)"
97
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/ralph-iteration-logger.py",
98
+ "timeout": 5,
99
+ "description": "Logs iterations to .map/<branch>/iteration_log.jsonl, alerts on file thrashing"
86
100
  }
87
101
  ]
88
102
  }
@@ -99,6 +113,18 @@
99
113
  "description": "Saves workflow restore point and prunes old logs"
100
114
  }
101
115
  ]
116
+ },
117
+ {
118
+ "matcher": "*",
119
+ "description": "Transcript Saver - save full conversation to .map/ before compaction",
120
+ "hooks": [
121
+ {
122
+ "type": "command",
123
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-compact-save-transcript.py",
124
+ "timeout": 30,
125
+ "description": "Saves full transcript as readable markdown to .map/<branch>/transcript-YYYY-MM-DD-HH-MM.md"
126
+ }
127
+ ]
102
128
  }
103
129
  ],
104
130
  "Stop": [
@@ -1,173 +0,0 @@
1
- #!/usr/bin/env bash
2
- # =============================================================================
3
- # Claude Code PreToolUse Hook: Block Dangerous Commands
4
- # =============================================================================
5
- #
6
- # Intercepts Bash tool calls and blocks destructive commands like:
7
- # - rm -rf (recursive force delete)
8
- # - git push --force to main/master branches
9
- # - git reset --hard
10
- #
11
- # USAGE:
12
- # This hook runs automatically before Bash tool calls.
13
- # Claude Code passes JSON via stdin with tool_name and tool_input.
14
- #
15
- # EXIT CODES:
16
- # 0 - Allow command execution
17
- # 0 + permissionDecision=deny - Block command execution (preferred)
18
- #
19
- # TESTING:
20
- # echo '{"tool_name": "Bash", "tool_input": {"command": "rm -rf /"}}' | bash block-dangerous.sh
21
- # # Expected: Exit code 0 with permissionDecision=deny in JSON output
22
- #
23
- # =============================================================================
24
-
25
- set -euo pipefail
26
-
27
- # Read JSON from stdin
28
- INPUT=$(cat)
29
-
30
- deny() {
31
- local reason="$1"
32
-
33
- if command -v jq >/dev/null 2>&1; then
34
- jq -n --arg reason "$reason" '{
35
- hookSpecificOutput: {
36
- hookEventName: "PreToolUse",
37
- permissionDecision: "deny",
38
- permissionDecisionReason: $reason
39
- }
40
- }'
41
- return 0
42
- fi
43
-
44
- if command -v python3 >/dev/null 2>&1; then
45
- python3 - "$reason" <<'PY'
46
- import json
47
- import sys
48
-
49
- reason = sys.argv[1]
50
- print(
51
- json.dumps(
52
- {
53
- "hookSpecificOutput": {
54
- "hookEventName": "PreToolUse",
55
- "permissionDecision": "deny",
56
- "permissionDecisionReason": reason,
57
- }
58
- }
59
- )
60
- )
61
- PY
62
- return 0
63
- fi
64
-
65
- local escaped=${reason//\\/\\\\}
66
- escaped=${escaped//\"/\\\"}
67
- printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$escaped"
68
- }
69
-
70
- if command -v jq >/dev/null 2>&1; then
71
- TOOL_NAME=$(jq -r '.tool_name // empty' <<<"$INPUT" 2>/dev/null || true)
72
- COMMAND=$(jq -r '.tool_input.command // empty' <<<"$INPUT" 2>/dev/null || true)
73
- elif command -v python3 >/dev/null 2>&1; then
74
- TOOL_NAME=$(
75
- python3 -c 'import json,sys; d=json.loads(sys.stdin.read() or "{}"); print(d.get("tool_name",""))' <<<"$INPUT" 2>/dev/null || true
76
- )
77
- COMMAND=$(
78
- python3 -c 'import json,sys; d=json.loads(sys.stdin.read() or "{}"); ti=d.get("tool_input") or {}; print(ti.get("command",""))' <<<"$INPUT" 2>/dev/null || true
79
- )
80
- else
81
- TOOL_NAME=""
82
- COMMAND=""
83
- fi
84
-
85
- # Only intercept Bash tool
86
- if [[ "$TOOL_NAME" != "Bash" ]]; then
87
- echo '{}'
88
- exit 0
89
- fi
90
-
91
- # If no command, allow
92
- if [[ -z "$COMMAND" ]]; then
93
- echo '{}'
94
- exit 0
95
- fi
96
-
97
- # Normalize command for pattern matching (lowercase for case-insensitive)
98
- COMMAND_LOWER=$(echo "$COMMAND" | tr '[:upper:]' '[:lower:]')
99
-
100
- # =============================================================================
101
- # Dangerous Pattern Checks
102
- # =============================================================================
103
-
104
- # Check for rm -rf (recursive force delete)
105
- # Matches: rm -rf, rm -fr, rm -r -f, rm -f -r, rm --recursive --force, and edge
106
- # cases where flags touch the path (e.g., rm -rf0dir).
107
- if echo "$COMMAND_LOWER" | grep -qE '(^|[[:space:];|&()])rm[[:space:]]'; then
108
- # Long flags: --recursive and --force
109
- if echo "$COMMAND_LOWER" | grep -qE -- '--recursive' && echo "$COMMAND_LOWER" | grep -qE -- '--force'; then
110
- deny "Blocked: rm -rf is prohibited (recursive force delete can cause irreversible data loss)"
111
- exit 0
112
- fi
113
-
114
- # Combined short flags: -rf / -fr (including edge cases where the path touches flags)
115
- if echo "$COMMAND_LOWER" | grep -qE '(^|[[:space:];|&()])rm[[:space:]].*-[^[:space:]]*r[^[:space:]]*f'; then
116
- deny "Blocked: rm -rf is prohibited (recursive force delete can cause irreversible data loss)"
117
- exit 0
118
- fi
119
- if echo "$COMMAND_LOWER" | grep -qE '(^|[[:space:];|&()])rm[[:space:]].*-[^[:space:]]*f[^[:space:]]*r'; then
120
- deny "Blocked: rm -rf is prohibited (recursive force delete can cause irreversible data loss)"
121
- exit 0
122
- fi
123
-
124
- # Separate short flags: -r -f / -f -r
125
- if echo "$COMMAND_LOWER" | grep -qE '(^|[[:space:]])-r([[:space:]]|$)' && echo "$COMMAND_LOWER" | grep -qE '(^|[[:space:]])-f([[:space:]]|$)'; then
126
- deny "Blocked: rm -rf is prohibited (recursive force delete can cause irreversible data loss)"
127
- exit 0
128
- fi
129
- fi
130
-
131
- # Check for git push --force to main/master
132
- # Matches: git push --force origin main, git push -f origin master
133
- if echo "$COMMAND" | grep -qE 'git\s+push\s+.*(-f|--force).*\s+(origin|upstream)\s+(main|master)(\s|$)'; then
134
- deny "Blocked: Force push to main/master is prohibited (can overwrite team work)"
135
- exit 0
136
- fi
137
-
138
- # Also check reverse order: git push origin main --force
139
- if echo "$COMMAND" | grep -qE 'git\s+push\s+(origin|upstream)\s+(main|master)\s+(-f|--force)'; then
140
- deny "Blocked: Force push to main/master is prohibited (can overwrite team work)"
141
- exit 0
142
- fi
143
-
144
- # Check for git reset --hard (without specific commit, or dangerous patterns)
145
- # Block: git reset --hard, git reset --hard HEAD~, git reset --hard origin/
146
- if echo "$COMMAND" | grep -qE 'git\s+reset\s+--hard(\s|$)'; then
147
- deny "Blocked: git reset --hard is prohibited (can cause irreversible loss of uncommitted changes)"
148
- exit 0
149
- fi
150
-
151
- # Check for dangerous chmod/chown on system directories
152
- if echo "$COMMAND" | grep -qE '(chmod|chown)\s+(-R|--recursive)\s+.*\s+/($|\s)'; then
153
- deny "Blocked: Recursive chmod/chown on / is prohibited (can break system permissions)"
154
- exit 0
155
- fi
156
-
157
- # Check for dd with of=/dev/
158
- if echo "$COMMAND" | grep -qE 'dd\s+.*of=/dev/'; then
159
- deny "Blocked: dd with of=/dev/* is prohibited (writing to raw devices can destroy data)"
160
- exit 0
161
- fi
162
-
163
- # Check for mkfs (format filesystem)
164
- if echo "$COMMAND" | grep -qE 'mkfs'; then
165
- deny "Blocked: mkfs is prohibited (formatting filesystems can destroy data)"
166
- exit 0
167
- fi
168
-
169
- # =============================================================================
170
- # All checks passed - allow command
171
- # =============================================================================
172
- echo '{}'
173
- exit 0
@@ -1,158 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Claude Code PreToolUse Hook: Block Sensitive File Access
4
- Intercepts Read/Edit/Write tool calls and prevents access to sensitive files
5
- like .env files, credentials, private keys, and secrets.
6
-
7
- USAGE:
8
- This hook runs automatically before Read/Edit/Write tool calls.
9
- No manual invocation needed - Claude Code handles hook execution.
10
-
11
- BLOCKED FILE PATTERNS:
12
- - .env, .env.local, .env.production (environment variables)
13
- - *credentials*, *secret* (credential/secret files)
14
- - *.pem, *_(private|secret|rsa|dsa|ecdsa).key (private keys)
15
- - *_rsa, *_dsa, *_ecdsa, *_ed25519 (SSH keys without extension)
16
- - *.p12, *.pfx, *.keystore, *.jks (certificates/keystores)
17
- - *.ppk (PuTTY keys)
18
-
19
- ALLOWED FILE PATTERNS:
20
- - *.pub (public keys - safe to read)
21
- - license.key, api.key (generic .key files without private key indicators)
22
-
23
- HOOK BEHAVIOR:
24
- - Exit code 0: Allow tool execution (non-sensitive file)
25
- - Exit code 0 + permissionDecision=deny: Block tool execution (sensitive file detected)
26
-
27
- TESTING:
28
- echo '{"tool_name": "Read", "tool_input": {"file_path": ".env"}}' | python3 block-secrets.py
29
- # Expected: Exit code 0, JSON on stdout with permissionDecision=deny
30
-
31
- PERFORMANCE:
32
- Target: <100ms per invocation
33
- Actual: ~27ms average (measured with 100 iterations)
34
- """
35
- import json
36
- import sys
37
- import re
38
- from pathlib import Path
39
-
40
- # Sensitive file patterns (PRE-COMPILED for performance)
41
- SENSITIVE_PATTERNS = [
42
- re.compile(r"\.env.*", re.IGNORECASE), # .env, .env.local, .env.production, etc.
43
- re.compile(
44
- r".*credentials.*", re.IGNORECASE
45
- ), # credentials.json, aws-credentials, etc.
46
- re.compile(r".*secret.*", re.IGNORECASE), # secrets.yaml, secret-key.txt, etc.
47
- re.compile(r".*\.pem$", re.IGNORECASE), # Private key files
48
- re.compile(
49
- r".*_(private|secret|rsa|dsa|ecdsa)\.key$", re.IGNORECASE
50
- ), # Specific private key files only
51
- re.compile(r".*_rsa$", re.IGNORECASE), # SSH keys without extension
52
- re.compile(r".*_dsa$", re.IGNORECASE), # DSA keys
53
- re.compile(r".*_ecdsa$", re.IGNORECASE), # ECDSA keys
54
- re.compile(r".*_ed25519$", re.IGNORECASE), # Ed25519 keys
55
- re.compile(r".*\.p12$", re.IGNORECASE), # PKCS#12 certificate files
56
- re.compile(r".*\.pfx$", re.IGNORECASE), # PKCS#12 certificate files (Windows)
57
- re.compile(r".*\.keystore$", re.IGNORECASE), # Java keystores
58
- re.compile(r".*\.jks$", re.IGNORECASE), # Java keystores
59
- re.compile(r".*\.ppk$", re.IGNORECASE), # PuTTY private keys
60
- ]
61
-
62
-
63
- SAFE_PATH_PREFIXES = [
64
- ".claude/hooks/",
65
- ".claude/agents/",
66
- ".claude/commands/",
67
- ".claude/references/",
68
- ".claude/skills/",
69
- "src/",
70
- "tests/",
71
- "docs/",
72
- "scripts/",
73
- ]
74
-
75
-
76
- def is_sensitive_file(file_path: str) -> bool:
77
- """Check if file path matches any sensitive file pattern.
78
-
79
- Checks ALL path components (not just filename) to catch patterns
80
- in directory names or parent paths. Skips files in known safe
81
- directories (hooks, agents, source code, tests, etc.)
82
- """
83
- path_obj = Path(file_path)
84
-
85
- # Normalize to relative path for prefix matching
86
- try:
87
- rel = str(path_obj.relative_to(Path.cwd()))
88
- except ValueError:
89
- rel = str(path_obj)
90
-
91
- # Allow known safe directories
92
- for prefix in SAFE_PATH_PREFIXES:
93
- if rel.startswith(prefix):
94
- return False
95
-
96
- # Check each path component against all patterns
97
- for part in path_obj.parts:
98
- for pattern in SENSITIVE_PATTERNS:
99
- if pattern.match(part):
100
- return True
101
- return False
102
-
103
-
104
- def block_access(file_path: str, tool_name: str):
105
- """Block tool execution and output error message."""
106
- print(
107
- json.dumps(
108
- {
109
- "hookSpecificOutput": {
110
- "hookEventName": "PreToolUse",
111
- "permissionDecision": "deny",
112
- "permissionDecisionReason": (
113
- f"Blocked: Access to sensitive file '{file_path}' is prohibited "
114
- f"(tool={tool_name})."
115
- ),
116
- }
117
- }
118
- )
119
- )
120
- sys.exit(0)
121
-
122
-
123
- def main():
124
- """Main hook execution logic."""
125
- # Load input from stdin
126
- try:
127
- input_data = json.load(sys.stdin)
128
- except json.JSONDecodeError as e:
129
- print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
130
- sys.exit(1)
131
-
132
- tool_name = input_data.get("tool_name", "")
133
- tool_input = input_data.get("tool_input", {})
134
-
135
- # Only intercept Read, Edit, Write tools
136
- if tool_name not in ["Read", "Edit", "Write"]:
137
- print("{}")
138
- sys.exit(0) # Allow other tools
139
-
140
- # Extract file_path from tool_input
141
- file_path = tool_input.get("file_path", "")
142
-
143
- # If no file_path, allow (shouldn't happen for Read/Edit/Write, but be safe)
144
- if not file_path:
145
- print("{}")
146
- sys.exit(0)
147
-
148
- # Check if file is sensitive
149
- if is_sensitive_file(file_path):
150
- block_access(file_path, tool_name)
151
-
152
- # Allow non-sensitive files
153
- print("{}")
154
- sys.exit(0)
155
-
156
-
157
- if __name__ == "__main__":
158
- main()
@@ -1,74 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Claude Code Prompt Improver Hook
4
- Intercepts user prompts and evaluates if they need enrichment before execution.
5
- Uses main session context for intelligent, non-pedantic evaluation.
6
- """
7
- import json
8
- import sys
9
-
10
- # Load input from stdin
11
- try:
12
- input_data = json.load(sys.stdin)
13
- except json.JSONDecodeError as e:
14
- print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
15
- sys.exit(1)
16
-
17
- prompt = input_data.get("prompt", "")
18
-
19
- # Escape quotes in prompt for safe embedding in wrapped instructions
20
- escaped_prompt = prompt.replace("\\", "\\\\").replace('"', '\\"')
21
-
22
-
23
- def output_json(text):
24
- """Output text in UserPromptSubmit JSON format"""
25
- output = {
26
- "hookSpecificOutput": {
27
- "hookEventName": "UserPromptSubmit",
28
- "additionalContext": text,
29
- }
30
- }
31
- print(json.dumps(output))
32
-
33
-
34
- # Check for bypass conditions
35
- # 1. Explicit bypass with * prefix
36
- # 2. Slash commands (built-in or custom)
37
- # 3. Memorize feature (# prefix)
38
- if prompt.startswith("*") or prompt.startswith("/") or prompt.startswith("#"):
39
- # User bypassed improvement - don't add evaluation wrapper
40
- # Output empty JSON to signal success without modification
41
- print("{}")
42
- sys.exit(0)
43
-
44
- # Build the improvement wrapper
45
- wrapped_prompt = f"""PROMPT EVALUATION
46
-
47
- Original user request: "{escaped_prompt}"
48
-
49
- EVALUATE: Is this prompt clear enough to execute, or does it need enrichment?
50
-
51
- PROCEED IMMEDIATELY if:
52
- - Detailed/specific OR you have sufficient context OR can infer intent
53
-
54
- ONLY ASK if genuinely vague (e.g., "fix the bug" with no context):
55
- - CRITICAL (NON-NEGOTIABLE) RULES:
56
- - Trust user intent by default. Check conversation history before doing research.
57
- - Do not rely on base knowledge.
58
- - Never skip Phase 1. Research before asking.
59
- - Don't announce evaluation - just proceed or ask.
60
-
61
- - PHASE 1 - RESEARCH (DO NOT SKIP):
62
- 1. Preface with brief note: "Prompt Improver Hook is seeking clarification because [specific reason: ambiguous scope/missing context/unclear requirements/etc]"
63
- 2. Create research plan with TodoWrite: Ask yourself "What do I need to research to clarify this vague request?" Research WHAT NEEDS CLARIFICATION, not just the project. Use available tools: Task/Explore for codebase, WebSearch for online research (current info, common approaches, best practices, typical architectures), Read/Grep as needed
64
- 3. Execute research
65
- 4. Use research findings (not your training) to formulate grounded questions with specific options
66
- 5. Mark completed
67
-
68
- - PHASE 2 - ASK (ONLY AFTER PHASE 1):
69
- 1. Use AskUserQuestion tool with max 1-6 questions offering specific options from your research
70
- 2. Use the answers to execute the original user request
71
- """
72
-
73
- output_json(wrapped_prompt)
74
- sys.exit(0)
@@ -1,87 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Post-Edit Reminder - PostToolUse Hook
4
-
5
- Lightweight reminder after Edit/Write operations to run tests.
6
- Only triggers when there's an active MAP workflow.
7
-
8
- Trigger: Edit|Write
9
- Exit codes: Always 0 (non-blocking)
10
- Output: ~80 char reminder via hookSpecificOutput.additionalContext
11
- """
12
-
13
- import json
14
- import os
15
- import re
16
- import subprocess
17
- import sys
18
- from pathlib import Path
19
-
20
-
21
- def sanitize_branch_name(branch: str) -> str:
22
- """Sanitize branch name for safe filesystem paths."""
23
- sanitized = branch.replace("/", "-")
24
- sanitized = re.sub(r"[^a-zA-Z0-9_.-]", "-", sanitized)
25
- sanitized = re.sub(r"-+", "-", sanitized).strip("-")
26
- if ".." in sanitized or sanitized.startswith("."):
27
- return "default"
28
- return sanitized or "default"
29
-
30
-
31
- def get_branch_name() -> str:
32
- """Get current git branch name."""
33
- try:
34
- result = subprocess.run(
35
- ["git", "rev-parse", "--abbrev-ref", "HEAD"],
36
- capture_output=True,
37
- text=True,
38
- timeout=1,
39
- )
40
- if result.returncode == 0:
41
- return sanitize_branch_name(result.stdout.strip())
42
- except Exception:
43
- pass
44
- return "default"
45
-
46
-
47
- def has_active_workflow(branch: str) -> bool:
48
- """Check if there's an active MAP workflow."""
49
- project_dir = Path(os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd()))
50
- state_file = project_dir / ".map" / branch / "workflow_state.json"
51
- return state_file.exists()
52
-
53
-
54
- def main() -> None:
55
- try:
56
- input_data = json.load(sys.stdin)
57
- except json.JSONDecodeError:
58
- print("{}")
59
- sys.exit(0)
60
-
61
- tool_name = input_data.get("tool_name", "")
62
-
63
- # Only for Edit/Write
64
- if tool_name not in ("Edit", "Write", "MultiEdit"):
65
- print("{}")
66
- sys.exit(0)
67
-
68
- # Only when MAP workflow is active
69
- branch = get_branch_name()
70
- if not has_active_workflow(branch):
71
- print("{}")
72
- sys.exit(0)
73
-
74
- # Inject lightweight reminder
75
- reminder = "[MAP] Code changed. Run tests before committing!"
76
- output = {
77
- "hookSpecificOutput": {
78
- "hookEventName": "PostToolUse",
79
- "additionalContext": reminder,
80
- }
81
- }
82
- print(json.dumps(output))
83
- sys.exit(0)
84
-
85
-
86
- if __name__ == "__main__":
87
- main()
File without changes
File without changes