Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 9 additions & 17 deletions .agents/skills/map-plan/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,26 +98,18 @@ shell_command:
- Outcome `map-fast`: recommend `$map-fast` and STOP.
- Outcome `map-plan`: continue below.

**Scale Advisory (standard mode only, when no `--light`/`--deep` flag was given):** After recording a `map-plan` outcome, run:
**Scale Advisory (standard mode only, when no `--light`/`--deep` flag was given):** After recording a `map-plan` outcome, estimate the task scope and run:

```
shell_command:
cmd: |
python3 -c "
import sys; sys.path.insert(0, 'src')
from pathlib import Path
from mapify_cli.config.project_config import load_map_config
cfg = load_map_config(Path('.'))
print('scale_auto:', cfg.scale_auto)
print('small:', cfg.scale_small_max_files, cfg.scale_small_max_lines)
print('medium:', cfg.scale_medium_max_files, cfg.scale_medium_max_lines)
"
```

If `scale_auto: true`, use your estimate of the task scope to surface an advisory (do not block):
- Estimated ≤ small thresholds → advise re-invoking as `$map-plan --light`.
- Estimated > medium thresholds → advise re-invoking as `$map-plan --deep`.
- Otherwise (medium range) → no advisory; standard mode is appropriate.
cmd: python3 .map/scripts/classify_scope.py --files ESTIMATED_FILES --lines ESTIMATED_LINES
```

If `auto_enabled: true` in the JSON result, surface an advisory based on `bracket` (do not block):
- `small` → advise re-invoking as `$map-plan --light`.
- `large` → advise re-invoking as `$map-plan --deep`.
- `trivial` → advise `$map-fast` instead.
- `medium` → no advisory; standard mode is appropriate.

---

Expand Down
23 changes: 7 additions & 16 deletions .claude/skills/map-plan/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,27 +120,18 @@ Outcomes:
- `map-wayfind`: too foggy to specify — you cannot write sharp acceptance criteria because several core decisions are still unresolved and entangled (not merely "needs a little research"). Recommend `/map-wayfind chart "<loose idea>"` to resolve the decision frontier first, then return with `/map-plan --wayfind <slug>`. STOP. (This is the inverse of the Wayfinding Handoff pre-flight: that consumes a finished map; this sends you to create one.)
- `map-plan`: continue.

**Scale Advisory (standard mode only, when no `--light`/`--deep` flag was given):** After recording a `map-plan` outcome, query the project's scale-adaptive config:
**Scale Advisory (standard mode only, when no `--light`/`--deep` flag was given):** After recording a `map-plan` outcome, estimate the task's scope (files to change, lines to add/modify) and run:

```bash
python3 -c "
import sys; sys.path.insert(0, 'src')
from pathlib import Path
from mapify_cli.config.project_config import load_map_config
cfg = load_map_config(Path('.'))
print('scale_auto:', cfg.scale_auto)
print('trivial:', cfg.scale_trivial_max_files, cfg.scale_trivial_max_lines)
print('small:', cfg.scale_small_max_files, cfg.scale_small_max_lines)
print('medium:', cfg.scale_medium_max_files, cfg.scale_medium_max_lines)
"
python3 .map/scripts/classify_scope.py --files ESTIMATED_FILES --lines ESTIMATED_LINES
```

If `scale_auto: true`, use your estimate of the task's scope to surface an advisory (do not block on this):
If `auto_enabled: true` in the JSON result, surface an advisory based on `bracket` (do not block):

- Estimated ≤ trivial thresholds → advise `/map-fast` instead (already covered by the `map-fast` outcome above).
- Estimated ≤ small thresholds → advise re-invoking as `/map-plan --light` for a lighter-weight plan.
- Estimated > medium thresholds → advise re-invoking as `/map-plan --deep` for full research + architecture review.
- Otherwise (medium range) → no advisory; standard mode is appropriate.
- `trivial` → advise `/map-fast` instead (already covered by the `map-fast` outcome above).
- `small` → advise re-invoking as `/map-plan --light` for a lighter-weight plan.
- `large` → advise re-invoking as `/map-plan --deep` for full research + architecture review.
- `medium` → no advisory; standard mode is appropriate.

The advisory is informational — the user's flags or your judgment prevail. If the user accepts, add the flag and restart; otherwise continue in standard mode.

Expand Down
144 changes: 144 additions & 0 deletions .map/scripts/classify_scope.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""classify_scope.py — Scale-adaptive scope classifier for MAP workflows (#287).

Reads scale thresholds from .map/config.yaml (dotted-key notation) and
classifies a change estimate into one of four brackets: trivial, small,
medium, or large, then returns the recommended MAP workflow depth.

Usage:
python3 .map/scripts/classify_scope.py --files 5 --lines 120

Output (JSON, exit 0):
{"bracket": "small", "recommended_workflow": "map-plan-light",
"estimated_files": 5, "estimated_lines": 120, "auto_enabled": true}

Config keys read from .map/config.yaml (dotted notation):
scale.auto (default: true)
scale.thresholds.trivial.max_files (default: 3)
scale.thresholds.trivial.max_lines (default: 50)
scale.thresholds.small.max_files (default: 10)
scale.thresholds.small.max_lines (default: 200)
scale.thresholds.medium.max_files (default: 30)
scale.thresholds.medium.max_lines (default: 1000)
"""

from __future__ import annotations

import argparse
import json
import re
import sys
from pathlib import Path


_BRACKET_WORKFLOW: dict[str, str] = {
"trivial": "map-fast",
"small": "map-plan-light",
"medium": "map-efficient",
"large": "map-efficient+map-tdd",
}

_DEFAULTS: dict[str, str] = {
"scale.auto": "true",
"scale.thresholds.trivial.max_files": "3",
"scale.thresholds.trivial.max_lines": "50",
"scale.thresholds.small.max_files": "10",
"scale.thresholds.small.max_lines": "200",
"scale.thresholds.medium.max_files": "30",
"scale.thresholds.medium.max_lines": "1000",
}


def _read_config(project_dir: Path) -> dict[str, str]:
"""Read .map/config.yaml scalar values without external dependencies.

Supports the same dotted-key YAML subset used by load_map_config().
"""
config_path = project_dir / ".map" / "config.yaml"
if not config_path.is_file():
return {}
try:
lines = config_path.read_text(encoding="utf-8").splitlines()
except OSError:
return {}
values: dict[str, str] = {}
for raw in lines:
line = raw.strip()
if not line or line.startswith("#") or ":" not in line:
continue
key, value = line.split(":", 1)
key = key.strip()
if not key or not re.fullmatch(r"[A-Za-z0-9_.]+", key):
continue
value = value.split("#", 1)[0].strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
value = value[1:-1]
values[key] = value
return values


def classify(
estimated_files: int,
estimated_lines: int,
project_dir: Path | None = None,
) -> dict[str, object]:
"""Classify scope and return a result dict matching ScopeClassification fields."""
raw = _read_config(project_dir or Path("."))

def _int(key: str) -> int:
return int(raw.get(key, _DEFAULTS[key]))

def _bool(key: str) -> bool:
return raw.get(key, _DEFAULTS[key]).lower() not in {"false", "0", "no"}

auto = _bool("scale.auto")
trivial_files = _int("scale.thresholds.trivial.max_files")
trivial_lines = _int("scale.thresholds.trivial.max_lines")
small_files = _int("scale.thresholds.small.max_files")
small_lines = _int("scale.thresholds.small.max_lines")
medium_files = _int("scale.thresholds.medium.max_files")
medium_lines = _int("scale.thresholds.medium.max_lines")

if estimated_files <= trivial_files and estimated_lines <= trivial_lines:
bracket = "trivial"
elif estimated_files <= small_files and estimated_lines <= small_lines:
bracket = "small"
elif estimated_files <= medium_files and estimated_lines <= medium_lines:
bracket = "medium"
else:
bracket = "large"

return {
"bracket": bracket,
"recommended_workflow": _BRACKET_WORKFLOW[bracket],
"estimated_files": estimated_files,
"estimated_lines": estimated_lines,
"auto_enabled": auto,
}


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Classify MAP workflow scope from estimated change size.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument("--files", type=int, required=True,
help="Estimated number of files to change (>= 0)")
parser.add_argument("--lines", type=int, required=True,
help="Estimated lines to add/modify (>= 0)")
parser.add_argument("--project-dir", default=".",
help="Project root (default: current directory)")
args = parser.parse_args(argv)

if args.files < 0 or args.lines < 0:
print("error: --files and --lines must be >= 0", file=sys.stderr)
return 1

result = classify(args.files, args.lines, Path(args.project_dir))
print(json.dumps(result))
return 0


if __name__ == "__main__":
sys.exit(main())
20 changes: 19 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ LLM cannot fabricate input — the same layered-defense posture MAP uses elsewhe
- **Token Budget Decisions**: `.map/<branch>/token_budget.json` records active prompt-path budget decisions from Actor context and review prompt builders only; it does not log dormant REGISTRY/FOCUS experiments. Each entry names the prompt path, configured budget, estimated tokens before/after enforcement, clipped sections, and source artifacts.
- **Constraint Typing**: `blueprint.json` separates non-negotiable `hard_constraints` from negotiable `soft_constraints`; hard constraints must be covered through `coverage_map` and bracketed validation criteria, while soft constraints need either coverage or explicit tradeoff rationale.
- **Provider Differences**: Workflow intent is shared, but orchestration mechanics differ between Claude Code (`.claude/`) and Codex CLI (`.codex/`).
- **Scale-Adaptive Intelligence** (`src/mapify_cli/scope_classifier.py`, issue #287): deterministic, no-LLM classification of an estimated change into one of four scale brackets that routes to the appropriate MAP workflow depth. `classify_scope(estimated_files, estimated_lines, *, config)` returns a frozen `ScopeClassification` carrying `bracket` (a `ScopeBracket` enum), `recommended_workflow`, `estimated_files`, `estimated_lines`, and `auto_enabled`. Four brackets with default inclusive-upper-bound thresholds: TRIVIAL (≤3 files AND ≤50 lines → `map-fast`), SMALL (≤10/200 → `map-plan-light`), MEDIUM (≤30/1000 → `map-efficient`), LARGE (otherwise → `map-efficient+map-tdd`). Thresholds and the `scale.auto` toggle are read from `MapConfig` (7 new `scale_*` fields) and surfaced in `.map/config.yaml` via dotted-key aliases (`scale.auto`, `scale.thresholds.{trivial,small,medium}.{max_files,max_lines}`). `generate_default_config()` documents the full `scale` section (commented-out defaults). Remaining slices (#287): Jinja skill-template integration for `--light`/`--deep` flags, `--force-full`/`--force-fast` overrides, and entry-point calls that surface the recommendation at workflow start.
- **Scale-Adaptive Intelligence** (`src/mapify_cli/scope_classifier.py`, issue #287, **COMPLETE** PRs #366/#368/#369): deterministic, no-LLM classification of an estimated change into one of four scale brackets that routes to the appropriate MAP workflow depth. `classify_scope(estimated_files, estimated_lines, *, config)` returns a frozen `ScopeClassification` (bracket, recommended_workflow, estimated_files, estimated_lines, auto_enabled). Brackets: TRIVIAL (≤3f/50l → `map-fast`), SMALL (≤10/200 → `map-plan-light`), MEDIUM (≤30/1000 → `map-efficient`), LARGE (otherwise → `map-efficient+map-tdd`). Thresholds and `scale.auto` read from `MapConfig` (7 `scale_*` fields) via dotted-key aliases in `.map/config.yaml`. `map-plan` skill (both Claude and Codex variants) integrates a **Scale Advisory** block that calls `python3 .map/scripts/classify_scope.py --files N --lines N` after workflow-fit gate; result is JSON with bracket/recommended_workflow/auto_enabled. The `classify_scope.py` script ships via `mapify init` to `.map/scripts/`, is stdlib-only (no mapify_cli import), reads `.map/config.yaml` directly, and validates `--files`/`--lines` ≥ 0. The `map-plan` skill also ships `--light` (2-5 minimal subtasks, no discovery/spec-review/architecture-review), `--deep` (extended discovery, architecture review via monitor agent in Step 4.5), `--force-full` (alias for --deep), and `--force-fast` (recommend map-fast and stop) modes; both Claude (Task syntax) and Codex (spawn_agent syntax) variants updated.

### Prompt Layering & Prefix Caching (#231)

Expand Down Expand Up @@ -2558,6 +2558,24 @@ cat .map/workflow_logs/feat_auth_20251023_143022.json | jq '.subtasks[].agents.e

---

## Open Issues (for next session)

All remaining open issues are enhancements (no bugs as of 2026-07-18). Prioritized by concreteness:

### #291 — SpecKit-style preset composition (layered template resolution)
Adds `.map/presets/` directory, 4-tier resolution (project → preset → extension → core), composition strategies (prepend/append/wrap/replace), `mapify preset add/remove/enable/disable/list/resolve` commands, and extension hook lifecycle. First slice: directory structure, YAML manifest format, and `mapify preset list`. Key constraint: must not bypass the `make check-render` single-source invariant — presets should compose at render time, not by editing generated trees.

### #363 — Architecture deepening report (`/map-architecture` skill)
New opt-in skill that ranks codebase areas by recent git hotspot + design friction, generates a candidate report (HTML or Markdown+Mermaid under `.map/<branch>/architecture-report/`), and holds off implementation until the user picks a candidate. First slice: create `src/mapify_cli/templates_src/skills/map-architecture/SKILL.md.jinja` + register in skill-rules.json. No Python code in slice 1 — just the workflow instructions.

### #353 — Eval-gated prompt profile canary and rollback
Local-first prompt lifecycle: versioned profiles under `.map/prompt-profiles/`, `active.json` pointer, `mapify prompt-profile list/diff/activate/rollback` commands. Integrates with skill-eval (trigger changes) and trajectory eval (#351, for body changes). First slice: manifest format + `mapify prompt-profile list`. Key constraint: do not edit generated trees as the durable authoring source.

### #339 — GRACE semantic code-contract anchor eval
Eval harness comparing inline code contracts vs prompt-injected vs no-anchor variants for bug-fix workflows. Variants: baseline, inline (LEX/MIN), prompt-injected (INJ), stale (LIE). First slice: fixture structure + report schema (JSON). No external model calls required for slice 1.

---

## References

- [MAP Paper - Nature Communications](https://github.com/Shanka123/MAP)
Expand Down
Loading