Skip to content

Commit c4aa1c5

Browse files
feat(compile): explicit apm compile -g + install-time hint for global root context (#1632)
* feat(compile): add explicit global compile hint Rebuilds PR #1632 on current main after the original branch could not be rebased due to historical add/add conflicts, preserving the user-visible apm compile -g behavior and the install-time read-only hint for issue #1485. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(compile): fold global compile review follow-ups Tightens the explicit global compile surface after panel review: dry-run output now uses info styling, --global rejects every explicitly supplied project-output flag via an allowlist, user root context writes scan generated content before writing, and marker ownership is anchored to the file header so quoted markers cannot clobber hand-authored files. Also updates security/install docs and adds regression traps for hand-authored root context preservation. Addresses panel follow-ups from cli-logging, DevX UX, supply-chain security, doc-writer, and test coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(compile): align global flag diagnostics Allows --verbose with the explicit global compile path, routes the handler through CommandLogger when called from the CLI, and keeps the security documentation precise about compile -g scan reporting. Addresses final panel follow-ups from CLI logging, DevX UX, and doc-writer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: tighten global compile safeguards Block critical hidden-character output in the new global compile path before writing root context files, reduce default output noise, and keep install hints scoped to verified root-context targets. Adds regression coverage for the security, dry-run, missing apm_modules, and logger-routing contracts raised by the shepherd panel. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: fold compile global review follow-ups Route global compile output through CommandLogger, clarify the install hint and help text, and enumerate user-scope root context outputs in docs. Addresses shepherd-driver panel follow-ups for PR #1632. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: clarify global compile target matrix Document user-scope root context outputs in the target matrix so the global compile guide does not drift from reference docs. Addresses final panel doc-writer follow-up. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: danielmeppiel <danielmeppiel@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent adb63ce commit c4aa1c5

17 files changed

Lines changed: 2324 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
154154
- `apm lock export --format cyclonedx|spdx` emits a standard SBOM inventory of installed packages, and a new declared-license recorder stores each package's manifest-declared license (`apm.yml` `license:` / `plugin.json`) in the lockfile after offline SPDX-id validation. APM records what a package declares -- it does not scan LICENSE text or gate installs on a license. (closes #1777) (#1820)
155155
- `apm install` / `apm pack` can now deploy an experimental Copilot-only `canvas` primitive: a package declaring `.apm/extensions/<name>/` ships verbatim to `.github/extensions/<name>/` (or `~/.copilot/extensions/<name>/` with `--global`), where Copilot CLI discovers it in-session. The surface is gated twice -- `apm experimental enable canvas` plus `--trust-canvas-extensions` for dependency-provided canvases -- and is fail-closed when the flag is off. (#1689)
156156
- `apm install` now blocks dependency-provided executables (hooks and `bin/`) by default, mirroring npm v12's default-deny model. A dependency's hooks or binaries deploy only after explicit approval in an `allowExecutables` block of `apm.yml`, managed via `apm approve` / `apm deny`; root-authored content and text-only primitives are unaffected. (#1723)
157+
- `apm compile --global` / `-g` compiles user-scope root context files such as
158+
`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, and `~/.gemini/GEMINI.md` from
159+
globally installed instructions. Compilation stays explicit; `apm install -g`
160+
prints a one-line hint pointing at `apm compile -g` when global instructions
161+
land on a root-context-only target, but writes no root context file. (#1632)
157162

158163
### Changed
159164

docs/src/content/docs/consumer/install-packages.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,12 @@ apm install -g <package> # install to user scope (~/.apm/)
178178
apm install -v # verbose: show resolution and integration
179179
```
180180

181+
Targets with native user-scope instruction files pick up global instructions
182+
during install. Targets whose user-scope instruction surface is a root context
183+
file require explicit
184+
[`apm compile --global`](../../reference/cli/compile/#global-compilation);
185+
`apm install -g` prints a hint and writes no root context file.
186+
181187
For the full flag reference, run `apm install --help` or see
182188
[CLI commands](../../reference/cli/install/).
183189

docs/src/content/docs/enterprise/security.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ download → scan source → block or deploy → report
136136
Content scanning extends beyond install:
137137

138138
- **`apm compile`** scans compiled output (AGENTS.md, CLAUDE.md, `.github/copilot-instructions.md`, commands) before writing to disk. Critical findings cause `apm compile` to exit with code 1 after writing — defense-in-depth since source files were already scanned at install, but compilation assembles content from multiple sources. `.github/copilot-instructions.md` is assembled from global instructions in `.apm/instructions/`, including those installed under `apm_modules/`.
139+
- **`apm compile --global`** scans user-scope root context files assembled from
140+
globally installed instructions before writing them. Critical findings stop
141+
the write and exit with code 1. Existing hand-authored root context files are
142+
skipped unless they carry APM's generated marker, so opting into global
143+
compilation does not clobber user-managed `CLAUDE.md`, `AGENTS.md`, or
144+
`GEMINI.md` files.
139145
- **`apm pack`** scans files before bundling. This catches hidden characters before a package is published, preventing authors from accidentally distributing tainted content.
140146
- **`apm unpack`** scans bundle contents before deployment. This is a pre-deployment gate matching `apm install` — critical findings block deployment unless `--force` is used. (Note: `apm unpack` is DEPRECATED; prefer `apm install <bundle-path>` for new pipelines -- it applies the same scan plus lockfile integration. See [Pack and distribute](../producer/pack-a-bundle/).)
141147

docs/src/content/docs/producer/compile.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,46 @@ you can omit `start_marker` and `end_marker` if you use those verbatim.
244244
- In distributed compile mode, subdirectory `AGENTS.md` files remain fully
245245
APM-owned and are overwritten on each run.
246246

247+
## Global compilation (-g)
248+
249+
Install a package once globally and root-context tools on your machine can pick
250+
up its instructions without per-project setup. For user-scope instructions, use
251+
the `--global` or `-g` flag:
252+
253+
```bash
254+
apm compile --global
255+
apm compile -g --dry-run
256+
```
257+
258+
This reads **global instructions** from `~/.apm/apm_modules/` (instructions
259+
without `applyTo:` frontmatter) and writes user-scope root context files for
260+
root-context targets:
261+
262+
- `~/.claude/CLAUDE.md` (or `$CLAUDE_CONFIG_DIR/CLAUDE.md`)
263+
- `~/.codex/AGENTS.md`
264+
- `~/.config/opencode/AGENTS.md`
265+
- `~/.copilot/AGENTS.md`
266+
- `~/.cursor/AGENTS.md`
267+
- `~/.gemini/GEMINI.md`
268+
269+
### Overwrite protection
270+
271+
When a root file exists but contains no APM marker, it is treated as
272+
hand-authored and never overwritten. Use `--dry-run` to preview what would
273+
be written without modifying files.
274+
275+
### Constraints
276+
277+
- Compilation is explicit. `apm install -g` (see
278+
[Install packages](../consumer/install-packages/)) does not write root context
279+
files; it prints a one-line hint pointing at `apm compile -g` when global
280+
instructions land on a root-context-only target.
281+
- `--global` cannot be combined with project-output flags such as `--target`,
282+
`--all`, `--watch`, `--root`, or `--output`.
283+
- Compiled output is security-scanned before it is written. Critical findings
284+
stop the write and make `apm compile -g` exit non-zero.
285+
- Skills-only packages (no global instructions) do not write root files.
286+
247287
## Pitfalls
248288

249289
- **Confusing compile's scope.** Compile only handles **instructions**

docs/src/content/docs/reference/cli/compile.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,25 @@ The dry-run output shows `[dry-run] would remove stale CLAUDE.md -- instructions
109109
| `--dry-run` | Show placement decisions without writing files. |
110110
| `-v, --verbose` | Show source attribution and optimizer analysis. |
111111

112+
### Global compilation
113+
114+
Global compilation keeps supported user-scope root-context targets in sync with
115+
globally installed instruction packages -- one command, no per-tool setup.
116+
117+
| Flag | Description |
118+
|------|-------------|
119+
| `-g, --global` | Compile user-scope root context files from `~/.apm/apm_modules`. Reads globally installed packages and writes one root context file per supported user-scope target (e.g. `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`). Not valid with project-output flags such as `--target`, `--all`, `--watch`, `--root`, or `--output`. Exits non-zero if `~/.apm/apm_modules` does not exist. |
120+
121+
`apm compile --global` is explicit. `apm install -g` does not run it; instead,
122+
when global instructions land on a root-context-only target, install prints a
123+
one-line hint pointing at `apm compile -g`. Run it manually after adding or
124+
removing global packages. Hand-authored files (files that do not carry the
125+
APM-generated marker) are never overwritten.
126+
127+
```bash
128+
apm compile -g
129+
```
130+
112131
## Examples
113132

114133
Compile for whatever the project is set up for:

docs/src/content/docs/reference/cli/install.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ Transport env vars: `APM_GIT_PROTOCOL` (`ssh` or `https`) sets the default initi
103103
- **No-op nudge.** When the lockfile is already satisfied and nothing needs deploying, install prints `[i] Run 'apm update' to check for newer versions.` so you know the silent success was not a missed refresh.
104104
- **Frozen mode.** With `--frozen`, install resolves only what is in `apm.lock.yaml`. A direct dependency missing from the lockfile, or a missing lockfile entirely, exits `1`. Orphan lockfile entries (locked but no longer in `apm.yml`) are tolerated; local-path deps are skipped. This is a structural check, not a content check -- run `apm audit --ci` for hash verification.
105105
- **Local `.apm/` deployment.** After dependencies are integrated, primitives in the project's own `.apm/` directory are deployed to the same targets. Local files win on collision. Skipped at `--global` and with `--only mcp`.
106+
- **User-scope root context hint.** Compilation stays explicit. After `apm install -g`, targets with native user-scope instruction files pick up global instructions during install. Targets whose user-scope instruction surface is a root context file require [`apm compile --global`](../compile/#global-compilation); install prints a one-line `[i]` hint and writes no root context file.
106107
- **Stale-file cleanup.** Files a still-present package previously deployed but no longer produces are removed from the workspace, gated by per-file content hashes recorded in the lockfile (user-edited files are kept with a warning).
107108
- **Enterprise marketplace gate.** When installing from a `*.ghe.com` marketplace, bare cross-repo `repo:` fields (e.g. `repo: owner/repo`) are refused before any network request runs, preventing dependency-confusion attacks. Host-qualify the field to proceed: `repo: corp.ghe.com/owner/repo` for an enterprise dep, or `repo: github.com/owner/repo` for a declared cross-host dep.
108109
- **Security scan.** Source files are scanned for hidden Unicode and other tag-character / bidi-override patterns before deployment. Critical findings block the package; the install exits `1`. Use `--force` to deploy anyway, or run `apm audit --strip` first to remediate.

docs/src/content/docs/reference/targets-matrix.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ GitHub Copilot (CLI and IDE).
9090
- hooks: `.github/hooks/<name>.json`
9191
- generated: `.github/copilot-instructions.md` (compile output)
9292
- **User scope.** Partial. `prompts` deploy under `~/.copilot/prompts/`; `instructions` from all packages are concatenated into `~/.copilot/copilot-instructions.md` (Copilot CLI reads only that single file at user scope). User-scope deploys land under `~/.copilot/`, not `~/.github/`.
93+
- **Global compile.** `apm compile -g` can also render global instructions to
94+
`~/.copilot/AGENTS.md` for root-context readers that honor `AGENTS.md`.
9395

9496
## claude
9597

@@ -120,6 +122,9 @@ Cursor.
120122
- skills: `.agents/skills/<name>/SKILL.md`
121123
- hooks: `.cursor/hooks.json`
122124
- **User scope.** Partial. `instructions` is excluded at user scope; Cursor reads global rules from its Settings UI rather than from disk.
125+
- **Global compile.** `apm compile -g` can render global instructions to
126+
`~/.cursor/AGENTS.md` for root-context readers that honor `AGENTS.md`; Cursor
127+
global rules still use the Settings UI.
123128
- **Caveat.** Command files use the shared `claude_command` transformer today; Cursor-specific frontmatter keys (`author`, `mcp`, `parameters`, ...) are dropped at install time and surfaced via diagnostics.
124129

125130
## codex
@@ -174,6 +179,8 @@ OpenCode.
174179
- commands: `.opencode/commands/<name>.md`
175180
- skills: `.agents/skills/<name>/SKILL.md`
176181
- **Caveat.** OpenCode has no hooks concept; the `hooks` primitive is silently skipped for this target.
182+
- **Global compile.** `apm compile -g` writes
183+
`~/.config/opencode/AGENTS.md` from global instructions.
177184

178185
## windsurf
179186

packages/apm-guide/.apm/skills/apm-usage/commands.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ If no `--target`, no `targets:` in `apm.yml`, and no harness signal is present,
5555

5656
| Command | Purpose | Key flags |
5757
|---------|---------|-----------|
58-
| `apm compile` | Compile agent context | `-o` output, `-t` target (comma-separated; resolution chain `--target` > apm.yml `targets:` > auto-detect), `--all` compile for every canonical target (preferred over deprecated `--target all`), `--chatmode`, `--dry-run`, `--no-links`, `--watch`, `--validate`, `--single-agents`, `-v` verbose, `--local-only`, `--clean`, `--with-constitution/--no-constitution`, `--no-dedup` / `--force-instructions` (opt out of Claude/Copilot deduplication), `--root DIR` redirect generated artifacts under DIR while sources resolve from `$PWD` (mirrors `pip install --target`; not valid with `--watch`) |
58+
| `apm compile` | Compile agent context | `-o` output, `-t` target (comma-separated; resolution chain `--target` > apm.yml `targets:` > auto-detect), `--all` compile for every canonical target (preferred over deprecated `--target all`), `-g`/`--global` (read global instructions from `~/.apm/apm_modules/`, write user-scope root files; cannot combine with project-output flags such as `--target`, `--all`, `--watch`, `--root`, or `--output`; critical hidden-character findings stop the write and exit 1), `--chatmode`, `--dry-run`, `--no-links`, `--watch`, `--validate`, `--single-agents`, `-v` verbose, `--local-only`, `--clean`, `--with-constitution/--no-constitution`, `--no-dedup` / `--force-instructions` (opt out of Claude/Copilot deduplication), `--root DIR` redirect generated artifacts under DIR while sources resolve from `$PWD` (mirrors `pip install --target`; not valid with `--watch`) |
5959

6060
`apm compile --watch` live-reloads `apm.yml`: editing `target:` / `targets:` mid-session takes effect on the next file event without restarting the watcher. The CLI `--target` flag, when passed to `apm compile --watch`, still outranks `apm.yml`. Re-resolution is gated on the changed file's basename being `apm.yml`, so `.instructions.md` edits do not pay an extra resolver round-trip and a stray `backup_apm.yml` cannot trigger a reload. `--clean` is ignored in watch mode and the watcher prints an explicit `[!]` warning at startup (`--clean is ignored in watch mode; run 'apm compile --clean' separately to remove orphaned outputs.`); run `apm compile --clean` separately between watch sessions to remove orphans.
6161

src/apm_cli/commands/compile/cli.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,97 @@ def _resolve_effective_target(
335335
return detected_target, detection_reason, config_target
336336

337337

338+
def _handle_global_flag(dry_run: bool, logger: CommandLogger) -> int:
339+
"""Handle --global compilation of user-scope root context files.
340+
341+
Returns 0 on success, 1 on error (for sys.exit).
342+
"""
343+
344+
from ...compilation import compile_user_root_contexts
345+
from ...core.scope import InstallScope, get_apm_dir
346+
from ...integration.targets import KNOWN_TARGETS
347+
348+
source_root = get_apm_dir(InstallScope.USER)
349+
apm_modules = source_root / "apm_modules"
350+
if not apm_modules.is_dir():
351+
display_path = _display_user_path(apm_modules)
352+
logger.error(
353+
f"User-scope apm_modules not found: {display_path}. "
354+
"Run 'apm install -g <package>' to install packages globally.",
355+
symbol="error",
356+
)
357+
return 1
358+
359+
results = compile_user_root_contexts(
360+
list(KNOWN_TARGETS.values()),
361+
source_root,
362+
dry_run=dry_run,
363+
logger=None,
364+
)
365+
366+
if not results:
367+
logger.info(
368+
"No user-scope targets produced output -- run 'apm install -g <package>' "
369+
"to add global instructions.",
370+
symbol="info",
371+
)
372+
return 0
373+
374+
has_error = False
375+
written_count = 0
376+
would_write_count = 0
377+
unchanged_count = 0
378+
for entry in results:
379+
status = entry.status
380+
tname = entry.target
381+
path = entry.path
382+
display_path = _display_user_path(path) if path is not None else None
383+
if status == "written":
384+
logger.success(f"{tname}: wrote {display_path}", symbol="check")
385+
written_count += 1
386+
elif status == "would-write":
387+
logger.info(f"{tname}: would write {display_path} (dry-run)", symbol="preview")
388+
would_write_count += 1
389+
elif status == "unchanged":
390+
logger.verbose_detail(f"{tname}: unchanged {display_path}")
391+
unchanged_count += 1
392+
elif status == "skipped-hand-authored":
393+
logger.info(f"{tname}: skipped (hand-authored) {display_path}", symbol="info")
394+
elif status == "skipped-no-instructions":
395+
logger.verbose_detail(f"{tname}: skipped (no global instructions)")
396+
elif status.startswith("error:"):
397+
logger.error(f"{tname}: {status[6:]}", symbol="error")
398+
has_error = True
399+
if entry.has_critical_security:
400+
has_error = True
401+
402+
if not has_error:
403+
changed_count = written_count + would_write_count
404+
if changed_count:
405+
verb = "Would compile" if dry_run else "Compiled"
406+
message = f"{verb} {changed_count} user-scope root context file(s)"
407+
if unchanged_count:
408+
message += f"; {unchanged_count} unchanged"
409+
message += "."
410+
if dry_run:
411+
logger.info(message, symbol="preview")
412+
else:
413+
logger.success(message, symbol="check")
414+
else:
415+
logger.info("No user-scope root context files changed.", symbol="info")
416+
417+
return 1 if has_error else 0
418+
419+
420+
def _display_user_path(path: Path) -> str:
421+
"""Render paths under HOME with a stable tilde prefix for CLI output."""
422+
try:
423+
rel = path.resolve().relative_to(Path.home().resolve())
424+
except ValueError:
425+
return str(path)
426+
return f"~/{rel.as_posix()}"
427+
428+
338429
def _validate_project(logger: CommandLogger, dry_run: bool, source_root: Path) -> None:
339430
"""Check APM project exists and has content.
340431
@@ -892,6 +983,19 @@ def _coerce_provenance_targets(value):
892983
"for scratch-dir verification. Cannot be combined with --watch."
893984
),
894985
)
986+
@click.option(
987+
"--global",
988+
"-g",
989+
"global_",
990+
is_flag=True,
991+
default=False,
992+
help=(
993+
"Compile user-scope root context files (~/.claude/CLAUDE.md, etc.) "
994+
"from ~/.apm/apm_modules. Cannot be combined with project-scoped output "
995+
"flags such as --target, --all, --watch, --root, or --output; use with "
996+
"--dry-run to preview changes."
997+
),
998+
)
895999
@click.pass_context
8961000
def compile( # noqa: PLR0913 -- Click handler
8971001
ctx,
@@ -911,12 +1015,16 @@ def compile( # noqa: PLR0913 -- Click handler
9111015
compile_all,
9121016
no_dedup,
9131017
root,
1018+
global_,
9141019
):
9151020
"""Compile APM context into distributed AGENTS.md files.
9161021
9171022
By default, uses distributed compilation to generate multiple focused AGENTS.md
9181023
files across your directory structure following the Minimal Context Principle.
9191024
1025+
Use --global / -g to compile user-scope root context files from globally
1026+
installed packages.
1027+
9201028
Use --single-agents for traditional single-file compilation when needed.
9211029
9221030
Target platforms:
@@ -953,6 +1061,39 @@ def compile( # noqa: PLR0913 -- Click handler
9531061
# consumers running with -W default, which we have none of.
9541062
logger.warning("'--target all' is deprecated; use '--all' instead.")
9551063

1064+
# --global: compile user-scope root context files from ~/.apm/apm_modules.
1065+
# Must be checked before --watch / --root guards so we return early.
1066+
if global_:
1067+
from click.core import ParameterSource
1068+
1069+
allowed_with_global = {"global_", "dry_run", "verbose"}
1070+
flag_names = {
1071+
"chatmode": "--chatmode",
1072+
"clean": "--clean",
1073+
"compile_all": "--all",
1074+
"legacy_skill_paths": "--legacy-skill-paths",
1075+
"local_only": "--local-only",
1076+
"no_dedup": "--no-dedup/--force-instructions",
1077+
"no_links": "--no-links",
1078+
"output": "--output",
1079+
"root": "--root",
1080+
"single_agents": "--single-agents",
1081+
"target": "--target",
1082+
"validate": "--validate",
1083+
"verbose": "--verbose",
1084+
"watch": "--watch",
1085+
"with_constitution": "--with-constitution/--no-constitution",
1086+
}
1087+
for name in sorted(set(ctx.params) - allowed_with_global):
1088+
if ctx.get_parameter_source(name) is ParameterSource.DEFAULT:
1089+
continue
1090+
flag = flag_names.get(name, f"--{name.replace('_', '-')}")
1091+
raise click.UsageError(f"--global is not valid with {flag}")
1092+
rc = _handle_global_flag(dry_run=dry_run, logger=logger)
1093+
if rc != 0:
1094+
ctx.exit(rc)
1095+
return
1096+
9561097
# --root + --watch is rejected: ``_watch_mode`` uses bare-relative
9571098
# paths (``Path(APM_DIR)``, ``AgentsCompiler(".")``) and the watch
9581099
# loop would scan the deploy root rather than the source tree. The

0 commit comments

Comments
 (0)