From a9934a605c76f15e5bd685cf8becac17038068bd Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Mon, 7 Sep 2026 14:19:56 +1000 Subject: [PATCH 1/2] fix(cli): name the GitHub rate limit on a direct run An exhausted anonymous GitHub quota surfaced as "SERVICE_UNAVAILABLE: the remote service returned HTTP 403". That blames skilld.dev for a GitHub limit and gives the user nothing to act on. A rate limited GitHub response is now RATE_LIMITED and names the wait from x-ratelimit-reset or retry-after, matching the update comparison path that already recognised it. Claude-Session: https://claude.ai/code/session_01ASGK6drkuCbo7vsPQcYCvn --- crates/skilld-command/src/remote.rs | 18 +++++++++++++++- crates/skilld-command/tests/remote.rs | 30 +++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/crates/skilld-command/src/remote.rs b/crates/skilld-command/src/remote.rs index a9c71785..3a7e26a7 100644 --- a/crates/skilld-command/src/remote.rs +++ b/crates/skilld-command/src/remote.rs @@ -852,7 +852,10 @@ impl SkilldRemote { continue; } if !(200..300).contains(&response.status) { - return Err(problem_error(&response)); + return Err(match allowed { + AllowedOrigin::Github => github_error(&response), + AllowedOrigin::Service(_) => problem_error(&response), + }); } return Ok(response); } @@ -2486,6 +2489,19 @@ fn problem_error(response: &HttpResponse) -> RemoteError { ) } +fn github_error(response: &HttpResponse) -> RemoteError { + if !is_rate_limited(response) { + return service_unavailable_error(response); + } + let detail = match bounded_rate_limit_wait(response) { + Some(seconds) => { + format!("GitHub rate limited this request. Retry after {seconds} seconds.") + } + None => "GitHub rate limited this request. Retry in a minute.".to_owned(), + }; + RemoteError::new("RATE_LIMITED", detail) +} + fn service_unavailable_error(response: &HttpResponse) -> RemoteError { match (response.status, retry_after_seconds(response)) { (429, Some(seconds)) => RemoteError::new( diff --git a/crates/skilld-command/tests/remote.rs b/crates/skilld-command/tests/remote.rs index 6df761e1..3f5a9af1 100644 --- a/crates/skilld-command/tests/remote.rs +++ b/crates/skilld-command/tests/remote.rs @@ -1846,6 +1846,36 @@ fn direct_github_access_resolves_an_exact_public_commit_without_tokens() { })); } +#[test] +fn a_direct_github_rate_limit_names_github_and_the_reset_wait() { + let reset = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + + 300; + let mut limited = response(403, br#"{"message":"API rate limit exceeded"}"#.to_vec()); + limited + .headers + .insert("x-ratelimit-remaining".to_owned(), "0".to_owned()); + limited + .headers + .insert("x-ratelimit-reset".to_owned(), reset.to_string()); + let http = Arc::new(FakeHttp::with([limited])); + let remote = SkilldRemote::new( + http.clone(), + Arc::new(NoTokenProvider), + NativeRemoteConfig::Unconfigured, + ) + .with_sleeper(Arc::new(NoSleep)); + let selector = RemoteSelector::parse("github:skilld-dev/skills/skills/example").unwrap(); + + let error = remote.prepare(&selector, true).unwrap_err(); + + assert_eq!(error.code, "RATE_LIMITED"); + assert!(error.message.starts_with("GitHub rate limited this request. Retry after ")); + assert_eq!(http.requests.lock().unwrap().len(), 1); +} + #[test] fn prepare_exact_rejects_a_conflicting_selector_commit_before_http() { let selector_commit = "0123456789abcdef0123456789abcdef01234567"; From b9d2fd7c6f471849cb37ccdc849fc753b814f289 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Mon, 7 Sep 2026 14:22:09 +1000 Subject: [PATCH 2/2] style(cli): apply rustfmt to the rate limit test Claude-Session: https://claude.ai/code/session_01ASGK6drkuCbo7vsPQcYCvn --- crates/skilld-command/tests/remote.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/skilld-command/tests/remote.rs b/crates/skilld-command/tests/remote.rs index 3f5a9af1..19019bd0 100644 --- a/crates/skilld-command/tests/remote.rs +++ b/crates/skilld-command/tests/remote.rs @@ -1872,7 +1872,11 @@ fn a_direct_github_rate_limit_names_github_and_the_reset_wait() { let error = remote.prepare(&selector, true).unwrap_err(); assert_eq!(error.code, "RATE_LIMITED"); - assert!(error.message.starts_with("GitHub rate limited this request. Retry after ")); + assert!( + error + .message + .starts_with("GitHub rate limited this request. Retry after ") + ); assert_eq!(http.requests.lock().unwrap().len(), 1); }