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.
Describe the bug
apm installrewritesapm.lock.yamlon every run when a project has unchanged LSP dependencies. The diff changes onlygenerated_at.This is distinct from #1702. The smallest repro does not require local
.apm/instructionsor 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_configsstate.To Reproduce
I reproduced this using APM
0.24.0throughuv run apm.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:Example install output tail on the second run:
Root cause
The main lockfile builder preserves existing MCP state and local
.apmstate before semantic comparison, but it does not preserve existing LSP state. On a second unchanged install, the in-memory lockfile temporarily dropslsp_serversandlsp_configs, fails the semantic no-op check, and rewritesapm.lock.yaml.A matching carry-forward for LSP state fixes the problem locally.
Validation
I validated a local patch that preserves existing
lsp_serversandlsp_configsbefore_write_if_changed(...):apm.lock.yamlon repeateduv run apm install --frozenEnvironment
58b683d48)I also prepared a small patch and regression test for this and can send a PR referencing this issue.