fix(install): accept YAML list form under singular target: key (#1188)#1197
Merged
Conversation
The install-pipeline parser (apm_yml.parse_targets_field) crashed on
'target: [copilot, claude]' because it called str(raw) on a Python
list, producing the literal repr "['copilot', 'claude']" and then
comma-splitting it into garbled tokens ("['copilot'", "'claude']").
The first token failed validation and surfaced as
'Unknown target [copilot' with unbalanced quotes.
Worse, the dry-run path uses a different parser
(target_detection.parse_target_field) which DOES handle lists, so
'apm install --dry-run' silently passed on the same apm.yml that
'apm install' rejected -- masking the bug from anyone who relied on
dry-run to validate their config.
Fix:
- apm_yml.parse_targets_field: detect isinstance(raw, list) inside
the singular-key branch and tokenize like the plural-key branch
does. Empty list under singular key falls through to auto-detect
(consistent with 'target:' with no value); only PLURAL 'targets: []'
raises EmptyTargetsListError.
- errors.render_unknown_target_error: strip bracket/quote noise from
the headline value (defense-in-depth for any future leaks) and
suggest 'copilot' as the default fix instead of valid[0], which
resolves to 'agent-skills' after sorted(CANONICAL_TARGETS) and is
actively misleading for users who typed 'copilot'/'claude'.
- install/phases/targets.py: pass symbol=None when emitting
already-rendered TargetResolutionError messages so the error block
doesn't get double-prefixed with '[x] [x]'.
Tests:
- tests/unit/core/test_target_resolution_v2.py: 8 new cases for
list-form parsing (single-item, two-item, whitespace, empty,
duplicates, unknown token, non-string, 'all' rejection).
- tests/unit/core/test_error_renderer.py: copilot-suggestion default,
garbled-value sanitization.
- tests/unit/install/phases/test_read_yaml_targets_list_form.py: new
file exercising the install-pipeline read path on flow-list,
block-list, CSV, and scalar forms; includes a cross-parser shape
check as a regression-trap for dry-run-vs-real divergence.
Plan B (eliminating the duplicate parser) is filed as a follow-up.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This pull request fixes apm install failing to parse a YAML list under the singular target: key in apm.yml, aligning install behavior with the dry-run parser and improving the unknown-target error output.
Changes:
- Extend
parse_targets_field()to accepttarget: [a, b]/ block-list forms under the singulartarget:key. - Improve unknown-target error rendering (sanitize headline value; prefer
copilotas the suggested fix). - Add unit tests covering singular-key YAML list parsing and the install-pipeline read path; adjust install-phase logging to avoid double
[x]prefixes.
Show a summary per file
| File | Description |
|---|---|
src/apm_cli/core/apm_yml.py |
Accept YAML list values under singular target: and validate tokens correctly. |
src/apm_cli/core/errors.py |
Sanitize unknown-target headline and adjust suggestion selection. |
src/apm_cli/install/phases/targets.py |
Suppress double-prefixing when logging already-rendered [x] errors. |
tests/unit/core/test_target_resolution_v2.py |
Add regression tests for singular target: YAML list parsing and edge cases. |
tests/unit/core/test_error_renderer.py |
Add tests for improved unknown-target rendering and suggestion choice. |
tests/unit/install/phases/test_read_yaml_targets_list_form.py |
New tests exercising install pipeline YAML read path for list/scalar/CSV forms. |
CHANGELOG.md |
Add an Unreleased entry describing the fix. |
Copilot's findings
- Files reviewed: 7/7 changed files
- Comments generated: 4
Comment on lines
211
to
215
| # The renderer already emits a leading "[x]"; pass | ||
| # symbol=None so logger.error doesn't double-prefix. | ||
| if ctx.logger: | ||
| ctx.logger.error(str(exc)) | ||
| ctx.logger.error(str(exc), symbol=None) | ||
| raise SystemExit(2) from exc |
Comment on lines
+119
to
+124
| valid_csv = ", ".join(sorted(valid)) | ||
| # Strip bracket/quote noise that can leak in from misparsed tokens | ||
| # (e.g. "['copilot'"). Defense-in-depth: callers should pass clean | ||
| # values, but this keeps the headline readable if they don't. | ||
| display_value = value.strip("[]'\" ") | ||
| suggestion = "copilot" if "copilot" in valid else (valid[0] if valid else "claude") |
Comment on lines
+120
to
127
| # Strip bracket/quote noise that can leak in from misparsed tokens | ||
| # (e.g. "['copilot'"). Defense-in-depth: callers should pass clean | ||
| # values, but this keeps the headline readable if they don't. | ||
| display_value = value.strip("[]'\" ") | ||
| suggestion = "copilot" if "copilot" in valid else (valid[0] if valid else "claude") | ||
| return ( | ||
| f"[x] Unknown target '{value}'\n" | ||
| f"[x] Unknown target '{display_value}'\n" | ||
| "\n" |
| - Stabilized `test_install_over_defer_threshold_starts_live_once` on slow CI runners by joining the deferred-start timer thread instead of relying on a 100ms grace window. (#1191) | ||
| - `triage-panel` scheduled sweep now paginates the candidate query oldest-first via the GitHub MCP `list_issues` tool instead of a single 200-issue page, so daily runs actually drain the untriaged backlog rather than processing one issue per cron tick. (#1193) | ||
| - `triage-panel` scheduled sweep switches the candidate query from `list_issues`+prose-driven pagination to `search_issues` with `-label:status/triaged sort:created-asc`, so untriaged candidates are filtered server-side; the previous approach silently noop'd because the MCP gateway DIFC filter dropped non-collaborator issues mid-page and the agent inferred a false `hasNextPage:false`. | ||
| - `apm install` now accepts the YAML list form under `target:` (e.g. `target: [copilot, claude]` or block-list), which previously crashed with `Unknown target '['copilot''` because the install-pipeline parser stringified the list before splitting; the unknown-target error renderer also strips bracket/quote noise from the headline value and suggests `copilot` instead of the alphabetically-first `agent-skills`. (#1197) |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- errors.py: derive fallback suggestion from sorted(valid) for stable ordering; fall back to raw value or '<empty>' when bracket/quote sanitization strips the headline to nothing. - targets.py: pass symbol='' instead of symbol=None to honor the CommandLogger.error type contract (str = 'error') while still suppressing the double-prefix. - CHANGELOG: shorten to one concise line per repo convention. - tests: cover the all-noise sanitization edge case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Collaborator
Author
|
Addressed all four Copilot review comments in
Lint clean, all 62 affected unit tests still pass. |
danielmeppiel
added a commit
that referenced
this pull request
May 8, 2026
* docs: update target reference for list-form support and cowork exclusion from all - cli-commands.md: clarify copilot-cowork is excluded from --target all; note it requires explicit --target copilot-cowork --global (#1191) - manifest-schema.md: add block-list YAML example for target: field, now that the singular target: key correctly parses list forms (#1197) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This was referenced May 8, 2026
danielmeppiel
added a commit
that referenced
this pull request
May 8, 2026
The unknown-target error renderer advertised every member of
CANONICAL_TARGETS as a recovery path, including the meta-target
"agent-skills". But "apm targets" intentionally omits "agent-skills"
from its table (it is a multi-harness fan-out target with no single
deploy_dir; visible only via "apm targets --json --all"). A user
following the error message's "apm targets # see all supported
harnesses" hint cannot confirm "agent-skills" exists, leaving them
stuck on a contradiction APM authored itself.
Filter "agent-skills" out of the rendered "Valid targets:" CSV and
out of the "did you mean?" suggestion fallback chain. The canonical
set still ACCEPTS "agent-skills" via "--target agent-skills" and the
"target:"/"targets:" keys in apm.yml, so power users who pass it
explicitly continue to work; we just stop steering beginners toward a
target the discovery command refuses to confirm.
Adds two tests:
- agent-skills must not appear anywhere in the unknown-target render.
- If the caller filters down to only agent-skills, the renderer must
fall back to a sane default suggestion ("claude") rather than
re-introducing agent-skills.
Closes #1208 (paired with #1197 which fixed the parser crash).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
danielmeppiel
added a commit
that referenced
this pull request
May 8, 2026
The unknown-target error renderer advertised every member of
CANONICAL_TARGETS as a recovery path, including the meta-target
"agent-skills". But "apm targets" intentionally omits "agent-skills"
from its table (it is a multi-harness fan-out target with no single
deploy_dir; visible only via "apm targets --json --all"). A user
following the error message's "apm targets # see all supported
harnesses" hint cannot confirm "agent-skills" exists, leaving them
stuck on a contradiction APM authored itself.
Filter "agent-skills" out of the rendered "Valid targets:" CSV and
out of the "did you mean?" suggestion fallback chain. The canonical
set still ACCEPTS "agent-skills" via "--target agent-skills" and the
"target:"/"targets:" keys in apm.yml, so power users who pass it
explicitly continue to work; we just stop steering beginners toward a
target the discovery command refuses to confirm.
Adds two tests:
- agent-skills must not appear anywhere in the unknown-target render.
- If the caller filters down to only agent-skills, the renderer must
fall back to a sane default suggestion ("claude") rather than
re-introducing agent-skills.
Closes #1208 (paired with #1197 which fixed the parser crash).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
danielmeppiel
added a commit
that referenced
this pull request
May 8, 2026
…tions (#1208) (#1215) * fix(errors): hide agent-skills from unknown-target suggestions (#1208) The unknown-target error renderer advertised every member of CANONICAL_TARGETS as a recovery path, including the meta-target "agent-skills". But "apm targets" intentionally omits "agent-skills" from its table (it is a multi-harness fan-out target with no single deploy_dir; visible only via "apm targets --json --all"). A user following the error message's "apm targets # see all supported harnesses" hint cannot confirm "agent-skills" exists, leaving them stuck on a contradiction APM authored itself. Filter "agent-skills" out of the rendered "Valid targets:" CSV and out of the "did you mean?" suggestion fallback chain. The canonical set still ACCEPTS "agent-skills" via "--target agent-skills" and the "target:"/"targets:" keys in apm.yml, so power users who pass it explicitly continue to work; we just stop steering beginners toward a target the discovery command refuses to confirm. Adds two tests: - agent-skills must not appear anywhere in the unknown-target render. - If the caller filters down to only agent-skills, the renderer must fall back to a sane default suggestion ("claude") rather than re-introducing agent-skills. Closes #1208 (paired with #1197 which fixed the parser crash). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(changelog): add entry for #1215 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(errors): avoid empty 'Valid targets:' when only meta-target visible Address PR #1215 review: when the caller passes a list containing only "agent-skills" (or empty), the rendered "Valid targets:" line previously collapsed to a bare colon because the visible-set filter consumed all entries. Compute the safety-net suggestion first and use it as the CSV fallback so the line always carries a single actionable harness name. Production call sites all pass the full canonical set, so this only exercises in tests and hypothetical future callers; covers defense-in-depth without changing behavior on the hot path. Extends test_unknown_target_error_falls_back_when_only_meta_target_visible to pin the new "Valid targets: claude" assertion alongside the existing suggestion-fallback checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(cli): align outdated help text with consistency test PR #1216 added help="Check for outdated locked dependencies" to the outdated command, but tests/unit/test_cli_consistency.py expects "Show outdated locked dependencies" (the original docstring wording the test was written against). The mismatch broke CI on main. Restore the docstring wording so the help description matches the test contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1188.
TL;DR
apm installnow acceptstarget: [copilot, claude](YAML flow-list) and the equivalent block-list form under the canonical singulartarget:key. Before this PR, the install pipeline crashed withUnknown target '['copilot''whileapm install --dry-runsilently accepted the same config.Problem (WHY)
Issue #1188 reports that this perfectly valid
apm.yml:crashes
apm installwith:Two bugs surface at once:
apm_cli.core.apm_yml.parse_targets_fielddoesstr(raw).strip()then comma-splits. Whenrawis a Python list,str([...])yields the literal repr"['copilot', 'claude']", splitting on,yields"['copilot'"," 'claude']", and the first token fails validation. Thetargets:(plural) branch handles lists correctly — only the singular branch was broken.core.target_detection.parse_target_field) which DOES handle lists. Soapm install --dry-runsilently passes on the exactapm.ymlthat realapm installrejects. Anyone using dry-run to validate config was misled.The error rendering also had three coupled UX paper cuts surfaced by this same code path:
[x] [x]prefix (renderer prepends one,logger.errorprepends another).'['copilot''headline with unbalanced quotes.valid[0]aftersorted(CANONICAL_TARGETS)=agent-skills— actively misleading when the user typedcopilot/claude.Approach (WHAT)
Surgical fix in
parse_targets_field(Plan A, ratified by the devx-ux expert) plus the three tightly-coupled error-rendering fixes the cli-logging-ux expert flagged on the same code path. Eliminating the duplicate parser entirely (Plan B) is the right architectural move —core.target_detection.parse_target_field's docstring already says "Single source of truth" — but it's a refactor with non-trivial blast radius (different return-type contract, different alias-resolution behavior, different empty-list semantics) and belongs in its own PR. Filed as follow-up.The fail-fast issue (target validation runs after 13s of dependency resolution) and a proper fuzzy-match
did_you_meanhelper are also follow-ups; they're feature work, not bug fixes.Implementation (HOW)
1.
src/apm_cli/core/apm_yml.py— parserIn the
if has_target:branch, detectisinstance(raw, list)and tokenize the same way thetargets:(plural) branch does. Empty list under the singular key falls through to auto-detect (matchestarget:with no value); only the pluraltargets: []raisesEmptyTargetsListError, which is the explicit-but-empty contradictory case.2.
src/apm_cli/core/errors.py—render_unknown_target_error[,],',", and spaces from the headline value (defense-in-depth for any future garbled-token leaks).copilotas the suggested fix (orvalid[0]as fallback ifcopilotsomehow isn't in the canonical set), instead ofvalid[0]which aftersorted(CANONICAL_TARGETS)resolves toagent-skills.3.
src/apm_cli/install/phases/targets.py— double[x]fixTwo
ctx.logger.error(str(exc))sites (lines 212, 251) were emitting already-rendered error blocks (which start with[x]) throughlogger.error(), which prepends another[x]via the defaultsymbol="error". Passsymbol=Noneto suppress the second prefix. The renderer's leading symbol is preserved.Tests
tests/unit/core/test_target_resolution_v2.py— 8 new parser cases:['copilot', 'claude'])['copilot']parity with scalartarget: copilot)str()and validates'all'mixed with canonical token is rejected (no escape hatch)tests/unit/core/test_error_renderer.py— 2 new renderer cases:copilot, not the alphabetically-firstagent-skillstests/unit/install/phases/test_read_yaml_targets_list_form.py— new file, 6 cases on the install-pipeline read path:target: [a, b])target:\n - a\n - b)Validation
Manual repro on the issue's exact
apm.yml:(Pre-fix on the same file:
Unknown target '['copilot''with unbalanced quotes and[x] [x]doubled prefix.)Trade-offs
_read_yaml_targetsdelegate toparse_target_field) is architecturally correct but blast-radius large. Plan A is 6 lines of correctness fix in one branch of one function. Filing Plan B as tech-debt with a link back here.Follow-ups (separate issues)
core.target_detection.parse_target_fieldso the install path and dry-run path share a single source of truth.did_you_mean: a realdifflib.get_close_matches()-backed suggestion helper. Hardcodedcopilotfallback is fine for now.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com