Skip to content

[BUG] apm install rewrites apm.lock.yaml on every run for unchanged LSP dependencies #2078

Description

@paul-ww

Describe the bug

apm install rewrites apm.lock.yaml on every run when a project has unchanged LSP dependencies. The diff changes only generated_at.

This is distinct from #1702. The smallest repro does not require local .apm/instructions or local .apm/skills. A single unchanged LSP dependency is enough.

In my repro, the trigger is the Astral marketplace plugin because it contributes lsp_servers / lsp_configs state.

To Reproduce

I reproduced this using APM 0.24.0 through uv run apm.

tmpdir="$(mktemp -d)"
mkdir -p "$tmpdir"

cat > "$tmpdir/apm.yml" <<'YAML'
name: apm-min-astral-only
version: 1.0.0

targets:
  - claude
  - copilot

includes: auto

dependencies:
  apm:
    - astral-sh/claude-code-plugins/plugins/astral#f3ce88a7ba830f53afd6d944c1d0278ed318e142
  mcp: []
YAML

UV_PROJECT=/path/to/repo-with-apm-installed uv --directory "$tmpdir" run --no-sync apm install
cp "$tmpdir/apm.lock.yaml" "$tmpdir/first.lock"
UV_PROJECT=/path/to/repo-with-apm-installed uv --directory "$tmpdir" run --no-sync apm install
diff -u "$tmpdir/first.lock" "$tmpdir/apm.lock.yaml"

Expected behavior

Second run should be a no-op and should not rewrite apm.lock.yaml.

Actual behavior

Second run rewrites only generated_at. Example diff:

@@ -1,5 +1,5 @@
 lockfile_version: '1'
-generated_at: '2026-07-09T07:35:45.785556+00:00'
+generated_at: '2026-07-09T07:35:47.010367+00:00'
 apm_version: 0.24.0
 dependencies:
 - repo_url: astral-sh/claude-code-plugins

Example install output tail on the second run:

[i] Targets: claude, copilot  (source: apm.yml)
  [+] github.com/astral-sh/claude-code-plugins/plugins/astral#f3ce88a7... @f3ce88a7 (cached)
  |-- Skill integrated -> .agents/skills/, .claude/skills/
  |-- 3 skill(s) integrated -> .agents/skills/, .claude/skills/

[*] Installed 1 APM dependency in 0.1s.

Root cause

The main lockfile builder preserves existing MCP state and local .apm state before semantic comparison, but it does not preserve existing LSP state. On a second unchanged install, the in-memory lockfile temporarily drops lsp_servers and lsp_configs, fails the semantic no-op check, and rewrites apm.lock.yaml.

A matching carry-forward for LSP state fixes the problem locally.

Validation

I validated a local patch that preserves existing lsp_servers and lsp_configs before _write_if_changed(...):

  • the minimal Astral-only repro stopped rewriting the lockfile
  • a real project that previously reproduced the issue stopped changing apm.lock.yaml on repeated uv run apm install --frozen

Environment

  • OS: Linux
  • Python: 3.14.5 in the reproducing environment
  • APM: 0.24.0 (58b683d48)

I also prepared a small patch and regression test for this and can send a PR referencing this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/lockfileLockfile schema, per-file provenance, integrity hashes, drift detection.help wantedExtra attention is neededpriority/lowAccepted but not time-sensitivestatus/acceptedDirection approved, safe to start work.status/triagedInitial agentic triage complete; pending maintainer ratification (silence = approval).theme/portabilityOne manifest, every target. Multi-target deploy, marketplace, packaging, install.type/bugSomething does not work as documented.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions