Skip to content

feat(install): compile global instructions into user-scope root context files on install -g#1632

Open
danielmeppiel wants to merge 13 commits into
mainfrom
danielmeppiel/issue-1485
Open

feat(install): compile global instructions into user-scope root context files on install -g#1632
danielmeppiel wants to merge 13 commits into
mainfrom
danielmeppiel/issue-1485

Conversation

@danielmeppiel
Copy link
Copy Markdown
Collaborator

TL;DR

apm install -g now auto-compiles global instructions into user-scope root context files (CLAUDE.md, AGENTS.md, GEMINI.md). Adds apm compile --global / -g as a standalone re-compile command. Closes #1485.


Problem

When apm install -g installed a package containing global (no apply_to) instructions, those instructions landed in ~/.apm/apm_modules but 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.py

Introduces compile_user_root_contexts(targets, source_root, *, dry_run, logger):

  • Discovers global instructions from ~/.apm/apm_modules (packages with no apply_to).
  • Resolves each target's deploy root via TargetProfile.for_scope(user_scope=True) -- None means skip.
  • Maps compile_family to root filename: claude -> CLAUDE.md, agents -> AGENTS.md, gemini -> GEMINI.md.
  • Emits content with the shared APM marker + build-ID; reuses _COPILOT_ROOT_GENERATED_MARKER from agents_compiler.py.
  • Overwrite protection: files without the APM marker are never touched.

CLI -- compile command

Adds --global / -g flag to apm compile. Mutually exclusive with --watch and --root. Calls compile_user_root_contexts with all known targets and ~/.apm as the source root.

Install hook -- install/phases/finalize.py

After a user-scope install (ctx.scope is InstallScope.USER), finalize calls _compile_user_root_contexts_after_install(ctx). All imports in the hook are lazy to avoid circular import paths.


Key Design Decisions

Decision Rationale
Overwrite protection Hand-authored root files must never be silently clobbered; the APM marker is the ownership signal.
Skip on no global instructions Skills-only packages write nothing; avoids empty or misleading root files.
Marker reuse Single definition of _COPILOT_ROOT_GENERATED_MARKER eliminates drift between compilers.
Lazy imports in finalize hook finalize.py sits deep in the install chain; eager imports would create a circular dependency.
CLAUDE_CONFIG_DIR honoured profile.resolved_deploy_root from for_scope(user_scope=True) already respects the env var.
ASCII-only output Consistent with the rest of the CLI; no emoji in generated file headers.

Validation

  • 39 new tests across three suites:
    • 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)
  • 2657 existing tests pass -- zero regressions.
  • ruff -- silent.
  • pylint -- 10.00 / 10.
  • Auth-signal lint -- clean.

danielmeppiel and others added 11 commits June 3, 2026 08:23
… 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>
Copilot AI review requested due to automatic review settings June 3, 2026 06:45
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/-g handling to apm 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

Comment on lines +24 to +29
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())
Comment on lines +329 to +336
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():
Comment on lines +47 to +57
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,
),
Comment on lines +54 to +63
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,
),
Comment on lines +221 to +226
## 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:
Comment on lines +95 to +101
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>
@danielmeppiel
Copy link
Copy Markdown
Collaborator Author

APM Review Panel: Advisory Recommendation

PR #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
CEO Synthesizer: apm-ceo


Arbitration

All six active panelists converge on a single critical finding: get_source_root(USER) returns ~ not ~/.apm, so compile_user_root_contexts looks for ~/apm_modules which never exists, and the feature silently skips compilation. The test-coverage-expert confirms the tests were green-but-wrong -- they mocked the same incorrect function, proving the wrong contract. This is the textbook case of a test suite that validates the bug rather than catching it. The python-architect and test-coverage-expert are in full agreement; no dissent to arbitrate.

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 --global/-g, and the finalize hook cannot double-compile. The architecture ships; the wiring had a one-line bug (get_source_root -> get_apm_dir) that has been folded in this shepherd run. The test mocks are corrected to patch get_apm_dir. This feature now delivers on its core promise: apm install -g automatically configures AI tools at user scope.

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

Principle Verdict
Portable by manifest ALIGNED -- root context files emitted to canonical harness locations
Secure by default PARTIAL -- overwrite-protection marker is sound; path containment deferred (forward-safety)
Governed by policy ALIGNED -- compile engine respects apm.yml applyTo declarations and scope guards
Multi-harness / multi-host ALIGNED -- engine writes to all configured harness targets
OSS community-driven ALIGNED -- closes community-filed issue #1485, no contributor friction
Pragmatic as npm ALIGNED -- install -g auto-compiling mirrors npm post-install hook mental model

Growth Note

The 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)

Priority Persona Finding Blocking?
1 (folded) python-architect + test-coverage-expert get_source_root -> get_apm_dir in finalize.py and compile/cli.py; test mocks corrected YES -- folded in this run
2 (folded) devx-ux-expert Mutual-exclusion errors (--global+--watch, --global+--root) now exit sys.exit(2) no -- folded
3 (folded) doc-writer --global/-g entry added to CLI reference page no -- folded
4 oss-growth-hacker Add one-line mention + example in README quickstart showing install -g auto-configures tools no -- defer (README requires maintainer approval)
5 supply-chain-security-expert Add ensure_path_within(home, deploy_root) guard for forward-safety no -- defer (hardcoded paths safe today)

Ship Recommendation

ship_with_followups

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.

@danielmeppiel danielmeppiel added the panel-review Trigger the apm-review-panel gh-aw workflow label Jun 3, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 3, 2026

APM Review Panel: needs_rework

NEEDS REWORK: two supply-chain security blockers (prompt-injection surface, missing path-containment guard) and one integration-test gap must be resolved before merge; feature positioning is strategically excellent

cc @danielmeppiel @sergio-sisternes-epam -- a fresh advisory pass is ready for your review.

PR #1632 ships APM's most strategically significant distribution primitive to date -- "publish once, land instructions in every AI tool" is the npm install -g moment the OSS Growth Hacker correctly identifies. The code quality is clean, the 39-test suite is thorough at the unit tier, and the UX ergonomics are largely solid. However, two supply-chain-security findings are genuinely blocking and are not mitigated by APM's existing package trust model.

On the first blocking finding: instruction.content.strip() flows verbatim from any installed package into ~/.claude/CLAUDE.md, ~/.codex/AGENTS.md, and their siblings. The key distinction from standard npm install -g risk is the target: adversarial content here does not execute code, it injects AI directives that persist silently across every AI session the user opens -- indefinitely, across every tool. A compromised transitive dependency (not even the top-level package the user requested) can rewrite the user's global AI persona without any confirmation or audit trail. APM's package trust model (users choose what to install) does not transfer adequate mitigation: users do not inspect instruction content before install, and the attack vector is transitive. A hard size cap per instruction (e.g. 8 KB) and a marker-string rejection guard are the minimum viable mitigations; a pre-write summary prompt for --yes-less invocations is strongly preferred but can be a follow-up if the size/marker guards ship in this PR. This finding is blocking.

On the second blocking finding: output_path = deploy_root / root_filename is written without calling ensure_path_within(output_path, deploy_root) from path_security.py. The project's own docstring -- "Every filesystem operation whose target is derived from user-controlled input must pass through one of these guards" -- is explicit. profile.resolved_deploy_root is dynamically resolved and could be influenced by a resolver callback; ensure_path_within is a one-line addition that closes this. This is a violation of APM's own security chokepoint contract and is blocking.

On the integration-test gap: the test-coverage-expert's outcome: missing on an integration-with-fixtures test for the core promise -- "apm install -g writes CLAUDE.md at ~/.claude/CLAUDE.md" -- is load-bearing evidence, not opinion. The install hook fires in a real pipeline; the 39 unit tests mock compile_user_root_contexts at the boundary and will not catch a regression where the hook is silently skipped or misconfigured. Given this PR touches install pipeline wiring, an integration fixture test is the right regression trap. This finding is elevated to blocking by its evidence tier and the secure-by-default surface it guards.

The silent auto-compile on install -g (DevX-UX finding, supported by cli-logging-expert) -- writing files outside the project tree with zero non-verbose output -- is a significant UX gap but does not rise to blocking. Users running apm install -g must see at minimum a non-verbose line that their AI context files were modified; this is not optional for a side-effect that reaches outside the project. The current implementation gates the message on verbose_detail, which is invisible on default output. This should ship with the fix but is not a security issue.

The python-architect and cli-logging-expert recommended findings (logger API mismatch, error results silently swallowed) are real gaps that should be addressed alongside the blockers; the logger mismatch is particularly relevant because it means compilation errors during install are invisible to the structured CLI output pipeline.

The oss-growth-hacker's framing is correct and should be amplified in the release post. The doc-writer's consumer-page gap (install-packages.md not mentioning the root-file side effect) must ship with the feature to avoid user confusion -- it is a behavioral change visible to every global installer.

Dissent. The supply-chain-security-expert classified the unconditional install hook as recommended, while the DevX-UX expert independently classified the silent auto-compile as recommended. Both converge on the same code path (finalize.py:107 / finalize.py:34). The CEO sides with both on the UX dimension but does not elevate the hook-unconditional-firing finding to blocking on its own; the real attack window concern is better addressed by the content sanitization (blocking finding 1). The test-coverage-expert's nit on idempotency round-trip testing remains a nit -- idempotency is important but not on a secure-by-default surface that warrants elevation. The cli-logging-expert and devx-ux-expert independently flagged skipped-hand-authored as info when it should be a warning; both panelists agree, no dissent to arbitrate.

Aligned with: Portable by manifest -- engine correctly iterates KNOWN_TARGETS and maps compile_family to per-tool filenames, the right extensibility point for future tools. Secure by default -- currently fails: verbatim package content flows into AI root context files without size cap or marker-string rejection; output_path not validated against ensure_path_within. Pragmatic as npm -- global compile flag mirrors npm/pip -g ergonomics well; mutual-exclusion error hints and non-verbose install output needed to close the ergonomic gap.

Growth signal. The "publish once, land in Claude/Codex/Copilot/Cursor/Gemini/Windsurf for every global installer" framing is the npm install -g moment for AI toolchain config. Once the two security blockers are resolved, this feature should anchor the release post with exactly that analogy. The marketplace "AI tool configurators" category is a high-signal growth vector -- even 3-5 popular packages adopting global instructions creates a visible pull for the rest. The "Make your package global-aware" producer quickstart section should ship as part of this PR's doc additions, not as a follow-up, because it converts the feature launch into a contributor hook from day one.

Panel summary

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

  1. [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.md enables supply-chain prompt injection via compromised transitive dependencies; existing package trust model does not mitigate this because instruction content is never inspected before install
  2. [Supply Chain Security Expert] (blocking-severity) Call ensure_path_within(output_path, deploy_root) from path_security.py before any write in compile_user_root_contexts -- project's own security chokepoint contract requires all filesystem writes derived from dynamic inputs to pass through ensure_path_within; resolved_deploy_root is dynamically computed and the guard is missing
  3. [Test Coverage Expert] (blocking-severity) Add an integration-with-fixtures test that drives a real apm install -g against a fixture package tree and asserts ~/.claude/CLAUDE.md is written with the APM marker -- evidence outcome: missing on integration-with-fixtures tier for the PR's core user promise; unit tests mock compile_user_root_contexts at the boundary and will not catch install-pipeline wiring regressions
  4. [DevX UX Expert] Emit a non-verbose output line when apm install -g writes root context files; side-effects outside the project tree must be visible on the default output path -- silent writes to ~/.claude/CLAUDE.md on install -g violate the pragmatic-as-npm ergonomic bar; users cannot audit what install did to their AI configuration
  5. [CLI Logging Expert] Surface error results from _compile_user_root_contexts_after_install; current code only messages on written entries, 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
Loading
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"]
Loading
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
Loading

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.
    Suggested: Add a note under the 'apm install -g' section: "After a global install, APM automatically compiles user-scope root context files (
    /.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."

  • [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 docs/src/content/docs/reference/cli/compile.md
    Suggested: Replace '
    /.apm/apm_modules' with 'the user-scope apm_modules directory (default: ~/.apm/apm_modules)'.

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
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 3, 2026

APM Review Panel: needs_rework

NEEDS REWORK: two supply-chain security blockers (prompt-injection surface, missing path-containment guard) and one integration-test gap must be resolved before merge; feature positioning is strategically excellent

cc @danielmeppiel @sergio-sisternes-epam -- a fresh advisory pass is ready for your review.

PR #1632 ships APM's most strategically significant distribution primitive to date -- "publish once, land instructions in every AI tool" is the npm install -g moment the OSS Growth Hacker correctly identifies. The code quality is clean, the 39-test suite is thorough at the unit tier, and the UX ergonomics are largely solid. However, two supply-chain-security findings are genuinely blocking and are not mitigated by APM's existing package trust model.

On the first blocking finding: instruction.content.strip() flows verbatim from any installed package into ~/.claude/CLAUDE.md, ~/.codex/AGENTS.md, and their siblings. The key distinction from standard npm install -g risk is the target: adversarial content here does not execute code, it injects AI directives that persist silently across every AI session the user opens -- indefinitely, across every tool. A compromised transitive dependency (not even the top-level package the user requested) can rewrite the user's global AI persona without any confirmation or audit trail. APM's package trust model (users choose what to install) does not transfer adequate mitigation: users do not inspect instruction content before install, and the attack vector is transitive. A hard size cap per instruction (e.g. 8 KB) and a marker-string rejection guard are the minimum viable mitigations; a pre-write summary prompt for --yes-less invocations is strongly preferred but can be a follow-up if the size/marker guards ship in this PR. This finding is blocking.

On the second blocking finding: output_path = deploy_root / root_filename is written without calling ensure_path_within(output_path, deploy_root) from path_security.py. The project's own docstring -- "Every filesystem operation whose target is derived from user-controlled input must pass through one of these guards" -- is explicit. profile.resolved_deploy_root is dynamically resolved and could be influenced by a resolver callback; ensure_path_within is a one-line addition that closes this. This is a violation of APM's own security chokepoint contract and is blocking.

On the integration-test gap: the test-coverage-expert's outcome: missing on an integration-with-fixtures test for the core promise -- "apm install -g writes CLAUDE.md at ~/.claude/CLAUDE.md" -- is load-bearing evidence, not opinion. The install hook fires in a real pipeline; the 39 unit tests mock compile_user_root_contexts at the boundary and will not catch a regression where the hook is silently skipped or misconfigured. Given this PR touches install pipeline wiring, an integration fixture test is the right regression trap. This finding is elevated to blocking by its evidence tier and the secure-by-default surface it guards.

The silent auto-compile on install -g (DevX-UX finding, supported by cli-logging-expert) -- writing files outside the project tree with zero non-verbose output -- is a significant UX gap but does not rise to blocking. Users running apm install -g must see at minimum a non-verbose line that their AI context files were modified; this is not optional for a side-effect that reaches outside the project. The current implementation gates the message on verbose_detail, which is invisible on default output. This should ship with the fix but is not a security issue.

The python-architect and cli-logging-expert recommended findings (logger API mismatch, error results silently swallowed) are real gaps that should be addressed alongside the blockers; the logger mismatch is particularly relevant because it means compilation errors during install are invisible to the structured CLI output pipeline.

The oss-growth-hacker's framing is correct and should be amplified in the release post. The doc-writer's consumer-page gap (install-packages.md not mentioning the root-file side effect) must ship with the feature to avoid user confusion -- it is a behavioral change visible to every global installer.

Dissent. The supply-chain-security-expert classified the unconditional install hook as recommended, while the DevX-UX expert independently classified the silent auto-compile as recommended. Both converge on the same code path (finalize.py:107 / finalize.py:34). The CEO sides with both on the UX dimension but does not elevate the hook-unconditional-firing finding to blocking on its own; the real attack window concern is better addressed by the content sanitization (blocking finding 1). The test-coverage-expert's nit on idempotency round-trip testing remains a nit -- idempotency is important but not on a secure-by-default surface that warrants elevation. The cli-logging-expert and devx-ux-expert independently flagged skipped-hand-authored as info when it should be a warning; both panelists agree, no dissent to arbitrate.

Aligned with: Portable by manifest -- engine correctly iterates KNOWN_TARGETS and maps compile_family to per-tool filenames, the right extensibility point for future tools. Secure by default -- currently fails: verbatim package content flows into AI root context files without size cap or marker-string rejection; output_path not validated against ensure_path_within. Pragmatic as npm -- global compile flag mirrors npm/pip -g ergonomics well; mutual-exclusion error hints and non-verbose install output needed to close the ergonomic gap.

Growth signal. The "publish once, land in Claude/Codex/Copilot/Cursor/Gemini/Windsurf for every global installer" framing is the npm install -g moment for AI toolchain config. Once the two security blockers are resolved, this feature should anchor the release post with exactly that analogy. The marketplace "AI tool configurators" category is a high-signal growth vector -- even 3-5 popular packages adopting global instructions creates a visible pull for the rest. The "Make your package global-aware" producer quickstart section should ship as part of this PR's doc additions, not as a follow-up, because it converts the feature launch into a contributor hook from day one.

Panel summary

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

  1. [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.md enables supply-chain prompt injection via compromised transitive dependencies; existing package trust model does not mitigate this because instruction content is never inspected before install
  2. [Supply Chain Security Expert] (blocking-severity) Call ensure_path_within(output_path, deploy_root) from path_security.py before any write in compile_user_root_contexts -- project's own security chokepoint contract requires all filesystem writes derived from dynamic inputs to pass through ensure_path_within; resolved_deploy_root is dynamically computed and the guard is missing
  3. [Test Coverage Expert] (blocking-severity) Add an integration-with-fixtures test that drives a real apm install -g against a fixture package tree and asserts ~/.claude/CLAUDE.md is written with the APM marker -- evidence outcome: missing on integration-with-fixtures tier for the PR's core user promise; unit tests mock compile_user_root_contexts at the boundary and will not catch install-pipeline wiring regressions
  4. [DevX UX Expert] Emit a non-verbose output line when apm install -g writes root context files; side-effects outside the project tree must be visible on the default output path -- silent writes to ~/.claude/CLAUDE.md on install -g violate the pragmatic-as-npm ergonomic bar; users cannot audit what install did to their AI configuration
  5. [CLI Logging Expert] Surface error results from _compile_user_root_contexts_after_install; current code only messages on written entries, 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
Loading
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"]
Loading
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
Loading

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.
    Suggested: Add a note under the 'apm install -g' section: "After a global install, APM automatically compiles user-scope root context files (
    /.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."

  • [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 docs/src/content/docs/reference/cli/compile.md
    Suggested: Replace '
    /.apm/apm_modules' with 'the user-scope apm_modules directory (default: ~/.apm/apm_modules)'.

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 ·

@github-actions github-actions Bot removed the panel-review Trigger the apm-review-panel gh-aw workflow label Jun 3, 2026
# Conflicts:
#	src/apm_cli/commands/compile/cli.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(install): -g should compile instructions into user-scope root context file

2 participants