Skip to content
6 changes: 4 additions & 2 deletions .claude/commands/map-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,8 +474,8 @@ Use the **Write** tool to create `.map/<branch>/step_state.json` with this struc
"_semantic_tag": "MAP_State_v1_0",
"workflow": "map-plan",
"started_at": "<current UTC timestamp in ISO 8601>",
"current_subtask": null,
"current_state": "INITIALIZED",
"current_subtask_id": null,
"current_step_phase": "INITIALIZED",
"completed_steps": [],
"pending_steps": [],
"subtask_sequence": ["ST-001", "ST-002", "ST-003"],
Expand All @@ -492,6 +492,8 @@ Use the **Write** tool to create `.map/<branch>/step_state.json` with this struc
}
```

**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.

**IMPORTANT:**
- Replace `subtask_sequence` with actual IDs from the decomposition
- Populate `aag_contracts` map with each subtask's AAG contract from the decomposer output — executors read this to set context for each subtask
Expand Down
6 changes: 5 additions & 1 deletion .claude/commands/map-tdd.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,14 @@ Verify that a plan or spec exists for this branch:
echo "spec: $(test -f .map/${BRANCH}/spec_${BRANCH}.md && echo EXISTS || echo MISSING)"
echo "task_plan: $(test -f .map/${BRANCH}/task_plan_${BRANCH}.md && echo EXISTS || echo MISSING)"
echo "step_state: $(test -f .map/${BRANCH}/step_state.json && echo EXISTS || echo MISSING)"
if [ -f ".map/${BRANCH}/step_state.json" ]; then
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')))")"
fi
```

- If **no spec and no task_plan**: Run `/map-plan` first. TDD requires clear acceptance criteria.
- If **step_state.json EXISTS**: Resume from checkpoint (same as /map-efficient resume logic).
- 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.
- 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`.
- If **task_plan EXISTS but no step_state**: Run `python3 .map/scripts/map_orchestrator.py resume_from_plan` then enable TDD mode.

### Enable TDD Mode (full workflow only)
Expand Down
42 changes: 39 additions & 3 deletions .claude/hooks/safety-guardrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@
"""

import json
import os
import re
import sys
from pathlib import Path

# =============================================================================
# Default constants (overridable via .map/config.yaml → safe_path_prefixes)
# =============================================================================

# Dangerous file patterns (case-insensitive)
DANGEROUS_FILE_PATTERNS = [
_DEFAULT_DANGEROUS_FILE_PATTERNS = [
r"\.env($|\.)", # .env, .env.local, .env.production
r"credentials",
r"private[_-]?key",
Expand All @@ -31,7 +37,7 @@
]

# Dangerous bash command patterns
DANGEROUS_COMMANDS = [
_DEFAULT_DANGEROUS_COMMANDS = [
r"rm\s+-rf\s+/", # rm -rf /
r"rm\s+-rf\s+\*", # rm -rf *
r"rm\s+-rf\s+\.\.", # rm -rf ..
Expand All @@ -46,7 +52,7 @@
]

# Safe path prefixes (skip checks for known safe directories)
SAFE_PATH_PREFIXES = [
_DEFAULT_SAFE_PATH_PREFIXES = [
"src/",
"lib/",
"test/",
Expand All @@ -64,6 +70,36 @@
]


def _load_config_overrides() -> dict:
"""Load overrides from .map/config.yaml if it exists.

Reads safe_path_prefixes from project config to allow customization.
Falls back to defaults when config is missing or unreadable.
"""
project_dir = Path(os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd()))
config_path = project_dir / ".map" / "config.yaml"
if not config_path.exists():
return {}
try:
import yaml # type: ignore[import-untyped]

with open(config_path) as f:
data = yaml.safe_load(f)
return data if isinstance(data, dict) else {}
except Exception:
return {}


# Load overrides once at module init
_config = _load_config_overrides()

DANGEROUS_FILE_PATTERNS = _config.get(
"dangerous_file_patterns", _DEFAULT_DANGEROUS_FILE_PATTERNS
)
DANGEROUS_COMMANDS = _config.get("dangerous_commands", _DEFAULT_DANGEROUS_COMMANDS)
SAFE_PATH_PREFIXES = _config.get("safe_path_prefixes", _DEFAULT_SAFE_PATH_PREFIXES)


def is_safe_path(path: str) -> bool:
"""Check if path is in known safe directory."""
return any(path.startswith(prefix) for prefix in SAFE_PATH_PREFIXES)
Comment on lines +96 to 105
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Config overrides are used without validating types. If .map/config.yaml sets safe_path_prefixes to a string (or contains non-string entries), any(path.startswith(prefix) for prefix in SAFE_PATH_PREFIXES) will iterate characters / raise TypeError, which can either break the hook or unintentionally mark broad paths as safe. Please validate/coerce overrides (e.g., ensure these are list[str], else fall back to defaults) for safe_path_prefixes, dangerous_file_patterns, and dangerous_commands before assigning the module-level constants.

Copilot uses AI. Check for mistakes.
Expand Down
27 changes: 23 additions & 4 deletions .map/scripts/map_step_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,15 @@ def replace_active_issues(
return {"status": "success", "path": str(issues_file), "count": len(issues)}


def _sanitize_for_json(text: str) -> str:
"""Remove control characters (U+0000-U+001F except \\n \\r \\t) that break JSON consumers.

Python's json.dumps escapes these correctly, but downstream tools
(jq via bash pipes, shell variable expansion) can corrupt them.
"""
return re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", text)


def build_handoff_bundle(branch: Optional[str] = None) -> dict:
"""Build a compact handoff bundle from branch-scoped human artifacts."""
branch_name = branch or get_branch_name()
Expand All @@ -307,7 +316,12 @@ def build_handoff_bundle(branch: Optional[str] = None) -> dict:

def read(name: str) -> str:
path = branch_dir / name
return path.read_text(encoding="utf-8") if path.exists() else ""
if not path.exists():
return ""
try:
return _sanitize_for_json(path.read_text(encoding="utf-8", errors="replace"))
except OSError:
return ""

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

def read(name: str) -> str:
path = branch_dir / name
return path.read_text(encoding="utf-8") if path.exists() else ""
if not path.exists():
return ""
try:
return _sanitize_for_json(path.read_text(encoding="utf-8", errors="replace"))
except OSError:
return ""

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

elif func_name == "build_handoff_bundle":
result = build_handoff_bundle()
print(json.dumps(result, indent=2))
print(json.dumps(result, indent=2, ensure_ascii=True))

elif func_name == "build_review_handoff":
result = build_review_handoff()
print(json.dumps(result, indent=2))
print(json.dumps(result, indent=2, ensure_ascii=True))

elif func_name == "ensure_known_issues_file":
result = ensure_known_issues_file()
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ The orchestration lives in `.claude/commands/map-*.md` prompts created by `mapif
| [Installation](docs/INSTALL.md) | All install methods, PATH setup, troubleshooting |
| [Usage Guide](docs/USAGE.md) | Workflows, examples, cost optimization, playbook |
| [Architecture](docs/ARCHITECTURE.md) | Agents, MCP integration, customization |
| [Platform Spec](docs/MAP_PLATFORM_SPEC.md) | Platform refactor roadmap, codebase analysis |

## Trouble?

Expand Down
Loading
Loading