Describe the bug
apm audit --ci falsely fails skill-subset-consistency in a fresh checkout containing valid committed APM outputs when a dependency uses skills:. The same manifest, lockfile and deployed files pass immediately after installation. Fresh scratch replay reports no drift; all other nine checks pass.
Reproduced on 0.30.0 and current release 0.32.0 with one public pinned dependency and one selected skill. No private repository, custom APM patches, MCP configuration or org policy is involved.
To Reproduce
- Create a Git repository with this
apm.yml and .gitignore containing apm_modules/:
name: audit-subset-repro
version: 1.0.0
description: Minimal public dependency audit reproduction.
targets:
- copilot
policy:
discovery_enabled: false
dependencies:
apm:
- git: dotnet/skills
path: plugins/dotnet-diag
ref: 4c72b17fa2c2aaa307d8f1e337ec75cab45ac5c7
skills:
- dump-collect
- Run
apm install, then apm audit --ci --no-policy --no-fail-fast --format json. Both exit 0.
- Commit all generated files, manifest, lockfile and
.gitignore.
- Run
git clone --no-local <seed-path> <fresh-path>. The clone has no apm_modules.
- Run the same audit command in the clone. It exits 1, reporting only the selected skill missing. The checkout remains unchanged and
apm_modules remains absent.
The complete script below automates these steps: python repro.py --apm /path/to/apm, or python repro.py with APM on PATH. It creates and retains temporary fixtures; it does not modify an existing repository. Public GitHub access is required.
Expected behavior
The fresh checkout should pass. Validate selected skill existence against the lock-pinned dependency tree prepared by audit, while preserving invalid-selection and deployed-drift checks.
The official audit-only pattern for committed outputs explicitly avoids installing into the checkout first, because installation overwrites the bytes audit should inspect.
Environment
- Azure Linux 3.0, x86_64.
- APM 0.30.0 / Python 3.12.11; APM 0.32.0 / Python 3.13.12.
- 0.32.0 installed from PyPI into an isolated uv tool directory.
- Fresh means no checkout-local
apm_modules, not absence of all host caches.
- Windows execution has not been tested.
Logs
Both versions: warm audit exit 0; fresh audit exit 1, 9/10 checks pass. Only failure:
skill-subset-consistency: 1 skill subset mismatch(es) -- regenerate lockfile (apm install)
dotnet/skills/plugins/dotnet-diag: recorded skill subset path(s) not found in package tree: dump-collect
Drift passes with no drift detected against lockfile and an empty drift list. The script verifies unchanged checkout contents.
Additional context / suspected cause
In 0.32.0 ci_checks.py, _check_skill_subset_consistency forwards project_root; _missing_recorded_skill_subset_paths uses dep_ref.get_install_path(project_root / APM_MODULES_DIR), which is absent in the clone. The adjacent configuration check instead consumes prepared_replay.modules_root.
This looks like a missing integration between subset validation and cold-checkout replay. Using the prepared dependency tree may be a fix direction; no fix is implemented or validated here. The lookup also remains in main at 4e916ecbddd14edb2f0c93d0199ac9bfa4a7d3dd (source inspected only).
This differs from #2172: drift passes here; the baseline subset-existence check fails specifically without checkout-local modules. Existing subset/audit issues were searched before filing.
Complete reproduction script
"""Reproduce APM subset audit failure using one public pinned dependency."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
parser = argparse.ArgumentParser()
parser.add_argument('--apm', default='apm', help='APM executable (0.30.0 or 0.32.0)')
args = parser.parse_args()
apm = shutil.which(args.apm)
if not apm:
raise SystemExit('APM executable not found')
root = Path(tempfile.mkdtemp(prefix='apm-subset-repro-'))
seed = root / 'seed'
seed.mkdir()
env = os.environ.copy()
# Keep public Git operations independent of user Git configuration.
env['GIT_CONFIG_GLOBAL'] = os.devnull
env['GIT_CONFIG_NOSYSTEM'] = '1'
env['GIT_TERMINAL_PROMPT'] = '0'
for key in list(env):
if key.startswith(('GIT_CONFIG_KEY_', 'GIT_CONFIG_VALUE_')) or key in ('GIT_CONFIG_COUNT', 'GITHUB_TOKEN', 'GH_TOKEN', 'GITHUB_APM_PAT'):
env.pop(key, None)
env['XDG_CACHE_HOME'] = str(root / 'cache')
def run(name, command, cwd, expected=0):
result = subprocess.run(command, cwd=cwd, env=env, capture_output=True, text=True, timeout=180)
(root / (name + '.log')).write_text(result.stdout + result.stderr, encoding='utf-8')
print(f'{name}: exit {result.returncode}', flush=True)
if result.returncode != expected:
raise RuntimeError(f'{name}: expected {expected}; inspect {root}')
return result
manifest = '''name: audit-subset-repro
version: 1.0.0
description: Minimal public dependency audit reproduction.
targets:
- copilot
policy:
discovery_enabled: false
dependencies:
apm:
- git: dotnet/skills
path: plugins/dotnet-diag
ref: 4c72b17fa2c2aaa307d8f1e337ec75cab45ac5c7
skills:
- dump-collect
'''
(seed / 'apm.yml').write_text(manifest, encoding='utf-8')
(seed / '.gitignore').write_text('apm_modules/\n', encoding='utf-8')
print(f'Artifacts: {root}', flush=True)
run('version', [apm, '--version'], seed)
run('git-init', ['git', 'init'], seed)
run('install', [apm, 'install'], seed)
run('warm-audit', [apm, 'audit', '--ci', '--no-policy', '--no-fail-fast', '--format', 'json'], seed)
run('git-add', ['git', 'add', '.'], seed)
run('git-commit', ['git', '-c', 'user.name=Repro', '-c', 'user.email=repro@example.invalid', 'commit', '-m', 'Add generated public fixture'], seed)
fresh = root / 'fresh'
run('git-clone', ['git', 'clone', '--no-local', str(seed), str(fresh)], root)
assert not (fresh / 'apm_modules').exists()
def snapshot():
return {str(p.relative_to(fresh)): hashlib.sha256(p.read_bytes()).hexdigest() for p in fresh.rglob('*') if p.is_file() and '.git' not in p.relative_to(fresh).parts}
before = snapshot()
result = run('fresh-audit', [apm, 'audit', '--ci', '--no-policy', '--no-fail-fast', '--format', 'json'], fresh, expected=1)
payload = json.loads(result.stdout)
assert [c['name'] for c in payload['checks'] if not c['passed']] == ['skill-subset-consistency']
assert payload['drift']['drift'] == []
assert not (fresh / 'apm_modules').exists()
assert snapshot() == before, 'Audit modified checkout files'
print('Confirmed: warm audit passes; fresh audit exits 1; checkout unchanged.', flush=True)
print(f'Read {root / "fresh-audit.log"} to verify the failing check.', flush=True)
Full 0.32.0 fresh audit output
{
"passed": false,
"checks": [
{
"name": "lockfile-exists",
"passed": true,
"message": "Lockfile present",
"details": []
},
{
"name": "ref-consistency",
"passed": true,
"message": "All dependency refs match lockfile",
"details": []
},
{
"name": "deployment-ledger-owners",
"passed": true,
"message": "All deployment ledger owners are valid",
"details": []
},
{
"name": "deployed-files-present",
"passed": true,
"message": "All deployed files present on disk",
"details": []
},
{
"name": "no-orphaned-packages",
"passed": true,
"message": "No orphaned packages in lockfile",
"details": []
},
{
"name": "skill-subset-consistency",
"passed": false,
"message": "1 skill subset mismatch(es) -- regenerate lockfile (apm install)",
"details": [
"dotnet/skills/plugins/dotnet-diag: recorded skill subset path(s) not found in package tree: dump-collect"
]
},
{
"name": "config-consistency",
"passed": true,
"message": "No MCP configs to check",
"details": []
},
{
"name": "content-integrity",
"passed": true,
"message": "No critical hidden Unicode or hash drift detected",
"details": []
},
{
"name": "includes-consent",
"passed": true,
"message": "No local content deployed -- includes consent check skipped",
"details": []
},
{
"name": "drift",
"passed": true,
"message": "no drift detected against lockfile",
"details": []
}
],
"summary": {
"total": 10,
"passed": 9,
"failed": 1
},
"drift": {
"drift": []
}
}
[>] Diffing scratch vs working tree...
[+] No drift detected
Prepared and reproduced with assistance from OpenAI Codex.
Describe the bug
apm audit --cifalsely failsskill-subset-consistencyin a fresh checkout containing valid committed APM outputs when a dependency usesskills:. The same manifest, lockfile and deployed files pass immediately after installation. Fresh scratch replay reports no drift; all other nine checks pass.Reproduced on 0.30.0 and current release 0.32.0 with one public pinned dependency and one selected skill. No private repository, custom APM patches, MCP configuration or org policy is involved.
To Reproduce
apm.ymland.gitignorecontainingapm_modules/:apm install, thenapm audit --ci --no-policy --no-fail-fast --format json. Both exit 0..gitignore.git clone --no-local <seed-path> <fresh-path>. The clone has noapm_modules.apm_modulesremains absent.The complete script below automates these steps:
python repro.py --apm /path/to/apm, orpython repro.pywith APM on PATH. It creates and retains temporary fixtures; it does not modify an existing repository. Public GitHub access is required.Expected behavior
The fresh checkout should pass. Validate selected skill existence against the lock-pinned dependency tree prepared by audit, while preserving invalid-selection and deployed-drift checks.
The official audit-only pattern for committed outputs explicitly avoids installing into the checkout first, because installation overwrites the bytes audit should inspect.
Environment
apm_modules, not absence of all host caches.Logs
Both versions: warm audit exit 0; fresh audit exit 1, 9/10 checks pass. Only failure:
Drift passes with
no drift detected against lockfileand an empty drift list. The script verifies unchanged checkout contents.Additional context / suspected cause
In 0.32.0 ci_checks.py,
_check_skill_subset_consistencyforwardsproject_root;_missing_recorded_skill_subset_pathsusesdep_ref.get_install_path(project_root / APM_MODULES_DIR), which is absent in the clone. The adjacent configuration check instead consumesprepared_replay.modules_root.This looks like a missing integration between subset validation and cold-checkout replay. Using the prepared dependency tree may be a fix direction; no fix is implemented or validated here. The lookup also remains in main at
4e916ecbddd14edb2f0c93d0199ac9bfa4a7d3dd(source inspected only).This differs from #2172: drift passes here; the baseline subset-existence check fails specifically without checkout-local modules. Existing subset/audit issues were searched before filing.
Complete reproduction script
Full 0.32.0 fresh audit output
Prepared and reproduced with assistance from OpenAI Codex.