fix(compile): preserve nested worktree files - #2453
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes apm compile --clean so it will not delete APM-generated AGENTS.md files that belong to nested Git worktrees under the project root, while still cleaning up stale/orphaned AGENTS.md files in the parent project. It also adds a real-CLI regression test, plus a static architecture boundary guard to prevent reintroducing an unprunable recursive scan.
Changes:
- Replace recursive
Path.rglob("AGENTS.md")cleanup discovery with a prunableos.walkthat stops descending into nested worktree roots (detected via.gitfile). - Add an end-to-end lifecycle regression test that creates a nested worktree, commits an APM-marked
AGENTS.md, runscompile --cleanfrom the parent, and asserts nested bytes + cleangit status. - Add a lint architecture boundary check (and a mutation-style integration test) to ensure the worktree-pruning scan shape remains enforced; document the new
--cleanboundary behavior.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/compilation/distributed_compiler.py |
Switch orphan discovery to os.walk and prune nested worktree roots before considering AGENTS.md for cleanup. |
tests/integration/test_compile_clean_nested_worktree.py |
Adds a real-CLI regression test covering nested-worktree preservation under compile --clean. |
tests/integration/test_architecture_authorities.py |
Adds a mutation-style guard test ensuring the architecture boundary check trips if the pruning predicate is removed. |
scripts/lint-architecture-boundaries.sh |
Adds an architecture boundary rule enforcing the prunable walk + .git-file boundary and rejecting reintroduction of rglob. |
docs/src/content/docs/reference/cli/compile.md |
Documents that --clean excludes nested Git worktrees from cleanup. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 2
- Review effort level: Lite
| '(directory_path / ".git").is_file()' \ | ||
| "$distributed_compiler" || true) | ||
| nested_worktree_prune_count=$(grep -Fc 'child_dirs.clear()' "$distributed_compiler" || true) | ||
| nested_worktree_rglob_hits=$(grep -En 'rglob\("AGENTS\.md"\)' "$distributed_compiler" || true) |
| | `-o, --output PATH` | Output file path. Only applies in single-file mode (`--single-agents`). Default: `AGENTS.md`. | | ||
| | `--single-agents` | Force single-file compilation (legacy). Writes one combined file at `--output` instead of a distributed per-directory target-file tree. Applies to every target that uses distributed placement. | | ||
| | `--clean` | Remove orphaned AGENTS.md files no longer produced by the current primitive set. For `--target claude`, also removes a stale APM-generated CLAUDE.md once instructions live in `.claude/rules/`. Hand-authored CLAUDE.md files (files without the `<!-- Generated by APM CLI -->` marker) are never deleted. | | ||
| | `--clean` | Remove orphaned AGENTS.md files no longer produced by the current primitive set. Nested Git worktrees are excluded from cleanup. For `--target claude`, also removes a stale APM-generated CLAUDE.md once instructions live in `.claude/rules/`. Hand-authored CLAUDE.md files (files without the `<!-- Generated by APM CLI -->` marker) are never deleted. | |
Make linked-worktree cleanup boundaries explicit in debug logs and documentation while preserving the non-following walk invariant. Add fast unit coverage for the boundary. Addresses apm-review-panel follow-ups. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 1 | 1 | Cleanup boundary is sound; normal placement analysis is a separate future surface. |
| CLI Logging Expert | 0 | 0 | 0 | No observable CLI output regression. |
| DevX UX Expert | 0 | 1 | 2 | Boundary diagnostics and recovery guidance are now present. |
| Supply Chain Security Expert | 0 | 0 | 1 | Cleanup containment improved; non-following walk is explicit. |
| OSS Growth Hacker | 0 | 0 | 0 | Fix builds trust for worktree-heavy contributors. |
| Doc Writer | 0 | 0 | 0 | CLI reference accurately states the cleanup boundary. |
| Test Coverage Expert | 0 | 0 | 1 | E2E, mutation, and unit coverage defend the user promise. |
| Performance Expert | 0 | 0 | 2 | Prunable traversal improves filesystem work without needing a benchmark. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 1 follow-up
- [Python Architect] Audit
ContextOptimizertraversal of non-hidden linked worktrees during placement generation -- it is a latent sibling surface outside this PR's cleanup scope.
Architecture
classDiagram
direction LR
class DistributedAgentsCompiler {
<<Compiler>>
+compile_distributed(primitives, config) CompilationResult
-_find_orphaned_agents_files(generated, suppressed) list~Path~
-_cleanup_orphaned_files(orphaned, dry_run) list~str~
-_file_has_apm_marker(path) bool
}
class ContextOptimizer {
<<Analyzer>>
+analyze_directory_structure() None
-_get_all_files() list~Path~
-_should_exclude_subdir(path) bool
}
class CompilationResult {
<<ValueObject>>
+success bool
+content_map dict
+suppressed_empty_paths list~Path~
}
class DirectoryMap {
<<ValueObject>>
+directories dict
+depth_map dict
}
DistributedAgentsCompiler *-- ContextOptimizer : delegates placement scoring
DistributedAgentsCompiler ..> CompilationResult : returns
ContextOptimizer ..> DirectoryMap : produces
note for DistributedAgentsCompiler "PR changes _find_orphaned_agents_files: os.walk plus .git-file predicate replaces rglob"
class DistributedAgentsCompiler:::touched
classDef touched fill:#fff3b0,stroke:#d47600
flowchart TD
A["apm compile --clean"] --> B["compile_distributed"]
B --> C["_find_orphaned_agents_files"]
C --> D["os.walk(self.base_dir)"]
D --> E{"non-root .git file?"}
E -- Yes --> F["clear child dirs and skip linked worktree"]
E -- No --> G{"AGENTS.md present?"}
G -- Yes --> H["marker-gated orphan candidate"]
G -- No --> D
H --> I["cleanup only when requested"]
F --> D
Recommendation
Land after the current CI run is green. Track the ContextOptimizer placement-analysis audit separately; it does not change this cleanup fix's user promise.
Full per-persona findings
Python Architect
- [recommended] Audit ContextOptimizer traversal of non-hidden linked worktrees during normal placement generation. It is deferred because this PR changes
compile --cleanorphan deletion, not normal placement analysis. - [nit] Confirmed the cleanup skip-dir set has one live owner after the fold.
CLI Logging Expert
No findings.
DevX UX Expert
- [recommended] Verbose nested-worktree boundary detail folded in
9098ad86d. - [nit] Recovery guidance folded in
9098ad86d. - [nit] The same recovery command is present in the
--cleanreference entry.
Supply Chain Security Expert
- [nit] Explicit
followlinks=Falsefolded in9098ad86d.
OSS Growth Hacker
No findings.
Auth Expert -- inactive
No auth, token, credential, host classification, or authorization surface changed.
Doc Writer
No findings.
Test Coverage Expert
- [nit] Fast
.git-file pruning unit proof folded in9098ad86d; the exact lifecycle e2e and architecture mutation guard also pass.
Performance Expert
- [nit] Cleanup skip-dir constant folded in
9098ad86d. - [nit] Explicit non-following walk folded in
9098ad86d.
This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ree-clean-boundary
fix(compile): preserve nested worktree files
TL;DR
apm compile --cleannow treats a nested Git worktree as a cleanup boundary, so it cannot delete that checkout's tracked APM-generatedAGENTS.mdfiles. The orphan scanner still removes stale files in the parent project. This closes #2436 with a real-CLI lifecycle regression test, a mutation-proof architecture guard, and a documented--cleanboundary.Important
Closes #2436. The lifecycle proof commits
AGENTS.mdin a nested worktree, runscompile --cleanfrom its parent, and verifies both bytes andgit status --porcelain.Problem (WHY)
compile --cleandeleted trackedAGENTS.mdfiles belonging to worktrees nested below the project root.Path.rglob("AGENTS.md")scan crossed checkout boundaries, so cleanup treated an independently tracked file as a parent orphan.Why these matter: cleanup must honor a checkout boundary before it decides that a generated file is disposable. The regression proof follows the repository principle that "Grounding outputs in deterministic tool execution transforms probabilistic generation into verifiable action."
Approach (WHAT)
os.walk, removing excluded directories before descent..gitfile, clear its child directory list and skip the whole nested worktree.Implementation (HOW)
src/apm_cli/compilation/distributed_compiler.py-- walks candidate directories so it can prune linked-worktree roots before consideringAGENTS.md; the top-level worktree remains eligible.tests/integration/test_compile_clean_nested_worktree.py-- creates a real nested Git worktree, commits its markedAGENTS.md, runscompile --clean, and checks parent cleanup plus nested byte and status invariants.scripts/lint-architecture-boundaries.shandtests/integration/test_architecture_authorities.py-- require the prunable walk and.git-file boundary; the architecture test mutates the predicate and expects the guard to fail.docs/src/content/docs/reference/cli/compile.md-- states that--cleanexcludes nested Git worktrees.Diagrams
Legend: the dashed node is the new boundary that prevents parent cleanup from entering a linked worktree.
flowchart LR subgraph Parent[Parent project] Scan[os.walk cleanup scan] Orphan[stale AGENTS.md] end subgraph Nested[Nested Git worktree] Boundary[.git file boundary] Tracked[tracked AGENTS.md] end Scan --> Orphan Scan --> Boundary Boundary -. prunes descendants .-> Tracked classDef new stroke-dasharray: 5 5; class Boundary new;Trade-offs
rglob. Choseos.walkbecause its mutable child list stops descent; rejected post-filtering because it still scans the nested checkout..gitfile boundary. Chose Git's linked-worktree marker; rejected an allowlist such as.worktreesbecause nested worktrees can use any directory name.Benefits
compile --cleanremoves the fixture's stale parent orphan while preserving one tracked nestedAGENTS.md.Validation
APM_E2E_TESTS=1 uv run pytest -q tests/integration/test_compile_clean_nested_worktree.py:uv run pytest -q tests/unit/compilation/test_distributed_compiler_hermetic.py:Repository quality and lint evidence
uv run --extra dev pytest -p no:cacheprovider -q tests/quality:uv run --frozen python scripts/check_test_assertions.py && uv run --frozen python scripts/check_exact_test_duplicates.py:CI lint mirror:
Scenario Evidence
apm compile --cleanin a project with a nested Git worktree: stale parent output is removed, while the nested checkout keeps its tracked instructions and stays clean.tests/integration/test_compile_clean_nested_worktree.py::test_compile_clean_preserves_nested_git_worktree_agents_file(regression-trap for #2436)tests/integration/test_compile_clean_nested_worktree.py::test_compile_clean_preserves_nested_git_worktree_agents_fileHow to test
AGENTS.mdin that nested checkout.AGENTS.mdonly in the parent checkout.apm compile --cleanfrom the parent and confirm the parent orphan is removed.git -C <nested> status --porcelain; both should remain unchanged and empty.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com