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.
- mapify_cli-1.2.0/.claude/hooks/README.md +271 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/PKG-INFO +1 -1
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/pyproject.toml +1 -1
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/__init__.py +177 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/playbook_manager.py +73 -11
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/recitation_manager.py +317 -1
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/commands/map-debug.md +18 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/commands/map-efficient.md +18 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/commands/map-feature.md +18 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/commands/map-refactor.md +18 -0
- mapify_cli-1.2.0/src/mapify_cli/templates/hooks/helpers/__init__.py +8 -0
- mapify_cli-1.2.0/src/mapify_cli/templates/hooks/helpers/inject_playbook_bullets.py +208 -0
- mapify_cli-1.2.0/src/mapify_cli/templates/hooks/helpers/quality_gates.py +474 -0
- mapify_cli-1.2.0/src/mapify_cli/templates/hooks/settings.hooks.json +47 -0
- mapify_cli-1.2.0/src/mapify_cli/templates/hooks/stop.sh +95 -0
- mapify_cli-1.2.0/src/mapify_cli/templates/hooks/user-prompt-submit.sh +66 -0
- mapify_cli-1.2.0/src/mapify_cli/templates/settings.hooks.json +47 -0
- mapify_cli-1.1.0/.claude/hooks/README.md +0 -55
- mapify_cli-1.1.0/src/mapify_cli/templates/hooks/README.md +0 -55
- mapify_cli-1.1.0/src/mapify_cli/templates/settings.hooks.json +0 -20
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/.claude/agents/README.md +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/.gitignore +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/README.md +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/docs/context-engineering/README.md +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/playbook_query.py +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/semantic_search.py +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/CHANGELOG.md +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/MCP-PATTERNS.md +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/README.md +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/actor.md +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/curator.md +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/documentation-reviewer.md +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/evaluator.md +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/monitor.md +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/predictor.md +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/reflector.md +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/task-decomposer.md +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/agents/test-generator.md +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/commands/map-fast.md +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/commands/map-review.md +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/templates/hooks/validate-agent-templates.sh +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/tools/__init__.py +0 -0
- {mapify_cli-1.1.0 → mapify_cli-1.2.0}/src/mapify_cli/tools/validate_dependencies.py +0 -0
- {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.
|
|
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
|
|
@@ -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', '
|
|
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:
|