mapify-cli 1.1.0__tar.gz → 1.2.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 (44) hide show
  1. mapify_cli-1.2.0/.claude/hooks/README.md +271 -0
  2. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/PKG-INFO +1 -1
  3. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/pyproject.toml +1 -1
  4. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/__init__.py +177 -0
  5. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/playbook_manager.py +73 -11
  6. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/recitation_manager.py +317 -1
  7. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/commands/map-debug.md +18 -0
  8. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/commands/map-efficient.md +18 -0
  9. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/commands/map-feature.md +18 -0
  10. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/commands/map-refactor.md +18 -0
  11. mapify_cli-1.2.0/src/mapify_cli/templates/hooks/helpers/__init__.py +8 -0
  12. mapify_cli-1.2.0/src/mapify_cli/templates/hooks/helpers/inject_playbook_bullets.py +208 -0
  13. mapify_cli-1.2.0/src/mapify_cli/templates/hooks/helpers/quality_gates.py +474 -0
  14. mapify_cli-1.2.0/src/mapify_cli/templates/hooks/settings.hooks.json +47 -0
  15. mapify_cli-1.2.0/src/mapify_cli/templates/hooks/stop.sh +95 -0
  16. mapify_cli-1.2.0/src/mapify_cli/templates/hooks/user-prompt-submit.sh +66 -0
  17. mapify_cli-1.2.0/src/mapify_cli/templates/settings.hooks.json +47 -0
  18. mapify_cli-1.1.0/.claude/hooks/README.md +0 -55
  19. mapify_cli-1.1.0/src/mapify_cli/templates/hooks/README.md +0 -55
  20. mapify_cli-1.1.0/src/mapify_cli/templates/settings.hooks.json +0 -20
  21. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/.claude/agents/README.md +0 -0
  22. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/.gitignore +0 -0
  23. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/README.md +0 -0
  24. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/docs/context-engineering/README.md +0 -0
  25. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/playbook_query.py +0 -0
  26. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/semantic_search.py +0 -0
  27. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/CHANGELOG.md +0 -0
  28. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/MCP-PATTERNS.md +0 -0
  29. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/README.md +0 -0
  30. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/actor.md +0 -0
  31. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/curator.md +0 -0
  32. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/documentation-reviewer.md +0 -0
  33. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/evaluator.md +0 -0
  34. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/monitor.md +0 -0
  35. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/predictor.md +0 -0
  36. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/reflector.md +0 -0
  37. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/task-decomposer.md +0 -0
  38. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/test-generator.md +0 -0
  39. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/commands/map-fast.md +0 -0
  40. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/commands/map-review.md +0 -0
  41. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/hooks/validate-agent-templates.sh +0 -0
  42. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/tools/__init__.py +0 -0
  43. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/tools/validate_dependencies.py +0 -0
  44. {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/workflow_logger.py +0 -0
@@ -0,0 +1,271 @@
1
+ # MAP Framework - Claude Code Hooks
2
+
3
+ This directory contains Claude Code hooks for the MAP Framework.
4
+
5
+ ## Active Hooks
6
+
7
+ ### UserPromptSubmit - Playbook Auto-Injection
8
+
9
+ **Hook**: `user-prompt-submit.sh`
10
+ **Triggers**: Before user prompt is submitted to Claude Code
11
+ **Purpose**: Automatically injects relevant playbook bullets to enhance context
12
+
13
+ **How It Works**:
14
+ 1. Extracts keywords from user message (filters stop words, handles unicode)
15
+ 2. Queries local playbook using `mapify playbook query` CLI
16
+ 3. Formats top 5 relevant bullets as markdown
17
+ 4. Injects as `additionalContext` field in JSON response
18
+
19
+ **Configuration** (edit `user-prompt-submit.sh`):
20
+ ```bash
21
+ MAX_BULLETS=5 # Number of bullets to inject (default: 5)
22
+ MIN_QUERY_LENGTH=10 # Minimum message length (default: 10 chars)
23
+ ```
24
+
25
+ **Example Output**:
26
+ ```json
27
+ {
28
+ "continue": true,
29
+ "additionalContext": "# Relevant Playbook Patterns\n\n## 1. [impl-0042] ...\n"
30
+ }
31
+ ```
32
+
33
+ **Edge Cases Handled**:
34
+ - Short messages (<10 chars) → Skip injection
35
+ - No playbook database → Skip injection gracefully
36
+ - mapify CLI not found → Skip injection gracefully
37
+ - Query timeout (>10s) → Skip injection gracefully
38
+ - No relevant bullets → Skip injection (no additionalContext)
39
+
40
+ **Performance**: <2s typical latency (keyword extraction + FTS5 query)
41
+
42
+ **Testing**: See [TESTING.md](TESTING.md) for comprehensive test guide
43
+
44
+ ---
45
+
46
+ ### SessionStart - MAP Workflow Context Restoration
47
+
48
+ **Hook**: `session-start.sh`
49
+ **Triggers**: At session start (before first user interaction)
50
+ **Purpose**: Automatically restores `.map/current_plan.md` checkpoint to maintain workflow continuity after context compaction
51
+
52
+ **How It Works**:
53
+ 1. SessionStart event triggers hook execution
54
+ 2. Checks if `.map/current_plan.md` checkpoint file exists
55
+ 3. Calls validator helper script (`helpers/validate_checkpoint_file.py`)
56
+ 4. Validator performs 4-layer security validation (path, size, UTF-8, sanitization)
57
+ 5. Strips control characters while preserving newlines and tabs
58
+ 6. Returns JSON with `additionalContext` containing sanitized checkpoint
59
+ 7. Claude receives restored plan automatically before first interaction
60
+
61
+ **Example Output** (Successful Injection):
62
+ ```json
63
+ {
64
+ "continue": true,
65
+ "additionalContext": "# 🔄 MAP Workflow Context Restored\n\nThis context was automatically restored from your previous session's checkpoint.\nThe plan below reflects your current task progress and helps maintain workflow continuity after context compaction.\n\n---\n\n# Current Task: feat_auth\n## Progress: 3/5 completed\n## Subtask 3: Implement JWT token validation"
66
+ }
67
+ ```
68
+
69
+ **Example Output** (No Checkpoint - New Session):
70
+ ```json
71
+ {
72
+ "continue": true
73
+ }
74
+ ```
75
+
76
+ **Edge Cases Handled**:
77
+ - No checkpoint file → Skip injection gracefully (new session)
78
+ - Checkpoint >256KB → Reject, log error to stderr, skip injection
79
+ - Path traversal (`../../../etc/passwd`) → Reject, log error to stderr, skip injection
80
+ - Invalid UTF-8 encoding → Reject, log error to stderr, skip injection
81
+ - Binary files → Rejected by UTF-8 validation
82
+ - Control characters (ESC codes, NULL bytes) → Sanitized automatically (preserves \n and \t)
83
+ - Empty content after sanitization → Skip injection
84
+ - Validator script missing → Skip injection gracefully, fallback to manual Phase 1
85
+ - Validator script fails → Skip injection gracefully, log error
86
+
87
+ **Security Validations** (4 Layers - Defense-in-Depth):
88
+ 1. **Path Security**: Blocks path traversal attacks (`../`, absolute paths outside `.map/`)
89
+ 2. **Size Bomb Protection**: Rejects files >256KB before reading into memory
90
+ 3. **UTF-8 Validation**: Ensures file is text (blocks binary files like images, executables)
91
+ 4. **Content Sanitization**: Strips control characters (prevents terminal injection, escape code attacks)
92
+
93
+ **Performance**:
94
+ - Typical: <0.5s for small checkpoints (5KB)
95
+ - Maximum: <2s for large checkpoints (100KB)
96
+ - Timeout: 5s (Claude Code hook timeout limit)
97
+ - Validation overhead: ~0.1s (path + size + UTF-8 checks)
98
+
99
+ **Testing**:
100
+ - **Unit Tests**: `tests/hooks/test_validate_checkpoint_file.py` (41 test cases covering all security layers)
101
+ - **Integration Tests**: `tests/hooks/test_session_start_integration.py` (23 test cases for full hook workflow)
102
+
103
+ **Manual Testing**:
104
+ ```bash
105
+ # Test with valid checkpoint
106
+ echo '{}' | .claude/hooks/session-start.sh
107
+
108
+ # Test with large file (size bomb attack)
109
+ dd if=/dev/zero of=.map/current_plan.md bs=1M count=1
110
+ echo '{}' | .claude/hooks/session-start.sh
111
+
112
+ # Test with path traversal (security)
113
+ ln -s /etc/passwd .map/current_plan.md
114
+ echo '{}' | .claude/hooks/session-start.sh
115
+
116
+ # Test with no checkpoint (new session)
117
+ rm -f .map/current_plan.md
118
+ echo '{}' | .claude/hooks/session-start.sh
119
+ ```
120
+
121
+ ---
122
+
123
+ ### PreToolUse - Template Variable Validation
124
+
125
+ **Hook**: `validate-agent-templates.sh`
126
+ **Triggers**: Before `Edit` or `Write` operations on `.claude/agents/*.md` files
127
+ **Purpose**: Prevents accidental removal of critical template variables
128
+
129
+ **Template Variables Protected**:
130
+ - `{{language}}` - Programming language context
131
+ - `{{project_name}}` - Project name
132
+ - `{{framework}}` - Framework context
133
+ - `{{#if playbook_bullets}}` - ACE learning system
134
+ - `{{#if feedback}}` - Monitor→Actor retry loops
135
+ - `{{subtask_description}}` - Task specification
136
+
137
+ **How It Works**:
138
+ 1. Detects when agent files are being modified
139
+ 2. Checks staged content for required template variables
140
+ 3. Blocks commit if variables are missing
141
+ 4. Provides clear error message
142
+
143
+ **Override** (use carefully):
144
+ ```bash
145
+ git commit --no-verify
146
+ ```
147
+
148
+ ---
149
+
150
+ ### Stop - Quality Gates (#NoMessLeftBehind)
151
+
152
+ **Hook**: `stop.sh`
153
+ **Triggers**: After `Write` or `Edit` operations on code files
154
+ **Purpose**: Runs automated quality checks before response submission
155
+
156
+ **Supported Languages**:
157
+ - **Python** (.py): `python -m py_compile` + `pytest` for related tests
158
+ - **Go** (.go): `go fmt` + `go vet` for formatting and static analysis
159
+ - **TypeScript** (.ts, .tsx): `tsc --noEmit` for type checking
160
+ - **Rust** (.rs): `rustc` syntax validation
161
+
162
+ **Checks Performed**:
163
+ 1. **Syntax validation**: Language-specific syntax checker
164
+ 2. **Related tests** (Python only): Runs pytest on:
165
+ - Corresponding test file (e.g., `test_foo.py` for `foo.py`)
166
+ - Test file itself (if already a test file)
167
+ - All tests (if file is in `src/` or `mapify_cli/`)
168
+
169
+ **Configuration**:
170
+ ```bash
171
+ # Disable quality gates entirely
172
+ export QUALITY_GATES_ENABLED=false
173
+
174
+ # Adjust timeout (default: 30s)
175
+ export QUALITY_GATES_TIMEOUT=60
176
+ ```
177
+
178
+ **How It Works**:
179
+ 1. Detects Python file modifications (Write/Edit tools)
180
+ 2. Runs syntax check with `py_compile`
181
+ 3. Finds and runs related pytest tests
182
+ 4. Reports results to stderr (non-blocking)
183
+ 5. Always exits 0 (warnings only, never blocks)
184
+
185
+ **Example Output** (Success):
186
+ ```
187
+ [stop/quality-gates] ========== Quality Gates Results ==========
188
+ File: tests/test_playbook_manager.py
189
+ Status: PASSED
190
+ Summary: All 2 check(s) passed, 0 skipped
191
+
192
+ ✅ python_syntax: Syntax check passed
193
+ ✅ pytest: Tests passed: tests/test_playbook_manager.py
194
+ [stop/quality-gates] ===========================================
195
+ [stop/quality-gates] ✅ All quality checks passed
196
+ ```
197
+
198
+ **Example Output** (Failure - non-blocking warning):
199
+ ```
200
+ [stop/quality-gates] ========== Quality Gates Results ==========
201
+ File: src/mapify_cli/example.py
202
+ Status: FAILED
203
+ Summary: 1 check(s) failed, 1 passed, 0 skipped
204
+
205
+ ❌ python_syntax: Syntax error in src/mapify_cli/example.py
206
+ SyntaxError: unterminated string literal (detected at line 2)
207
+
208
+ ✅ pytest: Tests passed
209
+ [stop/quality-gates] ===========================================
210
+ [stop/quality-gates] ⚠️ Some quality checks FAILED - review output above
211
+ [stop/quality-gates] Note: This is non-blocking, response will be submitted
212
+ ```
213
+
214
+ **Edge Cases Handled**:
215
+ - Non-Python files → Skipped
216
+ - Read/Glob tools → Skipped (no file modifications)
217
+ - pytest not installed → Test check skipped
218
+ - No related tests → Test check skipped
219
+ - Timeout (>30s) → Aborted gracefully
220
+ - Syntax errors → Reported, response still submitted
221
+
222
+ **Performance**: <5s for syntax check, <30s total (with tests)
223
+
224
+ **Testing**:
225
+ ```bash
226
+ # Test with valid Python file
227
+ echo '{"tool": "Write", "parameters": {"file_path": "tests/test_playbook_manager.py"}}' | \
228
+ .claude/hooks/stop.sh
229
+
230
+ # Test with syntax error
231
+ echo '{"tool": "Write", "parameters": {"file_path": "/tmp/broken.py"}}' | \
232
+ .claude/hooks/stop.sh
233
+
234
+ # Test with quality gates disabled
235
+ QUALITY_GATES_ENABLED=false \
236
+ echo '{"tool": "Write", "parameters": {"file_path": "test.py"}}' | \
237
+ .claude/hooks/stop.sh
238
+ ```
239
+
240
+ **Extending for Other Languages**:
241
+ Edit `.claude/hooks/helpers/quality_gates.py` to add support for TypeScript, Go, Rust, etc.
242
+ Update file extension regex in `stop.sh`:
243
+ ```bash
244
+ # Current: if [[ ! "$FILE_PATH" =~ \.(py)$ ]]; then
245
+ # Add TypeScript: if [[ ! "$FILE_PATH" =~ \.(py|ts|tsx)$ ]]; then
246
+ ```
247
+
248
+ ## Removed Hooks
249
+
250
+ The following hooks were removed because **bash hooks cannot call MCP tools**:
251
+
252
+ - ❌ `auto-store-knowledge.sh` (PostToolUse) - Tried to call cipher MCP
253
+ - ❌ `enrich-context.sh` (UserPromptSubmit) - Tried to search cipher MCP
254
+ - ❌ `session-init.sh` (SessionStart) - Tried to load from cipher MCP
255
+ - ❌ `track-metrics.sh` (SubagentStop) - Tried to store metrics in cipher MCP
256
+
257
+ **Why Removed**: Bash hooks execute outside Claude Code's context and cannot invoke MCP tools.
258
+
259
+ **Alternative**: Call MCP tools directly within agent prompts or slash commands.
260
+
261
+ ## Best Practices
262
+
263
+ **DO Use Hooks For**:
264
+ - ✅ File validation (grep, regex)
265
+ - ✅ Git operations (status, diff)
266
+ - ✅ Static analysis (linters)
267
+
268
+ **DON'T Use Hooks For**:
269
+ - ❌ MCP tool calls
270
+ - ❌ Interactive prompts
271
+ - ❌ Long operations (>10s timeout)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mapify-cli
3
- Version: 1.1.0
3
+ Version: 1.2.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 = "1.1.0"
3
+ version = "1.2.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"
@@ -1170,6 +1170,18 @@ def install_hooks(project_path: Path, with_hooks: bool = True) -> int:
1170
1170
  dest_file.chmod(dest_file.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
1171
1171
  hooks_count += 1
1172
1172
 
1173
+ # Copy helpers directory (Python helper scripts)
1174
+ helpers_src = hooks_template_dir / "helpers"
1175
+ if helpers_src.exists() and helpers_src.is_dir():
1176
+ helpers_dest = hooks_dir / "helpers"
1177
+ if helpers_dest.exists():
1178
+ shutil.rmtree(helpers_dest)
1179
+ shutil.copytree(helpers_src, helpers_dest)
1180
+ # Make Python scripts executable
1181
+ for py_file in helpers_dest.glob("*.py"):
1182
+ if py_file.name != "__init__.py":
1183
+ py_file.chmod(py_file.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
1184
+
1173
1185
  # Copy README.md
1174
1186
  readme_src = hooks_template_dir / "README.md"
1175
1187
  if readme_src.exists():
@@ -1898,6 +1910,171 @@ def recitation_clear():
1898
1910
  manager.clear_plan()
1899
1911
  console.print_json(data={"status": "success", "message": "Plan cleared"})
1900
1912
 
1913
+
1914
+ @recitation_app.command("generate-context")
1915
+ def recitation_generate_context():
1916
+ """Generate context.md from project metadata and playbook"""
1917
+ from mapify_cli.recitation_manager import RecitationManager
1918
+ manager = RecitationManager(Path.cwd())
1919
+ try:
1920
+ context_file = manager.generate_context_md()
1921
+ console.print_json(data={
1922
+ "status": "success",
1923
+ "message": "Generated context.md",
1924
+ "file": context_file
1925
+ })
1926
+ except Exception as e:
1927
+ console.print_json(data={
1928
+ "status": "error",
1929
+ "message": f"Failed to generate context.md: {str(e)}"
1930
+ })
1931
+ raise typer.Exit(1)
1932
+
1933
+
1934
+ @recitation_app.command("generate-tasks")
1935
+ def recitation_generate_tasks():
1936
+ """Regenerate tasks.md from current plan"""
1937
+ from mapify_cli.recitation_manager import RecitationManager
1938
+ manager = RecitationManager(Path.cwd())
1939
+ try:
1940
+ plan = manager.get_plan()
1941
+ if not plan:
1942
+ console.print_json(data={
1943
+ "status": "error",
1944
+ "message": "No active plan to generate tasks from"
1945
+ })
1946
+ raise typer.Exit(1)
1947
+
1948
+ manager._generate_tasks_md(plan)
1949
+ console.print_json(data={
1950
+ "status": "success",
1951
+ "message": "Generated tasks.md",
1952
+ "file": str(manager.tasks_file)
1953
+ })
1954
+ except Exception as e:
1955
+ console.print_json(data={
1956
+ "status": "error",
1957
+ "message": f"Failed to generate tasks.md: {str(e)}"
1958
+ })
1959
+ raise typer.Exit(1)
1960
+
1961
+
1962
+ @recitation_app.command("get-docs")
1963
+ def recitation_get_docs():
1964
+ """Get all dev docs content (plan + context + tasks)"""
1965
+ from mapify_cli.recitation_manager import RecitationManager
1966
+ manager = RecitationManager(Path.cwd())
1967
+ try:
1968
+ docs = manager.get_dev_docs()
1969
+ console.print_json(data={
1970
+ "status": "success",
1971
+ "docs": docs
1972
+ })
1973
+ except Exception as e:
1974
+ console.print_json(data={
1975
+ "status": "error",
1976
+ "message": f"Failed to get dev docs: {str(e)}"
1977
+ })
1978
+ raise typer.Exit(1)
1979
+
1980
+
1981
+ @recitation_app.command("checkpoint")
1982
+ def recitation_checkpoint():
1983
+ """Show current MAP workflow state and recovery instructions for post-compaction"""
1984
+ from mapify_cli.recitation_manager import RecitationManager
1985
+ import os
1986
+
1987
+ manager = RecitationManager(Path.cwd())
1988
+ map_dir = Path.cwd() / ".map"
1989
+
1990
+ # Check if .map directory exists
1991
+ if not map_dir.exists():
1992
+ console.print("[yellow]⚠️ No active MAP workflow found[/yellow]")
1993
+ console.print("\nThe `.map/` directory doesn't exist yet.")
1994
+ console.print("Start a MAP workflow with: [cyan]/map-feature[/cyan], [cyan]/map-debug[/cyan], or [cyan]/map-refactor[/cyan]")
1995
+ raise typer.Exit(0)
1996
+
1997
+ # Define files to check
1998
+ files_to_check = {
1999
+ "plan_json": manager.plan_json,
2000
+ "plan_md": manager.plan_file,
2001
+ "context_md": manager.context_file,
2002
+ "tasks_md": manager.tasks_file
2003
+ }
2004
+
2005
+ # Check which files exist
2006
+ existing_files = {name: path for name, path in files_to_check.items() if path.exists()}
2007
+
2008
+ if not existing_files:
2009
+ console.print("[yellow]⚠️ MAP workflow directory exists but no state files found[/yellow]")
2010
+ console.print(f"\nDirectory: {map_dir.absolute()}")
2011
+ console.print("Expected files are missing. The workflow may not have been initialized properly.")
2012
+ raise typer.Exit(0)
2013
+
2014
+ # Print checkpoint header
2015
+ console.print("\n[bold green]📍 MAP Workflow Checkpoint[/bold green]")
2016
+ console.print("=" * 60)
2017
+
2018
+ # Print file locations
2019
+ console.print("\n[bold]📁 State Files (absolute paths):[/bold]")
2020
+ for name, path in existing_files.items():
2021
+ file_size = path.stat().st_size
2022
+ console.print(f" • {path.absolute()} ([dim]{file_size} bytes[/dim])")
2023
+
2024
+ # Show current task status if plan exists
2025
+ if "plan_json" in existing_files:
2026
+ try:
2027
+ plan = manager.get_plan()
2028
+ if plan:
2029
+ console.print(f"\n[bold]🎯 Current Task:[/bold] {plan.goal}")
2030
+ console.print(f"[bold]📊 Progress:[/bold] {plan.current_subtask_id}/{len(plan.subtasks)} subtasks")
2031
+
2032
+ # Show current subtask
2033
+ current = next((s for s in plan.subtasks if s.id == plan.current_subtask_id), None)
2034
+ if current:
2035
+ console.print(f"[bold]▶️ Active:[/bold] {current.description} [{current.status}]")
2036
+ except Exception as e:
2037
+ console.print(f"[yellow]⚠️ Could not parse plan: {str(e)}[/yellow]")
2038
+
2039
+ # Print file contents with headers
2040
+ console.print("\n[bold]📄 File Contents:[/bold]")
2041
+ console.print("-" * 60)
2042
+
2043
+ for name, path in existing_files.items():
2044
+ console.print(f"\n[bold cyan]### {path.name}[/bold cyan]")
2045
+ try:
2046
+ content = path.read_text()
2047
+ # Truncate very long files
2048
+ if len(content) > 2000:
2049
+ console.print(content[:2000])
2050
+ console.print(f"\n[dim]... (truncated, {len(content) - 2000} more chars)[/dim]")
2051
+ else:
2052
+ console.print(content)
2053
+ except Exception as e:
2054
+ console.print(f"[red]Error reading file: {str(e)}[/red]")
2055
+
2056
+ # Print recovery instructions
2057
+ console.print("\n" + "=" * 60)
2058
+ console.print("[bold green]🔄 Recovery Instructions (for post-compaction):[/bold green]")
2059
+ console.print("\nAfter context compaction, paste this to Claude:\n")
2060
+
2061
+ recovery_message = f"""```
2062
+ Continue MAP workflow from checkpoint:
2063
+
2064
+ @{manager.plan_file.absolute()}"""
2065
+
2066
+ if "context_md" in existing_files:
2067
+ recovery_message += f"\n@{manager.context_file.absolute()}"
2068
+ if "tasks_md" in existing_files:
2069
+ recovery_message += f"\n@{manager.tasks_file.absolute()}"
2070
+
2071
+ recovery_message += "\n```"
2072
+
2073
+ console.print(recovery_message)
2074
+ console.print("\n[dim]Claude will automatically resume from where you left off.[/dim]")
2075
+ console.print("=" * 60 + "\n")
2076
+
2077
+
1901
2078
  # Playbook commands
1902
2079
 
1903
2080
  @playbook_app.command("stats")
@@ -46,6 +46,9 @@ QUALITY_SCORE_MAX = 10.0 # Typical max quality score for bullets
46
46
  RELEVANCE_WEIGHT = 0.7 # Weight for relevance in combined score
47
47
  QUALITY_WEIGHT = 0.3 # Weight for quality in combined score
48
48
 
49
+ # Schema version constants
50
+ CURRENT_SCHEMA_VERSION = '2.1' # Added executable_scripts field
51
+
49
52
 
50
53
  class PlaybookManager:
51
54
  """Manages ACE-style playbook with incremental delta updates."""
@@ -73,6 +76,9 @@ class PlaybookManager:
73
76
  # Enable WAL mode for better concurrency
74
77
  self.db_conn.execute("PRAGMA journal_mode=WAL")
75
78
 
79
+ # Run schema migrations if needed
80
+ self._migrate_schema()
81
+
76
82
  # Load playbook structure from SQLite for backward compatibility
77
83
  self.playbook = self._load_playbook_from_db()
78
84
 
@@ -163,7 +169,8 @@ class PlaybookManager:
163
169
  deprecated INTEGER DEFAULT 0,
164
170
  deprecation_reason TEXT,
165
171
  tags TEXT,
166
- related_bullets TEXT
172
+ related_bullets TEXT,
173
+ executable_scripts TEXT
167
174
  )
168
175
  """)
169
176
 
@@ -222,12 +229,54 @@ class PlaybookManager:
222
229
  cursor.execute("INSERT OR IGNORE INTO metadata VALUES ('version', '1.0')")
223
230
  cursor.execute(f"INSERT OR IGNORE INTO metadata VALUES ('last_updated', '{now}')")
224
231
  cursor.execute("INSERT OR IGNORE INTO metadata VALUES ('total_bullets', '0')")
225
- cursor.execute("INSERT OR IGNORE INTO metadata VALUES ('schema_version', '2.0')")
232
+ cursor.execute(f"INSERT OR IGNORE INTO metadata VALUES ('schema_version', '{CURRENT_SCHEMA_VERSION}')")
226
233
  cursor.execute("INSERT OR IGNORE INTO metadata VALUES ('top_k', '5')")
227
234
 
228
235
  conn.commit()
229
236
  conn.close()
230
237
 
238
+ def _migrate_schema(self) -> None:
239
+ """
240
+ Run schema migrations to upgrade database to current version.
241
+
242
+ Migrations are idempotent and safe to run multiple times.
243
+ Each migration checks schema_version before applying changes.
244
+ """
245
+ cursor = self.db_conn.cursor()
246
+
247
+ # Get current schema version
248
+ cursor.execute("SELECT value FROM metadata WHERE key = 'schema_version'")
249
+ result = cursor.fetchone()
250
+ current_version = result[0] if result else '2.0' # Default for old DBs without version
251
+
252
+ # Migration: 2.0 -> 2.1 (add executable_scripts field)
253
+ if current_version == '2.0':
254
+ print("Migrating schema from 2.0 to 2.1 (adding executable_scripts field)...", file=sys.stderr)
255
+
256
+ # Check if column already exists (idempotency)
257
+ cursor.execute("PRAGMA table_info(bullets)")
258
+ columns = [row[1] for row in cursor.fetchall()]
259
+
260
+ if 'executable_scripts' not in columns:
261
+ # Add new column with NULL default (backward compatible)
262
+ cursor.execute("ALTER TABLE bullets ADD COLUMN executable_scripts TEXT")
263
+ print("✓ Added executable_scripts column to bullets table", file=sys.stderr)
264
+ else:
265
+ print("✓ executable_scripts column already exists, skipping", file=sys.stderr)
266
+
267
+ # Update schema version
268
+ cursor.execute("UPDATE metadata SET value = '2.1' WHERE key = 'schema_version'")
269
+ self.db_conn.commit()
270
+ print("✓ Schema migration complete (2.0 -> 2.1)", file=sys.stderr)
271
+
272
+ elif current_version == CURRENT_SCHEMA_VERSION:
273
+ # Already at current version, no migration needed
274
+ pass
275
+
276
+ else:
277
+ # Future version or unknown version
278
+ print(f"Warning: Unknown schema version {current_version}, expected {CURRENT_SCHEMA_VERSION}", file=sys.stderr)
279
+
231
280
  def _migrate_json_to_sqlite(self) -> None:
232
281
  """
233
282
  Migrate existing playbook.json to SQLite database.
@@ -266,8 +315,8 @@ class PlaybookManager:
266
315
  helpful_count, harmful_count,
267
316
  created_at, last_used_at,
268
317
  deprecated, deprecation_reason,
269
- tags, related_bullets)
270
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
318
+ tags, related_bullets, executable_scripts)
319
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
271
320
  """, (
272
321
  bullet['id'],
273
322
  section_name,
@@ -280,7 +329,8 @@ class PlaybookManager:
280
329
  1 if bullet.get('deprecated', False) else 0,
281
330
  bullet.get('deprecation_reason'),
282
331
  json.dumps(bullet.get('tags', [])),
283
- json.dumps(bullet.get('related_bullets', []))
332
+ json.dumps(bullet.get('related_bullets', [])),
333
+ json.dumps(bullet.get('executable_scripts', [])) if bullet.get('executable_scripts') else None
284
334
  ))
285
335
  total_bullets += 1
286
336
  except sqlite3.IntegrityError as e:
@@ -334,7 +384,7 @@ class PlaybookManager:
334
384
  SELECT section, id, content, code_example,
335
385
  helpful_count, harmful_count, quality_score,
336
386
  created_at, last_used_at, deprecated, deprecation_reason,
337
- tags, related_bullets
387
+ tags, related_bullets, executable_scripts
338
388
  FROM bullets
339
389
  ORDER BY section, quality_score DESC
340
390
  """)
@@ -371,6 +421,8 @@ class PlaybookManager:
371
421
  bullet['tags'] = json.loads(row['tags'])
372
422
  if row['related_bullets']:
373
423
  bullet['related_bullets'] = json.loads(row['related_bullets'])
424
+ if row['executable_scripts']:
425
+ bullet['executable_scripts'] = json.loads(row['executable_scripts'])
374
426
 
375
427
  sections[section_name]['bullets'].append(bullet)
376
428
 
@@ -422,7 +474,8 @@ class PlaybookManager:
422
474
  content=op["content"],
423
475
  code_example=op.get("code_example"),
424
476
  related_to=op.get("related_to", []),
425
- tags=op.get("tags", [])
477
+ tags=op.get("tags", []),
478
+ executable_scripts=op.get("executable_scripts", [])
426
479
  )
427
480
  summary["added"] += 1
428
481
 
@@ -461,7 +514,8 @@ class PlaybookManager:
461
514
  content: str,
462
515
  code_example: Optional[str] = None,
463
516
  related_to: List[str] = None,
464
- tags: List[str] = None
517
+ tags: List[str] = None,
518
+ executable_scripts: List[str] = None
465
519
  ) -> str:
466
520
  """Add new bullet to section (saves to SQLite)."""
467
521
  if section not in self.playbook["sections"]:
@@ -477,8 +531,8 @@ class PlaybookManager:
477
531
  helpful_count, harmful_count,
478
532
  created_at, last_used_at,
479
533
  deprecated, deprecation_reason,
480
- tags, related_bullets)
481
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
534
+ tags, related_bullets, executable_scripts)
535
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
482
536
  """, (
483
537
  bullet_id,
484
538
  section,
@@ -491,7 +545,8 @@ class PlaybookManager:
491
545
  0, # deprecated
492
546
  None,
493
547
  json.dumps(tags or []),
494
- json.dumps(related_to or [])
548
+ json.dumps(related_to or []),
549
+ json.dumps(executable_scripts) if executable_scripts else None
495
550
  ))
496
551
 
497
552
  # Update metadata
@@ -514,6 +569,9 @@ class PlaybookManager:
514
569
  "deprecated": False,
515
570
  "deprecation_reason": None
516
571
  }
572
+ if executable_scripts:
573
+ bullet["executable_scripts"] = executable_scripts
574
+
517
575
  self.playbook["sections"][section]["bullets"].append(bullet)
518
576
  self.playbook["metadata"]["total_bullets"] += 1
519
577
 
@@ -949,6 +1007,10 @@ class PlaybookManager:
949
1007
  import string
950
1008
  fts_query = params.query
951
1009
 
1010
+ # FTS5 tokenizer splits hyphens at index time ("session-start" → ["session", "start"])
1011
+ # Align query tokenization by replacing hyphens with spaces
1012
+ fts_query = fts_query.replace('-', ' ')
1013
+
952
1014
  # Remove FTS5 special characters: @ # ( ) " ' :
953
1015
  fts_special_chars = '@#()"\':'
954
1016
  for char in fts_special_chars: