feat(install): compile global instructions into user-scope root context files on install -g#1632
feat(install): compile global instructions into user-scope root context files on install -g#1632danielmeppiel wants to merge 13 commits into
Conversation
… files
New shared function compile_user_root_contexts(targets, source_root, *, dry_run, logger)
in src/apm_cli/compilation/user_root_context.py:
- Discovers global (apply_to-less) instructions from source_root/apm_modules
- Resolves TargetProfile.for_scope(user_scope=True) per target; None => skip
- Maps compile_family to root filename:
claude -> CLAUDE.md
agents -> AGENTS.md
vscode -> AGENTS.md
gemini -> GEMINI.md
- Reuses _COPILOT_ROOT_GENERATED_MARKER from agents_compiler.py
- Overwrite protection: skips hand-authored files (no marker)
- Writes only the root file; no CWD changes
- Returns [{target, path, status}] dicts for caller reporting
Exported from src/apm_cli/compilation/__init__.py.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…feature - test_user_root_context.py: 16 tests covering compile_user_root_contexts() function - Tests for no apm_modules, target filtering, instruction discovery, file I/O - Tests for overwrite protection, dry-run, error handling, and logging - test_compile_global_flag.py: 13 tests for CLI --global flag integration - Tests for _handle_global_flag() with various result statuses - Tests for CLI flag validation and error handling - Tests for rich console output functions - test_finalize_user_compile.py: 10 tests for post-install compile hook - Tests for _compile_user_root_contexts_after_install() hook - Tests for finalize.run() integration with different install scopes - Tests for logger behavior and cross-module integration All 39 tests pass with proper mocking, fixtures, and comprehensive scenario coverage. Fixes #1485
…ize hook; add scope to test stub InstallLogger does not expose stdlib debug/info API; use stdlib logger for compile_user_root_contexts. Also adds scope field to _FakeCtx in test_finalize_phase.py so the new scope guard compiles cleanly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a user-scope “global compile” path so packages installed with apm install -g can surface global (no apply_to) instructions into user-level root context files (~/.claude/CLAUDE.md, ~/.copilot/AGENTS.md, ~/.gemini/GEMINI.md, etc.), and introduces apm compile --global/-g to re-run that compilation without reinstalling.
Changes:
- Adds
compile_user_root_contexts()engine to compile global instructions from the user package tree into per-target user root context files (with overwrite protection via the APM marker). - Hooks global compilation into the install finalize phase for
InstallScope.USER. - Adds
--global/-ghandling toapm compile, plus unit tests and documentation updates.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/install/test_finalize_phase.py | Extends the finalize-phase test context with a scope field. |
| tests/unit/install/phases/test_finalize_user_compile.py | Adds unit tests for the finalize hook that triggers user-scope compilation. |
| tests/unit/compilation/test_user_root_context.py | Adds unit tests for the new user-root compilation engine. |
| tests/unit/compilation/test_compile_global_flag.py | Adds unit tests for the new apm compile --global/-g CLI behavior. |
| src/apm_cli/install/phases/finalize.py | Calls a new post-install hook to compile user-scope root context files after -g installs. |
| src/apm_cli/compilation/user_root_context.py | Implements the engine that writes user-scope root context files from global instructions. |
| src/apm_cli/compilation/init.py | Exposes compile_user_root_contexts at the package level. |
| src/apm_cli/commands/compile/cli.py | Adds --global/-g flag and handler to compile user-scope root context files. |
| packages/apm-guide/.apm/skills/apm-usage/commands.md | Updates CLI usage docs to mention apm compile --global/-g. |
| docs/src/content/docs/producer/compile.md | Documents global compilation behavior, overwrite protection, and constraints. |
Copilot's findings
- Files reviewed: 10/10 changed files
- Comments generated: 6
| from apm_cli.compilation import compile_user_root_contexts | ||
| from apm_cli.core.scope import InstallScope, get_source_root | ||
| from apm_cli.integration.targets import KNOWN_TARGETS | ||
|
|
||
| source_root = get_source_root(InstallScope.USER) | ||
| targets = list(KNOWN_TARGETS.values()) |
| from ...compilation import compile_user_root_contexts | ||
| from ...core.scope import InstallScope, get_source_root | ||
| from ...integration.targets import KNOWN_TARGETS | ||
| from ...utils.console import _rich_error, _rich_info, _rich_success | ||
|
|
||
| source_root = get_source_root(InstallScope.USER) | ||
| apm_modules = source_root / "apm_modules" | ||
| if not apm_modules.is_dir(): |
| source_root = tmp_path / "source" | ||
| source_root.mkdir() | ||
| # apm_modules does NOT exist | ||
|
|
||
| mock_rich_error = MagicMock() | ||
|
|
||
| with ( | ||
| patch( | ||
| "apm_cli.core.scope.get_source_root", | ||
| return_value=source_root, | ||
| ), |
| source_root = Path.home() | ||
| ctx = _make_install_context(scope=InstallScope.USER) | ||
|
|
||
| mock_compile = MagicMock(return_value=[]) | ||
|
|
||
| with ( | ||
| patch( | ||
| "apm_cli.core.scope.get_source_root", | ||
| return_value=source_root, | ||
| ), |
| ## Global compilation (-g) | ||
|
|
||
| By default, `apm compile` reads instructions from your workspace and | ||
| writes root context files to `.github/`, `.claude/`, etc. For distributing | ||
| instructions to all AI tools on your user's machine (not scoped to a project), | ||
| use the `--global` or `-g` flag: |
| def compile_user_root_contexts( | ||
| targets, | ||
| source_root: Path, | ||
| *, | ||
| dry_run: bool = False, | ||
| logger=None, | ||
| ) -> list[dict]: |
…and type annotations Addresses all 6 Copilot inline findings (shepherd-driver fold run): - finalize.py and compile/cli.py: get_source_root(USER) returns Path.home() but apm_modules lives under get_apm_dir(USER) (~/.apm/). Both callers now call get_apm_dir(USER) so the feature no longer silently skips compilation. - test_compile_global_flag.py and test_finalize_user_compile.py: test mocks updated to patch get_apm_dir instead of get_source_root. Tests now validate the correct contract. Path.home() / '.apm' used as the mock return value. - compile/cli.py: mutual-exclusion errors for --global+--watch and --global+--root now call sys.exit(2) instead of bare return so CI scripts see a non-zero exit code on flag misuse. - compile/cli.py: _handle_global_flag output now uses symbol= API (symbol='check', 'preview', 'info', 'error') instead of hard-coded bracket prefixes in f-strings. - docs/reference/cli/compile.md: adds --global/-g flag entry in a new 'Global compilation' section with usage notes and overwrite-protection semantics. - user_root_context.py: adds type annotations to compile_user_root_contexts, _resolve_deploy_root, and _generate_content public-ish surface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel: Advisory RecommendationPR #1632 -- feat(install): compile global instructions into user-scope root context files on install -g Headline: Blocking path-resolution bug (get_source_root vs get_apm_dir) makes global compile silently no-op; shepherd has folded the fix before ship. Panel: python-architect, cli-logging-expert, devx-ux-expert, supply-chain-security-expert, oss-growth-hacker, auth-expert (inactive -- no auth surface), doc-writer, test-coverage-expert ArbitrationAll six active panelists converge on a single critical finding: Strategically, the design is sound and the reservations from the strategic-alignment gate are fully addressed: project-scope install/compile is untouched, the user-scope write is correctly gated behind The remaining recommended findings cluster around three themes: (1) exit-code correctness for CI consumers, (2) discoverability in docs, and (3) deploy-root path containment as a forward-safety measure. Exit-code fix and docs gap have been folded. Path containment is deferred. Principle Alignment
Growth NoteThe oss-growth-hacker correctly identifies this as a funnel-closing feature: global install now "just works" for AI tool configuration. The post-install moment is a prime delight hook. README/quickstart mention should land in the same release cycle to capture the adoption signal. Recommended Follow-ups (prioritized)
Ship Recommendation
The architecture is correct, the scope guards work, and the design delivers on the user promise. The blocking bug (path-resolution) has been folded in this shepherd run along with exit-code fixes, docs update, type annotations, and symbol= API usage. The feature is shippable. README discoverability and path containment guard are tracked as follow-ups for the next release cycle. Advisory only -- no merge gate. Maintainer decides. |
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 2 | 2 | Clean new module; one structural gap (InstallLogger API mismatch silently swallowed) and one minor abstraction leak worth addressing before merge. |
| CLI Logging Expert | 0 | 2 | 3 | skipped-hand-authored shown as [i] info not warning; install path silently drops error results; missing symbol on apm_modules-not-found error. |
| DevX UX Expert | 0 | 3 | 2 | Flag ergonomics and happy-path UX are solid; two gaps: mutual-exclusion errors exit(2) without hinting the fix, and silent auto-compile on install -g has no discoverable signal on the non-verbose path. |
| Supply Chain Security Expert | 2 | 3 | 1 | Package instruction content flows verbatim into AI root context files; no sanitization, size cap, or path containment guard on output_path. |
| OSS Growth Hacker | 0 | 2 | 2 | Global compile is a killer distribution story but docs bury the lede -- the 'publish once, land in every AI tool' hook is missing from both pages. |
| Doc Writer | 0 | 1 | 3 | New global-compile docs are structurally sound and code-accurate; one consumer-page gap and two small inaccuracies need fixing. |
| Test Coverage Expert | 0 | 2 | 1 | 39 unit tests cover the engine well; the core user promise (apm install -g writes CLAUDE.md) has no integration-with-fixtures test, and CLAUDE_CONFIG_DIR is untested in the new engine. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
- [Supply Chain Security Expert] (blocking-severity) Add content sanitization: hard size cap per instruction (8 KB) and rejection of instruction content containing the APM marker string, before any write to AI root context files -- verbatim package content in
~/.claude/CLAUDE.mdenables supply-chain prompt injection via compromised transitive dependencies; existing package trust model does not mitigate this because instruction content is never inspected before install - [Supply Chain Security Expert] (blocking-severity) Call
ensure_path_within(output_path, deploy_root)frompath_security.pybefore any write incompile_user_root_contexts-- project's own security chokepoint contract requires all filesystem writes derived from dynamic inputs to pass throughensure_path_within;resolved_deploy_rootis dynamically computed and the guard is missing - [Test Coverage Expert] (blocking-severity) Add an integration-with-fixtures test that drives a real
apm install -gagainst a fixture package tree and asserts~/.claude/CLAUDE.mdis written with the APM marker -- evidenceoutcome: missingonintegration-with-fixturestier for the PR's core user promise; unit tests mockcompile_user_root_contextsat the boundary and will not catch install-pipeline wiring regressions - [DevX UX Expert] Emit a non-verbose output line when
apm install -gwrites root context files; side-effects outside the project tree must be visible on the default output path -- silent writes to~/.claude/CLAUDE.mdon install -g violate the pragmatic-as-npm ergonomic bar; users cannot audit what install did to their AI configuration - [CLI Logging Expert] Surface error results from
_compile_user_root_contexts_after_install; current code only messages onwrittenentries, silently discarding OS write failures -- a user whose CLAUDE.md was not updated due to a write failure sees a successful install summary with no indication that the root context step failed
Architecture
classDiagram
direction LR
class TargetProfile {
<<ValueObject>>
+name str
+compile_family str|None
+resolved_deploy_root Path|None
+for_scope(user_scope) TargetProfile|None
}
class compile_user_root_contexts {
<<Pure>>
+targets Iterable
+source_root Path
+dry_run bool
+logger Logger|None
+returns list[dict]
}
class AgentsCompiler {
<<Compiler>>
+_COPILOT_ROOT_GENERATED_MARKER str
}
class _handle_global_flag {
<<CLIHandler>>
+dry_run bool
+returns int
}
class finalize_run {
<<InstallPhase>>
+ctx InstallContext
+returns InstallResult
}
class InstallContext {
<<Context>>
+scope InstallScope
+logger InstallLogger|None
}
class InstallLogger {
<<CommandLogger>>
+verbose_detail(msg)
}
class discover_primitives {
<<IOBoundary>>
+root str
+returns PrimitivesResult
}
class compile_user_root_contexts:::touched
class _handle_global_flag:::touched
class finalize_run:::touched
compile_user_root_contexts ..> TargetProfile : iterates
compile_user_root_contexts ..> AgentsCompiler : reads marker constant
compile_user_root_contexts ..> discover_primitives : calls
_handle_global_flag ..> compile_user_root_contexts : calls
finalize_run ..> compile_user_root_contexts : calls via helper
finalize_run ..> InstallContext : reads scope
InstallContext *-- InstallLogger : optional
note for compile_user_root_contexts "Collect-then-render: discovers all\nglobal instructions, then writes per-target"
note for finalize_run "Logger API mismatch: passes logger=None\nbecause InstallLogger != stdlib Logger"
classDef touched fill:#fff3b0,stroke:#d47600
flowchart TD
A(["apm install -g"]) --> B["finalize.run()\ninstall/phases/finalize.py"]
B --> C{"ctx.scope == USER?"}
C -- No --> Z(["InstallResult"])
C -- Yes --> D["_compile_user_root_contexts_after_install(ctx)\nfinalize.py:22"]
D --> E["[I/O] get_apm_dir(USER)\ncore/scope.py"]
E --> F["compile_user_root_contexts(targets, source_root)\ncompilation/user_root_context.py:100"]
F --> G["[FS] check apm_modules dir exists\n~/.apm/apm_modules"]
G -- Not found --> H(["return empty list"])
G -- Found --> I["[I/O] discover_primitives(apm_modules)\nprimitives/discovery.py"]
I --> J["filter: instructions with no apply_to"]
J --> K["for each target in KNOWN_TARGETS"]
K --> L["target.for_scope(user_scope=True)"]
L -- None --> K
L -- Profile --> M{"compile_family in _ROOT_FILENAME?"}
M -- No --> K
M -- Yes --> N{"global_instructions empty?"}
N -- Yes --> O["status: skipped-no-instructions"] --> K
N -- No --> P["_resolve_deploy_root(scoped)\nresolved_deploy_root or home/root_dir"]
P --> Q["_generate_content(instructions)\n_finalize_build_id + marker"]
Q --> R{"[I/O] output_path.exists()?"}
R -- No --> S["[FS] mkdir + write_text\nstatus: written"]
R -- Yes --> T["[I/O] read_text existing file"]
T --> U{"APM marker present?"}
U -- No --> V["status: skipped-hand-authored"]
U -- Yes --> W{"content unchanged?"}
W -- Yes --> X["status: unchanged"]
W -- No --> S
S --> K
K -- done --> Y["return results list"]
Y --> Z
A2(["apm compile --global"]) --> H2["_handle_global_flag(dry_run)\ncompile/cli.py:323"]
H2 --> E
H2 --> YY["render per-target status to console"]
sequenceDiagram
actor User
participant CLI as apm CLI
participant Finalize as finalize.run()
participant Engine as compile_user_root_contexts()
participant FS as Filesystem
User->>CLI: apm install -g pkg
CLI->>Finalize: run(ctx) [scope=USER]
Finalize->>Engine: compile_user_root_contexts(KNOWN_TARGETS, ~/.apm)
Engine->>FS: discover_primitives(~/.apm/apm_modules)
FS-->>Engine: primitives
loop per target (claude, copilot, cursor, ...)
Engine->>FS: read existing root file (if any)
FS-->>Engine: existing content or not-found
alt hand-authored (no APM marker)
Engine-->>Engine: skip
else APM-owned or new
Engine->>FS: write CLAUDE.md / AGENTS.md / GEMINI.md
end
end
Engine-->>Finalize: results list
Finalize-->>CLI: InstallResult
CLI-->>User: success message + verbose_detail count
Recommendation
Hold this PR for three targeted fixes before merge: (1) add minimum content sanitization -- per-instruction size cap and marker-string rejection -- in compile_user_root_contexts before any write; (2) add ensure_path_within(output_path, deploy_root) immediately after output_path is resolved; (3) add at least one integration-with-fixtures test that asserts the core promise end-to-end. These are all small, scoped changes that do not require design discussion. Once those three land, the remaining recommended findings (non-verbose install output, error surfacing, logger API bridge, doc additions) should be addressed in the same PR rather than deferred -- they are the difference between a feature that feels polished at launch and one that accumulates a tail of follow-up issues. The strategic framing and code structure are excellent; this is worth the extra day to harden.
Full per-persona findings
Python Architect
-
[recommended] Logger API mismatch is silently swallowed by passing logger=None from finalize.py at
src/apm_cli/install/phases/finalize.py:32
compile_user_root_contexts accepts a stdlib logging.Logger, but ctx.logger is an InstallLogger with a different API (verbose_detail, not debug/info). All compilation debug/warning output during install goes to the root stdlib logger, silently bypassing the structured CLI output pipeline.
Suggested: Add a thin property to InstallContext (or InstallLogger) that returns a stdlib logging.Logger wrapping verbose_detail/warn calls, then pass that logger to compile_user_root_contexts. -
[recommended] discover_primitives called once per compile_user_root_contexts call but targets loop iterates inside; primitive discovery design is fragile if a future caller wraps in a loop at
src/apm_cli/compilation/user_root_context.py:100
The primitives object is never type-hinted; the function signature uses object for TargetProfile instances, eroding IDE support and the architecture's own contract. -
[nit] _ROOT_FILENAME map uses 'vscode' key but copilot target returns compile_family='vscode' at user scope -- add clarifying comment at
src/apm_cli/compilation/user_root_context.py:35 -
[nit] finalize.py imports InstallScope twice: once under TYPE_CHECKING and once as a lazy runtime import inside run() at
src/apm_cli/install/phases/finalize.py:104
Move InstallScope to a top-level runtime import and remove the duplicate lazy import.
CLI Logging Expert
-
[recommended] skipped-hand-authored uses _rich_info/symbol=info but should warn the user at
src/apm_cli/commands/compile/cli.py:365
A hand-authored file being silently skipped is a yellow-flag condition: the user's global instructions were NOT applied to that target. Rendering it as blue [i] buries an actionable message.
Suggested: Use _rich_warning(f"{tname}: skipped (hand-authored) {path} -- add APM marker or use --force to overwrite", symbol="warning") -
[recommended] install path swallows error results from _compile_user_root_contexts_after_install silently at
src/apm_cli/install/phases/finalize.py:33
finalize.py only calls verbose_detail when results contain 'written' entries. If every result is an error (OS write failure), the function returns silently. The user sees a successful install summary but root context files were not updated. -
[nit] _rich_error at line 337 has no symbol= so the apm_modules-not-found error lacks the [x] prefix at
src/apm_cli/commands/compile/cli.py:337 -
[nit] _rich_info at line 351 (no results path) has no symbol= while all per-entry info lines do at
src/apm_cli/commands/compile/cli.py:351 -
[nit] symbol=light_bulb and symbol=page used elsewhere in file are not in STATUS_SYMBOLS and are silently dropped (pre-existing, not introduced by this PR) at
src/apm_cli/commands/compile/cli.py:872
DevX UX Expert
-
[recommended] Mutual-exclusion errors print the problem but not the fix at
src/apm_cli/commands/compile/cli.py:552
'--global cannot be combined with --watch' emits the error then exit(2). npm/pip pattern is to append a one-line hint: e.g. "Drop --watch and re-run, or use 'apm compile --global' separately." -
[recommended] Auto-compile triggered by apm install -g is invisible on the default (non-verbose) output path at
src/apm_cli/install/phases/finalize.py:34
finalize.py gates the message on verbose_detail. A user running plain 'apm install -g some-pkg' sees no indication that ~/.claude/CLAUDE.md was just written. Silent side-effects to files outside the project tree are especially surprising. -
[recommended] Empty-modules info message does not explain that only instructions without applyTo: are compiled globally at
src/apm_cli/commands/compile/cli.py:351
A user who installed a skills-only package globally will hit 'No user-scope targets produced output' and not understand why nothing happened.
Suggested: "No global instructions found in ~/.apm/apm_modules. Only instructions without an applyTo: field are compiled globally. Skills and scoped instructions are skipped." -
[nit] Help text for --global lists only ~/.claude/CLAUDE.md with 'etc.' -- spell out the full list or point to --dry-run at
src/apm_cli/commands/compile/cli.py:484 -
[nit] skipped-hand-authored output uses 'info' symbol rather than a warning symbol at
src/apm_cli/commands/compile/cli.py:366
Supply Chain Security Expert
-
[blocking] Unfiltered package content written verbatim into AI root context files enables supply-chain prompt injection at
src/apm_cli/compilation/user_root_context.py:93
compile_user_root_contexts() concatenates instruction.content.strip() directly from any installed package into ~/.claude/CLAUDE.md, ~/.codex/AGENTS.md, etc. A compromised package maintainer or dependency-confusion package publishes adversarial AI directives. The install hook fires automatically without user acknowledgement. There is no allowlist, no size cap, and no pre-write confirmation.
Suggested: Apply a hard size cap per instruction (8 KB) and a total cap per generated file (64 KB). Reject instruction content containing the APM marker string. Consider a pre-write summary prompt for --yes-less invocations. -
[blocking] output_path is not validated with ensure_path_within; a TargetProfile with an attacker-influenced root_dir could write outside the expected directory at
src/apm_cli/compilation/user_root_context.py:188
_resolve_deploy_root() returns profile.resolved_deploy_root which is dynamically set, potentially by a resolver callback. output_path = deploy_root / root_filename is written without calling ensure_path_within or validate_path_segments, violating the project's path_security.py chokepoint contract.
Suggested: Call ensure_path_within(output_path, deploy_root) from src/apm_cli/utils/path_security.py before any write. -
[recommended] TOCTOU race between output_path.exists() check and output_path.write_text() allows hand-authored file to be overwritten at
src/apm_cli/compilation/user_root_context.py:193
Sequence: (1) exists(), (2) read_text(), (3) marker check, (4) write_text(). A concurrent process could swap a hand-authored file between steps (3) and (4).
Suggested: Write to output_path.with_suffix('.tmp') then atomically rename with output_path.replace(). -
[recommended] APM marker is a plain human-readable string; a package can embed it to confuse future overwrite checks at
src/apm_cli/compilation/user_root_context.py:203
The marker '' is checked with a substring test. Instruction content containing the marker string will degrade the integrity signal and could enable social-engineering attacks.
Suggested: Detect and reject instruction content containing the marker string before compilation, logging a warning with the offending package name. -
[recommended] Install hook fires unconditionally for every USER-scope install, even when no newly installed package provides global instructions at
src/apm_cli/install/phases/finalize.py:107
The attack window opens for ANY user-scope install, including unrelated ones that coincidentally pull in a transitive dependency with global instructions. -
[nit] logger=None passed from finalize.py loses structured install-context logging; stdlib root logger may emit to unexpected handlers at
src/apm_cli/install/phases/finalize.py:32
OSS Growth Hacker
-
[recommended] Producer compile.md lacks the distribution hook for package authors at
docs/src/content/docs/producer/compile.md
The Global compilation section reads as an ops procedure. It never states the compelling consequence: a package author publishes once, and every user who runs 'apm install -g (pkg)' gets instructions surfaced in ALL their AI tools automatically. That is the 'npm install -g' moment. Without that sentence, a package author has no reason to invest in global instructions.
Suggested: Add a 1-2 sentence framing hook: "Publishing a package with global instructions means every user who runs 'apm install -g (your-pkg)' gets your context surfaced in Claude, Codex, Copilot, Cursor, Gemini, and Windsurf automatically -- no project setup required." -
[recommended] Reference CLI page buries -g in a table with no example; the feature deserves a runnable snippet at
docs/src/content/docs/reference/cli/compile.md
A developer scanning for 'how do I make my package affect all AI tools' will not stop at the one-row table. An 'Install globally and verify' example block under Examples would make the flow copy-pasteable. -
[nit] Constraints bullet 'Skills-only packages do not write root files' is user-hostile without a reason at
docs/src/content/docs/producer/compile.md
Suggested: "Skills-only packages (no global instructions) do not write root files -- skills are deployed directly into harness directories by apm install, not compiled into root context files." -
[nit] The closing sentence of producer/compile.md misses a callout to -g as a publish motivation at
docs/src/content/docs/producer/compile.md
Auth Expert -- inactive
PR does not touch auth flows, token management, credential resolution, or AuthResolver inputs; compile engine reads local ~/.apm/apm_modules only.
Doc Writer
-
[recommended] install-packages.md does not mention that apm install -g auto-compiles global root context files at
docs/src/content/docs/consumer/install-packages.md
A consumer reading install-packages.md sees 'apm install -g (package)' but has no signal that root context files (/.claude/CLAUDE.md etc.) are written as a side effect. This is a behavioural change visible to users./.claude/CLAUDE.md, ~/.codex/AGENTS.md, etc.) from any global instructions in the installed packages. Re-run manually with 'apm compile -g'. See Global compilation."
Suggested: Add a note under the 'apm install -g' section: "After a global install, APM automatically compiles user-scope root context files ( -
[nit] producer/compile.md Global section uses apply_to: but the rest of the page uses applyTo: (camelCase frontmatter convention) at
docs/src/content/docs/producer/compile.md
Suggested: Change 'apply_to:' to 'applyTo:' on the relevant line. -
[nit] producer/compile.md global paths list silently omits opencode (~/.config/opencode/AGENTS.md) at
docs/src/content/docs/producer/compile.md
The source docstring explicitly lists opencode with its non-standard XDG path; leaving it out makes the 'etc.' harder to interpret. -
[nit] reference/cli/compile.md global flag hardcodes
/.apm/apm_modules but the path is dynamically resolved via get_apm_dir(InstallScope.USER) which may honour XDG_DATA_HOME at/.apm/apm_modules' with 'the user-scope apm_modules directory (default: ~/.apm/apm_modules)'.docs/src/content/docs/reference/cli/compile.md
Suggested: Replace '
Test Coverage Expert
-
[recommended] No integration-with-fixtures test proves apm install -g writes CLAUDE.md to the user home directory
All 39 new tests mock compile_user_root_contexts at the boundary -- none drive a real apm install -g invocation against a fixture package tree and assert the file appears on disk. The install pipeline tier floor is integration-with-fixtures.
Proof (missing):tests/integration/test_global_install_e2e.py::test_install_global_writes_user_root_context_claude_md-- proves: Running 'apm install -g' with a package that has global instructions writes ~/.claude/CLAUDE.md containing the APM-generated marker [secure-by-default]
assert (fake_home / '.claude' / 'CLAUDE.md').exists() and '<!-- Generated by APM CLI' in (fake_home / '.claude' / 'CLAUDE.md').read_text() -
[recommended] CLAUDE_CONFIG_DIR env-var override path in _resolve_deploy_root has no test in the new compilation engine at
tests/unit/compilation/test_user_root_context.py
Grepped tests/unit/compilation/test_user_root_context.py for 'CLAUDE_CONFIG_DIR' -- zero hits. If CLAUDE_CONFIG_DIR handling in for_scope silently regresses, no test in the new suite would catch it.
Proof (missing):tests/unit/compilation/test_user_root_context.py::test_claude_config_dir_respected_as_deploy_root-- proves: When CLAUDE_CONFIG_DIR is set, compile_user_root_contexts writes CLAUDE.md into the custom directory
monkeypatch.setenv('CLAUDE_CONFIG_DIR', str(custom_dir)); assert result[0]['path'] == custom_dir / 'CLAUDE.md' -
[nit] Overwrite-guard test does not assert that APM-generated file is updated when instructions change (idempotency round-trip) at
tests/unit/compilation/test_user_root_context.py
Proof (missing):tests/unit/compilation/test_user_root_context.py::test_apm_generated_file_updated_when_instructions_change-- proves: A file written by APM on a first compile is overwritten on a second compile when instructions change
assert second_result[0]['status'] == 'written' and output_path.read_text() != first_content
This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.
Generated by PR Review Panel for issue #1632 · sonnet46 9M · ◷
1 similar comment
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 2 | 2 | Clean new module; one structural gap (InstallLogger API mismatch silently swallowed) and one minor abstraction leak worth addressing before merge. |
| CLI Logging Expert | 0 | 2 | 3 | skipped-hand-authored shown as [i] info not warning; install path silently drops error results; missing symbol on apm_modules-not-found error. |
| DevX UX Expert | 0 | 3 | 2 | Flag ergonomics and happy-path UX are solid; two gaps: mutual-exclusion errors exit(2) without hinting the fix, and silent auto-compile on install -g has no discoverable signal on the non-verbose path. |
| Supply Chain Security Expert | 2 | 3 | 1 | Package instruction content flows verbatim into AI root context files; no sanitization, size cap, or path containment guard on output_path. |
| OSS Growth Hacker | 0 | 2 | 2 | Global compile is a killer distribution story but docs bury the lede -- the 'publish once, land in every AI tool' hook is missing from both pages. |
| Doc Writer | 0 | 1 | 3 | New global-compile docs are structurally sound and code-accurate; one consumer-page gap and two small inaccuracies need fixing. |
| Test Coverage Expert | 0 | 2 | 1 | 39 unit tests cover the engine well; the core user promise (apm install -g writes CLAUDE.md) has no integration-with-fixtures test, and CLAUDE_CONFIG_DIR is untested in the new engine. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
- [Supply Chain Security Expert] (blocking-severity) Add content sanitization: hard size cap per instruction (8 KB) and rejection of instruction content containing the APM marker string, before any write to AI root context files -- verbatim package content in
~/.claude/CLAUDE.mdenables supply-chain prompt injection via compromised transitive dependencies; existing package trust model does not mitigate this because instruction content is never inspected before install - [Supply Chain Security Expert] (blocking-severity) Call
ensure_path_within(output_path, deploy_root)frompath_security.pybefore any write incompile_user_root_contexts-- project's own security chokepoint contract requires all filesystem writes derived from dynamic inputs to pass throughensure_path_within;resolved_deploy_rootis dynamically computed and the guard is missing - [Test Coverage Expert] (blocking-severity) Add an integration-with-fixtures test that drives a real
apm install -gagainst a fixture package tree and asserts~/.claude/CLAUDE.mdis written with the APM marker -- evidenceoutcome: missingonintegration-with-fixturestier for the PR's core user promise; unit tests mockcompile_user_root_contextsat the boundary and will not catch install-pipeline wiring regressions - [DevX UX Expert] Emit a non-verbose output line when
apm install -gwrites root context files; side-effects outside the project tree must be visible on the default output path -- silent writes to~/.claude/CLAUDE.mdon install -g violate the pragmatic-as-npm ergonomic bar; users cannot audit what install did to their AI configuration - [CLI Logging Expert] Surface error results from
_compile_user_root_contexts_after_install; current code only messages onwrittenentries, silently discarding OS write failures -- a user whose CLAUDE.md was not updated due to a write failure sees a successful install summary with no indication that the root context step failed
Architecture
classDiagram
direction LR
class TargetProfile {
<<ValueObject>>
+name str
+compile_family str|None
+resolved_deploy_root Path|None
+for_scope(user_scope) TargetProfile|None
}
class compile_user_root_contexts {
<<Pure>>
+targets Iterable
+source_root Path
+dry_run bool
+logger Logger|None
+returns list[dict]
}
class AgentsCompiler {
<<Compiler>>
+_COPILOT_ROOT_GENERATED_MARKER str
}
class _handle_global_flag {
<<CLIHandler>>
+dry_run bool
+returns int
}
class finalize_run {
<<InstallPhase>>
+ctx InstallContext
+returns InstallResult
}
class InstallContext {
<<Context>>
+scope InstallScope
+logger InstallLogger|None
}
class InstallLogger {
<<CommandLogger>>
+verbose_detail(msg)
}
class discover_primitives {
<<IOBoundary>>
+root str
+returns PrimitivesResult
}
class compile_user_root_contexts:::touched
class _handle_global_flag:::touched
class finalize_run:::touched
compile_user_root_contexts ..> TargetProfile : iterates
compile_user_root_contexts ..> AgentsCompiler : reads marker constant
compile_user_root_contexts ..> discover_primitives : calls
_handle_global_flag ..> compile_user_root_contexts : calls
finalize_run ..> compile_user_root_contexts : calls via helper
finalize_run ..> InstallContext : reads scope
InstallContext *-- InstallLogger : optional
note for compile_user_root_contexts "Collect-then-render: discovers all\nglobal instructions, then writes per-target"
note for finalize_run "Logger API mismatch: passes logger=None\nbecause InstallLogger != stdlib Logger"
classDef touched fill:#fff3b0,stroke:#d47600
flowchart TD
A(["apm install -g"]) --> B["finalize.run()\ninstall/phases/finalize.py"]
B --> C{"ctx.scope == USER?"}
C -- No --> Z(["InstallResult"])
C -- Yes --> D["_compile_user_root_contexts_after_install(ctx)\nfinalize.py:22"]
D --> E["[I/O] get_apm_dir(USER)\ncore/scope.py"]
E --> F["compile_user_root_contexts(targets, source_root)\ncompilation/user_root_context.py:100"]
F --> G["[FS] check apm_modules dir exists\n~/.apm/apm_modules"]
G -- Not found --> H(["return empty list"])
G -- Found --> I["[I/O] discover_primitives(apm_modules)\nprimitives/discovery.py"]
I --> J["filter: instructions with no apply_to"]
J --> K["for each target in KNOWN_TARGETS"]
K --> L["target.for_scope(user_scope=True)"]
L -- None --> K
L -- Profile --> M{"compile_family in _ROOT_FILENAME?"}
M -- No --> K
M -- Yes --> N{"global_instructions empty?"}
N -- Yes --> O["status: skipped-no-instructions"] --> K
N -- No --> P["_resolve_deploy_root(scoped)\nresolved_deploy_root or home/root_dir"]
P --> Q["_generate_content(instructions)\n_finalize_build_id + marker"]
Q --> R{"[I/O] output_path.exists()?"}
R -- No --> S["[FS] mkdir + write_text\nstatus: written"]
R -- Yes --> T["[I/O] read_text existing file"]
T --> U{"APM marker present?"}
U -- No --> V["status: skipped-hand-authored"]
U -- Yes --> W{"content unchanged?"}
W -- Yes --> X["status: unchanged"]
W -- No --> S
S --> K
K -- done --> Y["return results list"]
Y --> Z
A2(["apm compile --global"]) --> H2["_handle_global_flag(dry_run)\ncompile/cli.py:323"]
H2 --> E
H2 --> YY["render per-target status to console"]
sequenceDiagram
actor User
participant CLI as apm CLI
participant Finalize as finalize.run()
participant Engine as compile_user_root_contexts()
participant FS as Filesystem
User->>CLI: apm install -g pkg
CLI->>Finalize: run(ctx) [scope=USER]
Finalize->>Engine: compile_user_root_contexts(KNOWN_TARGETS, ~/.apm)
Engine->>FS: discover_primitives(~/.apm/apm_modules)
FS-->>Engine: primitives
loop per target (claude, copilot, cursor, ...)
Engine->>FS: read existing root file (if any)
FS-->>Engine: existing content or not-found
alt hand-authored (no APM marker)
Engine-->>Engine: skip
else APM-owned or new
Engine->>FS: write CLAUDE.md / AGENTS.md / GEMINI.md
end
end
Engine-->>Finalize: results list
Finalize-->>CLI: InstallResult
CLI-->>User: success message + verbose_detail count
Recommendation
Hold this PR for three targeted fixes before merge: (1) add minimum content sanitization -- per-instruction size cap and marker-string rejection -- in compile_user_root_contexts before any write; (2) add ensure_path_within(output_path, deploy_root) immediately after output_path is resolved; (3) add at least one integration-with-fixtures test that asserts the core promise end-to-end. These are all small, scoped changes that do not require design discussion. Once those three land, the remaining recommended findings (non-verbose install output, error surfacing, logger API bridge, doc additions) should be addressed in the same PR rather than deferred -- they are the difference between a feature that feels polished at launch and one that accumulates a tail of follow-up issues. The strategic framing and code structure are excellent; this is worth the extra day to harden.
Full per-persona findings
Python Architect
-
[recommended] Logger API mismatch is silently swallowed by passing logger=None from finalize.py at
src/apm_cli/install/phases/finalize.py:32
compile_user_root_contexts accepts a stdlib logging.Logger, but ctx.logger is an InstallLogger with a different API (verbose_detail, not debug/info). All compilation debug/warning output during install goes to the root stdlib logger, silently bypassing the structured CLI output pipeline.
Suggested: Add a thin property to InstallContext (or InstallLogger) that returns a stdlib logging.Logger wrapping verbose_detail/warn calls, then pass that logger to compile_user_root_contexts. -
[recommended] discover_primitives called once per compile_user_root_contexts call but targets loop iterates inside; primitive discovery design is fragile if a future caller wraps in a loop at
src/apm_cli/compilation/user_root_context.py:100
The primitives object is never type-hinted; the function signature uses object for TargetProfile instances, eroding IDE support and the architecture's own contract. -
[nit] _ROOT_FILENAME map uses 'vscode' key but copilot target returns compile_family='vscode' at user scope -- add clarifying comment at
src/apm_cli/compilation/user_root_context.py:35 -
[nit] finalize.py imports InstallScope twice: once under TYPE_CHECKING and once as a lazy runtime import inside run() at
src/apm_cli/install/phases/finalize.py:104
Move InstallScope to a top-level runtime import and remove the duplicate lazy import.
CLI Logging Expert
-
[recommended] skipped-hand-authored uses _rich_info/symbol=info but should warn the user at
src/apm_cli/commands/compile/cli.py:365
A hand-authored file being silently skipped is a yellow-flag condition: the user's global instructions were NOT applied to that target. Rendering it as blue [i] buries an actionable message.
Suggested: Use _rich_warning(f"{tname}: skipped (hand-authored) {path} -- add APM marker or use --force to overwrite", symbol="warning") -
[recommended] install path swallows error results from _compile_user_root_contexts_after_install silently at
src/apm_cli/install/phases/finalize.py:33
finalize.py only calls verbose_detail when results contain 'written' entries. If every result is an error (OS write failure), the function returns silently. The user sees a successful install summary but root context files were not updated. -
[nit] _rich_error at line 337 has no symbol= so the apm_modules-not-found error lacks the [x] prefix at
src/apm_cli/commands/compile/cli.py:337 -
[nit] _rich_info at line 351 (no results path) has no symbol= while all per-entry info lines do at
src/apm_cli/commands/compile/cli.py:351 -
[nit] symbol=light_bulb and symbol=page used elsewhere in file are not in STATUS_SYMBOLS and are silently dropped (pre-existing, not introduced by this PR) at
src/apm_cli/commands/compile/cli.py:872
DevX UX Expert
-
[recommended] Mutual-exclusion errors print the problem but not the fix at
src/apm_cli/commands/compile/cli.py:552
'--global cannot be combined with --watch' emits the error then exit(2). npm/pip pattern is to append a one-line hint: e.g. "Drop --watch and re-run, or use 'apm compile --global' separately." -
[recommended] Auto-compile triggered by apm install -g is invisible on the default (non-verbose) output path at
src/apm_cli/install/phases/finalize.py:34
finalize.py gates the message on verbose_detail. A user running plain 'apm install -g some-pkg' sees no indication that ~/.claude/CLAUDE.md was just written. Silent side-effects to files outside the project tree are especially surprising. -
[recommended] Empty-modules info message does not explain that only instructions without applyTo: are compiled globally at
src/apm_cli/commands/compile/cli.py:351
A user who installed a skills-only package globally will hit 'No user-scope targets produced output' and not understand why nothing happened.
Suggested: "No global instructions found in ~/.apm/apm_modules. Only instructions without an applyTo: field are compiled globally. Skills and scoped instructions are skipped." -
[nit] Help text for --global lists only ~/.claude/CLAUDE.md with 'etc.' -- spell out the full list or point to --dry-run at
src/apm_cli/commands/compile/cli.py:484 -
[nit] skipped-hand-authored output uses 'info' symbol rather than a warning symbol at
src/apm_cli/commands/compile/cli.py:366
Supply Chain Security Expert
-
[blocking] Unfiltered package content written verbatim into AI root context files enables supply-chain prompt injection at
src/apm_cli/compilation/user_root_context.py:93
compile_user_root_contexts() concatenates instruction.content.strip() directly from any installed package into ~/.claude/CLAUDE.md, ~/.codex/AGENTS.md, etc. A compromised package maintainer or dependency-confusion package publishes adversarial AI directives. The install hook fires automatically without user acknowledgement. There is no allowlist, no size cap, and no pre-write confirmation.
Suggested: Apply a hard size cap per instruction (8 KB) and a total cap per generated file (64 KB). Reject instruction content containing the APM marker string. Consider a pre-write summary prompt for --yes-less invocations. -
[blocking] output_path is not validated with ensure_path_within; a TargetProfile with an attacker-influenced root_dir could write outside the expected directory at
src/apm_cli/compilation/user_root_context.py:188
_resolve_deploy_root() returns profile.resolved_deploy_root which is dynamically set, potentially by a resolver callback. output_path = deploy_root / root_filename is written without calling ensure_path_within or validate_path_segments, violating the project's path_security.py chokepoint contract.
Suggested: Call ensure_path_within(output_path, deploy_root) from src/apm_cli/utils/path_security.py before any write. -
[recommended] TOCTOU race between output_path.exists() check and output_path.write_text() allows hand-authored file to be overwritten at
src/apm_cli/compilation/user_root_context.py:193
Sequence: (1) exists(), (2) read_text(), (3) marker check, (4) write_text(). A concurrent process could swap a hand-authored file between steps (3) and (4).
Suggested: Write to output_path.with_suffix('.tmp') then atomically rename with output_path.replace(). -
[recommended] APM marker is a plain human-readable string; a package can embed it to confuse future overwrite checks at
src/apm_cli/compilation/user_root_context.py:203
The marker '' is checked with a substring test. Instruction content containing the marker string will degrade the integrity signal and could enable social-engineering attacks.
Suggested: Detect and reject instruction content containing the marker string before compilation, logging a warning with the offending package name. -
[recommended] Install hook fires unconditionally for every USER-scope install, even when no newly installed package provides global instructions at
src/apm_cli/install/phases/finalize.py:107
The attack window opens for ANY user-scope install, including unrelated ones that coincidentally pull in a transitive dependency with global instructions. -
[nit] logger=None passed from finalize.py loses structured install-context logging; stdlib root logger may emit to unexpected handlers at
src/apm_cli/install/phases/finalize.py:32
OSS Growth Hacker
-
[recommended] Producer compile.md lacks the distribution hook for package authors at
docs/src/content/docs/producer/compile.md
The Global compilation section reads as an ops procedure. It never states the compelling consequence: a package author publishes once, and every user who runs 'apm install -g (pkg)' gets instructions surfaced in ALL their AI tools automatically. That is the 'npm install -g' moment. Without that sentence, a package author has no reason to invest in global instructions.
Suggested: Add a 1-2 sentence framing hook: "Publishing a package with global instructions means every user who runs 'apm install -g (your-pkg)' gets your context surfaced in Claude, Codex, Copilot, Cursor, Gemini, and Windsurf automatically -- no project setup required." -
[recommended] Reference CLI page buries -g in a table with no example; the feature deserves a runnable snippet at
docs/src/content/docs/reference/cli/compile.md
A developer scanning for 'how do I make my package affect all AI tools' will not stop at the one-row table. An 'Install globally and verify' example block under Examples would make the flow copy-pasteable. -
[nit] Constraints bullet 'Skills-only packages do not write root files' is user-hostile without a reason at
docs/src/content/docs/producer/compile.md
Suggested: "Skills-only packages (no global instructions) do not write root files -- skills are deployed directly into harness directories by apm install, not compiled into root context files." -
[nit] The closing sentence of producer/compile.md misses a callout to -g as a publish motivation at
docs/src/content/docs/producer/compile.md
Auth Expert -- inactive
PR does not touch auth flows, token management, credential resolution, or AuthResolver inputs; compile engine reads local ~/.apm/apm_modules only.
Doc Writer
-
[recommended] install-packages.md does not mention that apm install -g auto-compiles global root context files at
docs/src/content/docs/consumer/install-packages.md
A consumer reading install-packages.md sees 'apm install -g (package)' but has no signal that root context files (/.claude/CLAUDE.md etc.) are written as a side effect. This is a behavioural change visible to users./.claude/CLAUDE.md, ~/.codex/AGENTS.md, etc.) from any global instructions in the installed packages. Re-run manually with 'apm compile -g'. See Global compilation."
Suggested: Add a note under the 'apm install -g' section: "After a global install, APM automatically compiles user-scope root context files ( -
[nit] producer/compile.md Global section uses apply_to: but the rest of the page uses applyTo: (camelCase frontmatter convention) at
docs/src/content/docs/producer/compile.md
Suggested: Change 'apply_to:' to 'applyTo:' on the relevant line. -
[nit] producer/compile.md global paths list silently omits opencode (~/.config/opencode/AGENTS.md) at
docs/src/content/docs/producer/compile.md
The source docstring explicitly lists opencode with its non-standard XDG path; leaving it out makes the 'etc.' harder to interpret. -
[nit] reference/cli/compile.md global flag hardcodes
/.apm/apm_modules but the path is dynamically resolved via get_apm_dir(InstallScope.USER) which may honour XDG_DATA_HOME at/.apm/apm_modules' with 'the user-scope apm_modules directory (default: ~/.apm/apm_modules)'.docs/src/content/docs/reference/cli/compile.md
Suggested: Replace '
Test Coverage Expert
-
[recommended] No integration-with-fixtures test proves apm install -g writes CLAUDE.md to the user home directory
All 39 new tests mock compile_user_root_contexts at the boundary -- none drive a real apm install -g invocation against a fixture package tree and assert the file appears on disk. The install pipeline tier floor is integration-with-fixtures.
Proof (missing):tests/integration/test_global_install_e2e.py::test_install_global_writes_user_root_context_claude_md-- proves: Running 'apm install -g' with a package that has global instructions writes ~/.claude/CLAUDE.md containing the APM-generated marker [secure-by-default]
assert (fake_home / '.claude' / 'CLAUDE.md').exists() and '<!-- Generated by APM CLI' in (fake_home / '.claude' / 'CLAUDE.md').read_text() -
[recommended] CLAUDE_CONFIG_DIR env-var override path in _resolve_deploy_root has no test in the new compilation engine at
tests/unit/compilation/test_user_root_context.py
Grepped tests/unit/compilation/test_user_root_context.py for 'CLAUDE_CONFIG_DIR' -- zero hits. If CLAUDE_CONFIG_DIR handling in for_scope silently regresses, no test in the new suite would catch it.
Proof (missing):tests/unit/compilation/test_user_root_context.py::test_claude_config_dir_respected_as_deploy_root-- proves: When CLAUDE_CONFIG_DIR is set, compile_user_root_contexts writes CLAUDE.md into the custom directory
monkeypatch.setenv('CLAUDE_CONFIG_DIR', str(custom_dir)); assert result[0]['path'] == custom_dir / 'CLAUDE.md' -
[nit] Overwrite-guard test does not assert that APM-generated file is updated when instructions change (idempotency round-trip) at
tests/unit/compilation/test_user_root_context.py
Proof (missing):tests/unit/compilation/test_user_root_context.py::test_apm_generated_file_updated_when_instructions_change-- proves: A file written by APM on a first compile is overwritten on a second compile when instructions change
assert second_result[0]['status'] == 'written' and output_path.read_text() != first_content
This panel is advisory. It does not block merge. Re-apply the
panel-review label after addressing feedback to re-run.
Generated by PR Review Panel for issue #1632 · sonnet46 9M · ◷
# Conflicts: # src/apm_cli/commands/compile/cli.py
TL;DR
apm install -gnow auto-compiles global instructions into user-scope root context files (CLAUDE.md,AGENTS.md,GEMINI.md). Addsapm compile --global/-gas a standalone re-compile command. Closes #1485.Problem
When
apm install -ginstalled a package containing global (noapply_to) instructions, those instructions landed in~/.apm/apm_modulesbut were never surfaced to AI coding tools. Users had to manually run a project-scoped compile or live without the instructions taking effect. There was also no standalone command to re-compile the user-scope root files without reinstalling.Approach
Core engine --
compilation/user_root_context.pyIntroduces
compile_user_root_contexts(targets, source_root, *, dry_run, logger):~/.apm/apm_modules(packages with noapply_to).TargetProfile.for_scope(user_scope=True)--Nonemeans skip.compile_familyto root filename:claude -> CLAUDE.md,agents -> AGENTS.md,gemini -> GEMINI.md._COPILOT_ROOT_GENERATED_MARKERfromagents_compiler.py.CLI --
compilecommandAdds
--global/-gflag toapm compile. Mutually exclusive with--watchand--root. Callscompile_user_root_contextswith all known targets and~/.apmas the source root.Install hook --
install/phases/finalize.pyAfter a user-scope install (
ctx.scope is InstallScope.USER),finalizecalls_compile_user_root_contexts_after_install(ctx). All imports in the hook are lazy to avoid circular import paths.Key Design Decisions
_COPILOT_ROOT_GENERATED_MARKEReliminates drift between compilers.finalize.pysits deep in the install chain; eager imports would create a circular dependency.CLAUDE_CONFIG_DIRhonouredprofile.resolved_deploy_rootfromfor_scope(user_scope=True)already respects the env var.Validation
tests/unit/compilation/test_user_root_context.py-- 16 tests (engine logic, overwrite guard, skip conditions)tests/unit/compilation/test_compile_global_flag.py-- 13 tests (CLI flag, mutual-exclusion, dispatch)tests/unit/install/phases/test_finalize_user_compile.py-- 10 tests (hook invocation, scope guard)ruff-- silent.pylint-- 10.00 / 10.