# GitHub Source End-to-End Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Add end-to-end GitHub source support in `aim-core` for shorthand, repo URLs, release URLs, and direct asset URLs, with source-agnostic metadata parsing, install-origin-first channel selection, and registry-backed fallback channels. **Architecture:** Reshape the current GitHub skeleton from an adapter-centric model into explicit `source`, `metadata`, and `update` boundaries. Keep `aim-cli` thin by moving normalization, metadata interpretation, channel ranking, and recovery behavior into `aim-core`, then extend the registry so future updates can survive upstream changes. **Tech Stack:** Rust, Cargo workspace, serde, toml, reqwest-compatible fetch abstractions, clap, dialoguer, assert_cmd, predicates, fixture-driven tests in `crates/aim-core/tests` and `crates/aim-cli/tests`. --- ### Task 1: Introduce the new core boundary modules and types **Files:** - Create: `crates/aim-core/src/source/mod.rs` - Create: `crates/aim-core/src/source/input.rs` - Create: `crates/aim-core/src/source/github.rs` - Create: `crates/aim-core/src/metadata/mod.rs` - Create: `crates/aim-core/src/metadata/document.rs` - Create: `crates/aim-core/src/update/mod.rs` - Modify: `crates/aim-core/src/lib.rs` - Modify: `crates/aim-core/src/domain/source.rs` - Modify: `crates/aim-core/src/domain/update.rs` - Test: `crates/aim-core/tests/query_resolution.rs` **Step 1: Write the failing test** ```rust use aim_core::source::input::{classify_input, SourceInputKind}; #[test] fn classifies_github_release_asset_url() { let input = classify_input( "https://github.com/pingdotgg/t3code/releases/download/v0.0.11/T3-Code-0.0.11-x86_64.AppImage", ) .unwrap(); assert_eq!(input.kind, SourceInputKind::GitHubReleaseAssetUrl); } ``` **Step 2: Run test to verify it fails** Run: `cargo test classifies_github_release_asset_url --package aim-core --test query_resolution` Expected: FAIL because the `source` module and new input classification types do not exist yet **Step 3: Write minimal implementation** Add the new top-level modules and export them from `aim_core`. Introduce the minimum source and update domain types needed to classify GitHub inputs without rewriting existing workflows yet. **Step 4: Run test to verify it passes** Run: `cargo test classifies_github_release_asset_url --package aim-core --test query_resolution` Expected: PASS **Step 5: Commit** ```bash git add crates/aim-core/src/lib.rs crates/aim-core/src/source crates/aim-core/src/metadata crates/aim-core/src/update crates/aim-core/src/domain/source.rs crates/aim-core/src/domain/update.rs crates/aim-core/tests/query_resolution.rs git commit -m "feat: add source metadata and update module boundaries" ``` ### Task 2: Implement GitHub input normalization across all supported entry forms **Files:** - Modify: `crates/aim-core/src/source/input.rs` - Modify: `crates/aim-core/src/source/github.rs` - Modify: `crates/aim-core/src/app/query.rs` - Modify: `crates/aim-core/src/app/identity.rs` - Test: `crates/aim-core/tests/query_resolution.rs` - Test: `crates/aim-core/tests/identity_resolution.rs` **Step 1: Write the failing test** ```rust use aim_core::app::query::resolve_query; use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind}; #[test] fn resolves_owner_repo_to_github_repo_source() { let source = resolve_query("sharkdp/bat").unwrap(); assert_eq!(source.input_kind, SourceInputKind::RepoShorthand); assert_eq!(source.normalized_kind, NormalizedSourceKind::GitHubRepository); } ``` **Step 2: Run test to verify it fails** Run: `cargo test resolves_owner_repo_to_github_repo_source --package aim-core --test query_resolution` Expected: FAIL because normalized GitHub source kinds are not represented yet **Step 3: Write minimal implementation** Teach query resolution and identity normalization to recognize: - `owner/repo` - GitHub repo URLs - GitHub release URLs - direct GitHub release asset URLs Preserve the original input while returning a normalized source reference that can later drive discovery. **Step 4: Run test to verify it passes** Run: `cargo test resolves_owner_repo_to_github_repo_source --package aim-core --test query_resolution` Expected: PASS **Step 5: Commit** ```bash git add crates/aim-core/src/source/input.rs crates/aim-core/src/source/github.rs crates/aim-core/src/app/query.rs crates/aim-core/src/app/identity.rs crates/aim-core/tests/query_resolution.rs crates/aim-core/tests/identity_resolution.rs git commit -m "feat: normalize github input forms" ``` ### Task 3: Add GitHub discovery records for releases, assets, and linked metadata **Files:** - Modify: `crates/aim-core/src/source/github.rs` - Modify: `crates/aim-core/src/domain/source.rs` - Create: `crates/aim-core/tests/github_source_discovery.rs` - Modify: `crates/aim-core/src/adapters/test_support.rs` **Step 1: Write the failing test** ```rust use aim_core::source::github::discover_github_candidates; #[test] fn discovery_reports_appimage_assets_and_latest_linux_yml() { let discovery = discover_github_candidates(/* mocked github response */).unwrap(); assert!(discovery .assets .iter() .any(|asset| asset.name.ends_with(".AppImage"))); assert!(discovery .metadata_documents .iter() .any(|doc| doc.url.ends_with("latest-linux.yml"))); } ``` **Step 2: Run test to verify it fails** Run: `cargo test discovery_reports_appimage_assets_and_latest_linux_yml --package aim-core --test github_source_discovery` Expected: FAIL because source discovery does not yet return structured assets and metadata document records **Step 3: Write minimal implementation** Add GitHub discovery result types that expose: - releases - AppImage assets - discovered metadata document URLs - enough provenance to support later prompt and ranking logic Use existing test-support scaffolding rather than real network calls. **Step 4: Run test to verify it passes** Run: `cargo test discovery_reports_appimage_assets_and_latest_linux_yml --package aim-core --test github_source_discovery` Expected: PASS **Step 5: Commit** ```bash git add crates/aim-core/src/source/github.rs crates/aim-core/src/domain/source.rs crates/aim-core/src/adapters/test_support.rs crates/aim-core/tests/github_source_discovery.rs git commit -m "feat: add github source discovery records" ``` ### Task 4: Add source-agnostic metadata document and parser contracts **Files:** - Modify: `crates/aim-core/src/metadata/mod.rs` - Modify: `crates/aim-core/src/metadata/document.rs` - Create: `crates/aim-core/src/metadata/parser.rs` - Modify: `crates/aim-core/src/domain/update.rs` - Create: `crates/aim-core/tests/metadata_contract.rs` **Step 1: Write the failing test** ```rust use aim_core::metadata::{parse_document, MetadataDocument, ParsedMetadataKind}; #[test] fn unknown_document_returns_typed_warning_not_panic() { let doc = MetadataDocument::plain_text("https://example.test/notes.txt", b"not metadata"); let result = parse_document(&doc).unwrap(); assert_eq!(result.kind, ParsedMetadataKind::Unknown); assert!(!result.warnings.is_empty()); } ``` **Step 2: Run test to verify it fails** Run: `cargo test unknown_document_returns_typed_warning_not_panic --package aim-core --test metadata_contract` Expected: FAIL because the metadata parsing contract does not exist yet **Step 3: Write minimal implementation** Define: - metadata document input type - metadata parse result type - source-agnostic parser entry point - typed warnings for unsupported or malformed documents Keep the implementation minimal and independent from GitHub-specific code. **Step 4: Run test to verify it passes** Run: `cargo test unknown_document_returns_typed_warning_not_panic --package aim-core --test metadata_contract` Expected: PASS **Step 5: Commit** ```bash git add crates/aim-core/src/metadata crates/aim-core/src/domain/update.rs crates/aim-core/tests/metadata_contract.rs git commit -m "feat: add metadata parser contract" ``` ### Task 5: Implement `electron-builder` Linux metadata parsing **Files:** - Create: `crates/aim-core/src/metadata/electron_builder.rs` - Modify: `crates/aim-core/src/metadata/mod.rs` - Test: `crates/aim-core/tests/metadata_electron_builder.rs` - Create: `crates/aim-core/tests/fixtures/latest-linux.yml` **Step 1: Write the failing test** ```rust use aim_core::metadata::{parse_document, MetadataDocument, ParsedMetadataKind}; #[test] fn parses_latest_linux_yml_into_download_hints() { let raw = include_bytes!("fixtures/latest-linux.yml"); let doc = MetadataDocument::yaml("https://example.test/latest-linux.yml", raw); let result = parse_document(&doc).unwrap(); assert_eq!(result.kind, ParsedMetadataKind::ElectronBuilder); assert_eq!(result.hints.primary_download.as_deref(), Some("T3-Code-0.0.11-x86_64.AppImage")); } ``` **Step 2: Run test to verify it fails** Run: `cargo test parses_latest_linux_yml_into_download_hints --package aim-core --test metadata_electron_builder` Expected: FAIL because `electron-builder` metadata is not parsed yet **Step 3: Write minimal implementation** Add an `electron_builder` parser that extracts: - version - primary download artifact - checksum or digest when present - architecture hints when available - parser confidence and warnings **Step 4: Run test to verify it passes** Run: `cargo test parses_latest_linux_yml_into_download_hints --package aim-core --test metadata_electron_builder` Expected: PASS **Step 5: Commit** ```bash git add crates/aim-core/src/metadata/electron_builder.rs crates/aim-core/src/metadata/mod.rs crates/aim-core/tests/metadata_electron_builder.rs crates/aim-core/tests/fixtures/latest-linux.yml git commit -m "feat: parse electron builder linux metadata" ``` ### Task 6: Implement zsync metadata parsing and channel hints **Files:** - Create: `crates/aim-core/src/metadata/zsync.rs` - Modify: `crates/aim-core/src/metadata/mod.rs` - Test: `crates/aim-core/tests/metadata_zsync.rs` - Create: `crates/aim-core/tests/fixtures/example.zsync` **Step 1: Write the failing test** ```rust use aim_core::metadata::{parse_document, MetadataDocument, ParsedMetadataKind}; #[test] fn parses_zsync_document_into_channel_hints() { let raw = include_bytes!("fixtures/example.zsync"); let doc = MetadataDocument::plain_text("https://example.test/app.AppImage.zsync", raw); let result = parse_document(&doc).unwrap(); assert_eq!(result.kind, ParsedMetadataKind::Zsync); assert!(result.hints.primary_download.is_some()); } ``` **Step 2: Run test to verify it fails** Run: `cargo test parses_zsync_document_into_channel_hints --package aim-core --test metadata_zsync` Expected: FAIL because zsync parsing does not exist yet **Step 3: Write minimal implementation** Add a zsync parser that extracts download URL, filename, version-like hints where possible, and channel confidence without coupling it to one upstream source. **Step 4: Run test to verify it passes** Run: `cargo test parses_zsync_document_into_channel_hints --package aim-core --test metadata_zsync` Expected: PASS **Step 5: Commit** ```bash git add crates/aim-core/src/metadata/zsync.rs crates/aim-core/src/metadata/mod.rs crates/aim-core/tests/metadata_zsync.rs crates/aim-core/tests/fixtures/example.zsync git commit -m "feat: parse zsync metadata documents" ``` ### Task 7: Add update-channel ranking and artifact scoring **Files:** - Modify: `crates/aim-core/src/update/mod.rs` - Create: `crates/aim-core/src/update/channels.rs` - Create: `crates/aim-core/src/update/ranking.rs` - Modify: `crates/aim-core/src/app/update.rs` - Modify: `crates/aim-core/src/domain/update.rs` - Modify: `crates/aim-core/tests/update_planning.rs` **Step 1: Write the failing test** ```rust use aim_core::update::ranking::rank_channels; #[test] fn install_origin_match_beats_higher_level_fallback() { let ranked = rank_channels(/* preferred direct asset lineage */, /* github releases */, /* electron-builder */); assert_eq!(ranked[0].reason.as_str(), "install-origin-match"); } ``` **Step 2: Run test to verify it fails** Run: `cargo test install_origin_match_beats_higher_level_fallback --package aim-core --test update_planning` Expected: FAIL because channels and ranking reasons are not modeled yet **Step 3: Write minimal implementation** Implement channel and artifact ranking rules for: - install-origin-first preference - stable-over-prerelease by default - metadata-guided artifact selection ahead of filename heuristics - ordered alternates retained for fallback **Step 4: Run test to verify it passes** Run: `cargo test install_origin_match_beats_higher_level_fallback --package aim-core --test update_planning` Expected: PASS **Step 5: Commit** ```bash git add crates/aim-core/src/update crates/aim-core/src/app/update.rs crates/aim-core/src/domain/update.rs crates/aim-core/tests/update_planning.rs git commit -m "feat: add update channel ranking" ``` ### Task 8: Extend the registry model for source input, strategy, and fallback channels **Files:** - Modify: `crates/aim-core/src/registry/model.rs` - Modify: `crates/aim-core/src/registry/store.rs` - Modify: `crates/aim-core/src/domain/app.rs` - Modify: `crates/aim-core/tests/registry_roundtrip.rs` **Step 1: Write the failing test** ```rust use aim_core::registry::store::RegistryStore; #[test] fn registry_round_trips_update_strategy_and_alternates() { let store = RegistryStore::new(/* temp path */); let original = sample_record_with_strategy(); store.save(&[original.clone()]).unwrap(); let loaded = store.load().unwrap(); assert_eq!(loaded[0].update_strategy.preferred.reason, "install-origin-match"); assert_eq!(loaded[0].update_strategy.alternates.len(), 2); } ``` **Step 2: Run test to verify it fails** Run: `cargo test registry_round_trips_update_strategy_and_alternates --package aim-core --test registry_roundtrip` Expected: FAIL because the registry cannot persist the new strategy fields yet **Step 3: Write minimal implementation** Extend the registry model to persist: - original source input - normalized source reference - preferred channel - ordered alternates - selected metadata hints or snapshot references Keep loading backward-compatible for existing records that lack these fields. **Step 4: Run test to verify it passes** Run: `cargo test registry_round_trips_update_strategy_and_alternates --package aim-core --test registry_roundtrip` Expected: PASS **Step 5: Commit** ```bash git add crates/aim-core/src/registry/model.rs crates/aim-core/src/registry/store.rs crates/aim-core/src/domain/app.rs crates/aim-core/tests/registry_roundtrip.rs git commit -m "feat: persist update strategy and fallback channels" ``` ### Task 9: Wire the add flow through source discovery, metadata parsing, and channel selection **Files:** - Modify: `crates/aim-core/src/app/add.rs` - Modify: `crates/aim-core/src/app/identity.rs` - Modify: `crates/aim-core/src/source/github.rs` - Modify: `crates/aim-core/tests/github_add_flow.rs` **Step 1: Write the failing test** ```rust use aim_core::app::add::build_add_plan; #[test] fn add_plan_prefers_metadata_guided_appimage_when_available() { let plan = build_add_plan(/* github shorthand with latest-linux.yml */).unwrap(); assert_eq!(plan.selected_artifact.selection_reason, "metadata-guided"); } ``` **Step 2: Run test to verify it fails** Run: `cargo test add_plan_prefers_metadata_guided_appimage_when_available --package aim-core --test github_add_flow` Expected: FAIL because add planning does not yet route through metadata-aware ranking **Step 3: Write minimal implementation** Update add planning to: - resolve normalized GitHub source input - perform discovery - parse any fetched metadata documents - rank channels and artifacts - emit a plan that records why the winning artifact was chosen **Step 4: Run test to verify it passes** Run: `cargo test add_plan_prefers_metadata_guided_appimage_when_available --package aim-core --test github_add_flow` Expected: PASS **Step 5: Commit** ```bash git add crates/aim-core/src/app/add.rs crates/aim-core/src/app/identity.rs crates/aim-core/src/source/github.rs crates/aim-core/tests/github_add_flow.rs git commit -m "feat: wire add flow through source and metadata pipeline" ``` ### Task 10: Add prompt context for older releases and ambiguous artifacts **Files:** - Modify: `crates/aim-core/src/app/interaction.rs` - Modify: `crates/aim-core/src/app/add.rs` - Modify: `crates/aim-cli/src/ui/prompt.rs` - Modify: `crates/aim-cli/tests/ui_summary.rs` - Modify: `crates/aim-cli/tests/end_to_end_cli.rs` **Step 1: Write the failing test** ```rust use aim_core::app::add::build_add_plan; #[test] fn direct_old_release_url_requests_tracking_choice_prompt() { let plan = build_add_plan(/* direct old github asset url with newer releases available */).unwrap(); assert!(plan .interactions .iter() .any(|item| item.key == "tracking-preference")); } ``` **Step 2: Run test to verify it fails** Run: `cargo test direct_old_release_url_requests_tracking_choice_prompt --package aim-core --test github_add_flow` Expected: FAIL because the flow does not surface the new prompt context yet **Step 3: Write minimal implementation** Add typed prompt requests for: - older explicit release versus latest-supported tracking - ambiguous artifact ties after metadata and heuristics Keep prompt rendering in `aim-cli`, but define the request shape in `aim-core`. **Step 4: Run test to verify it passes** Run: `cargo test direct_old_release_url_requests_tracking_choice_prompt --package aim-core --test github_add_flow` Expected: PASS **Step 5: Commit** ```bash git add crates/aim-core/src/app/interaction.rs crates/aim-core/src/app/add.rs crates/aim-cli/src/ui/prompt.rs crates/aim-cli/tests/ui_summary.rs crates/aim-cli/tests/end_to_end_cli.rs git commit -m "feat: add prompt support for github tracking choices" ``` ### Task 11: Teach update planning to fall back when the preferred channel fails **Files:** - Modify: `crates/aim-core/src/app/update.rs` - Modify: `crates/aim-core/src/update/ranking.rs` - Modify: `crates/aim-core/tests/update_planning.rs` - Modify: `crates/aim-cli/tests/end_to_end_cli.rs` **Step 1: Write the failing test** ```rust use aim_core::app::update::build_update_plan; #[test] fn update_plan_uses_alternate_channel_after_preferred_failure() { let plan = build_update_plan(/* registry entry with failing preferred channel */).unwrap(); assert_eq!(plan.updates[0].selected_channel.kind.as_str(), "electron-builder"); assert_eq!(plan.updates[0].selection_reason.as_str(), "preferred-channel-failed"); } ``` **Step 2: Run test to verify it fails** Run: `cargo test update_plan_uses_alternate_channel_after_preferred_failure --package aim-core --test update_planning` Expected: FAIL because update planning does not yet retry alternates **Step 3: Write minimal implementation** Teach update planning to: - evaluate the preferred channel first - fall back through ordered alternates when the preferred path is stale, broken, or incompatible - preserve an explanation for the fallback decision **Step 4: Run test to verify it passes** Run: `cargo test update_plan_uses_alternate_channel_after_preferred_failure --package aim-core --test update_planning` Expected: PASS **Step 5: Commit** ```bash git add crates/aim-core/src/app/update.rs crates/aim-core/src/update/ranking.rs crates/aim-core/tests/update_planning.rs crates/aim-cli/tests/end_to_end_cli.rs git commit -m "feat: add update fallback channel behavior" ``` ### Task 12: Remove or slim the legacy GitHub adapter entry points **Files:** - Modify: `crates/aim-core/src/adapters/mod.rs` - Modify: `crates/aim-core/src/adapters/github.rs` - Modify: `crates/aim-core/src/adapters/traits.rs` - Modify: `crates/aim-core/tests/adapter_contract.rs` - Modify: `crates/aim-core/tests/adapter_smoke.rs` **Step 1: Write the failing test** ```rust use aim_core::adapters::github::GitHubAdapter; #[test] fn legacy_github_adapter_delegates_to_source_pipeline() { let adapter = GitHubAdapter::default(); let result = adapter.normalize("sharkdp/bat").unwrap(); assert_eq!(result.normalized_kind.as_str(), "github-repository"); } ``` **Step 2: Run test to verify it fails** Run: `cargo test legacy_github_adapter_delegates_to_source_pipeline --package aim-core --test adapter_contract` Expected: FAIL because the legacy adapter layer has not been reconciled with the new boundaries **Step 3: Write minimal implementation** Either: - slim the GitHub adapter into a compatibility wrapper over `source::github`, or - reduce the adapter layer so it no longer owns metadata or ranking responsibilities Do not leave duplicated GitHub logic in both places. **Step 4: Run test to verify it passes** Run: `cargo test legacy_github_adapter_delegates_to_source_pipeline --package aim-core --test adapter_contract` Expected: PASS **Step 5: Commit** ```bash git add crates/aim-core/src/adapters/mod.rs crates/aim-core/src/adapters/github.rs crates/aim-core/src/adapters/traits.rs crates/aim-core/tests/adapter_contract.rs crates/aim-core/tests/adapter_smoke.rs git commit -m "refactor: reconcile legacy adapter layer with source pipeline" ``` ### Task 13: Run full verification and update top-level docs if needed **Files:** - Modify: `README.md` - Modify: `.plans/001-github-source-end-to-end/2026-03-19-github-source-end-to-end-design.md` - Modify: `.plans/001-github-source-end-to-end/2026-03-19-github-source-end-to-end-implementation-plan.md` **Step 1: Run focused test suites** Run: `cargo test --package aim-core --test query_resolution --test identity_resolution --test github_source_discovery --test metadata_contract --test metadata_electron_builder --test metadata_zsync --test github_add_flow --test update_planning --test registry_roundtrip` Expected: PASS **Step 2: Run full workspace verification** Run: `cargo test --workspace` Expected: PASS Run: `cargo fmt --check` Expected: PASS Run: `cargo clippy --workspace --all-targets --all-features -- -D warnings` Expected: PASS **Step 3: Update docs minimally** Document any visible changes to supported GitHub input forms or update behavior in `README.md`. Only update the design or plan docs if implementation forced a justified divergence. **Step 4: Re-run doc-relevant tests if docs changed code examples** Run: `cargo test --workspace` Expected: PASS **Step 5: Commit** ```bash git add README.md .plans/001-github-source-end-to-end/2026-03-19-github-source-end-to-end-design.md .plans/001-github-source-end-to-end/2026-03-19-github-source-end-to-end-implementation-plan.md git commit -m "docs: finalize github source end-to-end support" ```