mapify-cli 3.19.0__py3-none-any.whl → 3.20.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mapify_cli/__init__.py +1 -1
- mapify_cli/config/project_config.py +11 -0
- mapify_cli/templates/agents/actor.md +1 -0
- mapify_cli/templates/hooks/README.md +2 -1
- mapify_cli/templates/hooks/scrub-internal-ids.py +173 -0
- mapify_cli/templates/map/scripts/scrub_internal_ids.py +536 -0
- mapify_cli/templates/references/hook-patterns.md +1 -0
- mapify_cli/templates/settings.json +11 -0
- mapify_cli/templates_src/agents/actor.md.jinja +1 -0
- mapify_cli/templates_src/hooks/README.md.jinja +2 -1
- mapify_cli/templates_src/hooks/scrub-internal-ids.py.jinja +173 -0
- mapify_cli/templates_src/map/scripts/scrub_internal_ids.py.jinja +536 -0
- mapify_cli/templates_src/references/hook-patterns.md.jinja +1 -0
- mapify_cli/templates_src/settings.json.jinja +11 -0
- {mapify_cli-3.19.0.dist-info → mapify_cli-3.20.0.dist-info}/METADATA +1 -1
- {mapify_cli-3.19.0.dist-info → mapify_cli-3.20.0.dist-info}/RECORD +19 -15
- {mapify_cli-3.19.0.dist-info → mapify_cli-3.20.0.dist-info}/WHEEL +0 -0
- {mapify_cli-3.19.0.dist-info → mapify_cli-3.20.0.dist-info}/entry_points.txt +0 -0
- {mapify_cli-3.19.0.dist-info → mapify_cli-3.20.0.dist-info}/licenses/LICENSE +0 -0
mapify_cli/__init__.py
CHANGED
|
@@ -122,6 +122,12 @@ class MapConfig:
|
|
|
122
122
|
# .map/config.yaml. When enabled, the map-so-search skill is available.
|
|
123
123
|
sofa_enabled: bool = False
|
|
124
124
|
|
|
125
|
+
# Strip MAP-internal workflow IDs (ST-/AC-/VC-/INV-/HC-) from run-changed
|
|
126
|
+
# code at workflow completion (Stop hook `scrub-internal-ids.py`). On by
|
|
127
|
+
# default; set `scrub_internal_ids: false` in .map/config.yaml to opt out
|
|
128
|
+
# and keep the IDs the framework wrote into comments/strings/test names.
|
|
129
|
+
scrub_internal_ids: bool = True
|
|
130
|
+
|
|
125
131
|
|
|
126
132
|
def load_map_config(project_path: Path) -> MapConfig:
|
|
127
133
|
"""Load MAP config from .map/config.yaml with fallback to defaults.
|
|
@@ -384,6 +390,11 @@ minimality: lite
|
|
|
384
390
|
# Stack Overflow for Agents (SOFA) integration — opt-in, off by default.
|
|
385
391
|
# Enable via `mapify init --sofa` or uncomment the line below.
|
|
386
392
|
# sofa.enabled: false
|
|
393
|
+
|
|
394
|
+
# Strip MAP-internal workflow IDs (ST-/AC-/VC-/INV-/HC-) from the code a run
|
|
395
|
+
# changed, at workflow completion (Stop hook). On by default; uncomment and set
|
|
396
|
+
# to false to keep the IDs the framework wrote into comments/strings/test names.
|
|
397
|
+
# scrub_internal_ids: true
|
|
387
398
|
"""
|
|
388
399
|
|
|
389
400
|
|
|
@@ -776,6 +776,7 @@ Follow this protocol exactly — do not infer "how seniors write" or add stylist
|
|
|
776
776
|
4. **Intent comments**: Add a one-line `# Intent: <why>` comment above any non-obvious logic block. Do NOT comment obvious code.
|
|
777
777
|
5. **Performance**: Clarity first, optimize only if proven necessary.
|
|
778
778
|
6. **Imports**: Group by stdlib → third-party → local. One blank line between groups.
|
|
779
|
+
7. **No internal workflow IDs in comments or strings**: NEVER write MAP-internal workflow identifiers — subtask `ST-001`, acceptance criteria `AC-3`, verification criteria `VC1`, invariants `INV-7`, hard constraints `HC-1` — into shipped code comments or string literals. They are workflow scaffolding, not user-facing documentation. State the *reason* without the ID (`# enforce single-writer invariant`, not `# INV-7 single writer`). The one exception is the transient `test_vc<n>_*` test-naming aid described above: keep it during the run — the framework strips the `vc<n>` segment from shipped tests automatically at completion.
|
|
779
780
|
|
|
780
781
|
## Error Handling Patterns
|
|
781
782
|
|
|
@@ -86,7 +86,7 @@ documented per event in the official Claude Code docs.
|
|
|
86
86
|
|
|
87
87
|
## Hook inventory
|
|
88
88
|
|
|
89
|
-
All
|
|
89
|
+
All 16 hooks (15 `.py` + `end-of-turn.sh`) are classified against the
|
|
90
90
|
`MAP_INVOKED_BY` recursion-guard contract. **REQUIRE_GUARD** hooks early-exit
|
|
91
91
|
when MAP spawns a nested subprocess; **FORBID_GUARD** hooks must always fire
|
|
92
92
|
and may not carry the guard. Full contract and per-hook rationale:
|
|
@@ -107,6 +107,7 @@ classification is enforced by `scripts/lint-hooks.py` (in `make lint` /
|
|
|
107
107
|
| `pre-compact-save-transcript.py` | `PreCompact` | No | REQUIRE_GUARD | Save full conversation transcript |
|
|
108
108
|
| `detect-clarification-triggers.py` | `UserPromptSubmit` | No | REQUIRE_GUARD | Detect "ask if unclear" + async/durability language |
|
|
109
109
|
| `end-of-turn.sh` | `Stop` | No | REQUIRE_GUARD | Auto-fix lint/format silently |
|
|
110
|
+
| `scrub-internal-ids.py` | `Stop` | No | REQUIRE_GUARD | On `WORKFLOW_COMPLETE`, strip leaked `ST-`/`AC-`/`VC-`/`INV-`/`HC-` IDs from run-changed code and commit the cleanup (gated, runs once) |
|
|
110
111
|
| `map-memory-capture.py` | `Stop` | No | REQUIRE_GUARD | Append per-turn scratch WAL record (cross-session memory) |
|
|
111
112
|
| `map-memory-endmark.py` | `SessionEnd` | No | REQUIRE_GUARD | Best-effort 'ended' marker for the session WAL |
|
|
112
113
|
| `map-memory-finalize.py` | `SessionStart` | No | REQUIRE_GUARD | Finalize prior dirty session scratches into digests (claude -p) |
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Strip MAP-internal workflow IDs from shipped code — Stop hook.
|
|
3
|
+
|
|
4
|
+
Runs when the main agent finishes a turn (Stop event). It is a thin, strictly
|
|
5
|
+
gated wrapper: the deterministic work lives in ``.map/scripts/scrub_internal_ids
|
|
6
|
+
.py`` (stdlib-only, importable-free, identical in this repo and installed
|
|
7
|
+
projects — same split as ``map-token-meter`` / ``map_step_runner``).
|
|
8
|
+
|
|
9
|
+
Why a Stop hook and not a skill step: the executor must not depend on the agent
|
|
10
|
+
remembering to call a command at close. The harness fires Stop deterministically;
|
|
11
|
+
the hook decides for itself whether the run is done.
|
|
12
|
+
|
|
13
|
+
Gating (no-op in ~all turns):
|
|
14
|
+
1. ``MAP_INVOKED_BY`` set -> exit (don't run inside a sub-agent).
|
|
15
|
+
2. no ``.map/<branch>/step_state.json`` OR ``workflow_status`` is not
|
|
16
|
+
``WORKFLOW_COMPLETE`` -> exit (run not finished).
|
|
17
|
+
3. marker ``.map/<branch>/.scrub_done`` present -> exit (already ran once).
|
|
18
|
+
4. ``scrub_internal_ids: false`` in ``.map/config.yaml`` -> exit (opt-out).
|
|
19
|
+
|
|
20
|
+
When it does run: calls the engine in ``clean`` mode, commits the resulting
|
|
21
|
+
working-tree changes as a dedicated ``chore(map): strip internal workflow IDs``
|
|
22
|
+
commit (never amends), and writes the marker so it fires exactly once.
|
|
23
|
+
|
|
24
|
+
Synchronous by design (justified exception to the async-hook rule): the scrub
|
|
25
|
+
must finish and commit before the run is considered done; a detached run would
|
|
26
|
+
race the close/PR and could land after it. The work is fast — scoped to the
|
|
27
|
+
run's git diff, like ``end-of-turn.sh``.
|
|
28
|
+
|
|
29
|
+
Exit codes: always 0. The scrub is advisory and must never block a turn.
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import json
|
|
34
|
+
import os
|
|
35
|
+
import re
|
|
36
|
+
import subprocess
|
|
37
|
+
import sys
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import NoReturn
|
|
40
|
+
|
|
41
|
+
PROJECT_DIR = Path(os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd()))
|
|
42
|
+
ENGINE = PROJECT_DIR / ".map" / "scripts" / "scrub_internal_ids.py"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _silent() -> NoReturn:
|
|
46
|
+
sys.stdout.write("{}")
|
|
47
|
+
sys.exit(0)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _sanitize_branch(branch: str) -> str:
|
|
51
|
+
sanitized = branch.replace("/", "-")
|
|
52
|
+
sanitized = re.sub(r"[^a-zA-Z0-9_.-]", "-", sanitized)
|
|
53
|
+
sanitized = re.sub(r"-+", "-", sanitized).strip("-")
|
|
54
|
+
if ".." in sanitized or sanitized.startswith("."):
|
|
55
|
+
return "default"
|
|
56
|
+
return sanitized or "default"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_branch() -> str:
|
|
60
|
+
try:
|
|
61
|
+
result = subprocess.run(
|
|
62
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
63
|
+
capture_output=True, text=True, cwd=PROJECT_DIR, timeout=2,
|
|
64
|
+
)
|
|
65
|
+
if result.returncode == 0:
|
|
66
|
+
return _sanitize_branch(result.stdout.strip())
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
return "default"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _scrub_enabled(branch_dir: Path) -> bool:
|
|
73
|
+
"""Honor the ``scrub_internal_ids`` opt-out in .map/config.yaml (default on).
|
|
74
|
+
|
|
75
|
+
Minimal stdlib parse — PyYAML is not guaranteed in installed projects.
|
|
76
|
+
"""
|
|
77
|
+
config = PROJECT_DIR / ".map" / "config.yaml"
|
|
78
|
+
if not config.exists():
|
|
79
|
+
return True
|
|
80
|
+
try:
|
|
81
|
+
text = config.read_text(encoding="utf-8")
|
|
82
|
+
except OSError:
|
|
83
|
+
return True
|
|
84
|
+
m = re.search(r"^\s*scrub[._]internal_ids\s*:\s*(\S+)", text, re.MULTILINE)
|
|
85
|
+
if m and m.group(1).strip().lower() in ("false", "no", "off", "0"):
|
|
86
|
+
return False
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main() -> None:
|
|
91
|
+
if os.environ.get("MAP_INVOKED_BY"):
|
|
92
|
+
sys.exit(0)
|
|
93
|
+
# Stop payload is read defensively but not required.
|
|
94
|
+
try:
|
|
95
|
+
json.load(sys.stdin)
|
|
96
|
+
except (json.JSONDecodeError, ValueError):
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
branch = _get_branch()
|
|
100
|
+
branch_dir = PROJECT_DIR / ".map" / branch
|
|
101
|
+
state_file = branch_dir / "step_state.json"
|
|
102
|
+
if not state_file.exists():
|
|
103
|
+
_silent()
|
|
104
|
+
try:
|
|
105
|
+
state = json.loads(state_file.read_text(encoding="utf-8"))
|
|
106
|
+
except (json.JSONDecodeError, OSError):
|
|
107
|
+
_silent()
|
|
108
|
+
|
|
109
|
+
status = str(state.get("workflow_status") or "").strip().upper()
|
|
110
|
+
if status != "WORKFLOW_COMPLETE":
|
|
111
|
+
_silent()
|
|
112
|
+
|
|
113
|
+
marker = branch_dir / ".scrub_done"
|
|
114
|
+
if marker.exists():
|
|
115
|
+
_silent()
|
|
116
|
+
if not _scrub_enabled(branch_dir):
|
|
117
|
+
marker.write_text("disabled\n", encoding="utf-8")
|
|
118
|
+
_silent()
|
|
119
|
+
if not ENGINE.exists():
|
|
120
|
+
_silent()
|
|
121
|
+
|
|
122
|
+
# Run the engine. MAP_INVOKED_BY guards any nested hook from re-entering.
|
|
123
|
+
env = {**os.environ, "MAP_INVOKED_BY": "scrub-internal-ids"}
|
|
124
|
+
try:
|
|
125
|
+
proc = subprocess.run(
|
|
126
|
+
[sys.executable, str(ENGINE), "clean", "--branch", branch],
|
|
127
|
+
capture_output=True, text=True, cwd=PROJECT_DIR, env=env, timeout=120,
|
|
128
|
+
)
|
|
129
|
+
except (OSError, subprocess.SubprocessError):
|
|
130
|
+
_silent()
|
|
131
|
+
|
|
132
|
+
report = {}
|
|
133
|
+
try:
|
|
134
|
+
report = json.loads(proc.stdout)
|
|
135
|
+
except (json.JSONDecodeError, ValueError):
|
|
136
|
+
report = {}
|
|
137
|
+
|
|
138
|
+
modified = report.get("files_modified") or []
|
|
139
|
+
if modified:
|
|
140
|
+
try:
|
|
141
|
+
subprocess.run(["git", "add", "--", *modified], cwd=PROJECT_DIR,
|
|
142
|
+
env=env, capture_output=True, text=True, timeout=15)
|
|
143
|
+
subprocess.run(
|
|
144
|
+
["git", "commit", "-m", "chore(map): strip internal workflow IDs"],
|
|
145
|
+
cwd=PROJECT_DIR, env=env, capture_output=True, text=True, timeout=30,
|
|
146
|
+
)
|
|
147
|
+
except (OSError, subprocess.SubprocessError):
|
|
148
|
+
pass # leave the cleaned working tree in place; never block
|
|
149
|
+
|
|
150
|
+
# Mark done so the scrub fires exactly once for this completed run.
|
|
151
|
+
try:
|
|
152
|
+
marker.write_text(
|
|
153
|
+
json.dumps({
|
|
154
|
+
"files_modified": modified,
|
|
155
|
+
"tokens_removed": report.get("tokens_removed", 0),
|
|
156
|
+
"tests_renamed": report.get("tests_renamed", []),
|
|
157
|
+
"residual": report.get("residual", []),
|
|
158
|
+
}) + "\n",
|
|
159
|
+
encoding="utf-8",
|
|
160
|
+
)
|
|
161
|
+
except OSError:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
if report.get("residual"):
|
|
165
|
+
sys.stderr.write(
|
|
166
|
+
"[scrub-internal-ids] could not auto-remove some internal IDs; "
|
|
167
|
+
f"see {marker} (residual list).\n"
|
|
168
|
+
)
|
|
169
|
+
_silent()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
main()
|