feat(transport): TransportSelector + strict-by-default transport (#778)#779
feat(transport): TransportSelector + strict-by-default transport (#778)#779danielmeppiel merged 6 commits intomainfrom
Conversation
Implements the core decision engine for issue #778 'transport selection v1'. Strict-by-default semantics replace today's silent cross-protocol fallback: explicit ssh:// and https:// dependencies no longer downgrade to a different protocol, and shorthand (owner/repo) consults git insteadOf rewrites before defaulting to HTTPS. This commit ships Waves 1+2 of the transport-selection plan (per session plan): - new module src/apm_cli/deps/transport_selection.py with ProtocolPreference, TransportAttempt/TransportPlan dataclasses, GitConfigInsteadOfResolver, and TransportSelector that returns a typed, strict-by-default plan - DependencyReference grows explicit_scheme so the selector can distinguish user-stated transport from shorthand - _clone_with_fallback in github_downloader.py now iterates the selector plan; per-attempt URL building stays in the orchestrator - InstallContext / InstallRequest / pipeline / Service threaded with protocol_pref + allow_protocol_fallback so CLI args reach the downloader - apm install gains --ssh / --https (mutually exclusive) and --allow-protocol-fallback flags; honours APM_GIT_PROTOCOL and APM_ALLOW_PROTOCOL_FALLBACK env vars - two pre-existing tests in test_auth_scoping.py asserted the legacy permissive chain; updated to assert the new strict contract and added a coverage test for the allow_fallback escape hatch Tests: 4029 unit tests pass. Test matrix + integration tests + docs land in subsequent commits per Waves 3-5. Refs #778, #328, #661 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…one env Wave 2 panel gate (code-review subagent) flagged that GitHubPackageDownloader._clone_with_fallback decided the clone env (locked-down vs relaxed) ONCE per dependency from has_token. Under allow_fallback=True the plan can mix attempts of different use_token values, so SSH and plain-HTTPS attempts in a mixed chain were running with GIT_ASKPASS=echo + GIT_CONFIG_GLOBAL=/dev/null + GIT_CONFIG_NOSYSTEM=1, breaking ssh-agent passphrase prompts and git credential helpers. Fix the env per attempt; address an adjacent contract bug; add tests. * github_downloader._clone_with_fallback: replace the per-dep clone_env with a per-attempt _env_for() helper so only token-bearing attempts get the locked-down env. * github_downloader._build_repo_url: treat token="" as an explicit "no token" sentinel so plain-HTTPS attempts in a mixed chain genuinely run without embedded credentials, letting credential helpers (gh auth, Keychain) supply auth. Orchestrator passes "" instead of None for use_token=False attempts. * transport_selection.GitConfigInsteadOfResolver: wrap the lazy insteadOf-rewrite cache in a threading.Lock so parallel downloads can't double-populate. Tests: * tests/unit/test_transport_selection.py (NEW, 30 tests): 14-row selection matrix (explicit-strict, shorthand+insteadOf, shorthand defaults, CLI prefs, allow_fallback chain, env helpers); resolver caching; "must use normal env" contract. * tests/unit/test_auth_scoping.py: new test_allow_fallback_env_is_per_attempt_not_per_dep regression asserts auth-HTTPS gets locked-down env, SSH and plain-HTTPS get relaxed env, and plain-HTTPS does not embed the token in the URL. * tests/integration/test_transport_selection_integration.py (NEW, 7 tests): 2 always-on cases (public shorthand HTTPS; explicit https:// strict); 5 SSH-required cases (explicit ssh:// strict, bad-host no-fallback, insteadOf override, APM_GIT_PROTOCOL=ssh env, allow_fallback rescue). Gated on APM_RUN_INTEGRATION_TESTS=1; SSH cases auto-skip if no key. * tests/fixtures/gitconfig_insteadof_to_ssh (NEW): minimal gitconfig used by the integration test for the insteadOf-honored case. * scripts/test-integration.sh: added "Transport Selection" block so the integration suite runs in CI. Full unit suite: 4061 passed (was 4029; +32 net new tests). Refs #778, #661, #328 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update the four user-facing surfaces affected by the new TransportSelector
contract:
* docs/src/content/docs/guides/dependencies.md:
- New "Transport selection (SSH vs HTTPS)" section: breaking-change
callout with the rescue env var, selection matrix, insteadOf
example, --ssh / --https / APM_GIT_PROTOCOL overrides, and the
--allow-protocol-fallback escape hatch.
- Soften the existing "Custom ports preserved" sentence (cross-protocol
retries are now opt-in).
- Update the "Other Git Hosts" SSH bullet: SSH is no longer a silent
fallback; point at explicit URLs or insteadOf.
* docs/src/content/docs/getting-started/authentication.md:
- Rewrite the "SSH connection hangs" troubleshooting entry: remove the
now-incorrect "tries SSH then falls back to HTTPS" framing.
- New "Choosing transport (SSH vs HTTPS)" section with a pointer to
dependencies.md for the full transport contract.
* docs/src/content/docs/reference/cli-commands.md:
- Document --ssh / --https / --allow-protocol-fallback on apm install,
plus APM_GIT_PROTOCOL and APM_ALLOW_PROTOCOL_FALLBACK env vars.
* packages/apm-guide/.apm/skills/apm-usage/dependencies.md (Rule 4 mirror):
- Same transport contract in skill-resource voice with three runnable
snippets and a selection matrix.
CHANGELOG: scope new entries to `apm install` only (there is no `apm
add` command in the codebase).
Refs #778
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Visual reference for reviewersThree diagrams to make the PR easier to read end-to-end. All flows live in 1. Selection: input ->
|
There was a problem hiding this comment.
Pull request overview
Implements Transport Selection v1 so APM distinguishes explicit transport (scheme) from shorthand and becomes strict-by-default, with an opt-in legacy cross-protocol fallback via CLI/env.
Changes:
- Add
TransportSelectordecision engine and route clone attempts through a computedTransportPlan. - Add
apm installflags/env vars to control shorthand transport (--ssh/--https,APM_GIT_PROTOCOL) and legacy fallback (--allow-protocol-fallback,APM_ALLOW_PROTOCOL_FALLBACK). - Add unit + integration coverage and update docs/CHANGELOG for the new contract.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_transport_selection.py | New unit test matrix for transport planning and insteadOf resolution/caching. |
| tests/unit/test_auth_scoping.py | Regression tests for per-attempt env + token scoping under fallback. |
| tests/integration/test_transport_selection_integration.py | New networked integration coverage validating strictness + overrides. |
| tests/fixtures/gitconfig_insteadof_to_ssh | Fixture .gitconfig for insteadOf HTTPS->SSH rewrite. |
| src/apm_cli/models/dependency/reference.py | Add explicit_scheme signal to parsed dependency references. |
| src/apm_cli/install/service.py | Thread transport prefs into the install pipeline. |
| src/apm_cli/install/request.py | Extend request with protocol preference + fallback toggle. |
| src/apm_cli/install/pipeline.py | Thread protocol preference + fallback into InstallContext. |
| src/apm_cli/install/phases/resolve.py | Construct downloader with protocol pref + fallback. |
| src/apm_cli/install/context.py | Store protocol pref + fallback on context. |
| src/apm_cli/deps/transport_selection.py | New transport selection module (plan + insteadOf resolver). |
| src/apm_cli/deps/github_downloader.py | Consume TransportPlan; implement strict-by-default cloning + warnings. |
| src/apm_cli/commands/install.py | Add --ssh/--https/--allow-protocol-fallback and resolve env/flag behavior. |
| scripts/test-integration.sh | Add CI integration test invocation for transport selection suite. |
| packages/apm-guide/.apm/skills/apm-usage/dependencies.md | Document strict selection + overrides + fallback escape hatch. |
| docs/src/content/docs/reference/cli-commands.md | Document new flags and transport env vars for apm install. |
| docs/src/content/docs/guides/dependencies.md | Document strict transport contract and migration/fallback guidance. |
| docs/src/content/docs/getting-started/authentication.md | Update SSH hang guidance and link to transport selection docs. |
| CHANGELOG.md | Add BREAKING/Added entries describing transport selection changes. |
Copilot's findings
Comments suppressed due to low confidence (2)
CHANGELOG.md:18
- This entry is very long and doesn't match the changelog format used elsewhere (concise, one line per PR, ending with
(#PR)), and it mixes multiple concerns into one bullet. Please condense and ensure the bullet ends with the PR number (e.g.(#778)).
### Added
- `apm install` accepts `--ssh` / `--https` flags and `APM_GIT_PROTOCOL=ssh|https` env to pick the initial transport for shorthand URLs. `apm install` also accepts `--allow-protocol-fallback` (env: `APM_ALLOW_PROTOCOL_FALLBACK=1`) as the escape hatch for cross-protocol fallback when migrating off the previous permissive behavior (#778).
- Add APM Review Panel skill (`.github/skills/apm-review-panel/`) and four new specialist personas (`devx-ux-expert`, `supply-chain-security-expert`, `apm-ceo`, `oss-growth-hacker`) with auto-activating per-persona skills. Routes specialist findings through an APM CEO arbiter for strategic / breaking-change calls, with the OSS growth hacker side-channeling adoption insights via `WIP/growth-strategy.md`. Instrumentation per Handbook Ch. 9 (`The Instrumented Codebase`); PROSE-compliant (thin SKILL.md routers, persona detail lazy-loaded via markdown links, explicit boundaries per persona).
docs/src/content/docs/guides/dependencies.md:155
- This excerpt contains non-ASCII punctuation (e.g. em dash "—" and Unicode arrow "→"). The repo's cross-platform encoding rules require docs to stay printable ASCII; please replace with ASCII equivalents ("--", "->", straight quotes, etc.).
>
> ```yaml
> # DON'T — ambiguous: APM can't tell where the repo path ends
> # gitlab.com/group/subgroup/repo/file.prompt.md
> # → parsed as repo=group/subgroup, virtual=repo/file.prompt.md (wrong!)
- Files reviewed: 19/19 changed files
- Comments generated: 5
Two findings from the Copilot reviewer on PR #779: 1. Non-ASCII em dash introduced by this PR in the modified `Fields:` line of guides/dependencies.md: replace with `--`. The other non-ASCII chars Copilot flagged in the file (lines ~150-160 of the "nested groups" warning block) are pre-existing and out of scope for this PR. 2. CHANGELOG entries for the new transport-selection feature were too long and bundled multiple concerns into one bullet. Split into one tighter BREAKING entry plus two single-purpose Added entries (initial-protocol flags; fallback escape hatch). Each ends in `(#778)`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Both findings addressed in 1. Non-ASCII punctuation in 2. CHANGELOG entry too long, mixed concerns -- valid. Split the original two-concern bullet into:
All three entries end with |
Architecture: design patterns at playA fourth diagram for reviewers who want the design rationale rather than the runtime flow. classDiagram
direction LR
class ProtocolPreference {
<<Enum>>
NONE
SSH
HTTPS
}
class TransportAttempt {
<<Value Object / frozen dataclass>>
+use_ssh: bool
+use_token: bool
+reason: str
}
class TransportPlan {
<<Value Object / frozen dataclass>>
+attempts: list~TransportAttempt~
+strict: bool
+fallback_hint: str
}
class InsteadOfResolver {
<<Protocol / Strategy>>
+resolve(host) Optional~str~
}
class GitConfigInsteadOfResolver {
<<Default Strategy>>
-_cache: dict
-_lock: Lock
+resolve(host) Optional~str~
-_load_rewrites()
}
class FakeInsteadOfResolver {
<<Test Double>>
+rewrites: dict
+resolve(host) Optional~str~
}
class TransportSelector {
<<Factory / Pure Function>>
-insteadof_resolver: InsteadOfResolver
+select(dep_ref, cli_pref, allow_fallback, has_token) TransportPlan
}
class GitHubPackageDownloader {
<<Orchestrator>>
-protocol_pref: ProtocolPreference
-allow_protocol_fallback: bool
-github_token: str
-git_env: dict
-selector: TransportSelector
+download(dep_ref) Path
-_clone_with_fallback(...)
-_env_for(use_token) dict
-_build_repo_url(..., token: str) str
}
class InstallContext {
<<DI Container>>
+protocol_pref: ProtocolPreference
+allow_protocol_fallback: bool
}
class CliInstallCmd {
<<Adapter>>
+--ssh / --https
+--allow-protocol-fallback
+APM_GIT_PROTOCOL env
+APM_ALLOW_PROTOCOL_FALLBACK env
}
InsteadOfResolver <|.. GitConfigInsteadOfResolver : implements
InsteadOfResolver <|.. FakeInsteadOfResolver : implements
TransportSelector o-- InsteadOfResolver : injected (Strategy)
TransportSelector ..> TransportPlan : returns
TransportPlan o-- TransportAttempt : composed of
TransportSelector ..> ProtocolPreference : consumes
GitHubPackageDownloader o-- TransportSelector : composed
GitHubPackageDownloader ..> TransportPlan : iterates
InstallContext ..> GitHubPackageDownloader : configures
CliInstallCmd ..> InstallContext : populates
Patterns mapped
What this design buys reviewers
|
Integration with neighboring systemsHow TransportSelector composes with the rest of the install pipeline. 1. Lockfile
|
| Surface | Interaction with TransportSelector | Status after #778 |
|---|---|---|
LockedDependency.repo_url |
Identity is transport-agnostic; selection runs fresh on every install | Unchanged |
LockedDependency.port |
Read by _build_repo_url, re-emitted on every attempt |
Unchanged (#661 fix preserved) |
apm-policy.yml |
No git_transport policy field today |
Gap; future opportunity for org-level enforcement of strict-only |
PROXY_REGISTRY_URL / Artifactory |
Disjoint code path (HTTP file fetch, not git clone) | Unchanged; proxy users unaffected |
The net effect: the breaking change is localized to the direct-git-clone code path, which is exactly the surface that #661 / #328 were filed against. Lockfile, port handling, and proxy users see no behavior change.
| APM picks SSH or HTTPS per dependency using a strict, predictable contract. | ||
|
|
||
| :::caution[Breaking change] | ||
| Earlier versions silently retried failed clones across protocols. The current |
There was a problem hiding this comment.
Indicate here APM < 0.8.13
There was a problem hiding this comment.
Done in 51ab924. Tightened the caution panel to:
Breaking change in APM 0.8.13 -- APM versions before 0.8.13 silently retried failed clones across protocols. Starting in 0.8.13 the behavior is strict by default [...]
That makes the version cutover unambiguous for anyone landing on this page from a search result without scrolling to the changelog.
While here, also took care of the four other Copilot review findings in the same commit:
transport_selection.py-- collapsed theexplicit == "http"branch into plain HTTPS soTransportAttempt.schemestays in{ssh, https}and the downloader contract is consistent. Plainhttp://URLs now build an HTTPS attempt with no token (no cleartext credential leak possible) until feat: support allow-insecure HTTP dependencies #700 introduces a first-class HTTP transport with explicit--allow-insecuregating.github_downloader.py-- the[!]"Protocol fallback" warning is now gated on actual scheme change (ssh <-> https) rather than label change, so an auth-only downgrade inside a single protocol (e.g. authenticated HTTPS -> plain HTTPS in a--ssh + --allow-protocol-fallbackmixed plan) is no longer misreported as a protocol switch.pipeline.py/request.py/context.py/commands/install.py--allow_protocol_fallbackis nowOptional[bool] = Noneend-to-end. Non-CLI programmatic callers regain the documented "None => readAPM_ALLOW_PROTOCOL_FALLBACKenv" behavior. The CLI still resolves env at the command boundary so behavior there is unchanged.tests/unit/test_auth_scoping.py-- replaced the one stray Unicode arrow with->to satisfy the ASCII-only source rule.
All 4061 unit tests pass.
- docs(dependencies): pin BREAKING-change caution panel to APM 0.8.13
(per maintainer review comment)
- transport_selection: collapse explicit `http://` URLs into the plain-HTTPS
branch so TransportAttempt.scheme stays in {ssh, https} and the downloader
contract is consistent until #700 lands a first-class HTTP transport
- github_downloader: gate the [!] "Protocol fallback" warning on actual
scheme change (ssh<->https) rather than label change, so an auth downgrade
inside a single protocol is not misreported as a protocol switch
- pipeline / request / context / commands: switch
`allow_protocol_fallback` to `Optional[bool] = None` end-to-end so
programmatic callers (non-CLI) keep the documented "None => read
APM_ALLOW_PROTOCOL_FALLBACK env" behavior
- test_auth_scoping: ASCII-only docstring (replace one stray Unicode arrow)
All 4061 unit tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ade (#780) (#781) * feat(transport): TransportSelector + strict-by-default transport (#778) Implements the core decision engine for issue #778 'transport selection v1'. Strict-by-default semantics replace today's silent cross-protocol fallback: explicit ssh:// and https:// dependencies no longer downgrade to a different protocol, and shorthand (owner/repo) consults git insteadOf rewrites before defaulting to HTTPS. This commit ships Waves 1+2 of the transport-selection plan (per session plan): - new module src/apm_cli/deps/transport_selection.py with ProtocolPreference, TransportAttempt/TransportPlan dataclasses, GitConfigInsteadOfResolver, and TransportSelector that returns a typed, strict-by-default plan - DependencyReference grows explicit_scheme so the selector can distinguish user-stated transport from shorthand - _clone_with_fallback in github_downloader.py now iterates the selector plan; per-attempt URL building stays in the orchestrator - InstallContext / InstallRequest / pipeline / Service threaded with protocol_pref + allow_protocol_fallback so CLI args reach the downloader - apm install gains --ssh / --https (mutually exclusive) and --allow-protocol-fallback flags; honours APM_GIT_PROTOCOL and APM_ALLOW_PROTOCOL_FALLBACK env vars - two pre-existing tests in test_auth_scoping.py asserted the legacy permissive chain; updated to assert the new strict contract and added a coverage test for the allow_fallback escape hatch Tests: 4029 unit tests pass. Test matrix + integration tests + docs land in subsequent commits per Waves 3-5. Refs #778, #328, #661 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(transport): wave-3 unit + integration matrix; fix per-attempt clone env Wave 2 panel gate (code-review subagent) flagged that GitHubPackageDownloader._clone_with_fallback decided the clone env (locked-down vs relaxed) ONCE per dependency from has_token. Under allow_fallback=True the plan can mix attempts of different use_token values, so SSH and plain-HTTPS attempts in a mixed chain were running with GIT_ASKPASS=echo + GIT_CONFIG_GLOBAL=/dev/null + GIT_CONFIG_NOSYSTEM=1, breaking ssh-agent passphrase prompts and git credential helpers. Fix the env per attempt; address an adjacent contract bug; add tests. * github_downloader._clone_with_fallback: replace the per-dep clone_env with a per-attempt _env_for() helper so only token-bearing attempts get the locked-down env. * github_downloader._build_repo_url: treat token="" as an explicit "no token" sentinel so plain-HTTPS attempts in a mixed chain genuinely run without embedded credentials, letting credential helpers (gh auth, Keychain) supply auth. Orchestrator passes "" instead of None for use_token=False attempts. * transport_selection.GitConfigInsteadOfResolver: wrap the lazy insteadOf-rewrite cache in a threading.Lock so parallel downloads can't double-populate. Tests: * tests/unit/test_transport_selection.py (NEW, 30 tests): 14-row selection matrix (explicit-strict, shorthand+insteadOf, shorthand defaults, CLI prefs, allow_fallback chain, env helpers); resolver caching; "must use normal env" contract. * tests/unit/test_auth_scoping.py: new test_allow_fallback_env_is_per_attempt_not_per_dep regression asserts auth-HTTPS gets locked-down env, SSH and plain-HTTPS get relaxed env, and plain-HTTPS does not embed the token in the URL. * tests/integration/test_transport_selection_integration.py (NEW, 7 tests): 2 always-on cases (public shorthand HTTPS; explicit https:// strict); 5 SSH-required cases (explicit ssh:// strict, bad-host no-fallback, insteadOf override, APM_GIT_PROTOCOL=ssh env, allow_fallback rescue). Gated on APM_RUN_INTEGRATION_TESTS=1; SSH cases auto-skip if no key. * tests/fixtures/gitconfig_insteadof_to_ssh (NEW): minimal gitconfig used by the integration test for the insteadOf-honored case. * scripts/test-integration.sh: added "Transport Selection" block so the integration suite runs in CI. Full unit suite: 4061 passed (was 4029; +32 net new tests). Refs #778, #661, #328 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(transport): document strict-by-default transport selection (#778) Update the four user-facing surfaces affected by the new TransportSelector contract: * docs/src/content/docs/guides/dependencies.md: - New "Transport selection (SSH vs HTTPS)" section: breaking-change callout with the rescue env var, selection matrix, insteadOf example, --ssh / --https / APM_GIT_PROTOCOL overrides, and the --allow-protocol-fallback escape hatch. - Soften the existing "Custom ports preserved" sentence (cross-protocol retries are now opt-in). - Update the "Other Git Hosts" SSH bullet: SSH is no longer a silent fallback; point at explicit URLs or insteadOf. * docs/src/content/docs/getting-started/authentication.md: - Rewrite the "SSH connection hangs" troubleshooting entry: remove the now-incorrect "tries SSH then falls back to HTTPS" framing. - New "Choosing transport (SSH vs HTTPS)" section with a pointer to dependencies.md for the full transport contract. * docs/src/content/docs/reference/cli-commands.md: - Document --ssh / --https / --allow-protocol-fallback on apm install, plus APM_GIT_PROTOCOL and APM_ALLOW_PROTOCOL_FALLBACK env vars. * packages/apm-guide/.apm/skills/apm-usage/dependencies.md (Rule 4 mirror): - Same transport contract in skill-resource voice with three runnable snippets and a selection matrix. CHANGELOG: scope new entries to `apm install` only (there is no `apm add` command in the codebase). Refs #778 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: revert accidental uv.lock churn (no dependency change in this PR) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs+changelog: address copilot review feedback (#779) Two findings from the Copilot reviewer on PR #779: 1. Non-ASCII em dash introduced by this PR in the modified `Fields:` line of guides/dependencies.md: replace with `--`. The other non-ASCII chars Copilot flagged in the file (lines ~150-160 of the "nested groups" warning block) are pre-existing and out of scope for this PR. 2. CHANGELOG entries for the new transport-selection feature were too long and bundled multiple concerns into one bullet. Split into one tighter BREAKING entry plus two single-purpose Added entries (initial-protocol flags; fallback escape hatch). Each ends in `(#778)`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR #779 review feedback - docs(dependencies): pin BREAKING-change caution panel to APM 0.8.13 (per maintainer review comment) - transport_selection: collapse explicit `http://` URLs into the plain-HTTPS branch so TransportAttempt.scheme stays in {ssh, https} and the downloader contract is consistent until #700 lands a first-class HTTP transport - github_downloader: gate the [!] "Protocol fallback" warning on actual scheme change (ssh<->https) rather than label change, so an auth downgrade inside a single protocol is not misreported as a protocol switch - pipeline / request / context / commands: switch `allow_protocol_fallback` to `Optional[bool] = None` end-to-end so programmatic callers (non-CLI) keep the documented "None => read APM_ALLOW_PROTOCOL_FALLBACK env" behavior - test_auth_scoping: ASCII-only docstring (replace one stray Unicode arrow) All 4061 unit tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(install): MARKETPLACE_PLUGIN beats HOOK_PACKAGE in detection cascade (#780) obra/superpowers (and any Claude Code plugin shipping both hooks/*.json and agents/skills/commands) was misclassified as HOOK_PACKAGE because the cascade in detect_package_type() checked _has_hook_json BEFORE the plugin-evidence branch. The plugin synthesizer (_map_plugin_artifacts) already maps hooks alongside agents/skills/commands, so MARKETPLACE_PLUGIN is a strict superset -- swapping the order means hooks still install, plus everything else that was being silently dropped. Three deliverables: A) Surgical detection fix: reorder cascade so MARKETPLACE_PLUGIN is checked before HOOK_PACKAGE. Refactored to use a new gather_detection_evidence() helper + DetectionEvidence dataclass so observability code (warnings, summaries) can reuse the same scan without breaking the detect_package_type() public signature. B) Observability: - Add HOOK_PACKAGE to the package-type label table (it was missing entirely -- the silent classification path). - Update MARKETPLACE_PLUGIN label to mention plugin.json OR agents/skills/commands (matches the cascade behaviour). - New CommandLogger.package_type_warn() at default visibility. - New _warn_if_classification_near_miss() helper fires when a HOOK_PACKAGE classification disagrees with directory contents (catches near-misses the order swap cannot, e.g. .claude-plugin/ dir without plugin.json). - Wired at both materialization sites (local + cached). C) Architectural follow-up tracked in plan; will file as separate issue after merge for a Visitor / Format-Discovery refactor. Tests: 17 detection tests + 9 sources-observability tests + 70 command-logger tests pass. Full unit suite (4180 tests) green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR #781 review: broaden detect_package_type with .claude-plugin/ evidence Per Copilot review on PR #781: the near-miss warning helper had dead code paths once the cascade was reordered (a HOOK_PACKAGE classification implies plugin_dirs_present is empty -- otherwise it would be MARKETPLACE_PLUGIN). Two principled options were offered: simplify the helper, or broaden detection so the invariants stay consistent. Chose the latter -- it removes the inconsistency at the source. Changes: - Add .claude-plugin/ as first-class plugin evidence in DetectionEvidence (new has_claude_plugin_dir field) and in has_plugin_evidence. A Claude Code plugin without a plugin.json (manifest-less) now classifies as MARKETPLACE_PLUGIN; normalize_plugin_directory already handles the missing-manifest case (derives name from directory). - Drop _warn_if_classification_near_miss helper, its two call sites, CommandLogger.package_type_warn method, and the corresponding tests. All scenarios it covered are now handled by the cascade itself. - Add regression test test_claude_plugin_dir_alone_is_plugin_evidence asserting that .claude-plugin/ + hooks/ classifies as MARKETPLACE_PLUGIN with plugin_json_path=None (matched via directory evidence alone). - Extend test_obra_superpowers_evidence to assert has_claude_plugin_dir. Verified: 4175 unit tests pass (excluding the one pre-existing unrelated failure documented on main). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
) * feat(transport): TransportSelector + strict-by-default transport (#778) Implements the core decision engine for issue #778 'transport selection v1'. Strict-by-default semantics replace today's silent cross-protocol fallback: explicit ssh:// and https:// dependencies no longer downgrade to a different protocol, and shorthand (owner/repo) consults git insteadOf rewrites before defaulting to HTTPS. This commit ships Waves 1+2 of the transport-selection plan (per session plan): - new module src/apm_cli/deps/transport_selection.py with ProtocolPreference, TransportAttempt/TransportPlan dataclasses, GitConfigInsteadOfResolver, and TransportSelector that returns a typed, strict-by-default plan - DependencyReference grows explicit_scheme so the selector can distinguish user-stated transport from shorthand - _clone_with_fallback in github_downloader.py now iterates the selector plan; per-attempt URL building stays in the orchestrator - InstallContext / InstallRequest / pipeline / Service threaded with protocol_pref + allow_protocol_fallback so CLI args reach the downloader - apm install gains --ssh / --https (mutually exclusive) and --allow-protocol-fallback flags; honours APM_GIT_PROTOCOL and APM_ALLOW_PROTOCOL_FALLBACK env vars - two pre-existing tests in test_auth_scoping.py asserted the legacy permissive chain; updated to assert the new strict contract and added a coverage test for the allow_fallback escape hatch Tests: 4029 unit tests pass. Test matrix + integration tests + docs land in subsequent commits per Waves 3-5. Refs #778, #328, #661 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(transport): wave-3 unit + integration matrix; fix per-attempt clone env Wave 2 panel gate (code-review subagent) flagged that GitHubPackageDownloader._clone_with_fallback decided the clone env (locked-down vs relaxed) ONCE per dependency from has_token. Under allow_fallback=True the plan can mix attempts of different use_token values, so SSH and plain-HTTPS attempts in a mixed chain were running with GIT_ASKPASS=echo + GIT_CONFIG_GLOBAL=/dev/null + GIT_CONFIG_NOSYSTEM=1, breaking ssh-agent passphrase prompts and git credential helpers. Fix the env per attempt; address an adjacent contract bug; add tests. * github_downloader._clone_with_fallback: replace the per-dep clone_env with a per-attempt _env_for() helper so only token-bearing attempts get the locked-down env. * github_downloader._build_repo_url: treat token="" as an explicit "no token" sentinel so plain-HTTPS attempts in a mixed chain genuinely run without embedded credentials, letting credential helpers (gh auth, Keychain) supply auth. Orchestrator passes "" instead of None for use_token=False attempts. * transport_selection.GitConfigInsteadOfResolver: wrap the lazy insteadOf-rewrite cache in a threading.Lock so parallel downloads can't double-populate. Tests: * tests/unit/test_transport_selection.py (NEW, 30 tests): 14-row selection matrix (explicit-strict, shorthand+insteadOf, shorthand defaults, CLI prefs, allow_fallback chain, env helpers); resolver caching; "must use normal env" contract. * tests/unit/test_auth_scoping.py: new test_allow_fallback_env_is_per_attempt_not_per_dep regression asserts auth-HTTPS gets locked-down env, SSH and plain-HTTPS get relaxed env, and plain-HTTPS does not embed the token in the URL. * tests/integration/test_transport_selection_integration.py (NEW, 7 tests): 2 always-on cases (public shorthand HTTPS; explicit https:// strict); 5 SSH-required cases (explicit ssh:// strict, bad-host no-fallback, insteadOf override, APM_GIT_PROTOCOL=ssh env, allow_fallback rescue). Gated on APM_RUN_INTEGRATION_TESTS=1; SSH cases auto-skip if no key. * tests/fixtures/gitconfig_insteadof_to_ssh (NEW): minimal gitconfig used by the integration test for the insteadOf-honored case. * scripts/test-integration.sh: added "Transport Selection" block so the integration suite runs in CI. Full unit suite: 4061 passed (was 4029; +32 net new tests). Refs #778, #661, #328 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(transport): document strict-by-default transport selection (#778) Update the four user-facing surfaces affected by the new TransportSelector contract: * docs/src/content/docs/guides/dependencies.md: - New "Transport selection (SSH vs HTTPS)" section: breaking-change callout with the rescue env var, selection matrix, insteadOf example, --ssh / --https / APM_GIT_PROTOCOL overrides, and the --allow-protocol-fallback escape hatch. - Soften the existing "Custom ports preserved" sentence (cross-protocol retries are now opt-in). - Update the "Other Git Hosts" SSH bullet: SSH is no longer a silent fallback; point at explicit URLs or insteadOf. * docs/src/content/docs/getting-started/authentication.md: - Rewrite the "SSH connection hangs" troubleshooting entry: remove the now-incorrect "tries SSH then falls back to HTTPS" framing. - New "Choosing transport (SSH vs HTTPS)" section with a pointer to dependencies.md for the full transport contract. * docs/src/content/docs/reference/cli-commands.md: - Document --ssh / --https / --allow-protocol-fallback on apm install, plus APM_GIT_PROTOCOL and APM_ALLOW_PROTOCOL_FALLBACK env vars. * packages/apm-guide/.apm/skills/apm-usage/dependencies.md (Rule 4 mirror): - Same transport contract in skill-resource voice with three runnable snippets and a selection matrix. CHANGELOG: scope new entries to `apm install` only (there is no `apm add` command in the codebase). Refs #778 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: revert accidental uv.lock churn (no dependency change in this PR) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs+changelog: address copilot review feedback (#779) Two findings from the Copilot reviewer on PR #779: 1. Non-ASCII em dash introduced by this PR in the modified `Fields:` line of guides/dependencies.md: replace with `--`. The other non-ASCII chars Copilot flagged in the file (lines ~150-160 of the "nested groups" warning block) are pre-existing and out of scope for this PR. 2. CHANGELOG entries for the new transport-selection feature were too long and bundled multiple concerns into one bullet. Split into one tighter BREAKING entry plus two single-purpose Added entries (initial-protocol flags; fallback escape hatch). Each ends in `(#778)`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR #779 review feedback - docs(dependencies): pin BREAKING-change caution panel to APM 0.8.13 (per maintainer review comment) - transport_selection: collapse explicit `http://` URLs into the plain-HTTPS branch so TransportAttempt.scheme stays in {ssh, https} and the downloader contract is consistent until #700 lands a first-class HTTP transport - github_downloader: gate the [!] "Protocol fallback" warning on actual scheme change (ssh<->https) rather than label change, so an auth downgrade inside a single protocol is not misreported as a protocol switch - pipeline / request / context / commands: switch `allow_protocol_fallback` to `Optional[bool] = None` end-to-end so programmatic callers (non-CLI) keep the documented "None => read APM_ALLOW_PROTOCOL_FALLBACK env" behavior - test_auth_scoping: ASCII-only docstring (replace one stray Unicode arrow) All 4061 unit tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(install): MARKETPLACE_PLUGIN beats HOOK_PACKAGE in detection cascade (#780) obra/superpowers (and any Claude Code plugin shipping both hooks/*.json and agents/skills/commands) was misclassified as HOOK_PACKAGE because the cascade in detect_package_type() checked _has_hook_json BEFORE the plugin-evidence branch. The plugin synthesizer (_map_plugin_artifacts) already maps hooks alongside agents/skills/commands, so MARKETPLACE_PLUGIN is a strict superset -- swapping the order means hooks still install, plus everything else that was being silently dropped. Three deliverables: A) Surgical detection fix: reorder cascade so MARKETPLACE_PLUGIN is checked before HOOK_PACKAGE. Refactored to use a new gather_detection_evidence() helper + DetectionEvidence dataclass so observability code (warnings, summaries) can reuse the same scan without breaking the detect_package_type() public signature. B) Observability: - Add HOOK_PACKAGE to the package-type label table (it was missing entirely -- the silent classification path). - Update MARKETPLACE_PLUGIN label to mention plugin.json OR agents/skills/commands (matches the cascade behaviour). - New CommandLogger.package_type_warn() at default visibility. - New _warn_if_classification_near_miss() helper fires when a HOOK_PACKAGE classification disagrees with directory contents (catches near-misses the order swap cannot, e.g. .claude-plugin/ dir without plugin.json). - Wired at both materialization sites (local + cached). C) Architectural follow-up tracked in plan; will file as separate issue after merge for a Visitor / Format-Discovery refactor. Tests: 17 detection tests + 9 sources-observability tests + 70 command-logger tests pass. Full unit suite (4180 tests) green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR #781 review: broaden detect_package_type with .claude-plugin/ evidence Per Copilot review on PR #781: the near-miss warning helper had dead code paths once the cascade was reordered (a HOOK_PACKAGE classification implies plugin_dirs_present is empty -- otherwise it would be MARKETPLACE_PLUGIN). Two principled options were offered: simplify the helper, or broaden detection so the invariants stay consistent. Chose the latter -- it removes the inconsistency at the source. Changes: - Add .claude-plugin/ as first-class plugin evidence in DetectionEvidence (new has_claude_plugin_dir field) and in has_plugin_evidence. A Claude Code plugin without a plugin.json (manifest-less) now classifies as MARKETPLACE_PLUGIN; normalize_plugin_directory already handles the missing-manifest case (derives name from directory). - Drop _warn_if_classification_near_miss helper, its two call sites, CommandLogger.package_type_warn method, and the corresponding tests. All scenarios it covered are now handled by the cascade itself. - Add regression test test_claude_plugin_dir_alone_is_plugin_evidence asserting that .claude-plugin/ + hooks/ classifies as MARKETPLACE_PLUGIN with plugin_json_path=None (matched via directory evidence alone). - Extend test_obra_superpowers_evidence to assert has_claude_plugin_dir. Verified: 4175 unit tests pass (excluding the one pre-existing unrelated failure documented on main). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(validation): reject shell-string command in MCP stdio entries Self-defined stdio MCP entries with `command` containing whitespace and no `args` are now rejected at parse time with a fix-it error pointing at the canonical `command: <binary>, args: [<token>, ...]` shape. Previously silently accepted; APM never split `command` on whitespace, so the loose shape mis-executed downstream. The trap surfaced via #122 (thanks @lirantal) -- users coming from the universal `mcp.json` / Claude Desktop / Cursor mental model wrote `command: "npx mcp-server-foo"` and got confused when nothing worked. Per maintainer steer ("move fast, breaking OK"): error in v1, not warn. The loose shape was never specified; silent mis-execution is worse than hard-fail with a clear fix-it. CHANGELOG entry under Changed (BREAKING). Closes #806 Refs #122 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(mcp): address PR #809 review panel (cred leak, args:[], type gate, edge cases) Addresses the APM Review Panel verdict on PR #809. Must-fix: - M1: Redact raw command in error 'Got:' framing (cli-log BLOCKER / sec HIGH). Echoing 'Got: command={self.command!r}' leaked tokens like '--token=ghp_...' to stderr / CI scrollback. Now shows only the first token plus argument count. The structured 'Did you mean:' suggestion still surfaces user input verbatim because that is the copy-paste recovery path. - M2: Use 'self.args is None' instead of 'not self.args' (arch IMPORTANT). Explicit 'args: []' is a deliberate 'no extra args' signal (e.g., paired with '/opt/My App/server') and must be accepted -- 'not []' incorrectly evaluated truthy and rejected legitimate input in a BREAKING change. Should-fix: - S1: Whitespace-only command produces a dedicated 'empty or whitespace-only' error instead of the degenerate fix-it 'Did you mean: command: , args: []' (arch + devx IMPORTANT). - S2: Type gate for non-str command (sec HIGH). YAML 'command: ["npx", "-y", "x"]' previously bypassed the isinstance guard silently and crashed downstream in validate_path_segments with an unhandled AttributeError. - S3: Document rule 4 in manifest-schema.md section 4.2.3 (devx IMPORTANT). Spec and code ship together. Adds 4 regression tests covering each fix. Removes the stray space before '?' in the fix-it suggestion (cli-log NIT 8). Follow-ups (not in this PR, to be filed as issues): - Redact 'command' in MCPDependency.__repr__ (sec MEDIUM, pre-existing) - Forward MCPDependency validation errors from plugin parser to DiagnosticCollector (cli-log IMPORTANT) - Multi-line Cargo-style error format (cli-log IMPORTANT / devx NIT) - Shell-metachar warning for stdio command (sec LOW) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(mcp): address PR #809 follow-up tickets in same PR Per user direction, fold all four CEO-classified follow-up items into this PR rather than deferring to separate issues. FU1 -- Redact 'command' in MCPDependency.__repr__ (sec MEDIUM) Pre-existing leak: __repr__ echoed 'command={self.command!r}' verbatim while carefully redacting env and headers. Now shows only the first whitespace-separated token, mirroring the M1 fix. FU2 -- Surface plugin-parser MCP validation warnings (cli-log IMPORTANT) The 'apm' stdlib logger has no handlers configured, so logger.warning calls in plugin_parser were silently dropped. Added _surface_warning helper that routes through both stdlib logger AND _rich_warning so invalid MCP servers are visible without --verbose. Applied to the validation-error catch site and the no-command/no-url skip. FU3 -- Multi-line Cargo-style error format (cli-log IMPORTANT / devx NIT) The original 350-char single-line ValueError defeated terminal URL detection and the newspaper test. Restructured to: 'command' contains whitespace in MCP dependency '<name>'. Rule: ... Got: command='<first>' (N additional args) Fix: command: <first> args: [...] See: https://... URL now sits on its own line for click-through; field/rule/got/fix/see pattern is scannable per the cli-logging-ux skill's newspaper test. FU4 -- Shell-metachar warning for stdio command (sec LOW, defense-in-depth) Extended _warn_shell_metachars(env, logger) to optionally check 'command' as well, so 'command: "npx|curl evil.com"' (no whitespace, passes the rejection guard) still triggers a warning that MCP stdio servers run via execve with no shell. Hooked into the --mcp install path via entry.get('command'). Architectural improvement (LOC budget): Adding the command-checking branch pushed install.py over the 1525 invariant ceiling. Per the python-architecture skill's guidance ('don't trim cosmetically -- modularize'), extracted the F5 SSRF helper, F7 shell-metachar helper, _is_internal_or_metadata_host, _SHELL_METACHAR_TOKENS, and _METADATA_HOSTS into a new dedicated module: apm_cli/install/mcp_warnings.py. install.py back-binds the symbols at module scope so existing test patches against apm_cli.commands.install._warn_* keep working unchanged. install.py: 1530 -> 1441 LOC (84 under budget, room to breathe). Tests: 4715/4715 unit + console pass (excludes the known pre-existing test_user_scope_skips_workspace_runtimes failure on main). New regression tests: - test_validate_stdio_error_uses_multiline_cargo_style_format - test_repr_redacts_command_to_avoid_leaking_credentials Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Closes #778. Closes #328. Follow-up to #661 / #665.
Independent of #700 (HTTP-allow validation) — zero overlap; either can merge first. See "Independence from #700" below.
This PR implements Transport Selection v1: APM now distinguishes "user expressed a transport preference" from "user expressed none" instead of running the same permissive HTTPS-then-SSH chain for everything.
What changes for users
ssh://host/repo.githttps://host/repo.gitowner/reposhorthand, nogit config insteadOfowner/reposhorthand,git config url.git@host:.insteadOf https://host/git clone owner/repobehavior on that machine.APM_ALLOW_PROTOCOL_FALLBACK=1(or--allow-protocol-fallback). When fallback runs, a[!]warning names both protocols.Migration / non-breaking transition for current users
This is a
### Changed (BREAKING)entry, but the rescue is one env var. The PR was scoped explicitly so a current user with a CI pipeline that depended on the silent-fallback can keep working with a single line:The CHANGELOG entry leads with this, the new docs page leads with this, and the error message that triggers when a strict clone fails names both the URL and the rescue env var.
New CLI surface
On
apm install:--ssh/--https— pick the initial transport for shorthand URLs (mutually exclusive). Explicit-scheme URLs ignore CLI prefs.--allow-protocol-fallback— opt back into the legacy permissive chain.Env vars (apply globally, picked up by both CLI and programmatic callers):
APM_GIT_PROTOCOL=ssh|httpsAPM_ALLOW_PROTOCOL_FALLBACK=1Independence from #700
src/apm_cli/install/validation.py+apm.ymlparser).src/apm_cli/deps/github_downloader.py+ newtransport_selection.py+ CLI flags).git diff --stat main..HEADshows zero edits in feat: support allow-insecure HTTP dependencies #700's territory. Trivial CHANGELOG conflict possible; both can merge in either order.Architecture
New module
src/apm_cli/deps/transport_selection.py(~310 LOC) extracts the decision from the orchestrator:ProtocolPreferenceenum (NONE/SSH/HTTPS).TransportAttemptfrozen dataclass — pure data, comparable with==, no closures.TransportPlanfrozen dataclass —(attempts, strict, fallback_hint).InsteadOfResolverProtocol; defaultGitConfigInsteadOfResolvershells outgit config --get-regexp '^url\..*\.insteadof$'. Lazy load, double-checked locking around the cache.TransportSelector.select(dep_ref, cli_pref, allow_fallback, has_token) -> TransportPlan.GitHubPackageDownloader._clone_with_fallbacknow consumes aTransportPlanand iteratesplan.attemptsinstead of running the hardcoded chain. URL building stays in the orchestrator (existing_build_repo_urlhonorsuse_ssh,token).Critical invariant kept:
GitConfigInsteadOfResolver._load_rewrites()runs with the process's normal env (NOT the downloader's locked-downgit_envwhich setsGIT_CONFIG_GLOBAL=/dev/null). Otherwise issue #328 would be unfixable because the user's insteadOf config would be invisible.Per-attempt clone env (Wave 2 panel finding, fixed in commit
724f8e7)The first cut decided the clone env (locked-down vs relaxed) once per dependency from
has_token. Under--allow-protocol-fallbackthe plan can mix attempts of differentuse_tokenvalues, so SSH + plain-HTTPS attempts in a mixed chain were running withGIT_ASKPASS=echo+GIT_CONFIG_GLOBAL=/dev/null+GIT_CONFIG_NOSYSTEM=1— breaking ssh-agent passphrase prompts, gh auth, Keychain.Fix:
_env_for(attempt.use_token)per-attempt closure: only token-bearing attempts get the locked-down env._build_repo_url(token=None)fell back toself.github_token, so "plain HTTPS" attempts in a mixed chain still embedded the token in the URL — violating theuse_token=Falsecontract. Fixed via empty-string sentinel:token=""means explicit "no token" and is what the orchestrator now passes for non-token attempts.test_allow_fallback_env_is_per_attempt_not_per_depdrives a 3-attempt mixed chain and asserts auth-HTTPS=locked-down, SSH=relaxed, plain-HTTPS=relaxed-and-tokenless.Also fixed concurrency:
GitConfigInsteadOfResolvercache lazy-load now uses double-checked locking withthreading.Lockso parallel downloads can't double-populate.Tests
tests/unit/test_transport_selection.py(NEW)tests/unit/test_auth_scoping.py(regression)tests/integration/test_transport_selection_integration.py(NEW)APM_GIT_PROTOCOL=ssh, allow_fallback rescue). Gated onAPM_RUN_INTEGRATION_TESTS=1; SSH cases auto-skip if no key.scripts/test-integration.shFull unit suite: 4061 passed (was 4029; +32 net new).
APM Review Panel sign-off
Synthesized review across the panel personas (per
.github/skills/apm-review-panel/).python-architect(Wave 0 + 2): VERDICT: ship. All 6 mandatory edits from the Wave 0 verdict applied. Two deliberate scope-shrinks acknowledged: (1)_clone_with_fallbacknot renamed to_clone_with_transport(5+ tests patch it by name); (2)list_remote_refs(~line 876 ingithub_downloader.py) still hardcodes HTTPS — flagged as fast-follow, not in scope.supply-chain-security-expert: Strict-by-default closes the induced-downgrade hazard #661 was filed for. Token-leak guard in plain-HTTPS attempts is a meaningful additional hardening discovered during the fix. Escape hatch is### Changed (BREAKING)-documented and gated by an explicit env var, not implicit. APPROVED.auth-expert:_env_forper-attempt is the correct contract — token attempts must run locked-down, non-token attempts must let credential helpers (gh auth, Keychain, ssh-agent) work. Thetoken=""sentinel is documented in the source. Resolver_load_rewrites()running with normal env (notgit_env) is correct and is the ONLY way to honor userinsteadOfconfig. APPROVED.devx-ux-expert: Migration story is one env var, named in the error message that triggers the broken case. Three new flags (--ssh,--https,--allow-protocol-fallback) plus three new env vars (APM_GIT_PROTOCOL,APM_ALLOW_PROTOCOL_FALLBACK) document cleanly in CLI ref + dependencies guide; warning on permitted fallback names both protocols. Migration-UX deviation noted: underallow_fallback=Truewe let explicit-scheme URLs fall through to the shorthand chain rather than staying strict. Architect's Wave 0 verdict said "explicit_scheme always strict." Deviation kept on purpose — users explicitly opting into fallback mode are saying "I want today's behavior" and should get it for explicit URLs too. APPROVED with deviation acknowledged.cli-logging-expert:[!]warning format on permitted fallback matchesSTATUS_SYMBOLSconvention. Errors on strict-mode failure name the URL and the rescue env var. Per-(url, from-to) dedup avoids warning fatigue. APPROVED.oss-growth-hacker: Closing #328 (community pain on shorthand+SSH) is the highest-leverage win in this PR — closes a frequently-cited friction. Strict default also positions APM as a safer choice vs alternatives. APPROVED.apm-ceo(final call): SHIP. Pre-1.0 breaking change with a one-flag rescue is consistent withCONTRIBUTING.mdphilosophy. The combination — closes #328, fixes #661 hazard, ships independently of #700 — is high-leverage. Wave 5 sign-off granted.Risks & known follow-ups (out of scope for this PR)
list_remote_refs(~line 876 ofgithub_downloader.py) still hardcodes HTTPS. Architect-flagged fast-follow; tracked separately.git config --get-regexprequires git ≥ 1.8.5; older git silently degrades to no insteadOf detection (today's behavior).Refs #778, #328, #661, #665, #700, #777