Skip to content

fix(compile): suppress empty AGENTS.md shells for copilot when instructions already in .github/instructions/#1742

Open
tillig wants to merge 3 commits into
microsoft:mainfrom
tillig:fix/1730-empty-agents-shells
Open

fix(compile): suppress empty AGENTS.md shells for copilot when instructions already in .github/instructions/#1742
tillig wants to merge 3 commits into
microsoft:mainfrom
tillig:fix/1730-empty-agents-shells

Conversation

@tillig

@tillig tillig commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Description

apm compile --target copilot no longer writes content-free AGENTS.md shells when instructions have already been deployed to .github/instructions/.

TL;DR — After apm install populates .github/instructions/, the instruction-dedup logic from #1550 correctly omits instruction bodies from AGENTS.md, but still wrote header-and-footer-only files — for the root placement and every distributed subdir placement (docs/AGENTS.md, src/AGENTS.md, …). These ~233-byte shells are pure noise. This PR suppresses any placement whose only content would have been the now-elided instructions, emits an INFO-style message explaining why, and teaches apm compile --clean to remove pre-existing APM-generated empty shells (hand-authored files are never touched).

Fixes #1730 (related: #1138, #1550)

Problem

can_dedup_agents_md_instructions() returns True only when the sole AGENTS.md consumer is Copilot (target set {vscode}). In that case _generate_agents_content() was gating just the instruction body on skip_instructions, so every placement still produced a non-empty string (header + footer) and was written:

  • Case A — root: a single empty AGENTS.md at the project root.
  • Case B — distributed: with applyTo-scoped instructions, one empty shell per subdirectory (docs/AGENTS.md, src/AGENTS.md, …).

The CLAUDE.md path already suppressed the analogous empty file (#1138); the Copilot/AGENTS.md path ported the skip logic but not the suppression. --clean did not help either: the shells are regenerated every run, so they were never classified as orphans.

Approach

Mirror and extend the CLAUDE.md behaviour on the distributed AGENTS.md path:

  1. Suppress at content-map build time. A new Phase 3b computes content per placement and, when skip_instructions is active, drops would-be-empty placements into a suppressed_empty_paths list instead of content_map, so they are never written.
  2. Decide "empty" from the model, not the rendered string. _is_placement_empty_shell() is model-based (no brittle header/footer string parsing) and with_constitution-aware, so the predicate agrees with the writer under --no-constitution.
  3. --clean removes stale shells. Suppressed paths are promoted to orphan candidates under --clean, gated on the APM-generated marker so hand-authored AGENTS.md files are never deleted.
  4. Explain it in output. An INFO-style line tells the user why files are absent, distinguishing full vs partial suppression.

Implementation

flowchart TD
    A[apm compile -t copilot] --> B{can_dedup_agents_md_instructions?}
    B -- no (Codex/OpenCode/Windsurf/Gemini/multi) --> F[skip_instructions = False<br/>full AGENTS.md written]
    B -- yes, .github/instructions populated --> C[skip_instructions = True]
    C --> D[Phase 3b: per placement]
    D --> E{_is_placement_empty_shell<br/>with_constitution-aware?}
    E -- empty shell --> G[suppressed_empty_paths<br/>not written]
    E -- has constitution / content --> H[content_map: written]
    G --> I{--clean?}
    I -- yes --> J[remove stale APM-generated<br/>shells, marker-gated]
    I -- no --> K[non-destructive: leave existing files]
Loading

Key points:

  • Target-gated: suppression can only fire when skip_instructions is already True, which can_dedup_agents_md_instructions() restricts to the Copilot-only set. Codex / OpenCode / Windsurf / Gemini and any multi-target build are provably unaffected (test-covered).
  • Constitution preserved: placements carrying a constitution (or other non-instruction content) are still written.
  • --no-dedup / --force-instructions still produce a full AGENTS.md.
  • Marker is single-sourced and public: the generated-file marker is a module constant (AGENTS_MD_GENERATED_MARKER) used at the write site and both --clean gate sites, so write-marker and check-marker can never drift. It is public (mirroring CLAUDE_HEADER) because it gates deletion and is a stable contract surface.
  • Suppress before generating: the empty-shell decision is made before content is generated, so suppressed placements skip content generation and link resolution entirely — no wasted work.
  • Constitution lookups cached: compile_distributed() memoizes constitution existence per directory (dict[Path, bool]), so repos with many placements under a shared tree read each constitution at most once per compile rather than once per placement.

Type of change

  • Bug fix
  • Documentation (updates docs/src/content/docs/producer/compile.md dedup note)
  • New feature
  • Maintenance / refactor

Testing

  • Tested locally
  • All existing tests pass
  • Added tests for new functionality

Evidence (run against this branch's src/):

  • ruff check src/ tests/ → All checks passed
  • ruff format --check src/ tests/ → 1235 files already formatted
  • pytest tests/unit/compilation/1135 passed

New tests (tests/unit/compilation/test_empty_agents_shells_1730.py, plus updates to the hermetic/phase3 suites) cover: Case A (root) and Case B (distributed subdirs) producing no empty file; the INFO message; --clean removing a stale APM-generated shell while leaving hand-authored files; constitution-bearing placements still written; multi-target builds unaffected; --no-dedup still full; and the with_constitution=False predicate/writer agreement. The Case A/B and --clean tests assert filesystem state (mutation-resistant — they fail if the suppression guard is reverted).

How to test manually

# Case B — the worse case: scoped instructions across subdirs
mkdir -p /tmp/apm-dist/.apm/instructions /tmp/apm-dist/docs /tmp/apm-dist/src && cd /tmp/apm-dist
cat > apm.yml <<'EOF'
name: test-dist
version: 1.0.0
targets: [copilot]
EOF
printf -- '---\ndescription: Markdown docs\napplyTo: "docs/**/*.md"\n---\nDocs guidance.\n' > .apm/instructions/docs.instructions.md
printf -- '---\ndescription: Python source\napplyTo: "src/**/*.py"\n---\nPython guidance.\n' > .apm/instructions/src.instructions.md
echo "# doc" > docs/readme.md; echo "x = 1" > src/main.py
mkdir -p .github/instructions && cp .apm/instructions/*.instructions.md .github/instructions/
apm compile --target copilot
find . -name AGENTS.md      # before: ./docs/AGENTS.md + ./src/AGENTS.md (empty). after: none

Spec conformance (OpenAPM v0.1)

  • N/A — this PR does not change OpenAPM-observable behaviour. The change is confined to src/apm_cli/compilation/ (not an OpenAPM critical path) and alters only compile-time AGENTS.md emission; no manifest, lockfile, resolution, policy, or security behaviour changes.

@tillig tillig requested a review from danielmeppiel as a code owner June 10, 2026 23:35
Copilot AI review requested due to automatic review settings June 10, 2026 23:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Fixes issue #1730 by preventing apm compile --target copilot from writing header/footer-only AGENTS.md “shell” files when instructions are already deployed to .github/instructions/, while also enabling --clean to remove previously-generated empty shells safely (marker-gated so hand-authored files are preserved).

Changes:

  • Suppress writing would-be-empty AGENTS.md placements (and expose suppressed paths in results) when skip_instructions=True and no constitution would be injected.
  • Marker-gate orphan detection/cleanup so --clean only deletes APM-generated AGENTS.md files, and can remove stale empty shells.
  • Add acceptance/unit tests, update docs and changelog, and adjust formatter/logging to avoid contradictory output when everything is suppressed.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/unit/compilation/test_empty_agents_shells_1730.py New acceptance + unit tests for suppression behavior, logging, and --clean semantics.
tests/unit/compilation/test_distributed_compiler_phase3.py Updates orphan/cleanup tests to use the APM marker and adds hand-authored protection coverage.
tests/unit/compilation/test_distributed_compiler_hermetic.py Mirrors phase3 updates; adds coverage for suppressed_empty_paths affecting --clean.
src/apm_cli/compilation/distributed_compiler.py Implements suppression logic, introduces generated-marker constant, and marker-gates orphan cleanup.
src/apm_cli/compilation/agents_compiler.py Passes with_constitution, suppresses formatter output when fully suppressed, and emits INFO message.
docs/src/content/docs/producer/compile.md Documents Copilot dedup behavior now omitting AGENTS.md when it would be redundant.
CHANGELOG.md Records the fix and the new --clean behavior for empty shells + hand-authored protection.

Comment thread src/apm_cli/compilation/distributed_compiler.py Outdated
Comment thread src/apm_cli/compilation/distributed_compiler.py Outdated
Comment thread src/apm_cli/compilation/distributed_compiler.py
Comment thread tests/unit/compilation/test_empty_agents_shells_1730.py
Comment thread tests/unit/compilation/test_distributed_compiler_phase3.py Outdated
tillig added a commit to tillig/apm that referenced this pull request Jun 10, 2026
- Decide empty-shell suppression before generating content, skipping
  content generation + link resolution for placements that will be
  suppressed (perf: avoids wasted work).
- Cache constitution-existence per directory across placements in
  compile_distributed() to avoid repeated disk reads; _is_placement_empty_shell
  consults the cache.
- Promote the generated-file marker to public AGENTS_MD_GENERATED_MARKER
  (was _AGENTS_MD_GENERATED_MARKER) since it gates deletion and is a
  contract surface depended on by tests; mirrors public CLAUDE_HEADER.
- Fix _cleanup_orphaned_files docstring to match debug-only/no-warning
  behavior for hand-authored skips.
- Make the INFO-message test hermetic by writing the source instruction
  file on disk.
tillig added 3 commits June 10, 2026 17:40
…ctions already in .github/instructions/ (closes microsoft#1730)

When `apm compile -t copilot` runs after `apm install` has deployed
instructions to `.github/instructions/`, the distributed compiler now
detects that each AGENTS.md placement would be an empty shell (header +
footer only, no instruction body) and suppresses writing those files.

Key changes:
- `distributed_compiler.py`: Phase 3b content-map pass checks
  `_is_placement_empty_shell()` for each placement when
  `skip_instructions=True`; constitution presence bypasses suppression
  since injection would add real body content; `_find_orphaned_agents_files`
  and `_cleanup_orphaned_files` now gate on `_AGENTS_MD_GENERATED_MARKER`
  to protect hand-authored AGENTS.md files; `CompilationResult` gains
  `suppressed_empty_paths` field.
- `agents_compiler.py`: Emits INFO-level user log when suppressed paths
  exist: "AGENTS.md not generated -- Copilot reads `.github/instructions/`
  directly, no further action needed".
- `--clean` removes pre-existing APM-generated empty shells via marker
  gate; hand-authored files are never deleted.
- Plain `apm compile` (no `--clean`) is non-destructive: suppresses new
  writes only, leaves any pre-existing files untouched.
- Multi-target builds (Codex/OpenCode/Windsurf/Gemini) are unaffected
  because `skip_instructions` is False for those targets.
- `--no-dedup` / `--force-instructions` still produces full AGENTS.md.
- 16 new acceptance tests in test_empty_agents_shells_1730.py; updated
  hermetic and phase3 test suites to use APM marker in orphan/cleanup
  tests.

CHANGELOG entry references microsoft#1730, microsoft#1138, microsoft#1550.
…hell suppression

Addresses all REQUIRED findings and cheap nits from the 6-persona review:

1. Fix contradictory terminal output (devx-ux): guard the placement-table
   formatter in _compile_distributed so it is suppressed when all placements
   are suppressed (suppressed_empty_paths non-empty AND content_map empty).
   INFO 'not generated' message still fires unconditionally.

2. Fix --no-constitution predicate/writer disagreement + model-based rewrite
   (primitives-architect + python-architect): thread with_constitution from
   CompilationConfig -> distributed_config -> compile_distributed ->
   _is_placement_empty_shell.  Rewrite _is_placement_empty_shell to use the
   model (constitution check gated by with_constitution flag) rather than
   parsing rendered content strings.  Drop the content parameter.  Add tests
   for the disagreement case (with_constitution=False + constitution on disk).

3. Use _AGENTS_MD_GENERATED_MARKER constant at injection site (supply-chain):
   replace the hardcoded literal in _generate_agents_content with the constant.

4. Update docs/src/content/docs/producer/compile.md (devx-ux): correct the
   Copilot deduplication note to say AGENTS.md is omitted entirely when it
   would only carry instructions (not "still generated"); document that it is
   still written when carrying non-instruction content (constitution).

5. Fix two misleading comments in distributed_compiler.py (nit): clarify
   the placement.agents dead-code note and correct the generated_paths comment.

6. Disambiguate partial-suppression INFO message (devx-ux nit): use 'subdir
   placements not generated' when some AGENTS.md files were written and others
   suppressed, versus 'not generated' for full suppression.

7. Demote defense-in-depth hand-authored skip to _logger.debug (cli-logging
   nit): unreachable in normal operation; remove the user-facing [!] warning
   that would double-prefix and surface as noise.  Update two tests accordingly.
- Decide empty-shell suppression before generating content, skipping
  content generation + link resolution for placements that will be
  suppressed (perf: avoids wasted work).
- Cache constitution-existence per directory across placements in
  compile_distributed() to avoid repeated disk reads; _is_placement_empty_shell
  consults the cache.
- Promote the generated-file marker to public AGENTS_MD_GENERATED_MARKER
  (was _AGENTS_MD_GENERATED_MARKER) since it gates deletion and is a
  contract surface depended on by tests; mirrors public CLAUDE_HEADER.
- Fix _cleanup_orphaned_files docstring to match debug-only/no-warning
  behavior for hand-authored skips.
- Make the INFO-message test hermetic by writing the source instruction
  file on disk.
@tillig tillig force-pushed the fix/1730-empty-agents-shells branch from 3c687e5 to ab3888b Compare June 11, 2026 00:41
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.

[BUG] apm compile -t copilot writes empty AGENTS.md shells (root + every distributed subdir) when instructions are already in .github/instructions/

2 participants