{"record":{"id":"960485760c17f445","repo":"Hmbown/CodeWhale","slug":"github-source-must-be-github-owner-repo-got-sp","errorCode":null,"errorMessage":"github source must be 'github:owner/repo' (got {spec})","messagePattern":"github source must be 'github:owner/repo' \\(got (.+?)\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/skills/install.rs","lineNumber":130,"sourceCode":"    ///   [`InstallSource::GitHubRepo`]\n    /// * any other `http://` or `https://` prefix → [`InstallSource::DirectUrl`]\n    /// * anything else → [`InstallSource::Registry`]\n    pub fn parse(spec: &str) -> Result<Self> {\n        let trimmed = spec.trim();\n        if trimmed.is_empty() {\n            bail!(\"install source must not be empty\");\n        }\n        if let Some(rest) = trimmed.strip_prefix(\"github:\") {\n            let rest = rest.trim();\n            // Reject obviously bogus values up front. We intentionally accept\n            // case-insensitive owner/repo so `github:Hmbown/Foo` works.\n            let (owner, repo) = rest.split_once('/').with_context(|| {\n                format!(\"github source must be 'github:owner/repo' (got {spec})\")\n            })?;\n            let owner = owner.trim();\n            let repo = repo.trim().trim_end_matches('/');\n            if owner.is_empty() || repo.is_empty() {\n                bail!(\"github source must be 'github:owner/repo' (got {spec})\");\n            }\n            if owner.contains('/') || repo.contains('/') {\n                bail!(\"github source must be 'github:owner/repo' (got {spec})\");\n            }\n            return Ok(Self::GitHubRepo(format!(\"{owner}/{repo}\")));\n        }\n        if trimmed.starts_with(\"https://\") || trimmed.starts_with(\"http://\") {\n            if let Some(repo) = parse_github_browser_url(trimmed) {\n                return Ok(Self::GitHubRepo(repo));\n            }\n            return Ok(Self::DirectUrl(trimmed.to_string()));\n        }\n        Ok(Self::Registry(trimmed.to_string()))\n    }\n}\n\n/// Detect bare `https://github.com/<owner>/<repo>` URLs (with or without a\n/// trailing `.git`) and return `owner/repo`. Returns `None` for any URL that","sourceCodeStart":112,"sourceCodeEnd":148,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/skills/install.rs#L112-L148","documentation":"InstallSource::parse splits a 'github:'-prefixed spec on its first '/'; when there is no slash at all, split_once returns None and this context error attaches. 'github:foo' (owner only, no repo) is the canonical trigger.","triggerScenarios":"'/skill install github:zai' where the repo part is missing, or 'github:' followed by a whitespace-only rest after trimming.","commonSituations":"Truncated copy-paste of a repo slug, specs built by string concatenation where the repo variable was empty, and forgetting the shorthand needs both owner and repo even when they look identical.","solutions":["Complete the spec: 'github:owner/repo'.","When generating specs in code, format with 'github:{owner}/{repo}' and reject empty components first.","For URLs with subpaths (monorepo subdirs), use the full https:// form; the github: shorthand only takes owner/repo."],"exampleFix":"# before\n/skill install github:zai\n\n# after\n/skill install github:zai/codewhale","handlingStrategy":"validation","validationCode":"fn valid_github_spec(spec: &str) -> bool {\n    let Some(rest) = spec.trim().strip_prefix(\"github:\") else { return true };\n    matches!(rest.split_once('/'), Some((o, r)) if !o.trim().is_empty() && !r.trim().is_empty())\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Build github: specs with a format! template that rejects empty components.","Prefer pasting full https://github.com/owner/repo URLs; the parser converts them.","Remember the shorthand takes exactly owner/repo, no subpaths."],"tags":["skills","install","github","spec-parsing","rust"],"backgroundTag":"malformed-github-repo-spec","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}