mapify-cli 3.5.0__py3-none-any.whl → 3.6.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.
Files changed (51) hide show
  1. mapify_cli/__init__.py +520 -1455
  2. mapify_cli/cli_ui.py +320 -0
  3. mapify_cli/config/__init__.py +38 -0
  4. mapify_cli/config/mcp.py +293 -0
  5. mapify_cli/config/project_config.py +275 -0
  6. mapify_cli/config/settings.py +170 -0
  7. mapify_cli/delivery/__init__.py +60 -0
  8. mapify_cli/delivery/agent_generator.py +381 -0
  9. mapify_cli/delivery/file_copier.py +443 -0
  10. mapify_cli/delivery/managed_file_copier.py +316 -0
  11. mapify_cli/schemas.py +162 -0
  12. mapify_cli/templates/agents/actor.md +43 -33
  13. mapify_cli/templates/agents/final-verifier.md +16 -1
  14. mapify_cli/templates/agents/monitor.md +22 -27
  15. mapify_cli/templates/agents/predictor.md +5 -22
  16. mapify_cli/templates/agents/task-decomposer.md +16 -3
  17. mapify_cli/templates/commands/map-check.md +116 -13
  18. mapify_cli/templates/commands/map-debate.md +2 -11
  19. mapify_cli/templates/commands/map-efficient.md +157 -217
  20. mapify_cli/templates/commands/map-learn.md +153 -98
  21. mapify_cli/templates/commands/map-plan.md +82 -16
  22. mapify_cli/templates/commands/map-resume.md +54 -27
  23. mapify_cli/templates/commands/map-review.md +58 -0
  24. mapify_cli/templates/commands/map-task.md +13 -9
  25. mapify_cli/templates/commands/map-tdd.md +23 -34
  26. mapify_cli/templates/hooks/post-compact-context.py +1 -1
  27. mapify_cli/templates/hooks/ralph-context-pruner.py +3 -3
  28. mapify_cli/templates/hooks/ralph-iteration-logger.py +88 -0
  29. mapify_cli/templates/hooks/safety-guardrails.py +39 -3
  30. mapify_cli/templates/hooks/workflow-context-injector.py +15 -14
  31. mapify_cli/templates/hooks/workflow-gate.py +153 -162
  32. mapify_cli/templates/map/scripts/diagnostics.py +187 -0
  33. mapify_cli/templates/map/scripts/map_orchestrator.py +255 -294
  34. mapify_cli/templates/map/scripts/map_step_runner.py +688 -52
  35. mapify_cli/templates/map/scripts/map_utils.py +30 -0
  36. mapify_cli/templates/references/escalation-matrix.md +24 -0
  37. mapify_cli/templates/references/step-state-schema.md +1 -12
  38. mapify_cli/templates/references/workflow-state-schema.md +3 -264
  39. mapify_cli/templates/rules/learned/README.md +18 -0
  40. mapify_cli/templates/settings.json +6 -0
  41. mapify_cli/templates/skills/map-learn/SKILL.md +321 -0
  42. mapify_cli/templates/skills/map-learn/templates/example-rules.md +19 -0
  43. mapify_cli/templates/skills/map-learn/templates/rules-unconditional.md +5 -0
  44. mapify_cli/templates/skills/map-learn/templates/rules-with-paths.md +10 -0
  45. mapify_cli/templates/skills/map-planning/SKILL.md +1 -3
  46. mapify_cli/templates/skills/map-workflows-guide/SKILL.md +2 -2
  47. mapify_cli/templates/skills/skill-rules.json +22 -0
  48. {mapify_cli-3.5.0.dist-info → mapify_cli-3.6.0.dist-info}/METADATA +15 -3
  49. {mapify_cli-3.5.0.dist-info → mapify_cli-3.6.0.dist-info}/RECORD +51 -36
  50. {mapify_cli-3.5.0.dist-info → mapify_cli-3.6.0.dist-info}/WHEEL +0 -0
  51. {mapify_cli-3.5.0.dist-info → mapify_cli-3.6.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.5.0"
26
+ __version__ = "3.6.0"
27
27
 
28
- import copy
29
28
  import os
30
29
  import subprocess
31
30
  import sys
32
31
  import shutil
33
- import json
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
- from rich.tree import Tree
58
- from typer.core import TyperGroup
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 available/configured"""
456
- # For now, we'll assume MCP servers are available if configured
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
- def create_reference_files(project_path: Path) -> int:
932
- """Create MAP reference files in .claude/references/
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
- references_dir = project_path / ".claude" / "references"
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
- if not commands_template_dir.exists():
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
- $ARGUMENTS
975
-
976
- Start with task decomposition (task-decomposer), then iterate through actor-monitor for each subtask.
977
- Predictor is called conditionally for high-risk subtasks only.
978
- Run /map-learn after workflow if you want to preserve lessons learned.
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
- if skills_template_dir.exists():
1041
- # Copy README.md and skill-rules.json to .claude/skills/
1042
- if (skills_template_dir / "README.md").exists():
1043
- shutil.copy2(skills_template_dir / "README.md", skills_dir / "README.md")
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
- if (skills_template_dir / "skill-rules.json").exists():
1046
- shutil.copy2(
1047
- skills_template_dir / "skill-rules.json",
1048
- skills_dir / "skill-rules.json",
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
- return count
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 _copy_map_subdir(
1062
- map_template_dir: Path, map_dir: Path, subdir: str, executable_glob: str
251
+ def count_project_markdown_files(
252
+ directory: Path, exclude_files: Optional[set[str]] = None
1063
253
  ) -> int:
1064
- """Copy a subdirectory from map templates to .map/ and make scripts executable."""
1065
- import shutil
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
- dest = map_dir / subdir
1072
- if dest.exists():
1073
- try:
1074
- shutil.rmtree(dest)
1075
- except (OSError, PermissionError) as e:
1076
- import sys
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 create_or_merge_project_settings_local(project_path: Path) -> None:
1192
- """Create/merge .claude/settings.local.json with safe project allowlist.
1193
-
1194
- Claude Code supports per-project approvals via `.claude/settings.local.json`.
1195
- This file is user-local (should not be committed) and is merged by Claude Code
1196
- with global settings from `~/.claude/settings.json`.
1197
-
1198
- IMPORTANT:
1199
- - Shared, repo-committed hooks MUST be configured in `.claude/settings.json`.
1200
- - `.claude/settings.local.json` is for user-local approvals/allowlists and should
1201
- not be used as the primary distribution mechanism for project hooks.
1202
-
1203
- We keep this allowlist intentionally narrow and focused on common safe actions
1204
- for local development workflows.
1205
- """
1206
-
1207
- settings_file = project_path / ".claude" / "settings.local.json"
1208
- settings_file.parent.mkdir(parents=True, exist_ok=True)
1209
-
1210
- default_permissions: Dict[str, Any] = {
1211
- "allow": [
1212
- # SourceCraft MCP helpers (project-scoped)
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
- # Load existing settings if present
1236
- if settings_file.exists():
1237
- try:
1238
- existing_settings = json.loads(settings_file.read_text())
1239
- except json.JSONDecodeError:
1240
- console.print(
1241
- f"[yellow]Warning:[/yellow] Corrupted {settings_file}, will recreate"
1242
- )
1243
- existing_settings = {}
1244
- else:
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
- existing_settings.setdefault("permissions", {})
1256
- permissions = existing_settings["permissions"]
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
- # Add server configurations
1297
- server_configs = {
1298
- "sequential-thinking": {
1299
- "enabled": True,
1300
- "description": "Chain-of-thought reasoning",
1301
- "config": {
1302
- "max_thoughts": 10,
1303
- "branch_exploration": True,
1304
- "hypothesis_verification": True,
1305
- },
1306
- },
1307
- "deepwiki": {
1308
- "enabled": True,
1309
- "description": "GitHub repository intelligence",
1310
- "config": {"auto_structure": True, "max_depth": 3, "cache_repos": True},
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
- # Update agent mappings based on selected servers
1320
- if "sequential-thinking" in mcp_servers:
1321
- for agent in [
1322
- "task-decomposer",
1323
- "monitor",
1324
- "evaluator",
1325
- "reflector",
1326
- "debate-arbiter",
1327
- ]:
1328
- if agent in config["agent_mcp_mappings"]:
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
- # Write config file
1336
- config_file = project_path / ".claude" / "mcp_config.json"
1337
- config_file.parent.mkdir(parents=True, exist_ok=True)
1338
- config_file.write_text(json.dumps(config, indent=2))
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
- # Project-level .mcp.json functions (for Claude Code MCP server configuration)
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 build_standard_mcp_servers() -> Dict[str, Dict[str, Any]]:
1347
- """Build standard MCP server configurations for Claude Code .mcp.json format.
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
- Note: These configs are for the project-level .mcp.json file that Claude Code
1353
- reads, separate from the internal .claude/mcp_config.json.
1354
- """
387
+ def get_branch_artifact_templates() -> Dict[str, str]:
388
+ """Return artifact templates aligned to MAP branch workspaces."""
1355
389
  return {
1356
- "sequential-thinking": {
1357
- "command": "npx",
1358
- "args": ["-y", "@modelcontextprotocol/server-sequential-thinking"],
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 read_project_mcp_json(path: Path) -> Optional[Dict[str, Any]]:
1368
- """Read .mcp.json from project root.
1369
-
1370
- Args:
1371
- path: Path to .mcp.json file
1372
-
1373
- Returns:
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
- Args:
1418
- path: Path to .mcp.json file
1419
- config: Configuration dict to write
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
- Raises:
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
- def merge_mcp_json(
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
- """Merge new MCP servers into existing .mcp.json configuration.
1438
-
1439
- Args:
1440
- existing: Existing .mcp.json content (may be empty dict)
1441
- new_servers: Dict mapping server names to their configs
1442
-
1443
- Returns:
1444
- Merged configuration with existing servers preserved
1445
-
1446
- Behavior:
1447
- - Preserves existing mcpServers entries (user customizations)
1448
- - Only adds new servers that don't exist
1449
- - Preserves other top-level keys (e.g., custom settings)
1450
- """
1451
- result = copy.deepcopy(existing)
1452
-
1453
- # Ensure mcpServers key exists
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
- tracker.complete("create-agents", "12 agents")
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
- tracker.complete("create-commands", "10 commands")
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 for installed tools...[/bold]\n")
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]Some tools are missing:[/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
- # In a real implementation, this would:
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
- console.print("[yellow]Upgrade feature coming soon![/yellow]")
2150
- console.print("For now, run: [cyan]mapify init . --force[/cyan] to update agents")
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