Skip to content

[BUG] Legacy ledger synthesis labels .agents/ paths with target 'agents', which resolves to copilot (.github/) #2743

Description

@ahegyes

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions