Conversation
There was a problem hiding this comment.
Pull request overview
Introduces a token-aware context-compression policy for MAP projects by measuring the last assistant turn’s input token usage from Claude Code transcript JSONL and injecting (or emitting) a /compact recommendation when a configured threshold is crossed.
Changes:
- Added
mapify_cli.token_budgetmodule (+ unit tests) to compute last-turn token usage, derive effective thresholds, and format the/compactnudge message. - Added a Claude Code
UserPromptSubmithook (context-meter.py) plus a PreCompact cooldown marker to prevent immediate re-nudging after compaction. - Extended project config +
mapify initflags to configure compression policy/threshold, and added an orchestrator stderr warning path for non-hook providers (e.g., Codex).
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
tests/test_token_budget.py |
New unit tests covering token counting, threshold logic, nudge decision, and message formatting. |
src/mapify_cli/token_budget.py |
New core implementation for reading transcript usage and generating /compact nudge text. |
src/mapify_cli/templates/settings.json |
Registers the context-meter.py hook in the shipped template settings. |
src/mapify_cli/templates/map/scripts/map_orchestrator.py |
Emits /compact recommendation to stderr when provided a transcript path and budget is crossed. |
src/mapify_cli/templates/hooks/pre-compact-save-transcript.py |
Writes last-compact.marker cooldown marker after PreCompact transcript save. |
src/mapify_cli/templates/hooks/context-meter.py |
New UserPromptSubmit hook that injects additionalContext with /compact guidance. |
src/mapify_cli/config/project_config.py |
Adds compression config fields, post-load validation, and helper to write overrides into .map/config.yaml. |
src/mapify_cli/__init__.py |
Adds mapify init flags for compression policy/threshold and persists them into .map/config.yaml. |
docs/context-compression-plan.md |
Design/implementation plan doc for the context meter and policy behavior. |
docs/USAGE.md |
Documents policies, defaults, and how the nudge appears/cools down (including orchestrator behavior). |
README.md |
Quick-start mention of compression policy flags. |
CHANGELOG.md |
Unreleased entry describing the new context compression policy feature set. |
.map/scripts/map_orchestrator.py |
Mirrors orchestrator changes in the .map/ script copy. |
.claude/settings.json |
Registers the context-meter.py hook for Claude Code runs in this repo. |
.claude/hooks/pre-compact-save-transcript.py |
Writes cooldown marker after compaction transcript save in the repo hook. |
.claude/hooks/context-meter.py |
Repo hook implementation for injecting additionalContext when threshold is crossed. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Status: approved, not yet started | ||
| Owner: TBD | ||
| Last updated: 2026-04-29 |
| | Field | Default | | ||
| | ------------------------------ | ---------------------------------------------------------------------------------------------- | | ||
| | `compression_policy` | `"auto"` — accepted: `never` / `auto` / `aggressive` | | ||
| | `compression_threshold_tokens` | `120_000` (~60% of Sonnet-200k; below Chroma-observed degradation zone) | | ||
| | `compression_focus` | `"MAP step state, last 2 monitor verdicts, pending subtasks; drop tool-result bodies older than 3 turns"` | |
| # Valid policy values. ``unknown`` policies are treated as ``never`` (fail | ||
| # safe — never inject the nudge if config is wrong). |
| # Default focus is the string used in the design doc. | ||
| assert "MAP step state" in msg | ||
| assert "/compact " in msg |
- docs/context-compression-plan.md: mark status as shipped (was "approved, not yet started" after PR #104 already implemented it) - token_budget.py: align format_compact_instruction default focus with the design-doc Defaults table (adds "drop tool-result bodies older than 3 turns") - token_budget.py: fix VALID_POLICIES comment to match actual fallback (unknown -> auto, not never; matches existing test) - test_token_budget.py: tighten test_blank_focus_falls_back_to_default to verify the full default string verbatim, so doc/code drift fails a test instead of a reviewer - workflow-gate.py (.claude, .codex, both templates): drop pre-existing unused datetime/timezone imports flagged by ruff
There was a problem hiding this comment.
Pull request overview
Adds a token-aware context compression (“/compact nudge”) policy to MAP Framework by tracking transcript token usage and warning when a configurable budget is exceeded, across both Claude Code (hook) and Codex (orchestrator stderr).
Changes:
- Introduces
mapify_cli.token_budgetutilities (token counting, threshold policy, nudge formatting) with a new dedicated test suite. - Extends
.map/config.yamlviaMapConfig(compression_policy,compression_threshold_tokens,compression_focus) and addsmapify initflags to write overrides. - Adds a Claude Code
UserPromptSubmithook (context-meter.py) plus an orchestrator--transcript-pathoption to emit the same guidance for non-hook providers.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
src/mapify_cli/token_budget.py |
New module for token counting + policy thresholding + formatting. |
tests/test_token_budget.py |
Unit tests covering transcript parsing and policy behavior. |
src/mapify_cli/config/project_config.py |
Adds compression config fields, validation, and init-time override writer. |
src/mapify_cli/__init__.py |
Adds mapify init flags for compression policy/threshold and writes them into config. |
src/mapify_cli/templates/hooks/context-meter.py |
New Claude Code hook to inject additionalContext nudge. |
.claude/hooks/context-meter.py |
Repo hook counterpart for local development/templates. |
src/mapify_cli/templates/hooks/pre-compact-save-transcript.py |
Writes cooldown marker to prevent immediate re-nudging post-compact. |
.claude/hooks/pre-compact-save-transcript.py |
Repo hook counterpart writing the same cooldown marker. |
src/mapify_cli/templates/map/scripts/map_orchestrator.py |
Emits nudge to stderr when transcript path is provided (Codex-friendly). |
.map/scripts/map_orchestrator.py |
Template source counterpart updated in lockstep. |
src/mapify_cli/templates/settings.json |
Registers the new UserPromptSubmit hook in templates. |
.claude/settings.json |
Registers the new hook for this repo’s Claude settings. |
docs/context-compression-plan.md |
Design/implementation plan for the feature. |
docs/USAGE.md |
Documents new policy options and init flags. |
README.md |
Quick-start mention of compression flags. |
CHANGELOG.md |
Unreleased entry for the new feature. |
src/mapify_cli/templates/hooks/workflow-gate.py |
Removes unused datetime import. |
src/mapify_cli/templates/codex/hooks/workflow-gate.py |
Removes unused datetime import. |
.claude/hooks/workflow-gate.py |
Removes unused datetime import. |
.codex/hooks/workflow-gate.py |
Removes unused datetime import. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Append `last-compact.marker` (UTC timestamp) write into the existing PreCompact | ||
| hook so that `context-meter.py` does not nudge again immediately after built-in |
| # Cooldown marker for context-meter.py - prevents the meter from injecting | ||
| # a fresh /compact nudge immediately after Claude Code's built-in | ||
| # auto-compact (~83.5%) has just run. mtime is what the meter compares | ||
| # against, so the file content is informational only. | ||
| marker = branch_dir / "last-compact.marker" | ||
| try: | ||
| marker.write_text(datetime.now().isoformat(), encoding="utf-8") | ||
| except (IOError, OSError): |
| # Cooldown marker for context-meter.py - prevents the meter from injecting | ||
| # a fresh /compact nudge immediately after Claude Code's built-in | ||
| # auto-compact (~83.5%) has just run. mtime is what the meter compares | ||
| # against, so the file content is informational only. | ||
| marker = branch_dir / "last-compact.marker" | ||
| try: | ||
| marker.write_text(datetime.now().isoformat(), encoding="utf-8") | ||
| except (IOError, OSError): |
| # Post-load validation for enum-like fields. We do not raise — a bad | ||
| # value falls back to the default so a typo does not break the user's | ||
| # workflow. | ||
| valid_policies = ("never", "auto", "aggressive") | ||
| if cfg.compression_policy not in valid_policies: | ||
| logger.warning( | ||
| "Invalid compression_policy %r in %s (expected one of %s). " | ||
| "Using default 'auto'.", | ||
| cfg.compression_policy, | ||
| config_file, | ||
| ", ".join(valid_policies), | ||
| ) | ||
| cfg.compression_policy = "auto" | ||
| if cfg.compression_threshold_tokens <= 0: | ||
| logger.warning( | ||
| "compression_threshold_tokens must be > 0 in %s " | ||
| "(got %d). Using default 120000.", | ||
| config_file, | ||
| cfg.compression_threshold_tokens, | ||
| ) | ||
| cfg.compression_threshold_tokens = 120_000 | ||
|
|
| # Valid policy values. Unknown policies are treated as ``auto`` (with a debug | ||
| # log in ``effective_threshold``) so misconfiguration does not silently | ||
| # disable the nudge — see ``effective_threshold`` and the matching test | ||
| # ``test_unknown_policy_treated_as_auto``. | ||
| VALID_POLICIES = ("never", "auto", "aggressive") | ||
|
|
||
|
|
| with pytest.raises(Exception): # noqa: PT011 — dataclasses.FrozenInstanceError | ||
| u.input_tokens = 5 # type: ignore[misc] |
- pre-compact-save-transcript.py (.claude + template): write last-compact.marker as UTC RFC3339 instead of naive local-time isoformat — matches the design doc's "UTC timestamp" wording and removes timezone ambiguity for cross-machine debugging - token_budget.VALID_POLICIES is now the single source of truth: project_config.load_map_config and mapify init both import it instead of redeclaring the tuple, so adding a future policy cannot drift across the three sites - tests/test_token_budget.py: tighten test_is_frozen to assert dataclasses.FrozenInstanceError instead of the bare Exception (the noqa was masking a too-broad raises) - tests/test_decomposition.py: add coverage for the new compression validation and apply_compression_overrides — invalid policy / non-positive threshold fall back to defaults, and the override helper handles commented placeholder, active entry, missing key, and missing-file cases
There was a problem hiding this comment.
Pull request overview
Adds a token-aware context compression policy to MAP so long-running sessions get a proactive /compact nudge before quality degrades, with shared logic across Claude Code hooks and Codex/orchestrator paths.
Changes:
- Introduces
mapify_cli.token_budgetutilities for extracting last-turn input token usage and generating/compactinstructions. - Extends
.map/config.yaml(MapConfig) with compression policy/threshold/focus, plus CLI flags inmapify initto set them. - Adds a Claude Code
UserPromptSubmithook (context-meter.py), cooldown marker handling, and orchestrator stderr warnings for non-hook environments (e.g., Codex).
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_token_budget.py | New unit tests covering token counting, threshold policy logic, and message formatting. |
| tests/test_decomposition.py | Adds tests for config validation and apply_compression_overrides. |
| src/mapify_cli/token_budget.py | New shared token budget/policy utilities used by hooks and orchestrator. |
| src/mapify_cli/templates/settings.json | Registers the new context-meter hook in template settings. |
| src/mapify_cli/templates/map/scripts/map_orchestrator.py | Adds transcript-path flag and stderr budget warning emission with cooldown. |
| src/mapify_cli/templates/hooks/workflow-gate.py | Removes unused datetime import in template hook. |
| src/mapify_cli/templates/hooks/pre-compact-save-transcript.py | Writes cooldown marker after compaction in template hook. |
| src/mapify_cli/templates/hooks/context-meter.py | New template hook that injects /compact nudge via additionalContext. |
| src/mapify_cli/templates/codex/hooks/workflow-gate.py | Removes unused datetime import in Codex template hook. |
| src/mapify_cli/config/project_config.py | Adds compression fields, validation, default config entries, and override writer. |
| src/mapify_cli/init.py | Adds mapify init flags and writes compression overrides into .map/config.yaml. |
| docs/context-compression-plan.md | Adds a design/implementation plan doc for the feature. |
| docs/USAGE.md | Documents compression policies, defaults, and how the nudge works. |
| README.md | Mentions init-time compression flags in the quick start. |
| CHANGELOG.md | Adds an Unreleased entry describing the new feature set. |
| .map/scripts/map_orchestrator.py | Mirrors orchestrator changes in the synced .map/scripts version. |
| .codex/hooks/workflow-gate.py | Removes unused datetime import in repo hook. |
| .claude/settings.json | Registers the new context-meter hook in repo Claude settings. |
| .claude/hooks/workflow-gate.py | Removes unused datetime import in repo hook. |
| .claude/hooks/pre-compact-save-transcript.py | Writes cooldown marker after compaction in repo hook. |
| .claude/hooks/context-meter.py | New repo hook that injects /compact nudge via additionalContext. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Get branch | ||
| branch = args.branch if args.branch else get_branch_name() | ||
|
|
||
| # Provider-agnostic context-budget warning. No-op when no transcript is | ||
| # available (Codex without explicit --transcript-path, etc.) or when the | ||
| # mapify_cli package is not importable from this script's environment. | ||
| _emit_context_budget_warning(branch, args.transcript_path) | ||
|
|
| # Same cooldown semantic as context-meter.py: skip if a compaction | ||
| # marker has been touched in the last 5 minutes. | ||
| marker = project_dir / ".map" / branch / "last-compact.marker" | ||
| if marker.is_file(): | ||
| try: | ||
| if (time.time() - marker.stat().st_mtime) < 5 * 60: | ||
| return |
| # Get branch | ||
| branch = args.branch if args.branch else get_branch_name() | ||
|
|
||
| # Provider-agnostic context-budget warning. No-op when no transcript is | ||
| # available (Codex without explicit --transcript-path, etc.) or when the | ||
| # mapify_cli package is not importable from this script's environment. | ||
| _emit_context_budget_warning(branch, args.transcript_path) | ||
|
|
| # Same cooldown semantic as context-meter.py: skip if a compaction | ||
| # marker has been touched in the last 5 minutes. | ||
| marker = project_dir / ".map" / branch / "last-compact.marker" | ||
| if marker.is_file(): | ||
| try: | ||
| if (time.time() - marker.stat().st_mtime) < 5 * 60: | ||
| return |
| config_path = write_default_config(project_path) | ||
| apply_compression_overrides( | ||
| config_path, compression, compression_threshold | ||
| ) |
| from mapify_cli.config.project_config import ( | ||
| apply_compression_overrides, | ||
| write_default_config, | ||
| ) | ||
|
|
||
| config_path = write_default_config(project_path) | ||
| apply_compression_overrides( | ||
| config_path, compression, compression_threshold | ||
| ) | ||
| tracker.complete( |
Re-runnable mapify init no longer overwrites compression overrides: - mapify init's --compression / --compression-threshold flags now default to None. - apply_compression_overrides accepts None per parameter and is skipped when both are None, so a bare ``mapify init .`` re-run preserves whatever the user has already set in .map/config.yaml. CLI validation only runs when the user actually passed a flag. - Tests cover: no-op when both flags absent, partial-policy-only, and partial-threshold-only. --branch is now sanitized before it touches the filesystem: - map_utils.sanitize_branch_name() is extracted as a public helper (and reused by get_branch_name). The orchestrator routes args.branch through it before building .map/<branch>/last-compact.marker so a malicious "../etc" branch arg can no longer escape the .map/ directory, and "feature/foo" no longer creates a different cooldown directory than the auto-detected "feature-foo". - Both .map/scripts/ and src/mapify_cli/templates/map/scripts/ kept in sync. - New tests/test_map_utils_sanitize.py loads the script via importlib and parametrises path-traversal cases.
No description provided.