Describe the bug
DeploymentLedgerCodec._legacy_locator() attributes a deployed path to a target by matching a path prefix against _LEGACY_TARGET_PREFIXES (apm_cli/core/deployment_ledger.py:29-39). Every entry in that map resolves back to a real target profile except .agents/, which maps to the string "agents".
"agents" is not a key in KNOWN_TARGETS, and normalize_target_name("agents") returns copilot, whose root_dir is .github — not .agents. APM's own deprecation warning states this explicitly:
'--target agents' is deprecated -- it maps to 'copilot' (.github/), not '.agents/'.
So when the deployment ledger is synthesized from the legacy deployed_files views (from_lockfile, deployment_ledger.py:180-192, taken whenever a lockfile carries no canonical deployments: block), every .agents/... row is written with a target that names a different target owning a different root.
.agents/ is additionally a shared root — codex, agent-skills, hermes, openclaw and antigravity all deploy there — so a single prefix cannot identify its owner. The other prefixes in the map (.claude/, .codex/, .github/, .cursor/, …) are each owned by exactly one target and round-trip correctly.
To Reproduce
Pure-function repro, no APM command run and no ~/.apm state touched:
import warnings; warnings.simplefilter("ignore")
from apm_cli.core.deployment_ledger import DeploymentLedgerCodec
from apm_cli.core.target_catalog import normalize_target_name
from apm_cli.integration.targets import KNOWN_TARGETS
for p in [".agents/skills/my-skill/SKILL.md", # subject
".claude/skills/my-skill/SKILL.md", # control
".codex/agents/my-agent.toml", # control
".github/agents/my-agent.agent.md"]: # control
t = DeploymentLedgerCodec._legacy_locator(p).target
norm = normalize_target_name(t)
root = getattr(KNOWN_TARGETS.get(norm), "root_dir", "—")
print(f"{p:38} target={t:8} normalizes={norm:8} in KNOWN_TARGETS={t in KNOWN_TARGETS!s:5} root={root}")
Output, identical on 0.28.0 and 0.29.0:
.agents/skills/my-skill/SKILL.md target=agents normalizes=copilot in KNOWN_TARGETS=False root=.github <-- mismatch
.claude/skills/my-skill/SKILL.md target=claude normalizes=claude in KNOWN_TARGETS=True root=.claude ok
.codex/agents/my-agent.toml target=codex normalizes=codex in KNOWN_TARGETS=True root=.codex ok
.github/agents/my-agent.agent.md target=copilot normalizes=copilot in KNOWN_TARGETS=True root=.github ok
Three controls round-trip; only the subject does not, so the check discriminates.
Expected behavior
A synthesized legacy row should carry a target that actually owns the path, or the sentinel the function already has for "unattributable" ("legacy", the next(..., "legacy") default two lines below). It should not name a target whose deploy root does not contain the path.
Because .agents/ is shared by several targets, prefix matching cannot identify its owner from the path alone. Options that would both be sound: fall back to "legacy" for .agents/, or resolve the owner from the declared/active target set rather than the prefix.
Environment
- OS: macOS 26.6.2
- Python Version: 3.14.7
- APM Version: 0.28.0 and 0.29.0 (both reproduce;
_LEGACY_TARGET_PREFIXES is byte-identical between them)
Additional context
Observed after upgrading 0.28.0 → 0.29.0 with a pre-existing lockfile, where a user-scope (-g) install produced a ledger with 233 rows labelled target: agents for .agents/skills/... paths, alongside correctly-labelled claude and codex rows for the same install. Re-running with the lockfile moved aside produced a ledger with no agents rows at all and correct per-target attribution.
I have not traced whether the mislabelled rows are what drove the subsequent reconciliation outcome, so I am reporting only the attribution defect above, which is self-contained and independently reproducible.
Describe the bug
DeploymentLedgerCodec._legacy_locator()attributes a deployed path to a target by matching a path prefix against_LEGACY_TARGET_PREFIXES(apm_cli/core/deployment_ledger.py:29-39). Every entry in that map resolves back to a real target profile except.agents/, which maps to the string"agents"."agents"is not a key inKNOWN_TARGETS, andnormalize_target_name("agents")returnscopilot, whoseroot_diris.github— not.agents. APM's own deprecation warning states this explicitly:So when the deployment ledger is synthesized from the legacy
deployed_filesviews (from_lockfile,deployment_ledger.py:180-192, taken whenever a lockfile carries no canonicaldeployments:block), every.agents/...row is written with a target that names a different target owning a different root..agents/is additionally a shared root —codex,agent-skills,hermes,openclawandantigravityall deploy there — so a single prefix cannot identify its owner. The other prefixes in the map (.claude/,.codex/,.github/,.cursor/, …) are each owned by exactly one target and round-trip correctly.To Reproduce
Pure-function repro, no APM command run and no
~/.apmstate touched:Output, identical on 0.28.0 and 0.29.0:
Three controls round-trip; only the subject does not, so the check discriminates.
Expected behavior
A synthesized legacy row should carry a target that actually owns the path, or the sentinel the function already has for "unattributable" (
"legacy", thenext(..., "legacy")default two lines below). It should not name a target whose deploy root does not contain the path.Because
.agents/is shared by several targets, prefix matching cannot identify its owner from the path alone. Options that would both be sound: fall back to"legacy"for.agents/, or resolve the owner from the declared/active target set rather than the prefix.Environment
_LEGACY_TARGET_PREFIXESis byte-identical between them)Additional context
Observed after upgrading 0.28.0 → 0.29.0 with a pre-existing lockfile, where a
user-scope (-g) install produced a ledger with 233 rows labelledtarget: agentsfor.agents/skills/...paths, alongside correctly-labelledclaudeandcodexrows for the same install. Re-running with the lockfile moved aside produced a ledger with noagentsrows at all and correct per-target attribution.I have not traced whether the mislabelled rows are what drove the subsequent reconciliation outcome, so I am reporting only the attribution defect above, which is self-contained and independently reproducible.