Skip to content

Commit f0f798f

Browse files
azalioclaude
andauthored
feat: platform refactor — spec, decomposition, config, managed copier (#90)
* docs: add MAP platform refactor spec with OpenSpec analysis and codebase audit - Landscape analysis: OpenSpec patterns (schema DAGs, context injection, multi-tool delivery, verification dimensions, migration) with clear differentiation from MAP's hard-gate philosophy - Per-phase codebase analysis with concrete file paths, LOC counts, and gap tables for all 7 implementation phases - Artifact inventory: 25 .map/ artifacts catalogued with producer/consumer and schema coverage assessment - Claude Code coupling map: portable vs platform-specific components - Implementation priority graph with dependency ordering and quick wins - 10 open questions enriched with codebase evidence * refactor: decompose __init__.py into cli_ui, delivery/, config/ submodules Extract 1422 lines (53%) from the monolithic __init__.py (2692→1270 lines) into focused submodules while maintaining full backward compatibility: - cli_ui.py: StepTracker, interactive selectors, banner display - delivery/agent_generator.py: 7 fallback agent content generators - delivery/file_copier.py: file copy/install functions (agents, commands, hooks, etc.) - config/settings.py: global/project permissions management - config/mcp.py: MCP server configuration and .mcp.json handling Quick wins included: - Add BLUEPRINT_SCHEMA to schemas.py (blueprint.json was parsed without validation) - Add validate_artifact() and load_and_validate() utilities to schemas.py - Support jsonschema Draft7/4 fallback for older environments All re-exports preserved in __init__.py — existing imports continue to work. Tests updated: mock paths adjusted for new module locations. 24 new decomposition tests + 88 template sync tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add .map/config.yaml project configuration system Introduce MapConfig dataclass with 23 configurable fields covering workflow profiles, policy thresholds, safety guardrails, pruner settings, and delivery options. Config is loaded from .map/config.yaml with graceful fallback to defaults when file is missing or malformed. - Add config/project_config.py with MapConfig, load_map_config(), generate_default_config(), write_default_config() - Wire write_default_config() into `mapify init` command - Update safety-guardrails.py hook to read config overrides from .map/config.yaml (dangerous_file_patterns, dangerous_commands, safe_path_prefixes) with fallback to hardcoded defaults - Sync hook template to .claude/hooks/ - Add 13 tests for config system (load, generate, write, edge cases) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add drift-aware managed file copier for upgrade safety Replace raw shutil.copy2() with copy_managed_file() that injects metadata headers (generated_by, mapify_version, template_hash) and detects user modifications during upgrade. - Add delivery/managed_file_copier.py with metadata injection for .md (HTML comment), .py (comment), .json (_map_managed key) - Add drift detection: compares content hash against stored template_hash - Auto-backup drifted files to .bak before overwriting - Integrate into all file_copier.py functions (agents, commands, references, hooks, configs) with optional DriftReport parameter - Update upgrade() to collect drift reports and show warnings with backup locations for modified files - Add 29 tests covering hashing, injection, extraction, roundtrips, drift detection, and backup creation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: review fixes — correctness bugs, DRY violations, missing tests - Fix load_and_validate returning data instead of None on invalid input - Deduplicate get_templates_dir (delegate from __init__.py to file_copier) - Use timestamped backup names to prevent collision on repeated upgrades - Unify Console instances (import from cli_ui instead of creating new) - Fix extract_metadata .py reconstruction (positional split, not search) - Add MapConfig type validation (wrong YAML types fall back to defaults) - Simplify no-op setdefault calls in settings.py - Remove unused mcp_section from create_reflector_content - Remove duplicate in-function imports from file_copier._copy_map_path - Add spec link to README docs table - Add 4 tests: guardrails config override, type coercion, backup collision, load_and_validate with invalid data * fix: remove duplicate import and add write error handling - Remove duplicate `from datetime import datetime, timezone` inside copy_managed_file() (already imported at module level) - Add try/except around dest.write_text() to gracefully handle write failures instead of crashing mid-upgrade - Add `from __future__ import annotations` to file_copier.py for consistent Python 3.10 compatibility with union type syntax Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: sanitize control characters in handoff bundle to prevent jq parse errors build_handoff_bundle and build_review_handoff read markdown/JSON artifacts that may contain raw control characters (ANSI escapes, null bytes from test output). These chars pass through Python json.dumps correctly but can corrupt bash variable expansion before reaching jq. - Add _sanitize_for_json() to strip U+0000-U+001F (except \n \r \t) - Use errors="replace" on read_text to handle non-UTF-8 files gracefully - Add OSError catch to prevent traceback on stdout from corrupted files - Use ensure_ascii=True in json.dumps for handoff CLI output * fix: align step_state field names between map-plan and workflow-gate map-plan created step_state.json with field names "current_state" and "current_subtask", but workflow-gate.py reads "current_step_phase" and "current_subtask_id". This mismatch caused the gate to see empty phase and block all edits after map-plan, requiring manual state patching. - Fix map-plan.md step_state template: use current_step_phase and current_subtask_id to match gate expectations - Fix map-tdd.md: add workflow_status check before resume, explicit guidance for COMPLETE/INITIALIZED states requiring reinit - Add _sanitize_for_json to map_step_runner.py handoff functions to prevent jq parse errors from control chars in artifact content --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ae7be28 commit f0f798f

24 files changed

Lines changed: 4591 additions & 1512 deletions

.claude/commands/map-plan.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -474,8 +474,8 @@ Use the **Write** tool to create `.map/<branch>/step_state.json` with this struc
474474
"_semantic_tag": "MAP_State_v1_0",
475475
"workflow": "map-plan",
476476
"started_at": "<current UTC timestamp in ISO 8601>",
477-
"current_subtask": null,
478-
"current_state": "INITIALIZED",
477+
"current_subtask_id": null,
478+
"current_step_phase": "INITIALIZED",
479479
"completed_steps": [],
480480
"pending_steps": [],
481481
"subtask_sequence": ["ST-001", "ST-002", "ST-003"],
@@ -492,6 +492,8 @@ Use the **Write** tool to create `.map/<branch>/step_state.json` with this struc
492492
}
493493
```
494494

495+
**IMPORTANT field names:** Use `current_subtask_id` (not `current_subtask`) and `current_step_phase` (not `current_state`). These field names must match what `workflow-gate.py` reads — mismatched names cause the gate to block all edits.
496+
495497
**IMPORTANT:**
496498
- Replace `subtask_sequence` with actual IDs from the decomposition
497499
- Populate `aag_contracts` map with each subtask's AAG contract from the decomposer output — executors read this to set context for each subtask

.claude/commands/map-tdd.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,14 @@ Verify that a plan or spec exists for this branch:
6666
echo "spec: $(test -f .map/${BRANCH}/spec_${BRANCH}.md && echo EXISTS || echo MISSING)"
6767
echo "task_plan: $(test -f .map/${BRANCH}/task_plan_${BRANCH}.md && echo EXISTS || echo MISSING)"
6868
echo "step_state: $(test -f .map/${BRANCH}/step_state.json && echo EXISTS || echo MISSING)"
69+
if [ -f ".map/${BRANCH}/step_state.json" ]; then
70+
echo "status: $(python3 -c "import json; d=json.load(open('.map/${BRANCH}/step_state.json')); print(d.get('workflow_status', d.get('current_step_phase', 'UNKNOWN')))")"
71+
fi
6972
```
7073

7174
- If **no spec and no task_plan**: Run `/map-plan` first. TDD requires clear acceptance criteria.
72-
- If **step_state.json EXISTS**: Resume from checkpoint (same as /map-efficient resume logic).
75+
- If **step_state.json EXISTS and status is COMPLETE or INITIALIZED**: Previous workflow finished or only plan exists. You MUST reinitialize for TDD by running `python3 .map/scripts/map_orchestrator.py resume_single_subtask "$SUBTASK_ID" --tdd` (single subtask) or `python3 .map/scripts/map_orchestrator.py resume_from_plan` then enable TDD mode (full workflow). Do NOT attempt edits without reinitializing — the workflow gate will block edits when current_step_phase is empty/INITIALIZED/COMPLETE.
76+
- If **step_state.json EXISTS and status is IN_PROGRESS**: Resume from checkpoint (same as /map-efficient resume logic). Check `current_step_phase` — if empty, reinitialize with `resume_from_plan`.
7377
- If **task_plan EXISTS but no step_state**: Run `python3 .map/scripts/map_orchestrator.py resume_from_plan` then enable TDD mode.
7478

7579
### Enable TDD Mode (full workflow only)

.claude/hooks/safety-guardrails.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,17 @@
1313
"""
1414

1515
import json
16+
import os
1617
import re
1718
import sys
19+
from pathlib import Path
20+
21+
# =============================================================================
22+
# Default constants (overridable via .map/config.yaml → safe_path_prefixes)
23+
# =============================================================================
1824

1925
# Dangerous file patterns (case-insensitive)
20-
DANGEROUS_FILE_PATTERNS = [
26+
_DEFAULT_DANGEROUS_FILE_PATTERNS = [
2127
r"\.env($|\.)", # .env, .env.local, .env.production
2228
r"credentials",
2329
r"private[_-]?key",
@@ -31,7 +37,7 @@
3137
]
3238

3339
# Dangerous bash command patterns
34-
DANGEROUS_COMMANDS = [
40+
_DEFAULT_DANGEROUS_COMMANDS = [
3541
r"rm\s+-rf\s+/", # rm -rf /
3642
r"rm\s+-rf\s+\*", # rm -rf *
3743
r"rm\s+-rf\s+\.\.", # rm -rf ..
@@ -46,7 +52,7 @@
4652
]
4753

4854
# Safe path prefixes (skip checks for known safe directories)
49-
SAFE_PATH_PREFIXES = [
55+
_DEFAULT_SAFE_PATH_PREFIXES = [
5056
"src/",
5157
"lib/",
5258
"test/",
@@ -64,6 +70,36 @@
6470
]
6571

6672

73+
def _load_config_overrides() -> dict:
74+
"""Load overrides from .map/config.yaml if it exists.
75+
76+
Reads safe_path_prefixes from project config to allow customization.
77+
Falls back to defaults when config is missing or unreadable.
78+
"""
79+
project_dir = Path(os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd()))
80+
config_path = project_dir / ".map" / "config.yaml"
81+
if not config_path.exists():
82+
return {}
83+
try:
84+
import yaml # type: ignore[import-untyped]
85+
86+
with open(config_path) as f:
87+
data = yaml.safe_load(f)
88+
return data if isinstance(data, dict) else {}
89+
except Exception:
90+
return {}
91+
92+
93+
# Load overrides once at module init
94+
_config = _load_config_overrides()
95+
96+
DANGEROUS_FILE_PATTERNS = _config.get(
97+
"dangerous_file_patterns", _DEFAULT_DANGEROUS_FILE_PATTERNS
98+
)
99+
DANGEROUS_COMMANDS = _config.get("dangerous_commands", _DEFAULT_DANGEROUS_COMMANDS)
100+
SAFE_PATH_PREFIXES = _config.get("safe_path_prefixes", _DEFAULT_SAFE_PATH_PREFIXES)
101+
102+
67103
def is_safe_path(path: str) -> bool:
68104
"""Check if path is in known safe directory."""
69105
return any(path.startswith(prefix) for prefix in SAFE_PATH_PREFIXES)

.map/scripts/map_step_runner.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,15 @@ def replace_active_issues(
299299
return {"status": "success", "path": str(issues_file), "count": len(issues)}
300300

301301

302+
def _sanitize_for_json(text: str) -> str:
303+
"""Remove control characters (U+0000-U+001F except \\n \\r \\t) that break JSON consumers.
304+
305+
Python's json.dumps escapes these correctly, but downstream tools
306+
(jq via bash pipes, shell variable expansion) can corrupt them.
307+
"""
308+
return re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", text)
309+
310+
302311
def build_handoff_bundle(branch: Optional[str] = None) -> dict:
303312
"""Build a compact handoff bundle from branch-scoped human artifacts."""
304313
branch_name = branch or get_branch_name()
@@ -307,7 +316,12 @@ def build_handoff_bundle(branch: Optional[str] = None) -> dict:
307316

308317
def read(name: str) -> str:
309318
path = branch_dir / name
310-
return path.read_text(encoding="utf-8") if path.exists() else ""
319+
if not path.exists():
320+
return ""
321+
try:
322+
return _sanitize_for_json(path.read_text(encoding="utf-8", errors="replace"))
323+
except OSError:
324+
return ""
311325

312326
verification = read("verification-summary.md")
313327
qa = read("qa-001.md")
@@ -362,7 +376,12 @@ def build_review_handoff(branch: Optional[str] = None) -> dict:
362376

363377
def read(name: str) -> str:
364378
path = branch_dir / name
365-
return path.read_text(encoding="utf-8") if path.exists() else ""
379+
if not path.exists():
380+
return ""
381+
try:
382+
return _sanitize_for_json(path.read_text(encoding="utf-8", errors="replace"))
383+
except OSError:
384+
return ""
366385

367386
plan_review_next = next_numbered_artifact_path("plan-review", branch_name)
368387
latest_plan_review_index = max(0, plan_review_next["index"] - 1)
@@ -1019,11 +1038,11 @@ def _run_git(args: list[str]) -> str:
10191038

10201039
elif func_name == "build_handoff_bundle":
10211040
result = build_handoff_bundle()
1022-
print(json.dumps(result, indent=2))
1041+
print(json.dumps(result, indent=2, ensure_ascii=True))
10231042

10241043
elif func_name == "build_review_handoff":
10251044
result = build_review_handoff()
1026-
print(json.dumps(result, indent=2))
1045+
print(json.dumps(result, indent=2, ensure_ascii=True))
10271046

10281047
elif func_name == "ensure_known_issues_file":
10291048
result = ensure_known_issues_file()

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ The orchestration lives in `.claude/commands/map-*.md` prompts created by `mapif
9393
| [Installation](docs/INSTALL.md) | All install methods, PATH setup, troubleshooting |
9494
| [Usage Guide](docs/USAGE.md) | Workflows, examples, cost optimization, playbook |
9595
| [Architecture](docs/ARCHITECTURE.md) | Agents, MCP integration, customization |
96+
| [Platform Spec](docs/MAP_PLATFORM_SPEC.md) | Platform refactor roadmap, codebase analysis |
9697

9798
## Trouble?
9899

0 commit comments

Comments
 (0)