mapify-cli 3.5.0__py3-none-any.whl → 3.7.0__py3-none-any.whl
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/__init__.py +520 -1455
- mapify_cli/cli_ui.py +320 -0
- mapify_cli/config/__init__.py +38 -0
- mapify_cli/config/mcp.py +293 -0
- mapify_cli/config/project_config.py +275 -0
- mapify_cli/config/settings.py +170 -0
- mapify_cli/delivery/__init__.py +60 -0
- mapify_cli/delivery/agent_generator.py +381 -0
- mapify_cli/delivery/file_copier.py +442 -0
- mapify_cli/delivery/managed_file_copier.py +349 -0
- mapify_cli/repo_insight.py +76 -0
- mapify_cli/schemas.py +162 -0
- mapify_cli/templates/agents/actor.md +65 -37
- mapify_cli/templates/agents/final-verifier.md +16 -1
- mapify_cli/templates/agents/monitor.md +99 -54
- mapify_cli/templates/agents/predictor.md +5 -22
- mapify_cli/templates/agents/task-decomposer.md +35 -4
- mapify_cli/templates/commands/map-check.md +116 -13
- mapify_cli/templates/commands/map-efficient.md +342 -284
- mapify_cli/templates/commands/map-learn.md +153 -98
- mapify_cli/templates/commands/map-plan.md +159 -20
- mapify_cli/templates/commands/map-resume.md +54 -27
- mapify_cli/templates/commands/map-review.md +72 -0
- mapify_cli/templates/commands/map-task.md +15 -11
- mapify_cli/templates/commands/map-tdd.md +66 -35
- mapify_cli/templates/hooks/post-compact-context.py +1 -1
- mapify_cli/templates/hooks/ralph-context-pruner.py +3 -3
- mapify_cli/templates/hooks/ralph-iteration-logger.py +88 -0
- mapify_cli/templates/hooks/safety-guardrails.py +39 -3
- mapify_cli/templates/hooks/workflow-context-injector.py +94 -16
- mapify_cli/templates/hooks/workflow-gate.py +153 -162
- mapify_cli/templates/map/scripts/diagnostics.py +187 -0
- mapify_cli/templates/map/scripts/map_orchestrator.py +563 -295
- mapify_cli/templates/map/scripts/map_step_runner.py +921 -51
- mapify_cli/templates/map/scripts/map_utils.py +30 -0
- mapify_cli/templates/references/escalation-matrix.md +24 -0
- mapify_cli/templates/references/step-state-schema.md +1 -12
- mapify_cli/templates/references/workflow-state-schema.md +3 -264
- mapify_cli/templates/rules/learned/README.md +18 -0
- mapify_cli/templates/settings.json +6 -0
- mapify_cli/templates/skills/map-learn/SKILL.md +321 -0
- mapify_cli/templates/skills/map-learn/templates/example-rules.md +19 -0
- mapify_cli/templates/skills/map-learn/templates/rules-unconditional.md +5 -0
- mapify_cli/templates/skills/map-learn/templates/rules-with-paths.md +10 -0
- mapify_cli/templates/skills/map-planning/SKILL.md +1 -3
- mapify_cli/templates/skills/skill-rules.json +13 -37
- mapify_cli/templates/workflow-rules.json +0 -19
- {mapify_cli-3.5.0.dist-info → mapify_cli-3.7.0.dist-info}/METADATA +19 -4
- mapify_cli-3.7.0.dist-info/RECORD +91 -0
- mapify_cli/templates/commands/map-debate.md +0 -417
- mapify_cli/templates/skills/map-cli-reference/SKILL.md +0 -124
- mapify_cli/templates/skills/map-cli-reference/scripts/check-command.sh +0 -117
- mapify_cli/templates/skills/map-workflows-guide/SKILL.md +0 -529
- mapify_cli/templates/skills/map-workflows-guide/resources/agent-architecture.md +0 -266
- mapify_cli/templates/skills/map-workflows-guide/resources/map-debug-deep-dive.md +0 -258
- mapify_cli/templates/skills/map-workflows-guide/resources/map-efficient-deep-dive.md +0 -202
- mapify_cli/templates/skills/map-workflows-guide/resources/map-fast-deep-dive.md +0 -234
- mapify_cli/templates/skills/map-workflows-guide/resources/map-feature-deep-dive.md +0 -235
- mapify_cli/templates/skills/map-workflows-guide/resources/map-refactor-deep-dive.md +0 -332
- mapify_cli/templates/skills/map-workflows-guide/scripts/validate-workflow-choice.py +0 -159
- mapify_cli-3.5.0.dist-info/RECORD +0 -87
- {mapify_cli-3.5.0.dist-info → mapify_cli-3.7.0.dist-info}/WHEEL +0 -0
- {mapify_cli-3.5.0.dist-info → mapify_cli-3.7.0.dist-info}/entry_points.txt +0 -0
mapify_cli/__init__.py
CHANGED
|
@@ -23,23 +23,19 @@ Or install globally:
|
|
|
23
23
|
mapify check
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
-
__version__ = "3.
|
|
26
|
+
__version__ = "3.7.0"
|
|
27
27
|
|
|
28
|
-
import copy
|
|
29
28
|
import os
|
|
30
29
|
import subprocess
|
|
31
30
|
import sys
|
|
32
31
|
import shutil
|
|
33
|
-
import
|
|
34
|
-
import uuid
|
|
32
|
+
import ssl
|
|
35
33
|
from datetime import datetime
|
|
36
34
|
from pathlib import Path
|
|
37
35
|
from typing import Optional, List, Dict, Any
|
|
38
36
|
|
|
39
37
|
import typer
|
|
40
38
|
import httpx
|
|
41
|
-
import readchar
|
|
42
|
-
import ssl
|
|
43
39
|
|
|
44
40
|
try:
|
|
45
41
|
import truststore
|
|
@@ -48,14 +44,51 @@ try:
|
|
|
48
44
|
except ImportError:
|
|
49
45
|
HAS_TRUSTSTORE = False
|
|
50
46
|
|
|
51
|
-
from rich.console import Console
|
|
52
47
|
from rich.panel import Panel
|
|
53
|
-
from rich.text import Text
|
|
54
48
|
from rich.live import Live
|
|
55
49
|
from rich.align import Align
|
|
56
50
|
from rich.table import Table
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
|
|
52
|
+
# Local submodule re-exports (v3.5.0 platform refactor)
|
|
53
|
+
from mapify_cli.cli_ui import console
|
|
54
|
+
from mapify_cli.cli_ui import (
|
|
55
|
+
StepTracker,
|
|
56
|
+
BannerGroup,
|
|
57
|
+
get_key as get_key,
|
|
58
|
+
select_with_arrows as select_with_arrows,
|
|
59
|
+
select_multiple_with_arrows as select_multiple_with_arrows,
|
|
60
|
+
show_banner,
|
|
61
|
+
BANNER as BANNER,
|
|
62
|
+
TAGLINE as TAGLINE,
|
|
63
|
+
)
|
|
64
|
+
from mapify_cli.delivery import (
|
|
65
|
+
create_task_decomposer_content as create_task_decomposer_content,
|
|
66
|
+
create_actor_content as create_actor_content,
|
|
67
|
+
create_monitor_content as create_monitor_content,
|
|
68
|
+
create_predictor_content as create_predictor_content,
|
|
69
|
+
create_evaluator_content as create_evaluator_content,
|
|
70
|
+
create_reflector_content as create_reflector_content,
|
|
71
|
+
create_documentation_reviewer_content as create_documentation_reviewer_content,
|
|
72
|
+
create_agent_files,
|
|
73
|
+
create_reference_files,
|
|
74
|
+
create_command_files,
|
|
75
|
+
create_skill_files,
|
|
76
|
+
create_hook_files,
|
|
77
|
+
create_config_files,
|
|
78
|
+
create_commands_dir as create_commands_dir,
|
|
79
|
+
create_map_tools,
|
|
80
|
+
create_rules_dir,
|
|
81
|
+
)
|
|
82
|
+
from mapify_cli.config import (
|
|
83
|
+
configure_global_permissions,
|
|
84
|
+
create_or_merge_project_settings_local,
|
|
85
|
+
create_mcp_config,
|
|
86
|
+
build_standard_mcp_servers,
|
|
87
|
+
read_project_mcp_json,
|
|
88
|
+
write_project_mcp_json as write_project_mcp_json,
|
|
89
|
+
merge_mcp_json as merge_mcp_json,
|
|
90
|
+
create_or_merge_project_mcp_json,
|
|
91
|
+
)
|
|
59
92
|
|
|
60
93
|
|
|
61
94
|
# Create secure SSL context with proper fallback
|
|
@@ -93,291 +126,6 @@ INDIVIDUAL_MCP_SERVERS = {
|
|
|
93
126
|
"deepwiki": "GitHub repository intelligence",
|
|
94
127
|
}
|
|
95
128
|
|
|
96
|
-
# ASCII Art Banner
|
|
97
|
-
BANNER = """
|
|
98
|
-
╔╦╗╔═╗╔═╗ ╦╔═╦╔╦╗
|
|
99
|
-
║║║╠═╣╠═╝ ╠╩╗║ ║
|
|
100
|
-
╩ ╩╩ ╩╩ ╩ ╩╩ ╩
|
|
101
|
-
"""
|
|
102
|
-
|
|
103
|
-
TAGLINE = "MAP Kit - Modular Agentic Planner Framework for Claude Code"
|
|
104
|
-
|
|
105
|
-
console = Console()
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
class StepTracker:
|
|
109
|
-
"""Track and render hierarchical steps as a tree"""
|
|
110
|
-
|
|
111
|
-
def __init__(self, title: str):
|
|
112
|
-
self.title = title
|
|
113
|
-
self.steps: List[Dict[str, Any]] = (
|
|
114
|
-
[]
|
|
115
|
-
) # list of dicts: {key, label, status, detail}
|
|
116
|
-
self._refresh_cb = None
|
|
117
|
-
|
|
118
|
-
def attach_refresh(self, cb):
|
|
119
|
-
self._refresh_cb = cb
|
|
120
|
-
|
|
121
|
-
def add(self, key: str, label: str):
|
|
122
|
-
if key not in [s["key"] for s in self.steps]:
|
|
123
|
-
self.steps.append(
|
|
124
|
-
{"key": key, "label": label, "status": "pending", "detail": ""}
|
|
125
|
-
)
|
|
126
|
-
self._maybe_refresh()
|
|
127
|
-
|
|
128
|
-
def start(self, key: str, detail: str = ""):
|
|
129
|
-
self._update(key, status="running", detail=detail)
|
|
130
|
-
|
|
131
|
-
def complete(self, key: str, detail: str = ""):
|
|
132
|
-
self._update(key, status="done", detail=detail)
|
|
133
|
-
|
|
134
|
-
def error(self, key: str, detail: str = ""):
|
|
135
|
-
self._update(key, status="error", detail=detail)
|
|
136
|
-
|
|
137
|
-
def skip(self, key: str, detail: str = ""):
|
|
138
|
-
self._update(key, status="skipped", detail=detail)
|
|
139
|
-
|
|
140
|
-
def _update(self, key: str, status: str, detail: str):
|
|
141
|
-
for s in self.steps:
|
|
142
|
-
if s["key"] == key:
|
|
143
|
-
s["status"] = status
|
|
144
|
-
if detail:
|
|
145
|
-
s["detail"] = detail
|
|
146
|
-
self._maybe_refresh()
|
|
147
|
-
return
|
|
148
|
-
# If not present, add it
|
|
149
|
-
self.steps.append(
|
|
150
|
-
{"key": key, "label": key, "status": status, "detail": detail}
|
|
151
|
-
)
|
|
152
|
-
self._maybe_refresh()
|
|
153
|
-
|
|
154
|
-
def _maybe_refresh(self):
|
|
155
|
-
if self._refresh_cb:
|
|
156
|
-
try:
|
|
157
|
-
self._refresh_cb()
|
|
158
|
-
except Exception:
|
|
159
|
-
pass
|
|
160
|
-
|
|
161
|
-
def render(self):
|
|
162
|
-
tree = Tree(f"[cyan]{self.title}[/cyan]", guide_style="grey50")
|
|
163
|
-
for step in self.steps:
|
|
164
|
-
label = step["label"]
|
|
165
|
-
detail_text = step["detail"].strip() if step["detail"] else ""
|
|
166
|
-
|
|
167
|
-
# Status symbols
|
|
168
|
-
status = step["status"]
|
|
169
|
-
if status == "done":
|
|
170
|
-
symbol = "[green]●[/green]"
|
|
171
|
-
elif status == "pending":
|
|
172
|
-
symbol = "[green dim]○[/green dim]"
|
|
173
|
-
elif status == "running":
|
|
174
|
-
symbol = "[cyan]○[/cyan]"
|
|
175
|
-
elif status == "error":
|
|
176
|
-
symbol = "[red]●[/red]"
|
|
177
|
-
elif status == "skipped":
|
|
178
|
-
symbol = "[yellow]○[/yellow]"
|
|
179
|
-
else:
|
|
180
|
-
symbol = " "
|
|
181
|
-
|
|
182
|
-
if status == "pending":
|
|
183
|
-
# Entire line light gray (pending)
|
|
184
|
-
if detail_text:
|
|
185
|
-
line = (
|
|
186
|
-
f"{symbol} [bright_black]{label} ({detail_text})[/bright_black]"
|
|
187
|
-
)
|
|
188
|
-
else:
|
|
189
|
-
line = f"{symbol} [bright_black]{label}[/bright_black]"
|
|
190
|
-
else:
|
|
191
|
-
# Label white, detail light gray in parentheses
|
|
192
|
-
if detail_text:
|
|
193
|
-
line = f"{symbol} [white]{label}[/white] [bright_black]({detail_text})[/bright_black]"
|
|
194
|
-
else:
|
|
195
|
-
line = f"{symbol} [white]{label}[/white]"
|
|
196
|
-
|
|
197
|
-
tree.add(line)
|
|
198
|
-
return tree
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def get_key():
|
|
202
|
-
"""Get a single keypress in a cross-platform way"""
|
|
203
|
-
key = readchar.readkey()
|
|
204
|
-
|
|
205
|
-
# Arrow keys
|
|
206
|
-
if key == readchar.key.UP or key == readchar.key.CTRL_P:
|
|
207
|
-
return "up"
|
|
208
|
-
if key == readchar.key.DOWN or key == readchar.key.CTRL_N:
|
|
209
|
-
return "down"
|
|
210
|
-
|
|
211
|
-
# Enter/Return - support multiple variants for cross-platform compatibility
|
|
212
|
-
if key == readchar.key.ENTER or key == "\r" or key == "\n":
|
|
213
|
-
return "enter"
|
|
214
|
-
# Also check for readchar.key.CR (carriage return) if it exists
|
|
215
|
-
if hasattr(readchar.key, "CR") and key == readchar.key.CR:
|
|
216
|
-
return "enter"
|
|
217
|
-
if hasattr(readchar.key, "LF") and key == readchar.key.LF:
|
|
218
|
-
return "enter"
|
|
219
|
-
|
|
220
|
-
# Space for toggle
|
|
221
|
-
if key == " ":
|
|
222
|
-
return "space"
|
|
223
|
-
|
|
224
|
-
# Escape
|
|
225
|
-
if key == readchar.key.ESC:
|
|
226
|
-
return "escape"
|
|
227
|
-
|
|
228
|
-
# Ctrl+C
|
|
229
|
-
if key == readchar.key.CTRL_C:
|
|
230
|
-
raise KeyboardInterrupt
|
|
231
|
-
|
|
232
|
-
return key
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
def select_with_arrows(
|
|
236
|
-
options: dict,
|
|
237
|
-
prompt_text: str = "Select an option",
|
|
238
|
-
default_key: Optional[str] = None,
|
|
239
|
-
) -> str:
|
|
240
|
-
"""Interactive selection using arrow keys"""
|
|
241
|
-
option_keys = list(options.keys())
|
|
242
|
-
if default_key and default_key in option_keys:
|
|
243
|
-
selected_index = option_keys.index(default_key)
|
|
244
|
-
else:
|
|
245
|
-
selected_index = 0
|
|
246
|
-
|
|
247
|
-
selected_key = None
|
|
248
|
-
|
|
249
|
-
def create_selection_panel():
|
|
250
|
-
"""Create the selection panel with current selection highlighted."""
|
|
251
|
-
table = Table.grid(padding=(0, 2))
|
|
252
|
-
table.add_column(style="cyan", justify="left", width=3)
|
|
253
|
-
table.add_column(style="white", justify="left")
|
|
254
|
-
|
|
255
|
-
for i, key in enumerate(option_keys):
|
|
256
|
-
if i == selected_index:
|
|
257
|
-
table.add_row("▶", f"[cyan]{key}[/cyan] [dim]({options[key]})[/dim]")
|
|
258
|
-
else:
|
|
259
|
-
table.add_row(" ", f"[cyan]{key}[/cyan] [dim]({options[key]})[/dim]")
|
|
260
|
-
|
|
261
|
-
table.add_row("", "")
|
|
262
|
-
table.add_row(
|
|
263
|
-
"", "[dim]Use ↑/↓ to navigate, Enter to select, Esc to cancel[/dim]"
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
return Panel(
|
|
267
|
-
table,
|
|
268
|
-
title=f"[bold]{prompt_text}[/bold]",
|
|
269
|
-
border_style="cyan",
|
|
270
|
-
padding=(1, 2),
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
console.print()
|
|
274
|
-
|
|
275
|
-
with Live(
|
|
276
|
-
create_selection_panel(), console=console, transient=True, auto_refresh=False
|
|
277
|
-
) as live:
|
|
278
|
-
while True:
|
|
279
|
-
try:
|
|
280
|
-
key = get_key()
|
|
281
|
-
if key == "up":
|
|
282
|
-
selected_index = (selected_index - 1) % len(option_keys)
|
|
283
|
-
elif key == "down":
|
|
284
|
-
selected_index = (selected_index + 1) % len(option_keys)
|
|
285
|
-
elif key == "enter":
|
|
286
|
-
selected_key = option_keys[selected_index]
|
|
287
|
-
break
|
|
288
|
-
elif key == "escape":
|
|
289
|
-
console.print("\n[yellow]Selection cancelled[/yellow]")
|
|
290
|
-
raise typer.Exit(1)
|
|
291
|
-
|
|
292
|
-
live.update(create_selection_panel(), refresh=True)
|
|
293
|
-
|
|
294
|
-
except KeyboardInterrupt:
|
|
295
|
-
console.print("\n[yellow]Selection cancelled[/yellow]")
|
|
296
|
-
raise typer.Exit(1)
|
|
297
|
-
|
|
298
|
-
return selected_key
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def select_multiple_with_arrows(
|
|
302
|
-
options: dict, prompt_text: str = "Select options"
|
|
303
|
-
) -> List[str]:
|
|
304
|
-
"""Interactive multiple selection using arrow keys and space"""
|
|
305
|
-
option_keys = list(options.keys())
|
|
306
|
-
selected_index = 0
|
|
307
|
-
selected_items: set[str] = set()
|
|
308
|
-
|
|
309
|
-
def create_selection_panel():
|
|
310
|
-
"""Create the selection panel with checkboxes"""
|
|
311
|
-
table = Table.grid(padding=(0, 2))
|
|
312
|
-
table.add_column(style="cyan", justify="left", width=3)
|
|
313
|
-
table.add_column(style="white", justify="left")
|
|
314
|
-
|
|
315
|
-
for i, key in enumerate(option_keys):
|
|
316
|
-
checkbox = "[x]" if key in selected_items else "[ ]"
|
|
317
|
-
if i == selected_index:
|
|
318
|
-
table.add_row(
|
|
319
|
-
"▶", f"{checkbox} [cyan]{key}[/cyan] [dim]({options[key]})[/dim]"
|
|
320
|
-
)
|
|
321
|
-
else:
|
|
322
|
-
table.add_row(
|
|
323
|
-
" ", f"{checkbox} [cyan]{key}[/cyan] [dim]({options[key]})[/dim]"
|
|
324
|
-
)
|
|
325
|
-
|
|
326
|
-
table.add_row("", "")
|
|
327
|
-
table.add_row("", f"[dim]Selected: {len(selected_items)}/{len(options)}[/dim]")
|
|
328
|
-
table.add_row(
|
|
329
|
-
"",
|
|
330
|
-
"[dim]Use ↑/↓ to navigate, Space to toggle, Enter to confirm, Esc to cancel[/dim]",
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
return Panel(
|
|
334
|
-
table,
|
|
335
|
-
title=f"[bold]{prompt_text}[/bold]",
|
|
336
|
-
border_style="cyan",
|
|
337
|
-
padding=(1, 2),
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
console.print()
|
|
341
|
-
|
|
342
|
-
with Live(
|
|
343
|
-
create_selection_panel(), console=console, transient=True, auto_refresh=False
|
|
344
|
-
) as live:
|
|
345
|
-
while True:
|
|
346
|
-
try:
|
|
347
|
-
key = get_key()
|
|
348
|
-
if key == "up":
|
|
349
|
-
selected_index = (selected_index - 1) % len(option_keys)
|
|
350
|
-
elif key == "down":
|
|
351
|
-
selected_index = (selected_index + 1) % len(option_keys)
|
|
352
|
-
elif key == "space":
|
|
353
|
-
current_key = option_keys[selected_index]
|
|
354
|
-
if current_key in selected_items:
|
|
355
|
-
selected_items.remove(current_key)
|
|
356
|
-
else:
|
|
357
|
-
selected_items.add(current_key)
|
|
358
|
-
elif key == "enter":
|
|
359
|
-
break
|
|
360
|
-
elif key == "escape":
|
|
361
|
-
console.print("\n[yellow]Selection cancelled[/yellow]")
|
|
362
|
-
raise typer.Exit(1)
|
|
363
|
-
|
|
364
|
-
live.update(create_selection_panel(), refresh=True)
|
|
365
|
-
|
|
366
|
-
except KeyboardInterrupt:
|
|
367
|
-
console.print("\n[yellow]Selection cancelled[/yellow]")
|
|
368
|
-
raise typer.Exit(1)
|
|
369
|
-
|
|
370
|
-
return list(selected_items)
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
class BannerGroup(TyperGroup):
|
|
374
|
-
"""Custom group that shows banner before help."""
|
|
375
|
-
|
|
376
|
-
def format_help(self, ctx, formatter):
|
|
377
|
-
# Show banner before help
|
|
378
|
-
show_banner()
|
|
379
|
-
super().format_help(ctx, formatter)
|
|
380
|
-
|
|
381
129
|
|
|
382
130
|
app = typer.Typer(
|
|
383
131
|
name="mapify",
|
|
@@ -393,21 +141,6 @@ validate_app = typer.Typer(name="validate", help="Validate task dependency graph
|
|
|
393
141
|
app.add_typer(validate_app, name="validate")
|
|
394
142
|
|
|
395
143
|
|
|
396
|
-
def show_banner():
|
|
397
|
-
"""Display the ASCII art banner."""
|
|
398
|
-
banner_lines = BANNER.strip().split("\n")
|
|
399
|
-
colors = ["bright_blue", "blue", "cyan"]
|
|
400
|
-
|
|
401
|
-
styled_banner = Text()
|
|
402
|
-
for i, line in enumerate(banner_lines):
|
|
403
|
-
color = colors[i % len(colors)]
|
|
404
|
-
styled_banner.append(line + "\n", style=color)
|
|
405
|
-
|
|
406
|
-
console.print(Align.center(styled_banner))
|
|
407
|
-
console.print(Align.center(Text(TAGLINE, style="italic bright_yellow")))
|
|
408
|
-
console.print()
|
|
409
|
-
|
|
410
|
-
|
|
411
144
|
def version_callback(value: bool):
|
|
412
145
|
"""Callback to show version and exit."""
|
|
413
146
|
if value:
|
|
@@ -452,10 +185,8 @@ def check_tool(tool: str) -> bool:
|
|
|
452
185
|
|
|
453
186
|
|
|
454
187
|
def check_mcp_server(server: str) -> bool:
|
|
455
|
-
"""Check if an MCP server is
|
|
456
|
-
|
|
457
|
-
# In a real implementation, you'd check actual MCP configuration
|
|
458
|
-
return True
|
|
188
|
+
"""Check if an MCP server is recognized by this installation."""
|
|
189
|
+
return server in build_standard_mcp_servers()
|
|
459
190
|
|
|
460
191
|
|
|
461
192
|
def is_debug_enabled(debug_flag: Optional[bool] = None) -> bool:
|
|
@@ -478,1076 +209,228 @@ def is_debug_enabled(debug_flag: Optional[bool] = None) -> bool:
|
|
|
478
209
|
|
|
479
210
|
|
|
480
211
|
def get_templates_dir() -> Path:
|
|
481
|
-
"""Get the path to bundled templates directory.
|
|
482
|
-
import importlib.resources
|
|
483
|
-
|
|
484
|
-
try:
|
|
485
|
-
# Python 3.11+ with importlib.resources.files
|
|
486
|
-
if hasattr(importlib.resources, "files"):
|
|
487
|
-
return Path(str(importlib.resources.files("mapify_cli") / "templates"))
|
|
488
|
-
except Exception:
|
|
489
|
-
pass
|
|
490
|
-
|
|
491
|
-
# Fallback to module directory
|
|
492
|
-
module_dir = Path(__file__).parent
|
|
493
|
-
templates_dir = module_dir / "templates"
|
|
494
|
-
if templates_dir.exists():
|
|
495
|
-
return templates_dir
|
|
496
|
-
|
|
497
|
-
# Development mode - check parent directories
|
|
498
|
-
for parent in [module_dir.parent, module_dir.parent.parent]:
|
|
499
|
-
templates_dir = parent / "templates"
|
|
500
|
-
if templates_dir.exists():
|
|
501
|
-
return templates_dir
|
|
502
|
-
|
|
503
|
-
raise RuntimeError("Templates directory not found. Please reinstall mapify-cli.")
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
def create_agent_files(project_path: Path, mcp_servers: List[str]) -> None:
|
|
507
|
-
"""Create MAP agent files in .claude/agents/"""
|
|
508
|
-
agents_dir = project_path / ".claude" / "agents"
|
|
509
|
-
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
510
|
-
|
|
511
|
-
# Get templates directory
|
|
512
|
-
templates_dir = get_templates_dir()
|
|
513
|
-
agents_template_dir = templates_dir / "agents"
|
|
514
|
-
|
|
515
|
-
if agents_template_dir.exists():
|
|
516
|
-
# Copy original agent files from templates (preserves template variables!)
|
|
517
|
-
import shutil
|
|
518
|
-
|
|
519
|
-
# Files to exclude from agent directory (documentation, not agents)
|
|
520
|
-
exclude_files = {"README.md", "CHANGELOG.md", "MCP-PATTERNS.md"}
|
|
521
|
-
|
|
522
|
-
for agent_template in agents_template_dir.glob("*.md"):
|
|
523
|
-
# Skip documentation files - they're not agents
|
|
524
|
-
if agent_template.name in exclude_files:
|
|
525
|
-
continue
|
|
526
|
-
dest_file = agents_dir / agent_template.name
|
|
527
|
-
shutil.copy2(agent_template, dest_file)
|
|
528
|
-
else:
|
|
529
|
-
# Fallback: generate simplified versions if templates not found
|
|
530
|
-
# NOTE: orchestrator removed (moved to slash commands in production architecture)
|
|
531
|
-
agents = {
|
|
532
|
-
"task-decomposer": create_task_decomposer_content(mcp_servers),
|
|
533
|
-
"actor": create_actor_content(mcp_servers),
|
|
534
|
-
"monitor": create_monitor_content(mcp_servers),
|
|
535
|
-
"predictor": create_predictor_content(mcp_servers),
|
|
536
|
-
"evaluator": create_evaluator_content(mcp_servers),
|
|
537
|
-
"reflector": create_reflector_content(mcp_servers),
|
|
538
|
-
"documentation-reviewer": create_documentation_reviewer_content(
|
|
539
|
-
mcp_servers
|
|
540
|
-
),
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
for name, content in agents.items():
|
|
544
|
-
agent_file = agents_dir / f"{name}.md"
|
|
545
|
-
agent_file.write_text(content)
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
def create_task_decomposer_content(mcp_servers: List[str]) -> str:
|
|
549
|
-
"""Create task-decomposer agent content"""
|
|
550
|
-
mcp_section = ""
|
|
551
|
-
if any(s in mcp_servers for s in ["sequential-thinking", "deepwiki"]):
|
|
552
|
-
mcp_section = """
|
|
553
|
-
## MCP Integration
|
|
554
|
-
|
|
555
|
-
**ALWAYS use these MCP tools:**
|
|
556
|
-
"""
|
|
557
|
-
if "sequential-thinking" in mcp_servers:
|
|
558
|
-
mcp_section += """
|
|
559
|
-
1. **mcp__sequential-thinking__sequentialthinking** - For complex planning
|
|
560
|
-
- Use when goal is ambiguous or has many dependencies
|
|
561
|
-
"""
|
|
562
|
-
if "deepwiki" in mcp_servers:
|
|
563
|
-
mcp_section += """
|
|
564
|
-
2. **mcp__deepwiki__ask_question** - Get insights from GitHub repositories
|
|
565
|
-
- Ask: "How does [repo] implement [feature]?"
|
|
566
|
-
"""
|
|
567
|
-
|
|
568
|
-
return f"""---
|
|
569
|
-
name: task-decomposer
|
|
570
|
-
description: Breaks complex goals into atomic, testable subtasks (MAP)
|
|
571
|
-
tools: Read, Grep, Glob
|
|
572
|
-
model: sonnet
|
|
573
|
-
---
|
|
574
|
-
|
|
575
|
-
# Role: Task Decomposition Specialist (MAP)
|
|
576
|
-
|
|
577
|
-
You are a software architect who turns high-level feature goals into clear, atomic, testable subtasks with explicit dependencies and acceptance criteria.
|
|
578
|
-
{mcp_section}
|
|
579
|
-
## Responsibilities
|
|
580
|
-
|
|
581
|
-
- Analyze the goal and repository context
|
|
582
|
-
- Identify prerequisites and dependencies
|
|
583
|
-
- Produce a logically ordered list of atomic subtasks
|
|
584
|
-
- Include affected files, risks, and acceptance criteria
|
|
585
|
-
|
|
586
|
-
## Output Format (JSON only)
|
|
587
|
-
|
|
588
|
-
Return a valid JSON document with subtasks, dependencies, and acceptance criteria.
|
|
589
|
-
"""
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
def create_actor_content(mcp_servers: List[str]) -> str:
|
|
593
|
-
"""Create actor agent content"""
|
|
594
|
-
mcp_section = ""
|
|
595
|
-
if "deepwiki" in mcp_servers:
|
|
596
|
-
mcp_section = """
|
|
597
|
-
# MCP INTEGRATION
|
|
598
|
-
|
|
599
|
-
**ALWAYS use these MCP tools:**
|
|
600
|
-
|
|
601
|
-
1. **mcp__deepwiki__read_wiki_contents** - Study implementation patterns
|
|
602
|
-
- Learn from production code examples
|
|
603
|
-
"""
|
|
604
|
-
|
|
605
|
-
return f"""---
|
|
606
|
-
name: actor
|
|
607
|
-
description: Generates production-ready implementation proposals (MAP)
|
|
608
|
-
tools: Read, Write, Edit, Bash, Grep, Glob
|
|
609
|
-
model: sonnet
|
|
610
|
-
---
|
|
611
|
-
|
|
612
|
-
# IDENTITY
|
|
613
|
-
|
|
614
|
-
You are a senior software engineer who writes clean, efficient, production-ready code.
|
|
615
|
-
{mcp_section}
|
|
616
|
-
# SOURCE OF TRUTH (CRITICAL FOR DOCUMENTATION)
|
|
617
|
-
|
|
618
|
-
**IF writing or updating documentation, ALWAYS find and read source documents FIRST:**
|
|
619
|
-
|
|
620
|
-
## Discovery Process
|
|
621
|
-
|
|
622
|
-
1. **Find design documents** via Glob:
|
|
623
|
-
- **/tech-design.md, **/architecture.md, **/design-doc.md, **/api-spec.md
|
|
624
|
-
- Look in: docs/, docs/private/, docs/architecture/, project root
|
|
625
|
-
- Check parent directories if in decomposition subfolder
|
|
626
|
-
|
|
627
|
-
2. **Read source BEFORE writing**:
|
|
628
|
-
- Extract API structures (spec, status fields, exact types)
|
|
629
|
-
- Extract lifecycle logic (enabled/disabled, install/uninstall triggers)
|
|
630
|
-
- Extract component responsibilities (who installs, who owns CRDs)
|
|
631
|
-
- Extract integration patterns (data flows, adapters needed)
|
|
632
|
-
|
|
633
|
-
3. **Use source as authority**:
|
|
634
|
-
- DON'T generalize from examples or DOD scenarios
|
|
635
|
-
- DON'T assume partial patterns apply globally
|
|
636
|
-
- DON'T write critical sections without verifying against source
|
|
637
|
-
- DO quote exact field names, types, logic from source
|
|
638
|
-
|
|
639
|
-
## Common Mistakes to Avoid
|
|
640
|
-
|
|
641
|
-
❌ Wrong: Using presets: [] (empty array for one engine) when source defines engines: {{}} (empty map for all engines)
|
|
642
|
-
❌ Wrong: Generalizing from DOD scenario to Uninstallation logic
|
|
643
|
-
❌ Wrong: Writing "triggers deletion" without checking what exactly gets deleted
|
|
644
|
-
|
|
645
|
-
✅ Right: Read tech-design.md → Find definitions → Use exact syntax
|
|
646
|
-
✅ Right: Check lifecycle section in source → Verify behavior → Document accurately
|
|
647
|
-
✅ Right: Look up component responsibilities → State correctly if source says so
|
|
648
|
-
|
|
649
|
-
## When Writing Documentation
|
|
650
|
-
|
|
651
|
-
- Step 1: Find source documents (Glob for **/tech-design.md, etc.)
|
|
652
|
-
- Step 2: Read source completely (don't just search for keywords)
|
|
653
|
-
- Step 3: Extract authoritative definitions (API, lifecycle, responsibilities)
|
|
654
|
-
- Step 4: Write section using source definitions
|
|
655
|
-
- Step 5: Cross-reference: Does my text match source? Line by line?
|
|
656
|
-
|
|
657
|
-
Remember: tech-design.md is source of truth, NOT DOD scenarios, NOT examples, NOT your interpretation.
|
|
658
|
-
|
|
659
|
-
# TASK
|
|
660
|
-
|
|
661
|
-
Implement the subtask with clean, testable code following project patterns.
|
|
662
|
-
|
|
663
|
-
# OUTPUT FORMAT
|
|
664
|
-
|
|
665
|
-
Provide implementation with approach, code changes, trade-offs, and testing considerations.
|
|
666
|
-
"""
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
def create_monitor_content(mcp_servers: List[str]) -> str:
|
|
670
|
-
"""Create monitor agent content"""
|
|
671
|
-
return """---
|
|
672
|
-
name: monitor
|
|
673
|
-
description: Reviews code for correctness, standards, security, and testability (MAP)
|
|
674
|
-
tools: Read, Grep, Bash, Glob
|
|
675
|
-
model: sonnet
|
|
676
|
-
---
|
|
677
|
-
|
|
678
|
-
# IDENTITY
|
|
679
|
-
|
|
680
|
-
You are a meticulous code reviewer and security expert. Your mission is to catch bugs, vulnerabilities, and violations before code reaches production.
|
|
681
|
-
|
|
682
|
-
# REVIEW CHECKLIST
|
|
683
|
-
|
|
684
|
-
Work through: Correctness, Security, Code Quality, Performance, Testability, Maintainability
|
|
685
|
-
|
|
686
|
-
## DOCUMENTATION CONSISTENCY (CRITICAL)
|
|
687
|
-
|
|
688
|
-
**When reviewing decomposition/implementation documents:**
|
|
689
|
-
|
|
690
|
-
- Find source of truth (tech-design.md, architecture.md):
|
|
691
|
-
* Use Glob: **/tech-design.md, **/architecture.md, **/design-doc.md
|
|
692
|
-
* Look in parent directories if reviewing decomposition
|
|
693
|
-
|
|
694
|
-
- Read source document FIRST
|
|
695
|
-
- Verify API consistency:
|
|
696
|
-
* All spec fields match source?
|
|
697
|
-
* All status fields match source?
|
|
698
|
-
* Field types and defaults consistent?
|
|
699
|
-
* Example: engines: {{}} vs presets: [] - different semantics!
|
|
700
|
-
|
|
701
|
-
- Verify lifecycle consistency:
|
|
702
|
-
* Does enabled: false behavior match source?
|
|
703
|
-
* Are uninstallation triggers correct?
|
|
704
|
-
* Are state transitions consistent?
|
|
705
|
-
* Check two-level patterns (e.g., enabled: false vs engines: {{}})
|
|
706
|
-
|
|
707
|
-
- Verify component responsibilities:
|
|
708
|
-
* Installation ownership matches source?
|
|
709
|
-
* CRD ownership consistent?
|
|
710
|
-
* Integration patterns same as source?
|
|
711
|
-
|
|
712
|
-
Red flags - mark as CRITICAL issue:
|
|
713
|
-
- Decomposition contradicts tech-design on lifecycle logic
|
|
714
|
-
- Missing critical spec/status fields from source
|
|
715
|
-
- Wrong component ownership
|
|
716
|
-
- Lifecycle levels confused (partial vs global state)
|
|
717
|
-
- Not using tech-design definitions (generalizing from examples instead)
|
|
718
|
-
|
|
719
|
-
# OUTPUT FORMAT (JSON)
|
|
720
|
-
|
|
721
|
-
Return strictly valid JSON with validation results and specific issues.
|
|
722
|
-
"""
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
def create_predictor_content(mcp_servers: List[str]) -> str:
|
|
726
|
-
"""Create predictor agent content"""
|
|
727
|
-
mcp_section = ""
|
|
728
|
-
if "deepwiki" in mcp_servers:
|
|
729
|
-
mcp_section = """
|
|
730
|
-
## MCP Integration
|
|
731
|
-
|
|
732
|
-
**ALWAYS use these MCP tools:**
|
|
733
|
-
|
|
734
|
-
1. **mcp__deepwiki__ask_question** - Check how repos handle similar changes
|
|
735
|
-
- Ask: "What breaks when changing [component]?"
|
|
736
|
-
"""
|
|
737
|
-
|
|
738
|
-
return f"""---
|
|
739
|
-
name: predictor
|
|
740
|
-
description: Predicts consequences and dependency impact of changes (MAP)
|
|
741
|
-
tools: Read, Grep, Glob, Bash
|
|
742
|
-
model: sonnet
|
|
743
|
-
---
|
|
744
|
-
|
|
745
|
-
# Role: Impact Analysis Specialist (MAP)
|
|
746
|
-
|
|
747
|
-
You analyze proposed changes to predict their effects across the codebase.
|
|
748
|
-
{mcp_section}
|
|
749
|
-
## Analysis Process
|
|
750
|
-
|
|
751
|
-
1. Read the proposed code changes
|
|
752
|
-
2. Identify directly modified files and APIs
|
|
753
|
-
3. Trace dependencies using Grep/Glob
|
|
754
|
-
4. Predict the resulting state and risks
|
|
755
|
-
|
|
756
|
-
## Output Format (JSON only)
|
|
757
|
-
|
|
758
|
-
Return JSON with predicted state, affected components, breaking changes, and risk assessment.
|
|
759
|
-
"""
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
def create_evaluator_content(mcp_servers: List[str]) -> str:
|
|
763
|
-
"""Create evaluator agent content"""
|
|
764
|
-
return """---
|
|
765
|
-
name: evaluator
|
|
766
|
-
description: Evaluates solution quality and completeness (MAP)
|
|
767
|
-
tools: Read, Bash, Grep
|
|
768
|
-
model: sonnet
|
|
769
|
-
---
|
|
770
|
-
|
|
771
|
-
# Role: Solution Quality Evaluator (MAP)
|
|
772
|
-
|
|
773
|
-
You provide objective scoring based on multi-dimensional quality criteria.
|
|
774
|
-
|
|
775
|
-
## Evaluation Criteria (0–10)
|
|
776
|
-
|
|
777
|
-
1. Functionality — meets requirements
|
|
778
|
-
2. Code Quality — readability, maintainability
|
|
779
|
-
3. Performance — efficiency
|
|
780
|
-
4. Security — best practices
|
|
781
|
-
5. Testability — ease of testing
|
|
782
|
-
6. Completeness — tests/docs/error handling
|
|
783
|
-
|
|
784
|
-
## Output Format (JSON only)
|
|
785
|
-
|
|
786
|
-
Return JSON with scores, strengths, weaknesses, and recommendation (proceed|improve|reconsider).
|
|
787
|
-
"""
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
def create_reflector_content(mcp_servers: List[str]) -> str:
|
|
791
|
-
"""Create reflector agent content"""
|
|
792
|
-
mcp_section = ""
|
|
793
|
-
|
|
794
|
-
return f"""---
|
|
795
|
-
name: reflector
|
|
796
|
-
description: Extracts structured lessons from execution attempts
|
|
797
|
-
tools: Read, Grep, Glob
|
|
798
|
-
model: sonnet
|
|
799
|
-
---
|
|
800
|
-
|
|
801
|
-
# IDENTITY
|
|
802
|
-
|
|
803
|
-
You are a reflection specialist who analyzes execution attempts to extract structured, actionable lessons learned.
|
|
804
|
-
{mcp_section}
|
|
805
|
-
# ROLE
|
|
806
|
-
|
|
807
|
-
Analyze Actor implementations and Monitor feedback to identify:
|
|
808
|
-
- What worked well (success patterns)
|
|
809
|
-
- What failed and why (failure patterns)
|
|
810
|
-
- Reusable insights for future implementations
|
|
811
|
-
- Anti-patterns to avoid
|
|
812
|
-
|
|
813
|
-
## Output Format (JSON)
|
|
814
|
-
|
|
815
|
-
Return JSON with:
|
|
816
|
-
- key_insight: Main lesson learned
|
|
817
|
-
- success_patterns: What worked well
|
|
818
|
-
- failure_patterns: What went wrong
|
|
819
|
-
- suggested_new_patterns: Pattern entries to add
|
|
820
|
-
- confidence: How reliable this insight is
|
|
821
|
-
"""
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
# Note: test-generator agent removed
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
def create_documentation_reviewer_content(mcp_servers: List[str]) -> str:
|
|
828
|
-
"""Create documentation-reviewer agent content"""
|
|
829
|
-
mcp_section = ""
|
|
830
|
-
if "deepwiki" in mcp_servers:
|
|
831
|
-
mcp_section = """
|
|
832
|
-
# MCP INTEGRATION
|
|
833
|
-
|
|
834
|
-
**ALWAYS use these tools for documentation review:**
|
|
835
|
-
|
|
836
|
-
1. **mcp__deepwiki__ask_question** - Compare with similar projects
|
|
837
|
-
- Ask: "How do other projects handle [integration]?"
|
|
838
|
-
- Learn from successful implementations
|
|
839
|
-
"""
|
|
840
|
-
|
|
841
|
-
return f"""---
|
|
842
|
-
name: documentation-reviewer
|
|
843
|
-
description: Reviews technical documentation for completeness, external dependencies, and architectural consistency
|
|
844
|
-
tools: Read, Grep, Glob, Fetch
|
|
845
|
-
model: sonnet
|
|
846
|
-
---
|
|
847
|
-
|
|
848
|
-
# IDENTITY
|
|
849
|
-
|
|
850
|
-
You are a technical documentation expert specialized in architecture reviews and dependency analysis.
|
|
851
|
-
{mcp_section}
|
|
852
|
-
# REVIEW CHECKLIST
|
|
853
|
-
|
|
854
|
-
## 1. EXTERNAL DEPENDENCIES SCAN
|
|
855
|
-
- Extract all URLs via pattern matching
|
|
856
|
-
- Use Fetch tool (10s timeout) to verify each URL
|
|
857
|
-
- Check for CRDs, Helm charts, installation instructions
|
|
858
|
-
- Determine installation responsibility
|
|
859
|
-
- Verify documentation completeness
|
|
860
|
-
|
|
861
|
-
## 2. CRD DETECTION LOGIC
|
|
862
|
-
Look for:
|
|
863
|
-
- YAML with apiVersion: apiextensions.k8s.io/v1
|
|
864
|
-
- kind: CustomResourceDefinition
|
|
865
|
-
- Mentions of "custom resource"
|
|
866
|
-
- Controller/operator projects
|
|
867
|
-
|
|
868
|
-
## 3. CONSISTENCY WITH SOURCE OF TRUTH (CRITICAL)
|
|
869
|
-
|
|
870
|
-
**ALWAYS verify decomposition documents against tech-design/architecture:**
|
|
871
|
-
|
|
872
|
-
### Source of Truth Discovery
|
|
873
|
-
- Find source documents via Glob: **/tech-design.md, **/architecture.md, **/design-doc.md
|
|
874
|
-
- Look in parent directories: docs/, docs/private/, project root
|
|
875
|
-
- Read source documents FIRST before reviewing decomposition
|
|
876
|
-
- Extract key concepts: API structures, lifecycle states, component responsibilities, integration patterns
|
|
877
|
-
|
|
878
|
-
### Consistency Validation
|
|
879
|
-
For each section in target document, verify against source:
|
|
880
|
-
- API fields match exactly (all spec and status fields present, types consistent)
|
|
881
|
-
* Example: engines: {{}} (empty map) vs engines.kyverno.presets: [] (empty array) - different semantics!
|
|
882
|
-
- Lifecycle logic matches (installation/uninstallation triggers same as in source)
|
|
883
|
-
* Check: Does enabled: false delete all? Does engines: {{}} delete ClusterPolicySet only?
|
|
884
|
-
- Component responsibilities match (who installs what, who owns CRDs, who triggers actions)
|
|
885
|
-
- Integration patterns match (data flow direction, adapter requirements, API versions)
|
|
886
|
-
|
|
887
|
-
### Red Flags (Auto-fail if found)
|
|
888
|
-
❌ Critical inconsistencies:
|
|
889
|
-
- Target document contradicts source on lifecycle logic
|
|
890
|
-
- Missing critical spec/status fields from source
|
|
891
|
-
- Wrong component ownership (e.g., "User installs" when source says "Component Manager installs")
|
|
892
|
-
- Lifecycle levels confused (e.g., using presets: [] when should be engines: {{}})
|
|
893
|
-
|
|
894
|
-
❌ Common mistakes to catch:
|
|
895
|
-
- Generalizing from DOD scenarios instead of using tech-design definitions
|
|
896
|
-
- Mixing partial state (presets: [] for one engine) with global state (engines: {{}} for all)
|
|
897
|
-
- Missing "two-level" patterns (e.g., enabled: false vs engines: {{}})
|
|
898
|
-
- Not reading tech-design before writing critical sections
|
|
899
|
-
|
|
900
|
-
## OUTPUT FORMAT (JSON)
|
|
901
|
-
|
|
902
|
-
Return strictly valid JSON with:
|
|
903
|
-
- valid: boolean
|
|
904
|
-
- summary: string
|
|
905
|
-
- external_dependencies_checked: array
|
|
906
|
-
- missing_requirements: array
|
|
907
|
-
- consistency_check: object with source_document, sections_verified, overall_consistency
|
|
908
|
-
- score: number (0-10)
|
|
909
|
-
- recommendation: "proceed|improve|reconsider"
|
|
910
|
-
|
|
911
|
-
# DECISION RULES
|
|
912
|
-
|
|
913
|
-
Return valid=false if:
|
|
914
|
-
- Any critical issues found
|
|
915
|
-
- External dependencies cannot be verified and are critical
|
|
916
|
-
- CRD installation completely undefined
|
|
917
|
-
- **Consistency check fails** (overall_consistency: "inconsistent")
|
|
918
|
-
- **Source document not read** before reviewing decomposition
|
|
919
|
-
- **Critical lifecycle logic mismatch** with source
|
|
920
|
-
|
|
921
|
-
# CONSTRAINTS
|
|
922
|
-
|
|
923
|
-
- Be PROACTIVE: Fetch EVERY external URL (with timeout protection)
|
|
924
|
-
- Handle errors gracefully: Don't fail on transient network issues
|
|
925
|
-
- Security conscious: Validate URLs (no private IPs, localhost)
|
|
926
|
-
- Performance aware: Cache results, parallel fetch up to 5 URLs
|
|
927
|
-
- Output strictly JSON
|
|
928
|
-
"""
|
|
929
|
-
|
|
212
|
+
"""Get the path to bundled templates directory.
|
|
930
213
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
Returns:
|
|
935
|
-
Number of reference files installed
|
|
214
|
+
Delegates to :func:`mapify_cli.delivery.file_copier.get_templates_dir`
|
|
215
|
+
to avoid duplication.
|
|
936
216
|
"""
|
|
937
|
-
|
|
938
|
-
references_dir.mkdir(parents=True, exist_ok=True)
|
|
939
|
-
|
|
940
|
-
# Get templates directory
|
|
941
|
-
templates_dir = get_templates_dir()
|
|
942
|
-
references_template_dir = templates_dir / "references"
|
|
943
|
-
|
|
944
|
-
count = 0
|
|
945
|
-
if references_template_dir.exists():
|
|
946
|
-
import shutil
|
|
947
|
-
|
|
948
|
-
for ref_file in references_template_dir.glob("*.md"):
|
|
949
|
-
dest_file = references_dir / ref_file.name
|
|
950
|
-
shutil.copy2(ref_file, dest_file)
|
|
951
|
-
count += 1
|
|
952
|
-
|
|
953
|
-
return count
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
def create_command_files(project_path: Path) -> None:
|
|
957
|
-
"""Create MAP slash commands in .claude/commands/"""
|
|
958
|
-
commands_dir = project_path / ".claude" / "commands"
|
|
959
|
-
commands_dir.mkdir(parents=True, exist_ok=True)
|
|
960
|
-
|
|
961
|
-
# Get templates directory
|
|
962
|
-
templates_dir = get_templates_dir()
|
|
963
|
-
commands_template_dir = templates_dir / "commands"
|
|
217
|
+
from mapify_cli.delivery.file_copier import get_templates_dir as _get
|
|
964
218
|
|
|
965
|
-
|
|
966
|
-
# Fallback to inline generation if templates not found
|
|
967
|
-
commands = {
|
|
968
|
-
"map-efficient": """---
|
|
969
|
-
description: Implement features with optimized workflow (recommended)
|
|
970
|
-
---
|
|
219
|
+
return _get()
|
|
971
220
|
|
|
972
|
-
Implement the following with efficient MAP workflow:
|
|
973
221
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
""
|
|
980
|
-
"map-debug": """---
|
|
981
|
-
description: Debug issue using MAP analysis
|
|
982
|
-
---
|
|
983
|
-
|
|
984
|
-
Debug the following issue using MAP workflow:
|
|
985
|
-
|
|
986
|
-
$ARGUMENTS
|
|
987
|
-
|
|
988
|
-
Decompose the debugging process (task-decomposer), implement fixes (actor), validate with monitor, and assess impact (predictor).
|
|
989
|
-
""",
|
|
990
|
-
"map-fast": """---
|
|
991
|
-
description: Quick implementation with minimal validation
|
|
992
|
-
---
|
|
993
|
-
|
|
994
|
-
Use minimal workflow to implement:
|
|
995
|
-
|
|
996
|
-
$ARGUMENTS
|
|
997
|
-
|
|
998
|
-
Implement quickly with basic monitor validation only. No learning, no predictor.
|
|
999
|
-
Use for small, low-risk changes where speed matters.
|
|
1000
|
-
""",
|
|
1001
|
-
"map-learn": """---
|
|
1002
|
-
description: Extract lessons from completed workflows
|
|
1003
|
-
---
|
|
1004
|
-
|
|
1005
|
-
Extract and preserve lessons from recent workflow:
|
|
1006
|
-
|
|
1007
|
-
$ARGUMENTS
|
|
1008
|
-
|
|
1009
|
-
Call Reflector to extract patterns from recent workflow.
|
|
1010
|
-
""",
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
for name, content in commands.items():
|
|
1014
|
-
command_file = commands_dir / f"{name}.md"
|
|
1015
|
-
command_file.write_text(content)
|
|
1016
|
-
else:
|
|
1017
|
-
# Copy templates from bundled directory
|
|
1018
|
-
import shutil
|
|
1019
|
-
|
|
1020
|
-
for command_template in commands_template_dir.glob("*.md"):
|
|
1021
|
-
dest_file = commands_dir / command_template.name
|
|
1022
|
-
shutil.copy2(command_template, dest_file)
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
def create_skill_files(project_path: Path) -> int:
|
|
1026
|
-
"""Create MAP skills in .claude/skills/
|
|
1027
|
-
|
|
1028
|
-
Returns:
|
|
1029
|
-
Number of skills installed
|
|
1030
|
-
"""
|
|
1031
|
-
skills_dir = project_path / ".claude" / "skills"
|
|
1032
|
-
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
1033
|
-
|
|
1034
|
-
# Get templates directory
|
|
1035
|
-
templates_dir = get_templates_dir()
|
|
1036
|
-
skills_template_dir = templates_dir / "skills"
|
|
222
|
+
def count_template_markdown_files(template_subdir: str) -> int:
|
|
223
|
+
"""Count shipped markdown templates in a subdirectory."""
|
|
224
|
+
template_dir = get_templates_dir() / template_subdir
|
|
225
|
+
if not template_dir.exists():
|
|
226
|
+
return 0
|
|
227
|
+
return len([path for path in template_dir.glob("*.md") if path.is_file()])
|
|
1037
228
|
|
|
1038
|
-
count = 0
|
|
1039
229
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
230
|
+
def count_agent_templates() -> int:
|
|
231
|
+
"""Count shipped agent templates, excluding documentation files."""
|
|
232
|
+
template_dir = get_templates_dir() / "agents"
|
|
233
|
+
if not template_dir.exists():
|
|
234
|
+
return 0
|
|
1044
235
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
)
|
|
236
|
+
exclude_files = {"README.md", "CHANGELOG.md", "MCP-PATTERNS.md"}
|
|
237
|
+
return len(
|
|
238
|
+
[
|
|
239
|
+
path
|
|
240
|
+
for path in template_dir.glob("*.md")
|
|
241
|
+
if path.is_file() and path.name not in exclude_files
|
|
242
|
+
]
|
|
243
|
+
)
|
|
1050
244
|
|
|
1051
|
-
# Copy each skill directory
|
|
1052
|
-
for skill_template in skills_template_dir.iterdir():
|
|
1053
|
-
if skill_template.is_dir() and skill_template.name != "__pycache__":
|
|
1054
|
-
target = skills_dir / skill_template.name
|
|
1055
|
-
shutil.copytree(skill_template, target, dirs_exist_ok=True)
|
|
1056
|
-
count += 1
|
|
1057
245
|
|
|
1058
|
-
|
|
246
|
+
def count_command_templates() -> int:
|
|
247
|
+
"""Count shipped slash command templates."""
|
|
248
|
+
return count_template_markdown_files("commands")
|
|
1059
249
|
|
|
1060
250
|
|
|
1061
|
-
def
|
|
1062
|
-
|
|
251
|
+
def count_project_markdown_files(
|
|
252
|
+
directory: Path, exclude_files: Optional[set[str]] = None
|
|
1063
253
|
) -> int:
|
|
1064
|
-
"""
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
src = map_template_dir / subdir
|
|
1068
|
-
if not src.exists():
|
|
254
|
+
"""Count markdown files in a project directory."""
|
|
255
|
+
if not directory.exists():
|
|
1069
256
|
return 0
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
print(
|
|
1079
|
-
f"Warning: Could not remove existing {dest}: {e}",
|
|
1080
|
-
file=sys.stderr,
|
|
1081
|
-
)
|
|
1082
|
-
shutil.copytree(src, dest, dirs_exist_ok=True)
|
|
1083
|
-
|
|
1084
|
-
count = 0
|
|
1085
|
-
for script in dest.rglob(executable_glob):
|
|
1086
|
-
script.chmod(script.stat().st_mode | 0o755)
|
|
1087
|
-
count += 1
|
|
1088
|
-
return count
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
def create_map_tools(project_path: Path) -> int:
|
|
1092
|
-
"""Create .map/ directory with static analysis tools and orchestrator scripts."""
|
|
1093
|
-
map_dir = project_path / ".map"
|
|
1094
|
-
map_dir.mkdir(parents=True, exist_ok=True)
|
|
1095
|
-
|
|
1096
|
-
templates_dir = get_templates_dir()
|
|
1097
|
-
map_template_dir = templates_dir / "map"
|
|
1098
|
-
|
|
1099
|
-
count = 0
|
|
1100
|
-
if map_template_dir.exists():
|
|
1101
|
-
count += _copy_map_subdir(map_template_dir, map_dir, "static-analysis", "*.sh")
|
|
1102
|
-
count += _copy_map_subdir(map_template_dir, map_dir, "scripts", "*.py")
|
|
1103
|
-
|
|
1104
|
-
return count
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
def configure_global_permissions() -> None:
|
|
1108
|
-
"""Configure global Claude Code permissions for read-only commands"""
|
|
1109
|
-
claude_dir = Path.home() / ".claude"
|
|
1110
|
-
settings_file = claude_dir / "settings.json"
|
|
1111
|
-
|
|
1112
|
-
# Create .claude directory if it doesn't exist
|
|
1113
|
-
claude_dir.mkdir(exist_ok=True)
|
|
1114
|
-
|
|
1115
|
-
# Default permissions for read-only commands
|
|
1116
|
-
default_permissions = {
|
|
1117
|
-
"allow": [
|
|
1118
|
-
"Bash(git status *)",
|
|
1119
|
-
"Bash(git log *)",
|
|
1120
|
-
"Bash(git diff *)",
|
|
1121
|
-
"Bash(git show *)",
|
|
1122
|
-
"Bash(git check-ignore *)",
|
|
1123
|
-
"Bash(git branch --show-current *)",
|
|
1124
|
-
"Bash(git branch -a *)",
|
|
1125
|
-
"Bash(git rev-parse *)",
|
|
1126
|
-
"Bash(git ls-files *)",
|
|
1127
|
-
"Bash(ls *)",
|
|
1128
|
-
"Bash(cat *)",
|
|
1129
|
-
"Bash(head *)",
|
|
1130
|
-
"Bash(tail *)",
|
|
1131
|
-
"Bash(wc *)",
|
|
1132
|
-
"Bash(grep *)",
|
|
1133
|
-
"Bash(find *)",
|
|
1134
|
-
"Bash(sort *)",
|
|
1135
|
-
"Bash(uniq *)",
|
|
1136
|
-
"Bash(jq *)",
|
|
1137
|
-
"Bash(which *)",
|
|
1138
|
-
"Bash(echo *)",
|
|
1139
|
-
"Bash(pwd *)",
|
|
1140
|
-
"Bash(whoami *)",
|
|
1141
|
-
"Bash(ruby -c *)",
|
|
1142
|
-
"Bash(go fmt /tmp/ *)",
|
|
1143
|
-
"Bash(gofmt -l *)",
|
|
1144
|
-
"Bash(gofmt -d *)",
|
|
1145
|
-
"Bash(go vet *)",
|
|
1146
|
-
"Bash(go build *)",
|
|
1147
|
-
"Bash(go test -c *)",
|
|
1148
|
-
"Bash(go mod download *)",
|
|
1149
|
-
"Bash(go mod tidy *)",
|
|
1150
|
-
"Bash(chmod +x *)",
|
|
1151
|
-
"Read(//Users/**)",
|
|
1152
|
-
"Read(//private/tmp/**)",
|
|
1153
|
-
"Glob(**)",
|
|
1154
|
-
],
|
|
1155
|
-
"deny": [],
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
# Read existing settings or create new
|
|
1159
|
-
if settings_file.exists():
|
|
1160
|
-
try:
|
|
1161
|
-
with open(settings_file, "r") as f:
|
|
1162
|
-
settings = json.load(f)
|
|
1163
|
-
except json.JSONDecodeError:
|
|
1164
|
-
console.print(
|
|
1165
|
-
"[yellow]Warning:[/yellow] Corrupted settings.json, will recreate"
|
|
1166
|
-
)
|
|
1167
|
-
settings = {}
|
|
1168
|
-
else:
|
|
1169
|
-
settings = {}
|
|
1170
|
-
|
|
1171
|
-
# Merge permissions (preserve user's custom permissions)
|
|
1172
|
-
if "permissions" not in settings:
|
|
1173
|
-
settings["permissions"] = default_permissions
|
|
1174
|
-
else:
|
|
1175
|
-
# Add new permissions if they don't exist
|
|
1176
|
-
existing_allow = set(settings["permissions"].get("allow", []))
|
|
1177
|
-
for perm in default_permissions["allow"]:
|
|
1178
|
-
if perm not in existing_allow:
|
|
1179
|
-
settings["permissions"].setdefault("allow", []).append(perm)
|
|
1180
|
-
|
|
1181
|
-
# Write back
|
|
1182
|
-
with open(settings_file, "w") as f:
|
|
1183
|
-
json.dump(settings, f, indent=2)
|
|
1184
|
-
|
|
1185
|
-
console.print(f"[green]✓[/green] Configured global permissions in {settings_file}")
|
|
1186
|
-
console.print(
|
|
1187
|
-
f"[dim] Added {len(default_permissions['allow'])} read-only command patterns[/dim]"
|
|
257
|
+
exclude_files = exclude_files or set()
|
|
258
|
+
return len(
|
|
259
|
+
[
|
|
260
|
+
path
|
|
261
|
+
for path in directory.glob("*.md")
|
|
262
|
+
if path.is_file() and path.name not in exclude_files
|
|
263
|
+
]
|
|
1188
264
|
)
|
|
1189
265
|
|
|
1190
266
|
|
|
1191
|
-
def
|
|
1192
|
-
"""
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
"
|
|
1212
|
-
|
|
1213
|
-
"mcp__sourcecraft__list_pull_request_comments",
|
|
1214
|
-
# Common safe Go workflows (project-scoped)
|
|
1215
|
-
"Bash(go test *)",
|
|
1216
|
-
"Bash(go test -c *)",
|
|
1217
|
-
"Bash(go vet *)",
|
|
1218
|
-
"Bash(go build *)",
|
|
1219
|
-
"Bash(go mod download *)",
|
|
1220
|
-
"Bash(go mod tidy *)",
|
|
1221
|
-
"Bash(gofmt -l *)",
|
|
1222
|
-
"Bash(gofmt -d *)",
|
|
1223
|
-
# Common safe Make targets
|
|
1224
|
-
"Bash(make generate manifests)",
|
|
1225
|
-
"Bash(make manifests)",
|
|
1226
|
-
# Common git workflows
|
|
1227
|
-
"Bash(git worktree add *)",
|
|
1228
|
-
# Used by some test/dev scripts to produce temporary dev certs
|
|
1229
|
-
'Bash(openssl req -x509 -newkey rsa:512 -keyout /dev/null -out /dev/stdout -days 365 -nodes -subj "/CN=test" 2>/dev/null)',
|
|
1230
|
-
],
|
|
1231
|
-
"deny": [],
|
|
1232
|
-
"ask": [],
|
|
267
|
+
def is_map_initialized(project_path: Path) -> bool:
|
|
268
|
+
"""Return True when the current directory looks like a MAP project."""
|
|
269
|
+
required_paths = [
|
|
270
|
+
project_path / ".claude" / "agents",
|
|
271
|
+
project_path / ".claude" / "commands",
|
|
272
|
+
project_path / ".claude" / "settings.json",
|
|
273
|
+
project_path / ".claude" / "workflow-rules.json",
|
|
274
|
+
]
|
|
275
|
+
return all(path.exists() for path in required_paths)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def get_project_health(project_path: Path) -> Dict[str, Any]:
|
|
279
|
+
"""Collect project health diagnostics for check/doctor commands."""
|
|
280
|
+
agent_exclude = {"README.md", "CHANGELOG.md", "MCP-PATTERNS.md"}
|
|
281
|
+
current_branch = sanitize_identifier(get_current_branch_name())
|
|
282
|
+
branch_dir = project_path / ".map" / current_branch
|
|
283
|
+
required_paths = {
|
|
284
|
+
".claude/agents": project_path / ".claude" / "agents",
|
|
285
|
+
".claude/commands": project_path / ".claude" / "commands",
|
|
286
|
+
".claude/settings.json": project_path / ".claude" / "settings.json",
|
|
287
|
+
".claude/workflow-rules.json": project_path / ".claude" / "workflow-rules.json",
|
|
288
|
+
".map/scripts": project_path / ".map" / "scripts",
|
|
1233
289
|
}
|
|
290
|
+
missing_paths = [name for name, path in required_paths.items() if not path.exists()]
|
|
1234
291
|
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
existing_settings = {}
|
|
1246
|
-
|
|
1247
|
-
if isinstance(existing_settings, dict) and existing_settings.get("hooks"):
|
|
1248
|
-
console.print(
|
|
1249
|
-
"[yellow]Warning:[/yellow] .claude/settings.local.json contains hooks. "
|
|
1250
|
-
"Claude Code loads hooks from BOTH .claude/settings.json and .claude/settings.local.json, "
|
|
1251
|
-
"so this can cause duplicate hook executions. "
|
|
1252
|
-
"Move shared hooks to .claude/settings.json and remove the hooks section from settings.local.json."
|
|
1253
|
-
)
|
|
292
|
+
agents_dir = project_path / ".claude" / "agents"
|
|
293
|
+
commands_dir = project_path / ".claude" / "commands"
|
|
294
|
+
mcp_json_path = project_path / ".mcp.json"
|
|
295
|
+
internal_mcp_path = project_path / ".claude" / "mcp_config.json"
|
|
296
|
+
branch_artifact_files = [
|
|
297
|
+
"qa-001.md",
|
|
298
|
+
"verification-summary.md",
|
|
299
|
+
"pr-draft.md",
|
|
300
|
+
]
|
|
301
|
+
numbered_artifact_prefixes = ["plan-review", "code-review"]
|
|
1254
302
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
# Merge allowlist (preserve user customizations)
|
|
1259
|
-
existing_allow = set(permissions.get("allow", []))
|
|
1260
|
-
for entry in default_permissions["allow"]:
|
|
1261
|
-
if entry not in existing_allow:
|
|
1262
|
-
permissions.setdefault("allow", []).append(entry)
|
|
1263
|
-
|
|
1264
|
-
permissions.setdefault("deny", permissions.get("deny", []))
|
|
1265
|
-
permissions.setdefault("ask", permissions.get("ask", []))
|
|
1266
|
-
|
|
1267
|
-
settings_file.write_text(json.dumps(existing_settings, indent=2) + "\n")
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
def create_mcp_config(project_path: Path, mcp_servers: List[str]) -> None:
|
|
1271
|
-
"""Create MCP configuration file"""
|
|
1272
|
-
config: Dict[str, Any] = {
|
|
1273
|
-
"mcp_servers": {},
|
|
1274
|
-
"agent_mcp_mappings": {
|
|
1275
|
-
"task-decomposer": [],
|
|
1276
|
-
"actor": [],
|
|
1277
|
-
"monitor": [],
|
|
1278
|
-
"predictor": [],
|
|
1279
|
-
"evaluator": [],
|
|
1280
|
-
"reflector": [],
|
|
1281
|
-
"documentation-reviewer": [],
|
|
1282
|
-
"debate-arbiter": [],
|
|
1283
|
-
"synthesizer": [],
|
|
1284
|
-
"research-agent": [],
|
|
1285
|
-
"final-verifier": [],
|
|
1286
|
-
},
|
|
1287
|
-
"workflow_settings": {
|
|
1288
|
-
"always_retrieve_knowledge": True,
|
|
1289
|
-
"store_successful_patterns": True,
|
|
1290
|
-
"use_professional_review": True,
|
|
1291
|
-
"enable_sequential_thinking": True,
|
|
1292
|
-
"knowledge_cache_ttl": 3600,
|
|
1293
|
-
},
|
|
1294
|
-
}
|
|
303
|
+
mcp_json_ok = False
|
|
304
|
+
if mcp_json_path.exists():
|
|
305
|
+
mcp_json_ok = read_project_mcp_json(mcp_json_path) is not None
|
|
1295
306
|
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
"
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
"
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
307
|
+
return {
|
|
308
|
+
"initialized": is_map_initialized(project_path),
|
|
309
|
+
"missing_paths": missing_paths,
|
|
310
|
+
"installed_agents": count_project_markdown_files(agents_dir, agent_exclude),
|
|
311
|
+
"installed_commands": count_project_markdown_files(commands_dir),
|
|
312
|
+
"expected_agents": count_agent_templates(),
|
|
313
|
+
"expected_commands": count_command_templates(),
|
|
314
|
+
"has_project_mcp": mcp_json_path.exists(),
|
|
315
|
+
"project_mcp_valid": mcp_json_ok,
|
|
316
|
+
"has_internal_mcp": internal_mcp_path.exists(),
|
|
317
|
+
"current_branch": current_branch,
|
|
318
|
+
"branch_workspace_exists": branch_dir.exists(),
|
|
319
|
+
"branch_workspace_files": (
|
|
320
|
+
sorted(path.name for path in branch_dir.iterdir() if path.is_file())
|
|
321
|
+
if branch_dir.exists()
|
|
322
|
+
else []
|
|
323
|
+
),
|
|
324
|
+
"branch_artifact_files": branch_artifact_files,
|
|
325
|
+
"numbered_artifact_prefixes": numbered_artifact_prefixes,
|
|
326
|
+
"expected_branch_artifact_count": len(branch_artifact_files)
|
|
327
|
+
+ len(numbered_artifact_prefixes),
|
|
328
|
+
"branch_artifact_count": (
|
|
329
|
+
len(
|
|
330
|
+
[name for name in branch_artifact_files if (branch_dir / name).exists()]
|
|
331
|
+
)
|
|
332
|
+
+ sum(
|
|
333
|
+
1
|
|
334
|
+
for prefix in numbered_artifact_prefixes
|
|
335
|
+
if any(branch_dir.glob(f"{prefix}-*.md"))
|
|
336
|
+
)
|
|
337
|
+
if branch_dir.exists()
|
|
338
|
+
else 0
|
|
339
|
+
),
|
|
1312
340
|
}
|
|
1313
341
|
|
|
1314
|
-
# Add selected servers
|
|
1315
|
-
for server in mcp_servers:
|
|
1316
|
-
if server in server_configs:
|
|
1317
|
-
config["mcp_servers"][server] = server_configs[server]
|
|
1318
342
|
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
config["agent_mcp_mappings"][agent].append("sequential-thinking")
|
|
343
|
+
def parse_version(version: str) -> tuple[int, ...]:
|
|
344
|
+
"""Parse a semantic-ish version string into an integer tuple."""
|
|
345
|
+
cleaned = version.strip().lstrip("v")
|
|
346
|
+
parts = []
|
|
347
|
+
for chunk in cleaned.split("."):
|
|
348
|
+
digits = "".join(ch for ch in chunk if ch.isdigit())
|
|
349
|
+
if not digits:
|
|
350
|
+
break
|
|
351
|
+
parts.append(int(digits))
|
|
352
|
+
return tuple(parts)
|
|
1330
353
|
|
|
1331
|
-
if "deepwiki" in mcp_servers:
|
|
1332
|
-
for agent in config["agent_mcp_mappings"]:
|
|
1333
|
-
config["agent_mcp_mappings"][agent].append("deepwiki")
|
|
1334
354
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
355
|
+
def sanitize_identifier(value: str, fallback: str = "main") -> str:
|
|
356
|
+
"""Sanitize a user or branch supplied identifier for filesystem use."""
|
|
357
|
+
sanitized = value.strip().replace("/", "-")
|
|
358
|
+
sanitized = "".join(ch if ch.isalnum() or ch in "._-" else "-" for ch in sanitized)
|
|
359
|
+
while "--" in sanitized:
|
|
360
|
+
sanitized = sanitized.replace("--", "-")
|
|
361
|
+
sanitized = sanitized.strip("-.")
|
|
362
|
+
return sanitized or fallback
|
|
1339
363
|
|
|
1340
364
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
365
|
+
def get_current_branch_name() -> str:
|
|
366
|
+
"""Return current git branch name, or 'main' when unavailable."""
|
|
367
|
+
try:
|
|
368
|
+
result = subprocess.run(
|
|
369
|
+
["git", "branch", "--show-current"],
|
|
370
|
+
check=True,
|
|
371
|
+
capture_output=True,
|
|
372
|
+
text=True,
|
|
373
|
+
cwd=Path.cwd(),
|
|
374
|
+
)
|
|
375
|
+
branch = result.stdout.strip()
|
|
376
|
+
return branch or "main"
|
|
377
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
378
|
+
return "main"
|
|
1344
379
|
|
|
1345
380
|
|
|
1346
|
-
def
|
|
1347
|
-
"""
|
|
381
|
+
def get_branch_workspace_dir(project_path: Path, branch: Optional[str] = None) -> Path:
|
|
382
|
+
"""Return the branch-scoped MAP workspace directory."""
|
|
383
|
+
branch_name = sanitize_identifier(branch or get_current_branch_name())
|
|
384
|
+
return project_path / ".map" / branch_name
|
|
1348
385
|
|
|
1349
|
-
Returns dict mapping server names to their Claude Code MCP configurations.
|
|
1350
|
-
Uses verified configurations from production installations.
|
|
1351
386
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
"""
|
|
387
|
+
def get_branch_artifact_templates() -> Dict[str, str]:
|
|
388
|
+
"""Return artifact templates aligned to MAP branch workspaces."""
|
|
1355
389
|
return {
|
|
1356
|
-
"
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
},
|
|
1360
|
-
"deepwiki": {
|
|
1361
|
-
"type": "http",
|
|
1362
|
-
"url": "https://mcp.deepwiki.com/mcp",
|
|
1363
|
-
},
|
|
390
|
+
"code-review-001.md": "# Code Review 001\n\n## Scope\n\n## Findings\n\n### High\n\n### Medium\n\n### Low\n\n## Verdict\n- [ ] Ready\n- [ ] Needs revision\n",
|
|
391
|
+
"qa-001.md": "# QA 001\n\n## Commands Run\n\n## Expected Result\n\n## Actual Result\n\n## Follow-ups\n",
|
|
392
|
+
"pr-draft.md": "# PR Draft\n\n## Summary\n\n## Validation\n\n## Risks / Rollback\n",
|
|
1364
393
|
}
|
|
1365
394
|
|
|
1366
395
|
|
|
1367
|
-
def
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
Parsed JSON dict if file exists and is valid, None otherwise
|
|
1375
|
-
|
|
1376
|
-
Handles:
|
|
1377
|
-
- File not found (returns None)
|
|
1378
|
-
- Invalid JSON (logs warning, creates backup, returns None)
|
|
1379
|
-
- Permission errors (logs warning, returns None)
|
|
1380
|
-
"""
|
|
1381
|
-
if not path.exists():
|
|
1382
|
-
return None
|
|
1383
|
-
|
|
1384
|
-
try:
|
|
1385
|
-
content = path.read_text(encoding="utf-8")
|
|
1386
|
-
return json.loads(content)
|
|
1387
|
-
except json.JSONDecodeError as e:
|
|
1388
|
-
console.print(f"[yellow]Warning:[/yellow] Invalid JSON in {path.name}: {e}")
|
|
1389
|
-
# Create backup with timestamp + UUID to prevent race conditions
|
|
1390
|
-
# UUID ensures unique names even with concurrent processes
|
|
1391
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
1392
|
-
unique_id = uuid.uuid4().hex[:8]
|
|
1393
|
-
backup_path = path.with_suffix(f".backup.{timestamp}_{unique_id}.json")
|
|
1394
|
-
try:
|
|
1395
|
-
if path.exists(): # Check before rename to handle concurrent processes
|
|
1396
|
-
path.rename(backup_path)
|
|
1397
|
-
console.print(
|
|
1398
|
-
f"[dim]Backed up corrupted file to {backup_path.name}[/dim]"
|
|
1399
|
-
)
|
|
1400
|
-
else:
|
|
1401
|
-
console.print(
|
|
1402
|
-
"[dim]Corrupted file already removed by another process[/dim]"
|
|
1403
|
-
)
|
|
1404
|
-
except OSError as backup_error:
|
|
1405
|
-
console.print(
|
|
1406
|
-
f"[yellow]Warning:[/yellow] Could not create backup: {backup_error}"
|
|
1407
|
-
)
|
|
1408
|
-
return None
|
|
1409
|
-
except (OSError, PermissionError) as e:
|
|
1410
|
-
console.print(f"[yellow]Warning:[/yellow] Cannot read {path.name}: {e}")
|
|
1411
|
-
return None
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
def write_project_mcp_json(path: Path, config: Dict[str, Any]) -> None:
|
|
1415
|
-
"""Write .mcp.json to project root with proper formatting.
|
|
396
|
+
def initialize_branch_workspace(
|
|
397
|
+
project_path: Path, branch: Optional[str] = None
|
|
398
|
+
) -> Path:
|
|
399
|
+
"""Create branch-scoped planning artifacts inside `.map/<branch>/`."""
|
|
400
|
+
branch_name = sanitize_identifier(branch or get_current_branch_name())
|
|
401
|
+
workspace_dir = get_branch_workspace_dir(project_path, branch_name)
|
|
402
|
+
workspace_dir.mkdir(parents=True, exist_ok=True)
|
|
1416
403
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
404
|
+
for file_name, content in get_branch_artifact_templates().items():
|
|
405
|
+
destination = workspace_dir / file_name
|
|
406
|
+
if not destination.exists():
|
|
407
|
+
destination.write_text(content, encoding="utf-8")
|
|
1420
408
|
|
|
1421
|
-
|
|
1422
|
-
OSError: If write fails (permission, disk space, etc.)
|
|
409
|
+
return workspace_dir
|
|
1423
410
|
|
|
1424
|
-
Format:
|
|
1425
|
-
- indent=2 for readability
|
|
1426
|
-
- UTF-8 encoding
|
|
1427
|
-
- Newline at end of file
|
|
1428
|
-
"""
|
|
1429
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1430
|
-
content = json.dumps(config, indent=2, ensure_ascii=False)
|
|
1431
|
-
path.write_text(content + "\n", encoding="utf-8")
|
|
1432
411
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
existing: Dict[str, Any], new_servers: Dict[str, Dict[str, Any]]
|
|
412
|
+
def get_branch_workspace_status(
|
|
413
|
+
project_path: Path, branch: Optional[str] = None
|
|
1436
414
|
) -> Dict[str, Any]:
|
|
1437
|
-
"""
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
if "mcpServers" not in result:
|
|
1455
|
-
result["mcpServers"] = {}
|
|
1456
|
-
|
|
1457
|
-
# Merge servers - existing entries take precedence (never overwrite user configs)
|
|
1458
|
-
for server_name, server_config in new_servers.items():
|
|
1459
|
-
if server_name not in result["mcpServers"]:
|
|
1460
|
-
result["mcpServers"][server_name] = server_config
|
|
1461
|
-
|
|
1462
|
-
return result
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
def create_or_merge_project_mcp_json(
|
|
1466
|
-
project_path: Path, mcp_servers: List[str]
|
|
1467
|
-
) -> None:
|
|
1468
|
-
"""Create or merge .mcp.json in project root for Claude Code.
|
|
1469
|
-
|
|
1470
|
-
Args:
|
|
1471
|
-
project_path: Project root directory
|
|
1472
|
-
mcp_servers: List of MCP server names to configure (e.g., ["sequential-thinking", "deepwiki"])
|
|
1473
|
-
|
|
1474
|
-
Behavior:
|
|
1475
|
-
- If mcp_servers is empty: No file created/modified (early return)
|
|
1476
|
-
- If .mcp.json exists: merge new servers (preserve existing)
|
|
1477
|
-
- If .mcp.json missing: create new with selected servers
|
|
1478
|
-
- Console output shows whether created or merged
|
|
1479
|
-
- Existing user servers NEVER overwritten
|
|
1480
|
-
- System directories (/etc, /sys, etc.) are rejected for safety
|
|
1481
|
-
|
|
1482
|
-
This creates the project-level .mcp.json that Claude Code uses,
|
|
1483
|
-
separate from the internal .claude/mcp_config.json.
|
|
1484
|
-
|
|
1485
|
-
Raises:
|
|
1486
|
-
typer.Exit(1): On file write errors or invalid paths
|
|
1487
|
-
"""
|
|
1488
|
-
# Path validation - resolve to prevent traversal
|
|
1489
|
-
resolved_path = project_path.resolve()
|
|
1490
|
-
|
|
1491
|
-
# Validate against system directories (defense-in-depth)
|
|
1492
|
-
forbidden_prefixes = ["/etc", "/sys", "/proc", "/boot", "/dev", "/var/run"]
|
|
1493
|
-
resolved_str = str(resolved_path)
|
|
1494
|
-
for forbidden in forbidden_prefixes:
|
|
1495
|
-
if resolved_str == forbidden or resolved_str.startswith(forbidden + "/"):
|
|
1496
|
-
console.print(
|
|
1497
|
-
f"[red]Error:[/red] Cannot initialize in system directory {forbidden}"
|
|
1498
|
-
)
|
|
1499
|
-
raise typer.Exit(1)
|
|
1500
|
-
|
|
1501
|
-
mcp_json_path = resolved_path / ".mcp.json"
|
|
1502
|
-
|
|
1503
|
-
# Build standard server configs for requested servers
|
|
1504
|
-
all_standard_servers = build_standard_mcp_servers()
|
|
1505
|
-
selected_servers = {
|
|
1506
|
-
name: config
|
|
1507
|
-
for name, config in all_standard_servers.items()
|
|
1508
|
-
if name in mcp_servers
|
|
415
|
+
"""Collect status information for branch-scoped planning artifacts."""
|
|
416
|
+
branch_name = sanitize_identifier(branch or get_current_branch_name())
|
|
417
|
+
workspace_dir = get_branch_workspace_dir(project_path, branch_name)
|
|
418
|
+
expected_files = list(get_branch_artifact_templates().keys())
|
|
419
|
+
existing_files = (
|
|
420
|
+
sorted(path.name for path in workspace_dir.iterdir())
|
|
421
|
+
if workspace_dir.exists()
|
|
422
|
+
else []
|
|
423
|
+
)
|
|
424
|
+
missing_files = [name for name in expected_files if name not in existing_files]
|
|
425
|
+
return {
|
|
426
|
+
"branch": branch_name,
|
|
427
|
+
"path": workspace_dir,
|
|
428
|
+
"exists": workspace_dir.exists(),
|
|
429
|
+
"existing_files": existing_files,
|
|
430
|
+
"missing_files": missing_files,
|
|
431
|
+
"is_complete": workspace_dir.exists() and not missing_files,
|
|
1509
432
|
}
|
|
1510
433
|
|
|
1511
|
-
if not selected_servers:
|
|
1512
|
-
# No servers to configure
|
|
1513
|
-
return
|
|
1514
|
-
|
|
1515
|
-
# Read existing config if present
|
|
1516
|
-
existing_config = read_project_mcp_json(mcp_json_path)
|
|
1517
|
-
|
|
1518
|
-
try:
|
|
1519
|
-
if existing_config is not None:
|
|
1520
|
-
# Merge mode - preserve existing entries
|
|
1521
|
-
merged_config = merge_mcp_json(existing_config, selected_servers)
|
|
1522
|
-
write_project_mcp_json(mcp_json_path, merged_config)
|
|
1523
|
-
|
|
1524
|
-
# Count how many new servers were added
|
|
1525
|
-
existing_servers = existing_config.get("mcpServers", {})
|
|
1526
|
-
new_count = len([s for s in selected_servers if s not in existing_servers])
|
|
1527
|
-
if new_count > 0:
|
|
1528
|
-
console.print(
|
|
1529
|
-
f"[green]✓[/green] Merged {new_count} new server(s) into .mcp.json"
|
|
1530
|
-
)
|
|
1531
|
-
else:
|
|
1532
|
-
console.print(
|
|
1533
|
-
"[green]✓[/green] .mcp.json already contains all requested servers"
|
|
1534
|
-
)
|
|
1535
|
-
else:
|
|
1536
|
-
# Create mode - new file
|
|
1537
|
-
new_config: Dict[str, Any] = {"mcpServers": selected_servers}
|
|
1538
|
-
write_project_mcp_json(mcp_json_path, new_config)
|
|
1539
|
-
console.print(
|
|
1540
|
-
f"[green]✓[/green] Created .mcp.json with {len(selected_servers)} server(s)"
|
|
1541
|
-
)
|
|
1542
|
-
|
|
1543
|
-
# Show which servers are configured
|
|
1544
|
-
console.print(
|
|
1545
|
-
f"[dim] Configured: {', '.join(sorted(selected_servers.keys()))}[/dim]"
|
|
1546
|
-
)
|
|
1547
|
-
except OSError as e:
|
|
1548
|
-
console.print(f"[red]Error:[/red] Failed to write .mcp.json: {e}")
|
|
1549
|
-
raise typer.Exit(1) from e
|
|
1550
|
-
|
|
1551
434
|
|
|
1552
435
|
def init_git_repo(project_path: Path, quiet: bool = False) -> bool:
|
|
1553
436
|
"""Initialize a git repository"""
|
|
@@ -1713,107 +596,6 @@ def get_latest_release(owner: str, repo: str) -> Optional[Dict[str, Any]]:
|
|
|
1713
596
|
return None
|
|
1714
597
|
|
|
1715
598
|
|
|
1716
|
-
def create_commands_dir(project_path: Path) -> None:
|
|
1717
|
-
"""Create commands directory with README."""
|
|
1718
|
-
commands_dir = project_path / ".claude" / "commands"
|
|
1719
|
-
commands_dir.mkdir(parents=True, exist_ok=True)
|
|
1720
|
-
|
|
1721
|
-
readme = commands_dir / "README.md"
|
|
1722
|
-
readme.write_text(
|
|
1723
|
-
"""# Claude Code Commands
|
|
1724
|
-
|
|
1725
|
-
This directory contains custom slash commands for Claude Code.
|
|
1726
|
-
|
|
1727
|
-
## Available Commands
|
|
1728
|
-
|
|
1729
|
-
- `/map-efficient` - Implement features with optimized workflow (recommended)
|
|
1730
|
-
- `/map-debug` - Debug issues using MAP analysis
|
|
1731
|
-
- `/map-fast` - Quick implementation with minimal validation
|
|
1732
|
-
- `/map-learn` - Extract lessons from completed workflows
|
|
1733
|
-
- `/map-release` - Execute MAP Framework package release workflow
|
|
1734
|
-
|
|
1735
|
-
## Creating Custom Commands
|
|
1736
|
-
|
|
1737
|
-
Create a new `.md` file in this directory with the following format:
|
|
1738
|
-
|
|
1739
|
-
```markdown
|
|
1740
|
-
---
|
|
1741
|
-
description: Brief description of your command
|
|
1742
|
-
---
|
|
1743
|
-
|
|
1744
|
-
Your command prompt here
|
|
1745
|
-
```
|
|
1746
|
-
|
|
1747
|
-
The filename becomes the command name (without the `.md` extension).
|
|
1748
|
-
"""
|
|
1749
|
-
)
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
def create_hook_files(project_path: Path) -> int:
|
|
1753
|
-
"""Create MAP hook files in .claude/hooks/
|
|
1754
|
-
|
|
1755
|
-
Returns:
|
|
1756
|
-
Number of hook files installed
|
|
1757
|
-
"""
|
|
1758
|
-
hooks_dir = project_path / ".claude" / "hooks"
|
|
1759
|
-
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
1760
|
-
|
|
1761
|
-
# Get templates directory
|
|
1762
|
-
templates_dir = get_templates_dir()
|
|
1763
|
-
hooks_template_dir = templates_dir / "hooks"
|
|
1764
|
-
|
|
1765
|
-
count = 0
|
|
1766
|
-
if hooks_template_dir.exists():
|
|
1767
|
-
import shutil
|
|
1768
|
-
|
|
1769
|
-
for hook_file in hooks_template_dir.iterdir():
|
|
1770
|
-
if hook_file.is_file():
|
|
1771
|
-
dest_file = hooks_dir / hook_file.name
|
|
1772
|
-
shutil.copy2(hook_file, dest_file)
|
|
1773
|
-
# Preserve executable permissions
|
|
1774
|
-
if hook_file.suffix in (".sh", ".py"):
|
|
1775
|
-
dest_file.chmod(0o755)
|
|
1776
|
-
count += 1
|
|
1777
|
-
|
|
1778
|
-
return count
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
def create_config_files(project_path: Path) -> int:
|
|
1782
|
-
"""Create MAP config files in .claude/
|
|
1783
|
-
|
|
1784
|
-
Copies configuration files:
|
|
1785
|
-
- settings.json
|
|
1786
|
-
- ralph-loop-config.json
|
|
1787
|
-
- workflow-rules.json
|
|
1788
|
-
|
|
1789
|
-
Returns:
|
|
1790
|
-
Number of config files installed
|
|
1791
|
-
"""
|
|
1792
|
-
claude_dir = project_path / ".claude"
|
|
1793
|
-
claude_dir.mkdir(parents=True, exist_ok=True)
|
|
1794
|
-
|
|
1795
|
-
# Get templates directory
|
|
1796
|
-
templates_dir = get_templates_dir()
|
|
1797
|
-
|
|
1798
|
-
config_files = [
|
|
1799
|
-
"settings.json",
|
|
1800
|
-
"ralph-loop-config.json",
|
|
1801
|
-
"workflow-rules.json",
|
|
1802
|
-
]
|
|
1803
|
-
|
|
1804
|
-
count = 0
|
|
1805
|
-
import shutil
|
|
1806
|
-
|
|
1807
|
-
for config_file in config_files:
|
|
1808
|
-
template_file = templates_dir / config_file
|
|
1809
|
-
if template_file.exists():
|
|
1810
|
-
dest_file = claude_dir / config_file
|
|
1811
|
-
shutil.copy2(template_file, dest_file)
|
|
1812
|
-
count += 1
|
|
1813
|
-
|
|
1814
|
-
return count
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
599
|
@app.command()
|
|
1818
600
|
def init(
|
|
1819
601
|
project_name: Optional[str] = typer.Argument(
|
|
@@ -1967,13 +749,15 @@ def init(
|
|
|
1967
749
|
# Create MAP files
|
|
1968
750
|
tracker.add("create-agents", "Create MAP agents")
|
|
1969
751
|
tracker.start("create-agents")
|
|
1970
|
-
create_agent_files(project_path, selected_mcp_servers)
|
|
1971
|
-
|
|
752
|
+
agent_count = create_agent_files(project_path, selected_mcp_servers)
|
|
753
|
+
agent_word = "agent" if agent_count == 1 else "agents"
|
|
754
|
+
tracker.complete("create-agents", f"{agent_count} {agent_word}")
|
|
1972
755
|
|
|
1973
756
|
tracker.add("create-commands", "Create slash commands")
|
|
1974
757
|
tracker.start("create-commands")
|
|
1975
|
-
create_command_files(project_path)
|
|
1976
|
-
|
|
758
|
+
command_count = create_command_files(project_path)
|
|
759
|
+
command_word = "command" if command_count == 1 else "commands"
|
|
760
|
+
tracker.complete("create-commands", f"{command_count} {command_word}")
|
|
1977
761
|
|
|
1978
762
|
tracker.add("create-skills", "Create skills")
|
|
1979
763
|
tracker.start("create-skills")
|
|
@@ -2005,6 +789,26 @@ def init(
|
|
|
2005
789
|
config_word = "file" if config_count == 1 else "files"
|
|
2006
790
|
tracker.complete("create-configs", f"{config_count} {config_word}")
|
|
2007
791
|
|
|
792
|
+
# Create default .map/config.yaml (project-level settings)
|
|
793
|
+
tracker.add("map-config", "Create .map/config.yaml")
|
|
794
|
+
tracker.start("map-config")
|
|
795
|
+
try:
|
|
796
|
+
from mapify_cli.config.project_config import write_default_config
|
|
797
|
+
|
|
798
|
+
config_path = write_default_config(project_path)
|
|
799
|
+
tracker.complete("map-config", str(config_path.relative_to(project_path)))
|
|
800
|
+
except Exception as e:
|
|
801
|
+
tracker.error("map-config", f"skipped: {e}")
|
|
802
|
+
|
|
803
|
+
# Create .claude/rules/learned/ directory for /map-learn persistence
|
|
804
|
+
tracker.add("rules-dir", "Create learned rules directory")
|
|
805
|
+
tracker.start("rules-dir")
|
|
806
|
+
rules_count = create_rules_dir(project_path)
|
|
807
|
+
tracker.complete(
|
|
808
|
+
"rules-dir",
|
|
809
|
+
f"{rules_count} file" if rules_count <= 1 else f"{rules_count} files",
|
|
810
|
+
)
|
|
811
|
+
|
|
2008
812
|
if selected_mcp_servers:
|
|
2009
813
|
# Create internal MCP config (for MAP Framework agent mappings)
|
|
2010
814
|
tracker.add("mcp-config", "Create internal MCP config")
|
|
@@ -2071,6 +875,9 @@ def init(
|
|
|
2071
875
|
steps_lines.append(
|
|
2072
876
|
" • [cyan]/map-learn[/] - Extract lessons from completed workflows"
|
|
2073
877
|
)
|
|
878
|
+
steps_lines.append(
|
|
879
|
+
f"{step_num + 1}. Run [cyan]/map-plan[/cyan] first when you want branch-scoped research, spec, and plan artifacts in `.map/<branch>/`"
|
|
880
|
+
)
|
|
2074
881
|
|
|
2075
882
|
steps_panel = Panel(
|
|
2076
883
|
"\n".join(steps_lines), title="Next Steps", border_style="cyan", padding=(1, 2)
|
|
@@ -2095,7 +902,7 @@ def check(debug: bool = typer.Option(False, "--debug", help="Enable debug loggin
|
|
|
2095
902
|
"command_start", "mapify check", metadata={"debug": debug}
|
|
2096
903
|
)
|
|
2097
904
|
show_banner()
|
|
2098
|
-
console.print("[bold]Checking
|
|
905
|
+
console.print("[bold]Checking MAP Framework environment...[/bold]\n")
|
|
2099
906
|
|
|
2100
907
|
tracker = StepTracker("Check Available Tools")
|
|
2101
908
|
|
|
@@ -2118,36 +925,294 @@ def check(debug: bool = typer.Option(False, "--debug", help="Enable debug loggin
|
|
|
2118
925
|
tracker.error(tool, "not found")
|
|
2119
926
|
results[tool] = False
|
|
2120
927
|
|
|
928
|
+
health = get_project_health(Path.cwd())
|
|
929
|
+
|
|
930
|
+
tracker.add("project", "Detect MAP project")
|
|
931
|
+
if health["initialized"]:
|
|
932
|
+
tracker.complete("project", "initialized")
|
|
933
|
+
else:
|
|
934
|
+
tracker.error("project", "not initialized")
|
|
935
|
+
|
|
936
|
+
tracker.add("templates", "Inspect bundled templates")
|
|
937
|
+
if health["expected_agents"] and health["expected_commands"]:
|
|
938
|
+
tracker.complete(
|
|
939
|
+
"templates",
|
|
940
|
+
f"{health['expected_agents']} agents, {health['expected_commands']} commands",
|
|
941
|
+
)
|
|
942
|
+
else:
|
|
943
|
+
tracker.error("templates", "missing bundled templates")
|
|
944
|
+
|
|
945
|
+
tracker.add("mcp", "Check supported MCP servers")
|
|
946
|
+
supported_servers = sorted(build_standard_mcp_servers().keys())
|
|
947
|
+
tracker.complete("mcp", ", ".join(supported_servers) or "none")
|
|
948
|
+
|
|
2121
949
|
console.print(tracker.render())
|
|
2122
950
|
console.print()
|
|
2123
951
|
|
|
2124
|
-
if all(results.values()):
|
|
952
|
+
if all(results.values()) and health["initialized"]:
|
|
2125
953
|
console.print(
|
|
2126
954
|
"[bold green]All tools are installed! MAP Framework is ready to use.[/bold green]"
|
|
2127
955
|
)
|
|
2128
956
|
else:
|
|
2129
|
-
console.print("[yellow]
|
|
957
|
+
console.print("[yellow]MAP environment needs attention:[/yellow]")
|
|
2130
958
|
if not results.get("git"):
|
|
2131
959
|
console.print(" • Install git: https://git-scm.com/downloads")
|
|
2132
960
|
if not results.get("claude"):
|
|
2133
961
|
console.print(
|
|
2134
962
|
" • Install Claude Code: https://docs.anthropic.com/en/docs/claude-code/setup"
|
|
2135
963
|
)
|
|
964
|
+
if not health["initialized"]:
|
|
965
|
+
console.print(" • Initialize this directory: mapify init .")
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
@app.command()
|
|
969
|
+
def doctor(debug: bool = typer.Option(False, "--debug", help="Enable debug logging")):
|
|
970
|
+
"""Run a detailed MAP project readiness diagnosis."""
|
|
971
|
+
if is_debug_enabled(debug):
|
|
972
|
+
from mapify_cli.workflow_logger import MapWorkflowLogger
|
|
973
|
+
|
|
974
|
+
workflow_logger = MapWorkflowLogger(Path.cwd(), enabled=True)
|
|
975
|
+
log_file = workflow_logger.start_session(
|
|
976
|
+
task_id=f"mapify_doctor_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
|
977
|
+
)
|
|
978
|
+
console.print(f"[dim]Debug logging enabled: {log_file}[/dim]")
|
|
979
|
+
workflow_logger.log_event(
|
|
980
|
+
"command_start", "mapify doctor", metadata={"debug": debug}
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
show_banner()
|
|
984
|
+
console.print("[bold]Running MAP doctor...[/bold]\n")
|
|
985
|
+
|
|
986
|
+
project_path = Path.cwd()
|
|
987
|
+
health = get_project_health(project_path)
|
|
988
|
+
tracker = StepTracker("MAP Doctor")
|
|
989
|
+
|
|
990
|
+
for tool_name, description in [
|
|
991
|
+
("git", "Git version control"),
|
|
992
|
+
("claude", "Claude Code CLI"),
|
|
993
|
+
]:
|
|
994
|
+
tracker.add(tool_name, description)
|
|
995
|
+
if check_tool(tool_name):
|
|
996
|
+
tracker.complete(tool_name, "available")
|
|
997
|
+
else:
|
|
998
|
+
tracker.error(tool_name, "not found")
|
|
999
|
+
|
|
1000
|
+
tracker.add("project", "MAP project structure")
|
|
1001
|
+
if not health["missing_paths"]:
|
|
1002
|
+
tracker.complete("project", "all core paths present")
|
|
1003
|
+
else:
|
|
1004
|
+
tracker.error("project", f"missing {len(health['missing_paths'])} path(s)")
|
|
1005
|
+
|
|
1006
|
+
tracker.add("templates", "Installed template counts")
|
|
1007
|
+
if (
|
|
1008
|
+
health["installed_agents"] == health["expected_agents"]
|
|
1009
|
+
and health["installed_commands"] == health["expected_commands"]
|
|
1010
|
+
):
|
|
1011
|
+
tracker.complete(
|
|
1012
|
+
"templates",
|
|
1013
|
+
f"{health['installed_agents']}/{health['expected_agents']} agents, "
|
|
1014
|
+
f"{health['installed_commands']}/{health['expected_commands']} commands",
|
|
1015
|
+
)
|
|
1016
|
+
else:
|
|
1017
|
+
tracker.error(
|
|
1018
|
+
"templates",
|
|
1019
|
+
f"agents {health['installed_agents']}/{health['expected_agents']}, "
|
|
1020
|
+
f"commands {health['installed_commands']}/{health['expected_commands']}",
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
tracker.add("planning", "Branch workspace artifacts")
|
|
1024
|
+
if health["branch_workspace_exists"]:
|
|
1025
|
+
tracker.complete(
|
|
1026
|
+
"planning",
|
|
1027
|
+
f"branch {health['current_branch']}: {health['branch_artifact_count']}/{health['expected_branch_artifact_count']} artifacts",
|
|
1028
|
+
)
|
|
1029
|
+
else:
|
|
1030
|
+
tracker.error("planning", f"missing .map/{health['current_branch']}")
|
|
1031
|
+
|
|
1032
|
+
tracker.add("mcp", "Project MCP configuration")
|
|
1033
|
+
if health["has_project_mcp"]:
|
|
1034
|
+
if health["project_mcp_valid"]:
|
|
1035
|
+
tracker.complete("mcp", ".mcp.json valid")
|
|
1036
|
+
else:
|
|
1037
|
+
tracker.error("mcp", ".mcp.json unreadable")
|
|
1038
|
+
elif health["has_internal_mcp"]:
|
|
1039
|
+
tracker.complete("mcp", "internal config only")
|
|
1040
|
+
else:
|
|
1041
|
+
tracker.complete("mcp", "no MCP config")
|
|
1042
|
+
|
|
1043
|
+
console.print(tracker.render())
|
|
1044
|
+
console.print()
|
|
1045
|
+
|
|
1046
|
+
details = Table(title="Doctor Details", show_header=True, header_style="bold cyan")
|
|
1047
|
+
details.add_column("Check")
|
|
1048
|
+
details.add_column("Status")
|
|
1049
|
+
details.add_column("Details")
|
|
1050
|
+
details.add_row(
|
|
1051
|
+
"Project",
|
|
1052
|
+
"OK" if health["initialized"] else "Needs init",
|
|
1053
|
+
(
|
|
1054
|
+
".claude + workflow configs detected"
|
|
1055
|
+
if health["initialized"]
|
|
1056
|
+
else "Run `mapify init .`"
|
|
1057
|
+
),
|
|
1058
|
+
)
|
|
1059
|
+
details.add_row(
|
|
1060
|
+
"Agents",
|
|
1061
|
+
f"{health['installed_agents']}/{health['expected_agents']}",
|
|
1062
|
+
"Installed vs bundled agent templates",
|
|
1063
|
+
)
|
|
1064
|
+
details.add_row(
|
|
1065
|
+
"Commands",
|
|
1066
|
+
f"{health['installed_commands']}/{health['expected_commands']}",
|
|
1067
|
+
"Installed vs bundled slash commands",
|
|
1068
|
+
)
|
|
1069
|
+
details.add_row(
|
|
1070
|
+
"Planning",
|
|
1071
|
+
(
|
|
1072
|
+
f"{health['branch_artifact_count']}/{health['expected_branch_artifact_count']}"
|
|
1073
|
+
if health["branch_workspace_exists"]
|
|
1074
|
+
else "missing"
|
|
1075
|
+
),
|
|
1076
|
+
f"Current branch workspace: .map/{health['current_branch']}/",
|
|
1077
|
+
)
|
|
1078
|
+
details.add_row(
|
|
1079
|
+
"MCP",
|
|
1080
|
+
(
|
|
1081
|
+
"valid"
|
|
1082
|
+
if health["project_mcp_valid"]
|
|
1083
|
+
else ("present" if health["has_project_mcp"] else "not configured")
|
|
1084
|
+
),
|
|
1085
|
+
".mcp.json status",
|
|
1086
|
+
)
|
|
1087
|
+
console.print(details)
|
|
1088
|
+
|
|
1089
|
+
if health["missing_paths"]:
|
|
1090
|
+
console.print()
|
|
1091
|
+
console.print("[yellow]Missing core paths:[/yellow]")
|
|
1092
|
+
for path_name in health["missing_paths"]:
|
|
1093
|
+
console.print(f" • {path_name}")
|
|
2136
1094
|
|
|
2137
1095
|
|
|
2138
1096
|
@app.command()
|
|
2139
1097
|
def upgrade():
|
|
2140
1098
|
"""Upgrade MAP agents to the latest version."""
|
|
2141
1099
|
show_banner()
|
|
1100
|
+
project_path = Path.cwd()
|
|
1101
|
+
|
|
1102
|
+
if not is_map_initialized(project_path):
|
|
1103
|
+
console.print(
|
|
1104
|
+
"[yellow]MAP Framework not initialized in this directory.[/yellow]"
|
|
1105
|
+
)
|
|
1106
|
+
console.print("Run: [cyan]mapify init .[/cyan]")
|
|
1107
|
+
raise typer.Exit(0)
|
|
1108
|
+
|
|
2142
1109
|
console.print("[cyan]Checking for updates...[/cyan]")
|
|
1110
|
+
latest_release = get_latest_release("azalio", "map-framework")
|
|
1111
|
+
latest_version = None
|
|
1112
|
+
|
|
1113
|
+
if latest_release and latest_release.get("tag_name"):
|
|
1114
|
+
latest_version = latest_release["tag_name"].lstrip("v")
|
|
1115
|
+
if parse_version(latest_version) > parse_version(__version__):
|
|
1116
|
+
console.print(
|
|
1117
|
+
f"[yellow]New version available:[/yellow] {latest_version} "
|
|
1118
|
+
f"(installed {__version__})"
|
|
1119
|
+
)
|
|
1120
|
+
if latest_release.get("html_url"):
|
|
1121
|
+
console.print(f"Release: [cyan]{latest_release['html_url']}[/cyan]")
|
|
1122
|
+
else:
|
|
1123
|
+
console.print(
|
|
1124
|
+
f"[green]You are on the latest installed version ({__version__}).[/green]"
|
|
1125
|
+
)
|
|
1126
|
+
else:
|
|
1127
|
+
console.print(
|
|
1128
|
+
"[dim]Could not fetch release metadata; refreshing local templates anyway.[/dim]"
|
|
1129
|
+
)
|
|
2143
1130
|
|
|
2144
|
-
|
|
2145
|
-
# 1. Fetch latest release from GitHub
|
|
2146
|
-
# 2. Compare versions
|
|
2147
|
-
# 3. Update agents if newer version available
|
|
1131
|
+
tracker = StepTracker("Upgrade MAP Framework Files")
|
|
2148
1132
|
|
|
2149
|
-
|
|
2150
|
-
|
|
1133
|
+
# Track drift across all file types
|
|
1134
|
+
from mapify_cli.delivery.managed_file_copier import DriftReport
|
|
1135
|
+
|
|
1136
|
+
drift_report = DriftReport()
|
|
1137
|
+
|
|
1138
|
+
existing_project_mcp = read_project_mcp_json(project_path / ".mcp.json")
|
|
1139
|
+
existing_server_names = []
|
|
1140
|
+
if existing_project_mcp:
|
|
1141
|
+
existing_server_names = list(existing_project_mcp.get("mcpServers", {}).keys())
|
|
1142
|
+
|
|
1143
|
+
tracker.add("agents", "Refresh agent templates")
|
|
1144
|
+
tracker.start("agents")
|
|
1145
|
+
agent_count = create_agent_files(project_path, existing_server_names, drift_report)
|
|
1146
|
+
tracker.complete("agents", f"{agent_count} files")
|
|
1147
|
+
|
|
1148
|
+
tracker.add("commands", "Refresh slash commands")
|
|
1149
|
+
tracker.start("commands")
|
|
1150
|
+
command_count = create_command_files(project_path, drift_report)
|
|
1151
|
+
tracker.complete("commands", f"{command_count} files")
|
|
1152
|
+
|
|
1153
|
+
tracker.add("skills", "Refresh skills")
|
|
1154
|
+
tracker.start("skills")
|
|
1155
|
+
skill_count = create_skill_files(project_path)
|
|
1156
|
+
tracker.complete("skills", f"{skill_count} folders")
|
|
1157
|
+
|
|
1158
|
+
tracker.add("references", "Refresh reference files")
|
|
1159
|
+
tracker.start("references")
|
|
1160
|
+
ref_count = create_reference_files(project_path, drift_report)
|
|
1161
|
+
tracker.complete("references", f"{ref_count} files")
|
|
1162
|
+
|
|
1163
|
+
tracker.add("hooks", "Refresh shared hooks")
|
|
1164
|
+
tracker.start("hooks")
|
|
1165
|
+
hook_count = create_hook_files(project_path, drift_report)
|
|
1166
|
+
tracker.complete("hooks", f"{hook_count} files")
|
|
1167
|
+
|
|
1168
|
+
tracker.add("configs", "Refresh config files")
|
|
1169
|
+
tracker.start("configs")
|
|
1170
|
+
config_count = create_config_files(project_path, drift_report)
|
|
1171
|
+
tracker.complete("configs", f"{config_count} files")
|
|
1172
|
+
|
|
1173
|
+
tracker.add("permissions", "Merge local approvals")
|
|
1174
|
+
tracker.start("permissions")
|
|
1175
|
+
create_or_merge_project_settings_local(project_path)
|
|
1176
|
+
tracker.complete("permissions", "settings.local.json updated")
|
|
1177
|
+
|
|
1178
|
+
if (project_path / ".claude" / "mcp_config.json").exists() or (
|
|
1179
|
+
project_path / ".mcp.json"
|
|
1180
|
+
).exists():
|
|
1181
|
+
tracker.add("mcp", "Preserve MCP config")
|
|
1182
|
+
tracker.complete("mcp", "left unchanged")
|
|
1183
|
+
|
|
1184
|
+
console.print()
|
|
1185
|
+
console.print(tracker.render())
|
|
1186
|
+
|
|
1187
|
+
# Show drift warnings if any files were modified by the user
|
|
1188
|
+
if drift_report.has_drift:
|
|
1189
|
+
console.print()
|
|
1190
|
+
console.print(
|
|
1191
|
+
f"[yellow]⚠ {len(drift_report.drifted_files)} file(s) had local modifications:[/yellow]"
|
|
1192
|
+
)
|
|
1193
|
+
for r in drift_report.drifted_files:
|
|
1194
|
+
try:
|
|
1195
|
+
rel = r.dest.relative_to(project_path)
|
|
1196
|
+
except ValueError:
|
|
1197
|
+
rel = r.dest
|
|
1198
|
+
backup_note = ""
|
|
1199
|
+
if r.backed_up and r.backup_path:
|
|
1200
|
+
try:
|
|
1201
|
+
backup_rel = r.backup_path.relative_to(project_path)
|
|
1202
|
+
except ValueError:
|
|
1203
|
+
backup_rel = r.backup_path
|
|
1204
|
+
backup_note = f" → backup: [cyan]{backup_rel}[/cyan]"
|
|
1205
|
+
console.print(f" [yellow]•[/yellow] {rel}{backup_note}")
|
|
1206
|
+
console.print(
|
|
1207
|
+
"[dim]Your changes were backed up to .bak files. "
|
|
1208
|
+
"Review and re-apply any customizations if needed.[/dim]"
|
|
1209
|
+
)
|
|
1210
|
+
|
|
1211
|
+
console.print()
|
|
1212
|
+
console.print("[bold green]Upgrade complete.[/bold green]")
|
|
1213
|
+
console.print(
|
|
1214
|
+
"[dim]Note: upgrade refreshes shipped MAP files but does not overwrite project-specific MCP selections.[/dim]"
|
|
1215
|
+
)
|
|
2151
1216
|
|
|
2152
1217
|
|
|
2153
1218
|
# Validate commands
|