mapify-cli 3.31.4__py3-none-any.whl → 3.33.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/skills_eval/apply_patcher.py +252 -213
- mapify_cli/templates/agents/monitor.md +21 -0
- mapify_cli/templates/codex/agents/monitor.toml +34 -2
- mapify_cli/templates/codex/hooks/context-meter.py +3 -3
- mapify_cli/templates/codex/hooks/detect-clarification-triggers.py +3 -3
- mapify_cli/templates/codex/hooks/end-of-turn.sh +11 -3
- mapify_cli/templates/codex/hooks/map-memory-capture.py +3 -3
- mapify_cli/templates/codex/hooks/map-memory-endmark.py +3 -3
- mapify_cli/templates/codex/hooks/map-memory-recall.py +3 -3
- mapify_cli/templates/codex/hooks/map-memory-session.py +3 -3
- mapify_cli/templates/codex/hooks/map-stop.py +3 -3
- mapify_cli/templates/codex/hooks/map-token-meter.py +3 -3
- mapify_cli/templates/codex/hooks/post-compact-context.py +3 -3
- mapify_cli/templates/codex/hooks/pre-compact-save-transcript.py +3 -3
- mapify_cli/templates/codex/hooks/ralph-context-pruner.py +3 -3
- mapify_cli/templates/codex/hooks/ralph-iteration-logger.py +3 -3
- mapify_cli/templates/codex/hooks/safety-guardrails.py +3 -3
- mapify_cli/templates/codex/hooks/scrub-internal-ids.py +3 -3
- mapify_cli/templates/codex/hooks/workflow-context-injector.py +3 -3
- mapify_cli/templates/codex/hooks/workflow-gate.py +86 -5
- mapify_cli/templates/codex/skills/map-efficient/SKILL.md +54 -3
- mapify_cli/templates/codex/skills/map-efficient/efficient-reference.md +71 -23
- mapify_cli/templates/codex/skills/map-explain/SKILL.md +3 -1
- mapify_cli/templates/codex/skills/map-learn/SKILL.md +5 -0
- mapify_cli/templates/codex/skills/map-memory-now/SKILL.md +8 -8
- mapify_cli/templates/codex/skills/map-plan/SKILL.md +12 -0
- mapify_cli/templates/codex/skills/map-release/SKILL.md +23 -30
- mapify_cli/templates/codex/skills/map-release/release-reference.md +145 -44
- mapify_cli/templates/codex/skills/map-resume/SKILL.md +8 -0
- mapify_cli/templates/codex/skills/map-resume/resume-reference.md +83 -0
- mapify_cli/templates/codex/skills/map-review/SKILL.md +542 -256
- mapify_cli/templates/codex/skills/map-review/adversarial-reference.md +87 -128
- mapify_cli/templates/codex/skills/map-review/review-reference.md +243 -335
- mapify_cli/templates/codex/skills/map-skill-eval/SKILL.md +1 -1
- mapify_cli/templates/codex/skills/map-wayfind/SKILL.md +20 -8
- mapify_cli/templates/codex/skills/map-wayfind/wayfind-reference.md +131 -3
- mapify_cli/templates/hooks/context-meter.py +3 -3
- mapify_cli/templates/hooks/detect-clarification-triggers.py +3 -3
- mapify_cli/templates/hooks/end-of-turn.sh +11 -3
- mapify_cli/templates/hooks/map-memory-capture.py +3 -3
- mapify_cli/templates/hooks/map-memory-endmark.py +3 -3
- mapify_cli/templates/hooks/map-memory-finalize.py +3 -3
- mapify_cli/templates/hooks/map-memory-recall.py +3 -3
- mapify_cli/templates/hooks/map-statusline.py +3 -3
- mapify_cli/templates/hooks/map-token-meter.py +3 -3
- mapify_cli/templates/hooks/post-compact-context.py +3 -3
- mapify_cli/templates/hooks/pre-compact-save-transcript.py +3 -3
- mapify_cli/templates/hooks/ralph-context-pruner.py +3 -3
- mapify_cli/templates/hooks/ralph-iteration-logger.py +3 -3
- mapify_cli/templates/hooks/safety-guardrails.py +3 -3
- mapify_cli/templates/hooks/scrub-internal-ids.py +3 -3
- mapify_cli/templates/hooks/workflow-context-injector.py +3 -3
- mapify_cli/templates/hooks/workflow-gate.py +90 -6
- mapify_cli/templates/map/scripts/map_interruption.py +981 -0
- mapify_cli/templates/map/scripts/map_orchestrator.py +224 -9
- mapify_cli/templates/map/scripts/map_step_runner.py +979 -289
- mapify_cli/templates/map/scripts/validate_spec_citations.py +3 -3
- mapify_cli/templates/map/scripts/wayfind_runner.py +575 -4
- mapify_cli/templates/skills/map-efficient/SKILL.md +5 -5
- mapify_cli/templates/skills/map-efficient/efficient-reference.md +123 -18
- mapify_cli/templates/skills/map-explain/SKILL.md +1 -1
- mapify_cli/templates/skills/map-memory-now/SKILL.md +8 -8
- mapify_cli/templates/skills/map-plan/SKILL.md +2 -0
- mapify_cli/templates/skills/map-release/SKILL.md +22 -29
- mapify_cli/templates/skills/map-release/release-reference.md +138 -52
- mapify_cli/templates/skills/map-resume/SKILL.md +8 -0
- mapify_cli/templates/skills/map-resume/resume-reference.md +83 -0
- mapify_cli/templates/skills/map-review/SKILL.md +67 -67
- mapify_cli/templates/skills/map-review/adversarial-reference.md +39 -41
- mapify_cli/templates/skills/map-review/review-reference.md +48 -32
- mapify_cli/templates/skills/map-wayfind/SKILL.md +19 -7
- mapify_cli/templates/skills/map-wayfind/wayfind-reference.md +131 -3
- mapify_cli/templates_src/_partials/monitor-review-context.md.jinja +19 -0
- mapify_cli/templates_src/_partials/python-version-guard.py.jinja +3 -3
- mapify_cli/templates_src/_partials/serial-blocker-repair.md.jinja +49 -0
- mapify_cli/templates_src/_partials/stale-attribution-recovery.md.jinja +30 -0
- mapify_cli/templates_src/agents/monitor.md.jinja +2 -0
- mapify_cli/templates_src/codex/agents/monitor.toml.jinja +15 -2
- mapify_cli/templates_src/codex/hooks/workflow-gate.py.jinja +83 -2
- mapify_cli/templates_src/codex/skills/map-auto/auto-reference.md.jinja +1 -78
- mapify_cli/templates_src/codex/skills/map-check/SKILL.md.jinja +1 -309
- mapify_cli/templates_src/codex/skills/map-check/check-reference.md.jinja +1 -70
- mapify_cli/templates_src/codex/skills/map-debug/SKILL.md.jinja +1 -406
- mapify_cli/templates_src/codex/skills/map-efficient/SKILL.md.jinja +1 -357
- mapify_cli/templates_src/codex/skills/map-efficient/efficient-reference.md.jinja +1 -351
- mapify_cli/templates_src/codex/skills/map-explain/SKILL.md.jinja +1 -280
- mapify_cli/templates_src/codex/skills/map-fast/SKILL.md.jinja +1 -191
- mapify_cli/templates_src/codex/skills/map-learn/SKILL.md.jinja +1 -417
- mapify_cli/templates_src/codex/skills/map-learn/templates/example-rules.md.jinja +1 -19
- mapify_cli/templates_src/codex/skills/map-learn/templates/rules-unconditional.md.jinja +1 -5
- mapify_cli/templates_src/codex/skills/map-learn/templates/rules-with-paths.md.jinja +1 -10
- mapify_cli/templates_src/codex/skills/map-memory-now/SKILL.md.jinja +1 -136
- mapify_cli/templates_src/codex/skills/map-plan/SKILL.md.jinja +1 -827
- mapify_cli/templates_src/codex/skills/map-plan/plan-reference.md.jinja +1 -226
- mapify_cli/templates_src/codex/skills/map-prd-review/SKILL.md.jinja +1 -204
- mapify_cli/templates_src/codex/skills/map-release/SKILL.md.jinja +1 -177
- mapify_cli/templates_src/codex/skills/map-release/release-reference.md.jinja +1 -456
- mapify_cli/templates_src/codex/skills/map-resume/SKILL.md.jinja +1 -294
- mapify_cli/templates_src/codex/skills/map-resume/resume-reference.md.jinja +1 -218
- mapify_cli/templates_src/codex/skills/map-review/SKILL.md.jinja +1 -412
- mapify_cli/templates_src/codex/skills/map-review/adversarial-reference.md.jinja +1 -224
- mapify_cli/templates_src/codex/skills/map-review/review-reference.md.jinja +1 -477
- mapify_cli/templates_src/codex/skills/map-skill-eval/SKILL.md.jinja +1 -178
- mapify_cli/templates_src/codex/skills/map-so-search/SKILL.md.jinja +1 -122
- mapify_cli/templates_src/codex/skills/map-so-search/scripts/sofa_search.py.jinja +1 -573
- mapify_cli/templates_src/codex/skills/map-state/SKILL.md.jinja +1 -217
- mapify_cli/templates_src/codex/skills/map-state/scripts/check-complete.sh.jinja +1 -68
- mapify_cli/templates_src/codex/skills/map-state/scripts/get-plan-path.sh.jinja +1 -40
- mapify_cli/templates_src/codex/skills/map-state/scripts/init-session.sh.jinja +1 -78
- mapify_cli/templates_src/codex/skills/map-state/scripts/show-focus.sh.jinja +1 -67
- mapify_cli/templates_src/codex/skills/map-state/templates/findings.md.jinja +1 -62
- mapify_cli/templates_src/codex/skills/map-state/templates/iteration_history.md.jinja +1 -93
- mapify_cli/templates_src/codex/skills/map-state/templates/progress.md.jinja +1 -75
- mapify_cli/templates_src/codex/skills/map-state/templates/task_plan.md.jinja +1 -70
- mapify_cli/templates_src/codex/skills/map-task/SKILL.md.jinja +1 -295
- mapify_cli/templates_src/codex/skills/map-tdd/SKILL.md.jinja +1 -536
- mapify_cli/templates_src/codex/skills/map-tokenreport/SKILL.md.jinja +1 -245
- mapify_cli/templates_src/codex/skills/map-understand/SKILL.md.jinja +1 -113
- mapify_cli/templates_src/codex/skills/map-upgrade/SKILL.md.jinja +1 -5
- mapify_cli/templates_src/codex/skills/map-wayfind/SKILL.md.jinja +1 -129
- mapify_cli/templates_src/codex/skills/map-wayfind/wayfind-reference.md.jinja +1 -134
- mapify_cli/templates_src/hooks/end-of-turn.sh.jinja +11 -3
- mapify_cli/templates_src/hooks/workflow-gate.py.jinja +87 -3
- mapify_cli/templates_src/map/scripts/map_interruption.py.jinja +981 -0
- mapify_cli/templates_src/map/scripts/map_orchestrator.py.jinja +221 -6
- mapify_cli/templates_src/map/scripts/map_step_runner.py.jinja +976 -286
- mapify_cli/templates_src/map/scripts/wayfind_runner.py.jinja +575 -4
- mapify_cli/templates_src/skills/map-auto/auto-reference.md.jinja +9 -8
- mapify_cli/templates_src/skills/map-check/SKILL.md.jinja +18 -12
- mapify_cli/templates_src/skills/map-check/check-reference.md.jinja +7 -6
- mapify_cli/templates_src/skills/map-debug/SKILL.md.jinja +77 -36
- mapify_cli/templates_src/skills/map-efficient/SKILL.md.jinja +364 -110
- mapify_cli/templates_src/skills/map-efficient/efficient-reference.md.jinja +373 -113
- mapify_cli/templates_src/skills/map-explain/SKILL.md.jinja +125 -35
- mapify_cli/templates_src/skills/map-fast/SKILL.md.jinja +37 -17
- mapify_cli/templates_src/skills/map-learn/SKILL.md.jinja +108 -58
- mapify_cli/templates_src/skills/map-learn/templates/example-rules.md.jinja +2 -1
- mapify_cli/templates_src/skills/map-learn/templates/rules-unconditional.md.jinja +2 -1
- mapify_cli/templates_src/skills/map-learn/templates/rules-with-paths.md.jinja +2 -1
- mapify_cli/templates_src/skills/map-memory-now/SKILL.md.jinja +35 -25
- mapify_cli/templates_src/skills/map-plan/SKILL.md.jinja +911 -245
- mapify_cli/templates_src/skills/map-plan/plan-reference.md.jinja +5 -4
- mapify_cli/templates_src/skills/map-prd-review/SKILL.md.jinja +88 -46
- mapify_cli/templates_src/skills/map-release/SKILL.md.jinja +33 -33
- mapify_cli/templates_src/skills/map-release/release-reference.md.jinja +154 -56
- mapify_cli/templates_src/skills/map-resume/SKILL.md.jinja +28 -14
- mapify_cli/templates_src/skills/map-resume/resume-reference.md.jinja +36 -26
- mapify_cli/templates_src/skills/map-review/SKILL.md.jinja +116 -69
- mapify_cli/templates_src/skills/map-review/adversarial-reference.md.jinja +62 -41
- mapify_cli/templates_src/skills/map-review/review-reference.md.jinja +55 -38
- mapify_cli/templates_src/skills/map-skill-eval/SKILL.md.jinja +62 -42
- mapify_cli/templates_src/skills/map-so-search/SKILL.md.jinja +11 -9
- mapify_cli/templates_src/skills/map-so-search/scripts/sofa_search.py.jinja +3 -3
- mapify_cli/templates_src/skills/map-state/SKILL.md.jinja +20 -14
- mapify_cli/templates_src/skills/map-state/scripts/check-complete.sh.jinja +3 -2
- mapify_cli/templates_src/skills/map-state/scripts/get-plan-path.sh.jinja +6 -4
- mapify_cli/templates_src/skills/map-state/scripts/init-session.sh.jinja +6 -4
- mapify_cli/templates_src/skills/map-state/scripts/show-focus.sh.jinja +19 -8
- mapify_cli/templates_src/skills/map-state/templates/findings.md.jinja +10 -5
- mapify_cli/templates_src/skills/map-state/templates/iteration_history.md.jinja +1 -0
- mapify_cli/templates_src/skills/map-state/templates/progress.md.jinja +1 -0
- mapify_cli/templates_src/skills/map-state/templates/task_plan.md.jinja +1 -0
- mapify_cli/templates_src/skills/map-task/SKILL.md.jinja +42 -35
- mapify_cli/templates_src/skills/map-tdd/SKILL.md.jinja +62 -38
- mapify_cli/templates_src/skills/map-tokenreport/SKILL.md.jinja +25 -19
- mapify_cli/templates_src/skills/map-understand/SKILL.md.jinja +91 -46
- mapify_cli/templates_src/skills/map-upgrade/SKILL.md.jinja +3 -1
- mapify_cli/templates_src/skills/map-wayfind/SKILL.md.jinja +36 -21
- mapify_cli/templates_src/skills/map-wayfind/wayfind-reference.md.jinja +148 -14
- mapify_cli/tools/validate_dependencies.py +5 -5
- {mapify_cli-3.31.4.dist-info → mapify_cli-3.33.0.dist-info}/METADATA +19 -7
- {mapify_cli-3.31.4.dist-info → mapify_cli-3.33.0.dist-info}/RECORD +176 -171
- {mapify_cli-3.31.4.dist-info → mapify_cli-3.33.0.dist-info}/WHEEL +0 -0
- {mapify_cli-3.31.4.dist-info → mapify_cli-3.33.0.dist-info}/entry_points.txt +0 -0
- {mapify_cli-3.31.4.dist-info → mapify_cli-3.33.0.dist-info}/licenses/LICENSE +0 -0
mapify_cli/__init__.py
CHANGED
|
@@ -23,7 +23,7 @@ Or install globally:
|
|
|
23
23
|
mapify check
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
-
__version__ = "3.
|
|
26
|
+
__version__ = "3.33.0"
|
|
27
27
|
|
|
28
28
|
# MUST stay the first import: it turns "this interpreter is older than 3.11" into
|
|
29
29
|
# a message that says so, instead of the ImportError raised by `from datetime
|
|
@@ -1,238 +1,277 @@
|
|
|
1
|
-
"""Apply
|
|
2
|
-
|
|
3
|
-
Invariants enforced by this module
|
|
4
|
-
-----------------------------------
|
|
5
|
-
INV-5 / source-untouched: only the specified .jinja file is written;
|
|
6
|
-
generated trees are updated via render_repo_trees.
|
|
7
|
-
INV-9 / path-safety: target .jinja must resolve under templates_src/;
|
|
8
|
-
paths under .git/ are rejected.
|
|
9
|
-
VC3 / no-op messages: two distinct messages for the two no-write outcomes;
|
|
10
|
-
baseline is NEVER written back (no_improvement guard).
|
|
11
|
-
VC4 / fail-loud: patch_skill_description raises ValueError on bad input
|
|
12
|
-
with NO partial write; YAML block scalar format enforced.
|
|
13
|
-
"""
|
|
1
|
+
"""Apply a provider-specific winner without modifying the opposite skill."""
|
|
14
2
|
|
|
15
3
|
from __future__ import annotations
|
|
16
4
|
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import stat
|
|
17
10
|
import subprocess
|
|
11
|
+
import tempfile
|
|
18
12
|
from pathlib import Path
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
from typing import TypedDict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _JournalEntry(TypedDict):
|
|
17
|
+
path: str
|
|
18
|
+
exists: bool
|
|
19
|
+
mode: int
|
|
20
|
+
backup: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _plain_description(text: str, winner: str) -> str:
|
|
24
|
+
"""Replace one complete, supported YAML description scalar."""
|
|
25
|
+
if not text.startswith("---\n") or "\n---" not in text:
|
|
26
|
+
raise ValueError("No YAML frontmatter found or no closing '---'")
|
|
27
|
+
end = text.index("\n---", 4)
|
|
28
|
+
lines = text[4:end].splitlines(keepends=True)
|
|
29
|
+
starts = [i for i, line in enumerate(lines) if line.startswith("description:")]
|
|
30
|
+
if len(starts) != 1:
|
|
31
|
+
raise ValueError("Expected exactly one description key")
|
|
32
|
+
i = starts[0]
|
|
33
|
+
scalar = lines[i].split(":", 1)[1].strip()
|
|
34
|
+
if not scalar or scalar.startswith(("[", "{", "&", "*")):
|
|
35
|
+
raise ValueError("Unsupported description scalar")
|
|
36
|
+
j = i + 1
|
|
37
|
+
while j < len(lines) and (not lines[j].strip() or lines[j].startswith((" ", "\t"))):
|
|
38
|
+
j += 1
|
|
39
|
+
if j > i + 1 and scalar not in ("|", "|-", "|+", ">", ">-", ">+"):
|
|
40
|
+
raise ValueError("Unsupported multiline description scalar")
|
|
41
|
+
while j > i + 1 and not lines[j - 1].strip():
|
|
42
|
+
j -= 1
|
|
43
|
+
replacement = "description: |-\n" + "".join(" " + line + "\n" for line in winner.split("\n"))
|
|
44
|
+
header = "".join(lines[:i]) + replacement + "".join(lines[j:])
|
|
45
|
+
return "---\n" + header.rstrip("\n") + text[end:]
|
|
21
46
|
|
|
22
47
|
|
|
23
48
|
def patch_skill_description(skill_md_path: Path, new_description: str) -> None:
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
The file MUST begin with ``---\\n`` and contain a closing ``\\n---``.
|
|
27
|
-
The ``description:`` key MUST exist at column 0 in the frontmatter.
|
|
28
|
-
|
|
29
|
-
All computation is done in-memory; the file is written ONCE at the end.
|
|
30
|
-
On any validation error the file is left completely untouched.
|
|
31
|
-
|
|
32
|
-
Args:
|
|
33
|
-
skill_md_path: Path to the .jinja file to patch (or any SKILL.md).
|
|
34
|
-
new_description: Replacement description text (already .strip()'d).
|
|
35
|
-
|
|
36
|
-
Raises:
|
|
37
|
-
ValueError: frontmatter missing, description key absent, or path rejected.
|
|
38
|
-
FileNotFoundError: skill_md_path does not exist.
|
|
39
|
-
"""
|
|
40
|
-
# Intent: read-then-compute-then-write-once; no partial write on failure.
|
|
49
|
+
"""Preserved plain YAML API; validate fully before the single write."""
|
|
41
50
|
text = skill_md_path.read_text(encoding="utf-8")
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if
|
|
52
|
-
raise ValueError(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
51
|
+
patched = _plain_description(text, new_description)
|
|
52
|
+
_atomic_bytes(skill_md_path, patched.encode(), stat.S_IMODE(skill_md_path.stat().st_mode))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _provider_description(text: str, winner: str, provider: str) -> str:
|
|
56
|
+
"""Bounded provider-header contract, not a general Jinja source parser."""
|
|
57
|
+
from mapify_cli.delivery.template_renderer import get_environment
|
|
58
|
+
from mapify_cli.skill_ir import parse_frontmatter
|
|
59
|
+
|
|
60
|
+
if not text.startswith("---\n") or "\n---\n" not in text:
|
|
61
|
+
raise ValueError("Unsupported description structure: missing frontmatter")
|
|
62
|
+
header, body = text[4:].split("\n---\n", 1)
|
|
63
|
+
for directive in re.findall(r"\[%.*?%\]", header, flags=re.DOTALL):
|
|
64
|
+
token = directive[2:-2].strip().strip("-").strip()
|
|
65
|
+
if token not in ("else", "endif") and not re.fullmatch(
|
|
66
|
+
r"if PROVIDER == ([\"'])(?:claude|codex)\1|set cmd = \"/\" if PROVIDER == \"claude\" else \"\$\"", token
|
|
67
|
+
):
|
|
68
|
+
raise ValueError(f"Unsupported description directive: {directive}")
|
|
69
|
+
if re.search(r"<%(?!\s*cmd\s*%>)|\[#", header):
|
|
70
|
+
raise ValueError("Unsupported description expression")
|
|
71
|
+
if any(t in winner for t in ("[%", "<%", "[#")) or not winner.strip():
|
|
72
|
+
raise ValueError("Unsupported winner description")
|
|
73
|
+
env = get_environment()
|
|
74
|
+
rendered = env.from_string("---\n" + header + "\n---\n").render(PROVIDER=provider)
|
|
75
|
+
parse_frontmatter(rendered.split("---", 2)[1])
|
|
76
|
+
patched = _plain_description(rendered, winner)
|
|
77
|
+
parsed = parse_frontmatter(patched.split("---", 2)[1])
|
|
78
|
+
if parsed.get("description") != winner:
|
|
79
|
+
raise ValueError("Unsupported description structure: winner did not round-trip")
|
|
80
|
+
opposite = "codex" if provider == "claude" else "claude"
|
|
81
|
+
original_other = env.from_string("---\n" + header + "\n---\n").render(PROVIDER=opposite)
|
|
82
|
+
# Independent provider descriptions have unambiguous scalar spans. Edit
|
|
83
|
+
# only the selected block, retaining every other source byte.
|
|
84
|
+
branches = list(re.finditer(r'\[% if PROVIDER == ([\"\'])(claude|codex)\1 %\]description:', header))
|
|
85
|
+
for branch in branches:
|
|
86
|
+
stop = header.find("[% endif", branch.end())
|
|
87
|
+
alternate = header.find("[% else %]description:", branch.end(), stop)
|
|
88
|
+
if alternate < 0:
|
|
89
|
+
continue
|
|
90
|
+
first = branch.end() - len("description:")
|
|
91
|
+
second = alternate + len("[% else %]")
|
|
92
|
+
start, limit = (first, alternate) if provider == branch.group(2) else (second, stop)
|
|
93
|
+
segment = header[start:limit]
|
|
94
|
+
patched_segment = _plain_description("---\n" + segment + "\n---\n", winner)[4:-5]
|
|
95
|
+
if segment.endswith("\n"):
|
|
96
|
+
patched_segment += "\n"
|
|
97
|
+
candidate_header = header[:start] + patched_segment + header[limit:]
|
|
98
|
+
result = "---\n" + candidate_header + "\n---\n" + body
|
|
99
|
+
if env.from_string("---\n" + candidate_header + "\n---\n").render(PROVIDER=opposite) != original_other:
|
|
100
|
+
raise ValueError("Unsupported conditional description whitespace")
|
|
101
|
+
return result
|
|
102
|
+
binding = '[% set cmd = "/" if PROVIDER == "claude" else "$" -%]'
|
|
103
|
+
combined = ("---\n" + binding + f'[% if PROVIDER == "{provider}" %]'
|
|
104
|
+
+ patched[4:-4] + "[% else %]" + original_other[4:-4] + "[% endif -%]\n---\n")
|
|
105
|
+
if env.from_string(combined).render(PROVIDER=opposite) != original_other:
|
|
106
|
+
raise ValueError("Unsupported description whitespace structure")
|
|
107
|
+
return combined + body
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _atomic_bytes(path: Path, data: bytes, mode: int) -> None:
|
|
111
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
fd, name = tempfile.mkstemp(prefix=".mapify-apply-", dir=path.parent)
|
|
113
|
+
try:
|
|
114
|
+
with os.fdopen(fd, "wb") as stream:
|
|
115
|
+
stream.write(data)
|
|
116
|
+
stream.flush()
|
|
117
|
+
os.fsync(stream.fileno())
|
|
118
|
+
os.chmod(name, mode)
|
|
119
|
+
os.replace(name, path)
|
|
120
|
+
directory_fd = os.open(path.parent, os.O_RDONLY)
|
|
121
|
+
try:
|
|
122
|
+
os.fsync(directory_fd)
|
|
123
|
+
finally:
|
|
124
|
+
os.close(directory_fd)
|
|
125
|
+
finally:
|
|
126
|
+
Path(name).unlink(missing_ok=True)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _recover_transaction(root: Path) -> None:
|
|
130
|
+
journal = root / ".map/skill-eval-apply-transaction"
|
|
131
|
+
manifest = journal / "manifest.json"
|
|
132
|
+
if any(p.is_symlink() for p in (journal, journal.parent, manifest)):
|
|
133
|
+
raise ValueError(f"Unsafe apply recovery journal: {journal}")
|
|
134
|
+
if not manifest.exists():
|
|
135
|
+
return
|
|
136
|
+
entries = json.loads(manifest.read_text(encoding="utf-8"))
|
|
137
|
+
if not (journal / "complete").exists():
|
|
138
|
+
for entry in entries:
|
|
139
|
+
target = root / entry["path"]
|
|
140
|
+
backup = journal / entry["backup"]
|
|
141
|
+
if (not target.resolve().is_relative_to(root.resolve())
|
|
142
|
+
or not backup.resolve().is_relative_to(journal.resolve())
|
|
143
|
+
or any(p.is_symlink() for p in (target, *target.parents, backup))):
|
|
144
|
+
raise ValueError(f"Unsafe recovery path in {manifest}")
|
|
145
|
+
if entry["exists"]:
|
|
146
|
+
_atomic_bytes(target, (journal / entry["backup"]).read_bytes(), entry["mode"])
|
|
147
|
+
else:
|
|
148
|
+
target.unlink(missing_ok=True)
|
|
149
|
+
shutil.rmtree(journal)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _commit_transaction(root: Path, updates: dict[Path, bytes]) -> list[Path]:
|
|
153
|
+
journal = root / ".map/skill-eval-apply-transaction"
|
|
154
|
+
for path in updates:
|
|
155
|
+
if any(p.is_symlink() for p in (path, *path.parents) if p != root.parent):
|
|
156
|
+
raise ValueError(f"Refusing symlink output: {path}")
|
|
157
|
+
if journal.exists():
|
|
158
|
+
raise ValueError(f"Incomplete apply journal requires inspection: {journal}")
|
|
159
|
+
journal.mkdir(parents=True, exist_ok=False)
|
|
160
|
+
entries: list[_JournalEntry] = []
|
|
161
|
+
for i, (path, data) in enumerate(updates.items()):
|
|
162
|
+
exists = path.exists()
|
|
163
|
+
if exists and path.read_bytes() == data:
|
|
164
|
+
continue
|
|
165
|
+
mode = stat.S_IMODE(path.stat().st_mode) if exists else 0o644
|
|
166
|
+
entry: _JournalEntry = {"path": str(path.relative_to(root)), "exists": exists, "mode": mode, "backup": str(i)}
|
|
167
|
+
if exists:
|
|
168
|
+
_atomic_bytes(journal / str(i), path.read_bytes(), 0o600)
|
|
169
|
+
entries.append(entry)
|
|
170
|
+
_atomic_bytes(journal / "manifest.json", json.dumps(entries).encode(), 0o600)
|
|
171
|
+
fd = os.open(journal, os.O_RDONLY)
|
|
172
|
+
try:
|
|
173
|
+
os.fsync(fd)
|
|
174
|
+
finally:
|
|
175
|
+
os.close(fd)
|
|
176
|
+
try:
|
|
177
|
+
for entry in entries:
|
|
178
|
+
path = root / str(entry["path"])
|
|
179
|
+
_atomic_bytes(path, updates[path], int(entry["mode"]))
|
|
180
|
+
_atomic_bytes(journal / "complete", b"complete", 0o600)
|
|
181
|
+
except BaseException:
|
|
182
|
+
_recover_transaction(root)
|
|
183
|
+
raise
|
|
184
|
+
shutil.rmtree(journal)
|
|
185
|
+
return [root / str(entry["path"]) for entry in entries]
|
|
110
186
|
|
|
111
187
|
|
|
112
188
|
def apply_optimized_description(
|
|
113
|
-
*,
|
|
114
|
-
|
|
115
|
-
winner: str,
|
|
116
|
-
current_description: str,
|
|
117
|
-
no_improvement: bool,
|
|
118
|
-
repo_root: Path,
|
|
119
|
-
stage: bool = True,
|
|
120
|
-
provider: str = "claude",
|
|
189
|
+
*, skill: str, winner: str, current_description: str, no_improvement: bool,
|
|
190
|
+
repo_root: Path, stage: bool = True, provider: str = "claude",
|
|
121
191
|
) -> str:
|
|
122
|
-
"""
|
|
123
|
-
|
|
124
|
-
Prints a user-facing message and returns an outcome code string.
|
|
125
|
-
|
|
126
|
-
Outcome codes:
|
|
127
|
-
``'no_improvement'`` — baseline wins; nothing written.
|
|
128
|
-
``'identical'`` — winner == current; nothing written.
|
|
129
|
-
``'applied'`` — .jinja patched and trees re-rendered.
|
|
130
|
-
|
|
131
|
-
Args:
|
|
132
|
-
skill: Skill name (e.g. ``'map-skill-eval'``).
|
|
133
|
-
winner: Candidate description text that won the eval.
|
|
134
|
-
current_description: Current description text from the .jinja.
|
|
135
|
-
no_improvement: True when baseline beat every candidate on held-out test.
|
|
136
|
-
repo_root: Absolute path to the repository root.
|
|
137
|
-
stage: If True, stage the patched .jinja + existing gate trees via a scoped
|
|
138
|
-
``git add -- <paths>`` after rendering (warn-only on failure; never commits).
|
|
139
|
-
|
|
140
|
-
Returns:
|
|
141
|
-
Outcome code string.
|
|
142
|
-
|
|
143
|
-
Raises:
|
|
144
|
-
ValueError: Path safety violation (outside templates_src or under .git/).
|
|
145
|
-
FileNotFoundError: .jinja source file does not exist.
|
|
146
|
-
"""
|
|
147
|
-
# --- VC3: no_improvement guard — NEVER write baseline back ---
|
|
192
|
+
"""Serialize recovery/promotion across processes using the installer barrier."""
|
|
148
193
|
if no_improvement:
|
|
149
|
-
print(
|
|
150
|
-
"No improvement found: current description already optimal on held-out test"
|
|
151
|
-
)
|
|
194
|
+
print("No improvement found: current description already optimal on held-out test")
|
|
152
195
|
return "no_improvement"
|
|
153
|
-
|
|
154
|
-
# --- VC3: winner identical guard ---
|
|
155
196
|
if winner == current_description:
|
|
156
197
|
print("Winner identical to current description; no file changes")
|
|
157
198
|
return "identical"
|
|
199
|
+
from mapify_cli.update_state import installer_process_lock
|
|
158
200
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
jinja = (
|
|
165
|
-
repo_root
|
|
166
|
-
/ "src"
|
|
167
|
-
/ "mapify_cli"
|
|
168
|
-
/ "templates_src"
|
|
169
|
-
/ provider_root
|
|
170
|
-
/ skill
|
|
171
|
-
/ "SKILL.md.jinja"
|
|
172
|
-
)
|
|
173
|
-
resolved = jinja.resolve()
|
|
174
|
-
|
|
175
|
-
# Intent: reject paths that escape templates_src or touch .git/
|
|
176
|
-
try:
|
|
177
|
-
resolved.relative_to(templates_src_root)
|
|
178
|
-
except ValueError:
|
|
179
|
-
raise ValueError(
|
|
180
|
-
f"Path safety violation: {resolved} is not under {templates_src_root}"
|
|
181
|
-
) from None
|
|
182
|
-
|
|
183
|
-
if ".git" in resolved.parts:
|
|
184
|
-
raise ValueError(f"Path safety violation: {resolved} is under .git/")
|
|
185
|
-
|
|
186
|
-
if not jinja.exists():
|
|
187
|
-
raise FileNotFoundError(
|
|
188
|
-
f"SKILL.md.jinja not found for skill '{skill}': {jinja}"
|
|
201
|
+
repo_root = repo_root.resolve()
|
|
202
|
+
with installer_process_lock(repo_root, timeout_s=30):
|
|
203
|
+
return _apply_optimized_description(
|
|
204
|
+
skill=skill, winner=winner, current_description=current_description,
|
|
205
|
+
no_improvement=no_improvement, repo_root=repo_root, stage=stage, provider=provider,
|
|
189
206
|
)
|
|
190
207
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
208
|
+
|
|
209
|
+
def _apply_optimized_description(
|
|
210
|
+
*, skill: str, winner: str, current_description: str, no_improvement: bool,
|
|
211
|
+
repo_root: Path, stage: bool = True, provider: str = "claude",
|
|
212
|
+
) -> str:
|
|
213
|
+
"""Render in isolation, then transactionally promote only selected skill files."""
|
|
214
|
+
if no_improvement:
|
|
215
|
+
print("No improvement found: current description already optimal on held-out test")
|
|
216
|
+
return "no_improvement"
|
|
217
|
+
if winner == current_description:
|
|
218
|
+
print("Winner identical to current description; no file changes")
|
|
219
|
+
return "identical"
|
|
220
|
+
if provider not in ("claude", "codex"):
|
|
221
|
+
raise ValueError(f"Unsupported provider: {provider}")
|
|
222
|
+
source_root = (repo_root / "src/mapify_cli/templates_src").resolve()
|
|
223
|
+
jinja = source_root / "skills" / skill / "SKILL.md.jinja"
|
|
224
|
+
if not jinja.resolve().is_relative_to(source_root) or ".git" in jinja.resolve().parts:
|
|
225
|
+
raise ValueError(f"Path safety violation outside templates_src or under .git: {jinja}")
|
|
226
|
+
_recover_transaction(repo_root)
|
|
227
|
+
original = jinja.read_text(encoding="utf-8")
|
|
228
|
+
candidate = _provider_description(original, winner, provider)
|
|
229
|
+
from mapify_cli.delivery.template_renderer import get_environment, render_repo_trees
|
|
230
|
+
|
|
231
|
+
with tempfile.TemporaryDirectory(prefix="mapify-apply-") as tmp:
|
|
232
|
+
sandbox = Path(tmp)
|
|
233
|
+
source = sandbox / "src/mapify_cli/templates_src"
|
|
234
|
+
shutil.copytree(source_root, source)
|
|
235
|
+
target = source / jinja.relative_to(source_root)
|
|
236
|
+
target.write_text(candidate, encoding="utf-8")
|
|
237
|
+
env = get_environment(source)
|
|
238
|
+
name = jinja.relative_to(source_root).as_posix()
|
|
239
|
+
opposite = "codex" if provider == "claude" else "claude"
|
|
240
|
+
before_env = get_environment(source_root)
|
|
241
|
+
if env.get_template(name).render(PROVIDER=opposite) != before_env.get_template(name).render(PROVIDER=opposite):
|
|
242
|
+
raise ValueError("Unsupported description structure: opposite provider changed")
|
|
243
|
+
from mapify_cli.skill_ir import parse_frontmatter
|
|
244
|
+
|
|
245
|
+
selected = env.get_template(name).render(PROVIDER=provider)
|
|
246
|
+
if parse_frontmatter(selected.split("---", 2)[1]).get("description") != winner:
|
|
247
|
+
raise ValueError("Unsupported description structure: selected winner mismatch")
|
|
248
|
+
for runtime in ("claude", "codex"):
|
|
249
|
+
render_repo_trees(runtime, repo_root=sandbox, templates_src_root=source)
|
|
250
|
+
roots = (".claude/skills", "src/mapify_cli/templates/skills") if provider == "claude" else (
|
|
251
|
+
".agents/skills", "src/mapify_cli/templates/codex/skills"
|
|
213
252
|
)
|
|
214
|
-
|
|
215
|
-
for
|
|
216
|
-
|
|
217
|
-
|
|
253
|
+
updates = {jinja: target.read_bytes()}
|
|
254
|
+
for root in roots:
|
|
255
|
+
rel = Path(root) / skill / "SKILL.md"
|
|
256
|
+
rendered_bytes = (sandbox / rel).read_bytes()
|
|
257
|
+
generated_text = rendered_bytes.decode()
|
|
258
|
+
if parse_frontmatter(generated_text.split("---", 2)[1]).get("description") != winner:
|
|
259
|
+
raise ValueError("Unsupported provider source: generated winner differs from common source")
|
|
260
|
+
provider_template = ("codex/" if provider == "codex" else "") + name
|
|
261
|
+
baseline = before_env.get_template(provider_template).render(PROVIDER=provider)
|
|
262
|
+
destination = repo_root / rel
|
|
263
|
+
if destination.exists() and destination.read_bytes() != baseline.encode():
|
|
264
|
+
raise ValueError(f"Refusing modified or stale generated skill: {destination}; preserve your edits and render explicitly first")
|
|
265
|
+
if _plain_description(baseline, winner) != generated_text:
|
|
266
|
+
raise ValueError("Unsupported provider source: non-description generated content changed")
|
|
267
|
+
updates[repo_root / rel] = rendered_bytes
|
|
268
|
+
changed = _commit_transaction(repo_root, updates)
|
|
269
|
+
if stage and changed:
|
|
218
270
|
result = subprocess.run(
|
|
219
|
-
["git", "add", "--", *
|
|
220
|
-
cwd=repo_root,
|
|
221
|
-
capture_output=True,
|
|
222
|
-
text=True,
|
|
223
|
-
check=False,
|
|
271
|
+
["git", "add", "--", *(str(p.relative_to(repo_root)) for p in changed)],
|
|
272
|
+
cwd=repo_root, capture_output=True, text=True, check=False,
|
|
224
273
|
)
|
|
225
|
-
if result.returncode
|
|
226
|
-
print(
|
|
227
|
-
|
|
228
|
-
f"{result.stderr.strip()}"
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
# --- Reminder: skill-rules.json is NOT auto-patched ---
|
|
232
|
-
print(
|
|
233
|
-
f"Applied new description for '{skill}'. "
|
|
234
|
-
"Note: skill-rules.json description is NOT auto-patched — "
|
|
235
|
-
"update it by hand or via ST-008."
|
|
236
|
-
)
|
|
237
|
-
|
|
274
|
+
if result.returncode:
|
|
275
|
+
print(f"Warning: 'git add' failed (exit {result.returncode}): {result.stderr.strip()}")
|
|
276
|
+
print(f"Applied new description for '{skill}'. Note: skill-rules.json description is NOT auto-patched")
|
|
238
277
|
return "applied"
|
|
@@ -40,6 +40,27 @@ You are a **validation agent**, NOT a code editor. Your role:
|
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
43
|
+
## Review context (MANDATORY)
|
|
44
|
+
|
|
45
|
+
- **Actor subtask validation:** When dispatched to validate an Actor subtask,
|
|
46
|
+
enforce its AAG contract and mutation boundary below. Missing or invalid
|
|
47
|
+
blueprint/subtask metadata is a blocking setup error, not a standalone review.
|
|
48
|
+
- **Standalone /map-review:** When dispatched to review a diff/PR using a review
|
|
49
|
+
bundle rather than validate an Actor subtask, assess correctness, security,
|
|
50
|
+
tests, regressions, and any supplied requirements against that diff.
|
|
51
|
+
Do not require an Actor AAG contract, blueprint, or subtask ID, and do not run
|
|
52
|
+
the Actor mutation-boundary command. Missing planning artifacts are context
|
|
53
|
+
limitations, not defects in the reviewed PR. Continue all applicable build,
|
|
54
|
+
test, and quality gates; report concrete defects with source evidence.
|
|
55
|
+
- Never infer standalone review from a missing blueprint. An explicit Actor
|
|
56
|
+
subtask validation assignment takes precedence over generic review wording.
|
|
57
|
+
If the assignment is ambiguous, ask the parent to clarify before a verdict.
|
|
58
|
+
Do not create a synthetic blueprint or reset/migrate workflow state to review.
|
|
59
|
+
|
|
60
|
+
The Actor-specific sequence and rejection rules below (AAG, allowed scope,
|
|
61
|
+
subtask metadata) apply only to Actor subtask validation, not standalone review.
|
|
62
|
+
|
|
63
|
+
|
|
43
64
|
<Monitor_Contract_Verification_v2_9>
|
|
44
65
|
|
|
45
66
|
## Contract-Based Verification Protocol
|
|
@@ -31,6 +31,27 @@ Your output: JSON with valid: true|false and issues[] array.
|
|
|
31
31
|
|
|
32
32
|
---
|
|
33
33
|
|
|
34
|
+
## Review context (MANDATORY)
|
|
35
|
+
|
|
36
|
+
- **Actor subtask validation:** When dispatched to validate an Actor subtask,
|
|
37
|
+
enforce its AAG contract and mutation boundary below. Missing or invalid
|
|
38
|
+
blueprint/subtask metadata is a blocking setup error, not a standalone review.
|
|
39
|
+
- **Standalone /map-review:** When dispatched to review a diff/PR using a review
|
|
40
|
+
bundle rather than validate an Actor subtask, assess correctness, security,
|
|
41
|
+
tests, regressions, and any supplied requirements against that diff.
|
|
42
|
+
Do not require an Actor AAG contract, blueprint, or subtask ID, and do not run
|
|
43
|
+
the Actor mutation-boundary command. Missing planning artifacts are context
|
|
44
|
+
limitations, not defects in the reviewed PR. Continue all applicable build,
|
|
45
|
+
test, and quality gates; report concrete defects with source evidence.
|
|
46
|
+
- Never infer standalone review from a missing blueprint. An explicit Actor
|
|
47
|
+
subtask validation assignment takes precedence over generic review wording.
|
|
48
|
+
If the assignment is ambiguous, ask the parent to clarify before a verdict.
|
|
49
|
+
Do not create a synthetic blueprint or reset/migrate workflow state to review.
|
|
50
|
+
|
|
51
|
+
The Actor-specific sequence and rejection rules below (AAG, allowed scope,
|
|
52
|
+
subtask metadata) apply only to Actor subtask validation, not standalone review.
|
|
53
|
+
|
|
54
|
+
|
|
34
55
|
# Contract-Based Verification Protocol
|
|
35
56
|
|
|
36
57
|
Primary Mission: Verify that Actor's implementation exactly matches the AAG contract
|
|
@@ -52,8 +73,19 @@ Verification sequence (execute in order):
|
|
|
52
73
|
|
|
53
74
|
3. Verify Goal is achieved -- trace code path to confirm the stated outcome
|
|
54
75
|
4. Verify Action is implemented -- check that the specified method/operation exists
|
|
55
|
-
5. Verify
|
|
56
|
-
|
|
76
|
+
5. Verify mutation boundary (MANDATORY): Run
|
|
77
|
+
python3 .map/scripts/map_step_runner.py validate_mutation_boundary <branch> <subtask_id>
|
|
78
|
+
to compare the actual git diff against the subtask's declared affected_files.
|
|
79
|
+
- status="clean" -> continue.
|
|
80
|
+
- status="warning" -> record the unexpected files; do NOT auto-reject.
|
|
81
|
+
The CLI also appends to .map/<branch>/scope-violations.log for audit.
|
|
82
|
+
- status="violation" (MAP_STRICT_SCOPE=1) -> valid: false with the unexpected
|
|
83
|
+
list. The Actor must re-scope.
|
|
84
|
+
- status="error" (missing blueprint, unknown subtask, git failure, not a git
|
|
85
|
+
repo) -> valid: false with the returned message. The CLI exits non-zero;
|
|
86
|
+
repair the setup before re-running Monitor. Never silently skip this gate.
|
|
87
|
+
6. Verify scope -- confirm changes stay within Actor's allowed_scope
|
|
88
|
+
7. Run quality gates below
|
|
57
89
|
|
|
58
90
|
Deterministic REJECT rule:
|
|
59
91
|
If implementation deviates from the AAG contract -> valid: false -- regardless of how
|
|
@@ -36,13 +36,13 @@ import sys
|
|
|
36
36
|
# evaluated annotations) fails with a message that never mentions the version.
|
|
37
37
|
# Name the real cause instead. Kept in sync with
|
|
38
38
|
# mapify_cli/python_runtime.MINIMUM_PYTHON.
|
|
39
|
-
#
|
|
40
|
-
#
|
|
39
|
+
# Compare the runtime tuple: the executing interpreter is the user's `python3`,
|
|
40
|
+
# not necessarily the Python version targeted by the project's static checker.
|
|
41
41
|
#
|
|
42
42
|
# FAIL-OPEN mode. Exit 1 is a non-blocking hook error for Claude Code (only
|
|
43
43
|
# exit 2, or a JSON deny, blocks a tool call), so the reason reaches the user
|
|
44
44
|
# and the session continues -- a broken interpreter is not a policy decision.
|
|
45
|
-
if sys.version_info < (3, 11):
|
|
45
|
+
if tuple(sys.version_info) < (3, 11):
|
|
46
46
|
_MAP_PYTHON_PROBLEM = (
|
|
47
47
|
f"MAP requires Python 3.11 or newer, but {sys.executable} is "
|
|
48
48
|
f"Python {sys.version_info[0]}.{sys.version_info[1]}.\n"
|
|
@@ -37,13 +37,13 @@ import sys
|
|
|
37
37
|
# evaluated annotations) fails with a message that never mentions the version.
|
|
38
38
|
# Name the real cause instead. Kept in sync with
|
|
39
39
|
# mapify_cli/python_runtime.MINIMUM_PYTHON.
|
|
40
|
-
#
|
|
41
|
-
#
|
|
40
|
+
# Compare the runtime tuple: the executing interpreter is the user's `python3`,
|
|
41
|
+
# not necessarily the Python version targeted by the project's static checker.
|
|
42
42
|
#
|
|
43
43
|
# FAIL-OPEN mode. Exit 1 is a non-blocking hook error for Claude Code (only
|
|
44
44
|
# exit 2, or a JSON deny, blocks a tool call), so the reason reaches the user
|
|
45
45
|
# and the session continues -- a broken interpreter is not a policy decision.
|
|
46
|
-
if sys.version_info < (3, 11):
|
|
46
|
+
if tuple(sys.version_info) < (3, 11):
|
|
47
47
|
_MAP_PYTHON_PROBLEM = (
|
|
48
48
|
f"MAP requires Python 3.11 or newer, but {sys.executable} is "
|
|
49
49
|
f"Python {sys.version_info[0]}.{sys.version_info[1]}.\n"
|