Skip to content

feat: add GitHub Skill Sync repository preview - #3225

Draft
Patrick-Erichsen wants to merge 2 commits into
mainfrom
pe/claw-581-bulk-adoption-groundwork
Draft

feat: add GitHub Skill Sync repository preview#3225
Patrick-Erichsen wants to merge 2 commits into
mainfrom
pe/claw-581-bulk-adoption-groundwork

Conversation

@Patrick-Erichsen

@Patrick-Erichsen Patrick-Erichsen commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add first-class GitHub Skill Sync settings for a verified publisher to select one of its public repositories
  • authorize personal and organization publishers through immutable GitHub identities and fresh organization-admin membership
  • resolve canonical repository redirects, discover skills directly from repository archives, and preview new destinations, Hosted Skill replacements, unavailable entries, and ownership conflicts
  • show one repository-level replacement warning and retain the existing source-removal flow
  • ignore stale preview responses when the selected repository or publisher changes

Dependency boundary

Activation remains disabled until the canonical GitHub Skill Sync engine exposes the repository enrollment contract. This PR does not enumerate skills from skills.sh, create another mirror, run scans, change shared schema, alter ranking, or mutate production.

Tests

  • bunx vitest run src/routes/-settings.test.tsx convex/githubSkillSyncSettings.test.ts convex/lib/githubSkillSyncSettings.test.ts (39 passed)
  • bun run ci:unit (5,096 passed, 1 skipped)
  • bun run ci:static
  • bun run ci:types-build
  • bunx convex dev --once --typecheck=disable against local local-amantus-clawdhub-2080
  • real local Convex action calls proved both public endpoints reject unauthenticated requests before GitHub access
  • final autoreview: no accepted/actionable findings

@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clawhub Ready Ready Preview, Comment Jul 23, 2026 9:33pm

Request Review

@Patrick-Erichsen
Patrick-Erichsen force-pushed the pe/claw-581-bulk-adoption-groundwork branch from 381d679 to b1a8dfc Compare July 23, 2026 21:30
@Patrick-Erichsen Patrick-Erichsen changed the title feat: add publisher bulk adoption groundwork feat: add GitHub Skill Sync repository preview Jul 23, 2026
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. labels Jul 24, 2026
@clawsweeper

clawsweeper Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Codex review: found issues before merge. Reviewed August 4, 2026, 10:09 PM ET / August 5, 2026, 02:09 UTC.

ClawSweeper review

What this changes

This draft adds a publisher-settings flow that lists public GitHub repositories and previews discovered skill destinations, replacements, conflicts, and unavailable entries before GitHub Skill Sync enrollment.

Merge readiness

Blocked until real behavior proof is added - 11 items remain

Keep open for maintainer review: the draft removes the shipped GitHub-source enrollment path while its own activation remains disabled, and it is now incompatible with substantial current-main changes to the sync contract. A maintainer must also decide whether preview access intentionally expands from Official to all verified publishers.

Priority: P2
Reviewed head: b1a8dfcbf37b2f06384277a81448fb68ccbe63d0
Owner decision: Required. See Decision needed.

Review scores

Measure Result What it means
Overall readiness 🧂 unranked krab (1/6) The draft has meaningful implementation work, but two P1 compatibility/current-main blockers and missing real UI proof make it unready to merge.
Proof confidence 🧂 unranked krab (1/6) Needs real behavior proof before merge: The PR body lists tests and unauthenticated action calls, but it provides no inspectable after-fix run of the new settings preview in a real browser. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Patch quality 🧂 unranked krab (1/6) Security review found an item that needs attention.

Verification

Check Result Evidence
Real behavior Needs proof Needs real behavior proof before merge: The PR body lists tests and unauthenticated action calls, but it provides no inspectable after-fix run of the new settings preview in a real browser. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed 5 items Shipped enrollment path: Current settings submit the selected repository to the enrollment action and render the existing GitHub source form; the PR replaces that action while stating activation is disabled.
Current authorization and identity contract: Current main centralizes publisher-admin, immutable owner-ID, fresh organization-admin, repository-ID conflict, and cross-publisher checks in the enrollment setup query.
Official-publisher policy: The checked-in policy says only manageable Official publishers may configure source-backed GitHub Skill Sync, while this draft describes verified publishers and removes the UI’s Official filter.
Findings 2 actionable findings [P1] Preserve source enrollment until its replacement exists
[P1] Rebase the snapshot preview onto the current contract
Security Needs attention Avoid repository-scoped installation credentials for public previews: The review discussion identifies a settings helper that leaves GitHub App use at the shared helper’s default. Current main explicitly disables installation tokens for arbitrary public repositories because the app may not be installed there; preserve that policy after rebase.

How this fits together

Publisher settings use verified GitHub identities to enroll public repositories as source-backed skills. The proposed preview reads GitHub repository metadata and archives, then classifies each discovered skill against existing ClawHub destinations before the user proceeds to source enrollment.

flowchart LR
  A[Publisher settings] --> B[Verified GitHub identity]
  B --> C[Public repository list]
  C --> D[Repository metadata and archive]
  D --> E[Skill discovery]
  E --> F[Ownership and destination checks]
  F --> G[Preview in settings]
  G --> H[Source enrollment]
Loading

Decision needed

Question Recommendation
Should GitHub Skill Sync preview and eventual enrollment remain limited to manageable Official publishers, or should the product intentionally expand this capability to all verified publishers? Keep the Official boundary: Retain the current eligibility rule and limit the preview to manageable Official publishers while completing the rebase.

Why: The checked-in policy and shipped UI retain an Official boundary, while the draft changes that audience as part of a preview feature without a documented policy update.

Before merge

  • Preserve source enrollment until its replacement exists (P1) - This replaces configurePublicGitHubSkillSource with preview-only actions, but the PR states activation remains disabled. The shipped settings flow still provides the only in-product enrollment path, so existing Official publishers would lose it. Keep the current control or wire the actual enrollment handoff in this change.
  • Rebase the snapshot preview onto the current contract (P1) - Current main substantially changed this module: snapshot discovery now requires an action context with runAction and is not exported as this older hunk expects. The preview handler was designed with only runQuery, and GitHub already reports the branch dirty. Rebase and use a shared current-main wrapper before relying on this export.
  • Resolve security concern: Avoid repository-scoped installation credentials for public previews - The review discussion identifies a settings helper that leaves GitHub App use at the shared helper’s default. Current main explicitly disables installation tokens for arbitrary public repositories because the app may not be installed there; preserve that policy after rebase.
  • Resolve merge risk (P1) - Existing Official publishers would lose the in-product source-enrollment control even though this draft deliberately leaves enrollment inactive.
  • Resolve merge risk (P1) - The branch is dirty against current main, whose snapshot helper now requires an action context and has changed substantially since the PR base.
  • Resolve merge risk (P1) - The proposed settings-specific GitHub requests need to retain the current public-source header policy that explicitly disables repository-scoped GitHub App installation tokens for arbitrary public repositories.
  • Complete next step (P2) - A collaborator-authored feature needs maintainer direction on the eligibility boundary before any narrow repair or rebase can be safely routed.
  • Improve patch quality - Rebase on current main and retain the existing enrollment path until the preview has a complete handoff.
  • Improve patch quality - Resolve the Official-versus-verified publisher decision in the spec and test matrix.
  • Improve patch quality - Add redacted real-browser proof of repository selection, preview results, and an unavailable or conflict state; tests alone do not establish this UI behavior.

Findings

  • [P1] Preserve source enrollment until its replacement exists — src/routes/settings.tsx:317
  • [P1] Rebase the snapshot preview onto the current contract — convex/githubSkillSync.ts:1282
  • [medium] Avoid repository-scoped installation credentials for public previews
Agent review details

Security

Needs attention: The settings-specific GitHub fetch path must be reconciled with current main’s explicit public-repository authentication policy before merge.

Review metrics

Metric Value Why it matters
Patch scope +1,775/-142 across 9 files The draft replaces a production settings workflow and adds a new Convex action surface, so rebase and upgrade review need to cover both UI and backend contracts.
Production versus tests production +1,145, tests +630 The substantial production surface needs an integrated real settings-flow proof in addition to its unit coverage.

Merge-risk options

Maintainer options:

  1. Rebase around the current enrollment contract (recommended)
    Preserve the shipped enrollment control, reuse current identity and header helpers, and update the preview to the current snapshot API before another merge review.
  2. Pause pending eligibility direction
    Pause this draft if maintainers do not want to decide whether non-Official verified publishers should receive this source-management surface.

Technical review

Best possible solution:

Rebase onto current main, build preview on the shared enrollment authorization and public-GitHub fetch policy, retain the existing enrollment control until a complete enrollment handoff is ready, and document any approved eligibility change.

Do we have a high-confidence way to reproduce the issue?

Yes, source-reproducible: current main shows the settings form calling the shipped enrollment action, while this draft’s supplied diff replaces that control despite its stated disabled activation boundary.

Is this the best way to solve the issue?

No: preview can be a useful addition, but removing enrollment before the replacement enrollment contract exists is not the narrowest maintainable path; preserve the current form and reuse its shared authorization and fetch helpers.

Full review comments:

  • [P1] Preserve source enrollment until its replacement exists — src/routes/settings.tsx:317
    This replaces configurePublicGitHubSkillSource with preview-only actions, but the PR states activation remains disabled. The shipped settings flow still provides the only in-product enrollment path, so existing Official publishers would lose it. Keep the current control or wire the actual enrollment handoff in this change.
    Confidence: 0.99
  • [P1] Rebase the snapshot preview onto the current contract — convex/githubSkillSync.ts:1282
    Current main substantially changed this module: snapshot discovery now requires an action context with runAction and is not exported as this older hunk expects. The preview handler was designed with only runQuery, and GitHub already reports the branch dirty. Rebase and use a shared current-main wrapper before relying on this export.
    Confidence: 0.98

Overall correctness: patch is incorrect
Overall confidence: 0.98

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 98a6e04e39e6.

Labels

Label changes:

  • add merge-risk: 🚨 security-boundary: The preview duplicates repository enrollment authorization and GitHub request authentication behavior at an identity-sensitive boundary.
  • add merge-risk: 🚨 availability: Using repository-scoped installation credentials for an arbitrary public repository can make listing or preview fail for repositories outside that installation.

Label justifications:

  • P2: This is a bounded but currently merge-blocked publisher GitHub-sync feature with compatibility and authorization implications.
  • merge-risk: 🚨 compatibility: Replacing the current enrollment control can remove a shipped workflow for existing Official publishers.
  • merge-risk: 🚨 security-boundary: The preview duplicates repository enrollment authorization and GitHub request authentication behavior at an identity-sensitive boundary.
  • merge-risk: 🚨 availability: Using repository-scoped installation credentials for an arbitrary public repository can make listing or preview fail for repositories outside that installation.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs real behavior proof before merge: The PR body lists tests and unauthenticated action calls, but it provides no inspectable after-fix run of the new settings preview in a real browser. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Evidence

Security concerns:

  • [medium] Avoid repository-scoped installation credentials for public previews
    The review discussion identifies a settings helper that leaves GitHub App use at the shared helper’s default. Current main explicitly disables installation tokens for arbitrary public repositories because the app may not be installed there; preserve that policy after rebase.
    Confidence: 0.96

What I checked:

  • Shipped enrollment path: Current settings submit the selected repository to the enrollment action and render the existing GitHub source form; the PR replaces that action while stating activation is disabled. (src/routes/settings.tsx:795, 87ca030c30f3)
  • Current authorization and identity contract: Current main centralizes publisher-admin, immutable owner-ID, fresh organization-admin, repository-ID conflict, and cross-publisher checks in the enrollment setup query. (convex/githubSkillSync.ts:493, 87ca030c30f3)
  • Official-publisher policy: The checked-in policy says only manageable Official publishers may configure source-backed GitHub Skill Sync, while this draft describes verified publishers and removes the UI’s Official filter. (specs/official-publishers.md:25, 87ca030c30f3)
  • Current-main drift: From the PR base to current main, the sync module changed by +1,874/-113 lines and settings by +77/-1; GitHub currently reports this branch as dirty. Current snapshot discovery also requires an action context with runAction. (convex/githubSkillSync.ts:2953, 98a6e04e39e6)
  • Release provenance for the existing path: The current enrollment and shared GitHub-header behavior is in v0.23.3, so replacing it requires explicit upgrade-compatible behavior. (convex/githubSkillSync.ts:3199, 87ca030c30f3)

Likely related people:

  • Patrick Erichsen: Current main attributes the enrollment UI, authorization setup contract, and GitHub header policy to the v0.23.3 commit, with a further current-main sync change authored in 98a6e04. (role: recent GitHub Skill Sync contributor; confidence: high; commits: 87ca030c30f3, 98a6e04e39e6; files: src/routes/settings.tsx, convex/githubSkillSync.ts, specs/official-publishers.md)

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (1 earlier review cycle)
  • reviewed 2026-07-24T05:33:28.189Z sha b1a8dfc :: found issues before merge. :: [P1] Preserve source enrollment until activation exists

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale due to inactivity.
Please update it or it will be closed.

@github-actions github-actions Bot added the stale label Aug 1, 2026

@Yigtwxx Yigtwxx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read through this against main at a9d04bb0. Some of the structure here is genuinely nice: the classifier lives in a pure, dependency-free module with a discriminated-union destination and an assertNever in the UI, so the interesting logic is directly unit-testable; assertGitHubSkillSyncRuntimeEnabled() is the first statement in both public actions with a test proving no fetch and no runQuery happen when the rollout is off, which is a well-constructed fail-closed test rather than a nominal one; the useRef request-id guard plus the cancelled flag is a correct double guard with coverage; and pinning snapshot.commit into the preview is the right call for a "what will happen" screen.

Three things I'd want resolved before this leaves draft, in order:

  1. buildGitHubSettingsHeaders omits useGitHubApp, which is opt-out rather than opt-in, so every new GitHub call authenticates as the App installation — the sibling helper in githubSkillSync.ts disables that explicitly and documents why. The fetchMock-based tests can't see it.
  2. fetchGitHubSkillSourceSnapshot gained a leading ctx parameter on main, so this branch won't type-check as-is, and the fix propagates into the handler's own ctx type.
  3. The publisher-authorization block is a statement-for-statement fork of getPublicGitHubSkillSourceSetupContextInternal. Two copies of an authorization boundary is the one duplication that tends to bite silently.

Six more inline: three smaller helpers duplicated from elsewhere in convex/, a source lookup by mutable repo string in the path that already detects renames, an org/personal asymmetry in login verification (plus the specific untested branches), unused pagination whose hasMore disagrees with the filtered list, "no skills found" modelled as an error on an auto-selected repo, and the silently dropped official publisher gate in settings.tsx.

Comment on lines +288 to +293
async function buildGitHubSettingsHeaders(fetcher: typeof fetch) {
return await buildGitHubApiHeaders({
userAgent: "clawhub/github-skill-sync-settings",
fetchImpl: fetcher,
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sends a GitHub App installation token to endpoints that can't use one.

buildGitHubApiHeaders treats useGitHubApp as opt-out, not opt-in — convex/lib/githubAuth.ts:48 is if (options.useGitHubApp !== false). Since this call omits the flag, every request built from it (/user/{id}, /organizations/{id}, /users/{login}/repos, /orgs/{login}/repos, /repos/{owner}/{name}, and the archive fetch behind the snapshot) authenticates as the installation.

The sibling helper 90 lines away in convex/githubSkillSync.ts:3199-3210 disables it explicitly and states why in a comment: "Installation tokens are repository-scoped and cannot reliably read an arbitrary public repository selected by a publisher." That's the same constraint this file is under — a publisher picking any of their public repos is precisely the case where the installation may not be present.

In a deployment where an installation token is available, repository listing and preview would 403/404 or silently return a truncated repo set for anything outside the installation. The unit tests can't catch it because they inject a fetchMock, so the header contents never reach a real endpoint.

Suggested change
async function buildGitHubSettingsHeaders(fetcher: typeof fetch) {
return await buildGitHubApiHeaders({
userAgent: "clawhub/github-skill-sync-settings",
fetchImpl: fetcher,
});
}
async function buildGitHubSettingsHeaders(fetcher: typeof fetch) {
return await buildGitHubApiHeaders({
userAgent: "clawhub/github-skill-sync-settings",
fetchImpl: fetcher,
// Installation tokens are repository-scoped and cannot reliably read an
// arbitrary public repository selected by a publisher.
useGitHubApp: false,
useOAuthAppClientCredentials: true,
});
}

Reusing buildGitHubSkillSourceHeaders directly would work too, and would keep the two from drifting.

Comment on lines +108 to +152
export async function getGitHubSkillSyncPublisherContextHandler(
ctx: QueryCtx,
args: {
publisherId: Id<"publishers">;
userId: Id<"users">;
now?: number;
},
): Promise<PublisherContext> {
const { publisher } = await requirePublisherRole(ctx, {
publisherId: args.publisherId,
userId: args.userId,
allowed: ["admin"],
});
if (publisher.kind === "user") {
if (publisher.linkedUserId !== args.userId) throw new ConvexError("Forbidden");
const githubOwnerId = parseGitHubNumericId(
await getGitHubProviderAccountId(ctx, args.userId),
"Reconnect GitHub to verify your personal account",
);
return {
publisherId: publisher._id,
publisherHandle: publisher.handle,
publisherKind: "user",
githubOwnerId,
};
}

const githubOwnerId = parseGitHubNumericId(
publisher.githubOrgId,
"Connect a verified GitHub organization to this publisher",
);
if (!publisher.githubVerifiedAt) {
throw new ConvexError("Connect a verified GitHub organization to this publisher");
}
const membership = await ctx.db
.query("githubOrgMemberships")
.withIndex("by_user_and_github_org", (q) =>
q.eq("userId", args.userId).eq("githubOrgId", githubOwnerId),
)
.unique();
const now = args.now ?? Date.now();
if (
!membership ||
membership.role !== "admin" ||
now - membership.syncedAt > GITHUB_ORG_MEMBERSHIP_VERIFICATION_MAX_AGE_MS

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a statement-for-statement fork of getPublicGitHubSkillSourceSetupContextInternal (convex/githubSkillSync.ts:493-537 on main): same requirePublisherRole({ allowed: ["admin"] }), same publisher.linkedUserId !== userId"Forbidden", same getGitHubProviderAccountId match, same githubOrgId + githubVerifiedAt gate, same githubOrgMemberships.by_user_and_github_org lookup with role !== "admin" and the same GITHUB_ORG_MEMBERSHIP_VERIFICATION_MAX_AGE_MS staleness check.

That's the authorization boundary for GitHub source enrollment, which makes it the worst place in the file to keep two copies — a future hardening lands in one and quietly misses the other, and nothing fails loudly when it does. Exporting the existing internal query, or lifting the shared body into convex/lib/, would keep one definition of who may enrol a repo.

Worth noting this cuts against the file's own habit elsewhere: slug resolution correctly reuses getSkillBySlugForPublisher / getSkillSlugAliasBySlugForPublisher rather than re-deriving it, so this reads like drift rather than intent.

Comment on lines +475 to +481
const snapshot = await fetchGitHubSkillSourceSnapshot(
{
repo: metadata.repo,
defaultBranch: metadata.defaultBranch,
},
fetcher,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call no longer type-checks against current main. fetchGitHubSkillSourceSnapshot gained a leading ctx parameter (convex/githubSkillSync.ts:2953-2954):

async function fetchGitHubSkillSourceSnapshot(
  ctx: Pick<ActionCtx, "runAction">,
  { repo, defaultBranch }: { repo: string; defaultBranch: string },
  fetcher: typeof fetch = fetch,
)

It needs runAction for the raster-icon validation that landed with it, and every call site on main now passes ctx first (:2585, :2779, :2865).

That also propagates to this handler's own signature: previewGitHubSkillSyncRepositoryHandler types its ctx as Pick<ActionCtx, "runQuery">, which no longer satisfies the callee. It would need Pick<ActionCtx, "runQuery" | "runAction">, and the same widening on requireActionPublisherContext if you thread it through.

Flagging it because the branch is a couple of weeks old and this is the kind of drift that only surfaces at ci:types-build time.

Comment on lines +97 to +106
function parseGitHubNumericId(value: unknown, message: string) {
const normalized =
typeof value === "number" && Number.isSafeInteger(value) && value > 0
? String(value)
: typeof value === "string" && /^[1-9]\d*$/.test(value.trim())
? value.trim()
: "";
if (!normalized) throw new ConvexError(message);
return normalized;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three canonical helpers get re-implemented in this file. Grouping them here rather than spreading the same note around:

parseGitHubNumericId is normalizeGitHubNumericId (convex/githubSkillSync.ts:3170) with a different name — same two branches, same /^[1-9]\d*$/. And parseRepositoryMetadata (:317 and :325 in this file) then inlines that same normalization twice more for row.id and owner.id instead of calling even this local copy, so the logic exists four times in one file.

normalizeRepo (:535) is a copy of convex/githubSkillSync.ts:3323 with the regex widened to accept https?, www., a bare github.com/ prefix, and to strip [?#]. If that widening is right — and it looks right — it belongs on the canonical function. Two parsers that disagree about which strings are valid repo references, applied to the same user input on different screens, is a bug waiting for the right paste.

clampInteger (:547) is byte-for-byte convex/githubImport.ts:885, and semantically clampInt at convex/githubSkillSync.ts:202.

None of these are wrong on their own; it's the aggregate that concerns me, given the file is otherwise disciplined about reuse.

Comment on lines +242 to +245
const source = await ctx.db
.query("githubSkillSources")
.withIndex("by_repo", (q) => q.eq("repo", args.repo))
.unique();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This resolves the existing source by the mutable repo string, in the one code path that already knows the repo may have just been renamed — previewGitHubSkillSyncRepositoryHandler computes redirected: metadata.requestedRepo.toLowerCase() !== metadata.repo.toLowerCase() and passes the post-redirect full_name in as args.repo.

main added by_github_repository_id for exactly this (convex/githubSkillSync.ts:543-546), resolving by immutable id with the repo string as fallback.

Concrete failure: publisher @acme has an enrolled source stored as acme/skills; the repo is renamed to acme/agent-skills; a different publisher previews acme/agent-skills. The by_repo lookup returns null, so source is null, so resolvePreviewDestination can never return source-conflict — and the already-enrolled skills are reported as clean new-destination / replacement. That inverts the guarantee the preview screen exists to give.

metadata.repositoryId is already in hand at the call site, so mirroring the main lookup order should be a small change.

): Promise<GitHubSkillSyncRepositoryListResult> {
assertGitHubSkillSyncRuntimeEnabled();
const context = await requireActionPublisherContext(ctx, args.publisherId, authOverride);
const login = context.githubLogin ?? (await fetchVerifiedOwnerLogin(context, fetcher));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ?? here creates an asymmetry between the two publisher kinds that's worth making deliberate.

For a personal publisher, context.githubLogin is always undefined, so fetchVerifiedOwnerLogin runs and asserts id === context.githubOwnerId against /user/{id} — the login is verified against the immutable owner id on every call.

For an org, githubLogin is populated from the stored githubOrgMemberships.login, so this short-circuits and the login is never re-verified against githubOwnerId. GitHub org logins are renameable, and that row is only as fresh as GITHUB_ORG_MEMBERSHIP_VERIFICATION_MAX_AGE_MS allows. After a rename, /orgs/{stale-login}/repos either 404s or — if the old login has been claimed by someone else — lists a different org's repositories, which then get filtered by context.githubOwnerId in toRepositoryListItem and come back empty. The ownership filter saves you from showing the wrong repos, but the user just sees "No public repositories were returned for this publisher" with no way to understand why.

The org path is also the one path with no test coverage, which is how the asymmetry stayed invisible. Either drop the ?? so both kinds verify, or keep the shortcut and add a test pinning that a stale login yields a recoverable error rather than an empty list.

Other specific branches with no coverage, while this file is open: the alias-conflict arm of resolvePreviewDestination is only tested through the pure classifier, never at the DB level; parseRepositoryMetadata's row.private !== false / visibility !== "public" rejection is untested; and the stale-response race test covers the repo change but not onPublisherChange, which bumps the same request id.

Comment on lines +421 to +424
page,
perPage,
hasMore: body.length === perPage,
repositories,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

page / perPage / hasMore are computed and returned, but the settings screen calls this once with { perPage: 100 } and renders result.repositories only — so a publisher with more than 100 public repos has no way to reach repo 101. Either wire a "Load more" or drop the three fields from the return type until something consumes them; shipping an unused pagination contract in a ReturnType<typeof action> is harder to remove later than to not add.

There's also a mismatch between the two: hasMore is derived from the raw body.length, while repositories is the filtered list. A page of 100 repos that all fail the expectedOwnerId / private / visibility checks renders zero rows with hasMore: true, and the UI shows "No public repositories were returned for this publisher" — which is misleading, since there are more pages. Deriving the empty-state copy from hasMore as well as repositories.length would separate "you have none" from "none on this page".

Comment on lines +482 to +484
if (snapshot.skills.length === 0) {
throw new ConvexError("No skills were found in that public GitHub repo.");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"This repo contains no skills" is a legitimate answer to "what would happen if I synced this repo", not a failure — but modelling it as ConvexError means the settings screen turns it into toast.error.

That lands harder than it looks because the repository list auto-selects the first selectable repo, so a publisher whose most-recently-pushed repo happens to have no SKILL.md gets an error toast immediately on opening the screen, before touching anything. An empty preview with total: 0 and the existing summary counts at zero would say the same thing without reading as a fault.

Related: archived and fork are parsed into GitHubRepositoryListItem, asserted selectable: true in the backend test, and then never rendered — and fetchVerifiedRepositoryMetadata only rejects disabled. So a publisher can select an archived fork with no signal anywhere in the flow. Either surface them as badges in the list or drop them from the type, so the data and the UI agree on what matters.

Comment thread src/routes/settings.tsx
const officialGitHubSourcePublishers = manageablePublishers.filter(
(entry) => entry.publisher.official === true,
);
const githubSourcePublishers = manageablePublishers;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This drops the official publisher gate, and the PR body doesn't mention it.

Before, this was manageablePublishers.filter((entry) => entry.publisher.official === true), and the query below moved from listForManageableOfficialPublishers — which calls isOfficialPublisher (convex/githubSkillSources.ts:182) — to listForPublisher, which gates on the admin role only. Neither of the two new actions checks official either.

This may well be correct: the configure action on main no longer gates on official at all, so the UI filter could be the last holdout of a policy that has already moved. But it's a change to who is eligible to enrol a GitHub source, riding inside a PR titled "repository preview", with no test asserting the new audience. Worth a line in the description and a -settings.test.tsx case for a managed non-official publisher, so the next person reading this diff doesn't have to reconstruct whether it was intentional.

@clawsweeper clawsweeper Bot added merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. labels Aug 5, 2026

@Patrick-Erichsen Patrick-Erichsen left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This draft adds a repository-preview flow for GitHub Skill Sync. It should stay draft: current main changed the snapshot contract and public-source credential policy, while this branch would remove the only shipped enrollment control before its replacement is active.

LOC: +1775/-142 (9 files)

Findings: rebase onto the current action-context API; retain existing source enrollment until preview hands off to an implemented activation path; keep installation tokens disabled for arbitrary public repositories; decide Official-only versus all verified publishers; add real-browser proof for selection, results, conflict, and unavailable states.

Best-fix verdict: too broad and stale. Preview is useful, but it must be layered onto the current immutable snapshot/source contract rather than replacing enrollment prematurely.

Alternatives considered: keep preview read-only alongside the existing enrollment control, then remove the old path only in the later activation slice.

Code read: settings preview UI, GitHub request helpers, snapshot discovery action/query boundary, current enrollment flow, and current-main public-source header policy.

Remaining uncertainty: intended eligibility boundary and activation design.

@github-actions github-actions Bot removed the stale label Aug 5, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale due to inactivity.
Please update it or it will be closed.

@github-actions github-actions Bot added the stale label Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 availability 🚨 Merging this PR could cause crashes, hangs, restart loops, stalls, or process outages. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. merge-risk: 🚨 security-boundary 🚨 Merging this PR could weaken sandboxing, authorization, credentials, or sensitive data. P2 Normal backlog priority with limited blast radius. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. stale status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants